diff --git a/.github/workflows/acceptance.yaml b/.github/workflows/acceptance.yaml new file mode 100644 index 00000000..e4888edd --- /dev/null +++ b/.github/workflows/acceptance.yaml @@ -0,0 +1,114 @@ +name: real-data acceptance + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + acceptance: + runs-on: ubuntu-latest + services: + arangodb: + image: arangodb:3.12 + env: + ARANGO_NO_AUTH: "1" + ports: + - 8529:8529 + options: >- + --health-cmd="wget --quiet --tries=1 --spider http://127.0.0.1:8529/_api/version || exit 1" + --health-interval=10s --health-timeout=5s --health-retries=20 + clickhouse: + image: clickhouse/clickhouse-server:25.3 + env: + CLICKHOUSE_DB: loom_acceptance + CLICKHOUSE_USER: loom_ci + CLICKHOUSE_PASSWORD: loom_ci_password + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" + ports: + - 8123:8123 + - 9000:9000 + options: >- + --health-cmd="wget --quiet --tries=1 --spider --user=loom_ci --password=loom_ci_password http://localhost:8123/ping || exit 1" + --health-interval=10s --health-timeout=5s --health-retries=20 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Cache validated NCPI fixture + uses: actions/cache@v4 + with: + path: .cache/acceptance/fixture + key: acceptance-fixture-${{ hashFiles('testdata/acceptance/ncpi-tcga-brca/fixture.lock.json') }} + - name: Run real-data acceptance + env: + LOOM_ACCEPTANCE_MODE: github + LOOM_ACCEPTANCE_ARTIFACTS: .artifacts/acceptance/github-${{ github.run_id }} + LOOM_ACCEPTANCE_FIXTURE_CACHE: .cache/acceptance/fixture + LOOM_ACCEPTANCE_CLICKHOUSE_USERNAME: loom_ci + LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD: loom_ci_password + GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_EVENT_BEFORE: ${{ github.event.before }} + run: make acceptance-performance + - name: Summarize acceptance evidence + if: always() + env: + ACCEPTANCE_ROOT: .artifacts/acceptance/github-${{ github.run_id }} + shell: bash + run: | + report="$ACCEPTANCE_ROOT/current/report.json" + cleanup="$ACCEPTANCE_ROOT/current/cleanup.json" + performance="$ACCEPTANCE_ROOT/performance.json" + + if [[ ! -f "$report" ]]; then + echo "::warning::No acceptance report was produced. Inspect the acceptance step and uploaded evidence." + exit 0 + fi + + emit_summary() { + jq -r ' + def stage($name): first(.stages[] | select(.name == $name)); + (stage("generation_upload")) as $load | + (stage("execution_registry")) as $registry | + (stage("clickhouse_physical")) as $clickhouse | + (stage("graphql")) as $graphql | + (stage("idempotent_publish")) as $idempotent | + (stage("acceptance")) as $acceptance | + (.fixture.Counts | add) as $resources | + "Status: \(.status)", + "Fixture: \($resources) resources (\([.fixture.Counts | to_entries[] | "\(.key)=\(.value)"] | join(", ")))", + "ArangoDB: \($load.details.summary.vertices_inserted) vertices, \($load.details.summary.edges_inserted) edges, \($load.details.summary.generation_errors + $load.details.summary.edge_errors) load errors", + "Explorer: \($registry.details.state), idempotent republish reused execution=\($idempotent.details.execution_id)", + "ClickHouse: \($clickhouse.details.row_count) rows, \($clickhouse.details.unique_patients) unique patients", + "GraphQL: \($graphql.details.dataset.rowCount) rows, digest=\($graphql.details.row_digest)", + "Acceptance runtime: \($acceptance.seconds * 1000 | round / 1000)s" + ' "$report" + + if [[ -f "$cleanup" ]]; then + jq -r '"Cleanup: " + (if .cleanup_status == 0 and .status == "0" then "PASSED" else "FAILED" end)' "$cleanup" + fi + if [[ -f "$performance" ]]; then + jq -r '"Performance: \(.status)" + (if .reason then " (\(.reason))" else "" end)' "$performance" + fi + } + + { + echo "## Real-data acceptance" + echo + echo '```text' + emit_summary + echo '```' + } | tee -a "$GITHUB_STEP_SUMMARY" + - name: Upload acceptance evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: acceptance-${{ github.run_id }} + path: .artifacts/acceptance + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index d1a296bf..e1c7ce41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ .DS_Store .gocache/ +.cache/ +.artifacts/ bin/ data/ META/ /arango-fhir-proto /arango-fhir-server +/ui/node_modules/ +/ui/**/node_modules/ +/ui/apps/*/dist/ +/ui/packages/*/dist/ diff --git a/Dockerfile b/Dockerfile index cc16b168..d0b4fded 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,16 +34,25 @@ RUN --mount=type=cache,target=/go/pkg/mod \ -trimpath \ -ldflags="-s -w" \ -o /out/arango-fhir-proto ./cmd/arango-fhir-proto - +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + GOOS="$TARGETOS" GOARCH="$TARGETARCH" \ + go build -mod=mod \ + -trimpath \ + -ldflags="-s -w" \ + -o /out/loom-acceptance ./cmd/loom-acceptance FROM alpine:3.22 RUN apk add --no-cache ca-certificates tzdata && \ addgroup -S arango-fhir && \ - adduser -S -G arango-fhir -h /app arango-fhir + adduser -S -G arango-fhir -h /app arango-fhir && \ + mkdir -p /var/cache/loom /var/lib/loom/artifacts && \ + chown -R arango-fhir:arango-fhir /var/cache/loom /var/lib/loom WORKDIR /app COPY --from=builder /out/arango-fhir-server /app/arango-fhir-server COPY --from=builder /out/arango-fhir-proto /app/arango-fhir-proto +COPY --from=builder /out/loom-acceptance /app/loom-acceptance COPY --from=builder /src/schemas /app/schemas USER arango-fhir diff --git a/Makefile b/Makefile index 96c8e554..ee2288cd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-cli build-server clean compiler-bench dataframe-demo dataframe-profile dataframe-boundaries dataframe-test conformance generate generate-openapi generate-fhir generate-graphql graphql-check gqlgen-check openapi-check test docker-build docker-run +.PHONY: build build-cli build-server clean compiler-bench dataframe-demo dataframe-profile dataframe-boundaries dataframe-test conformance generate generate-openapi generate-fhir generate-graphql graphql-check gqlgen-check openapi-check test docker-build docker-run acceptance-real acceptance-performance demo-up demo-down demo-smoke demo-browser-smoke repository-up release-ui GO ?= go GO_VERSION ?= 1.26.5 @@ -18,8 +18,29 @@ DATAFRAME_VARIABLES ?= examples/meta_gdc_case_matrix.variables.json DATAFRAME_PROFILE_VARIABLES ?= examples/meta_gdc_case_matrix.variables.json DATAFRAME_PROFILE_LIMIT ?= 1000 +# Make treats the positional version as a second goal. Reject every ambiguous +# form while parsing so no release recipe can start with the wrong version. +ifneq ($(filter release-ui,$(MAKECMDGOALS)),) +ifneq ($(firstword $(MAKECMDGOALS)),release-ui) +$(error Usage: make release-ui X.Y.Z) +endif +ifneq ($(words $(MAKECMDGOALS)),2) +$(error Usage: make release-ui X.Y.Z) +endif +ifeq ($(origin VERSION),command line) +$(error Usage: do not set VERSION=; run make release-ui X.Y.Z) +endif +release-ui-version := $(word 2,$(MAKECMDGOALS)) +.PHONY: $(release-ui-version) +$(release-ui-version): + @: +endif + build: build-cli build-server +release-ui: + ./scripts/release-loom-ui.sh release "$(release-ui-version)" + generate: generate-fhir generate-graphql generate-openapi build-cli: @@ -78,7 +99,7 @@ dataframe-boundaries: dataframe-test: dataframe-boundaries mkdir -p $(GOCACHE_DIR) - GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./internal/dataframe/spec ./internal/dataframe/semantic ./internal/dataframe/compiler/ir ./internal/dataframe/compiler/lower ./internal/dataframe/compiler/optimize ./internal/dataframe/compiler/render/aql ./internal/dataframe/compiler ./internal/dataframe/runtime -count=1 + GOCACHE=$(GOCACHE_DIR) GOTOOLCHAIN=$(GO_TOOLCHAIN) $(GO) test $(GOFLAGS) ./internal/dataframe/spec ./internal/dataframe/semantic ./internal/dataframe/compiler/ir ./internal/dataframe/compiler/lower ./internal/dataframe/compiler/optimize ./internal/dataframe/compiler/render/aql ./internal/dataframe/compiler ./internal/dataframe/execution -count=1 conformance: mkdir -p $(GOCACHE_DIR) @@ -90,5 +111,28 @@ docker-build: docker-run: docker run --rm -p 8080:8080 $(IMAGE) +# Full real-data acceptance path. The script owns temporary Kubernetes +# port-forwards, the current-worktree Loom process, and guarded run databases. +acceptance-real: + ./scripts/acceptance-real.sh + +acceptance-performance: + ./scripts/acceptance-performance.sh + +demo-up: + ./scripts/demo-up.sh + +demo-down: + ./scripts/demo-down.sh + +demo-smoke: + ./scripts/demo-smoke.sh + +demo-browser-smoke: + ./scripts/demo-browser-smoke.sh + +repository-up: + ./scripts/loom-repo-up.sh --repository "$(if $(REPOSITORY),$(REPOSITORY),$(CURDIR))" + clean: rm -rf bin diff --git a/README.md b/README.md index e6f6927f..0a97384f 100644 --- a/README.md +++ b/README.md @@ -61,16 +61,16 @@ recipe, translation-version, and output selector. | `POST /graphql/graph` | Arango graph and recipe control plane: explicit graph traversal, typed FHIR reads, recipe validation, preview, execution, publication reads, and recipe control. | | `POST /graphql/dataframe` | Arango-backed FHIR dataframe compiler and executor (`runFhirDataframe`). | | `POST /api/v1/projects/:project/explorers/...` | REST Explorer lifecycle and V2 intent authoring used by the Builder. | -| `PUT /api/v1/projects/:project/resources/:resourceType` | Primary multipart NDJSON resource loader. | +| `POST /api/v1/datasets/:project/generations/:generation` | Multipart immutable-generation loader used by ETL. | `GET /graphql/graph` serves GraphQL Playground for the graph API. `GET /apollo` opens Apollo Sandbox pointed at `/graphql/graph`. There is intentionally no `/graphql` compatibility route. The canonical contract for every server route is -[`openapi/openapi.yaml`](openapi/openapi.yaml). It includes health, ingestion, -snapshot/release, recipe execution, GraphQL transport, Explorer lifecycle, and -Explorer authoring operations. The `:project` path parameter is the tenancy +[`openapi/openapi.yaml`](openapi/openapi.yaml). It includes health, generation +ingestion/activation, recipe execution, GraphQL transport, Explorer lifecycle, +and Explorer authoring operations. The `:project` path parameter is the tenancy identity used for authorization and becomes the published row `project_id` (for example, `HTAN_INT-BForePC`). @@ -98,7 +98,22 @@ HTTP endpoint is required. The ClickHouse account needs database creation plus ## Local development -The local Compose stack starts both ArangoDB and ClickHouse: +To run the complete no-auth Explorer demo with the locked FHIR Aggregator +sample, use: + +```bash +make demo-up +``` + +Open [the standalone Explorer](http://127.0.0.1:3080). See +[Run the standalone Explorer demo](docs/DEMO_QUICKSTART.md) for the seeded +dataset, smoke checks, logs, and reset commands. + +To launch from a separate repository's hydrated `META` data and native Loom +`CONFIG` workspace, including local CONFIG write-back on Publish, see +[Launch Loom from a data repository](docs/LOCAL_REPOSITORY_DEMO.md). + +For backend-only development, the lightweight Compose stack starts ArangoDB and ClickHouse: ```bash rtk docker compose -f experimental/docker-compose.yml up -d @@ -239,17 +254,12 @@ query ExplorerRows($input: DataframeRowsInput!) { } ``` -The reader requires the exact selector `(recipe, translationVersion, output)`. -There is no server-side default recipe or `dataType` alias. Loom derives the -authorized project set from the principal, selects each project's active -pointer-backed publication, reconciles compatible outputs, and federates them -into one logical dataframe. Every row exposes `project_id`, -derived from the publication project identity (for example, -`HTAN_INT-BForePC`), and it is filterable and sortable like other scalar -columns. Rows remain permissive JSON; columns and capabilities are discovered -at runtime so a new publication does not require a GraphQL regeneration or -server restart. See [`docs/DATAFRAME_FEDERATION.md`](docs/DATAFRAME_FEDERATION.md) -for the source-selection, authorization, schema, and operational contract. +The reader requires an explicit `projectId` and exact selector +`(recipe, translationVersion, output)`. There is no server-side default recipe, +`dataType` alias, or cross-project federation. Loom authorizes the requested +project, resolves its active pointer-backed publication, and reads that one +physical dataframe. Columns and capabilities remain runtime-discovered, so a +new publication does not require GraphQL regeneration or a server restart. ## Authorization and tenancy @@ -310,12 +320,12 @@ without rebuilding the server image. | [`internal/dataframe/compiler`](internal/dataframe/compiler) | Typed plan IR, lowering, optimization, and AQL rendering. | | [`internal/dataframe/recipe`](internal/dataframe/recipe) | Recipe contract, validation, schema resolution, execution, and control services. | | [`internal/dataframe/publication`](internal/dataframe/publication) | Backend-neutral bounded streaming publication contract. | -| [`internal/dataframe/published`](internal/dataframe/published) | Safe published-data reads and federation. | +| [`internal/dataframe/published`](internal/dataframe/published) | Safe single-project published-data reads and aggregates. | | [`internal/store/arango`](internal/store/arango) | ArangoDB boundary. | | [`internal/store/clickhouse`](internal/store/clickhouse) | Typed ClickHouse driver boundary and DDL/DML. | | [`internal/api/graphql/graph`](internal/api/graphql/graph) | GraphQL HTTP transport and error presentation. | | [`internal/api/graphql/graph/query`](internal/api/graphql/graph/query) | Arango graph and FHIR dataframe API services. | -| [`internal/api/graphql/graph/materialization`](internal/api/graphql/graph/materialization) | Published-dataframe transport mapping. | +| [`internal/api/graphql/graph/dataframe`](internal/api/graphql/graph/dataframe) | Published-dataframe reads, aggregates, export, and GraphQL model mapping. | ## Build, generation, and tests diff --git a/cmd/loom-acceptance/main.go b/cmd/loom-acceptance/main.go new file mode 100644 index 00000000..dbf63301 --- /dev/null +++ b/cmd/loom-acceptance/main.go @@ -0,0 +1,225 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/calypr/loom/internal/acceptance" + "github.com/calypr/loom/internal/explorer" +) + +type smokeExpectation struct { + Management string + Generation string + OutputID string + OutputTitle string + Columns []string +} + +type smokeOracle struct { + Columns []string `json:"columns"` +} + +func main() { + var ( + loomURL = flag.String("loom-url", "http://127.0.0.1:8080", "Loom HTTP URL") + arangoURL = flag.String("arango-url", "http://127.0.0.1:8529", "ArangoDB HTTP URL") + clickhouseURL = flag.String("clickhouse-url", "clickhouse://127.0.0.1:9000", "ClickHouse native URL") + clickhouseUser = flag.String("clickhouse-username", "default", "ClickHouse username") + clickhousePassword = flag.String("clickhouse-password", "", "ClickHouse password (prefer LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD)") + lockPath = flag.String("fixture-lock", "testdata/acceptance/ncpi-tcga-brca/fixture.lock.json", "metadata-only fixture lock") + cacheDir = flag.String("fixture-cache", ".cache/acceptance/fixture", "content-addressed fixture cache") + workspacePath = flag.String("workspace", "testdata/acceptance/ncpi-tcga-brca/workspace.json", "Explorer workspace") + oraclePath = flag.String("oracle", "testdata/acceptance/ncpi-tcga-brca/oracle.json", "expected result oracle") + artifactDir = flag.String("artifacts", ".artifacts/acceptance/local", "evidence directory") + project = flag.String("project", "NCPI_ACCEPTANCE", "run-specific project") + generation = flag.String("generation", "tcga-brca-locked", "run-specific immutable generation") + runID = flag.String("run-id", "", "run identifier; defaults to random 16-byte hex") + refresh = flag.Bool("refresh-fixture", false, "discover and write a metadata-only fixture lock, then exit") + fixtureOnly = flag.Bool("fixture-only", false, "validate/acquire the locked fixture and print counts, then exit") + fhirEndpoint = flag.String("fhir-endpoint", acceptance.DefaultFHIRBase, "public FHIR Aggregator base URL") + studyID = flag.String("study-id", acceptance.DefaultStudyID, "FHIR Aggregator ResearchStudy ID") + performanceBase = flag.String("performance-base-report", "", "base acceptance report to compare") + performanceCurrent = flag.String("performance-current-report", "", "current acceptance report to compare") + performanceRepeatBase = flag.String("performance-repeat-base-report", "", "repeat base acceptance report") + performanceRepeatCurrent = flag.String("performance-repeat-current-report", "", "repeat current acceptance report") + performanceOutput = flag.String("performance-output", "", "performance comparison JSON output") + smokeOnly = flag.Bool("smoke-only", false, "verify the running demo Explorer contract, then exit") + smokeManagement = flag.String("smoke-management", "REPOSITORY", "expected Explorer management mode") + smokeOutputID = flag.String("smoke-output-id", "tcga_brca_cohort", "expected Explorer output ID") + smokeOutputTitle = flag.String("smoke-output-title", "TCGA-BRCA patient cohort", "expected Explorer output title") + ) + flag.Parse() + if *smokeOnly { + want, err := loadSmokeExpectation(*smokeManagement, *generation, *smokeOutputID, *smokeOutputTitle, *oraclePath) + if err != nil { + fatalf("load smoke expectation: %v", err) + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + state, err := fetchExplorer(ctx, *loomURL, *project) + if err != nil { + fatalf("fetch Explorer: %v", err) + } + if err := verifyExplorerContract(state, want); err != nil { + fatalf("demo contract mismatch: %v", err) + } + fmt.Printf("demo Explorer contract verified: management=%s generation=%s output=%q columns=%d\n", want.Management, want.Generation, want.OutputTitle, len(want.Columns)) + return + } + if *performanceBase != "" || *performanceCurrent != "" { + if *performanceBase == "" || *performanceCurrent == "" || *performanceOutput == "" { + fatalf("performance comparison requires base, current, and output paths") + } + report, err := acceptance.ComparePerformanceReportFiles(*performanceBase, *performanceCurrent, *performanceRepeatBase, *performanceRepeatCurrent) + if err != nil { + fatal(err) + } + if err := acceptance.WritePerformanceComparison(*performanceOutput, report); err != nil { + fatal(err) + } + fmt.Printf("acceptance performance %s observations=%d\n", report.Status, len(report.Observations)) + switch report.Status { + case "SUSPECTED": + os.Exit(3) + case "FAILED": + os.Exit(1) + } + return + } + if *clickhousePassword == "" { + *clickhousePassword = os.Getenv("LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD") + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) + defer cancel() + if *refresh { + lock, err := acceptance.RefreshLock(ctx, *fhirEndpoint, *studyID, *lockPath, &http.Client{Timeout: 45 * time.Second}, acceptance.DefaultLimits()) + if err != nil { + fatal(err) + } + fmt.Printf("refreshed fixture lock %s: %d resources\n", lock.Digest, sumCounts(lock.Counts)) + return + } + lock, err := acceptance.LoadLock(*lockPath) + if err != nil { + fatal(err) + } + if *fixtureOnly { + fixture, err := (&acceptance.Fetcher{CacheDir: *cacheDir, Lock: lock, Limits: acceptance.DefaultLimits(), HTTPClient: &http.Client{Timeout: 45 * time.Second}}).Acquire(ctx) + if err != nil { + fatal(err) + } + fmt.Printf("fixture %s meta=%s resources=%d counts=%v\n", fixture.Digest, fixture.MetaDir, sumCounts(fixture.Counts), fixture.Counts) + return + } + runner, err := acceptance.New(acceptance.Config{ + Target: acceptance.StaticTarget{Conn: acceptance.Connections{LoomURL: *loomURL, ArangoURL: *arangoURL, ClickHouseURL: *clickhouseURL, ClickHouseUsername: *clickhouseUser, ClickHousePassword: *clickhousePassword}}, + Fixture: acceptance.Fetcher{CacheDir: *cacheDir, Lock: lock, Limits: acceptance.DefaultLimits(), HTTPClient: &http.Client{Timeout: 45 * time.Second}}, + Run: acceptance.RunSpec{RunID: acceptance.RunID(*runID), Project: *project, Generation: *generation}, ArtifactDir: *artifactDir, WorkspacePath: *workspacePath, OraclePath: *oraclePath, + HTTPClient: &http.Client{Timeout: 2 * time.Minute}, SourceCommit: sourceCommit(), + }) + if err != nil { + fatal(err) + } + report, err := runner.Run(ctx) + if err != nil { + fatalf("acceptance failed (report=%s): %v", filepath.Join(*artifactDir, "report.json"), err) + } + fmt.Printf("acceptance %s run=%s fixture=%s\n", report.Status, report.Run.Run, report.Fixture.Digest) +} + +func sourceCommit() string { + if value := os.Getenv("GITHUB_SHA"); value != "" { + return value + } + return "local" +} +func sumCounts(counts map[string]int) int { + total := 0 + for _, count := range counts { + total += count + } + return total +} + +func loadSmokeExpectation(management, generation, outputID, outputTitle, oraclePath string) (smokeExpectation, error) { + data, err := os.ReadFile(oraclePath) + if err != nil { + return smokeExpectation{}, err + } + var value smokeOracle + if err := json.Unmarshal(data, &value); err != nil { + return smokeExpectation{}, err + } + want := smokeExpectation{Management: management, Generation: generation, OutputID: outputID, OutputTitle: outputTitle, Columns: value.Columns} + if strings.TrimSpace(want.Management) == "" || strings.TrimSpace(want.Generation) == "" || strings.TrimSpace(want.OutputID) == "" || strings.TrimSpace(want.OutputTitle) == "" || len(want.Columns) == 0 { + return smokeExpectation{}, errors.New("management, generation, output ID, output title, and oracle columns are required") + } + return want, nil +} + +func fetchExplorer(ctx context.Context, base, project string) (explorer.ExplorerStateV1, error) { + path := strings.TrimRight(base, "/") + "/api/v1/projects/" + url.PathEscape(project) + "/explorers/default" + request, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil) + if err != nil { + return explorer.ExplorerStateV1{}, err + } + response, err := http.DefaultClient.Do(request) + if err != nil { + return explorer.ExplorerStateV1{}, err + } + defer response.Body.Close() + if response.StatusCode < 200 || response.StatusCode >= 300 { + return explorer.ExplorerStateV1{}, fmt.Errorf("%s returned %s", request.URL.Path, response.Status) + } + var state explorer.ExplorerStateV1 + if err := json.NewDecoder(response.Body).Decode(&state); err != nil { + return explorer.ExplorerStateV1{}, err + } + return state, nil +} + +func verifyExplorerContract(state explorer.ExplorerStateV1, want smokeExpectation) error { + if string(state.Management) != want.Management { + return fmt.Errorf("management=%q want %q", state.Management, want.Management) + } + if state.Runtime == nil { + return errors.New("runtime is null") + } + if state.Runtime.Generation != want.Generation { + return fmt.Errorf("generation=%q want %q", state.Runtime.Generation, want.Generation) + } + for _, output := range state.Runtime.Outputs { + if output.OutputID != want.OutputID { + continue + } + if output.Title != want.OutputTitle { + return fmt.Errorf("output %q title=%q want %q", want.OutputID, output.Title, want.OutputTitle) + } + columns := make([]string, len(output.Columns)) + for index, column := range output.Columns { + columns[index] = column.Column + } + if !slices.Equal(columns, want.Columns) { + return fmt.Errorf("output %q columns=%q want %q", want.OutputID, columns, want.Columns) + } + return nil + } + return fmt.Errorf("output %q is missing", want.OutputID) +} + +func fatal(err error) { fatalf("%v", err) } +func fatalf(format string, args ...any) { + _, _ = fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} diff --git a/cmd/loom-acceptance/main_test.go b/cmd/loom-acceptance/main_test.go new file mode 100644 index 00000000..dbc96182 --- /dev/null +++ b/cmd/loom-acceptance/main_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "testing" + + "github.com/calypr/loom/internal/explorer" +) + +func TestVerifyExplorerContract(t *testing.T) { + state := explorer.ExplorerStateV1{ + Management: explorer.ManagementInteractive, + Runtime: &explorer.ExplorerRuntimeV1{ + Generation: "demo-generation", + Outputs: []explorer.ExplorerRuntimeOutputV1{{ + OutputID: "patients", + Title: "Patient cohort", + Columns: []explorer.ExplorerRuntimeColumnV1{ + {Column: "patient_id"}, + {Column: "age"}, + }, + }}, + }, + } + want := smokeExpectation{Management: "INTERACTIVE", Generation: "demo-generation", OutputID: "patients", OutputTitle: "Patient cohort", Columns: []string{"patient_id", "age"}} + if err := verifyExplorerContract(state, want); err != nil { + t.Fatal(err) + } +} + +func TestVerifyExplorerContractRejectsWrongDataset(t *testing.T) { + state := explorer.ExplorerStateV1{ + Management: explorer.ManagementRepository, + Runtime: &explorer.ExplorerRuntimeV1{ + Generation: "repo-generation", + Outputs: []explorer.ExplorerRuntimeOutputV1{{ + OutputID: "patients", + Title: "Repository output", + Columns: []explorer.ExplorerRuntimeColumnV1{{Column: "identifier_system"}}, + }}, + }, + } + want := smokeExpectation{Management: "INTERACTIVE", Generation: "demo-generation", OutputID: "patients", OutputTitle: "Patient cohort", Columns: []string{"patient_id"}} + if err := verifyExplorerContract(state, want); err == nil { + t.Fatal("verifyExplorerContract succeeded for repository state") + } +} diff --git a/cmd/loom-repository/main.go b/cmd/loom-repository/main.go new file mode 100644 index 00000000..a4c81b58 --- /dev/null +++ b/cmd/loom-repository/main.go @@ -0,0 +1,426 @@ +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/calypr/loom/internal/projectid" + "github.com/calypr/loom/internal/repositoryseed" +) + +func main() { + if len(os.Args) < 2 { + fatalf("usage: loom-repository [options]") + } + switch os.Args[1] { + case "preflight": + manifest, _, _ := flagsAndPrepare("preflight", os.Args[2:]) + if err := json.NewEncoder(os.Stdout).Encode(manifest); err != nil { + fatalf("encode manifest: %v", err) + } + case "launch": + manifest, loomRoot, options := flagsAndPrepare("launch", os.Args[2:]) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + if err := launch(ctx, loomRoot, manifest, options); err != nil { + fatalf("repository launch failed: %v", err) + } + default: + fatalf("unknown command %q", os.Args[1]) + } +} + +type launchOptions struct { + composeProject string + apiHost string + apiPort int + uiHost string + uiPort int +} + +var composeProjectPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`) + +func flagsAndPrepare(name string, args []string) (repositoryseed.Manifest, string, launchOptions) { + composeProjectDefault := "" + apiHostDefault, apiPortDefault := "127.0.0.1", 8080 + uiHostDefault, uiPortDefault := "127.0.0.1", 3080 + if name == "launch" { + composeProjectDefault = os.Getenv("LOOM_REPOSITORY_COMPOSE_PROJECT") + apiHostDefault = envOrDefault("LOOM_REPOSITORY_API_HOST", apiHostDefault) + apiPortDefault = envPort("LOOM_REPOSITORY_API_PORT", apiPortDefault) + uiHostDefault = envOrDefault("LOOM_REPOSITORY_UI_HOST", uiHostDefault) + uiPortDefault = envPort("LOOM_REPOSITORY_UI_PORT", uiPortDefault) + } + fs := flag.NewFlagSet(name, flag.ExitOnError) + repository := fs.String("repository", ".", "repository containing META and CONFIG") + config := fs.String("config", "", "CONFIG file, relative to the repository") + project := fs.String("project", "", "canonical PROGRAM/PROJECT override") + loomRoot := fs.String("loom-root", ".", "Loom source checkout") + composeProject := fs.String("compose-project", composeProjectDefault, "Docker Compose project name") + apiHost := fs.String("api-host", apiHostDefault, "host address for the Loom API") + apiPort := fs.Int("api-port", apiPortDefault, "host port for the Loom API") + uiHost := fs.String("ui-host", uiHostDefault, "host address for the Loom UI") + uiPort := fs.Int("ui-port", uiPortDefault, "host port for the Loom UI") + _ = fs.Parse(args) + manifest, err := repositoryseed.Prepare(repositoryseed.Options{Repository: *repository, Config: *config, Project: *project}) + if err != nil { + fatalf("repository preflight failed: %v", err) + } + root, err := filepath.Abs(*loomRoot) + if err != nil { + fatalf("resolve Loom source: %v", err) + } + if name != "launch" { + return manifest, root, launchOptions{} + } + if *composeProject == "" { + *composeProject = repositoryComposeProject(manifest.Repository) + } + options, err := newLaunchOptions(*composeProject, *apiHost, *apiPort, *uiHost, *uiPort) + if err != nil { + fatalf("invalid launch configuration: %v", err) + } + return manifest, root, options +} + +func newLaunchOptions(composeProject, apiHost string, apiPort int, uiHost string, uiPort int) (launchOptions, error) { + if !composeProjectPattern.MatchString(composeProject) { + return launchOptions{}, fmt.Errorf("Compose project %q must match %s", composeProject, composeProjectPattern) + } + if strings.TrimSpace(apiHost) == "" || strings.TrimSpace(uiHost) == "" { + return launchOptions{}, errors.New("API and UI hosts must not be empty") + } + if apiPort < 1 || apiPort > 65535 || uiPort < 1 || uiPort > 65535 { + return launchOptions{}, errors.New("API and UI ports must be between 1 and 65535") + } + return launchOptions{composeProject: composeProject, apiHost: apiHost, apiPort: apiPort, uiHost: uiHost, uiPort: uiPort}, nil +} + +func (options launchOptions) composeArgs(loomRoot string) []string { + return []string{"compose", "--project-name", options.composeProject, "-f", filepath.Join(loomRoot, "compose.yaml"), "-f", filepath.Join(loomRoot, "compose.repository.yaml")} +} + +func (options launchOptions) composeEnvironment() []string { + return []string{ + "LOOM_COMPOSE_PROJECT_NAME=" + options.composeProject, + "LOOM_API_HOST=" + options.apiHost, + "LOOM_API_PORT=" + strconv.Itoa(options.apiPort), + "LOOM_UI_HOST=" + options.uiHost, + "LOOM_UI_PORT=" + strconv.Itoa(options.uiPort), + } +} + +func (options launchOptions) apiURL() string { + return "http://" + net.JoinHostPort(connectHost(options.apiHost), strconv.Itoa(options.apiPort)) +} + +func (options launchOptions) uiURL() string { + return "http://" + net.JoinHostPort(connectHost(options.uiHost), strconv.Itoa(options.uiPort)) +} + +func connectHost(host string) string { + switch host { + case "0.0.0.0": + return "127.0.0.1" + case "::": + return "::1" + default: + return host + } +} + +func repositoryComposeProject(repository string) string { + sum := sha256.Sum256([]byte(repository)) + return fmt.Sprintf("loom-repository-%x", sum[:6]) +} + +func envOrDefault(name, fallback string) string { + if value := os.Getenv(name); value != "" { + return value + } + return fallback +} + +func envPort(name string, fallback int) int { + value := os.Getenv(name) + if value == "" { + return fallback + } + port, err := strconv.Atoi(value) + if err != nil { + fatalf("invalid %s: %v", name, err) + } + return port +} + +func launch(ctx context.Context, loomRoot string, manifest repositoryseed.Manifest, options launchOptions) error { + compose := options.composeArgs(loomRoot) + environment := append(os.Environ(), + "LOOM_REPOSITORY_CONFIG_DIR="+manifest.ConfigDir, + "LOOM_REPOSITORY_CONFIG_NAME="+manifest.ConfigName, + "LOOM_REPOSITORY_PROJECT="+manifest.Project, + ) + environment = append(environment, options.composeEnvironment()...) + runDocker := func(arguments ...string) error { + command := exec.CommandContext(ctx, "docker", append(compose, arguments...)...) + command.Dir, command.Env, command.Stdout, command.Stderr = loomRoot, environment, os.Stdout, os.Stderr + return command.Run() + } + fmt.Printf("[1/6] repository validated project=%s generation=%s files=%d\n", manifest.Project, manifest.Generation, len(manifest.Files)) + if err := stopStaleUI(runDocker); err != nil { + return fmt.Errorf("stop stale UI: %w", err) + } + if err := runDocker("up", "--build", "-d", "arangodb", "clickhouse", "loom-api"); err != nil { + return fmt.Errorf("start infrastructure: %w", err) + } + fmt.Println("[2/6] waiting for Loom API") + client := &http.Client{Timeout: 10 * time.Minute} + apiURL := options.apiURL() + uiURL := options.uiURL() + if err := waitHTTP(ctx, client, apiURL+"/readyz", 3*time.Minute); err != nil { + return err + } + fmt.Println("[3/6] checking repository META generation") + load, err := loadRepositoryMeta(ctx, client, apiURL, manifest) + if err != nil { + return err + } + fmt.Printf(" generation response reused=%v\n", load["reused"]) + fmt.Println("[4/6] compiling and publishing CONFIG into ClickHouse") + publication, err := publish(ctx, client, apiURL, manifest) + if err != nil { + return err + } + if activated, _ := publication["activated"].(bool); !activated { + return fmt.Errorf("publication did not activate the repository generation") + } + executionID, _ := publication["executionId"].(string) + if executionID == "" { + return errors.New("publication response omitted executionId") + } + fmt.Println("[5/6] verifying publication and Explorer state") + if _, err := getJSON(ctx, client, apiURL+"/api/v1/dataframe/recipe-executions/"+url.PathEscape(executionID)); err != nil { + return fmt.Errorf("verify ClickHouse execution: %w", err) + } + viewerPath := viewerURL(apiURL, manifest) + if _, err := getJSON(ctx, client, viewerPath); err != nil { + return fmt.Errorf("verify Explorer: %w", err) + } + fmt.Println("[6/6] starting local UI") + if err := runDocker("up", "--build", "--no-deps", "-d", "loom-ui"); err != nil { + return fmt.Errorf("start UI: %w", err) + } + if err := waitHTTP(ctx, client, uiURL+"/", 2*time.Minute); err != nil { + return err + } + query := url.Values{"project": {manifest.Project}, "explorer": {"default"}} + baseUI, err := url.Parse(uiURL) + if err != nil { + return err + } + builder := *baseUI + builder.Path, builder.RawQuery = "/", query.Encode()+"&mode=builder" + viewer := *baseUI + viewer.Path, viewer.RawQuery = "/", query.Encode()+"&mode=viewer" + fmt.Printf("Loom repository demo is ready\n project: %s\n generation: %s\n Builder: %s\n Viewer: %s\n Publish writes atomically to: %s\n", manifest.Project, manifest.Generation, builder.String(), viewer.String(), manifest.ConfigPath) + return nil +} + +func stopStaleUI(runDocker func(...string) error) error { + return runDocker("stop", "loom-ui") +} + +func loadRepositoryMeta(ctx context.Context, client *http.Client, base string, manifest repositoryseed.Manifest) (map[string]any, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, generationStatusURL(base, manifest), nil) + if err != nil { + return nil, err + } + response, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("check generation status: %w", err) + } + defer response.Body.Close() + body, err := io.ReadAll(io.LimitReader(response.Body, 8<<20)) + if err != nil { + return nil, err + } + switch response.StatusCode { + case http.StatusNotFound: + fmt.Println(" generation is absent; loading repository META") + return upload(ctx, client, base, manifest) + case http.StatusOK: + var value map[string]any + if err := json.Unmarshal(body, &value); err != nil { + return nil, fmt.Errorf("decode generation status: %w", err) + } + reusable, ok := value["reusable"].(bool) + if !ok { + return nil, errors.New("generation status response omitted reusable") + } + if reusable { + fmt.Println(" generation is already reusable; skipping META upload") + value["reused"] = true + return value, nil + } + state, _ := value["state"].(string) + if state == "" { + state = "unknown" + } + return nil, fmt.Errorf("generation %s/%s already exists in state %s and is not reusable", manifest.Project, manifest.Generation, state) + default: + return nil, fmt.Errorf("%s returned %s: %s", request.URL.Path, response.Status, strings.TrimSpace(string(body))) + } +} + +func upload(ctx context.Context, client *http.Client, base string, manifest repositoryseed.Manifest) (map[string]any, error) { + reader, writer := io.Pipe() + multipartWriter := multipart.NewWriter(writer) + done := make(chan error, 1) + go func() { + var result error + for _, input := range manifest.Files { + path := filepath.Join(manifest.Repository, filepath.FromSlash(input.Path)) + file, err := os.Open(path) + if err != nil { + result = err + break + } + part, err := multipartWriter.CreateFormFile("file", filepath.Base(path)) + if err == nil { + _, err = io.Copy(part, file) + } + closeErr := file.Close() + if err == nil { + err = closeErr + } + if err != nil { + result = err + break + } + } + if result == nil { + _ = multipartWriter.WriteField("project", manifest.Project) + _ = multipartWriter.WriteField("generation", manifest.Generation) + _ = multipartWriter.WriteField("defer_activation", "false") + result = multipartWriter.Close() + } + _ = writer.CloseWithError(result) + done <- result + }() + request, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL(base, manifest), reader) + if err != nil { + return nil, err + } + request.Header.Set("Content-Type", multipartWriter.FormDataContentType()) + value, err := doJSON(client, request) + writeErr := <-done + if err != nil { + return nil, fmt.Errorf("load generation: %w", err) + } + if writeErr != nil { + return nil, fmt.Errorf("stream META: %w", writeErr) + } + return value, nil +} + +func publish(ctx context.Context, client *http.Client, base string, manifest repositoryseed.Manifest) (map[string]any, error) { + path := publishURL(base, manifest) + request, err := http.NewRequestWithContext(ctx, http.MethodPost, path, bytes.NewReader(manifest.Workspace)) + if err != nil { + return nil, err + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Loom-Source-Commit", manifest.SourceCommit) + value, err := doJSON(client, request) + if err != nil { + return nil, fmt.Errorf("publish CONFIG: %w", err) + } + return value, nil +} + +// Generation storage uses legacy project IDs while Explorer APIs expose canonical IDs. +func uploadURL(base string, manifest repositoryseed.Manifest) string { + return strings.TrimRight(base, "/") + "/api/v1/datasets/" + url.PathEscape(projectid.Legacy(manifest.Project)) + "/generations/" + url.PathEscape(manifest.Generation) +} + +func generationStatusURL(base string, manifest repositoryseed.Manifest) string { + return uploadURL(base, manifest) +} + +func publishURL(base string, manifest repositoryseed.Manifest) string { + return strings.TrimRight(base, "/") + "/api/v1/projects/" + url.PathEscape(projectid.Canonical(manifest.Project)) + "/generations/" + url.PathEscape(manifest.Generation) + "/explorer-config" +} + +func viewerURL(base string, manifest repositoryseed.Manifest) string { + return strings.TrimRight(base, "/") + "/api/v1/projects/" + url.PathEscape(projectid.Canonical(manifest.Project)) + "/explorers/default" +} + +func getJSON(ctx context.Context, client *http.Client, path string) (map[string]any, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, err + } + return doJSON(client, request) +} + +func doJSON(client *http.Client, request *http.Request) (map[string]any, error) { + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + body, err := io.ReadAll(io.LimitReader(response.Body, 8<<20)) + if err != nil { + return nil, err + } + if response.StatusCode < 200 || response.StatusCode >= 300 { + return nil, fmt.Errorf("%s returned %s: %s", request.URL.Path, response.Status, strings.TrimSpace(string(body))) + } + var value map[string]any + if err := json.Unmarshal(body, &value); err != nil { + return nil, fmt.Errorf("decode %s: %w", request.URL.Path, err) + } + return value, nil +} + +func waitHTTP(ctx context.Context, client *http.Client, address string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + request, _ := http.NewRequestWithContext(ctx, http.MethodGet, address, nil) + response, err := client.Do(request) + if err == nil { + _ = response.Body.Close() + if response.StatusCode >= 200 && response.StatusCode < 300 { + return nil + } + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Second): + } + } + return fmt.Errorf("timed out waiting for %s", address) +} + +func fatalf(format string, arguments ...any) { + _, _ = fmt.Fprintf(os.Stderr, format+"\n", arguments...) + os.Exit(1) +} diff --git a/cmd/loom-repository/main_test.go b/cmd/loom-repository/main_test.go new file mode 100644 index 00000000..4ec15d66 --- /dev/null +++ b/cmd/loom-repository/main_test.go @@ -0,0 +1,231 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/calypr/loom/internal/projectid" + "github.com/calypr/loom/internal/repositoryseed" +) + +func TestLaunchOptionsDeriveComposeAndPublicEndpoints(t *testing.T) { + options, err := newLaunchOptions("custom-stack", "0.0.0.0", 18080, "127.0.0.2", 13080) + if err != nil { + t.Fatal(err) + } + if got := strings.Join(options.composeArgs("/loom"), " "); got != "compose --project-name custom-stack -f /loom/compose.yaml -f /loom/compose.repository.yaml" { + t.Fatalf("compose arguments = %q", got) + } + if got := options.apiURL(); got != "http://127.0.0.1:18080" { + t.Fatalf("API URL = %q", got) + } + if got := options.uiURL(); got != "http://127.0.0.2:13080" { + t.Fatalf("UI URL = %q", got) + } + if got := strings.Join(options.composeEnvironment(), "\n"); !strings.Contains(got, "LOOM_API_HOST=0.0.0.0") || !strings.Contains(got, "LOOM_API_PORT=18080") || !strings.Contains(got, "LOOM_UI_HOST=127.0.0.2") || !strings.Contains(got, "LOOM_UI_PORT=13080") { + t.Fatalf("Compose environment = %q", got) + } +} + +func TestLaunchOptionsRejectInvalidConfiguration(t *testing.T) { + tests := []struct { + name string + project string + apiPort int + uiPort int + }{ + {name: "empty project", apiPort: 8080, uiPort: 3080}, + {name: "invalid project", project: "not valid!", apiPort: 8080, uiPort: 3080}, + {name: "invalid API port", project: "repo-stack", uiPort: 3080}, + {name: "invalid UI port", project: "repo-stack", apiPort: 8080, uiPort: 70000}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := newLaunchOptions(test.project, "127.0.0.1", test.apiPort, "127.0.0.1", test.uiPort); err == nil { + t.Fatal("newLaunchOptions succeeded, want validation error") + } + }) + } +} + +func TestLaunchOptionsProbeMatchingLoopbackFamiliesForWildcardBindings(t *testing.T) { + options, err := newLaunchOptions("repo-stack", "::", 18080, "::", 13080) + if err != nil { + t.Fatal(err) + } + if got := options.apiURL(); got != "http://[::1]:18080" { + t.Fatalf("API URL = %q", got) + } + if got := options.uiURL(); got != "http://[::1]:13080" { + t.Fatalf("UI URL = %q", got) + } +} + +func TestRepositoryURLsUseStorageAndPublicProjectIdentities(t *testing.T) { + manifest := repositoryseed.Manifest{Project: "HTAN_INT/BForePC", Generation: "repo-0123456789abcdef"} + + load := uploadURL("http://127.0.0.1:8080", manifest) + if !strings.Contains(load, "/api/v1/datasets/HTAN_INT-BForePC/generations/") { + t.Fatalf("generation upload URL = %q, want legacy storage project %q", load, projectid.Legacy(manifest.Project)) + } + + publication := publishURL("http://127.0.0.1:8080", manifest) + if !strings.Contains(publication, "/api/v1/projects/HTAN_INT%2FBForePC/generations/") { + t.Fatalf("repository publication URL = %q, want canonical public project %q", publication, manifest.Project) + } + + viewer := viewerURL("http://127.0.0.1:8080", manifest) + if !strings.Contains(viewer, "/api/v1/projects/HTAN_INT%2FBForePC/explorers/default") { + t.Fatalf("Viewer URL = %q, want canonical public project %q", viewer, manifest.Project) + } +} + +func TestRepositoryURLsDoNotChangeOpaqueProjectIDs(t *testing.T) { + manifest := repositoryseed.Manifest{Project: "NCPI_ACCEPTANCE", Generation: "repo-0123456789abcdef"} + if got := uploadURL("http://127.0.0.1:8080", manifest); !strings.Contains(got, "/datasets/NCPI_ACCEPTANCE/") { + t.Fatalf("opaque upload URL = %q", got) + } +} + +func TestPublicProjectPathEscapesCanonicalSlash(t *testing.T) { + manifest := repositoryseed.Manifest{Project: "HTAN_INT/BForePC", Generation: "repo-0123456789abcdef"} + for _, path := range []string{publishURL("http://127.0.0.1:8080", manifest), viewerURL("http://127.0.0.1:8080", manifest)} { + if strings.Contains(path, "/HTAN_INT/BForePC/") { + t.Fatalf("unescaped canonical slash in URL %q", path) + } + if !strings.Contains(path, url.PathEscape(manifest.Project)) { + t.Fatalf("URL %q does not contain escaped canonical project", path) + } + } +} + +func TestLaunchStopsUIBeforeInfrastructure(t *testing.T) { + var calls [][]string + if err := stopStaleUI(func(args ...string) error { + calls = append(calls, append([]string(nil), args...)) + return nil + }); err != nil { + t.Fatal(err) + } + if len(calls) != 1 || strings.Join(calls[0], " ") != "stop loom-ui" { + t.Fatalf("stale UI command = %#v, want [stop loom-ui]", calls) + } +} + +func TestLoadRepositoryMetaSkipsUploadForReusableGeneration(t *testing.T) { + manifest := repositoryseed.Manifest{Project: "NCPI_ACCEPTANCE", Generation: "repo-0123456789abcdef"} + var methods []string + client := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { + methods = append(methods, request.Method) + if request.Method != http.MethodGet { + return jsonResponse(t, http.StatusMethodNotAllowed, map[string]any{"error": "unexpected method"}), nil + } + return jsonResponse(t, http.StatusOK, map[string]any{ + "project": manifest.Project, "generation": manifest.Generation, + "state": "STAGED", "reusable": true, + }), nil + })} + + result, err := loadRepositoryMeta(context.Background(), client, "http://loom.test", manifest) + if err != nil { + t.Fatal(err) + } + if reused, _ := result["reused"].(bool); !reused { + t.Fatalf("result = %#v, want reused", result) + } + if len(methods) != 1 { + t.Fatalf("requests = %v, want one status request", methods) + } +} + +func TestLoadRepositoryMetaRejectsExistingNonReusableGeneration(t *testing.T) { + manifest := repositoryseed.Manifest{Project: "NCPI_ACCEPTANCE", Generation: "repo-0123456789abcdef"} + var methods []string + client := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { + methods = append(methods, request.Method) + return jsonResponse(t, http.StatusOK, map[string]any{ + "project": manifest.Project, "generation": manifest.Generation, + "state": "FAILED", "reusable": false, + }), nil + })} + + _, err := loadRepositoryMeta(context.Background(), client, "http://loom.test", manifest) + if err == nil || !strings.Contains(err.Error(), "FAILED") { + t.Fatalf("loadRepositoryMeta error = %v, want FAILED state", err) + } + if len(methods) != 1 || methods[0] != http.MethodGet { + t.Fatalf("requests = %v, want only GET", methods) + } +} + +func TestLoadRepositoryMetaUploadsGenerationAfterNotFound(t *testing.T) { + manifest := repositoryseed.Manifest{Project: "NCPI_ACCEPTANCE", Generation: "repo-0123456789abcdef"} + var methods []string + client := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { + methods = append(methods, request.Method) + if request.Method == http.MethodGet { + return jsonResponse(t, http.StatusNotFound, map[string]any{"code": "DATASET_NOT_FOUND"}), nil + } + if request.Method != http.MethodPost { + return jsonResponse(t, http.StatusMethodNotAllowed, map[string]any{"error": "unexpected method"}), nil + } + if _, err := io.Copy(io.Discard, request.Body); err != nil { + t.Fatal(err) + } + return jsonResponse(t, http.StatusOK, map[string]any{"reused": false}), nil + })} + + result, err := loadRepositoryMeta(context.Background(), client, "http://loom.test", manifest) + if err != nil { + t.Fatal(err) + } + if reused, _ := result["reused"].(bool); reused { + t.Fatalf("result = %#v, want new load", result) + } + if strings.Join(methods, ",") != "GET,POST" { + t.Fatalf("requests = %v, want GET then POST", methods) + } +} + +func TestLoadRepositoryMetaDoesNotUploadAfterStatusFailure(t *testing.T) { + manifest := repositoryseed.Manifest{Project: "NCPI_ACCEPTANCE", Generation: "repo-0123456789abcdef"} + var methods []string + client := &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { + methods = append(methods, request.Method) + return jsonResponse(t, http.StatusServiceUnavailable, map[string]any{"code": "BACKEND_UNAVAILABLE"}), nil + })} + + _, err := loadRepositoryMeta(context.Background(), client, "http://loom.test", manifest) + if err == nil || !strings.Contains(err.Error(), "Service Unavailable") { + t.Fatalf("loadRepositoryMeta error = %v, want service unavailable", err) + } + if len(methods) != 1 || methods[0] != http.MethodGet { + t.Fatalf("requests = %v, want only GET", methods) + } +} + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request) +} + +func jsonResponse(t *testing.T, status int, value any) *http.Response { + t.Helper() + body, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + return &http.Response{ + StatusCode: status, + Status: http.StatusText(status), + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + } +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 00000000..35c93b73 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,129 @@ +name: ${LOOM_COMPOSE_PROJECT_NAME:-loom-demo} + +services: + arangodb: + image: arangodb:3.12 + environment: + ARANGO_NO_AUTH: "1" + healthcheck: + test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://127.0.0.1:8529/_api/version"] + interval: 5s + timeout: 5s + retries: 30 + volumes: + - arangodb_data:/var/lib/arangodb3 + + clickhouse: + image: clickhouse/clickhouse-server:25.3 + environment: + CLICKHOUSE_DB: loom_acceptance_d000000000000001 + CLICKHOUSE_USER: loom_demo + CLICKHOUSE_PASSWORD: loom_demo_password + CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" + healthcheck: + test: ["CMD-SHELL", "wget --quiet --tries=1 --spider --user=loom_demo --password=loom_demo_password http://127.0.0.1:8123/ping"] + interval: 5s + timeout: 5s + retries: 30 + volumes: + - clickhouse_data:/var/lib/clickhouse + + loom-api: + image: ${LOOM_API_IMAGE:-loom-demo-api:local} + build: + context: . + command: + - --listen + - :8080 + - --no-auth + - --url + - http://arangodb:8529 + - --database + - loom_acceptance_d000000000000001 + - --schema + - /app/schemas/graph-fhir.json + - --clickhouse-url + - clickhouse://clickhouse:9000 + - --clickhouse-database + - loom_acceptance_d000000000000001 + - --clickhouse-username + - loom_demo + - --clickhouse-password + - loom_demo_password + - --dataframer-recipe + - /app/testdata/recipe.json + - --recipe-query-page-rows + - ${LOOM_RECIPE_QUERY_PAGE_ROWS:-25} + depends_on: + arangodb: + condition: service_healthy + clickhouse: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:8080/readyz >/dev/null"] + interval: 5s + timeout: 5s + retries: 30 + ports: + - target: 8080 + published: ${LOOM_API_PORT:-8080} + host_ip: ${LOOM_API_HOST:-127.0.0.1} + volumes: + - ${LOOM_DEMO_FIXTURE_DIR:-./testdata/acceptance/ncpi-tcga-brca}:/app/testdata:ro + + demo-seed: + image: ${LOOM_API_IMAGE:-loom-demo-api:local} + entrypoint: ["/app/loom-acceptance"] + command: + - --run-id + - d000000000000001 + - --loom-url + - http://loom-api:8080 + - --arango-url + - http://arangodb:8529 + - --clickhouse-url + - clickhouse://clickhouse:9000 + - --clickhouse-username + - loom_demo + - --clickhouse-password + - loom_demo_password + - --fixture-lock + - /app/testdata/fixture.lock.json + - --fixture-cache + - /var/cache/loom + - --workspace + - /app/testdata/workspace.json + - --oracle + - /app/testdata/oracle.json + - --artifacts + - /var/lib/loom/artifacts + - --project + - ${LOOM_DEMO_PROJECT:-NCPI_ACCEPTANCE} + - --generation + - ${LOOM_DEMO_GENERATION:-tcga-brca-locked} + depends_on: + loom-api: + condition: service_healthy + volumes: + - ${LOOM_DEMO_FIXTURE_DIR:-./testdata/acceptance/ncpi-tcga-brca}:/app/testdata:ro + - fixture_cache:/var/cache/loom + - demo_artifacts:/var/lib/loom/artifacts + restart: "no" + + loom-ui: + build: + context: ./ui + dockerfile: apps/demo/Dockerfile + depends_on: + loom-api: + condition: service_healthy + ports: + - target: 8080 + published: ${LOOM_UI_PORT:-3080} + host_ip: ${LOOM_UI_HOST:-127.0.0.1} + +volumes: + arangodb_data: + clickhouse_data: + fixture_cache: + demo_artifacts: diff --git a/conformance/compiler/policy_ablation_test.go b/conformance/compiler/policy_ablation_test.go index c16b083d..e01e36f8 100644 --- a/conformance/compiler/policy_ablation_test.go +++ b/conformance/compiler/policy_ablation_test.go @@ -13,11 +13,11 @@ import ( "time" "github.com/calypr/loom/internal/dataframe/compiler/ir" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/store/arango" ) -func explainCompiledQuery(ctx context.Context, opts arango.ConnectionOptions, compiled runtime.CompiledQuery) (arango.ExplainResult, error) { +func explainCompiledQuery(ctx context.Context, opts arango.ConnectionOptions, compiled dataframeexecution.CompiledQuery) (arango.ExplainResult, error) { client, err := arango.Open(ctx, opts.URL, opts.Database) if err != nil { return arango.ExplainResult{}, err diff --git a/docs/CODE_GENERATION.md b/docs/CODE_GENERATION.md index 79290214..d167482d 100644 --- a/docs/CODE_GENERATION.md +++ b/docs/CODE_GENERATION.md @@ -34,7 +34,7 @@ The generated executors expose gqlgen's resolver interfaces. gqlgen requires resolver receivers and preserved resolver implementations to share one Go package, so its resolver directories also contain thin GraphQL adapter glue. Application behavior remains in `internal/api/graphql/graph/query`, -`internal/api/graphql/graph/materialization`, and the internal dataframe services. +`internal/api/graphql/graph/dataframe`, and the internal dataframe services. Database access and compiler logic do not belong in `generated/`. Resolver implementations are the narrow exception to the no-edit rule: gqlgen @@ -49,7 +49,7 @@ The handwritten GraphQL inputs and runtime wiring are deliberately separate: | `internal/api/graphql/graph/schema/` | Handwritten primary GraphQL schema. | | `internal/api/graphql/graph/` | HTTP handler and shared GraphQL error presentation. | | `internal/api/graphql/graph/query/` | Arango graph and FHIR dataframe request services. | -| `internal/api/graphql/graph/materialization/` | Published-dataframe transport mapping. | +| `internal/api/graphql/graph/dataframe/` | Published-dataframe reads, aggregates, export, and GraphQL model mapping. | ## Sources of truth and outputs diff --git a/docs/DATAFRAMER_RECIPE_REFERENCE.md b/docs/DATAFRAMER_RECIPE_REFERENCE.md index 4b80c68b..342cd942 100644 --- a/docs/DATAFRAMER_RECIPE_REFERENCE.md +++ b/docs/DATAFRAMER_RECIPE_REFERENCE.md @@ -157,6 +157,7 @@ An output controls rows rooted at one FHIR resource type. | `name` | Yes | Public dataframe/output name, unique in the recipe. | | `rootResourceType` | Yes | FHIR resource type selected as `root` (for example `DocumentReference`). | | `rowGrain` | Yes | Human-readable grain label (`resource`, `file`, `study_enrollment`, etc.). It documents the contract; it is not a FHIR selector. | +| `traversalColumnNaming` | No | `PATH` (default) prefixes nested fields with every traversal alias; `ALIAS` uses only the globally unique owning alias. | | `fields` | No | Explicit named scalar or object projections. | | `filters` | No | Predicates applied at this node. | | `pivots` | No | Bounded code/key-to-column projections. | diff --git a/docs/DATAFRAME_FEDERATION.md b/docs/DATAFRAME_FEDERATION.md deleted file mode 100644 index 7349967e..00000000 --- a/docs/DATAFRAME_FEDERATION.md +++ /dev/null @@ -1,254 +0,0 @@ -# Published Dataframe Federation - -This guide describes how Loom turns independently published project dataframes -into one authorized logical dataframe. It covers the publication catalog, -exact selectors, active generations, authorization, schema reconciliation, and -the GraphQL reader surface. - -The federation layer is a read boundary. It does not compile recipes or expose -arbitrary ClickHouse tables. Recipe compilation and publication create durable -project-local outputs; federation selects compatible outputs and presents them -as one logical dataset. - -## The execution path - -```text -GraphQL request - -> exact dataframe selector - -> authorized project candidates - -> project_id filter pushdown - -> active generation per project - -> READY/PUBLISHED pointer-backed execution - -> source-level authorization scope - -> schema reconciliation - -> authorized ClickHouse UNION ALL - -> rows, aggregates, or streaming export -``` - -The browser never supplies a project, generation, materialization table, or -SQL table name. Loom derives those values from the authenticated principal and -the publication catalog. - -## Exact dataframe identity - -Every published output is identified by three required values: - -```text -(recipe, translationVersion, output) -``` - -For example: - -```text -(documents, v2, DocumentReference) -``` - -The selector is shared by recipe publication, project releases, federation, -GraphQL, and ETL. A name-only lookup is not sufficient because two recipes can -produce the same output name and one recipe can have multiple versions. - -Use the exact selector in new GraphQL requests: - -```graphql -query Rows($input: DataframeRowsInput!) { - dataframeRows(input: $input) { - materialization { - selector { recipe translationVersion output } - availability - completeness - includedProjectCount - expectedProjectCount - projectStatuses { - project - state - generation - executionId - errorCode - retryable - } - } - columns - rows - totalCount - pageInfo { hasNextPage endCursor } - } -} -``` - -```json -{ - "input": { - "selector": { - "recipe": "documents", - "translationVersion": "v2", - "output": "DocumentReference" - }, - "first": 100 - } -} -``` - -The selector is mandatory. Loom does not resolve a server-side default recipe, -accept a `dataType` alias, or infer a translation version. This keeps a read -bound to the exact recipe contract that produced its publication. - -## How sources are selected - -For each request, the materialization service performs these steps: - -1. Resolve the principal's candidate projects. If project claims are present, - those claims are authoritative; otherwise Loom discovers projects with - current published outputs. -2. Apply an equality or `IN` filter on `project_id` before source discovery. - This avoids reading or reconciling projects the caller did not request. -3. Resolve each project's active dataset generation when an active-generation - resolver is configured. -4. List successful publication executions and resolve the pointer for each - `(project, generation, recipe, translationVersion)` identity. -5. Keep only the execution whose pointer still names that execution and whose - exact selector matches the request. -6. Resolve the caller's read scope for every remaining project and generation. - Sources with no authorized scope are excluded. -7. Reconcile the authorized sources into a `FederatedDataset`. - -The pointer check is important: a completed physical table is not automatically -visible. It becomes readable only through the catalog pointer selected for its -logical identity. - -## Publication and visibility - -Publication is a durable multi-output bundle. New workflows use these states: - -```text -QUEUED -> RUNNING -> VALIDATING -> PUBLISHED - \-> FAILED -``` - -Physical ClickHouse tables are written and verified before the catalog pointer -is advanced. A reader therefore sees either the prior pointer or the new -verified publication, not a partially written bundle. New workflows use -`PUBLISHED`; storage adapters may normalize historical persisted states while -those records are being retired, but no new API accepts or emits them. - -Project releases add another visibility boundary. A staged generation and its -verified exact-selector publications become active together through the active -release pointer. A newer generation or publication does not become visible -merely because it exists. - -## Authorization and tenancy - -Each published source carries project identity and may carry -`auth_resource_path` values. Federation enforces both dimensions: - -- `project_id` identifies which project contributed a row and supports source - selection and filtering. -- `auth_resource_path` is an authorization predicate derived from the server's - scope resolver or principal, never from an arbitrary client filter. -- An unrestricted scope reads the complete source. -- A path-limited scope reads only matching rows and marks the dataset's row - count as potentially incomplete. -- A source with no effective authorized paths is excluded. - -`project_id` is synthesized from catalog metadata for legacy tables that do not -physically contain the column. This prevents a legacy table from claiming a -different project identity. - -## Schema reconciliation - -Federation reconciles source schemas before issuing the union query. - -- Columns present in only some sources become nullable in the logical schema. -- Missing scalar values are returned as `NULL`. -- Missing array values are returned as an empty array of the reconciled type. -- A source with an invalid table, invalid column metadata, or duplicate column - names is excluded with `RECIPE_CONTRACT_VIOLATION`. -- Incompatible types for the same logical column fail the request with - `FEDERATION_INCOMPATIBLE` and include the conflicting column, projects, and - types in structured details. -- Internal columns such as `__loom_row_id` and `auth_resource_path` are not - exposed as ordinary client columns. - -The resulting metadata includes a revision digest derived from the selected -source identities, physical tables, and reconciled columns. It is used for -cursor and read consistency decisions. - -## Availability and project status - -The materialization metadata reports: - -| Availability | Meaning | -| --- | --- | -| `AVAILABLE` | All expected authorized projects have current usable sources. | -| `DEGRADED` | At least one expected project is missing, stale, failed, building, or excluded, but usable sources remain. | -| `UNAVAILABLE` | No usable authorized source exists. | - -Project status values are `CURRENT`, `STALE`, `BUILDING`, `FAILED`, `MISSING`, -and `EXCLUDED`. Statuses are filtered to the caller's authorized projects; -unauthorized project identities are never disclosed. - -Degraded federation is intentional. One project with a failed publication -should not hide valid data from other authorized projects, but clients should -surface the status metadata when presenting results. - -## Read and export surfaces - -The same federated source set is used by: - -- `dataframeDataset` and `dataframeDatasets` for discovery and metadata; -- `dataframeRows` for cursor-paginated rows; -- `dataframeAggregate` and `dataframeAggregations` for aggregates; -- the dataframe export route for CSV, TSV, JSON, and JSONL streaming. - -There is no materialization-ID or physical-table read path. All reads use the -exact selector and resolve the active publication through the catalog. - -## Operational setup - -For a project to contribute to a federated dataframe, all of the following -must be true: - -1. The project has an active or otherwise eligible immutable dataset generation. -2. The exact recipe/version/output has been materialized for that generation. -3. The publication completed successfully and its output was verified. -4. The catalog pointer names that execution. -5. The caller has an authorized project and resource-path scope. -6. The source schema can be reconciled with the other selected projects. - -For a new recipe version, publish and verify it first, then promote or select -that exact version. Do not rely on “latest” behavior to switch a production -reader between versions. - -Recommended configuration for release verification is a list of exact -selectors, not output names alone. See -`LOOM_REQUIRED_DATAFRAME_SELECTORS` and the release contract documentation. - -## Troubleshooting - -| Symptom | Likely cause | -| --- | --- | -| `INVALID_SELECTOR` | Selector is missing or incomplete. | -| `UNAVAILABLE` / `DATASET_NOT_FOUND` | No pointer-backed exact publication is active or authorized. | -| `DEGRADED` | One or more expected projects are missing, stale, failed, building, or excluded. | -| `FEDERATION_INCOMPATIBLE` | Same logical column has incompatible ClickHouse types across selected sources. | -| `RECIPE_CONTRACT_VIOLATION` | A publication has invalid physical table or column metadata. | -| Incomplete `totalCount` | The caller has path-limited authorization, so count completeness is not guaranteed. | - -Structured errors include stable codes and explicit retryability. Clients -should branch on the error code rather than parsing human-readable messages. - -## Code map - -| Responsibility | Implementation | -| --- | --- | -| Exact selector identity | `internal/dataset/dataframe_selector.go` | -| Publication bundle and pointer identity | `internal/dataframe/publication/bundle.go` | -| Durable ClickHouse publication | `internal/dataframe/publication/clickhouse/` | -| Source selection and schema reconciliation | `internal/dataframe/published/federation.go` | -| Single-source compatibility reads | `internal/dataframe/published/read.go` | -| GraphQL authorization and transport mapping | `internal/api/graphql/graph/materialization/` | -| Public GraphQL schema | `internal/api/graphql/graph/schema/schema.graphqls` | -| Immutable generations and release activation | `internal/dataset/` | -| Stable error taxonomy | `internal/dataframe/errors/errors.go` | - -The broader identity, state, migration, and configuration contract is in -[`loom-reliability-contract.md`](loom-reliability-contract.md). diff --git a/docs/DEMO_CONFIGURATION.md b/docs/DEMO_CONFIGURATION.md new file mode 100644 index 00000000..d6c24585 --- /dev/null +++ b/docs/DEMO_CONFIGURATION.md @@ -0,0 +1,42 @@ +# Demo configuration + +The demo scripts read these environment variables. Use the same values for +`demo-up`, `demo-smoke`, `demo-browser-smoke`, and `demo-down`. + +## Stack settings + +| Variable | Default | Purpose | +| --- | --- | --- | +| `LOOM_DEMO_COMPOSE_PROJECT` | `loom-demo` | Compose project and volume namespace | +| `LOOM_DEMO_API_HOST` | `127.0.0.1` | API bind address | +| `LOOM_DEMO_API_PORT` | `8080` | API host port | +| `LOOM_DEMO_UI_HOST` | `127.0.0.1` | UI bind address | +| `LOOM_DEMO_UI_PORT` | `3080` | UI host port | +| `LOOM_DEMO_API_URL` | derived from the API host and port | URL used by readiness and smoke checks | +| `LOOM_DEMO_UI_URL` | derived from the UI host and port | URL used by readiness and smoke checks | +| `LOOM_API_IMAGE` | `loom-demo-api:local` | API and seed image tag | +| `LOOM_RECIPE_QUERY_PAGE_ROWS` | `25` | Root documents per bounded preview/materialization query; `0` restores single-query execution | + +## Dataset settings + +| Variable | Default | Purpose | +| --- | --- | --- | +| `LOOM_DEMO_PROJECT` | `NCPI_ACCEPTANCE` | Seeded project | +| `LOOM_DEMO_GENERATION` | `tcga-brca-locked` | Seeded generation | +| `LOOM_DEMO_FIXTURE_DIR` | `./testdata/acceptance/ncpi-tcga-brca` | Directory that contains `fixture.lock.json`, `workspace.json`, `oracle.json`, and `recipe.json` | + +## Smoke expectations + +| Variable | Default | Purpose | +| --- | --- | --- | +| `LOOM_DEMO_MANAGEMENT` | `REPOSITORY` | Expected Explorer management mode | +| `LOOM_DEMO_OUTPUT_ID` | `tcga_brca_cohort` | Expected runtime output ID | +| `LOOM_DEMO_OUTPUT_TITLE` | `TCGA-BRCA patient cohort` | Expected runtime output title | +| `LOOM_DEMO_EXPECTED_COLUMN_LABEL` | `Patient ID` | Column label required by the browser smoke check | +| `LOOM_DEMO_EXPECTED_CELL` | `TCGA-` | Cell text required by the browser smoke check | +| `LOOM_DEMO_EXPECTED_RESOURCES` | `Patient Condition Specimen Observation ResearchStudy` | Space-separated resource names required by the Builder smoke check | +| `LOOM_DEMO_BROWSER_URL` | project-specific URL derived from `LOOM_DEMO_UI_URL` | Base URL used by the browser smoke check | + +`demo-smoke` reads the ordered physical column names from `oracle.json` in the +fixture directory. The check fails if the running output has a different +schema. diff --git a/docs/DEMO_QUICKSTART.md b/docs/DEMO_QUICKSTART.md new file mode 100644 index 00000000..1c8c1c5f --- /dev/null +++ b/docs/DEMO_QUICKSTART.md @@ -0,0 +1,105 @@ +# Run the standalone Explorer demo + +This tutorial starts Loom, ArangoDB, ClickHouse, and the Loom-owned Explorer UI. The seed job downloads the locked open-access FHIR Aggregator sample, loads 100 TCGA-BRCA patients and their related resources, publishes the patient cohort dataframe, and verifies the result before the UI opens. + +The demo does not use Calypr, Fence, or browser authentication. Both exposed ports bind to `127.0.0.1`. + +## Start the demo + +Install Docker Desktop or another Docker installation with Compose v2. Then run: + +```bash +make demo-up +``` + +The first run builds the Loom API and UI images and downloads 6,858 locked FHIR resources. Later runs reuse the content-addressed fixture cache and the database volumes. + +Wait for this line: + +```text +Loom demo is ready at http://127.0.0.1:3080 +``` + +Open [the standalone Explorer](http://127.0.0.1:3080). The page starts with the `NCPI_ACCEPTANCE` project, the `default` Explorer, and the published TCGA-BRCA patient cohort. + +The Builder is available from the same application. Preview and publish requests use Loom's V2 authoring API through the UI container's same-origin proxy. + +Set the same variables on each demo command to select a Compose project, +different host ports, or another fixture and dataset. For example: + +```bash +LOOM_DEMO_COMPOSE_PROJECT=analyst-demo \ +LOOM_DEMO_API_PORT=18080 \ +LOOM_DEMO_UI_PORT=13080 \ +make demo-up +``` + +See [Demo configuration](DEMO_CONFIGURATION.md) for every setting and its +default. When you bind to `0.0.0.0` or `::`, set the corresponding URL to a +reachable host such as `http://127.0.0.1:18080`. + +## Check the running demo + +Run the smoke check: + +```bash +make demo-smoke +``` + +This checks Loom readiness, the UI document, and the expected Explorer +management mode, generation, output ID, output title, and ordered schema. For a +custom fixture, set `LOOM_DEMO_OUTPUT_ID`, `LOOM_DEMO_OUTPUT_TITLE`, and, when +needed, `LOOM_DEMO_MANAGEMENT` on the smoke command. + +Verify that the seeded Explorer renders through the real Builder and Viewer in +headless Chrome: + +```bash +make demo-browser-smoke +``` + +Set `CHROME_BIN` if Chrome or Chromium is not on a standard path. + +The seed job has already verified these deeper contracts: + +- ArangoDB contains the locked Patient, Condition, Observation, ResearchStudy, and Specimen counts. +- ClickHouse contains 100 patient rows and 100 unique patients. +- The published dataframe schema matches `testdata/acceptance/ncpi-tcga-brca/oracle.json`. +- The GraphQL rows match the locked row digest. +- Publishing the same workspace twice reuses the same execution. + +To inspect a service in the default project, run: + +```bash +docker compose logs loom-api +docker compose logs loom-ui +docker compose logs demo-seed +``` + +## Stop or reset the demo + +Stop the containers and retain the downloaded fixture and database data: + +```bash +make demo-down +``` + +To remove the demo data and force a clean seed on the next run, remove the Compose volumes: + +```bash +./scripts/demo-down.sh --volumes +``` + +The volume removal deletes only volumes in the `loom-demo` Compose project. +Set `LOOM_DEMO_COMPOSE_PROJECT` to stop or reset a custom project. + +## Change the example Explorer + +The demo seed reads these versioned inputs: + +- `fixture.lock.json` fixes the upstream FHIR resource identities and digests. +- `workspace.json` defines the patient-level Explorer and Builder workspace. +- `oracle.json` fixes the expected schema, coverage, row count, and row digest. +- `recipe.json` supplies Loom's startup recipe before the Builder publishes the full workspace. + +After you change the workspace or oracle, run `./scripts/demo-down.sh --volumes` and then `make demo-up`. Keep the fixture lock unchanged unless you intend to select a new upstream cohort. diff --git a/docs/DEVELOPER_ARCHITECTURE.md b/docs/DEVELOPER_ARCHITECTURE.md index b8b8dc86..a8d8c3e4 100644 --- a/docs/DEVELOPER_ARCHITECTURE.md +++ b/docs/DEVELOPER_ARCHITECTURE.md @@ -43,7 +43,7 @@ persistence; `explorer` owns portable Explorer configs, editable drafts, immutable revisions, and publication state; `explorer/arango` owns its durable adapter; `dataframe/publication` owns dataframe publishing contracts and the runner; `dataframe/publication/{arango,clickhouse}` own storage adapters; and -`dataframe/published` owns safe published-data reading and federation. +`dataframe/published` owns safe single-project published-data reads and aggregates. `cmd/arango-fhir-proto` is the operator CLI. Its supported commands are: @@ -61,9 +61,10 @@ runner; `dataframe/publication/{arango,clickhouse}` own storage adapters; and catalog diagnostics. `cmd/arango-fhir-server` owns the HTTP process. Generated OpenAPI registration -mounts health, ingestion, snapshot/release, recipe execution, GraphQL, and +mounts health, generation ingestion/activation, recipe execution, GraphQL, and Explorer operations; handwritten strict-interface implementations remain under -`internal/server`. +`internal/server`. Snapshot and standalone release-management HTTP APIs are not +part of the server surface; Explorer publication owns its release transaction. The GraphQL dataframe mutation is the live compiler transport. Explorer V2 authoring adds an intent-to-native-recipe lowering phase, then calls the same @@ -75,9 +76,8 @@ for the complete Builder-to-AQL sequence. The HTTP API names its backend boundaries explicitly. `/graphql/graph` is the Arango graph/control-plane GraphQL endpoint and published ClickHouse dataframe reader, while `/graphql/dataframe` is the Arango-backed FHIR dataframe -compiler endpoint. Published ClickHouse dataframe discovery and reads follow the -stable-GraphQL, dynamic-data contract described in -[`DATAFRAME_FEDERATION.md`](DATAFRAME_FEDERATION.md). +compiler endpoint. Published ClickHouse dataframe discovery and reads require +an explicit authorized project and exact dataframe selector. Only registered READY publication outputs are exposed; adding a dataset or column must not require GraphQL regeneration or a Loom restart. Publication and published-data reads are ClickHouse-only; Loom has no Elasticsearch @@ -185,7 +185,7 @@ GraphQL request -> internal/api/graphql/graph HTTP handler -> generated gqlgen executor and internal resolver binding -> internal/api/graphql/graph/query.Service - -> dataframe/runtime.Service + -> dataframe/execution.Service -> dataframe/spec request contracts -> dataframe/semantic logical plan -> dataframe/compiler/ir typed physical plan @@ -195,7 +195,7 @@ GraphQL request -> Arango query execution/streaming ``` -Runtime preparation and execution live in `internal/dataframe/runtime`; +Runtime preparation and execution live in `internal/dataframe/execution`; compiler orchestration lives in `internal/dataframe/compiler`; structured transport errors live in `internal/dataframe/errors`. `internal/catalog` owns scoped observed-field and relationship facts. `internal/fhir/schema` owns structural metadata and @@ -209,7 +209,7 @@ The ClickHouse read path is parallel but separate: GraphQL request -> internal/api/graphql/graph HTTP handler -> generated graph executor and internal resolver binding - -> internal/api/graphql/graph/materialization.Service + -> internal/api/graphql/graph/dataframe.Service -> dataframe/published.Reader -> ClickHouse ``` @@ -239,11 +239,12 @@ When adding code, use this lookup table: | FHIR edge route or endpoint lowering | `internal/dataframe/compiler/lower` | | Cost-gated physical rewrite | `internal/dataframe/compiler/optimize` | | AQL text or bind emission | `internal/dataframe/compiler/render/aql` | -| Catalog, auth, generation, cursor, or profiling | `internal/dataframe/runtime` | +| Catalog, auth, generation, cursor, or profiling | `internal/dataframe/execution` | -For the complete ownership map, see the package guide in this document and the -[documentation index](README.md). Historical package-reorganization plans may -exist outside this checkout, but they are not runtime contract references. +For the complete ownership and dependency map, see the generated +[package audit lookup table](PACKAGE_AUDIT.md). Historical +package-reorganization plans may exist outside this checkout, but they are not +runtime contract references. ## Compatibility tracks diff --git a/docs/EXPLORER_AUTHORING.md b/docs/EXPLORER_AUTHORING.md index 2daf61c7..31e7e553 100644 --- a/docs/EXPLORER_AUTHORING.md +++ b/docs/EXPLORER_AUTHORING.md @@ -23,11 +23,10 @@ All routes are under: | --- | --- | --- | | Read capability | `GET /capability` | exact active compiler-proven snapshot | | Read Builder | `GET /builder` | capability plus saved/active V2 workspace when present | -| Save draft | `PUT /draft` | `{workspace, snapshotToken, expectedDraftVersion, expectedDraftDigest?}` | -| Export workspace | `GET /export` | canonical portable V2 workspace | -| Compile | `POST /compile` | `{workspace, snapshotToken}` | -| Compile alias | `POST /builder` | same operation for the current frontend | -| Suggestions | `GET /capabilities/:snapshotToken/candidates/:candidateId/suggestions` | bounded values for one exact snapshot | +| Apply commands | `POST /commands` | atomically update the server-owned draft | +| Reconcile | `POST /reconcile` | compile the exact persisted draft into a receipt | +| Compile workspace | `POST /builder` | compile a supplied workspace for the current frontend | +| Suggestions | `POST /suggestions` | search candidate definitions for a node | | Preview | `POST /preview` | `{receiptId, outputId, limit?}` | | Publish | `POST /publish` | `{receiptId}` | @@ -64,8 +63,8 @@ Artifactless receipts created by older servers return ## Draft and publication behavior -Loom stores canonical V2 workspace drafts with draft-version and optional -digest compare-and-swap. Repository/ETL publication advances that same version, +Loom stores canonical V2 workspace drafts through atomic Builder commands with +draft-version and digest compare-and-swap. Repository/ETL publication advances that same version, so an external replacement produces `DRAFT_CONFLICT` instead of silently overwriting browser work. Compile-before-preview is the safe fallback when the latest editor state has no receipt. Receipts remain immutable and diff --git a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md index 4c362eed..3fcdc9e7 100644 --- a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md +++ b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md @@ -6,7 +6,8 @@ The frontend chooses only capability IDs and presentation intent; it never constructs recipe selectors, graph storage details, physical plans, or AQL. This is the implemented Goal D boundary. The public authoring surface is V2. -V1 remains only for offline migration of stored legacy documents. +V1 authoring documents remain only for offline migration, while the +`ExplorerStateV1` viewer projection is still the live frontend read contract. ## Production path @@ -42,8 +43,7 @@ scoped Arango query ClickHouse outputs The HTTP endpoints are: - `GET .../authoring/v2/capability` -- `POST .../authoring/v2/compile` (with `/builder` retained as the same compile - operation for the current frontend) +- `POST .../authoring/v2/reconcile` for compilation of the exact persisted draft - `POST .../authoring/v2/preview` with `receiptId`, `outputId`, and an optional limit - `POST .../authoring/v2/publish` with `receiptId` @@ -69,6 +69,12 @@ snapshot. It supports: - compiler-proven projection modes; and - deterministic public column, emission, traversal alias, and recipe IDs. +Semantic Builder outputs use `traversalColumnNaming: ALIAS`. Builder occurrence +IDs are globally unique namespaces, so a nested column keeps its authored +`occurrence__column` name instead of acquiring ancestor prefixes during +physical lowering. General recipes retain the backwards-compatible `PATH` +default and continue to compose every traversal alias. + An invalid route, stale candidate, ambiguous occurrence, unsupported projection mode, or invalid presentation reference is returned as a structured error with stage, code, JSON path, message, and details. These are authoring errors, not @@ -100,7 +106,8 @@ The receipt pins: - public output contract and its digest; - candidate/occurrence/emission mappings; - emitted public columns; -- per-output fingerprints; and +- canonical per-output execution fingerprints; +- durable explicit/discovered column provenance used by publication; and - deterministic warnings. Request ID and creation time are operational metadata and do not affect the @@ -108,6 +115,12 @@ receipt ID. Receipt IDs are content-addressed. Reads validate the stored ID, compilation key, resolved-recipe digest, and output-contract digest before the artifact can execute. +Execution fingerprints cover the unoptimized renderer-neutral operation graph, +bind values, row identity, dynamic validation behavior, and ordered output +schema. They exclude optimizer reports and decisions, optimized plans, +diagnostic counters, source/debug provenance, preview limits, output selectors, +and publication-only authorization projections. + The receipt deliberately does **not** persist: - optimized physical IR; @@ -132,7 +145,8 @@ The result is validated, then persisted with immutable `INSERT ... OPTIONS { overwriteMode: "ignore" }` semantics. Loom reads and re-lowers the authoritative stored document with the same verifier used by preview before returning success. A compile never returns 200 with an artifact -that preview would immediately reject. The Arango collection +that preview would immediately reject. Contract failures identify the mismatched +component without exposing plans, bind values, or authorization paths. The Arango collection has a composite index over project, Explorer, compilation key, receipt format, and compiler contract. Receipt lookup for execution is always scoped by project, Explorer, and receipt ID. @@ -221,8 +235,9 @@ compiled again. Compiler-contract changes, capability/schema changes, generation changes, authorization-scope changes, or intent changes produce a different compilation key and receipt. Physical optimizer changes do not require rewriting receipts; -runtime lowering simply uses the current deterministic optimizer and verifies -the frozen semantic outputs. +optimizer state is outside the canonical execution fingerprint. Runtime +lowering uses the current optimizer after verifying the frozen unoptimized +execution contract. ## Implementation map @@ -233,8 +248,8 @@ the frozen semantic outputs. | Native V2 translation | `internal/explorer/compilation` | | Versioned receipt domain and integrity | `internal/explorer/compilation_receipt.go` | | Memory and Arango receipt persistence | `internal/explorer/memory_store.go`, `internal/explorer/arango/store.go` | -| Resolved-recipe execution boundary | `internal/dataframe/recipe/engine/engine.go` | -| V2 compile/preview/publish routes | `internal/server/explorer_authoring_v2_routes.go` | -| Production composition | `internal/server/server.go` | -| Receipt publication | `internal/server/explorer_publication.go` | -| Compiler-owned output/config validation | `internal/server/explorer_receipt_contract.go` | +| Explorer list/create/authoring/receipt/preview/publication workflows | `internal/explorer/lifecycle` | +| Resolved-recipe execution boundary | `internal/dataframe/execution/engine.go` | +| Generated HTTP request/response adapters | `internal/server/openapi_explorer_routes.go` | +| Production lifecycle composition and execution adapters | `internal/server/explorer_lifecycle_adapter.go` | +| Compiler-owned output/config validation policy | `internal/explorer/lifecycle/policy.go` | diff --git a/docs/GRAPHQL_API.md b/docs/GRAPHQL_API.md index bae81e93..a7aa3bab 100644 --- a/docs/GRAPHQL_API.md +++ b/docs/GRAPHQL_API.md @@ -424,8 +424,8 @@ Clients should not send generation IDs or physical table names as query selectors. `authResourcePaths` on graph requests is a narrowing request. It cannot widen -the principal's authorized scope. Flat reads apply authorization to each -published source automatically, including federated reads across projects. +the principal's authorized scope. Flat reads authorize the explicit project +and apply its generation-scoped resource paths to the selected publication. ## Current boundaries diff --git a/docs/LOCAL_REPOSITORY_DEMO.md b/docs/LOCAL_REPOSITORY_DEMO.md new file mode 100644 index 00000000..2b184603 --- /dev/null +++ b/docs/LOCAL_REPOSITORY_DEMO.md @@ -0,0 +1,88 @@ +# Launch Loom from a data repository + +Use the repository launcher when a checkout contains hydrated FHIR metadata and +a native Loom Explorer workspace: + +```text +repository/ +├── META/ +│ ├── Patient.ndjson +│ └── Specimen.ndjson +└── CONFIG/ + └── HTAN_INT-BForePC.json +``` + +The CONFIG file must be an Explorer Builder workspace with API version +`loom.calypr.org/explorer-authoring/v2`. Legacy Gen3/Guppy Explorer JSON is not +accepted. Rebuild or export that configuration through the current Loom Builder +before using it with this launcher. + +From the data repository, run: + +```bash +/path/to/loom/scripts/loom-repo-up.sh +``` + +If the CONFIG filename does not identify the project, pass it explicitly: + +```bash +/path/to/loom/scripts/loom-repo-up.sh \ + --project HTAN_INT/BForePC +``` + +Use `--config CONFIG/name.json` when CONFIG contains more than one JSON file. +From the Loom checkout, the equivalent command is: + +```bash +make repository-up REPOSITORY=/path/to/data-repository +``` + +Each repository path gets a stable `loom-repository-` Compose project +by default, so it cannot reuse the standalone demo's containers or volumes. +Override the runtime when needed: + +```bash +/path/to/loom/scripts/loom-repo-up.sh \ + --compose-project bforepc-demo \ + --api-port 18080 \ + --ui-port 13080 +``` + +The launcher also accepts `--api-host` and `--ui-host`. Their environment +equivalents are `LOOM_REPOSITORY_COMPOSE_PROJECT`, +`LOOM_REPOSITORY_API_HOST`, `LOOM_REPOSITORY_API_PORT`, +`LOOM_REPOSITORY_UI_HOST`, and `LOOM_REPOSITORY_UI_PORT`; command-line flags +take precedence. + +## Startup behavior + +The launcher completes repository preflight before running Docker. It reads the +working tree directly and never invokes Git LFS, `git-drs`, smudge filters, or +credential helpers. If any selected META or CONFIG file is still an LFS/DRS +pointer, it lists every unresolved path and exits without changing Docker or the +databases. + +After preflight succeeds, the launcher: + +1. Starts ArangoDB, ClickHouse, and the no-auth Loom API. +2. Checks whether the content-addressed generation is reusable. If the + generation is absent, uploads every `META/*.ndjson` file. +3. Publishes the CONFIG workspace, which materializes its outputs in ClickHouse. +4. Verifies the execution registry and Explorer Viewer response. +5. Starts the frontend and prints direct Builder and Viewer URLs. + +The META content digest determines the generation ID, so changing CONFIG does +not reload ArangoDB. Repeating the same launch checks the persisted generation +before upload and reuses the same generation and publication identities. + +## Local CONFIG write-back + +The repository Compose overlay mounts only the selected CONFIG directory as +writable. In this explicit no-auth mode, a successful Builder Publish atomically +replaces the selected CONFIG file with the canonical published V2 workspace. +The frontend package does not write files and normal Loom/Calypr deployments do +not enable this server option. + +If materialization or publication fails, the frontend is not started. Existing +database containers remain available for diagnosis and a subsequent launch +retries the same content-addressed operation. diff --git a/docs/PACKAGE_AUDIT.csv b/docs/PACKAGE_AUDIT.csv new file mode 100644 index 00000000..b416f9e8 --- /dev/null +++ b/docs/PACKAGE_AUDIT.csv @@ -0,0 +1,59 @@ +location,import_path,package_name,kind,responsibility,production_files,test_files,other_files,generated_files,production_loc,test_loc,exported_symbol_count,exported_symbols,internal_dependencies,test_dependencies,production_importers,test_only_importers,external_dependencies,runtime_signals,compatibility_signals,decision_evidence,test_scope,audit_status +.,github.com/calypr/loom,main,executable,Start the default Loom server process through the internal server composition root.,main.go,—,go.mod,—,7,0,0,—,internal/server,—,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test .,inventory complete; semantic disposition pending +cmd/arango-fhir-proto,github.com/calypr/loom/cmd/arango-fhir-proto,main,executable,"Provide operator commands for FHIR loading, catalog diagnostics, generation repair, and activation.",main.go,main_test.go,—,—,594,166,0,—,internal/catalog; internal/catalog/arango; internal/dataset; internal/dataset/arango; internal/ingest; internal/store/arango,—,—,—,—,—,legacy=4,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/arango-fhir-proto,inventory complete; semantic disposition pending +cmd/arango-fhir-server,github.com/calypr/loom/cmd/arango-fhir-server,main,executable,Start the explicit Loom HTTP server binary.,main.go,—,—,—,7,0,0,—,internal/server,—,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/arango-fhir-server,inventory complete; semantic disposition pending +cmd/dataframe-profile,github.com/calypr/loom/cmd/dataframe-profile,main,executable,Run and compare developer-facing dataframe profiling workloads.,main.go,—,—,—,322,0,0,—,conformance/compiler; generated/graphql/graph/model; internal/api/graphql/graph/query; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/recipe; internal/dataframe/semantic; internal/store/arango,—,—,—,—,—,legacy=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/dataframe-profile,inventory complete; semantic disposition pending +cmd/dataframe-query,github.com/calypr/loom/cmd/dataframe-query,main,executable,Execute a checked-in GraphQL dataframe request and report response timing and diagnostics.,main.go,main_test.go,—,—,241,81,0,—,—,—,—,—,—,—,compatibility=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/dataframe-query,inventory complete; semantic disposition pending +cmd/explorer-config-v2-convert,github.com/calypr/loom/cmd/explorer-config-v2-convert,main,executable,Convert legacy Explorer configuration input into the V2 authoring representation offline.,main.go,—,—,—,377,0,0,—,internal/dataframe/recipe; internal/explorer; internal/explorer/authoringv2,—,—,—,—,—,legacy=1,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/explorer-config-v2-convert,inventory complete; semantic disposition pending +cmd/generate,github.com/calypr/loom/cmd/generate,main,executable,"Generate FHIR models, schema metadata, validation, extraction, and GraphQL artifacts from source schemas.",extraction.go; graphql.go; helpers.go; main.go; metadata.go; model.go; names.go; resources.go; schema.go; validation.go,extraction_test.go; graphql_test.go; metadata_test.go; model_test.go; resources_test.go; root_test.go; schema_fixture_test.go,fhir_helpers.go.tmpl,—,1668,394,9,Definition; EdgeDocument; Link; Property; Result; Schema; TargetHints; TargetSchema; TemplatePointers,—,—,—,—,—,go:embed,compatibility=1,executable boundary; compare orchestration with sibling commands before combining; runtime/build signals present; static importer counts are insufficient for deletion,go test ./cmd/generate,inventory complete; semantic disposition pending +cmd/gqlgenfix,github.com/calypr/loom/cmd/gqlgenfix,main,executable,Post-process gqlgen output to enforce Loom-specific generated-code invariants.,main.go,main_test.go,—,—,108,40,1,MarshalJSON,—,—,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/gqlgenfix,inventory complete; semantic disposition pending +cmd/loom-acceptance,github.com/calypr/loom/cmd/loom-acceptance,main,executable,Run the locked real-data acceptance scenario and emit machine-readable verification artifacts.,main.go,—,—,—,119,0,0,—,internal/acceptance,—,—,—,—,—,—,executable boundary; compare orchestration with sibling commands before combining,go test ./cmd/loom-acceptance,inventory complete; semantic disposition pending +conformance/compiler,github.com/calypr/loom/conformance/compiler,compilerfixture,conformance,Load and exercise the canonical dataframe compiler oracle corpus for conformance and performance checks.,types.go,benchmark_test.go; corpus_coverage_test.go; gdc_fixture_test.go; generated_semantics_coverage_test.go; physical_plan_cost_test.go; policy_ablation_test.go; run_test.go; schema_roots_test.go; types_test.go,—,—,111,1214,4,Expected; Fixture; LoadDir; Validate,internal/dataframe/recipe,internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema; internal/store/arango,cmd/dataframe-profile,—,—,—,—,non-runtime conformance boundary,go test ./conformance/compiler,inventory complete; semantic disposition pending +generated/fhir,github.com/calypr/loom/generated/fhir,fhir,generated,"Expose generated FHIR resource models and their extraction, validation, and GraphQL helpers.",extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go,—,—,extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go,72020,0,155,Address; Age; Annotation; Attachment; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; CodeableConcept; CodeableReference; Coding; ConcreteResource; Condition; ConditionParticipant; ConditionStage; ContactDetail; ContactPoint; Count; DataRequirement; DataRequirementCodeFilter; DataRequirementDateFilter; DataRequirementSort; DataRequirementValueFilter; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportSupportingInfo; Directory; Distance; DocumentReference; DocumentReferenceAttester; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceRelatesTo; Dosage; DosageDoseAndRate; Duration; EdgeDocument; Expression; ExtendedContactDetail; Extension; ExtractEdges; FHIRComments; FHIRPrimitiveExtension; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryParticipant; FamilyMemberHistoryProcedure; GetID; GetResourceType; Group; GroupCharacteristic; GroupMember; HumanName; Identifier; ImagingStudy; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesPerformer; IsFHIRResource; MarshalJSON; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationPerformer; MedicationBatch; MedicationIngredient; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestSubstitution; MedicationStatement; MedicationStatementAdherence; Meta; Money; Narrative; NewConcreteResource; Observation; ObservationComponent; ObservationReferenceRange; ObservationTriggeredBy; Organization; OrganizationQualification; ParameterDefinition; Patient; PatientCommunication; PatientContact; PatientLink; Period; Practitioner; PractitionerCommunication; PractitionerQualification; PractitionerRole; Procedure; ProcedureFocalDevice; ProcedurePerformer; Quantity; Range; Ratio; RatioRange; Reference; RelatedArtifact; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyComparisonGroup; ResearchStudyLabel; ResearchStudyObjective; ResearchStudyOutcomeMeasure; ResearchStudyProgressStatus; ResearchStudyRecruitment; ResearchSubject; ResearchSubjectProgress; Resource; Result; SampledData; Signature; Specimen; SpecimenCollection; SpecimenContainer; SpecimenFeature; SpecimenProcessing; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCode; SubstanceDefinitionMoiety; SubstanceDefinitionMolecularWeight; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionProperty; SubstanceDefinitionRelationship; SubstanceDefinitionSourceMaterial; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceIngredient; Task; TaskInput; TaskOutput; TaskPerformer; TaskRestriction; Timing; TimingRepeat; TriggerDefinition; UnmarshalJSON; UsageContext; Validate; ValidateAndExtract; ValidateFhirBinary; ValidateFhirDate; ValidateFhirDateTime; ValidateFhirTime; ValidateFhirURI; ValidateFhirUUID,internal/fhir/schema,—,generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/dataframe/recipe; internal/dataframe/spec; internal/ingest,—,github.com/bytedance/sonic; github.com/google/uuid,1 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/fhir,inventory complete; semantic disposition pending +generated/fhirschema,github.com/calypr/loom/generated/fhirschema,fhirschema,generated,Expose generated raw FHIR schema metadata tables.,generated.go,—,—,generated.go,48359,0,3,Definition; Property; Traversal,—,—,internal/fhir/schema,—,—,—,—,"generated boundary; change its source/generator, not emitted code",go test ./generated/fhirschema,inventory complete; semantic disposition pending +generated/graphql/graph/executor,github.com/calypr/loom/generated/graphql/graph/executor,executor,generated,Provide gqlgen-generated GraphQL executable schema and execution plumbing.,fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go,—,—,fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go,155252,0,328,Address; AddressResolver; Age; AgeResolver; Annotation; AnnotationResolver; Attachment; AttachmentResolver; Availability; AvailabilityAvailableTime; AvailabilityAvailableTimeResolver; AvailabilityNotAvailableTime; AvailabilityNotAvailableTimeResolver; AvailabilityResolver; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmarkResolver; BodyStructureIncludedStructureBodyLandmarkOrientationResolver; BodyStructureIncludedStructureResolver; BodyStructureResolver; CodeableConcept; CodeableConceptResolver; CodeableReference; CodeableReferenceResolver; Coding; CodingResolver; Complexity; ComplexityRoot; Condition; ConditionParticipant; ConditionParticipantResolver; ConditionResolver; ConditionStage; ConditionStageResolver; Config; ContactDetail; ContactDetailResolver; ContactPoint; ContactPointResolver; Count; CountResolver; DataRequirement; DataRequirementCodeFilter; DataRequirementCodeFilterResolver; DataRequirementDateFilter; DataRequirementDateFilterResolver; DataRequirementResolver; DataRequirementSort; DataRequirementSortResolver; DataRequirementValueFilter; DataRequirementValueFilterResolver; DataframeAggregateResult; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeMaterialization; DataframeOptimizationDecision; DataframeOptimizationPolicy; DataframePageInfo; DataframeQueryDiagnostics; DataframeRecipeArangoAssessment; DataframeRecipeColumn; DataframeRecipeExecution; DataframeRecipeExecutionOutput; DataframeRecipeExpansionExplanation; DataframeRecipeExplainCollectionScan; DataframeRecipeExplainIndexLocation; DataframeRecipeExplainIndexSummary; DataframeRecipeExplainPlanEstimate; DataframeRecipeExplainWarning; DataframeRecipeExplanation; DataframeRecipeExpressionExplanation; DataframeRecipeOptimizationDecision; DataframeRecipeOptimizationExplanation; DataframeRecipeOptimizationRuleState; DataframeRecipeOutputExplanation; DataframeRecipeOutputValidation; DataframeRecipePhysicalExplanation; DataframeRecipePhysicalOutputExplanation; DataframeRecipePreflight; DataframeRecipePreview; DataframeRecipePreviewOutput; DataframeRecipeResult; DataframeRecipeResultOutput; DataframeRecipeRevision; DataframeRecipeValidation; DataframeRichSourceReuse; DataframeRowConnection; DataframeSelector; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportMediaResolver; DiagnosticReportResolver; DiagnosticReportSupportingInfo; DiagnosticReportSupportingInfoResolver; DirectiveRoot; Directory; Distance; DistanceResolver; DocumentReference; DocumentReferenceAttester; DocumentReferenceAttesterResolver; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceContentProfileResolver; DocumentReferenceContentResolver; DocumentReferenceRelatesTo; DocumentReferenceRelatesToResolver; DocumentReferenceResolver; Dosage; DosageDoseAndRate; DosageDoseAndRateResolver; DosageResolver; Duration; DurationResolver; Exec; Expression; ExpressionResolver; ExtendedContactDetail; ExtendedContactDetailResolver; Extension; ExtensionResolver; FHIRPrimitiveExtension; FHIRPrimitiveExtensionResolver; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryConditionResolver; FamilyMemberHistoryParticipant; FamilyMemberHistoryParticipantResolver; FamilyMemberHistoryProcedure; FamilyMemberHistoryProcedureResolver; FamilyMemberHistoryResolver; FhirDataframeResult; FhirGraphNode; FhirGraphPageInfo; FhirGraphPath; FhirGraphQueryExplanation; FhirGraphQueryResult; FhirGraphRelationship; Group; GroupCharacteristic; GroupCharacteristicResolver; GroupMember; GroupMemberResolver; GroupResolver; HumanName; HumanNameResolver; Identifier; IdentifierResolver; ImagingStudy; ImagingStudyResolver; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesInstanceResolver; ImagingStudySeriesPerformer; ImagingStudySeriesPerformerResolver; ImagingStudySeriesResolver; MarshalJSON; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationDosageResolver; MedicationAdministrationPerformer; MedicationAdministrationPerformerResolver; MedicationAdministrationResolver; MedicationBatch; MedicationBatchResolver; MedicationIngredient; MedicationIngredientResolver; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestDispenseRequestInitialFillResolver; MedicationRequestDispenseRequestResolver; MedicationRequestResolver; MedicationRequestSubstitution; MedicationRequestSubstitutionResolver; MedicationResolver; MedicationStatement; MedicationStatementAdherence; MedicationStatementAdherenceResolver; MedicationStatementResolver; Meta; MetaResolver; Money; MoneyResolver; Mutation; MutationResolver; Narrative; NarrativeResolver; NewExecutableSchema; Observation; ObservationComponent; ObservationComponentResolver; ObservationReferenceRange; ObservationReferenceRangeResolver; ObservationResolver; ObservationTriggeredBy; ObservationTriggeredByResolver; Organization; OrganizationQualification; OrganizationQualificationResolver; OrganizationResolver; ParameterDefinition; ParameterDefinitionResolver; Patient; PatientCommunication; PatientCommunicationResolver; PatientContact; PatientContactResolver; PatientLink; PatientLinkResolver; PatientResolver; Period; PeriodResolver; Practitioner; PractitionerCommunication; PractitionerCommunicationResolver; PractitionerQualification; PractitionerQualificationResolver; PractitionerResolver; PractitionerRole; PractitionerRoleResolver; Procedure; ProcedureFocalDevice; ProcedureFocalDeviceResolver; ProcedurePerformer; ProcedurePerformerResolver; ProcedureResolver; Quantity; QuantityResolver; QueryResolver; Range; RangeResolver; Ratio; RatioRange; RatioRangeResolver; RatioResolver; Reference; ReferenceResolver; RelatedArtifact; RelatedArtifactResolver; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyAssociatedPartyResolver; ResearchStudyComparisonGroup; ResearchStudyComparisonGroupResolver; ResearchStudyLabel; ResearchStudyLabelResolver; ResearchStudyObjective; ResearchStudyObjectiveResolver; ResearchStudyOutcomeMeasure; ResearchStudyOutcomeMeasureResolver; ResearchStudyProgressStatus; ResearchStudyProgressStatusResolver; ResearchStudyRecruitment; ResearchStudyRecruitmentResolver; ResearchStudyResolver; ResearchSubject; ResearchSubjectProgress; ResearchSubjectProgressResolver; ResearchSubjectResolver; ResolverRoot; Resource; ResourceResolver; SampledData; SampledDataResolver; Schema; Signature; SignatureResolver; Specimen; SpecimenCollection; SpecimenCollectionResolver; SpecimenContainer; SpecimenContainerResolver; SpecimenFeature; SpecimenFeatureResolver; SpecimenProcessing; SpecimenProcessingResolver; SpecimenResolver; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCharacterizationResolver; SubstanceDefinitionCode; SubstanceDefinitionCodeResolver; SubstanceDefinitionMoiety; SubstanceDefinitionMoietyResolver; SubstanceDefinitionMolecularWeight; SubstanceDefinitionMolecularWeightResolver; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionNameOfficialResolver; SubstanceDefinitionNameResolver; SubstanceDefinitionProperty; SubstanceDefinitionPropertyResolver; SubstanceDefinitionRelationship; SubstanceDefinitionRelationshipResolver; SubstanceDefinitionResolver; SubstanceDefinitionSourceMaterial; SubstanceDefinitionSourceMaterialResolver; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceDefinitionStructureRepresentationResolver; SubstanceDefinitionStructureResolver; SubstanceIngredient; SubstanceIngredientResolver; SubstanceResolver; Task; TaskInput; TaskInputResolver; TaskOutput; TaskOutputResolver; TaskPerformer; TaskPerformerResolver; TaskResolver; TaskRestriction; TaskRestrictionResolver; Timing; TimingRepeat; TimingRepeatResolver; TimingResolver; TriggerDefinition; TriggerDefinitionResolver; UsageContext; UsageContextResolver,generated/fhir; generated/graphql/graph/model,—,internal/api/graphql/graph; internal/api/graphql/graph/resolver,—,github.com/99designs/gqlgen/graphql; github.com/99designs/gqlgen/graphql/introspection; github.com/vektah/gqlparser/v2; github.com/vektah/gqlparser/v2/ast,138 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/graphql/graph/executor,inventory complete; semantic disposition pending +generated/graphql/graph/model,github.com/calypr/loom/generated/graphql/graph/model,model,generated,Provide gqlgen-generated GraphQL transport model types.,models.go,—,—,models.go,1332,0,95,DataframeAggregateInput; DataframeAggregateResult; DataframeAggregationSpecInput; DataframeAggregationsInput; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeDatasetInput; DataframeFilterInput; DataframeMaterialization; DataframeMaterializationState; DataframeOptimizationDecision; DataframeOptimizationPolicy; DataframePageInfo; DataframeQueryDiagnostics; DataframeRecipeArangoAssessment; DataframeRecipeBindingsInput; DataframeRecipeColumn; DataframeRecipeExecution; DataframeRecipeExecutionOutput; DataframeRecipeExecutionState; DataframeRecipeExpansionExplanation; DataframeRecipeExplainCollectionScan; DataframeRecipeExplainIndexLocation; DataframeRecipeExplainIndexSummary; DataframeRecipeExplainPlanEstimate; DataframeRecipeExplainWarning; DataframeRecipeExplanation; DataframeRecipeExpressionExplanation; DataframeRecipeOptimizationDecision; DataframeRecipeOptimizationExplanation; DataframeRecipeOptimizationRuleState; DataframeRecipeOutputExplanation; DataframeRecipeOutputValidation; DataframeRecipePhysicalExplanation; DataframeRecipePhysicalOutputExplanation; DataframeRecipePreflight; DataframeRecipePreview; DataframeRecipePreviewOutput; DataframeRecipeResult; DataframeRecipeResultOutput; DataframeRecipeRevision; DataframeRecipeValidation; DataframeRichSourceReuse; DataframeRowConnection; DataframeRowsInput; DataframeSelector; DataframeSelectorInput; DataframeSortInput; ExplainDataframeRecipeInput; FHIRResource; FHIRResourceType; FhirAggregateInput; FhirAggregateOperation; FhirCatalogProjectionInput; FhirCodeValueInput; FhirColumnNaming; FhirDataframeInput; FhirDataframeResult; FhirFieldPredicateInput; FhirFieldPredicateOperation; FhirFieldSelectInput; FhirFieldSelectorInput; FhirFilterInput; FhirFilterOperator; FhirFilterQuantifier; FhirFilterValueInput; FhirFilterValueKind; FhirGraphNode; FhirGraphPageInfo; FhirGraphPath; FhirGraphQueryExplanation; FhirGraphQueryInput; FhirGraphQueryResult; FhirGraphRelationship; FhirGraphTraversalInput; FhirPivotDiscoveryInput; FhirPivotInput; FhirRepresentativeSliceInput; FhirTraversalMatchMode; FhirTraversalStepInput; FhirValueMode; IsValid; MarshalGQL; MarshalJSON; Mutation; PreflightDataframeRecipeInput; PreviewDataframeRecipeInput; Query; RegisterDataframeRecipeRevisionInput; RunDataframeRecipeInput; String; UnmarshalGQL; UnmarshalJSON; ValidateDataframeRecipeInput,—,—,cmd/dataframe-profile; generated/graphql/graph/executor; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver,—,—,1 exported interface(s),—,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/graphql/graph/model,inventory complete; semantic disposition pending +generated/loomapi,github.com/calypr/loom/generated/loomapi,loomapi,generated,"Provide OpenAPI-generated HTTP request, response, and strict-server contracts.",api.gen.go,—,—,api.gen.go,4505,0,375,APIVersion; Action; ActionColumn; ActivateDatasetGeneration; ActivateDatasetGeneration200JSONResponse; ActivateDatasetGeneration400JSONResponse; ActivateDatasetGeneration401JSONResponse; ActivateDatasetGeneration403JSONResponse; ActivateDatasetGeneration409JSONResponse; ActivateDatasetGeneration500JSONResponse; ActivateDatasetGeneration503JSONResponse; ActivateDatasetGenerationParams; ActivateDatasetGenerationRequestObject; ActivateDatasetGenerationResponseObject; ApplyCommandsRequest; ApplyCommandsResponse; ApplyExplorerBuilderCommands; ApplyExplorerBuilderCommands200JSONResponse; ApplyExplorerBuilderCommands400JSONResponse; ApplyExplorerBuilderCommands401JSONResponse; ApplyExplorerBuilderCommands403JSONResponse; ApplyExplorerBuilderCommands409JSONResponse; ApplyExplorerBuilderCommands422JSONResponse; ApplyExplorerBuilderCommands500JSONResponse; ApplyExplorerBuilderCommands503JSONResponse; ApplyExplorerBuilderCommandsJSONRequestBody; ApplyExplorerBuilderCommandsRequestObject; ApplyExplorerBuilderCommandsResponseObject; AsLegacyErrorBody; AsLegacyErrorResponseError0; AuthoringBadRequest; AuthoringBadRequestJSONResponse; AuthoringCapability; AuthoringCapabilityKind; AuthoringCapabilityOperations; AuthoringClientClosedRequest; AuthoringClientClosedRequestJSONResponse; AuthoringCommand; AuthoringCommandInitialPresentation; AuthoringCommandType; AuthoringConflict; AuthoringConflictJSONResponse; AuthoringFeatures; AuthoringForbidden; AuthoringForbiddenJSONResponse; AuthoringGatewayTimeout; AuthoringGatewayTimeoutJSONResponse; AuthoringInternalError; AuthoringInternalErrorJSONResponse; AuthoringNotFound; AuthoringNotFoundJSONResponse; AuthoringPayloadTooLarge; AuthoringPayloadTooLargeJSONResponse; AuthoringTooManyRequests; AuthoringTooManyRequestsJSONResponse; AuthoringUnavailable; AuthoringUnavailableJSONResponse; AuthoringUnprocessable; AuthoringUnprocessableJSONResponse; BuilderState; BuilderStateKind; BuilderStateLifecycleState; CandidateSearchRequest; CandidateSearchResponse; CandidateSearchResponseKind; Catalog; CatalogCandidate; CatalogEdge; CatalogNode; ChartPresentation; Column; ColumnSource; ColumnSourceKind; ColumnSourceProjectionMode; CommandResult; CommandResultType; CompileResponse; CompileResponseKind; ContractColumn; CreateDatasetGeneration; CreateDatasetGeneration200JSONResponse; CreateDatasetGeneration400JSONResponse; CreateDatasetGeneration401JSONResponse; CreateDatasetGeneration403JSONResponse; CreateDatasetGeneration409JSONResponse; CreateDatasetGeneration415JSONResponse; CreateDatasetGeneration422JSONResponse; CreateDatasetGeneration500JSONResponse; CreateDatasetGeneration503JSONResponse; CreateDatasetGenerationMultipartBody; CreateDatasetGenerationMultipartRequestBody; CreateDatasetGenerationRequestObject; CreateDatasetGenerationResponseObject; CreateExplorer; CreateExplorer201JSONResponse; CreateExplorer400JSONResponse; CreateExplorer401JSONResponse; CreateExplorer403JSONResponse; CreateExplorer409JSONResponse; CreateExplorer422JSONResponse; CreateExplorer500JSONResponse; CreateExplorerJSONRequestBody; CreateExplorerRequestObject; CreateExplorerResponseObject; Diagnostic; Document; DocumentKind; ErrorBody; ErrorResponse; ExecuteDataframeGraphQL; ExecuteDataframeGraphQL200JSONResponse; ExecuteDataframeGraphQL400JSONResponse; ExecuteDataframeGraphQL401JSONResponse; ExecuteDataframeGraphQL422JSONResponse; ExecuteDataframeGraphQL500JSONResponse; ExecuteDataframeGraphQLJSONRequestBody; ExecuteDataframeGraphQLRequestObject; ExecuteDataframeGraphQLResponseObject; ExecuteGraphQL; ExecuteGraphQL200JSONResponse; ExecuteGraphQL400JSONResponse; ExecuteGraphQL401JSONResponse; ExecuteGraphQL422JSONResponse; ExecuteGraphQL500JSONResponse; ExecuteGraphQLJSONRequestBody; ExecuteGraphQLRequestObject; ExecuteGraphQLResponseObject; ExplorerId; ExplorerMetadata; FiberServerOptions; FileActions; FilterPresentation; FixedFilter; FromLegacyErrorBody; FromLegacyErrorResponseError0; Generation; Get; GetApolloSandbox; GetApolloSandbox200TexthtmlResponse; GetApolloSandbox401JSONResponse; GetApolloSandbox404JSONResponse; GetApolloSandboxRequestObject; GetApolloSandboxResponseObject; GetExplorer; GetExplorer200JSONResponse; GetExplorer401JSONResponse; GetExplorer403JSONResponse; GetExplorer404JSONResponse; GetExplorer500JSONResponse; GetExplorerAuthoringCapability; GetExplorerAuthoringCapability200JSONResponse; GetExplorerAuthoringCapability401JSONResponse; GetExplorerAuthoringCapability403JSONResponse; GetExplorerAuthoringCapability500JSONResponse; GetExplorerAuthoringCapabilityRequestObject; GetExplorerAuthoringCapabilityResponseObject; GetExplorerBuilder; GetExplorerBuilder200JSONResponse; GetExplorerBuilder401JSONResponse; GetExplorerBuilder403JSONResponse; GetExplorerBuilder404JSONResponse; GetExplorerBuilder409JSONResponse; GetExplorerBuilder500JSONResponse; GetExplorerBuilder503JSONResponse; GetExplorerBuilderRequestObject; GetExplorerBuilderResponseObject; GetExplorerRequestObject; GetExplorerResponseObject; GetGraphQLPlayground; GetGraphQLPlayground200TexthtmlResponse; GetGraphQLPlayground401JSONResponse; GetGraphQLPlayground404JSONResponse; GetGraphQLPlaygroundRequestObject; GetGraphQLPlaygroundResponseObject; GetHealth; GetHealth200JSONResponse; GetHealth503JSONResponse; GetHealthRequestObject; GetHealthResponseObject; GetLiveness; GetLiveness200JSONResponse; GetLivenessRequestObject; GetLivenessResponseObject; GetReadiness; GetReadiness200JSONResponse; GetReadiness503JSONResponse; GetReadinessRequestObject; GetReadinessResponseObject; GetRecipeExecution; GetRecipeExecution200JSONResponse; GetRecipeExecution401JSONResponse; GetRecipeExecution403JSONResponse; GetRecipeExecution404JSONResponse; GetRecipeExecutionRequestObject; GetRecipeExecutionResponseObject; GetSpec; GetSpecJSON; GetSwagger; GraphQLBadRequest; GraphQLBadRequestJSONResponse; GraphQLInternalError; GraphQLInternalErrorJSONResponse; GraphQLUnprocessable; GraphQLUnprocessableJSONResponse; HTMLTexthtmlResponse; HandlerMiddlewareFunc; LegacyBadRequest; LegacyBadRequestJSONResponse; LegacyConflict; LegacyConflictJSONResponse; LegacyErrorBody; LegacyErrorResponse; LegacyErrorResponseError0; LegacyErrorResponse_Error; LegacyForbidden; LegacyForbiddenJSONResponse; LegacyInternalError; LegacyInternalErrorJSONResponse; LegacyNotFound; LegacyNotFoundJSONResponse; LegacyUnavailable; LegacyUnavailableJSONResponse; LegacyUnprocessable; LegacyUnprocessableJSONResponse; ListExplorers; ListExplorers200JSONResponse; ListExplorers401JSONResponse; ListExplorers403JSONResponse; ListExplorers500JSONResponse; ListExplorersRequestObject; ListExplorersResponseObject; MarshalJSON; MergeLegacyErrorBody; MergeLegacyErrorResponseError0; MiddlewareFunc; NewStrictHandler; Output; PathToRawSpec; PreviewExplorer; PreviewExplorer200JSONResponse; PreviewExplorer400JSONResponse; PreviewExplorer401JSONResponse; PreviewExplorer403JSONResponse; PreviewExplorer404JSONResponse; PreviewExplorer409JSONResponse; PreviewExplorer413JSONResponse; PreviewExplorer422JSONResponse; PreviewExplorer429JSONResponse; PreviewExplorer499JSONResponse; PreviewExplorer500JSONResponse; PreviewExplorer503JSONResponse; PreviewExplorer504JSONResponse; PreviewExplorerJSONRequestBody; PreviewExplorerRequestObject; PreviewExplorerResponseObject; PreviewRequest; PreviewResponse; PreviewResponseKind; Project; ProjectionMode; PublicationOutput; PublishExplorer; PublishExplorer200JSONResponse; PublishExplorer400JSONResponse; PublishExplorer401JSONResponse; PublishExplorer403JSONResponse; PublishExplorer404JSONResponse; PublishExplorer409JSONResponse; PublishExplorer422JSONResponse; PublishExplorer500JSONResponse; PublishExplorer503JSONResponse; PublishExplorerJSONRequestBody; PublishExplorerRequestObject; PublishExplorerResponseObject; PublishRepositoryExplorerConfig; PublishRepositoryExplorerConfig200JSONResponse; PublishRepositoryExplorerConfig400JSONResponse; PublishRepositoryExplorerConfig401JSONResponse; PublishRepositoryExplorerConfig403JSONResponse; PublishRepositoryExplorerConfig409JSONResponse; PublishRepositoryExplorerConfig422JSONResponse; PublishRepositoryExplorerConfig500JSONResponse; PublishRepositoryExplorerConfig503JSONResponse; PublishRepositoryExplorerConfigJSONRequestBody; PublishRepositoryExplorerConfigParams; PublishRepositoryExplorerConfigRequestObject; PublishRepositoryExplorerConfigResponseObject; PublishRequest; PublishResponse; PublishResponseKind; RawJSON; ReceiptOutput; ReconcileExplorerBuilder; ReconcileExplorerBuilder200JSONResponse; ReconcileExplorerBuilder400JSONResponse; ReconcileExplorerBuilder401JSONResponse; ReconcileExplorerBuilder403JSONResponse; ReconcileExplorerBuilder404JSONResponse; ReconcileExplorerBuilder409JSONResponse; ReconcileExplorerBuilder422JSONResponse; ReconcileExplorerBuilder500JSONResponse; ReconcileExplorerBuilder503JSONResponse; ReconcileExplorerBuilderJSONRequestBody; ReconcileExplorerBuilderRequestObject; ReconcileExplorerBuilderResponseObject; ReconcileRequest; RegisterHandlers; RegisterHandlersWithOptions; RouteNode; RoutePolicy; SearchExplorerCandidates; SearchExplorerCandidates200JSONResponse; SearchExplorerCandidates400JSONResponse; SearchExplorerCandidates401JSONResponse; SearchExplorerCandidates403JSONResponse; SearchExplorerCandidates409JSONResponse; SearchExplorerCandidates500JSONResponse; SearchExplorerCandidates503JSONResponse; SearchExplorerCandidatesJSONRequestBody; SearchExplorerCandidatesRequestObject; SearchExplorerCandidatesResponseObject; ServerInterface; ServerInterfaceWrapper; ServiceBadRequest; ServiceBadRequestJSONResponse; ServiceConflict; ServiceConflictJSONResponse; ServiceErrorBody; ServiceErrorResponse; ServiceForbidden; ServiceForbiddenJSONResponse; ServiceInternalError; ServiceInternalErrorJSONResponse; ServiceNotFound; ServiceNotFoundJSONResponse; ServiceUnauthorized; ServiceUnauthorizedJSONResponse; ServiceUnavailable; ServiceUnavailableJSONResponse; ServiceUnprocessable; ServiceUnprocessableJSONResponse; ServiceUnsupportedMediaType; ServiceUnsupportedMediaTypeJSONResponse; Set; SharedFilterBinding; StrictHandlerFunc; StrictMiddlewareFunc; StrictServerInterface; Tab; TablePresentation; TablePresentationCellRenderer; UnmarshalJSON; Valid; VisitActivateDatasetGenerationResponse; VisitApplyExplorerBuilderCommandsResponse; VisitCreateDatasetGenerationResponse; VisitCreateExplorerResponse; VisitExecuteDataframeGraphQLResponse; VisitExecuteGraphQLResponse; VisitGetApolloSandboxResponse; VisitGetExplorerAuthoringCapabilityResponse; VisitGetExplorerBuilderResponse; VisitGetExplorerResponse; VisitGetGraphQLPlaygroundResponse; VisitGetHealthResponse; VisitGetLivenessResponse; VisitGetReadinessResponse; VisitGetRecipeExecutionResponse; VisitListExplorersResponse; VisitPreviewExplorerResponse; VisitPublishExplorerResponse; VisitPublishRepositoryExplorerConfigResponse; VisitReconcileExplorerBuilderResponse; VisitSearchExplorerCandidatesResponse; Workspace,internal/explorer/authoringv2,—,internal/server,—,github.com/getkin/kin-openapi/openapi3; github.com/gofiber/fiber/v3; github.com/oapi-codegen/runtime; github.com/oapi-codegen/runtime/types,23 exported interface(s),legacy=10; deprecated=1; compatibility=1,"generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion",go test ./generated/loomapi,inventory complete; semantic disposition pending +internal/acceptance,github.com/calypr/loom/internal/acceptance,acceptance,domain/library,"Fetch and cache the locked FHIR fixture, drive the real-data acceptance scenario, and verify its outputs and performance evidence.",fixture.go; performance.go; runner.go; scenario.go,fixture_test.go; performance_test.go; scenario_test.go,—,—,1972,655,41,Acquire; Close; ComparePerformance; ComparePerformanceReportFiles; ComparePerformanceRuns; ComputedDigest; Config; Connections; DefaultLimits; Fetcher; FixtureDigest; FixtureLock; Lease; Limits; LoadLock; Namespace; New; Oracle; PerformanceComparisonReport; PerformanceFailure; PerformanceObservation; PerformanceStageSeconds; PerformanceSuspicion; RefreshLock; Report; ResourceLock; Run; RunID; RunScenario; RunSpec; Runner; ScenarioConfig; ScenarioResult; SelectionRule; StageReport; StaticTarget; Target; Validate; ValidatedFixture; WriteLock; WritePerformanceComparison,internal/dataframe/publication; internal/dataframe/publication/arango; internal/store/arango; internal/store/clickhouse,internal/dataframe/publication; internal/explorer/authoringv2,cmd/loom-acceptance,—,—,2 exported interface(s),compatibility=1,one production importer (cmd/loom-acceptance); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/acceptance,inventory complete; semantic disposition pending +internal/api/bulk/load,github.com/calypr/loom/internal/api/bulk/load,load,transport adapter,Adapt multipart HTTP generation uploads and activation requests to ingest and dataset services.,errors.go; generation_staging.go; handler.go; operations.go; service.go,errors_test.go; operations_test.go; service_test.go,—,—,399,350,15,ActivateDatasetGeneration; Config; CreateDatasetGeneration; DataframeReleaseStore; GenerationActivator; GenerationLoadRequest; GenerationLoadResult; GenerationLoader; Handler; IngestRunner; NewHandler; NewService; RunGeneration; Service; ServiceConfig,internal/authscope; internal/dataframe/errors; internal/dataframe/publication; internal/dataset; internal/ingest,internal/authscope; internal/dataframe/errors; internal/dataframe/publication; internal/dataset; internal/ingest,internal/server,—,github.com/gofiber/fiber/v3,2 exported interface(s),—,one production importer (internal/server); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/api/bulk/load,completed/keep +internal/api/graphql/graph,github.com/calypr/loom/internal/api/graphql/graph,graphqlapi,transport adapter,"Own GraphQL HTTP handler construction, error presentation, and generated executor binding.",doc.go; errors.go; handler.go,aggregate_http_integration_test.go; errors_test.go; handler_test.go; http_integration_test.go,—,—,288,831,8,Flush; Header; NewApolloSandboxHandler; NewHandler; NewPlaygroundHandler; RouteConfig; Write; WriteHeader,generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/errors,internal/api/graphql/graph; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/authscope; internal/catalog; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataset,internal/server,internal/api/graphql/graph,github.com/99designs/gqlgen/graphql; github.com/99designs/gqlgen/graphql/handler; github.com/99designs/gqlgen/graphql/playground; github.com/gofiber/fiber/v3/middleware/adaptor; github.com/vektah/gqlparser/v2/gqlerror,—,—,one production importer (internal/server); inspect ownership/inline potential,go test ./internal/api/graphql/graph,completed/keep +internal/api/graphql/graph/dataframe,github.com/calypr/loom/internal/api/graphql/graph/dataframe,dataframe,transport adapter,"Adapt published ClickHouse dataframe discovery, row, aggregate, and export operations to GraphQL.",aggregates.go; export.go; model.go; rows.go; service.go,aggregates_test.go; export_test.go; filter_test.go; model_test.go; service_test.go,—,—,1084,316,17,AggregateInput; AggregateRowsResult; AggregationsInput; AggregationsJSON; ColumnFromPhysical; ColumnModel; Config; Dataset; ExportDataframe; Model; NewService; PersistedFailure; ProjectDatasets; Rows; Service; WithOperationContext; Write,generated/graphql/graph/model; internal/api/http; internal/authscope; internal/dataframe/errors; internal/dataframe/publication; internal/dataframe/published,generated/graphql/graph/model; internal/authscope; internal/dataframe/errors; internal/dataframe/published,internal/api/graphql/graph/resolver,—,—,—,legacy=8,one production importer (internal/api/graphql/graph/resolver); inspect ownership/inline potential,go test ./internal/api/graphql/graph/dataframe,inventory complete; semantic disposition pending +internal/api/graphql/graph/query,github.com/calypr/loom/internal/api/graphql/graph/query,queryapi,transport adapter,Resolve and authorize Arango-backed FHIR dataframe and graph query requests for GraphQL.,active_generation.go; doc.go; errors.go; fhir_service.go; fieldrefs.go; graph_service.go; helpers.go; input_resolution.go; introspection.go; recipe_discovery.go; recipe_mapping.go; recipe_resolve.go; service.go; types.go,active_generation_test.go; auth_scope_test.go; errors_test.go; fhir_service_test.go; fieldrefs_test.go; graph_service_test.go; introspection_test.go; recipe_mapping_test.go,—,—,1917,741,29,Config; ExplainFHIRGraph; FHIRGraphExplanation; FHIRGraphNode; FHIRGraphPageInfo; FHIRGraphPath; FHIRGraphQuery; FHIRGraphRelationship; FHIRGraphResult; FHIRGraphTraversal; FHIRListRequest; FHIRListResult; FHIRReadContext; FieldHint; FieldPredicate; FieldSelector; Fields; Introspect; IntrospectionRequest; IntrospectionResponse; ListFHIR; ListFHIRInContext; NewService; RecipeBundleFromInput; RelatedResourceHints; ResourceHints; Run; RunFHIRGraph; Service,generated/graphql/graph/model; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataset; internal/fhir/schema,generated/graphql/graph/model; internal/authscope; internal/catalog; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataset,cmd/dataframe-profile; internal/api/graphql/graph/resolver; internal/server,internal/api/graphql/graph,—,—,—,broad surface (14 production files); inspect cohesion,go test ./internal/api/graphql/graph/query,inventory complete; semantic disposition pending +internal/api/graphql/graph/resolver,github.com/calypr/loom/internal/api/graphql/graph/resolver,resolver,transport adapter,Implement gqlgen field resolvers and map GraphQL values to query and lifecycle services.,dataframe_diagnostics.go; fhir_reference_loader.go; fhir_resolver_helpers.go; fhir_schema.resolvers.go; graph_resolvers.go; graphql_output_helpers.go; operation_context.go; recipe_control_graphql.go; recipe_execution_reader.go; recipe_revision_model.go; resolver.go; schema.resolvers.go,fhir_reference_loader_test.go; output_mapping_test.go; recipe_control_test.go,—,fhir_schema.resolvers.go; schema.resolvers.go,5380,293,362,AbatementDateTime; AbatementString; Active; Actual; ActualComparisonGroup; ActualNumber; Address; Age; AgeString; Alias; AllDay; AllowedBoolean; AmountString; Annotation; AsNeeded; AssignedComparisonGroup; Attachment; AuthorString; AuthoredOn; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; AvailableEndTime; AvailableStartTime; BirthDate; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; BornDate; BornString; Citation; City; Code; CodeMap; CodeableConcept; CodeableReference; Coding; CollectedDateTime; Combined; Comment; Comparator; Conclusion; Condition; ConditionParticipant; ConditionStage; ContactDetail; ContactPoint; ContentType; ContributedToDeath; Count; CountMax; Country; Creation; Currency; Data; DataRequirement; DataRequirementCodeFilter; DataRequirementDateFilter; DataRequirementSort; DataRequirementValueFilter; DataframeAggregate; DataframeAggregations; DataframeDataset; DataframeRecipeExecution; DataframeRecipeRevision; DataframeRecipeRevisions; DataframeRows; Date; DateAsserted; DayOfWeek; DaysOfWeek; DeceasedBoolean; DeceasedDate; DeceasedDateTime; DeceasedString; Description; DescriptionSummary; DiagnosticReport; DiagnosticReportMedia; DiagnosticReportSupportingInfo; Dimensions; Direction; Display; Distance; District; Div; DoNotPerform; DocStatus; DocumentReference; DocumentReferenceAttester; DocumentReferenceContent; DocumentReferenceContentProfile; DocumentReferenceRelatesTo; Documentation; Dosage; DosageDoseAndRate; Duration; DurationMax; DurationUnit; EffectiveDateTime; EffectiveInstant; End; EndDate; EstimatedAge; Event; Exclude; ExpirationDate; Expiry; ExplainDataframeRecipe; ExplainFhirGraph; Expression; ExtendedContactDetail; Extension; FHIRPrimitiveExtension; Factor; Family; FamilyMemberHistory; FamilyMemberHistoryCondition; FamilyMemberHistoryParticipant; FamilyMemberHistoryProcedure; FhirComments; FhirGraph; Frames; Frequency; FrequencyMax; Gender; Given; Group; GroupCharacteristic; GroupMember; Hash; Height; HumanName; Identifier; ImagingStudy; ImagingStudySeries; ImagingStudySeriesInstance; ImagingStudySeriesPerformer; ImplicitRules; Inactive; Instance; InstantiatesCanonical; InstantiatesURI; Intent; Interval; IntervalUnit; IsActive; IsDefining; IsSubPotent; Issued; Label; Language; LastModified; LastUpdated; Limit; Line; LinkID; LotNumber; LowerLimit; Max; Medication; MedicationAdministration; MedicationAdministrationDosage; MedicationAdministrationPerformer; MedicationBatch; MedicationIngredient; MedicationRequest; MedicationRequestDispenseRequest; MedicationRequestDispenseRequestInitialFill; MedicationRequestSubstitution; MedicationStatement; MedicationStatementAdherence; Membership; Meta; Min; MolecularFormula; MolecularFormulaByMoiety; Money; MultipleBirthBoolean; MultipleBirthInteger; MustSupport; Mutation; Name; Narrative; NewAuthorizedRecipeExecutionReader; NewResolver; Number; NumberOfInstances; NumberOfRepeatsAllowed; NumberOfSeries; Observation; ObservationComponent; ObservationReferenceRange; ObservationTriggeredBy; OccurenceDateTime; OccurrenceDateTime; OccurrenceString; Offset; Offsets; OnsetDateTime; OnsetString; Organization; OrganizationQualification; Pages; ParameterDefinition; Path; Patient; PatientCommunication; PatientContact; PatientInstruction; PatientLink; PerformedDateTime; PerformedString; Period; PeriodMax; PeriodUnit; PostalCode; Practitioner; PractitionerCommunication; PractitionerQualification; PractitionerRole; Preferred; Prefix; PreflightDataframeRecipe; PreviewDataframeRecipe; Priority; Procedure; ProcedureFocalDevice; ProcedurePerformer; Profile; ProjectDataframeDatasets; PublicationDate; PublicationStatus; Quantity; Query; Range; Rank; Ratio; RatioRange; Reason; ReceivedTime; RecipeAuthorizer; RecipeControl; RecipeExecution; RecipeExecutionOutput; RecipeExecutionReader; RecipeExecutionStore; Recorded; RecordedDate; Reference; RegisterDataframeRecipeRevision; RelatedArtifact; RenderedDosageInstruction; Repetitions; Reported; ReportedBoolean; Representation; ResearchStudy; ResearchStudyAssociatedParty; ResearchStudyComparisonGroup; ResearchStudyLabel; ResearchStudyObjective; ResearchStudyOutcomeMeasure; ResearchStudyProgressStatus; ResearchStudyRecruitment; ResearchSubject; ResearchSubjectProgress; Resolver; ResolverConfig; Resource; RunDataframeRecipe; RunFhirDataframe; SampledData; SearchParam; Sequence; SigFormat; Signature; Size; Source; Specimen; SpecimenCollection; SpecimenContainer; SpecimenFeature; SpecimenProcessing; Start; StartDate; Started; State; Status; StatusChanged; StatusDate; SubscriptionTopic; Substance; SubstanceDefinition; SubstanceDefinitionCharacterization; SubstanceDefinitionCode; SubstanceDefinitionMoiety; SubstanceDefinitionMolecularWeight; SubstanceDefinitionName; SubstanceDefinitionNameOfficial; SubstanceDefinitionProperty; SubstanceDefinitionRelationship; SubstanceDefinitionSourceMaterial; SubstanceDefinitionStructure; SubstanceDefinitionStructureRepresentation; SubstanceIngredient; Suffix; System; TargetFormat; TargetNumber; Task; TaskInput; TaskOutput; TaskPerformer; TaskRestriction; Text; Time; TimeDateTime; TimeOfDay; Timing; TimingDate; TimingDateTime; TimingRepeat; Title; TriggerDefinition; Type; UID; URL; Unit; UpperLimit; UsageContext; Use; UserSelected; ValidateDataframeRecipe; Value; ValueBase64Binary; ValueBoolean; ValueCanonical; ValueCode; ValueDate; ValueDateTime; ValueDecimal; ValueID; ValueInstant; ValueInteger; ValueInteger64; ValueMarkdown; ValueOid; ValuePositiveInt; ValueSet; ValueString; ValueTime; ValueURI; ValueURL; ValueUUID; ValueUnsignedInt; Version; VersionID; When; Width; WithOperationContext,generated/fhir; generated/graphql/graph/executor; generated/graphql/graph/model; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataset; internal/fhir/schema,generated/fhir; generated/graphql/graph/model; internal/api/graphql/graph/query; internal/authscope; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec,internal/api/graphql/graph; internal/server,—,github.com/99designs/gqlgen/graphql; github.com/vektah/gqlparser/v2/ast,3 exported interface(s),—,broad surface (12 production files); inspect cohesion; mixed handwritten/generated package (2 generated file(s)); preserve regeneration boundary; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/api/graphql/graph/resolver,inventory complete; semantic disposition pending +internal/api/http,github.com/calypr/loom/internal/api/http,httpapi,transport adapter,"Own shared Fiber server setup, middleware, request IDs, errors, health, and readiness behavior.",doc.go; errors.go; middleware.go; request_id.go; routes.go; server.go,errors_test.go; http_test.go; request_id_test.go,—,—,540,261,15,App; ContextWithRequestID; ErrorResponse; HTTPConfig; HTTPErrorBody; HTTPServer; Health; Liveness; Logger; MapDataframeError; MappedError; NewHTTPServer; Readiness; RequestIDFromContext; StatusForErrorCode,internal/authscope; internal/dataframe/errors,internal/authscope; internal/dataframe/errors,internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/server,—,github.com/gofiber/fiber/v3; github.com/google/uuid,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/api/http,inventory complete; semantic disposition pending +internal/authscope,github.com/calypr/loom/internal/authscope,authscope,domain/library,"Represent request principals and resolve authorized project, generation, and resource scopes.",authentication.go; doc.go; fence_resource_access.go; principal.go; scope_resolver.go,authentication_test.go; scope_test.go,—,—,854,335,34,AllowAllAuthorizer; Authenticate; Authenticator; AuthorizeProject; AuthorizeReadProject; AuthorizeWrite; Authorizer; BasicAuthenticator; BearerTokenAuthenticator; CalyprAuthenticator; Clone; ContextWithPrincipal; FenceUserAccessClient; GetAllowedResources; InvalidateGeneration; InvalidateProject; NewFenceUserAccessClient; NewFenceUserAccessClientWithTTL; NewScopeResolver; NormalizeAuthResourcePath; Permission; Principal; PrincipalFromContext; ReadScope; ReadScopeMode; ResolveReadScope; ResolveReadScopeForGeneration; ResolveWriteScopeForGeneration; ResourceAccessClient; ScopeAuthorizer; ScopeResolver; ScopeResolverConfig; StaticAuthenticator; Unrestricted,internal/catalog; internal/projectid,internal/catalog,internal/api/bulk/load; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer/lifecycle; internal/server,internal/api/graphql/graph; internal/dataframe/execution,—,3 exported interface(s),legacy=3,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/authscope,inventory complete; semantic disposition pending +internal/catalog,github.com/calypr/loom/internal/catalog,catalog,domain/library,"Own persistence-neutral observed FHIR field, pivot, relationship, and capability facts.",auth_scope.go; cache.go; capability_evidence.go; generation.go; helpers.go; limits.go; rebuild.go; relationships.go; types.go; write_profiler.go,auth_scope_test.go; cache_test.go; capability_evidence_test.go; generation_test.go; relationships_test.go; write_profiler_test.go,—,—,1821,749,55,AuthResourcePathOptions; Cache; CapabilityEvidence; CapabilityEvidenceOptions; CapabilityEvidenceReader; DefaultProfileLimits; DiscoverFields; DiscoverReferences; Documents; EffectiveAuthResourcePathsUnrestricted; EvidenceDiagnostic; EvidenceStatus; ExplicitAuthResourcePathsUnrestricted; ExtensionValueObservation; FieldCatalogDocument; FieldEnrichmentDigest; FieldEnrichmentObservation; FieldEnrichmentOptions; FieldEnrichmentResult; HasDatasetGeneration; InvalidRelationship; InvalidateAll; InvalidateProject; Merge; MergeRelationshipCounts; NewCache; NewProfilerForGenerationWithLimits; NewShapePlanCacheWithLimit; NormalizeDatasetGeneration; NormalizeProfileLimits; ObservePayload; PopulatedField; PopulatedFieldOptions; PopulatedReference; PopulatedReferenceOptions; ProfileLimits; Profiler; ReadCapabilityEvidence; RelationshipAuditOptions; RelationshipAuditSummary; RelationshipCatalogDocument; RelationshipCatalogDocuments; RelationshipCountsFromRawEdges; RelationshipKey; RelationshipObservation; RelationshipObservationDigest; RelationshipObservationOptions; RelationshipObservationResult; RelationshipRebuildOptions; RelationshipRebuildSummary; ResourceInventoryDigest; ResourceInventoryObservation; ResourceInventoryOptions; ResourceInventoryResult; ShapePlanCache,internal/fhir/schema,internal/fhir/schema,cmd/arango-fhir-proto; internal/api/graphql/graph/query; internal/authscope; internal/catalog/arango; internal/ingest; internal/server,internal/api/graphql/graph,github.com/bytedance/sonic,1 exported interface(s),legacy=11; migration=1; compatibility=1,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/catalog,inventory complete; semantic disposition pending +internal/catalog/arango,github.com/calypr/loom/internal/catalog/arango,arango,storage adapter,Persist and retrieve catalog facts in ArangoDB.,codec.go; evidence.go; fields.go; relationships.go; resources.go; store.go,store_test.go,—,—,917,269,16,AccumulateRelationshipCatalog; AuditRelationshipEdges; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverReferences; DiscoverRelationshipObservations; DiscoverResourceInventory; New; ReadFieldEnrichment; ReadRelationshipObservations; ReadResourceInventory; RebuildRelationshipCatalog; Store; WriteFieldCatalog; WriteRelationshipCatalog,internal/catalog; internal/fhir/schema; internal/store/arango,internal/catalog; internal/store/arango,cmd/arango-fhir-proto; internal/ingest; internal/server,—,github.com/bytedance/sonic,—,compatibility=1,storage adapter; preserve interface and persisted-data contracts,go test ./internal/catalog/arango,inventory complete; semantic disposition pending +internal/dataframe/compiler,github.com/calypr/loom/internal/dataframe/compiler,compiler,domain/library,"Orchestrate semantic planning, physical lowering, optimization, rendering, and compiled-query assembly.",api.go; clone_helpers.go; compiled_query.go; graph_execution.go; ir_helpers.go; optimizer.go; output.go; recipe_execution.go,filter_semantics_test.go; filter_test.go; generic_physical_plan_test.go; grain_test.go; physical_diagnostics_test.go; physical_lowering_test.go; physical_optimize_test.go; physical_plan_test.go; physical_render_test.go; physical_required_match_test.go; physical_scope_test.go; plan_test_helpers_test.go; selection_semantics_test.go; semantic_validation_test.go,—,—,355,2447,5,CompileRecipeOutputWithPolicy; CompileResolvedGraphQueryWithPolicy; CompileResolvedRecipePlanWithPolicy; CompiledQuery; PublicOutputColumns,internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec,internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/expression; internal/dataframe/semantic; internal/dataframe/spec,cmd/dataframe-profile; internal/api/graphql/graph/query; internal/dataframe/execution,conformance/compiler,—,—,legacy=1,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/compiler,inventory complete; semantic disposition pending +internal/dataframe/compiler/capability,github.com/calypr/loom/internal/dataframe/compiler/capability,capability,domain/library,Probe which dataframe language capabilities the compiler can prove and support.,probe.go,probe_test.go,—,—,622,138,16,CandidateCapability; CandidateRequest; Chart; Filter; Options; ProbeCandidate; ProbeRoot; ProbeTraversal; Rendered; Result; RootCapability; RootRequest; Scope; Traversal; TraversalCapability; TraversalRequest,internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema,internal/authscope; internal/dataframe/recipe; internal/dataframe/spec,internal/server,—,—,—,—,one production importer (internal/server); inspect ownership/inline potential,go test ./internal/dataframe/compiler/capability,inventory complete; semantic disposition pending +internal/dataframe/compiler/ir,github.com/calypr/loom/internal/dataframe/compiler/ir,ir,domain/library,"Define typed physical operations, values, scope proofs, policies, and validation.",clone.go; dataset_generation.go; diagnostics.go; doc.go; navigation.go; physical_operations.go; physical_types.go; physical_validation.go; physical_validation_expr.go; physical_validation_refs.go; physical_validation_shapes.go; physical_values.go; policy.go; prefix.go; scope.go; selector_helpers.go,unnest_test.go,—,—,3739,105,92,AddDecision; CanonicalExecutionPhysicalPlan; ClonePhysicalExpression; ClonePhysicalOperation; ClonePhysicalOperations; ClonePhysicalPlan; ClonePhysicalPredicateExpression; ClonePhysicalSubplan; CompilerPlanDiagnostics; DatasetGenerationBindValue; DecomposePhysicalTraversalPrefixAt; DefaultPhysicalOptimizationPolicy; Error; EstimatePreparedSelectorWork; EstimateTraversalSharingWork; NewPhysicalOptimizationReport; NormalizeDatasetGeneration; PhysicalAggregate; PhysicalAggregateOperation; PhysicalCall; PhysicalCardinality; PhysicalDerivedLet; PhysicalExpression; PhysicalExpressionKind; PhysicalExpressionLet; PhysicalExpressionProjection; PhysicalExtract; PhysicalFilter; PhysicalGraphReturn; PhysicalKeySet; PhysicalKeyedMap; PhysicalLimit; PhysicalLiteral; PhysicalMapReduction; PhysicalNullBehavior; PhysicalObject; PhysicalObjectKeys; PhysicalObjectLookup; PhysicalOperation; PhysicalOperationKind; PhysicalOptimizationDecision; PhysicalOptimizationPolicy; PhysicalOptimizationReport; PhysicalOptimizationRule; PhysicalOptimizationRuleState; PhysicalPathExtend; PhysicalPathNode; PhysicalPathRelationship; PhysicalPathSeed; PhysicalPivotMap; PhysicalPlan; PhysicalPlanDiagnostics; PhysicalPredicate; PhysicalPredicateExpression; PhysicalPredicateKind; PhysicalPreparedField; PhysicalPreparedReference; PhysicalPreparedSet; PhysicalProjection; PhysicalReturn; PhysicalRootScan; PhysicalScopeWindowEnd; PhysicalSelectorExecutionMode; PhysicalSet; PhysicalSetKind; PhysicalSetOutput; PhysicalSetOutputField; PhysicalSetProjection; PhysicalSetProjectionField; PhysicalSlice; PhysicalSort; PhysicalSource; PhysicalSubplan; PhysicalTraversal; PhysicalTraversalDecision; PhysicalTraversalDirection; PhysicalTraversalPrefix; PhysicalTraversalPrefixDecomposition; PhysicalTraversalPrefixError; PhysicalTraversalPrefixRejectionReason; PhysicalTraversalStrategy; PhysicalTraversalSubset; PhysicalUnnest; PhysicalUnnestJoinMode; PhysicalValue; RichConsumerGroup; RichSourceReuse; RuleEnabled; TotalConsumers; Validate; ValidateGenericPhysicalPlanScope; WithRule,internal/dataframe/spec; internal/fhir/schema,—,cmd/dataframe-profile; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/execution; internal/server,conformance/compiler,—,—,legacy=6; compatibility=3,broad surface (16 production files); inspect cohesion,go test ./internal/dataframe/compiler/ir,inventory complete; semantic disposition pending +internal/dataframe/compiler/lower,github.com/calypr/loom/internal/dataframe/compiler/lower,lower,domain/library,Lower logical FHIR dataframe plans into storage-aware physical IR.,auth_scope.go; child_sets.go; doc.go; filter_literal.go; graph_plan.go; helpers.go; plan_orchestration.go; recipe_compile.go; recipe_dynamic_columns.go; recipe_expression.go; recipe_expression_lowering.go; recipe_schema.go; required_match.go; root_projection.go; storage_route.go; traversal.go,graph_plan_test.go; recipe_expression_test.go; recipe_output_test.go; recipe_semantic_path_test.go; traversal_test.go,—,—,2966,1177,12,BuildGenericPhysicalPlanWithPolicy; BuildGraphPhysicalPlan; BuildPhysicalTraversal; CompileResolvedGraphPlan; CompileResolvedRecipePlan; CompiledOutputColumn; CompiledRecipe; CompiledRecipeOutput; DynamicColumnMetadata; LowerRecipeExpression; TraversalLoweringRequest; TraversalLoweringResult,internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema,internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/compiler/render/aql; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema,internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution; internal/server,internal/dataframe/compiler/render/aql,—,—,legacy=3; compatibility=1,broad surface (16 production files); inspect cohesion,go test ./internal/dataframe/compiler/lower,inventory complete; semantic disposition pending +internal/dataframe/compiler/optimize,github.com/calypr/loom/internal/dataframe/compiler/optimize,optimize,domain/library,Apply explainable semantics-preserving rewrites to validated physical IR.,doc.go; optimize.go,optimize_test.go,—,—,410,122,1,OptimizePhysicalPlanWithPolicy,internal/dataframe/compiler/ir,internal/dataframe/compiler/ir,internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution,—,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/compiler/optimize,inventory complete; semantic disposition pending +internal/dataframe/compiler/render/aql,github.com/calypr/loom/internal/dataframe/compiler/render/aql,aql,domain/library,Render validated physical IR as deterministic parameterized AQL.,calls.go; collections.go; doc.go; graph_render.go; navigation_layout.go; navigation_render.go; predicates.go; render.go; selector.go; selectors.go; validation.go; values.go,call_test.go; graph_test.go; unnest_test.go,—,—,2541,321,2,RenderPhysicalPlan; RenderedPhysicalPlan,internal/dataframe/compiler/ir; internal/dataframe/spec,internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/semantic; internal/dataframe/spec,internal/dataframe/compiler; internal/dataframe/compiler/capability,internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/execution,—,—,—,broad surface (12 production files); inspect cohesion,go test ./internal/dataframe/compiler/render/aql,inventory complete; semantic disposition pending +internal/dataframe/errors,github.com/calypr/loom/internal/dataframe/errors,dataframeerrors,domain/library,Define and normalize stable structured dataframe errors for transport presentation.,errors.go,errors_test.go,—,—,429,163,20,AsUserError; Code; Details; Error; ErrorCode; ErrorOption; FieldPath; IsRetryableCode; NewError; Normalize; PublicMessage; Retryable; SanitizePersistedFailure; Unwrap; UserError; WithCause; WithDetails; WithFieldPath; WithRetryable; Wrap,—,—,internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/server,—,—,1 exported interface(s),legacy=1,small shared semantic contract; compare direct callers before inlining; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/errors,inventory complete; semantic disposition pending +internal/dataframe/execution,github.com/calypr/loom/internal/dataframe/execution,execution,domain/library,"Prepare, authorize, execute, explain, and post-process compiled dataframe requests.",control.go; control_explain_types.go; control_types.go; engine.go; execution_service.go; explain.go; helpers.go; named_uuid.go; postquery.go; service.go; types.go,auth_scope_test.go; engine_test.go; execution_test.go; explain_test.go; helpers_test.go; identity_test.go; named_uuid_test.go; postquery_test.go,—,—,1706,771,54,AssessmentFromArango; CompileResolvedBundle; CompiledQuery; CompiledQueryFingerprint; CompilerPlanDiagnostics; Config; Control; DynamicColumnCheck; DynamicDriftError; Engine; Error; Explain; ExplainAssessment; ExplainCollectionScan; ExplainIndexLocation; ExplainIndexSummary; ExplainPhysical; ExplainPlanEstimate; ExplainWarning; Materialize; MaterializeBundle; MaterializeResolvedBundle; MaterializeVersion; New; NewService; OutputRows; OutputStream; PhysicalExplanation; PhysicalOutputExplanation; Preview; PreviewBundle; PreviewOutput; PreviewRequest; PreviewResolvedBundle; PreviewSummary; QueryDiagnostics; QueryRows; ResolutionError; Resolve; ResolveBundle; ResolveVersion; Resolved; Result; RowIdentity; Run; RunCompiled; Service; ServiceConfig; Stream; StreamResult; Streams; Unwrap; Validate; Validation,internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/errors; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/store/arango,internal/authscope; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/errors; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec,internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/explorer/lifecycle; internal/server,conformance/compiler; internal/api/graphql/graph,github.com/google/uuid,—,legacy=2; deprecated=1,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/execution,inventory complete; semantic disposition pending +internal/dataframe/expression,github.com/calypr/loom/internal/dataframe/expression,expression,domain/library,Define and validate the typed backend-neutral expression AST used by recipes.,expression_checking.go; expression_types.go; recipe_adapter.go,expression_test.go; recipe_adapter_test.go,—,—,850,234,26,AllowsMany; Call; Cardinality; Check; CheckedExpression; Constant; Document; DocumentRef; Expression; FromRecipe; FromRecipeInContexts; Function; Literal; NodeKind; NullBehavior; NullType; Optional; Select; SelectorRef; SelectorResolver; String; Type; TypeContext; Valid; Validate; ValueKind,internal/dataframe/recipe,internal/dataframe/recipe,internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/semantic,internal/dataframe/compiler,—,—,compatibility=3,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/expression,inventory complete; semantic disposition pending +internal/dataframe/publication,github.com/calypr/loom/internal/dataframe/publication,publication,domain/library,"Own backend-neutral dataframe publication contracts, bundles, and publication orchestration.",bundle.go; flat_columns.go; runner.go; types.go,flat_columns_test.go; runner_test.go,—,—,776,311,34,BundleCatalog; BundleExecution; BundleIdentity; BundleOutputRecord; BundlePointer; BundleState; Canonical; CanonicalizeLegacy; ColumnProvenance; DataframeSelector; Error; ExactExecutionCatalog; FinalSchemaDigest; FlatColumnName; Key; Limits; LogicalColumn; OutputSchema; OutputStream; PhaseError; PhysicalColumn; PointerName; PublicationIdentity; Publish; PublishedOutput; QualifyFlatRow; Queryable; Result; Selector; Successful; Target; Transaction; Unwrap; WithPhase,internal/dataframe/errors; internal/dataset,internal/dataframe/errors,internal/acceptance; internal/api/bulk/load; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/resolver; internal/dataframe/publication/arango; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/explorer; internal/explorer/lifecycle; internal/server,internal/api/graphql/graph,—,4 exported interface(s),legacy=1; compatibility=1,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/publication,inventory complete; semantic disposition pending +internal/dataframe/publication/arango,github.com/calypr/loom/internal/dataframe/publication/arango,arango,storage adapter,Persist dataframe publication registry and bundle metadata in ArangoDB.,bundle_registry.go; registry.go,bundle_registry_test.go,—,—,286,60,15,AcquireBundleLease; BootstrapSpec; Client; CompareAndSwapPointer; FindExecutionByKey; FindExecutionBySelector; GetExecution; GetPointer; ListExecutions; New; PublishExecution; Registry; ReleaseBundleLease; RenewBundleLease; SaveExecution,internal/dataframe/publication; internal/store/arango,internal/dataframe/publication; internal/store/arango,internal/acceptance; internal/explorer/arango; internal/server,—,—,—,—,storage adapter; preserve interface and persisted-data contracts,go test ./internal/dataframe/publication/arango,inventory complete; semantic disposition pending +internal/dataframe/publication/clickhouse,github.com/calypr/loom/internal/dataframe/publication/clickhouse,clickhouse,storage adapter,Materialize and inspect published dataframe bundles in ClickHouse.,bundle_store.go,clickhouse_bundle_test.go,—,—,828,548,16,Abort; Begin; BundleClickHouseStore; ClickHouseBundleStore; Commit; CreateOutput; ExistingPublishedOutputs; FinalizeSchema; Idempotent; InsertRows; NewBundleStore; Reconcile; SetFinalSchemaDigest; SetOutputMetadata; SupportsObjectValues; WriteBatch,internal/dataframe/errors; internal/dataframe/publication; internal/store/clickhouse,internal/dataframe/publication; internal/dataframe/published; internal/store/clickhouse,internal/server,—,github.com/google/uuid,1 exported interface(s),—,one production importer (internal/server); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/publication/clickhouse,inventory complete; semantic disposition pending +internal/dataframe/published,github.com/calypr/loom/internal/dataframe/published,published,domain/library,"Read, page, aggregate, and export authorized published ClickHouse dataframes.",aggregate_execute.go; aggregate_plan.go; aggregate_sql.go; aggregate_types.go; current.go; errors.go; export.go; page.go; query_helpers.go; read.go; schema.go; types.go,aggregate_clickhouse_integration_test.go; aggregate_execute_test.go; aggregate_plan_test.go; current_test.go; errors_test.go; page_test.go; read_test.go,—,—,2102,555,30,AggregateBatchRequest; AggregateBatchResult; AggregateJob; AggregateJobResult; AggregateResponseMode; AggregateResult; AggregationResult; AggregationSpec; AggregationsRequest; AggregationsResult; ClickHouseQueryer; Column; CurrentProjectDataset; CurrentProjectDatasets; DataframeSelector; ExcludeSelfFilters; ExecuteAggregateBatch; ExportFormat; ExportRequest; Filter; Materialization; Normalize; NormalizeAggregationResults; Page; PageRequest; Reader; Sort; State; Stream; StreamRequest,internal/dataframe/errors; internal/dataframe/publication; internal/dataset,internal/dataframe/errors; internal/dataframe/publication; internal/dataset; internal/store/clickhouse,internal/api/graphql/graph/dataframe; internal/api/graphql/graph/resolver; internal/server,internal/api/graphql/graph; internal/dataframe/publication/clickhouse,—,1 exported interface(s),legacy=3; compatibility=2,broad surface (12 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/published,inventory complete; semantic disposition pending +internal/dataframe/recipe,github.com/calypr/loom/internal/dataframe/recipe,recipe,domain/library,"Define, canonicalize, expand, encode, and validate persistence-neutral recipe documents.",canonical.go; document_types.go; document_validation.go; dynamic_validation.go; fragments.go; json_encoding.go; revisions.go; strict_json.go; validation_rich.go,documentation_test.go; fragments_test.go; types_test.go,—,—,2072,497,51,Aggregate; AggregateOperation; ArrayQuantifier; Bundle; CanonicalJSON; CatalogProjection; Clone; CodeValue; ColumnNaming; Digest; DocumentRef; DynamicColumn; Error; ExpandBundle; ExpandExpression; ExpandFragments; Expansion; Explain; Explanation; Expression; ExtensionColumn; ExtensionColumnMapping; Field; Filter; FilterOperator; FilterValue; FilterValueKind; Fragment; FragmentLibrary; Identity; MarshalJSON; Normalized; Output; OutputExplanation; Parse; Pivot; PivotDiscovery; RecipeRevision; RepresentativeSlice; RevisionStore; RootColumnNaming; RuntimeBindings; SortedNames; Traversal; TraversalColumnNaming; TraversalMatchMode; UnmarshalJSON; Valid; Validate; ValidationError; ValueMode,generated/fhir; internal/authscope,—,cmd/dataframe-profile; cmd/explorer-config-v2-convert; conformance/compiler; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution; internal/dataframe/expression; internal/dataframe/recipe/exec; internal/dataframe/recipe/exec/arango; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/server,internal/explorer/arango,—,1 exported interface(s),legacy=3; compatibility=5,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/recipe,inventory complete; semantic disposition pending +internal/dataframe/recipe/exec,github.com/calypr/loom/internal/dataframe/recipe/exec,exec,domain/library,Define durable recipe-version storage contracts and immutable registration behavior.,persistent.go,persistent_test.go,—,—,99,102,5,Entry; PersistentRegistry; Reader; RegisterVersion; Store,internal/dataframe/recipe,internal/dataframe/recipe,internal/api/graphql/graph/resolver; internal/dataframe/execution; internal/dataframe/recipe/exec/arango; internal/server,—,—,2 exported interface(s),—,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/recipe/exec,inventory complete; semantic disposition pending +internal/dataframe/recipe/exec/arango,github.com/calypr/loom/internal/dataframe/recipe/exec/arango,arango,storage adapter,Persist recipe versions and execution-use records in ArangoDB.,registry.go; revisions.go,registry_test.go; revisions_test.go,—,—,291,350,15,BootstrapSpec; Client; Get; List; LoadRecipe; LoadRecipeVersion; New; NewRevisionRegistry; RecipeVersionUsed; Register; Registry; ReplaceRecipeVersion; RevisionBootstrapSpec; RevisionRegistry; SaveRecipe,internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/store/arango,internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/store/arango,internal/server,—,—,—,legacy=1,one production importer (internal/server); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts,go test ./internal/dataframe/recipe/exec/arango,inventory complete; semantic disposition pending +internal/dataframe/recipe/schema,github.com/calypr/loom/internal/dataframe/recipe/schema,schema,domain/library,Resolve catalog-backed recipe declarations into concrete validated schemas.,discovery.go; extension_columns.go; resolve.go,resolve_test.go,—,—,875,270,8,Discovery; ExtensionValueObservation; FieldCandidate; Fields; Resolve; Resolved; Scope; SourceSnapshot,internal/dataframe/recipe; internal/fhir/schema,internal/dataframe/recipe; internal/dataframe/semantic,internal/api/graphql/graph/query; internal/server,—,—,1 exported interface(s),legacy=2,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataframe/recipe/schema,inventory complete; semantic disposition pending +internal/dataframe/semantic,github.com/calypr/loom/internal/dataframe/semantic,semantic,domain/library,Convert validated dataframe requests and recipes into backend-neutral logical FHIR plans.,doc.go; dynamic_schema.go; execution_context.go; recipe_filter.go; recipe_plan_build.go; recipe_plan_output.go; recipe_plan_types.go; recipe_projection.go; recipe_rich_shaping.go; recipe_traversal.go; resolved_plan.go; selection_semantics.go; selector_spec.go; semantic_nodes.go; semantic_validation.go,dynamic_schema_test.go; recipe_plan_test.go; recipe_rich_shaping_test.go; resolved_plan_test.go,—,—,2385,402,36,BuildRecipePlan; Candidate; Column; DynamicSpec; ExecutionContext; ExpansionExplanation; Explain; ExpressionExplanation; FallbackSelectors; Freeze; FrozenSchema; LowerRecipeFilters; LowerRecipeFiltersForAlias; NormalizeRecipeMatchMode; OutputPlan; OutputPlanExplanation; PrimarySelector; RecipePlan; RecipePlanExplanation; ResolveRecipePlan; ResolveSemanticField; ResolvedColumn; ResolvedRecipePlan; SelectionSemanticSpec; SemanticAggregate; SemanticDynamicMap; SemanticExpression; SemanticField; SemanticNode; SemanticPivot; SemanticSlice; SemanticUnnest; UnnestExplanation; UnnestJoinMode; Validate; ValidateSemanticGraph,internal/authscope; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/spec; internal/fhir/schema,internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/spec,cmd/dataframe-profile; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution,conformance/compiler; internal/dataframe/compiler/render/aql; internal/dataframe/recipe/schema,—,—,legacy=2; compatibility=1,broad surface (15 production files); inspect cohesion,go test ./internal/dataframe/semantic,inventory complete; semantic disposition pending +internal/dataframe/spec,github.com/calypr/loom/internal/dataframe/spec,spec,domain/library,"Define the backend-independent dataframe request, filter, grain, relationship, and selector language.",doc.go; filter.go; filter_semantics.go; grain.go; relationship_match.go; selector_validation.go; selectors.go,—,—,—,665,0,30,AllowsMany; ArrayQuantifier; Cardinality; Clone; CodeValue; ContainsFilter; DefaultRowIdentity; ExpandsRows; FilterOperator; FilterValue; FilterValueKind; InferRowGrain; IsRequired; OperatorSupportsKind; ParseSelector; ProjectionMode; Required; RootResourceForGrain; RowGrain; RowIdentity; Selector; SelectorCardinality; SelectorStep; TraversalMatchMode; TypedFilter; Valid; Validate; ValidateProjection; ValidateRootGrain; ValidateTypedFilterForResource,generated/fhir; internal/fhir/schema,—,internal/api/graphql/graph/query; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/execution; internal/dataframe/semantic; internal/explorer/compilation; internal/server,conformance/compiler; internal/api/graphql/graph/resolver,—,—,legacy=1; compatibility=2,no automatic consolidation signal; perform semantic review,go test ./internal/dataframe/spec,inventory complete; semantic disposition pending +internal/dataset,github.com/calypr/loom/internal/dataset,dataset,domain/library,"Define immutable dataset generation manifests, releases, active pointers, and lifecycle validation.",active_resolver.go; dataframe_selector.go; doc.go; manifest.go; ref.go; release.go; schema.go; validation.go,active_resolver_test.go; manifest_test.go; memory_store_test.go; ref_test.go; release_test.go; schema_test.go,README.md,—,562,451,32,Activate; ActivationRequest; Active; ActiveRelease; ActiveResolver; ContractVerification; Create; DataframeSelector; Error; IsStaged; Key; Manifest; ManifestReader; NewManifest; NewRef; NewSchemaSnapshot; ProjectRelease; PublicationVerification; PublicationVerifier; Ref; ReleasePublication; ReleaseRepository; ReleaseRequirementsError; ReleaseService; ResolveActive; SchemaSnapshot; State; Transition; Unwrap; Valid; Validate; ValidateGeneration,—,—,cmd/arango-fhir-proto; internal/api/bulk/load; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/publication; internal/dataframe/published; internal/dataset/arango; internal/explorer; internal/explorer/arango; internal/explorer/lifecycle; internal/ingest; internal/server,internal/api/graphql/graph,—,4 exported interface(s),legacy=1; compatibility=1,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/dataset,inventory complete; semantic disposition pending +internal/dataset/arango,github.com/calypr/loom/internal/dataset/arango,arango,storage adapter,Persist immutable dataset generation lifecycle state in ArangoDB.,collections.go; doc.go; document_codec.go; manifest_lifecycle.go; release_store.go,store_test.go,—,—,731,311,14,Activate; ActiveDocumentKey; BootstrapSpec; CollectionSpecs; CompareAndSwapActivateRelease; CreateManifest; ManifestDocumentKey; New; ReadActiveRelease; ReadManifest; ResolveActiveManifest; SaveRelease; Store; TransitionManifest,internal/dataset; internal/store/arango,internal/dataset; internal/store/arango,cmd/arango-fhir-proto; internal/explorer/arango; internal/ingest; internal/server,—,—,—,compatibility=2,storage adapter; preserve interface and persisted-data contracts,go test ./internal/dataset/arango,inventory complete; semantic disposition pending +internal/explorer,github.com/calypr/loom/internal/explorer,explorer,domain/library,"Define Explorer aggregate identities, configs, revisions, receipts, output contracts, stores, and core service operations.",authoring_errors.go; canonical_json.go; compilation.go; compilation_receipt.go; config_v2.go; identity.go; output_contract.go; service.go; state_v1.go; store.go; types.go; viewer_projection.go,compilation_receipt_test.go; output_contract_test.go; repository_revision_id_test.go; types_test.go,—,—,1727,337,69,ActivateRepositoryGeneration; ActiveRevision; ApplyWorkspaceCommands; AuthoringDiagnostic; AuthoringError; BuildViewerProjection; CompilationArtifactDigest; CompilationKey; CompilationReceipt; CompilationReceiptForExplorer; CompilationWarning; ConfigAction; ConfigChart; ConfigColumn; ConfigExplorer; ConfigFilter; ConfigManagementForID; ConfigTable; ConfigV2; ConfigView; CreateInteractiveFrom; DatasetMetadata; DatasetOutput; DecodePublicOutputContracts; Diagnostic; EmittedColumn; Error; Explorer; ExplorerRuntimeBindingV1; ExplorerRuntimeColumnV1; ExplorerRuntimeOutputV1; ExplorerRuntimeSchemaV1; ExplorerRuntimeTableColumnV1; ExplorerRuntimeTableV1; ExplorerRuntimeV1; ExplorerStateV1; ExplorerStateV1Active; ExplorerStateV1Draft; ExplorerStateV1Generated; ExplorerSummaryV1; FailRevision; FileActions; Get; IdentityMapping; List; LoadExplorerState; ManagementMode; Materialization; NewService; Output; PublicOutputColumn; PublicOutputContract; PublicOutputContracts; PublicationMetadata; PublishAuthoring; ReceiptID; RepositoryRevisionID; Revision; RevisionStatus; Service; SharedFilter; StableExplorerID; Store; StoreCompilationReceipt; Unwrap; UpsertRepositoryV2; Validate; ValidateAgainst; ValidateID,internal/dataframe/publication; internal/dataframe/recipe; internal/dataset; internal/explorer/authoringv2; internal/projectid,internal/dataframe/recipe,cmd/explorer-config-v2-convert; internal/explorer/arango; internal/explorer/compilation; internal/explorer/lifecycle; internal/server,—,—,1 exported interface(s),legacy=11; migration=1; compatibility=2,broad surface (12 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/explorer,inventory complete; semantic disposition pending +internal/explorer/arango,github.com/calypr/loom/internal/explorer/arango,arango,storage adapter,"Persist Explorer drafts, revisions, receipts, publications, capabilities, and repository state in ArangoDB.",activation.go; capability_snapshot_store.go; codec.go; collections.go; drafts.go; explorers.go; publication.go; receipts.go; repository.go; revisions.go; store.go,capability_snapshot_store_test.go; collections_test.go; receipt_store_test.go; repository_test.go; store_test.go,—,—,917,419,22,ActivateRepositoryGeneration; BootstrapSpec; CapabilitySnapshotCollectionSpec; CapabilitySnapshotStore; CollectionSpecs; Create; FailRevision; Get; GetByIdentity; GetByToken; GetCompilationReceiptForExplorer; GetRevision; InsertCompilationReceipt; InsertRevision; List; MigrateLegacyRepositoryConfigs; New; NewCapabilitySnapshotStore; PublishAuthoring; Put; SaveDraft; Store,internal/dataframe/publication/arango; internal/dataset; internal/dataset/arango; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/projectid; internal/store/arango,internal/dataframe/recipe; internal/explorer; internal/explorer/capability; internal/store/arango,internal/server,—,—,—,legacy=11; migration=4; compatibility=1,one production importer (internal/server); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts,go test ./internal/explorer/arango,inventory complete; semantic disposition pending +internal/explorer/authoringv2,github.com/calypr/loom/internal/explorer/authoringv2,authoringv2,domain/library,"Define, validate, canonicalize, and mutate the V2 Explorer Builder authoring workspace.",canonical.go; commands.go; semantic_types.go; types.go; validation.go,commands_test.go; types_test.go; workspace_test.go,—,—,1567,341,34,Action; ActionColumn; ApplyCommands; ApplyCommandsRequest; ApplyCommandsResponse; BuilderState; CanonicalJSON; CatalogCandidate; CatalogDiagnostic; CatalogEdge; CatalogNode; CatalogSnapshot; ChartPresentation; Column; ColumnSource; Command; CommandResult; DecodeWorkspace; Digest; Document; ExplorerMetadata; FileActions; FilterPresentation; FixedFilter; NormalizePresentationOrders; Output; RouteNode; RoutePolicy; SharedFilterBinding; Tab; TablePresentation; Validate; ValidateForPublication; Workspace,—,—,cmd/explorer-config-v2-convert; generated/loomapi; internal/explorer; internal/explorer/arango; internal/explorer/compilation; internal/explorer/lifecycle; internal/server,internal/acceptance,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/explorer/authoringv2,inventory complete; semantic disposition pending +internal/explorer/capability,github.com/calypr/loom/internal/explorer/capability,capability,domain/library,Build the immutable compiler-backed capability contract exposed to Explorer Builder.,builder.go; domain.go; repository.go,builder_test.go,—,—,801,173,34,Allows; Build; Builder; Candidate; CandidateProof; CanonicalPayload; ChartAggregation; Clone; CompilerCallbacks; Diagnostic; Edge; EdgeProof; Evidence; FieldObservation; FilterOperator; IsUsable; NewBuilder; NewSnapshot; Node; NodeProof; Operation; Policy; ProjectionMode; ProjectionPolicy; RelationshipObservation; Repository; ResourceObservation; RoutePolicy; Snapshot; SnapshotIdentity; Status; Usable; ValidateSnapshot; ValidateToken,—,—,internal/explorer/arango; internal/explorer/compilation; internal/explorer/lifecycle; internal/server,—,—,1 exported interface(s),—,runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/explorer/capability,inventory complete; semantic disposition pending +internal/explorer/compilation,github.com/calypr/loom/internal/explorer/compilation,compilation,domain/library,Translate validated Explorer authoring intent into native recipe and compilation artifacts.,compile.go; semantic_compile.go,translate_test.go; workspace_test.go,—,—,716,192,8,Compile; CompileWorkspace; Error; PresentationColumn; PresentationConfig; Result; Unwrap; WorkspaceResult,internal/dataframe/recipe; internal/dataframe/spec; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/fhir/schema,internal/dataframe/recipe; internal/explorer/authoringv2; internal/explorer/capability,internal/explorer/lifecycle; internal/server,—,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/explorer/compilation,inventory complete; semantic disposition pending +internal/explorer/lifecycle,github.com/calypr/loom/internal/explorer/lifecycle,lifecycle,domain/library,"Coordinate transport-neutral Explorer authoring, preview, publication, query, and policy operations.",authoring.go; errors.go; policy.go; preview.go; publish.go; query.go; repository_publish.go; service.go; types.go,service_test.go,—,—,1019,298,42,ApplyCommands; AuthorizedCapability; Builder; BuilderRequest; Capability; CapabilityResolver; Clone; CompileReceiptRequest; Config; Create; CreateRequest; Error; ErrorClass; Execution; ExecutionOutput; GenerationValidator; Get; List; ListResult; New; Preview; PreviewRequest; PreviewResult; Publish; PublishRepository; PublishRequest; PublishResult; ReceiptCompiler; ReceiptMaterializer; ReceiptPreviewer; ReceiptReader; Reconcile; ReconcileRequest; ReleaseActivator; ReleasePreparer; RepositoryPublishRequest; RepositoryPublishResult; Service; Suggestions; SuggestionsRequest; SuggestionsResult; Unwrap,internal/authscope; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/recipe; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/projectid,internal/authscope; internal/dataframe/execution; internal/dataframe/recipe; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability,internal/server,—,—,—,legacy=6,one production importer (internal/server); inspect ownership/inline potential,go test ./internal/explorer/lifecycle,inventory complete; semantic disposition pending +internal/fhir/schema,github.com/calypr/loom/internal/fhir/schema,schema,domain/library,"Add handwritten selector, traversal, pivot, and compiler semantics to generated FHIR metadata.",compiler_semantics.go; fields.go; generated_metadata.go; pivots.go; selectors.go; terminal_metadata.go; traversals.go,compiler_semantics_test.go; fields_test.go; pivots_test.go; root_resources_test.go; selectors_test.go; terminal_metadata_test.go; traversals_test.go,—,—,974,374,39,CanonicalPath; CanonicalResourceType; CanonicalizePath; ChoiceValueSelectorOptions; CompilerTraversal; ConcreteResourceType; ContainsFilter; DefaultPivotSpec; DefinitionExists; FieldKind; FieldPredicateSpec; FieldSelectorSpec; FieldSelectorSpecFromPath; FieldSemantics; FieldSpec; FieldsForResource; HasResource; LookupField; LookupTraversal; ParseSelector; PivotSpec; PrimitiveKind; ResolveCompilerTraversal; ResolveFieldSemantics; ResolvePath; ResolveTerminalScalarMetadata; ResolvedPath; ResolvesToCodeableConcept; ResourceSpec; ResourceTypes; Selector; SelectorExpression; SelectorFromField; SelectorStep; TerminalScalarMetadata; TraversalDirection; TraversalMultiplicity; TraversalSpec; ValidatePivotSelectors,generated/fhirschema,—,generated/fhir; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/catalog; internal/catalog/arango; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer/compilation; internal/ingest,conformance/compiler,—,—,compatibility=1,no automatic consolidation signal; perform semantic review,go test ./internal/fhir/schema,inventory complete; semantic disposition pending +internal/ingest,github.com/calypr/loom/internal/ingest,ingest,domain/library,"Preflight and load FHIR NDJSON into generation-scoped Arango vertices, edges, and catalog facts.",backend.go; doc.go; documents.go; files.go; generated_load.go; generation_identity.go; generation_orchestration.go; generation_workers.go; load_options.go; preflight.go; progress.go; resource_load.go; row_builder.go; source_schema.go,backend_test.go; bench_test.go; documents_test.go; file_pipeline_test.go; files_test.go; generated_load_test.go; generation_identity_test.go; generation_load_test.go; integration_test.go; load_options_test.go; preflight_test.go; row_builder_test.go; source_schema_test.go,—,—,2302,1614,31,ActivationOutcomeError; Build; DiscoverNDJSON; EdgeDocument; EdgeFromGrip; Error; EventSink; GeneratedRowBuilder; GenerationLoadIncompleteError; GenericRowBuilder; IngestionMode; Load; LoadOptions; LoadSummary; NewGeneratedRowBuilder; NewGenericRowBuilder; OpenLineScanner; PreflightError; PreflightFiles; PreflightIssue; PreflightReport; PreflightResource; ResourceTypeFromPath; RowBuilder; RowErrorSample; SanitizeKey; SecondsSince; Unwrap; Valid; VertexDocument; VertexFromFHIRWithExtra,generated/fhir; internal/catalog; internal/catalog/arango; internal/dataset; internal/dataset/arango; internal/fhir/schema; internal/store/arango,generated/fhir; internal/catalog; internal/catalog/arango; internal/dataset; internal/fhir/schema; internal/store/arango,cmd/arango-fhir-proto; internal/api/bulk/load; internal/server,—,github.com/bmeg/jsonschema/v6; github.com/bmeg/jsonschemagraph/graph; github.com/bmeg/jsonschemagraph/model; github.com/bmeg/jsonschemagraph/util; github.com/bytedance/sonic,1 exported interface(s),legacy=1; migration=1; compatibility=1,broad surface (14 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/ingest,inventory complete; semantic disposition pending +internal/projectid,github.com/calypr/loom/internal/projectid,projectid,domain/library,Define canonical and legacy-compatible public project identity normalization.,projectid.go,projectid_test.go,—,—,51,18,2,Canonical; Legacy,—,—,internal/authscope; internal/explorer; internal/explorer/arango; internal/explorer/lifecycle; internal/server,—,—,—,legacy=5; compatibility=1,small shared semantic contract; compare direct callers before inlining,go test ./internal/projectid,inventory complete; semantic disposition pending +internal/server,github.com/calypr/loom/internal/server,server,domain/library,"Compose storage, domain services, generated OpenAPI handlers, GraphQL adapters, and HTTP routes.",auth_wiring.go; explain_wiring.go; explorer_authoring_http.go; explorer_capability.go; explorer_config_route.go; explorer_lifecycle_helpers.go; explorer_lifecycle_wire.go; explorer_preview_response.go; explorer_preview_security.go; explorer_publication.go; explorer_receipt_contract.go; openapi_data_routes.go; openapi_explorer_authoring_direct.go; openapi_explorer_conversion.go; openapi_explorer_direct.go; openapi_explorer_routes.go; openapi_graphql_routes.go; openapi_recipe_execution.go; openapi_routes.go; options.go; recipe_authorization.go; recipe_discovery.go; recipe_publication.go; release_verifier.go; routes.go; server.go,config_test.go; explorer_capability_test.go; explorer_config_route_test.go; explorer_preview_response_test.go; explorer_preview_security_test.go; explorer_receipt_contract_test.go; explorer_state_route_test.go; http_test_helpers_test.go; openapi_explorer_direct_test.go; openapi_recipe_execution_test.go; openapi_route_inventory_test.go; openapi_test_helpers_test.go; recipe_discovery_test.go; release_verifier_test.go; routes_test.go; wiring_test.go,—,—,3673,1977,49,ActivateDatasetGeneration; ApplyExplorerBuilderCommands; AuthConfig; AuthorizeRead; AuthorizeWrite; BasicAuthConfig; CalyprAuthConfig; ClickHouseConfig; Config; CreateDatasetGeneration; CreateExplorer; DataframerConfig; DefaultConfig; Error; ExecuteDataframeGraphQL; ExecuteGraphQL; Fields; Finish; GetApolloSandbox; GetExplorer; GetExplorerAuthoringCapability; GetExplorerBuilder; GetGraphQLPlayground; GetHealth; GetLiveness; GetReadiness; GetRecipeExecution; HTTPRoutes; ListExplorers; LoadConfig; PreviewExplorer; ProbeCandidate; ProbeEdge; ProbeNode; PublishExplorer; PublishRepositoryExplorerConfig; ReconcileExplorerBuilder; Resolve; ResolveForCompilation; ResolveForExecution; ResolveToken; Run; SearchExplorerCandidates; ServerConfig; Unwrap; Validate; VerifyPublication; Visit; Write,generated/loomapi; internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/authscope; internal/catalog; internal/catalog/arango; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/publication/arango; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/exec/arango; internal/dataframe/recipe/schema; internal/dataframe/spec; internal/dataset; internal/dataset/arango; internal/explorer; internal/explorer/arango; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/ingest; internal/projectid; internal/store/arango; internal/store/clickhouse,generated/loomapi; internal/api/bulk/load; internal/api/graphql/graph/resolver; internal/api/http; internal/authscope; internal/catalog; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/schema; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/ingest,.; cmd/arango-fhir-server,—,github.com/gofiber/fiber/v3; go.yaml.in/yaml/v3; golang.org/x/sync/singleflight,—,legacy=35; migration=1,broad surface (26 production files); inspect cohesion,go test ./internal/server,inventory complete; semantic disposition pending +internal/store/arango,github.com/calypr/loom/internal/store/arango,arango,storage adapter,"Provide the low-level typed ArangoDB client, query streaming, profiling, and Explain assessment boundary.",client.go; explain.go; explain_assessment.go; explain_client.go; options.go; profile.go; profile_client.go; types.go,client_queryrows_test.go; explain_assessment_test.go; explain_client_test.go; explain_test.go; profile_client_test.go; profile_integration_test.go; profile_test.go,—,—,1118,860,53,AQLExecutor; AssessExplainResult; BatchInserter; Bootstrap; BootstrapSpec; Client; Close; CollectionExists; CollectionSpec; ConnectionOptions; ExecuteAQL; Explain; ExplainAssessment; ExplainCollection; ExplainCollectionScan; ExplainIndex; ExplainIndexLocation; ExplainIndexSummary; ExplainIndexUse; ExplainIndexes; ExplainNode; ExplainOptions; ExplainPlan; ExplainPlanEstimate; ExplainRequest; ExplainResult; ExplainStats; ExplainWarning; InsertBatchRaw; Open; OptimizerOptions; ParseExplainResult; ParseProfileResult; Profile; ProfileExtra; ProfileNode; ProfileNodeGroup; ProfileNodeSummary; ProfileOptions; ProfilePhases; ProfileRequest; ProfileResult; ProfileStats; ProfileSummary; QueryRows; RowBatchClient; RowQueryer; RowVisitor; SummarizeProfile; TransactionCollections; TransactionFunc; UnmarshalJSON; WithTransaction,—,—,cmd/arango-fhir-proto; cmd/dataframe-profile; internal/acceptance; internal/catalog/arango; internal/dataframe/execution; internal/dataframe/publication/arango; internal/dataframe/recipe/exec/arango; internal/dataset/arango; internal/explorer/arango; internal/ingest; internal/server,conformance/compiler,github.com/arangodb/go-driver/v2/arangodb; github.com/arangodb/go-driver/v2/arangodb/shared; github.com/arangodb/go-driver/v2/connection; github.com/arangodb/go-driver/v2/utils,4 exported interface(s),—,storage adapter; preserve interface and persisted-data contracts; runtime/build signals present; static importer counts are insufficient for deletion,go test ./internal/store/arango,inventory complete; semantic disposition pending +internal/store/clickhouse,github.com/calypr/loom/internal/store/clickhouse,clickhouse,storage adapter,Provide the low-level typed ClickHouse connection and database-management boundary.,client.go,client_test.go; integration_test.go,—,—,557,256,15,AddColumn; Client; Close; Column; CreateTable; DropColumns; DropTable; EnsureDatabase; InsertRows; New; Options; Ping; QueryRowsArgs; QueryRowsArgsVisit; VerifyOutput,—,—,internal/acceptance; internal/dataframe/publication/clickhouse; internal/server,internal/dataframe/published,github.com/ClickHouse/clickhouse-go/v2; github.com/ClickHouse/clickhouse-go/v2/lib/driver,—,—,storage adapter; preserve interface and persisted-data contracts,go test ./internal/store/clickhouse,inventory complete; semantic disposition pending diff --git a/docs/PACKAGE_AUDIT.md b/docs/PACKAGE_AUDIT.md new file mode 100644 index 00000000..c9088815 --- /dev/null +++ b/docs/PACKAGE_AUDIT.md @@ -0,0 +1,118 @@ +# Package audit lookup table + +This is the repository-wide evidence index for package-boundary decisions. It is generated from the current Go source tree by `scripts/package_audit.py`; package responsibilities are curated in that script, while files, imports, importers, line counts, exported symbols, and signals are measured. The sortable source of record is [`PACKAGE_AUDIT.csv`](PACKAGE_AUDIT.csv). + +A row marked as a consolidation candidate is **not** deletion authorization. Before moving or deleting code, read the package and its tests, repeat call-site searches, check interface/runtime registration, confirm compatibility constraints, and run the listed focused test plus affected importer tests. + +## Snapshot + +- Packages: 58 +- Production Go files: 340 +- Test Go files: 184 +- Production Go lines: 340,906 +- Test Go lines: 22,889 +- Package kinds: conformance=1, domain/library=29, executable=9, generated=5, storage adapter=8, transport adapter=6 +- Non-entrypoint handwritten packages with no production importer: — +- Non-entrypoint handwritten packages with one production importer: internal/acceptance; internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/dataframe/compiler/capability; internal/dataframe/publication/clickhouse; internal/dataframe/recipe/exec/arango; internal/explorer/arango; internal/explorer/lifecycle + +## How to use the table + +- Start with **Responsibility**, **Depends on**, and **Imported by** to judge whether the boundary has a distinct job and correct dependency direction. +- Use **Contents** and **API** to estimate the change surface; the API list is a top-level function/type estimate, not a compatibility guarantee. +- Treat **Decision evidence** as a review queue. It reports structural facts and prompts, not a final keep/merge/delete verdict. +- Treat generated, persisted, migration, compatibility, `init`, build-tag, embedded-data, and interface signals as deletion blockers until verified. +- The full CSV also records test-only dependencies/importers, external dependencies, generated files, compatibility marker counts, and exact focused test commands. + +## Regression-safety workflow + +Every package decision uses a before/after verification bracket. Use `regression-guard:verify-change` as the controller when available. The table identifies the initial change surface; pstack's `blast-radius` skill checks the contracts that static imports miss. + +1. Invoke `regression-guard:verify-change`, then run `pstack:blast-radius` for the proposed package move, merge, or deletion. Name the behavior that must remain true and require an executable proof. +2. Before editing, run `python3 scripts/package_audit_verify.py `. This tests the package and all direct production and test-only importers recorded in the CSV. +3. Make one package-boundary change. Do not batch independent package decisions. +4. Run the same focused command after the change. For a move, merge, or deletion, also run `python3 scripts/package_audit_verify.py --full`. +5. Regenerate this table with `python3 scripts/package_audit.py`, inspect the changed dependency/importer rows, and record the decision and proof before starting the next package. + +A green test run proves only the exercised contracts. Runtime registration, generated-code ownership, persisted data, wire formats, external consumers, and deployment behavior still require the specific proof identified by the blast-radius review. + +## Completed decisions + +These records preserve verified package-boundary decisions for both live packages and packages removed from the current inventory. The sortable source of record is [`PACKAGE_AUDIT_DECISIONS.csv`](PACKAGE_AUDIT_DECISIONS.csv). + +| Source | Disposition | Destination | Rationale | Contracts preserved | Verification | Status | +| --- | --- | --- | --- | --- | --- | --- | +| internal/api/graphql | merged | internal/api/graphql/graph | one production importer and error presentation is transport policy owned by graph | public error codes/messages/redaction/request IDs/cause/path/location | focused graph+server tests; full repository verification; extension-map isolation regression | complete | +| internal/api/bulk/load | keep | internal/api/bulk/load | owns multipart staging, authorization, ingest error translation, generation reuse, and release activation apart from generated OpenAPI response mapping | multipart fields; JSON result fields; auth scope; stable error codes; activation safety | focused load+server tests; full repository verification | complete | +| internal/api/graphql/graph | keep | internal/api/graphql/graph | owns gqlgen handler construction, GraphQL error presentation, and response-status adaptation apart from server composition and generated execution | GraphQL error shape; request IDs; partial-data HTTP 200; failure status lifting; playground and sandbox handlers | focused graph+server tests; full repository verification; extension-map isolation regression | complete | + +## Complete package table + +| Location | Kind | Responsibility | Contents | LOC (prod/test) | API | Depends on | Imported by (production) | Decision evidence | +| --- | --- | --- | --- | ---: | --- | --- | --- | --- | +| `.` | executable | Start the default Loom server process through the internal server composition root. | P: main.go; T: —; Other: go.mod | 7/0 | 0: — | internal/server | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/arango-fhir-proto` | executable | Provide operator commands for FHIR loading, catalog diagnostics, generation repair, and activation. | P: main.go; T: main_test.go; Other: — | 594/166 | 0: — | internal/catalog; internal/catalog/arango; internal/dataset; internal/dataset/arango; internal/ingest; internal/store/arango | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/arango-fhir-server` | executable | Start the explicit Loom HTTP server binary. | P: main.go; T: —; Other: — | 7/0 | 0: — | internal/server | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/dataframe-profile` | executable | Run and compare developer-facing dataframe profiling workloads. | P: main.go; T: —; Other: — | 322/0 | 0: — | conformance/compiler; generated/graphql/graph/model; internal/api/graphql/graph/query; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/recipe; internal/dataframe/semantic; internal/store/arango | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/dataframe-query` | executable | Execute a checked-in GraphQL dataframe request and report response timing and diagnostics. | P: main.go; T: main_test.go; Other: — | 241/81 | 0: — | — | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/explorer-config-v2-convert` | executable | Convert legacy Explorer configuration input into the V2 authoring representation offline. | P: main.go; T: —; Other: — | 377/0 | 0: — | internal/dataframe/recipe; internal/explorer; internal/explorer/authoringv2 | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/generate` | executable | Generate FHIR models, schema metadata, validation, extraction, and GraphQL artifacts from source schemas. | P: extraction.go; graphql.go; helpers.go; main.go; metadata.go; model.go; names.go; resources.go; schema.go; validation.go; T: extraction_test.go; graphql_test.go; metadata_test.go; model_test.go; resources_test.go; root_test.go; schema_fixture_test.go; Other: fhir_helpers.go.tmpl | 1,668/394 | 9: Definition; EdgeDocument; Link; Property; Result; Schema; TargetHints; TargetSchema; TemplatePointers | — | — | executable boundary; compare orchestration with sibling commands before combining; runtime/build signals present; static importer counts are insufficient for deletion | +| `cmd/gqlgenfix` | executable | Post-process gqlgen output to enforce Loom-specific generated-code invariants. | P: main.go; T: main_test.go; Other: — | 108/40 | 1: MarshalJSON | — | — | executable boundary; compare orchestration with sibling commands before combining | +| `cmd/loom-acceptance` | executable | Run the locked real-data acceptance scenario and emit machine-readable verification artifacts. | P: main.go; T: —; Other: — | 119/0 | 0: — | internal/acceptance | — | executable boundary; compare orchestration with sibling commands before combining | +| `conformance/compiler` | conformance | Load and exercise the canonical dataframe compiler oracle corpus for conformance and performance checks. | P: types.go; T: benchmark_test.go; corpus_coverage_test.go; gdc_fixture_test.go; generated_semantics_coverage_test.go; physical_plan_cost_test.go; policy_ablation_test.go; run_test.go; schema_roots_test.go; types_test.go; Other: — | 111/1,214 | 4: Expected; Fixture; LoadDir; Validate | internal/dataframe/recipe | cmd/dataframe-profile | non-runtime conformance boundary | +| `generated/fhir` | generated | Expose generated FHIR resource models and their extraction, validation, and GraphQL helpers. | P: extract.go; graphql.go; helpers.go; model.go; resources.go; validate.go; T: —; Other: — | 72,020/0 | 155: Address; Age; Annotation; Attachment; Availability; AvailabilityAvailableTime; AvailabilityNotAvailableTime; BodyStructure; BodyStructureIncludedStructure; BodyStructureIncludedStructureBodyLandmarkOrientation; BodyStructureIncludedStructureBodyLandmarkOrientationDistanceFromLandmark; CodeableConcept; … (+143) | internal/fhir/schema | generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/dataframe/recipe; internal/dataframe/spec; internal/ingest | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | +| `generated/fhirschema` | generated | Expose generated raw FHIR schema metadata tables. | P: generated.go; T: —; Other: — | 48,359/0 | 3: Definition; Property; Traversal | — | internal/fhir/schema | generated boundary; change its source/generator, not emitted code | +| `generated/graphql/graph/executor` | generated | Provide gqlgen-generated GraphQL executable schema and execution plumbing. | P: fhir_schema.generated.go; prelude.generated.go; root_.generated.go; schema.generated.go; T: —; Other: — | 155,252/0 | 328: Address; AddressResolver; Age; AgeResolver; Annotation; AnnotationResolver; Attachment; AttachmentResolver; Availability; AvailabilityAvailableTime; AvailabilityAvailableTimeResolver; AvailabilityNotAvailableTime; … (+316) | generated/fhir; generated/graphql/graph/model | internal/api/graphql/graph; internal/api/graphql/graph/resolver | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | +| `generated/graphql/graph/model` | generated | Provide gqlgen-generated GraphQL transport model types. | P: models.go; T: —; Other: — | 1,332/0 | 95: DataframeAggregateInput; DataframeAggregateResult; DataframeAggregationSpecInput; DataframeAggregationsInput; DataframeAggregationsResult; DataframeColumn; DataframeCompilerPlanDiagnostics; DataframeDatasetInput; DataframeFilterInput; DataframeMaterialization; DataframeMaterializationState; DataframeOptimizationDecision; … (+83) | — | cmd/dataframe-profile; generated/graphql/graph/executor; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | +| `generated/loomapi` | generated | Provide OpenAPI-generated HTTP request, response, and strict-server contracts. | P: api.gen.go; T: —; Other: — | 4,505/0 | 375: APIVersion; Action; ActionColumn; ActivateDatasetGeneration; ActivateDatasetGeneration200JSONResponse; ActivateDatasetGeneration400JSONResponse; ActivateDatasetGeneration401JSONResponse; ActivateDatasetGeneration403JSONResponse; ActivateDatasetGeneration409JSONResponse; ActivateDatasetGeneration500JSONResponse; ActivateDatasetGeneration503JSONResponse; ActivateDatasetGenerationParams; … (+363) | internal/explorer/authoringv2 | internal/server | generated boundary; change its source/generator, not emitted code; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/acceptance` | domain/library | Fetch and cache the locked FHIR fixture, drive the real-data acceptance scenario, and verify its outputs and performance evidence. | P: fixture.go; performance.go; runner.go; scenario.go; T: fixture_test.go; performance_test.go; scenario_test.go; Other: — | 1,972/655 | 41: Acquire; Close; ComparePerformance; ComparePerformanceReportFiles; ComparePerformanceRuns; ComputedDigest; Config; Connections; DefaultLimits; Fetcher; FixtureDigest; FixtureLock; … (+29) | internal/dataframe/publication; internal/dataframe/publication/arango; internal/store/arango; internal/store/clickhouse | cmd/loom-acceptance | one production importer (cmd/loom-acceptance); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/api/bulk/load` | transport adapter | Adapt multipart HTTP generation uploads and activation requests to ingest and dataset services. | P: errors.go; generation_staging.go; handler.go; operations.go; service.go; T: errors_test.go; operations_test.go; service_test.go; Other: — | 399/350 | 15: ActivateDatasetGeneration; Config; CreateDatasetGeneration; DataframeReleaseStore; GenerationActivator; GenerationLoadRequest; GenerationLoadResult; GenerationLoader; Handler; IngestRunner; NewHandler; NewService; … (+3) | internal/authscope; internal/dataframe/errors; internal/dataframe/publication; internal/dataset; internal/ingest | internal/server | one production importer (internal/server); inspect ownership/inline potential; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/api/graphql/graph` | transport adapter | Own GraphQL HTTP handler construction, error presentation, and generated executor binding. | P: doc.go; errors.go; handler.go; T: aggregate_http_integration_test.go; errors_test.go; handler_test.go; http_integration_test.go; Other: — | 288/831 | 8: Flush; Header; NewApolloSandboxHandler; NewHandler; NewPlaygroundHandler; RouteConfig; Write; WriteHeader | generated/graphql/graph/executor; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/errors | internal/server | one production importer (internal/server); inspect ownership/inline potential | +| `internal/api/graphql/graph/dataframe` | transport adapter | Adapt published ClickHouse dataframe discovery, row, aggregate, and export operations to GraphQL. | P: aggregates.go; export.go; model.go; rows.go; service.go; T: aggregates_test.go; export_test.go; filter_test.go; model_test.go; service_test.go; Other: — | 1,084/316 | 17: AggregateInput; AggregateRowsResult; AggregationsInput; AggregationsJSON; ColumnFromPhysical; ColumnModel; Config; Dataset; ExportDataframe; Model; NewService; PersistedFailure; … (+5) | generated/graphql/graph/model; internal/api/http; internal/authscope; internal/dataframe/errors; internal/dataframe/publication; internal/dataframe/published | internal/api/graphql/graph/resolver | one production importer (internal/api/graphql/graph/resolver); inspect ownership/inline potential | +| `internal/api/graphql/graph/query` | transport adapter | Resolve and authorize Arango-backed FHIR dataframe and graph query requests for GraphQL. | P: active_generation.go; doc.go; errors.go; fhir_service.go; fieldrefs.go; graph_service.go; helpers.go; input_resolution.go; introspection.go; recipe_discovery.go; recipe_mapping.go; recipe_resolve.go; service.go; types.go; T: active_generation_test.go; auth_scope_test.go; errors_test.go; fhir_service_test.go; fieldrefs_test.go; graph_service_test.go; introspection_test.go; recipe_mapping_test.go; Other: — | 1,917/741 | 29: Config; ExplainFHIRGraph; FHIRGraphExplanation; FHIRGraphNode; FHIRGraphPageInfo; FHIRGraphPath; FHIRGraphQuery; FHIRGraphRelationship; FHIRGraphResult; FHIRGraphTraversal; FHIRListRequest; FHIRListResult; … (+17) | generated/graphql/graph/model; internal/authscope; internal/catalog; internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/recipe; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/dataset; internal/fhir/schema | cmd/dataframe-profile; internal/api/graphql/graph/resolver; internal/server | broad surface (14 production files); inspect cohesion | +| `internal/api/graphql/graph/resolver` | transport adapter | Implement gqlgen field resolvers and map GraphQL values to query and lifecycle services. | P: dataframe_diagnostics.go; fhir_reference_loader.go; fhir_resolver_helpers.go; fhir_schema.resolvers.go; graph_resolvers.go; graphql_output_helpers.go; operation_context.go; recipe_control_graphql.go; recipe_execution_reader.go; recipe_revision_model.go; resolver.go; schema.resolvers.go; T: fhir_reference_loader_test.go; output_mapping_test.go; recipe_control_test.go; Other: — | 5,380/293 | 362: AbatementDateTime; AbatementString; Active; Actual; ActualComparisonGroup; ActualNumber; Address; Age; AgeString; Alias; AllDay; AllowedBoolean; … (+350) | generated/fhir; generated/graphql/graph/executor; generated/graphql/graph/model; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataset; internal/fhir/schema | internal/api/graphql/graph; internal/server | broad surface (12 production files); inspect cohesion; mixed handwritten/generated package (2 generated file(s)); preserve regeneration boundary; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/api/http` | transport adapter | Own shared Fiber server setup, middleware, request IDs, errors, health, and readiness behavior. | P: doc.go; errors.go; middleware.go; request_id.go; routes.go; server.go; T: errors_test.go; http_test.go; request_id_test.go; Other: — | 540/261 | 15: App; ContextWithRequestID; ErrorResponse; HTTPConfig; HTTPErrorBody; HTTPServer; Health; Liveness; Logger; MapDataframeError; MappedError; NewHTTPServer; … (+3) | internal/authscope; internal/dataframe/errors | internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/server | no automatic consolidation signal; perform semantic review | +| `internal/authscope` | domain/library | Represent request principals and resolve authorized project, generation, and resource scopes. | P: authentication.go; doc.go; fence_resource_access.go; principal.go; scope_resolver.go; T: authentication_test.go; scope_test.go; Other: — | 854/335 | 34: AllowAllAuthorizer; Authenticate; Authenticator; AuthorizeProject; AuthorizeReadProject; AuthorizeWrite; Authorizer; BasicAuthenticator; BearerTokenAuthenticator; CalyprAuthenticator; Clone; ContextWithPrincipal; … (+22) | internal/catalog; internal/projectid | internal/api/bulk/load; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/recipe; internal/dataframe/semantic; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/catalog` | domain/library | Own persistence-neutral observed FHIR field, pivot, relationship, and capability facts. | P: auth_scope.go; cache.go; capability_evidence.go; generation.go; helpers.go; limits.go; rebuild.go; relationships.go; types.go; write_profiler.go; T: auth_scope_test.go; cache_test.go; capability_evidence_test.go; generation_test.go; relationships_test.go; write_profiler_test.go; Other: — | 1,821/749 | 55: AuthResourcePathOptions; Cache; CapabilityEvidence; CapabilityEvidenceOptions; CapabilityEvidenceReader; DefaultProfileLimits; DiscoverFields; DiscoverReferences; Documents; EffectiveAuthResourcePathsUnrestricted; EvidenceDiagnostic; EvidenceStatus; … (+43) | internal/fhir/schema | cmd/arango-fhir-proto; internal/api/graphql/graph/query; internal/authscope; internal/catalog/arango; internal/ingest; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/catalog/arango` | storage adapter | Persist and retrieve catalog facts in ArangoDB. | P: codec.go; evidence.go; fields.go; relationships.go; resources.go; store.go; T: store_test.go; Other: — | 917/269 | 16: AccumulateRelationshipCatalog; AuditRelationshipEdges; DiscoverExistingAuthResourcePaths; DiscoverFieldEnrichment; DiscoverFields; DiscoverReferences; DiscoverRelationshipObservations; DiscoverResourceInventory; New; ReadFieldEnrichment; ReadRelationshipObservations; ReadResourceInventory; … (+4) | internal/catalog; internal/fhir/schema; internal/store/arango | cmd/arango-fhir-proto; internal/ingest; internal/server | storage adapter; preserve interface and persisted-data contracts | +| `internal/dataframe/compiler` | domain/library | Orchestrate semantic planning, physical lowering, optimization, rendering, and compiled-query assembly. | P: api.go; clone_helpers.go; compiled_query.go; graph_execution.go; ir_helpers.go; optimizer.go; output.go; recipe_execution.go; T: filter_semantics_test.go; filter_test.go; generic_physical_plan_test.go; grain_test.go; physical_diagnostics_test.go; physical_lowering_test.go; physical_optimize_test.go; physical_plan_test.go; physical_render_test.go; physical_required_match_test.go; physical_scope_test.go; plan_test_helpers_test.go; selection_semantics_test.go; semantic_validation_test.go; Other: — | 355/2,447 | 5: CompileRecipeOutputWithPolicy; CompileResolvedGraphQueryWithPolicy; CompileResolvedRecipePlanWithPolicy; CompiledQuery; PublicOutputColumns | internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec | cmd/dataframe-profile; internal/api/graphql/graph/query; internal/dataframe/execution | no automatic consolidation signal; perform semantic review | +| `internal/dataframe/compiler/capability` | domain/library | Probe which dataframe language capabilities the compiler can prove and support. | P: probe.go; T: probe_test.go; Other: — | 622/138 | 16: CandidateCapability; CandidateRequest; Chart; Filter; Options; ProbeCandidate; ProbeRoot; ProbeTraversal; Rendered; Result; RootCapability; RootRequest; … (+4) | internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema | internal/server | one production importer (internal/server); inspect ownership/inline potential | +| `internal/dataframe/compiler/ir` | domain/library | Define typed physical operations, values, scope proofs, policies, and validation. | P: clone.go; dataset_generation.go; diagnostics.go; doc.go; navigation.go; physical_operations.go; physical_types.go; physical_validation.go; physical_validation_expr.go; physical_validation_refs.go; physical_validation_shapes.go; physical_values.go; policy.go; prefix.go; scope.go; selector_helpers.go; T: unnest_test.go; Other: — | 3,739/105 | 92: AddDecision; CanonicalExecutionPhysicalPlan; ClonePhysicalExpression; ClonePhysicalOperation; ClonePhysicalOperations; ClonePhysicalPlan; ClonePhysicalPredicateExpression; ClonePhysicalSubplan; CompilerPlanDiagnostics; DatasetGenerationBindValue; DecomposePhysicalTraversalPrefixAt; DefaultPhysicalOptimizationPolicy; … (+80) | internal/dataframe/spec; internal/fhir/schema | cmd/dataframe-profile; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/compiler/render/aql; internal/dataframe/execution; internal/server | broad surface (16 production files); inspect cohesion | +| `internal/dataframe/compiler/lower` | domain/library | Lower logical FHIR dataframe plans into storage-aware physical IR. | P: auth_scope.go; child_sets.go; doc.go; filter_literal.go; graph_plan.go; helpers.go; plan_orchestration.go; recipe_compile.go; recipe_dynamic_columns.go; recipe_expression.go; recipe_expression_lowering.go; recipe_schema.go; required_match.go; root_projection.go; storage_route.go; traversal.go; T: graph_plan_test.go; recipe_expression_test.go; recipe_output_test.go; recipe_semantic_path_test.go; traversal_test.go; Other: — | 2,966/1,177 | 12: BuildGenericPhysicalPlanWithPolicy; BuildGraphPhysicalPlan; BuildPhysicalTraversal; CompileResolvedGraphPlan; CompileResolvedRecipePlan; CompiledOutputColumn; CompiledRecipe; CompiledRecipeOutput; DynamicColumnMetadata; LowerRecipeExpression; TraversalLoweringRequest; TraversalLoweringResult | internal/authscope; internal/dataframe/compiler/ir; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/semantic; internal/dataframe/spec; internal/fhir/schema | internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution; internal/server | broad surface (16 production files); inspect cohesion | +| `internal/dataframe/compiler/optimize` | domain/library | Apply explainable semantics-preserving rewrites to validated physical IR. | P: doc.go; optimize.go; T: optimize_test.go; Other: — | 410/122 | 1: OptimizePhysicalPlanWithPolicy | internal/dataframe/compiler/ir | internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/execution | no automatic consolidation signal; perform semantic review | +| `internal/dataframe/compiler/render/aql` | domain/library | Render validated physical IR as deterministic parameterized AQL. | P: calls.go; collections.go; doc.go; graph_render.go; navigation_layout.go; navigation_render.go; predicates.go; render.go; selector.go; selectors.go; validation.go; values.go; T: call_test.go; graph_test.go; unnest_test.go; Other: — | 2,541/321 | 2: RenderPhysicalPlan; RenderedPhysicalPlan | internal/dataframe/compiler/ir; internal/dataframe/spec | internal/dataframe/compiler; internal/dataframe/compiler/capability | broad surface (12 production files); inspect cohesion | +| `internal/dataframe/errors` | domain/library | Define and normalize stable structured dataframe errors for transport presentation. | P: errors.go; T: errors_test.go; Other: — | 429/163 | 20: AsUserError; Code; Details; Error; ErrorCode; ErrorOption; FieldPath; IsRetryableCode; NewError; Normalize; PublicMessage; Retryable; … (+8) | — | internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/server | small shared semantic contract; compare direct callers before inlining; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/execution` | domain/library | Prepare, authorize, execute, explain, and post-process compiled dataframe requests. | P: control.go; control_explain_types.go; control_types.go; engine.go; execution_service.go; explain.go; helpers.go; named_uuid.go; postquery.go; service.go; types.go; T: auth_scope_test.go; engine_test.go; execution_test.go; explain_test.go; helpers_test.go; identity_test.go; named_uuid_test.go; postquery_test.go; Other: — | 1,706/771 | 54: AssessmentFromArango; CompileResolvedBundle; CompiledQuery; CompiledQueryFingerprint; CompilerPlanDiagnostics; Config; Control; DynamicColumnCheck; DynamicDriftError; Engine; Error; Explain; … (+42) | internal/dataframe/compiler; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/optimize; internal/dataframe/errors; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/semantic; internal/dataframe/spec; internal/store/arango | internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/explorer/lifecycle; internal/server | no automatic consolidation signal; perform semantic review | +| `internal/dataframe/expression` | domain/library | Define and validate the typed backend-neutral expression AST used by recipes. | P: expression_checking.go; expression_types.go; recipe_adapter.go; T: expression_test.go; recipe_adapter_test.go; Other: — | 850/234 | 26: AllowsMany; Call; Cardinality; Check; CheckedExpression; Constant; Document; DocumentRef; Expression; FromRecipe; FromRecipeInContexts; Function; … (+14) | internal/dataframe/recipe | internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/semantic | no automatic consolidation signal; perform semantic review | +| `internal/dataframe/publication` | domain/library | Own backend-neutral dataframe publication contracts, bundles, and publication orchestration. | P: bundle.go; flat_columns.go; runner.go; types.go; T: flat_columns_test.go; runner_test.go; Other: — | 776/311 | 34: BundleCatalog; BundleExecution; BundleIdentity; BundleOutputRecord; BundlePointer; BundleState; Canonical; CanonicalizeLegacy; ColumnProvenance; DataframeSelector; Error; ExactExecutionCatalog; … (+22) | internal/dataframe/errors; internal/dataset | internal/acceptance; internal/api/bulk/load; internal/api/graphql/graph/dataframe; internal/api/graphql/graph/resolver; internal/dataframe/publication/arango; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/explorer; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/publication/arango` | storage adapter | Persist dataframe publication registry and bundle metadata in ArangoDB. | P: bundle_registry.go; registry.go; T: bundle_registry_test.go; Other: — | 286/60 | 15: AcquireBundleLease; BootstrapSpec; Client; CompareAndSwapPointer; FindExecutionByKey; FindExecutionBySelector; GetExecution; GetPointer; ListExecutions; New; PublishExecution; Registry; … (+3) | internal/dataframe/publication; internal/store/arango | internal/acceptance; internal/explorer/arango; internal/server | storage adapter; preserve interface and persisted-data contracts | +| `internal/dataframe/publication/clickhouse` | storage adapter | Materialize and inspect published dataframe bundles in ClickHouse. | P: bundle_store.go; T: clickhouse_bundle_test.go; Other: — | 828/548 | 16: Abort; Begin; BundleClickHouseStore; ClickHouseBundleStore; Commit; CreateOutput; ExistingPublishedOutputs; FinalizeSchema; Idempotent; InsertRows; NewBundleStore; Reconcile; … (+4) | internal/dataframe/errors; internal/dataframe/publication; internal/store/clickhouse | internal/server | one production importer (internal/server); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/published` | domain/library | Read, page, aggregate, and export authorized published ClickHouse dataframes. | P: aggregate_execute.go; aggregate_plan.go; aggregate_sql.go; aggregate_types.go; current.go; errors.go; export.go; page.go; query_helpers.go; read.go; schema.go; types.go; T: aggregate_clickhouse_integration_test.go; aggregate_execute_test.go; aggregate_plan_test.go; current_test.go; errors_test.go; page_test.go; read_test.go; Other: — | 2,102/555 | 30: AggregateBatchRequest; AggregateBatchResult; AggregateJob; AggregateJobResult; AggregateResponseMode; AggregateResult; AggregationResult; AggregationSpec; AggregationsRequest; AggregationsResult; ClickHouseQueryer; Column; … (+18) | internal/dataframe/errors; internal/dataframe/publication; internal/dataset | internal/api/graphql/graph/dataframe; internal/api/graphql/graph/resolver; internal/server | broad surface (12 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/recipe` | domain/library | Define, canonicalize, expand, encode, and validate persistence-neutral recipe documents. | P: canonical.go; document_types.go; document_validation.go; dynamic_validation.go; fragments.go; json_encoding.go; revisions.go; strict_json.go; validation_rich.go; T: documentation_test.go; fragments_test.go; types_test.go; Other: — | 2,072/497 | 51: Aggregate; AggregateOperation; ArrayQuantifier; Bundle; CanonicalJSON; CatalogProjection; Clone; CodeValue; ColumnNaming; Digest; DocumentRef; DynamicColumn; … (+39) | generated/fhir; internal/authscope | cmd/dataframe-profile; cmd/explorer-config-v2-convert; conformance/compiler; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution; internal/dataframe/expression; internal/dataframe/recipe/exec; internal/dataframe/recipe/exec/arango; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/explorer; internal/explorer/compilation; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/recipe/exec` | domain/library | Define durable recipe-version storage contracts and immutable registration behavior. | P: persistent.go; T: persistent_test.go; Other: — | 99/102 | 5: Entry; PersistentRegistry; Reader; RegisterVersion; Store | internal/dataframe/recipe | internal/api/graphql/graph/resolver; internal/dataframe/execution; internal/dataframe/recipe/exec/arango; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/recipe/exec/arango` | storage adapter | Persist recipe versions and execution-use records in ArangoDB. | P: registry.go; revisions.go; T: registry_test.go; revisions_test.go; Other: — | 291/350 | 15: BootstrapSpec; Client; Get; List; LoadRecipe; LoadRecipeVersion; New; NewRevisionRegistry; RecipeVersionUsed; Register; Registry; ReplaceRecipeVersion; … (+3) | internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/store/arango | internal/server | one production importer (internal/server); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts | +| `internal/dataframe/recipe/schema` | domain/library | Resolve catalog-backed recipe declarations into concrete validated schemas. | P: discovery.go; extension_columns.go; resolve.go; T: resolve_test.go; Other: — | 875/270 | 8: Discovery; ExtensionValueObservation; FieldCandidate; Fields; Resolve; Resolved; Scope; SourceSnapshot | internal/dataframe/recipe; internal/fhir/schema | internal/api/graphql/graph/query; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataframe/semantic` | domain/library | Convert validated dataframe requests and recipes into backend-neutral logical FHIR plans. | P: doc.go; dynamic_schema.go; execution_context.go; recipe_filter.go; recipe_plan_build.go; recipe_plan_output.go; recipe_plan_types.go; recipe_projection.go; recipe_rich_shaping.go; recipe_traversal.go; resolved_plan.go; selection_semantics.go; selector_spec.go; semantic_nodes.go; semantic_validation.go; T: dynamic_schema_test.go; recipe_plan_test.go; recipe_rich_shaping_test.go; resolved_plan_test.go; Other: — | 2,385/402 | 36: BuildRecipePlan; Candidate; Column; DynamicSpec; ExecutionContext; ExpansionExplanation; Explain; ExpressionExplanation; FallbackSelectors; Freeze; FrozenSchema; LowerRecipeFilters; … (+24) | internal/authscope; internal/dataframe/expression; internal/dataframe/recipe; internal/dataframe/spec; internal/fhir/schema | cmd/dataframe-profile; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/lower; internal/dataframe/execution | broad surface (15 production files); inspect cohesion | +| `internal/dataframe/spec` | domain/library | Define the backend-independent dataframe request, filter, grain, relationship, and selector language. | P: doc.go; filter.go; filter_semantics.go; grain.go; relationship_match.go; selector_validation.go; selectors.go; T: —; Other: — | 665/0 | 30: AllowsMany; ArrayQuantifier; Cardinality; Clone; CodeValue; ContainsFilter; DefaultRowIdentity; ExpandsRows; FilterOperator; FilterValue; FilterValueKind; InferRowGrain; … (+18) | generated/fhir; internal/fhir/schema | internal/api/graphql/graph/query; internal/dataframe/compiler; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/compiler/render/aql; internal/dataframe/execution; internal/dataframe/semantic; internal/explorer/compilation; internal/server | no automatic consolidation signal; perform semantic review | +| `internal/dataset` | domain/library | Define immutable dataset generation manifests, releases, active pointers, and lifecycle validation. | P: active_resolver.go; dataframe_selector.go; doc.go; manifest.go; ref.go; release.go; schema.go; validation.go; T: active_resolver_test.go; manifest_test.go; memory_store_test.go; ref_test.go; release_test.go; schema_test.go; Other: README.md | 562/451 | 32: Activate; ActivationRequest; Active; ActiveRelease; ActiveResolver; ContractVerification; Create; DataframeSelector; Error; IsStaged; Key; Manifest; … (+20) | — | cmd/arango-fhir-proto; internal/api/bulk/load; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/dataframe/publication; internal/dataframe/published; internal/dataset/arango; internal/explorer; internal/explorer/arango; internal/explorer/lifecycle; internal/ingest; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/dataset/arango` | storage adapter | Persist immutable dataset generation lifecycle state in ArangoDB. | P: collections.go; doc.go; document_codec.go; manifest_lifecycle.go; release_store.go; T: store_test.go; Other: — | 731/311 | 14: Activate; ActiveDocumentKey; BootstrapSpec; CollectionSpecs; CompareAndSwapActivateRelease; CreateManifest; ManifestDocumentKey; New; ReadActiveRelease; ReadManifest; ResolveActiveManifest; SaveRelease; … (+2) | internal/dataset; internal/store/arango | cmd/arango-fhir-proto; internal/explorer/arango; internal/ingest; internal/server | storage adapter; preserve interface and persisted-data contracts | +| `internal/explorer` | domain/library | Define Explorer aggregate identities, configs, revisions, receipts, output contracts, stores, and core service operations. | P: authoring_errors.go; canonical_json.go; compilation.go; compilation_receipt.go; config_v2.go; identity.go; output_contract.go; service.go; state_v1.go; store.go; types.go; viewer_projection.go; T: compilation_receipt_test.go; output_contract_test.go; repository_revision_id_test.go; types_test.go; Other: — | 1,727/337 | 69: ActivateRepositoryGeneration; ActiveRevision; ApplyWorkspaceCommands; AuthoringDiagnostic; AuthoringError; BuildViewerProjection; CompilationArtifactDigest; CompilationKey; CompilationReceipt; CompilationReceiptForExplorer; CompilationWarning; ConfigAction; … (+57) | internal/dataframe/publication; internal/dataframe/recipe; internal/dataset; internal/explorer/authoringv2; internal/projectid | cmd/explorer-config-v2-convert; internal/explorer/arango; internal/explorer/compilation; internal/explorer/lifecycle; internal/server | broad surface (12 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/explorer/arango` | storage adapter | Persist Explorer drafts, revisions, receipts, publications, capabilities, and repository state in ArangoDB. | P: activation.go; capability_snapshot_store.go; codec.go; collections.go; drafts.go; explorers.go; publication.go; receipts.go; repository.go; revisions.go; store.go; T: capability_snapshot_store_test.go; collections_test.go; receipt_store_test.go; repository_test.go; store_test.go; Other: — | 917/419 | 22: ActivateRepositoryGeneration; BootstrapSpec; CapabilitySnapshotCollectionSpec; CapabilitySnapshotStore; CollectionSpecs; Create; FailRevision; Get; GetByIdentity; GetByToken; GetCompilationReceiptForExplorer; GetRevision; … (+10) | internal/dataframe/publication/arango; internal/dataset; internal/dataset/arango; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/projectid; internal/store/arango | internal/server | one production importer (internal/server); inspect ownership/inline potential; storage adapter; preserve interface and persisted-data contracts | +| `internal/explorer/authoringv2` | domain/library | Define, validate, canonicalize, and mutate the V2 Explorer Builder authoring workspace. | P: canonical.go; commands.go; semantic_types.go; types.go; validation.go; T: commands_test.go; types_test.go; workspace_test.go; Other: — | 1,567/341 | 34: Action; ActionColumn; ApplyCommands; ApplyCommandsRequest; ApplyCommandsResponse; BuilderState; CanonicalJSON; CatalogCandidate; CatalogDiagnostic; CatalogEdge; CatalogNode; CatalogSnapshot; … (+22) | — | cmd/explorer-config-v2-convert; generated/loomapi; internal/explorer; internal/explorer/arango; internal/explorer/compilation; internal/explorer/lifecycle; internal/server | no automatic consolidation signal; perform semantic review | +| `internal/explorer/capability` | domain/library | Build the immutable compiler-backed capability contract exposed to Explorer Builder. | P: builder.go; domain.go; repository.go; T: builder_test.go; Other: — | 801/173 | 34: Allows; Build; Builder; Candidate; CandidateProof; CanonicalPayload; ChartAggregation; Clone; CompilerCallbacks; Diagnostic; Edge; EdgeProof; … (+22) | — | internal/explorer/arango; internal/explorer/compilation; internal/explorer/lifecycle; internal/server | runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/explorer/compilation` | domain/library | Translate validated Explorer authoring intent into native recipe and compilation artifacts. | P: compile.go; semantic_compile.go; T: translate_test.go; workspace_test.go; Other: — | 716/192 | 8: Compile; CompileWorkspace; Error; PresentationColumn; PresentationConfig; Result; Unwrap; WorkspaceResult | internal/dataframe/recipe; internal/dataframe/spec; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/fhir/schema | internal/explorer/lifecycle; internal/server | no automatic consolidation signal; perform semantic review | +| `internal/explorer/lifecycle` | domain/library | Coordinate transport-neutral Explorer authoring, preview, publication, query, and policy operations. | P: authoring.go; errors.go; policy.go; preview.go; publish.go; query.go; repository_publish.go; service.go; types.go; T: service_test.go; Other: — | 1,019/298 | 42: ApplyCommands; AuthorizedCapability; Builder; BuilderRequest; Capability; CapabilityResolver; Clone; CompileReceiptRequest; Config; Create; CreateRequest; Error; … (+30) | internal/authscope; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/recipe; internal/dataset; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/projectid | internal/server | one production importer (internal/server); inspect ownership/inline potential | +| `internal/fhir/schema` | domain/library | Add handwritten selector, traversal, pivot, and compiler semantics to generated FHIR metadata. | P: compiler_semantics.go; fields.go; generated_metadata.go; pivots.go; selectors.go; terminal_metadata.go; traversals.go; T: compiler_semantics_test.go; fields_test.go; pivots_test.go; root_resources_test.go; selectors_test.go; terminal_metadata_test.go; traversals_test.go; Other: — | 974/374 | 39: CanonicalPath; CanonicalResourceType; CanonicalizePath; ChoiceValueSelectorOptions; CompilerTraversal; ConcreteResourceType; ContainsFilter; DefaultPivotSpec; DefinitionExists; FieldKind; FieldPredicateSpec; FieldSelectorSpec; … (+27) | generated/fhirschema | generated/fhir; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/catalog; internal/catalog/arango; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/recipe/schema; internal/dataframe/semantic; internal/dataframe/spec; internal/explorer/compilation; internal/ingest | no automatic consolidation signal; perform semantic review | +| `internal/ingest` | domain/library | Preflight and load FHIR NDJSON into generation-scoped Arango vertices, edges, and catalog facts. | P: backend.go; doc.go; documents.go; files.go; generated_load.go; generation_identity.go; generation_orchestration.go; generation_workers.go; load_options.go; preflight.go; progress.go; resource_load.go; row_builder.go; source_schema.go; T: backend_test.go; bench_test.go; documents_test.go; file_pipeline_test.go; files_test.go; generated_load_test.go; generation_identity_test.go; generation_load_test.go; integration_test.go; load_options_test.go; preflight_test.go; row_builder_test.go; source_schema_test.go; Other: — | 2,302/1,614 | 31: ActivationOutcomeError; Build; DiscoverNDJSON; EdgeDocument; EdgeFromGrip; Error; EventSink; GeneratedRowBuilder; GenerationLoadIncompleteError; GenericRowBuilder; IngestionMode; Load; … (+19) | generated/fhir; internal/catalog; internal/catalog/arango; internal/dataset; internal/dataset/arango; internal/fhir/schema; internal/store/arango | cmd/arango-fhir-proto; internal/api/bulk/load; internal/server | broad surface (14 production files); inspect cohesion; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/projectid` | domain/library | Define canonical and legacy-compatible public project identity normalization. | P: projectid.go; T: projectid_test.go; Other: — | 51/18 | 2: Canonical; Legacy | — | internal/authscope; internal/explorer; internal/explorer/arango; internal/explorer/lifecycle; internal/server | small shared semantic contract; compare direct callers before inlining | +| `internal/server` | domain/library | Compose storage, domain services, generated OpenAPI handlers, GraphQL adapters, and HTTP routes. | P: auth_wiring.go; explain_wiring.go; explorer_authoring_http.go; explorer_capability.go; explorer_config_route.go; explorer_lifecycle_helpers.go; explorer_lifecycle_wire.go; explorer_preview_response.go; explorer_preview_security.go; explorer_publication.go; explorer_receipt_contract.go; openapi_data_routes.go; openapi_explorer_authoring_direct.go; openapi_explorer_conversion.go; openapi_explorer_direct.go; openapi_explorer_routes.go; openapi_graphql_routes.go; openapi_recipe_execution.go; openapi_routes.go; options.go; recipe_authorization.go; recipe_discovery.go; recipe_publication.go; release_verifier.go; routes.go; server.go; T: config_test.go; explorer_capability_test.go; explorer_config_route_test.go; explorer_preview_response_test.go; explorer_preview_security_test.go; explorer_receipt_contract_test.go; explorer_state_route_test.go; http_test_helpers_test.go; openapi_explorer_direct_test.go; openapi_recipe_execution_test.go; openapi_route_inventory_test.go; openapi_test_helpers_test.go; recipe_discovery_test.go; release_verifier_test.go; routes_test.go; wiring_test.go; Other: — | 3,673/1,977 | 49: ActivateDatasetGeneration; ApplyExplorerBuilderCommands; AuthConfig; AuthorizeRead; AuthorizeWrite; BasicAuthConfig; CalyprAuthConfig; ClickHouseConfig; Config; CreateDatasetGeneration; CreateExplorer; DataframerConfig; … (+37) | generated/loomapi; internal/api/bulk/load; internal/api/graphql/graph; internal/api/graphql/graph/query; internal/api/graphql/graph/resolver; internal/api/http; internal/authscope; internal/catalog; internal/catalog/arango; internal/dataframe/compiler/capability; internal/dataframe/compiler/ir; internal/dataframe/compiler/lower; internal/dataframe/errors; internal/dataframe/execution; internal/dataframe/publication; internal/dataframe/publication/arango; internal/dataframe/publication/clickhouse; internal/dataframe/published; internal/dataframe/recipe; internal/dataframe/recipe/exec; internal/dataframe/recipe/exec/arango; internal/dataframe/recipe/schema; internal/dataframe/spec; internal/dataset; internal/dataset/arango; internal/explorer; internal/explorer/arango; internal/explorer/authoringv2; internal/explorer/capability; internal/explorer/compilation; internal/explorer/lifecycle; internal/ingest; internal/projectid; internal/store/arango; internal/store/clickhouse | .; cmd/arango-fhir-server | broad surface (26 production files); inspect cohesion | +| `internal/store/arango` | storage adapter | Provide the low-level typed ArangoDB client, query streaming, profiling, and Explain assessment boundary. | P: client.go; explain.go; explain_assessment.go; explain_client.go; options.go; profile.go; profile_client.go; types.go; T: client_queryrows_test.go; explain_assessment_test.go; explain_client_test.go; explain_test.go; profile_client_test.go; profile_integration_test.go; profile_test.go; Other: — | 1,118/860 | 53: AQLExecutor; AssessExplainResult; BatchInserter; Bootstrap; BootstrapSpec; Client; Close; CollectionExists; CollectionSpec; ConnectionOptions; ExecuteAQL; Explain; … (+41) | — | cmd/arango-fhir-proto; cmd/dataframe-profile; internal/acceptance; internal/catalog/arango; internal/dataframe/execution; internal/dataframe/publication/arango; internal/dataframe/recipe/exec/arango; internal/dataset/arango; internal/explorer/arango; internal/ingest; internal/server | storage adapter; preserve interface and persisted-data contracts; runtime/build signals present; static importer counts are insufficient for deletion | +| `internal/store/clickhouse` | storage adapter | Provide the low-level typed ClickHouse connection and database-management boundary. | P: client.go; T: client_test.go; integration_test.go; Other: — | 557/256 | 15: AddColumn; Client; Close; Column; CreateTable; DropColumns; DropTable; EnsureDatabase; InsertRows; New; Options; Ping; … (+3) | — | internal/acceptance; internal/dataframe/publication/clickhouse; internal/server | storage adapter; preserve interface and persisted-data contracts | + +## Regeneration and validation + +```bash +python3 scripts/package_audit.py +python3 -m unittest scripts.tests.test_package_audit +``` + +Regenerate after adding, removing, or moving a Go package. A generator failure means the curated responsibility map and the source tree disagree; resolve that mismatch rather than emitting an unclassified row. diff --git a/docs/PACKAGE_AUDIT_DECISIONS.csv b/docs/PACKAGE_AUDIT_DECISIONS.csv new file mode 100644 index 00000000..6823cbf0 --- /dev/null +++ b/docs/PACKAGE_AUDIT_DECISIONS.csv @@ -0,0 +1,4 @@ +source,disposition,destination,rationale,contracts,verification,status +internal/api/graphql,merged,internal/api/graphql/graph,one production importer and error presentation is transport policy owned by graph,public error codes/messages/redaction/request IDs/cause/path/location,focused graph+server tests; full repository verification; extension-map isolation regression,complete +internal/api/bulk/load,keep,internal/api/bulk/load,"owns multipart staging, authorization, ingest error translation, generation reuse, and release activation apart from generated OpenAPI response mapping",multipart fields; JSON result fields; auth scope; stable error codes; activation safety,focused load+server tests; full repository verification,complete +internal/api/graphql/graph,keep,internal/api/graphql/graph,"owns gqlgen handler construction, GraphQL error presentation, and response-status adaptation apart from server composition and generated execution",GraphQL error shape; request IDs; partial-data HTTP 200; failure status lifting; playground and sandbox handlers,focused graph+server tests; full repository verification; extension-map isolation regression,complete diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 999495b3..11264b70 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -95,8 +95,8 @@ open http://127.0.0.1:8080/apollo Current Builder authoring uses the versioned REST V2 workflow. List project Explorers, then load the selected Explorer's -`/authoring/v2/builder` state. Save canonical drafts with `PUT /draft`, compile -to an immutable receipt with `POST /compile`, preview that receipt with +`/authoring/v2/builder` state. Persist canonical edits with `POST /commands`, +compile the exact draft to an immutable receipt with `POST /reconcile`, preview that receipt with `POST /preview`, and activate it only with `POST /publish`. Loom owns catalog resolution, recipe lowering, compilation, materialization, persistence, and diagnostics. diff --git a/docs/README.md b/docs/README.md index be1638f2..a8d446a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ audits do not belong here. ## Start here +- [Standalone Explorer demo](DEMO_QUICKSTART.md) runs the no-auth Builder and Viewer against the locked FHIR Aggregator cohort with one command. - [Quickstart](QUICKSTART.md) — run Loom locally, load sample data, and make a first dataframe request. - [HTTP API and OpenAPI contract](../openapi/README.md) — canonical paths, @@ -24,6 +25,8 @@ audits do not belong here. - [Developer architecture](DEVELOPER_ARCHITECTURE.md) — package boundaries, compiler ownership, storage model, and runtime call paths. +- [Package audit lookup table](PACKAGE_AUDIT.md) — complete package locations, + responsibilities, contents, dependencies, importers, and consolidation evidence. - [Dataframer recipes](DATAFRAMER_RECIPES.md) — native recipe authoring for repository, ETL, and developer workflows. - [Dataframer recipe reference](DATAFRAMER_RECIPE_REFERENCE.md) — complete @@ -32,8 +35,6 @@ audits do not belong here. files, and regeneration commands. - [Compiler performance](COMPILER_PERFORMANCE.md) — benchmark and tuning guidance. -- [Dataframe federation](DATAFRAME_FEDERATION.md) — published-data source - selection and cross-project reads. - [Reliability contract](loom-reliability-contract.md) — immutable generations, publication, selectors, and error-state guarantees. diff --git a/docs/REAL_DATA_ACCEPTANCE.md b/docs/REAL_DATA_ACCEPTANCE.md new file mode 100644 index 00000000..abd47d53 --- /dev/null +++ b/docs/REAL_DATA_ACCEPTANCE.md @@ -0,0 +1,155 @@ +# Real-data acceptance test + +## Purpose + +`make acceptance-real` proves the path an NCPI researcher uses. The command loads a fixed FHIR Aggregator cohort into ArangoDB, publishes an Explorer V2 workspace into ClickHouse, reads the Explorer viewer state, and queries the published dataframe through GraphQL. `make acceptance-performance` runs that proof for a Git base and the current worktree on the same services, then applies the regression gate. + +Local runs use the existing Kubernetes database pods through port-forwards. GitHub runs use job service containers. Both modes build and start Loom from the checkout under test; neither uses the deployed Loom pod as evidence. + +## Fixture contract + +The fixture is a locked cohort of 100 TCGA-BRCA Patients from `ResearchStudy/638f6162-e000-5167-8216-962d86b74a98` at `https://google-fhir.fhir-aggregator.org`. + +The lock records: + +- the source endpoint and study; +- the deterministic patient selection rule; +- every resource type, ID, version, and canonical SHA-256 digest; +- exact per-type counts; +- the fixture format and lock digest. + +The repository does not store raw FHIR payloads while redistribution terms remain unclear. A cold run fetches the locked resources into a content-addressed cache. A warm run performs no network requests. Upstream drift fails the test and never rewrites the lock. + +The fetcher enforces hard limits before it commits the cache: + +- 100 Patients; +- 50,000 resources; +- 512 MiB of uncompressed JSON; +- 250 FHIR result pages; +- 5 minutes of fetch time. + +The fixture includes all Specimen, Observation, DocumentReference, and Condition resources whose subject points to a selected Patient. It also includes the ResearchStudy. ResearchSubject is deliberately excluded: the Aggregator exposes the R4 `individual` shape while Loom validates the R5 `subject` shape, and this recipe does not traverse ResearchSubject. Other references may point outside the fixture because the acceptance recipe does not traverse them. + +`refresh-fixture` updates only the fixture lock. A separate explicit command produces candidate expected results. A maintainer reviews those results before changing the checked-in oracle. + +## Researcher dataframe + +The checked-in workspace publishes one Patient-grain output named `tcga_brca_cohort`. Each of its 100 rows combines Patient demographics with clinical and biospecimen facts reached through the FHIR graph: + +- `Patient -> Condition` supplies distinct histology, pathological stage, and the condition count. +- `Patient -> Observation` supplies age at diagnosis, diagnostic method, and days to death by exact observation code. +- `Patient -> Specimen` supplies total, tumor, and normal specimen counts and proves paired tumor-normal availability. +- `Patient -> Specimen -> Observation` supplies the earliest and latest collection day. + +The published columns are: + +```text +patient_id submitter_id +birth_sex race +ethnicity primary_histology +pathological_stage age_at_diagnosis_days +diagnostic_method days_to_death +condition_count specimen_count +tumor_specimen_count normal_specimen_count +has_paired_tumor_normal earliest_collection_day +latest_collection_day +``` + +`primary_histology` is a sorted distinct array because five fixture patients have more than one histology. Choosing the first Condition would discard real diagnoses. FHIR Aggregator supplies age-at-diagnosis intervals as negative days, following the source dataset convention; the acceptance dataframe preserves those values without an undocumented sign change. + +The oracle makes coverage readable in addition to checking the exact normalized row digest. It requires stage, pairing, specimen counts, and collection range for all 100 patients; age at diagnosis for 98; diagnostic method for 88; days to death for 6; race for 89; and ethnicity for 84. + +The acceptance command checks that every requested field and traversal exists in the generation catalog before publication. It does not weaken the workspace when a path is missing. + +## Command contract + +The public Go interface stays small. + +```go +type FixtureDigest string +type RunID string + +type Namespace struct { + Run RunID + Project string + Generation string + ArangoDatabase string + ClickHouseDatabase string +} + +type ValidatedFixture struct { + Digest FixtureDigest + MetaDir string + Counts map[string]int +} + +type Lease interface { + Connections() Connections + Namespace() Namespace + Close(context.Context) error +} + +type Target interface { + Acquire(context.Context, RunSpec) (Lease, error) +} + +type Runner struct{} + +func New(Config) (*Runner, error) +func (r *Runner) Run(context.Context) (Report, error) +``` + +`Runner.Run` owns the acceptance transaction. Callers do not coordinate load, publication, verification, or cleanup stages. + +## Execution targets + +The Kubernetes target resolves services by label or fixed-name fallback. It port-forwards only ArangoDB and ClickHouse. It builds the loader and Loom server from the current worktree, creates guarded run-specific database names, starts the local server with `--no-auth`, and waits for `/readyz`. + +The GitHub target receives ArangoDB and ClickHouse service URLs from the workflow. It builds and starts the same checkout with the same run-specific database policy. + +Database names must match `^loom_acceptance_[a-f0-9]{16}$`. Cleanup drops only the two captured databases after it verifies the names. It does not delete records from shared application databases. Cleanup then lists Arango databases and queries ClickHouse `system.databases` to prove that both names are absent. + +## Assertions + +The command performs these checks: + +1. Upload the per-type NDJSON files through the generation HTTP API. +2. Compare the load response and direct Arango counts with the fixture lock. +3. Reject dangling recipe-path edges and missing catalog fields. +4. Publish the checked-in workspace through the repository Explorer route. +5. Resolve the ClickHouse table through the returned execution ID and the Arango publication registry. +6. Compare the physical schema, row count, and unique Patient count with the checked-in oracle. +7. Require a non-empty Explorer viewer state containing the publication recipe. +8. Query GraphQL dataset metadata, sorted rows, counts, and facets. Require numeric `rowCount` and `totalCount`, array-shaped `rows`, and reject any GraphQL `errors` value. Compare per-column coverage and boolean truth counts with the readable oracle, then compare the normalized rows with the exact `row_digest`. +9. Publish the same workspace again and prove that its execution and selector are unchanged. + +The command writes partial evidence before cleanup. A cleanup failure is joined with the original scenario failure. + +## Performance comparison + +Correctness always blocks a change. Performance compares the base and the current checkout on the same machine, against the same fixture and service versions. Local dirty-tree runs use `HEAD` as the base. Pull requests use the pull request base SHA. Pushes use the previous commit when GitHub provides it. + +Each variant receives empty run-specific databases. The runner warms and validates the fixture before either variant and alternates execution order from the head digest. It records: + +- generation upload time; +- Explorer publication time; +- Explorer viewer latency; +- the combined GraphQL metadata, rows, count, and facet probe latency. + +A metric is a suspected regression when the head is at least twice as slow and exceeds the absolute floor. The floor is 5 seconds for ingestion and publication, and 100 milliseconds for API probes. The runner repeats a suspected regression in reverse order. CI fails only when the same metric crosses both limits twice. + +If the base commit cannot build or does not support the protocol, the report uses `BASE_UNAVAILABLE`. That status fails unless the explicit `LOOM_ACCEPTANCE_ALLOW_BASE_UNAVAILABLE=true` rollout flag is present. Remove the workflow flag once the acceptance protocol exists on every supported base branch. + +## Files + +```text +cmd/loom-acceptance/ Command boundary +internal/acceptance/ Fixture, target, scenario, evidence, and comparison logic +testdata/acceptance/ncpi-tcga-brca/ Fixture lock, provenance, workspace, GraphQL document, and oracle +scripts/acceptance-real.sh Stable local and CI entry point +scripts/acceptance-performance.sh Same-machine base/current regression driver +.github/workflows/acceptance.yaml GitHub service and artifact wiring +.codex/skills/verify-loom/ Agent-facing launch, drive, evidence, and cleanup instructions +``` + +The commands write `report.json`, normalized responses, timings, cleanup proofs, service logs, and `performance.json` under `.artifacts/acceptance/`. Reports exclude credentials and raw FHIR payloads. diff --git a/docs/SIMP.md b/docs/SIMP.md new file mode 100644 index 00000000..3fe6677a --- /dev/null +++ b/docs/SIMP.md @@ -0,0 +1,359 @@ +# Every named interface + + ## Authentication, GraphQL, bulk loading, ingest + + # Interface Production reality Definitive call + ━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 1 internal/authscope/principal.go:68 Four real implementations: static, basic, Calypr, bearer token KEEP. Real authentication strategy boundary + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 2 internal/authscope/principal.go:72 AllowAllAuthorizer and ScopeAuthorizer; many consumers KEEP + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 3 internal/authscope/scope_resolver.go:14 External Fence client boundary KEEP + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 4 internal/api/graphql/graph/resolver/resolver.go:21 One implementation, execution.Control; one repository consumer COLLAPSE TO CONCRETE execution.Control + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 5 internal/api/graphql/graph/resolver/resolver.go:79 One server implementation; no test implementation COLLAPSE TO READ/WRITE FUNCTIONS + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 6 internal/api/graphql/graph/resolver/recipe_execution_reader.go:17 One method, one Arango implementation COLLAPSE TO GetExecution FUNCTION + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 7 internal/api/bulk/load/service.go:54 Duplicates manifest reading plus one activation command DELETE/COMBINE into manifest and activation callbacks + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 8 internal/api/bulk/load/service.go:59 Two read methods used by one service COLLAPSE TO TWO FUNCTIONS + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 9 internal/api/bulk/load/service.go:64 One command method, one production implementation COLLAPSE TO FUNCTION + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 10 internal/api/bulk/load/service.go:68 One resource-load command COLLAPSE TO FUNCTION + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 11 internal/api/bulk/load/snapshot_service.go:18 Only LocalSnapshotBlobs; tests also use the concrete type COLLAPSE TO CONCRETE + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 12 internal/ingest/row_builder.go:40 Strategy selected internally between generated and generic builders COLLAPSE TO BUILD FUNCTION; retain concrete builders + ───── ─────────────────────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────── + 13 internal/ingest/generated_load.go:13 Satisfied by all 23 generated FHIR roots COMBINE INTO GENERATED fhir.ConcreteResource + + The last call explicitly preserves all 23 typed FHIR Go types. It only moves Validate and ExtractEdges into the existing generated contract and deletes the ingest-local duplicate. + + ## Catalog, dataset, and shared Arango capabilities + + # Interface Production reality Definitive call + ━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 14 internal/catalog/arango/store.go:17 Catalog genuinely uses query, insert, AQL, bootstrap, and collection checks KEEP, but embed shared Arango primitives + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 15 internal/catalog/types.go:169 Cohesive three-part evidence read with independent failure reporting KEEP + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 16 internal/catalog/write_persist.go:11 Identical to other Arango ExecuteAQL contracts COMBINE into shared store/arango.AQLExecutor + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 17 internal/store/arango/client.go:31 Merely exposes QueryRows; name adds no contract COMBINE into shared RowQueryer + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 18 internal/dataset/snapshot.go:151 Real snapshot lifecycle boundary KEEP/TRIM: remove federation-only ListSnapshotProjects + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 19 internal/dataset/inventory.go:8 Only feeds unauthenticated federation candidate discovery DELETE with federation + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 20 internal/dataset/retention.go:19 Cohesive metadata-retention boundary KEEP + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 21 internal/dataset/retention.go:24 One method, one filesystem implementation COLLAPSE TO FUNCTION + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 22 internal/dataset/release.go:84 One method, one publication-registry adapter COLLAPSE TO FUNCTION + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 23 internal/dataset/release.go:88 One method, one production implementation COLLAPSE TO FUNCTION + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 24 internal/dataset/release.go:92 Real release persistence boundary KEEP/TRIM: remove federation-only ListReleaseProjects + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 25 internal/dataset/active_resolver.go:9 One method delegated by several consumers COLLAPSE TO FUNCTION + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 26 internal/dataset/arango/manifest_lifecycle.go:26 Duplicate QueryRows contract COMBINE into shared RowQueryer + ───── ────────────────────────────────────────────────── ───────────────────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────── + 27 internal/server/release_verifier.go:15 Exists solely for federation project discovery DELETE + + The shared Arango vocabulary should be: + + type RowQueryer interface { + QueryRows(context.Context, string, int, map[string]any, RowVisitor) error + } + + type BatchInserter interface { + InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error + } + + type AQLExecutor interface { + ExecuteAQL(context.Context, string, map[string]any) error + } + + Packages compose these primitives. Do not replace the duplicates with a new mega-ArangoClient. + + ## Explorer + + # Interface Production reality Definitive call + ━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 28 internal/explorer/store.go:24 All 14 methods are used against real Arango persistence KEEP, but combine CreateInteractive and CreateRepository into Create + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 29 internal/explorer/lifecycle/types.go:24 Mirrors *explorer.Service; exists primarily for one fake COLLAPSE TO CONCRETE *explorer.Service + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 30 internal/explorer/capability/repository.go:20 Real content-addressed snapshot persistence KEEP + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 31 internal/explorer/capability/builder.go:45 Production already materializes the slice before building DELETE, replace with Evidence.Resources + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 32 capability.RelationshipEvidence Same in-memory evidence object DELETE, replace with value + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 33 capability.FieldEvidence Same in-memory evidence object DELETE, replace with value + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 34 capability.Observer No typed consumer; only embeds the previous three DELETE OUTRIGHT + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 35 internal/explorer/capability/builder.go:90 One implementation; tests already use callbacks COLLAPSE TO CALLBACK STRUCT + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 36 internal/explorer/arango/store.go:19 Useful query/transaction provider seam with isolated Arango tests KEEP + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 37 internal/server/explorer_capability.go:73 No interface-valued consumer DELETE + ───── ─────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────── + 38 ExplorerCapabilityExecutionResolver No interface-valued consumer DELETE + + The capability replacement should be a value, not another repository abstraction: + + type Evidence struct { + Resources []ResourceObservation + Relationships []RelationshipObservation + Fields []FieldObservation + + ResourcesAvailable bool + RelationshipsAvailable bool + FieldsAvailable bool + } + + The availability state is necessary to preserve fail-closed behavior. An unavailable evidence source must not become indistinguishable from a valid empty result. + + ## Compiler, recipe, and errors + + # Interface Production reality Definitive call + ━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 39 internal/dataframe/compiler/capability/probe.go:104 No implementation, configuration, or test DELETE with CostEstimate, CostLimit, and both checks + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 40 internal/dataframe/errors/errors.go:71 Real transport-neutral error classification contract KEEP + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 41 internal/dataframe/execution/engine.go:29 Name-based recipe loading is still live COMBINE with VersionedRegistry; retain this name + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 42 execution.VersionedRegistry Same implementation; currently discovered by assertion DELETE INTO Registry + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 43 internal/dataframe/recipe/revisions.go:23 All methods used by GraphQL and digest-based resolution KEEP + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 44 internal/dataframe/recipe/schema/resolve.go:55 One callback method COLLAPSE TO FUNCTION + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 45 internal/dataframe/recipe/exec/persistent.go:15 Base of a runtime optional hierarchy COMBINE with VersionedStore; retain this name + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 46 recipe/exec.VersionedStore Same production and test implementations already implement all methods DELETE INTO Store + ───── ────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────── + 47 internal/dataframe/recipe/exec/arango/registry.go:18 Identical to publication Arango query/write client COMBINE through shared Arango capabilities + + If the queued publication worker is removed, Engine.MaterializeVersion may lose its only runtime consumer. That should be rechecked after worker deletion; the storage contract can still support immutable + versions without keeping every execution wrapper. + + ## Publication and ClickHouse + + The current layering is: + + Publish + -> publication.Target + -> clickhouse.Target + -> IdentityBundleStore + -> AtomicBundleTx + -> clickhouse transaction adapter + -> publication.Transaction + -> clickHouseBundleTx + + That is too many names for one ClickHouse transaction. + + # Interface Production reality Definitive call + ━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 48 internal/dataframe/publication/bundle.go:18 ClickHouse-only intermediate transaction COMBINE INTO Transaction and concrete ClickHouse transaction + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 49 AtomicBundleSchemaFinalizer Never used by its declared name DELETE + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 50 internal/dataframe/publication/bundle.go:198 Ten methods mixing reads, pointers, executions, queues, and leases SPLIT/REPLACE, then delete this declaration + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 51 SelectorExecutionCatalog Federation-only optimized fallback DELETE + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 52 ExactExecutionCatalog One release-verification method COLLAPSE TO FUNCTION + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 53 internal/dataframe/publication/types.go:76 Useful backend-neutral boundary used by the streaming publisher KEEP + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 54 ObjectValueTarget Optional capability, but production always supports objects DELETE; make object support part of the target contract + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 55 internal/dataframe/publication/types.go:89 Correct publication transaction abstraction KEEP/EXPAND with mandatory finalization and abort behavior + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 56 SchemaFinalizer Optional extension for behavior production requires DELETE INTO Transaction + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 57 internal/dataframe/publication/arango/bundle_registry.go:21 Duplicate shared Arango method COMBINE into shared AQLExecutor + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 58 internal/dataframe/publication/arango/registry.go:11 Same query/write contract as recipe registry COMBINE through shared Arango capabilities + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 59 internal/dataframe/publication/clickhouse/bundle_store.go:26 One concrete implementation behind an extra target wrapper DELETE; make ClickHouseBundleStore implement Target + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 60 ClaimedIdentityBundleStore Exists for the queued worker resume path DELETE WITH WORKER + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 61 internal/dataframe/publication/clickhouse/bundle_store.go:47 Real storage boundary between publication and ClickHouse client KEEP/COMBINE DropColumns into it + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 62 bundleColumnDropper Optional assertion for required production behavior DELETE INTO BundleClickHouseStore + ───── ────────────────────────────────────────────────────────────── ──────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────── + 63 BundleClickHouseColumnStore Declared but never consumed by name DELETE + + The replacement for BundleCatalog should be two cohesive ports: + + type PublicationExecutionStore interface { + FindExecutionByKey(...) + SaveExecution(...) + GetPointer(...) + PublishExecution(...) + ListExecutions(...) // startup crash reconciliation + } + + type PublicationLeaseStore interface { + AcquireBundleLease(...) + RenewBundleLease(...) + ReleaseBundleLease(...) + } + + Other consumers should receive exact functions such as GetExecution, FindPublishedOutput, or ListProjectOutputs; they should not depend on either large store interface. + + ClickHouseBundleStore should directly implement publication.Target. That removes target.go’s adapter ladder and lets its concrete transaction directly satisfy the mandatory publication transaction. + + ## Federation and published reads + + # Interface Production reality Definitive call + ━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━ + 64 internal/dataframe/published/federation.go:68 Legacy fallback from execution metadata DELETE WITH FEDERATION + ───── ─────────────────────────────────────────────── ──────────────────────────────────────────────────────────── ──────────────────────── + 65 ProjectStatusResolver Implemented only by server federation status resolver DELETE + ───── ─────────────────────────────────────────────── ──────────────────────────────────────────────────────────── ──────────────────────── + 66 ReleaseExecutionResolver Same implementation; release-aware federation fallback DELETE + ───── ─────────────────────────────────────────────── ──────────────────────────────────────────────────────────── ──────────────────────── + 67 FederationSnapshotResolver Same implementation; latest federation iteration DELETE + ───── ─────────────────────────────────────────────── ──────────────────────────────────────────────────────────── ──────────────────────── + 68 internal/dataframe/published/read.go:17 Clean ClickHouse read boundary used by rows and aggregates KEEP + + # Federation: delete the feature, not single-project reading + + Federation is intertwined with the current dataframe GraphQL API, but it is separable from Explorer publication. + + Delete: + + - internal/dataframe/published/federation.go + - internal/dataframe/published/federation_query.go + - federation tests + - internal/server/federation_status.go + - all four federation interfaces + - SelectorExecutionCatalog + - ExecutionProjectSource and ProjectInventory + - ListSnapshotProjects, ListReleaseProjects, ListExecutionProjects + - candidate-project discovery wiring + - federation cache entries and cross-project status generation + - availability/completeness/project-status GraphQL fields + + The existing projectDataframeDatasets(projectId:) query is the correct anchor. Change other dataframe reads to require an explicit project: + + input DataframeRowsInput { + projectId: String! + selector: DataframeSelectorInput! + ... + } + + Do the same for dataset, aggregate, aggregations, and export inputs. Then: + + principal + -> authorize explicit project + -> resolve that project's active release + -> find one published output + -> query one ClickHouse table + + This removes: + + - catalog-wide publication scans; + - active-release joins across every candidate project; + - schema reconciliation; + - cross-project authorization maps; + - multi-table UNION queries; + - federation status calculations; + - several retry/fallback paths. + + The aggregate code can remain, but it should plan against one physical table. The current batching of multiple GraphQL aggregate root fields is valuable and independent of federation. + + # Publication: retain direct Explorer save, delete the iterations around it + + Explorer publication is real and required: + + compile receipt + -> materialize receipt + -> verify queryable outputs + -> activate release + -> persist active Explorer revision + + Keep that. + + Delete the parallel/legacy entry points: + + - startDataframeMaterialization GraphQL mutation; + - ClickHouse publication worker and worker tests; + - worker polling goroutine; + - publication retry/attempt configuration; + - ClaimedIdentityBundleStore; + - BeginClaimedBundleFor; + - queued-command resume logic; + - retry-specific fields and transitions once stored compatibility is handled; + - old lifecycle.Config.Materialize fallback; + - old bundle-based explorerBundleMaterializer; + - old preview callback when PreviewReceipt is always configured; + - standalone materializeDataframeRecipeBundle if the supported product flow is exclusively Explorer preview → publish/save; + - GraphQL activateProjectRelease, while retaining internal release activation for Explorer publication. + + Keep startup reconciliation for genuinely abandoned staging tables, but remove its “requeue bounded retry work” behavior. Reconciliation should only clean stale private tables and mark abandoned executions + failed. + + # Hidden interfaces not included in the original 68 + + There are another 14 interface-shaped constructs: + + - 10 optional runtime assertions in publication; + - one anonymous ReadManifest assertion in snapshot loading; + - two anonymous catalog writer parameter interfaces; + - one incomplete compile-time assertion for execution.Control. + + The publication assertions cover: + + - idempotent status; + - existing outputs; + - abort; + - schema finalization; + - final schema digest; + - output metadata. + + Every production ClickHouse transaction supports these. Make them mandatory on the retained transaction contract or eliminate them with the adapter layer. Optional capability probing is making the happy path + significantly harder to read. + + The snapshot manifest assertion is a legacy recovery path and should become an explicit callback temporarily, then be deleted when manifest-only legacy generations are no longer supported. + + The Control assertion omits Run, even though RecipeControl requires it. Delete it when RecipeControl becomes concrete. + + # Expected impact + + A conservative estimate for the federation, worker, adapter, and associated test cleanup is: + + - 2,500–4,000 net Go lines removed; + - 68 named internal interfaces reduced to approximately 24; + - all 14 hidden interface literals/assertions removed; + - fewer background goroutines and publication state transitions; + - fewer Arango scans during dataframe reads; + - single-table ClickHouse queries instead of federation unions; + - substantially smaller mocks and fixtures; + - no change to the 23 typed FHIR Go roots. + + # Recommended execution order + + 1. Introduce the three shared Arango primitives and remove duplicate client method sets. + 2. Delete CostPolicy and the two unused server capability resolver interfaces. + 3. Replace Explorer capability evidence interfaces with one evidence value. + 4. Delete federation and change dataframe GraphQL reads to explicit-project semantics. + 5. Delete the queued publication worker and standalone materialization mutations. + 6. Collapse the ClickHouse publication adapter ladder while preserving direct receipt publication. + 7. Convert one-method bulk, dataset, recipe, and resolver interfaces to functions. + 8. Collapse the two recipe registry hierarchies. + 9. Collapse lifecycle.Store to the concrete Explorer service. + 10. Regenerate GraphQL, run all tests, then update Graphify. + + The first major implementation slice should be steps 1–4. That removes the clearest duplication and the abandoned federation feature before we reshape publication around the one remaining product workflow. diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 00000000..2f6e49e1 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,20 @@ +# Architectural TODOs + +## Explorer persistence follow-up + +The package audit removed the lifecycle mirror interface, repository-config +read/write methods, duplicate create methods, and generic revision transition. +`lifecycle.Service` now uses `explorer.Service` directly and its implementation +is organized by query, authoring, preview, interactive publication, and +repository publication workflows. + +The remaining `internal/explorer.Store` has 11 methods spanning owner drafts, +immutable receipts/revisions, atomic interactive publication, and repository +activation. Revisit it only after deciding whether these records will continue +to share one Arango transaction boundary; do not replace it mechanically with +many one-method interfaces for tests. + +The old `loom_repository_explorer_configs` collection is read-only migration +input for one compatibility window. Startup restores a missing canonical +default owner from its active revision. Remove the legacy collection spec and +migration after deployed instances have crossed that window. diff --git a/docs/frontend/explorer-v2-capability-authoring.md b/docs/frontend/explorer-v2-capability-authoring.md index f6220f93..3f93ca38 100644 --- a/docs/frontend/explorer-v2-capability-authoring.md +++ b/docs/frontend/explorer-v2-capability-authoring.md @@ -26,10 +26,10 @@ Requests use the existing authenticated session and project authorization. | --- | --- | --- | | Capability metadata | `GET .../capability` | supported operations, preview limits, and feature flags | | Candidate search | `POST .../suggestions` | `{snapshotToken, nodeId, query?}` → matching candidate definitions | -| Candidate suggestions | `GET .../capabilities/{snapshotToken}/candidates/{candidateId}/suggestions` | `ExplorerCandidateSuggestions` | | Builder read | `GET .../builder` | `ExplorerBuilderState` | -| Builder reconcile | `POST .../builder` | `{workspace, snapshotToken}` → `ExplorerBuilderReceipt` | -| Compile alias | `POST .../compile` | exact alias of POST Builder | +| Builder commands | `POST .../commands` | atomic server-owned draft changes | +| Builder reconcile | `POST .../reconcile` | compile the exact persisted draft into an `ExplorerBuilderReceipt` | +| Direct compile | `POST .../builder` | `{workspace, snapshotToken}` → `ExplorerBuilderReceipt` | | Preview | `POST .../preview` | `{receiptId, outputId, limit?}` → `ExplorerBuilderPreview` | | Publish | `POST .../publish` | `{receiptId}` → `ExplorerBuilderPublication` | @@ -127,7 +127,7 @@ compile it, persist an immutable receipt, and return: "snapshotToken": "sha256:...", "generation": "generation-...", "intentDigest": "sha256:...", - "compilerVersion": "loom.explorer.compiler/v7+authoring-v2-native-4", + "compilerVersion": "loom.explorer.compiler/v10+authoring-v2-native-5", "outputs": [], "diagnostics": [] } diff --git a/docs/loom-reliability-contract.md b/docs/loom-reliability-contract.md index fba7e040..fbf17e73 100644 --- a/docs/loom-reliability-contract.md +++ b/docs/loom-reliability-contract.md @@ -1,7 +1,7 @@ # Loom Reliability Contract This document freezes the compatibility contract for versioned dataframe -publication, immutable ingest, project releases, federation, and ETL. New code +publication, immutable ingest, project releases, and ETL. New code must use these identities and states. The public APIs have no name-only or default-contract compatibility path. @@ -10,15 +10,13 @@ default-contract compatibility path. A dataframe is identified by the exact selector `(recipeName, translationVersion, outputName)`. Serialized selectors use the fields `recipe`, `translationVersion`, and `output`; no component is inferred by -internal publication or federation code. +internal publication or read code. A recipe version is identified by `(name, translationVersion)`. Versions are retained. A version may be replaced only before its first durable execution is created; after that point its content digest is immutable. -An immutable FHIR generation is identified by `(projectID, gitCommit)`. A -resource upload within it is identified by `(projectID, gitCommit, -resourceType)`, and its checksum is part of the idempotency contract. +An immutable FHIR generation is identified by `(projectID, gitCommit)`. ## States @@ -26,9 +24,6 @@ resourceType)`, and its checksum is part of the idempotency contract. - Publication: `QUEUED`, `RUNNING`, `VALIDATING`, `PUBLISHED`, `FAILED`. - Project release: `ACTIVE`; visibility is determined solely by the atomic active-release pointer. -- Federation availability: `AVAILABLE`, `DEGRADED`, `UNAVAILABLE`. -- Authorized project state: `CURRENT`, `STALE`, `BUILDING`, `FAILED`, - `MISSING`, `EXCLUDED`. Historical storage rows may be normalized by the storage adapters while they are retired, but new workflows use only `PUBLISHED`. @@ -68,31 +63,19 @@ Dataset discovery, rows, aggregate, aggregations, and export require a complete `selector`. No `dataType`, materialization-ID, or default recipe resolution is accepted. -Dataset metadata exposes the exact selector, active contract version, -availability, completeness, included and expected project counts, and an -authorization-filtered project status list. Project status includes project -state, generation, execution ID, timestamps, error code, and retryability. -Unauthorized project identities are never returned. +Dataset metadata exposes the exact selector and active project publication. +Every read requires an explicit authorized project; cross-project discovery +and schema reconciliation are not supported. Explorer Preview and Publish are +the supported materialization workflow. -Operator operations support: +## Ingest API -1. starting an asynchronous exact-version materialization; -2. polling its durable execution; -3. activating a completed project release; and -4. activating a project release containing the exact published selectors. - -## Snapshot HTTP API - -Snapshot operations are idempotent: - -1. create or resume a generation keyed by Git commit; -2. upload one resource type with checksum verification; -3. finalize a complete generation as `STAGED`; -4. inspect generation status; and -5. abort an unfinalized generation without changing the active release. - -Repeating an operation with identical content succeeds. Different content for -the same generation/resource identity returns a non-retryable conflict. +ETL uploads a complete multipart generation through +`POST /api/v1/datasets/{project}/generations/{generation}` and activates it +through the corresponding `/activate` operation after dataframe publication. +The operator CLI retains both mutable local loading and immutable generation +loading. Loom does not expose the retired per-resource snapshot or standalone +release-management HTTP workflows. ## Error taxonomy @@ -101,9 +84,6 @@ Public structured errors use stable codes: - `DYNAMIC_SCHEMA_DRIFT`: runtime/project-specific fields cannot be reconciled; - `RECIPE_CONTRACT_VIOLATION`: a recipe output violates its declared contract; - `PUBLICATION_FAILED`: materialization or output verification failed; -- `FEDERATION_INCOMPATIBLE`: published sources cannot share a logical schema; -- `CHECKSUM_CONFLICT`: an immutable resource key was reused with new content; -- `GENERATION_INCOMPLETE`: finalize was requested before all uploads completed; - `RELEASE_REQUIREMENTS_UNMET`: required selectors are not published and queryable for the staged generation; - `RELEASE_ACTIVATION_CONFLICT`: compare-and-swap observed a newer active @@ -120,9 +100,6 @@ HTTP status or message. for release activation. - `LOOM_PUBLICATION_WORKER_LEASE`: publication worker lease duration. - `LOOM_PUBLICATION_MAX_ATTEMPTS`: bounded publication retry count. -- `LOOM_SNAPSHOT_RETENTION`: inactive generation/release retention duration. -- `LOOM_ETL_LEGACY_MUTABLE_UPLOAD`: ETL rollout escape hatch; snapshot mode is - the default after integration acceptance. Configuration files may expose equivalent structured fields, but environment names and meanings remain stable. diff --git a/generated/graphql/graph/executor/prelude.generated.go b/generated/graphql/graph/executor/prelude.generated.go index e7edb2ca..3de3a270 100644 --- a/generated/graphql/graph/executor/prelude.generated.go +++ b/generated/graphql/graph/executor/prelude.generated.go @@ -1901,24 +1901,6 @@ func (ec *executionContext) marshalOFloat2ᚖfloat64(ctx context.Context, sel as return graphql.WrapContextMarshaler(ctx, res) } -func (ec *executionContext) unmarshalOID2ᚖstring(ctx context.Context, v any) (*string, error) { - if v == nil { - return nil, nil - } - res, err := graphql.UnmarshalID(v) - return &res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalOID2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { - if v == nil { - return graphql.Null - } - _ = sel - _ = ctx - res := graphql.MarshalID(*v) - return res -} - func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) { if v == nil { return nil, nil diff --git a/generated/graphql/graph/executor/root_.generated.go b/generated/graphql/graph/executor/root_.generated.go index a86af822..51037e64 100644 --- a/generated/graphql/graph/executor/root_.generated.go +++ b/generated/graphql/graph/executor/root_.generated.go @@ -607,25 +607,20 @@ type ComplexityRoot struct { } DataframeMaterialization struct { - Availability func(childComplexity int) int - Columns func(childComplexity int) int - Completeness func(childComplexity int) int - CreatedAt func(childComplexity int) int - DatasetGeneration func(childComplexity int) int - Error func(childComplexity int) int - ErrorCode func(childComplexity int) int - ErrorRetryable func(childComplexity int) int - ExpectedProjectCount func(childComplexity int) int - ID func(childComplexity int) int - IncludedProjectCount func(childComplexity int) int - Name func(childComplexity int) int - ProjectID func(childComplexity int) int - ProjectStatuses func(childComplexity int) int - ReadyAt func(childComplexity int) int - Revision func(childComplexity int) int - RowCount func(childComplexity int) int - Selector func(childComplexity int) int - State func(childComplexity int) int + Columns func(childComplexity int) int + CreatedAt func(childComplexity int) int + DatasetGeneration func(childComplexity int) int + Error func(childComplexity int) int + ErrorCode func(childComplexity int) int + ErrorRetryable func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + ProjectID func(childComplexity int) int + ReadyAt func(childComplexity int) int + Revision func(childComplexity int) int + RowCount func(childComplexity int) int + Selector func(childComplexity int) int + State func(childComplexity int) int } DataframeOptimizationDecision struct { @@ -650,17 +645,6 @@ type ComplexityRoot struct { HasNextPage func(childComplexity int) int } - DataframeProjectStatus struct { - CreatedAt func(childComplexity int) int - ErrorCode func(childComplexity int) int - ExecutionID func(childComplexity int) int - Generation func(childComplexity int) int - Project func(childComplexity int) int - Retryable func(childComplexity int) int - State func(childComplexity int) int - UpdatedAt func(childComplexity int) int - } - DataframeQueryDiagnostics struct { ArangoQueryMs func(childComplexity int) int CompilationMs func(childComplexity int) int @@ -1868,14 +1852,11 @@ type ComplexityRoot struct { } Mutation struct { - ActivateProjectRelease func(childComplexity int, input model.ActivateProjectReleaseInput) int - MaterializeDataframeRecipeBundle func(childComplexity int, input model.MaterializeDataframeRecipeInput) int - PreviewDataframeRecipe func(childComplexity int, input model.PreviewDataframeRecipeInput) int - RegisterDataframeRecipeRevision func(childComplexity int, input model.RegisterDataframeRecipeRevisionInput) int - RunDataframeRecipe func(childComplexity int, input model.RunDataframeRecipeInput) int - RunFhirDataframe func(childComplexity int, input model.FhirDataframeInput, limit *int) int - StartDataframeMaterialization func(childComplexity int, input model.StartDataframeMaterializationInput) int - ValidateDataframeRecipe func(childComplexity int, input model.ValidateDataframeRecipeInput) int + PreviewDataframeRecipe func(childComplexity int, input model.PreviewDataframeRecipeInput) int + RegisterDataframeRecipeRevision func(childComplexity int, input model.RegisterDataframeRecipeRevisionInput) int + RunDataframeRecipe func(childComplexity int, input model.RunDataframeRecipeInput) int + RunFhirDataframe func(childComplexity int, input model.FhirDataframeInput, limit *int) int + ValidateDataframeRecipe func(childComplexity int, input model.ValidateDataframeRecipeInput) int } Narrative struct { @@ -2330,14 +2311,6 @@ type ComplexityRoot struct { ResourceType func(childComplexity int) int } - ProjectRelease struct { - Generation func(childComplexity int) int - ID func(childComplexity int) int - Project func(childComplexity int) int - Revision func(childComplexity int) int - State func(childComplexity int) int - } - Quantity struct { Code func(childComplexity int) int Comparator func(childComplexity int) int @@ -2361,7 +2334,6 @@ type ComplexityRoot struct { DataframeAggregate func(childComplexity int, input model.DataframeAggregateInput) int DataframeAggregations func(childComplexity int, input model.DataframeAggregationsInput) int DataframeDataset func(childComplexity int, input model.DataframeDatasetInput) int - DataframeDatasets func(childComplexity int) int DataframeRecipeExecution func(childComplexity int, id string) int DataframeRecipeRevision func(childComplexity int, projectID string, name string, digest string) int DataframeRecipeRevisions func(childComplexity int, projectID string, name string) int @@ -5549,24 +5521,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.DataframeCompilerPlanDiagnostics.TraversalSets(childComplexity), true - case "DataframeMaterialization.availability": - if e.ComplexityRoot.DataframeMaterialization.Availability == nil { - break - } - - return e.ComplexityRoot.DataframeMaterialization.Availability(childComplexity), true case "DataframeMaterialization.columns": if e.ComplexityRoot.DataframeMaterialization.Columns == nil { break } return e.ComplexityRoot.DataframeMaterialization.Columns(childComplexity), true - case "DataframeMaterialization.completeness": - if e.ComplexityRoot.DataframeMaterialization.Completeness == nil { - break - } - - return e.ComplexityRoot.DataframeMaterialization.Completeness(childComplexity), true case "DataframeMaterialization.createdAt": if e.ComplexityRoot.DataframeMaterialization.CreatedAt == nil { break @@ -5597,24 +5557,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.DataframeMaterialization.ErrorRetryable(childComplexity), true - case "DataframeMaterialization.expectedProjectCount": - if e.ComplexityRoot.DataframeMaterialization.ExpectedProjectCount == nil { - break - } - - return e.ComplexityRoot.DataframeMaterialization.ExpectedProjectCount(childComplexity), true case "DataframeMaterialization.id": if e.ComplexityRoot.DataframeMaterialization.ID == nil { break } return e.ComplexityRoot.DataframeMaterialization.ID(childComplexity), true - case "DataframeMaterialization.includedProjectCount": - if e.ComplexityRoot.DataframeMaterialization.IncludedProjectCount == nil { - break - } - - return e.ComplexityRoot.DataframeMaterialization.IncludedProjectCount(childComplexity), true case "DataframeMaterialization.name": if e.ComplexityRoot.DataframeMaterialization.Name == nil { break @@ -5627,12 +5575,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.DataframeMaterialization.ProjectID(childComplexity), true - case "DataframeMaterialization.projectStatuses": - if e.ComplexityRoot.DataframeMaterialization.ProjectStatuses == nil { - break - } - - return e.ComplexityRoot.DataframeMaterialization.ProjectStatuses(childComplexity), true case "DataframeMaterialization.readyAt": if e.ComplexityRoot.DataframeMaterialization.ReadyAt == nil { break @@ -5745,55 +5687,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.DataframePageInfo.HasNextPage(childComplexity), true - case "DataframeProjectStatus.createdAt": - if e.ComplexityRoot.DataframeProjectStatus.CreatedAt == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.CreatedAt(childComplexity), true - case "DataframeProjectStatus.errorCode": - if e.ComplexityRoot.DataframeProjectStatus.ErrorCode == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.ErrorCode(childComplexity), true - case "DataframeProjectStatus.executionId": - if e.ComplexityRoot.DataframeProjectStatus.ExecutionID == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.ExecutionID(childComplexity), true - case "DataframeProjectStatus.generation": - if e.ComplexityRoot.DataframeProjectStatus.Generation == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.Generation(childComplexity), true - case "DataframeProjectStatus.project": - if e.ComplexityRoot.DataframeProjectStatus.Project == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.Project(childComplexity), true - case "DataframeProjectStatus.retryable": - if e.ComplexityRoot.DataframeProjectStatus.Retryable == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.Retryable(childComplexity), true - case "DataframeProjectStatus.state": - if e.ComplexityRoot.DataframeProjectStatus.State == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.State(childComplexity), true - case "DataframeProjectStatus.updatedAt": - if e.ComplexityRoot.DataframeProjectStatus.UpdatedAt == nil { - break - } - - return e.ComplexityRoot.DataframeProjectStatus.UpdatedAt(childComplexity), true - case "DataframeQueryDiagnostics.arangoQueryMs": if e.ComplexityRoot.DataframeQueryDiagnostics.ArangoQueryMs == nil { break @@ -11653,28 +11546,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.Money.XValue(childComplexity), true - case "Mutation.activateProjectRelease": - if e.ComplexityRoot.Mutation.ActivateProjectRelease == nil { - break - } - - args, err := ec.field_Mutation_activateProjectRelease_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.ComplexityRoot.Mutation.ActivateProjectRelease(childComplexity, args["input"].(model.ActivateProjectReleaseInput)), true - case "Mutation.materializeDataframeRecipeBundle": - if e.ComplexityRoot.Mutation.MaterializeDataframeRecipeBundle == nil { - break - } - - args, err := ec.field_Mutation_materializeDataframeRecipeBundle_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.ComplexityRoot.Mutation.MaterializeDataframeRecipeBundle(childComplexity, args["input"].(model.MaterializeDataframeRecipeInput)), true case "Mutation.previewDataframeRecipe": if e.ComplexityRoot.Mutation.PreviewDataframeRecipe == nil { break @@ -11719,17 +11590,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.Mutation.RunFhirDataframe(childComplexity, args["input"].(model.FhirDataframeInput), args["limit"].(*int)), true - case "Mutation.startDataframeMaterialization": - if e.ComplexityRoot.Mutation.StartDataframeMaterialization == nil { - break - } - - args, err := ec.field_Mutation_startDataframeMaterialization_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.ComplexityRoot.Mutation.StartDataframeMaterialization(childComplexity, args["input"].(model.StartDataframeMaterializationInput)), true case "Mutation.validateDataframeRecipe": if e.ComplexityRoot.Mutation.ValidateDataframeRecipe == nil { break @@ -14114,37 +13974,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.ProcedurePerformer.ResourceType(childComplexity), true - case "ProjectRelease.generation": - if e.ComplexityRoot.ProjectRelease.Generation == nil { - break - } - - return e.ComplexityRoot.ProjectRelease.Generation(childComplexity), true - case "ProjectRelease.id": - if e.ComplexityRoot.ProjectRelease.ID == nil { - break - } - - return e.ComplexityRoot.ProjectRelease.ID(childComplexity), true - case "ProjectRelease.project": - if e.ComplexityRoot.ProjectRelease.Project == nil { - break - } - - return e.ComplexityRoot.ProjectRelease.Project(childComplexity), true - case "ProjectRelease.revision": - if e.ComplexityRoot.ProjectRelease.Revision == nil { - break - } - - return e.ComplexityRoot.ProjectRelease.Revision(childComplexity), true - case "ProjectRelease.state": - if e.ComplexityRoot.ProjectRelease.State == nil { - break - } - - return e.ComplexityRoot.ProjectRelease.State(childComplexity), true - case "Quantity.code": if e.ComplexityRoot.Quantity.Code == nil { break @@ -14285,12 +14114,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.Query.DataframeDataset(childComplexity, args["input"].(model.DataframeDatasetInput)), true - case "Query.dataframeDatasets": - if e.ComplexityRoot.Query.DataframeDatasets == nil { - break - } - - return e.ComplexityRoot.Query.DataframeDatasets(childComplexity), true case "Query.dataframeRecipeExecution": if e.ComplexityRoot.Query.DataframeRecipeExecution == nil { break @@ -19776,7 +19599,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { opCtx := graphql.GetOperationContext(ctx) ec := newExecutionContext(opCtx, e, make(chan graphql.DeferredResult)) inputUnmarshalMap := graphql.BuildUnmarshalerMap( - ec.unmarshalInputActivateProjectReleaseInput, ec.unmarshalInputDataframeAggregateInput, ec.unmarshalInputDataframeAggregationSpecInput, ec.unmarshalInputDataframeAggregationsInput, @@ -19802,12 +19624,10 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputFhirPivotInput, ec.unmarshalInputFhirRepresentativeSliceInput, ec.unmarshalInputFhirTraversalStepInput, - ec.unmarshalInputMaterializeDataframeRecipeInput, ec.unmarshalInputPreflightDataframeRecipeInput, ec.unmarshalInputPreviewDataframeRecipeInput, ec.unmarshalInputRegisterDataframeRecipeRevisionInput, ec.unmarshalInputRunDataframeRecipeInput, - ec.unmarshalInputStartDataframeMaterializationInput, ec.unmarshalInputValidateDataframeRecipeInput, ) first := true @@ -19885,7 +19705,6 @@ func newExecutionContext( var sources = []*ast.Source{ {Name: "../../../../internal/api/graphql/graph/schema/schema.graphqls", Input: `type Query { - dataframeDatasets: [DataframeMaterialization!]! projectDataframeDatasets(projectId: String!): [DataframeMaterialization!]! dataframeDataset(input: DataframeDatasetInput!): DataframeMaterialization dataframeRows(input: DataframeRowsInput!): DataframeRowConnection! @@ -19910,34 +19729,9 @@ type Mutation { runDataframeRecipe(input: RunDataframeRecipeInput!): DataframeRecipeResult! validateDataframeRecipe(input: ValidateDataframeRecipeInput!): DataframeRecipeValidation! previewDataframeRecipe(input: PreviewDataframeRecipeInput!): DataframeRecipePreview! - materializeDataframeRecipeBundle(input: MaterializeDataframeRecipeInput!): DataframeRecipeExecution! - startDataframeMaterialization(input: StartDataframeMaterializationInput!): DataframeRecipeExecution! - activateProjectRelease(input: ActivateProjectReleaseInput!): ProjectRelease! registerDataframeRecipeRevision(input: RegisterDataframeRecipeRevisionInput!): DataframeRecipeRevision! } -input StartDataframeMaterializationInput { - project: String! - generation: String! - selector: DataframeSelectorInput! -} - -input ActivateProjectReleaseInput { - project: String! - releaseId: ID! - expectedActiveRevision: String -} - -type ProjectRelease { - id: ID! - project: String! - generation: String! - revision: String! - state: ProjectReleaseState! -} - -enum ProjectReleaseState { ACTIVE } - input RegisterDataframeRecipeRevisionInput { projectId: String! name: String! @@ -19991,11 +19785,6 @@ input RunDataframeRecipeInput { outputs: [String!] } -input MaterializeDataframeRecipeInput { - name: String! - bindings: DataframeRecipeBindingsInput! -} - type DataframeRecipeValidation { name: String! recipeDigest: String! @@ -20246,11 +20035,6 @@ type DataframeMaterialization { errorCode: String errorRetryable: Boolean selector: DataframeSelector - availability: DataframeAvailability - completeness: Float - includedProjectCount: Int - expectedProjectCount: Int - projectStatuses: [DataframeProjectStatus!]! } input DataframeSelectorInput { @@ -20265,33 +20049,8 @@ type DataframeSelector { output: String! } -enum DataframeAvailability { - AVAILABLE - DEGRADED - UNAVAILABLE -} - -enum DataframeProjectState { - CURRENT - STALE - BUILDING - FAILED - MISSING - EXCLUDED -} - -type DataframeProjectStatus { - project: String! - state: DataframeProjectState! - generation: String - executionId: ID - createdAt: String - updatedAt: String - errorCode: String - retryable: Boolean! -} - input DataframeDatasetInput { + projectId: String! selector: DataframeSelectorInput! } @@ -20307,6 +20066,7 @@ input DataframeSortInput { } input DataframeRowsInput { + projectId: String! selector: DataframeSelectorInput! columns: [String!] filters: [DataframeFilterInput!] @@ -20329,6 +20089,7 @@ type DataframeRowConnection { } input DataframeAggregateInput { + projectId: String! selector: DataframeSelectorInput! groupBy: [String!] filters: [DataframeFilterInput!] @@ -20353,6 +20114,7 @@ input DataframeAggregationSpecInput { } input DataframeAggregationsInput { + projectId: String! selector: DataframeSelectorInput! filters: [DataframeFilterInput!] specs: [DataframeAggregationSpecInput!]! @@ -24427,16 +24189,6 @@ func (ec *executionContext) childFields_DataframeMaterialization(ctx context.Con return ec.fieldContext_DataframeMaterialization_errorRetryable(ctx, field) case "selector": return ec.fieldContext_DataframeMaterialization_selector(ctx, field) - case "availability": - return ec.fieldContext_DataframeMaterialization_availability(ctx, field) - case "completeness": - return ec.fieldContext_DataframeMaterialization_completeness(ctx, field) - case "includedProjectCount": - return ec.fieldContext_DataframeMaterialization_includedProjectCount(ctx, field) - case "expectedProjectCount": - return ec.fieldContext_DataframeMaterialization_expectedProjectCount(ctx, field) - case "projectStatuses": - return ec.fieldContext_DataframeMaterialization_projectStatuses(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type DataframeMaterialization", field.Name) } @@ -24485,28 +24237,6 @@ func (ec *executionContext) childFields_DataframePageInfo(ctx context.Context, f return nil, fmt.Errorf("no field named %q was found under type DataframePageInfo", field.Name) } -func (ec *executionContext) childFields_DataframeProjectStatus(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "project": - return ec.fieldContext_DataframeProjectStatus_project(ctx, field) - case "state": - return ec.fieldContext_DataframeProjectStatus_state(ctx, field) - case "generation": - return ec.fieldContext_DataframeProjectStatus_generation(ctx, field) - case "executionId": - return ec.fieldContext_DataframeProjectStatus_executionId(ctx, field) - case "createdAt": - return ec.fieldContext_DataframeProjectStatus_createdAt(ctx, field) - case "updatedAt": - return ec.fieldContext_DataframeProjectStatus_updatedAt(ctx, field) - case "errorCode": - return ec.fieldContext_DataframeProjectStatus_errorCode(ctx, field) - case "retryable": - return ec.fieldContext_DataframeProjectStatus_retryable(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type DataframeProjectStatus", field.Name) -} - func (ec *executionContext) childFields_DataframeQueryDiagnostics(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "inputResolutionMs": @@ -27807,22 +27537,6 @@ func (ec *executionContext) childFields_ProcedurePerformer(ctx context.Context, return nil, fmt.Errorf("no field named %q was found under type ProcedurePerformer", field.Name) } -func (ec *executionContext) childFields_ProjectRelease(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_ProjectRelease_id(ctx, field) - case "project": - return ec.fieldContext_ProjectRelease_project(ctx, field) - case "generation": - return ec.fieldContext_ProjectRelease_generation(ctx, field) - case "revision": - return ec.fieldContext_ProjectRelease_revision(ctx, field) - case "state": - return ec.fieldContext_ProjectRelease_state(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type ProjectRelease", field.Name) -} - func (ec *executionContext) childFields_Quantity(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "_code": diff --git a/generated/graphql/graph/executor/schema.generated.go b/generated/graphql/graph/executor/schema.generated.go index e3cbdf5a..8411767a 100644 --- a/generated/graphql/graph/executor/schema.generated.go +++ b/generated/graphql/graph/executor/schema.generated.go @@ -25,13 +25,9 @@ type MutationResolver interface { RunDataframeRecipe(ctx context.Context, input model.RunDataframeRecipeInput) (*model.DataframeRecipeResult, error) ValidateDataframeRecipe(ctx context.Context, input model.ValidateDataframeRecipeInput) (*model.DataframeRecipeValidation, error) PreviewDataframeRecipe(ctx context.Context, input model.PreviewDataframeRecipeInput) (*model.DataframeRecipePreview, error) - MaterializeDataframeRecipeBundle(ctx context.Context, input model.MaterializeDataframeRecipeInput) (*model.DataframeRecipeExecution, error) - StartDataframeMaterialization(ctx context.Context, input model.StartDataframeMaterializationInput) (*model.DataframeRecipeExecution, error) - ActivateProjectRelease(ctx context.Context, input model.ActivateProjectReleaseInput) (*model.ProjectRelease, error) RegisterDataframeRecipeRevision(ctx context.Context, input model.RegisterDataframeRecipeRevisionInput) (*model.DataframeRecipeRevision, error) } type QueryResolver interface { - DataframeDatasets(ctx context.Context) ([]*model.DataframeMaterialization, error) ProjectDataframeDatasets(ctx context.Context, projectID string) ([]*model.DataframeMaterialization, error) DataframeDataset(ctx context.Context, input model.DataframeDatasetInput) (*model.DataframeMaterialization, error) DataframeRows(ctx context.Context, input model.DataframeRowsInput) (*model.DataframeRowConnection, error) @@ -73,34 +69,6 @@ type QueryResolver interface { // region ***************************** args.gotpl ***************************** -func (ec *executionContext) field_Mutation_activateProjectRelease_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", - func(ctx context.Context, v any) (model.ActivateProjectReleaseInput, error) { - return ec.unmarshalNActivateProjectReleaseInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐActivateProjectReleaseInput(ctx, v) - }) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} - -func (ec *executionContext) field_Mutation_materializeDataframeRecipeBundle_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", - func(ctx context.Context, v any) (model.MaterializeDataframeRecipeInput, error) { - return ec.unmarshalNMaterializeDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐMaterializeDataframeRecipeInput(ctx, v) - }) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_previewDataframeRecipe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -165,20 +133,6 @@ func (ec *executionContext) field_Mutation_runFhirDataframe_args(ctx context.Con return args, nil } -func (ec *executionContext) field_Mutation_startDataframeMaterialization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", - func(ctx context.Context, v any) (model.StartDataframeMaterializationInput, error) { - return ec.unmarshalNStartDataframeMaterializationInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐStartDataframeMaterializationInput(ctx, v) - }) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_validateDataframeRecipe_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -2006,130 +1960,6 @@ func (ec *executionContext) fieldContext_DataframeMaterialization_selector(_ con return fc, nil } -func (ec *executionContext) _DataframeMaterialization_availability(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeMaterialization_availability(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.Availability, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeAvailability) graphql.Marshaler { - return ec.marshalODataframeAvailability2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeAvailability(ctx, selections, v) - }, - true, - false, - ) -} -func (ec *executionContext) fieldContext_DataframeMaterialization_availability(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type DataframeAvailability does not have child fields")) -} - -func (ec *executionContext) _DataframeMaterialization_completeness(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeMaterialization_completeness(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.Completeness, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *float64) graphql.Marshaler { - return ec.marshalOFloat2ᚖfloat64(ctx, selections, v) - }, - true, - false, - ) -} -func (ec *executionContext) fieldContext_DataframeMaterialization_completeness(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type Float does not have child fields")) -} - -func (ec *executionContext) _DataframeMaterialization_includedProjectCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeMaterialization_includedProjectCount(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.IncludedProjectCount, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *int) graphql.Marshaler { - return ec.marshalOInt2ᚖint(ctx, selections, v) - }, - true, - false, - ) -} -func (ec *executionContext) fieldContext_DataframeMaterialization_includedProjectCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type Int does not have child fields")) -} - -func (ec *executionContext) _DataframeMaterialization_expectedProjectCount(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeMaterialization_expectedProjectCount(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.ExpectedProjectCount, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *int) graphql.Marshaler { - return ec.marshalOInt2ᚖint(ctx, selections, v) - }, - true, - false, - ) -} -func (ec *executionContext) fieldContext_DataframeMaterialization_expectedProjectCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeMaterialization", field, false, false, errors.New("field of type Int does not have child fields")) -} - -func (ec *executionContext) _DataframeMaterialization_projectStatuses(ctx context.Context, field graphql.CollectedField, obj *model.DataframeMaterialization) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeMaterialization_projectStatuses(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.ProjectStatuses, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeProjectStatus) graphql.Marshaler { - return ec.marshalNDataframeProjectStatus2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeProjectStatusᚄ(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeMaterialization_projectStatuses(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeMaterialization", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeProjectStatus(ctx, field) - }, - } - return fc, nil -} - func (ec *executionContext) _DataframeOptimizationDecision_rule(ctx context.Context, field graphql.CollectedField, obj *model.DataframeOptimizationDecision) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -2438,429 +2268,245 @@ func (ec *executionContext) fieldContext_DataframePageInfo_endCursor(_ context.C return graphql.NewScalarFieldContext("DataframePageInfo", field, false, false, errors.New("field of type String does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_project(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_inputResolutionMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_project(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_inputResolutionMs(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.Project, nil + return obj.InputResolutionMs, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { - return ec.marshalNString2string(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_project(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type String does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_inputResolutionMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_state(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_requestPreparationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_state(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_requestPreparationMs(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.State, nil + return obj.RequestPreparationMs, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v model.DataframeProjectState) graphql.Marshaler { - return ec.marshalNDataframeProjectState2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeProjectState(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type DataframeProjectState does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_requestPreparationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_generation(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_compilationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_generation(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_compilationMs(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.Generation, nil + return obj.CompilationMs, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { - return ec.marshalOString2ᚖstring(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) }, true, - false, + true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_generation(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type String does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_compilationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_executionId(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_arangoQueryMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_executionId(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_arangoQueryMs(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.ExecutionID, nil + return obj.ArangoQueryMs, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { - return ec.marshalOID2ᚖstring(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) }, true, - false, + true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_executionId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type ID does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_arangoQueryMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_rowMaterializationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_createdAt(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.CreatedAt, nil + return obj.RowMaterializationMs, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { - return ec.marshalOString2ᚖstring(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) }, true, - false, + true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type String does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_updatedAt(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_resultAssemblyMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_updatedAt(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.UpdatedAt, nil + return obj.ResultAssemblyMs, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { - return ec.marshalOString2ᚖstring(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) }, true, - false, + true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type String does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_errorCode(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_totalMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_errorCode(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_totalMs(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.ErrorCode, nil + return obj.TotalMs, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { - return ec.marshalOString2ᚖstring(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { + return ec.marshalNFloat2float64(ctx, selections, v) }, true, - false, + true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_errorCode(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type String does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_totalMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) } -func (ec *executionContext) _DataframeProjectStatus_retryable(ctx context.Context, field graphql.CollectedField, obj *model.DataframeProjectStatus) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeQueryDiagnostics_plan(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeProjectStatus_retryable(ctx, field) + return ec.fieldContext_DataframeQueryDiagnostics_plan(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.Retryable, nil + return obj.Plan, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { - return ec.marshalNBoolean2bool(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeCompilerPlanDiagnostics) graphql.Marshaler { + return ec.marshalNDataframeCompilerPlanDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeCompilerPlanDiagnostics(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_DataframeProjectStatus_retryable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeProjectStatus", field, false, false, errors.New("field of type Boolean does not have child fields")) +func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_plan(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataframeQueryDiagnostics", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeCompilerPlanDiagnostics(ctx, field) + }, + } + return fc, nil } -func (ec *executionContext) _DataframeQueryDiagnostics_inputResolutionMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeRecipeArangoAssessment_plans(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_inputResolutionMs(ctx, field) + return ec.fieldContext_DataframeRecipeArangoAssessment_plans(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.InputResolutionMs, nil + return obj.Plans, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { - return ec.marshalNFloat2float64(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainPlanEstimate) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplainPlanEstimate2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExplainPlanEstimateᚄ(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_inputResolutionMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) +func (ec *executionContext) fieldContext_DataframeRecipeArangoAssessment_plans(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DataframeRecipeArangoAssessment", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeRecipeExplainPlanEstimate(ctx, field) + }, + } + return fc, nil } -func (ec *executionContext) _DataframeQueryDiagnostics_requestPreparationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { +func (ec *executionContext) _DataframeRecipeArangoAssessment_fullCollectionScans(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_requestPreparationMs(ctx, field) + return ec.fieldContext_DataframeRecipeArangoAssessment_fullCollectionScans(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.RequestPreparationMs, nil + return obj.FullCollectionScans, nil }, nil, - func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { - return ec.marshalNFloat2float64(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_requestPreparationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) -} - -func (ec *executionContext) _DataframeQueryDiagnostics_compilationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_compilationMs(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.CompilationMs, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { - return ec.marshalNFloat2float64(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_compilationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) -} - -func (ec *executionContext) _DataframeQueryDiagnostics_arangoQueryMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_arangoQueryMs(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.ArangoQueryMs, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { - return ec.marshalNFloat2float64(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_arangoQueryMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) -} - -func (ec *executionContext) _DataframeQueryDiagnostics_rowMaterializationMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.RowMaterializationMs, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { - return ec.marshalNFloat2float64(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_rowMaterializationMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) -} - -func (ec *executionContext) _DataframeQueryDiagnostics_resultAssemblyMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.ResultAssemblyMs, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { - return ec.marshalNFloat2float64(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_resultAssemblyMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) -} - -func (ec *executionContext) _DataframeQueryDiagnostics_totalMs(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_totalMs(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.TotalMs, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v float64) graphql.Marshaler { - return ec.marshalNFloat2float64(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_totalMs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("DataframeQueryDiagnostics", field, false, false, errors.New("field of type Float does not have child fields")) -} - -func (ec *executionContext) _DataframeQueryDiagnostics_plan(ctx context.Context, field graphql.CollectedField, obj *model.DataframeQueryDiagnostics) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeQueryDiagnostics_plan(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.Plan, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeCompilerPlanDiagnostics) graphql.Marshaler { - return ec.marshalNDataframeCompilerPlanDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeCompilerPlanDiagnostics(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeQueryDiagnostics_plan(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeQueryDiagnostics", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeCompilerPlanDiagnostics(ctx, field) - }, - } - return fc, nil -} - -func (ec *executionContext) _DataframeRecipeArangoAssessment_plans(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeRecipeArangoAssessment_plans(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.Plans, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainPlanEstimate) graphql.Marshaler { - return ec.marshalNDataframeRecipeExplainPlanEstimate2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExplainPlanEstimateᚄ(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_DataframeRecipeArangoAssessment_plans(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "DataframeRecipeArangoAssessment", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeRecipeExplainPlanEstimate(ctx, field) - }, - } - return fc, nil -} - -func (ec *executionContext) _DataframeRecipeArangoAssessment_fullCollectionScans(ctx context.Context, field graphql.CollectedField, obj *model.DataframeRecipeArangoAssessment) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_DataframeRecipeArangoAssessment_fullCollectionScans(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.FullCollectionScans, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainCollectionScan) graphql.Marshaler { - return ec.marshalNDataframeRecipeExplainCollectionScan2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExplainCollectionScanᚄ(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeRecipeExplainCollectionScan) graphql.Marshaler { + return ec.marshalNDataframeRecipeExplainCollectionScan2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExplainCollectionScanᚄ(ctx, selections, v) }, true, true, @@ -7225,465 +6871,186 @@ func (ec *executionContext) fieldContext_Mutation_runFhirDataframe(ctx context.C } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_runFhirDataframe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _Mutation_runDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_runDataframeRecipe(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().RunDataframeRecipe(ctx, fc.Args["input"].(model.RunDataframeRecipeInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeResult) graphql.Marshaler { - return ec.marshalNDataframeRecipeResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeResult(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_runDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeRecipeResult(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_runDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _Mutation_validateDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_validateDataframeRecipe(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().ValidateDataframeRecipe(ctx, fc.Args["input"].(model.ValidateDataframeRecipeInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeValidation) graphql.Marshaler { - return ec.marshalNDataframeRecipeValidation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeValidation(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_validateDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeRecipeValidation(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_validateDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _Mutation_previewDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_previewDataframeRecipe(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().PreviewDataframeRecipe(ctx, fc.Args["input"].(model.PreviewDataframeRecipeInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipePreview) graphql.Marshaler { - return ec.marshalNDataframeRecipePreview2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipePreview(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_previewDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeRecipePreview(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_previewDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _Mutation_materializeDataframeRecipeBundle(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_materializeDataframeRecipeBundle(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().MaterializeDataframeRecipeBundle(ctx, fc.Args["input"].(model.MaterializeDataframeRecipeInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeExecution) graphql.Marshaler { - return ec.marshalNDataframeRecipeExecution2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExecution(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_materializeDataframeRecipeBundle(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeRecipeExecution(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_materializeDataframeRecipeBundle_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _Mutation_startDataframeMaterialization(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_startDataframeMaterialization(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().StartDataframeMaterialization(ctx, fc.Args["input"].(model.StartDataframeMaterializationInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeExecution) graphql.Marshaler { - return ec.marshalNDataframeRecipeExecution2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExecution(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_startDataframeMaterialization(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeRecipeExecution(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_startDataframeMaterialization_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _Mutation_activateProjectRelease(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_activateProjectRelease(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().ActivateProjectRelease(ctx, fc.Args["input"].(model.ActivateProjectReleaseInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.ProjectRelease) graphql.Marshaler { - return ec.marshalNProjectRelease2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐProjectRelease(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_activateProjectRelease(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_ProjectRelease(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_activateProjectRelease_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _Mutation_registerDataframeRecipeRevision(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_registerDataframeRecipeRevision(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().RegisterDataframeRecipeRevision(ctx, fc.Args["input"].(model.RegisterDataframeRecipeRevisionInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeRevision) graphql.Marshaler { - return ec.marshalNDataframeRecipeRevision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeRevision(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_registerDataframeRecipeRevision(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeRecipeRevision(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_registerDataframeRecipeRevision_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - -func (ec *executionContext) _ProjectRelease_id(ctx context.Context, field graphql.CollectedField, obj *model.ProjectRelease) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_ProjectRelease_id(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.ID, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { - return ec.marshalNID2string(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_ProjectRelease_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("ProjectRelease", field, false, false, errors.New("field of type ID does not have child fields")) -} - -func (ec *executionContext) _ProjectRelease_project(ctx context.Context, field graphql.CollectedField, obj *model.ProjectRelease) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_ProjectRelease_project(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.Project, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { - return ec.marshalNString2string(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_ProjectRelease_project(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("ProjectRelease", field, false, false, errors.New("field of type String does not have child fields")) + if fc.Args, err = ec.field_Mutation_runFhirDataframe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil } -func (ec *executionContext) _ProjectRelease_generation(ctx context.Context, field graphql.CollectedField, obj *model.ProjectRelease) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_runDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_ProjectRelease_generation(ctx, field) + return ec.fieldContext_Mutation_runDataframeRecipe(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.Generation, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().RunDataframeRecipe(ctx, fc.Args["input"].(model.RunDataframeRecipeInput)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { - return ec.marshalNString2string(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeResult) graphql.Marshaler { + return ec.marshalNDataframeRecipeResult2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeResult(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_ProjectRelease_generation(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("ProjectRelease", field, false, false, errors.New("field of type String does not have child fields")) +func (ec *executionContext) fieldContext_Mutation_runDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeRecipeResult(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_runDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil } -func (ec *executionContext) _ProjectRelease_revision(ctx context.Context, field graphql.CollectedField, obj *model.ProjectRelease) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_validateDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_ProjectRelease_revision(ctx, field) + return ec.fieldContext_Mutation_validateDataframeRecipe(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.Revision, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().ValidateDataframeRecipe(ctx, fc.Args["input"].(model.ValidateDataframeRecipeInput)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler { - return ec.marshalNString2string(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeValidation) graphql.Marshaler { + return ec.marshalNDataframeRecipeValidation2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeValidation(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_ProjectRelease_revision(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("ProjectRelease", field, false, false, errors.New("field of type String does not have child fields")) +func (ec *executionContext) fieldContext_Mutation_validateDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeRecipeValidation(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_validateDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil } -func (ec *executionContext) _ProjectRelease_state(ctx context.Context, field graphql.CollectedField, obj *model.ProjectRelease) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_previewDataframeRecipe(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_ProjectRelease_state(ctx, field) + return ec.fieldContext_Mutation_previewDataframeRecipe(ctx, field) }, func(ctx context.Context) (any, error) { - return obj.State, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().PreviewDataframeRecipe(ctx, fc.Args["input"].(model.PreviewDataframeRecipeInput)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v model.ProjectReleaseState) graphql.Marshaler { - return ec.marshalNProjectReleaseState2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐProjectReleaseState(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipePreview) graphql.Marshaler { + return ec.marshalNDataframeRecipePreview2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipePreview(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_ProjectRelease_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("ProjectRelease", field, false, false, errors.New("field of type ProjectReleaseState does not have child fields")) +func (ec *executionContext) fieldContext_Mutation_previewDataframeRecipe(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_DataframeRecipePreview(ctx, field) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_previewDataframeRecipe_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil } -func (ec *executionContext) _Query_dataframeDatasets(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_registerDataframeRecipeRevision(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Query_dataframeDatasets(ctx, field) + return ec.fieldContext_Mutation_registerDataframeRecipeRevision(ctx, field) }, func(ctx context.Context) (any, error) { - return ec.Resolvers.Query().DataframeDatasets(ctx) + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().RegisterDataframeRecipeRevision(ctx, fc.Args["input"].(model.RegisterDataframeRecipeRevisionInput)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v []*model.DataframeMaterialization) graphql.Marshaler { - return ec.marshalNDataframeMaterialization2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeMaterializationᚄ(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *model.DataframeRecipeRevision) graphql.Marshaler { + return ec.marshalNDataframeRecipeRevision2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeRevision(ctx, selections, v) }, true, true, ) } -func (ec *executionContext) fieldContext_Query_dataframeDatasets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_registerDataframeRecipeRevision(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "Mutation", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_DataframeMaterialization(ctx, field) + return ec.childFields_DataframeRecipeRevision(ctx, field) }, } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_registerDataframeRecipeRevision_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } return fc, nil } @@ -9307,8 +8674,8 @@ func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field // region **************************** input.gotpl ***************************** -func (ec *executionContext) unmarshalInputActivateProjectReleaseInput(ctx context.Context, obj any) (model.ActivateProjectReleaseInput, error) { - var it model.ActivateProjectReleaseInput +func (ec *executionContext) unmarshalInputDataframeAggregateInput(ctx context.Context, obj any) (model.DataframeAggregateInput, error) { + var it model.DataframeAggregateInput if obj == nil { return it, nil } @@ -9318,57 +8685,20 @@ func (ec *executionContext) unmarshalInputActivateProjectReleaseInput(ctx contex asMap[k] = v } - fieldsInOrder := [...]string{"project", "releaseId", "expectedActiveRevision"} + fieldsInOrder := [...]string{"projectId", "selector", "groupBy", "filters", "operation", "column"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { - case "project": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("project")) + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Project = data - case "releaseId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("releaseId")) - data, err := ec.unmarshalNID2string(ctx, v) - if err != nil { - return it, err - } - it.ReleaseID = data - case "expectedActiveRevision": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("expectedActiveRevision")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) - if err != nil { - return it, err - } - it.ExpectedActiveRevision = data - } - } - return it, nil -} - -func (ec *executionContext) unmarshalInputDataframeAggregateInput(ctx context.Context, obj any) (model.DataframeAggregateInput, error) { - var it model.DataframeAggregateInput - if obj == nil { - return it, nil - } - - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"selector", "groupBy", "filters", "operation", "column"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { + it.ProjectID = data case "selector": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selector")) data, err := ec.unmarshalNDataframeSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeSelectorInput(ctx, v) @@ -9496,13 +8826,20 @@ func (ec *executionContext) unmarshalInputDataframeAggregationsInput(ctx context asMap[k] = v } - fieldsInOrder := [...]string{"selector", "filters", "specs"} + fieldsInOrder := [...]string{"projectId", "selector", "filters", "specs"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data case "selector": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selector")) data, err := ec.unmarshalNDataframeSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeSelectorInput(ctx, v) @@ -9540,13 +8877,20 @@ func (ec *executionContext) unmarshalInputDataframeDatasetInput(ctx context.Cont asMap[k] = v } - fieldsInOrder := [...]string{"selector"} + fieldsInOrder := [...]string{"projectId", "selector"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data case "selector": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selector")) data, err := ec.unmarshalNDataframeSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeSelectorInput(ctx, v) @@ -9680,13 +9024,20 @@ func (ec *executionContext) unmarshalInputDataframeRowsInput(ctx context.Context asMap["first"] = 100 } - fieldsInOrder := [...]string{"selector", "columns", "filters", "sort", "first", "after"} + fieldsInOrder := [...]string{"projectId", "selector", "columns", "filters", "sort", "first", "after"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { + case "projectId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("projectId")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ProjectID = data case "selector": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selector")) data, err := ec.unmarshalNDataframeSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeSelectorInput(ctx, v) @@ -10975,43 +10326,6 @@ func (ec *executionContext) unmarshalInputFhirTraversalStepInput(ctx context.Con return it, nil } -func (ec *executionContext) unmarshalInputMaterializeDataframeRecipeInput(ctx context.Context, obj any) (model.MaterializeDataframeRecipeInput, error) { - var it model.MaterializeDataframeRecipeInput - if obj == nil { - return it, nil - } - - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"name", "bindings"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Name = data - case "bindings": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) - data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) - if err != nil { - return it, err - } - it.Bindings = data - } - } - return it, nil -} - func (ec *executionContext) unmarshalInputPreflightDataframeRecipeInput(ctx context.Context, obj any) (model.PreflightDataframeRecipeInput, error) { var it model.PreflightDataframeRecipeInput if obj == nil { @@ -11155,52 +10469,8 @@ func (ec *executionContext) unmarshalInputRegisterDataframeRecipeRevisionInput(c return it, nil } -func (ec *executionContext) unmarshalInputRunDataframeRecipeInput(ctx context.Context, obj any) (model.RunDataframeRecipeInput, error) { - var it model.RunDataframeRecipeInput - if obj == nil { - return it, nil - } - - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"name", "bindings", "outputs"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Name = data - case "bindings": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) - data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) - if err != nil { - return it, err - } - it.Bindings = data - case "outputs": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("outputs")) - data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) - if err != nil { - return it, err - } - it.Outputs = data - } - } - return it, nil -} - -func (ec *executionContext) unmarshalInputStartDataframeMaterializationInput(ctx context.Context, obj any) (model.StartDataframeMaterializationInput, error) { - var it model.StartDataframeMaterializationInput +func (ec *executionContext) unmarshalInputRunDataframeRecipeInput(ctx context.Context, obj any) (model.RunDataframeRecipeInput, error) { + var it model.RunDataframeRecipeInput if obj == nil { return it, nil } @@ -11210,34 +10480,34 @@ func (ec *executionContext) unmarshalInputStartDataframeMaterializationInput(ctx asMap[k] = v } - fieldsInOrder := [...]string{"project", "generation", "selector"} + fieldsInOrder := [...]string{"name", "bindings", "outputs"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { - case "project": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("project")) + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Project = data - case "generation": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("generation")) - data, err := ec.unmarshalNString2string(ctx, v) + it.Name = data + case "bindings": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bindings")) + data, err := ec.unmarshalNDataframeRecipeBindingsInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeBindingsInput(ctx, v) if err != nil { return it, err } - it.Generation = data - case "selector": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selector")) - data, err := ec.unmarshalNDataframeSelectorInput2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeSelectorInput(ctx, v) + it.Bindings = data + case "outputs": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("outputs")) + data, err := ec.unmarshalOString2ᚕstringᚄ(ctx, v) if err != nil { return it, err } - it.Selector = data + it.Outputs = data } } return it, nil @@ -11617,31 +10887,6 @@ func (ec *executionContext) _DataframeMaterialization(ctx context.Context, sel a if out.Values[i] == graphql.RequiredNull { out.Invalids++ } - case "availability": - out.Values[i] = ec._DataframeMaterialization_availability(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "completeness": - out.Values[i] = ec._DataframeMaterialization_completeness(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "includedProjectCount": - out.Values[i] = ec._DataframeMaterialization_includedProjectCount(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "expectedProjectCount": - out.Values[i] = ec._DataframeMaterialization_expectedProjectCount(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "projectStatuses": - out.Values[i] = ec._DataframeMaterialization_projectStatuses(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -11827,79 +11072,6 @@ func (ec *executionContext) _DataframePageInfo(ctx context.Context, sel ast.Sele return out } -var dataframeProjectStatusImplementors = []string{"DataframeProjectStatus"} - -func (ec *executionContext) _DataframeProjectStatus(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeProjectStatus) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, dataframeProjectStatusImplementors) - - out := graphql.NewFieldSet(fields) - deferredFieldSet := graphql.NewFieldSet(nil) - deferLabelToView := make(map[string]*graphql.FieldSetView) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("DataframeProjectStatus") - case "project": - out.Values[i] = ec._DataframeProjectStatus_project(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "state": - out.Values[i] = ec._DataframeProjectStatus_state(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "generation": - out.Values[i] = ec._DataframeProjectStatus_generation(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "executionId": - out.Values[i] = ec._DataframeProjectStatus_executionId(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "createdAt": - out.Values[i] = ec._DataframeProjectStatus_createdAt(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "updatedAt": - out.Values[i] = ec._DataframeProjectStatus_updatedAt(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "errorCode": - out.Values[i] = ec._DataframeProjectStatus_errorCode(ctx, field, obj) - if out.Values[i] == graphql.RequiredNull { - out.Invalids++ - } - case "retryable": - out.Values[i] = ec._DataframeProjectStatus_retryable(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - - ec.ProcessDeferredGroup(graphql.DeferredGroup{ - Defers: deferLabelToView, - Path: graphql.GetPath(ctx), - FieldSet: deferredFieldSet, - Context: ctx, - }) - - return out -} - var dataframeQueryDiagnosticsImplementors = []string{"DataframeQueryDiagnostics"} func (ec *executionContext) _DataframeQueryDiagnostics(ctx context.Context, sel ast.SelectionSet, obj *model.DataframeQueryDiagnostics) graphql.Marshaler { @@ -14099,27 +13271,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } - case "materializeDataframeRecipeBundle": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_materializeDataframeRecipeBundle(ctx, field) - }) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "startDataframeMaterialization": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_startDataframeMaterialization(ctx, field) - }) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "activateProjectRelease": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_activateProjectRelease(ctx, field) - }) - if out.Values[i] == graphql.Null { - out.Invalids++ - } case "registerDataframeRecipeRevision": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_registerDataframeRecipeRevision(ctx, field) @@ -14148,64 +13299,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return out } -var projectReleaseImplementors = []string{"ProjectRelease"} - -func (ec *executionContext) _ProjectRelease(ctx context.Context, sel ast.SelectionSet, obj *model.ProjectRelease) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, projectReleaseImplementors) - - out := graphql.NewFieldSet(fields) - deferredFieldSet := graphql.NewFieldSet(nil) - deferLabelToView := make(map[string]*graphql.FieldSetView) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("ProjectRelease") - case "id": - out.Values[i] = ec._ProjectRelease_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "project": - out.Values[i] = ec._ProjectRelease_project(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "generation": - out.Values[i] = ec._ProjectRelease_generation(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "revision": - out.Values[i] = ec._ProjectRelease_revision(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "state": - out.Values[i] = ec._ProjectRelease_state(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.Deferred, int32(min(len(deferLabelToView), math.MaxInt32))) - - ec.ProcessDeferredGroup(graphql.DeferredGroup{ - Defers: deferLabelToView, - Path: graphql.GetPath(ctx), - FieldSet: deferredFieldSet, - Context: ctx, - }) - - return out -} - var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -14226,28 +13319,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") - case "dataframeDatasets": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_dataframeDatasets(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, - func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "projectDataframeDatasets": field := field @@ -15057,11 +14128,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr // region ***************************** type.gotpl ***************************** -func (ec *executionContext) unmarshalNActivateProjectReleaseInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐActivateProjectReleaseInput(ctx context.Context, v any) (model.ActivateProjectReleaseInput, error) { - res, err := ec.unmarshalInputActivateProjectReleaseInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalNDataframeAggregateInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeAggregateInput(ctx context.Context, v any) (model.DataframeAggregateInput, error) { res, err := ec.unmarshalInputDataframeAggregateInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -15247,42 +14313,6 @@ func (ec *executionContext) marshalNDataframePageInfo2ᚖgithubᚗcomᚋcalypr return ec._DataframePageInfo(ctx, sel, v) } -func (ec *executionContext) unmarshalNDataframeProjectState2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeProjectState(ctx context.Context, v any) (model.DataframeProjectState, error) { - var res model.DataframeProjectState - err := res.UnmarshalGQL(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNDataframeProjectState2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeProjectState(ctx context.Context, sel ast.SelectionSet, v model.DataframeProjectState) graphql.Marshaler { - return v -} - -func (ec *executionContext) marshalNDataframeProjectStatus2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeProjectStatusᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeProjectStatus) graphql.Marshaler { - ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { - fc := graphql.GetFieldContext(ctx) - fc.Result = &v[i] - return ec.marshalNDataframeProjectStatus2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeProjectStatus(ctx, sel, v[i]) - }) - - for _, e := range ret { - if e == graphql.Null { - return graphql.Null - } - } - - return ret -} - -func (ec *executionContext) marshalNDataframeProjectStatus2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeProjectStatus(ctx context.Context, sel ast.SelectionSet, v *model.DataframeProjectStatus) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._DataframeProjectStatus(ctx, sel, v) -} - func (ec *executionContext) marshalNDataframeQueryDiagnostics2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeQueryDiagnostics(ctx context.Context, sel ast.SelectionSet, v *model.DataframeQueryDiagnostics) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -15324,20 +14354,6 @@ func (ec *executionContext) marshalNDataframeRecipeColumn2ᚖgithubᚗcomᚋcaly return ec._DataframeRecipeColumn(ctx, sel, v) } -func (ec *executionContext) marshalNDataframeRecipeExecution2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExecution(ctx context.Context, sel ast.SelectionSet, v model.DataframeRecipeExecution) graphql.Marshaler { - return ec._DataframeRecipeExecution(ctx, sel, &v) -} - -func (ec *executionContext) marshalNDataframeRecipeExecution2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExecution(ctx context.Context, sel ast.SelectionSet, v *model.DataframeRecipeExecution) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._DataframeRecipeExecution(ctx, sel, v) -} - func (ec *executionContext) marshalNDataframeRecipeExecutionOutput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeRecipeExecutionOutputᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DataframeRecipeExecutionOutput) graphql.Marshaler { ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { fc := graphql.GetFieldContext(ctx) @@ -16222,11 +15238,6 @@ func (ec *executionContext) marshalNJSON2encodingᚋjsonᚐRawMessage(ctx contex return ec._JSON(ctx, sel, v) } -func (ec *executionContext) unmarshalNMaterializeDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐMaterializeDataframeRecipeInput(ctx context.Context, v any) (model.MaterializeDataframeRecipeInput, error) { - res, err := ec.unmarshalInputMaterializeDataframeRecipeInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalNPreflightDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐPreflightDataframeRecipeInput(ctx context.Context, v any) (model.PreflightDataframeRecipeInput, error) { res, err := ec.unmarshalInputPreflightDataframeRecipeInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -16237,30 +15248,6 @@ func (ec *executionContext) unmarshalNPreviewDataframeRecipeInput2githubᚗcom return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNProjectRelease2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐProjectRelease(ctx context.Context, sel ast.SelectionSet, v model.ProjectRelease) graphql.Marshaler { - return ec._ProjectRelease(ctx, sel, &v) -} - -func (ec *executionContext) marshalNProjectRelease2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐProjectRelease(ctx context.Context, sel ast.SelectionSet, v *model.ProjectRelease) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._ProjectRelease(ctx, sel, v) -} - -func (ec *executionContext) unmarshalNProjectReleaseState2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐProjectReleaseState(ctx context.Context, v any) (model.ProjectReleaseState, error) { - var res model.ProjectReleaseState - err := res.UnmarshalGQL(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNProjectReleaseState2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐProjectReleaseState(ctx context.Context, sel ast.SelectionSet, v model.ProjectReleaseState) graphql.Marshaler { - return v -} - func (ec *executionContext) unmarshalNRegisterDataframeRecipeRevisionInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐRegisterDataframeRecipeRevisionInput(ctx context.Context, v any) (model.RegisterDataframeRecipeRevisionInput, error) { res, err := ec.unmarshalInputRegisterDataframeRecipeRevisionInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -16271,32 +15258,11 @@ func (ec *executionContext) unmarshalNRunDataframeRecipeInput2githubᚗcomᚋcal return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalNStartDataframeMaterializationInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐStartDataframeMaterializationInput(ctx context.Context, v any) (model.StartDataframeMaterializationInput, error) { - res, err := ec.unmarshalInputStartDataframeMaterializationInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalNValidateDataframeRecipeInput2githubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐValidateDataframeRecipeInput(ctx context.Context, v any) (model.ValidateDataframeRecipeInput, error) { res, err := ec.unmarshalInputValidateDataframeRecipeInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalODataframeAvailability2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeAvailability(ctx context.Context, v any) (*model.DataframeAvailability, error) { - if v == nil { - return nil, nil - } - var res = new(model.DataframeAvailability) - err := res.UnmarshalGQL(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalODataframeAvailability2ᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeAvailability(ctx context.Context, sel ast.SelectionSet, v *model.DataframeAvailability) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return v -} - func (ec *executionContext) unmarshalODataframeFilterInput2ᚕᚖgithubᚗcomᚋcalyprᚋloomᚋgeneratedᚋgraphqlᚋgraphᚋmodelᚐDataframeFilterInputᚄ(ctx context.Context, v any) ([]*model.DataframeFilterInput, error) { if v == nil { return nil, nil diff --git a/generated/graphql/graph/model/models.go b/generated/graphql/graph/model/models.go index 4c18b067..961734b0 100644 --- a/generated/graphql/graph/model/models.go +++ b/generated/graphql/graph/model/models.go @@ -16,13 +16,8 @@ type FHIRResource interface { GetResourceType() string } -type ActivateProjectReleaseInput struct { - Project string `json:"project"` - ReleaseID string `json:"releaseId"` - ExpectedActiveRevision *string `json:"expectedActiveRevision,omitempty"` -} - type DataframeAggregateInput struct { + ProjectID string `json:"projectId"` Selector *DataframeSelectorInput `json:"selector"` GroupBy []string `json:"groupBy,omitempty"` Filters []*DataframeFilterInput `json:"filters,omitempty"` @@ -47,9 +42,10 @@ type DataframeAggregationSpecInput struct { } type DataframeAggregationsInput struct { - Selector *DataframeSelectorInput `json:"selector"` - Filters []*DataframeFilterInput `json:"filters,omitempty"` - Specs []*DataframeAggregationSpecInput `json:"specs"` + ProjectID string `json:"projectId"` + Selector *DataframeSelectorInput `json:"selector"` + Filters []*DataframeFilterInput `json:"filters,omitempty"` + Specs []*DataframeAggregationSpecInput `json:"specs"` } type DataframeAggregationsResult struct { @@ -82,7 +78,8 @@ type DataframeCompilerPlanDiagnostics struct { } type DataframeDatasetInput struct { - Selector *DataframeSelectorInput `json:"selector"` + ProjectID string `json:"projectId"` + Selector *DataframeSelectorInput `json:"selector"` } type DataframeFilterInput struct { @@ -92,25 +89,20 @@ type DataframeFilterInput struct { } type DataframeMaterialization struct { - ID string `json:"id"` - Name string `json:"name"` - Revision string `json:"revision"` - ProjectID string `json:"projectId"` - DatasetGeneration string `json:"datasetGeneration"` - State DataframeMaterializationState `json:"state"` - Columns []*DataframeColumn `json:"columns"` - RowCount *int `json:"rowCount,omitempty"` - CreatedAt string `json:"createdAt"` - ReadyAt *string `json:"readyAt,omitempty"` - Error *string `json:"error,omitempty"` - ErrorCode *string `json:"errorCode,omitempty"` - ErrorRetryable *bool `json:"errorRetryable,omitempty"` - Selector *DataframeSelector `json:"selector,omitempty"` - Availability *DataframeAvailability `json:"availability,omitempty"` - Completeness *float64 `json:"completeness,omitempty"` - IncludedProjectCount *int `json:"includedProjectCount,omitempty"` - ExpectedProjectCount *int `json:"expectedProjectCount,omitempty"` - ProjectStatuses []*DataframeProjectStatus `json:"projectStatuses"` + ID string `json:"id"` + Name string `json:"name"` + Revision string `json:"revision"` + ProjectID string `json:"projectId"` + DatasetGeneration string `json:"datasetGeneration"` + State DataframeMaterializationState `json:"state"` + Columns []*DataframeColumn `json:"columns"` + RowCount *int `json:"rowCount,omitempty"` + CreatedAt string `json:"createdAt"` + ReadyAt *string `json:"readyAt,omitempty"` + Error *string `json:"error,omitempty"` + ErrorCode *string `json:"errorCode,omitempty"` + ErrorRetryable *bool `json:"errorRetryable,omitempty"` + Selector *DataframeSelector `json:"selector,omitempty"` } type DataframeOptimizationDecision struct { @@ -135,17 +127,6 @@ type DataframePageInfo struct { EndCursor *string `json:"endCursor,omitempty"` } -type DataframeProjectStatus struct { - Project string `json:"project"` - State DataframeProjectState `json:"state"` - Generation *string `json:"generation,omitempty"` - ExecutionID *string `json:"executionId,omitempty"` - CreatedAt *string `json:"createdAt,omitempty"` - UpdatedAt *string `json:"updatedAt,omitempty"` - ErrorCode *string `json:"errorCode,omitempty"` - Retryable bool `json:"retryable"` -} - type DataframeQueryDiagnostics struct { InputResolutionMs float64 `json:"inputResolutionMs"` RequestPreparationMs float64 `json:"requestPreparationMs"` @@ -397,12 +378,13 @@ type DataframeRowConnection struct { } type DataframeRowsInput struct { - Selector *DataframeSelectorInput `json:"selector"` - Columns []string `json:"columns,omitempty"` - Filters []*DataframeFilterInput `json:"filters,omitempty"` - Sort *DataframeSortInput `json:"sort,omitempty"` - First *int `json:"first,omitempty"` - After *string `json:"after,omitempty"` + ProjectID string `json:"projectId"` + Selector *DataframeSelectorInput `json:"selector"` + Columns []string `json:"columns,omitempty"` + Filters []*DataframeFilterInput `json:"filters,omitempty"` + Sort *DataframeSortInput `json:"sort,omitempty"` + First *int `json:"first,omitempty"` + After *string `json:"after,omitempty"` } type DataframeSelector struct { @@ -618,11 +600,6 @@ type FhirTraversalStepInput struct { Traverse []*FhirTraversalStepInput `json:"traverse,omitempty"` } -type MaterializeDataframeRecipeInput struct { - Name string `json:"name"` - Bindings *DataframeRecipeBindingsInput `json:"bindings"` -} - type Mutation struct { } @@ -638,14 +615,6 @@ type PreviewDataframeRecipeInput struct { Outputs []string `json:"outputs,omitempty"` } -type ProjectRelease struct { - ID string `json:"id"` - Project string `json:"project"` - Generation string `json:"generation"` - Revision string `json:"revision"` - State ProjectReleaseState `json:"state"` -} - type Query struct { } @@ -662,74 +631,11 @@ type RunDataframeRecipeInput struct { Outputs []string `json:"outputs,omitempty"` } -type StartDataframeMaterializationInput struct { - Project string `json:"project"` - Generation string `json:"generation"` - Selector *DataframeSelectorInput `json:"selector"` -} - type ValidateDataframeRecipeInput struct { Name string `json:"name"` Bindings *DataframeRecipeBindingsInput `json:"bindings"` } -type DataframeAvailability string - -const ( - DataframeAvailabilityAvailable DataframeAvailability = "AVAILABLE" - DataframeAvailabilityDegraded DataframeAvailability = "DEGRADED" - DataframeAvailabilityUnavailable DataframeAvailability = "UNAVAILABLE" -) - -var AllDataframeAvailability = []DataframeAvailability{ - DataframeAvailabilityAvailable, - DataframeAvailabilityDegraded, - DataframeAvailabilityUnavailable, -} - -func (e DataframeAvailability) IsValid() bool { - switch e { - case DataframeAvailabilityAvailable, DataframeAvailabilityDegraded, DataframeAvailabilityUnavailable: - return true - } - return false -} - -func (e DataframeAvailability) String() string { - return string(e) -} - -func (e *DataframeAvailability) UnmarshalGQL(v any) error { - str, ok := v.(string) - if !ok { - return fmt.Errorf("enums must be strings") - } - - *e = DataframeAvailability(str) - if !e.IsValid() { - return fmt.Errorf("%s is not a valid DataframeAvailability", str) - } - return nil -} - -func (e DataframeAvailability) MarshalGQL(w io.Writer) { - fmt.Fprint(w, strconv.Quote(e.String())) -} - -func (e *DataframeAvailability) UnmarshalJSON(b []byte) error { - s, err := strconv.Unquote(string(b)) - if err != nil { - return err - } - return e.UnmarshalGQL(s) -} - -func (e DataframeAvailability) MarshalJSON() ([]byte, error) { - var buf bytes.Buffer - e.MarshalGQL(&buf) - return buf.Bytes(), nil -} - type DataframeMaterializationState string const ( @@ -789,69 +695,6 @@ func (e DataframeMaterializationState) MarshalJSON() ([]byte, error) { return buf.Bytes(), nil } -type DataframeProjectState string - -const ( - DataframeProjectStateCurrent DataframeProjectState = "CURRENT" - DataframeProjectStateStale DataframeProjectState = "STALE" - DataframeProjectStateBuilding DataframeProjectState = "BUILDING" - DataframeProjectStateFailed DataframeProjectState = "FAILED" - DataframeProjectStateMissing DataframeProjectState = "MISSING" - DataframeProjectStateExcluded DataframeProjectState = "EXCLUDED" -) - -var AllDataframeProjectState = []DataframeProjectState{ - DataframeProjectStateCurrent, - DataframeProjectStateStale, - DataframeProjectStateBuilding, - DataframeProjectStateFailed, - DataframeProjectStateMissing, - DataframeProjectStateExcluded, -} - -func (e DataframeProjectState) IsValid() bool { - switch e { - case DataframeProjectStateCurrent, DataframeProjectStateStale, DataframeProjectStateBuilding, DataframeProjectStateFailed, DataframeProjectStateMissing, DataframeProjectStateExcluded: - return true - } - return false -} - -func (e DataframeProjectState) String() string { - return string(e) -} - -func (e *DataframeProjectState) UnmarshalGQL(v any) error { - str, ok := v.(string) - if !ok { - return fmt.Errorf("enums must be strings") - } - - *e = DataframeProjectState(str) - if !e.IsValid() { - return fmt.Errorf("%s is not a valid DataframeProjectState", str) - } - return nil -} - -func (e DataframeProjectState) MarshalGQL(w io.Writer) { - fmt.Fprint(w, strconv.Quote(e.String())) -} - -func (e *DataframeProjectState) UnmarshalJSON(b []byte) error { - s, err := strconv.Unquote(string(b)) - if err != nil { - return err - } - return e.UnmarshalGQL(s) -} - -func (e DataframeProjectState) MarshalJSON() ([]byte, error) { - var buf bytes.Buffer - e.MarshalGQL(&buf) - return buf.Bytes(), nil -} - type DataframeRecipeExecutionState string const ( @@ -1487,56 +1330,3 @@ func (e FhirValueMode) MarshalJSON() ([]byte, error) { e.MarshalGQL(&buf) return buf.Bytes(), nil } - -type ProjectReleaseState string - -const ( - ProjectReleaseStateActive ProjectReleaseState = "ACTIVE" -) - -var AllProjectReleaseState = []ProjectReleaseState{ - ProjectReleaseStateActive, -} - -func (e ProjectReleaseState) IsValid() bool { - switch e { - case ProjectReleaseStateActive: - return true - } - return false -} - -func (e ProjectReleaseState) String() string { - return string(e) -} - -func (e *ProjectReleaseState) UnmarshalGQL(v any) error { - str, ok := v.(string) - if !ok { - return fmt.Errorf("enums must be strings") - } - - *e = ProjectReleaseState(str) - if !e.IsValid() { - return fmt.Errorf("%s is not a valid ProjectReleaseState", str) - } - return nil -} - -func (e ProjectReleaseState) MarshalGQL(w io.Writer) { - fmt.Fprint(w, strconv.Quote(e.String())) -} - -func (e *ProjectReleaseState) UnmarshalJSON(b []byte) error { - s, err := strconv.Unquote(string(b)) - if err != nil { - return err - } - return e.UnmarshalGQL(s) -} - -func (e ProjectReleaseState) MarshalJSON() ([]byte, error) { - var buf bytes.Buffer - e.MarshalGQL(&buf) - return buf.Bytes(), nil -} diff --git a/generated/loomapi/api.gen.go b/generated/loomapi/api.gen.go index 52c6562d..0e4910eb 100644 --- a/generated/loomapi/api.gen.go +++ b/generated/loomapi/api.gen.go @@ -58,9 +58,6 @@ func (e AuthoringCapabilityKind) Valid() bool { const ( Builder AuthoringCapabilityOperations = "builder" Commands AuthoringCapabilityOperations = "commands" - Compile AuthoringCapabilityOperations = "compile" - Draft AuthoringCapabilityOperations = "draft" - Export AuthoringCapabilityOperations = "export" Preview AuthoringCapabilityOperations = "preview" Publish AuthoringCapabilityOperations = "publish" Reconcile AuthoringCapabilityOperations = "reconcile" @@ -74,12 +71,6 @@ func (e AuthoringCapabilityOperations) Valid() bool { return true case Commands: return true - case Compile: - return true - case Draft: - return true - case Export: - return true case Preview: return true case Publish: @@ -116,17 +107,18 @@ func (e AuthoringCommandInitialPresentation) Valid() bool { // Defines values for AuthoringCommandType. const ( - ADDCOLUMN AuthoringCommandType = "ADD_COLUMN" - ADDROUTE AuthoringCommandType = "ADD_ROUTE" - CREATETABLE AuthoringCommandType = "CREATE_TABLE" - DELETETABLE AuthoringCommandType = "DELETE_TABLE" - DUPLICATETABLE AuthoringCommandType = "DUPLICATE_TABLE" - REMOVECOLUMN AuthoringCommandType = "REMOVE_COLUMN" - REMOVEROUTE AuthoringCommandType = "REMOVE_ROUTE" - RENAMETABLE AuthoringCommandType = "RENAME_TABLE" - REORDERTABLES AuthoringCommandType = "REORDER_TABLES" - SETTABLEROOT AuthoringCommandType = "SET_TABLE_ROOT" - UPDATECOLUMN AuthoringCommandType = "UPDATE_COLUMN" + ADDCOLUMN AuthoringCommandType = "ADD_COLUMN" + ADDROUTE AuthoringCommandType = "ADD_ROUTE" + CREATETABLE AuthoringCommandType = "CREATE_TABLE" + DELETETABLE AuthoringCommandType = "DELETE_TABLE" + DUPLICATETABLE AuthoringCommandType = "DUPLICATE_TABLE" + REMOVECOLUMN AuthoringCommandType = "REMOVE_COLUMN" + REMOVEROUTE AuthoringCommandType = "REMOVE_ROUTE" + RENAMETABLE AuthoringCommandType = "RENAME_TABLE" + REORDERTABLES AuthoringCommandType = "REORDER_TABLES" + SETTABLEROOT AuthoringCommandType = "SET_TABLE_ROOT" + UPDATECOLUMN AuthoringCommandType = "UPDATE_COLUMN" + UPDATEROUTEEDGE AuthoringCommandType = "UPDATE_ROUTE_EDGE" ) // Valid indicates whether the value is a known member of the AuthoringCommandType enum. @@ -154,6 +146,8 @@ func (e AuthoringCommandType) Valid() bool { return true case UPDATECOLUMN: return true + case UPDATEROUTEEDGE: + return true default: return false } @@ -207,21 +201,6 @@ func (e CandidateSearchResponseKind) Valid() bool { } } -// Defines values for CandidateSuggestionsKind. -const ( - ExplorerCandidateSuggestions CandidateSuggestionsKind = "ExplorerCandidateSuggestions" -) - -// Valid indicates whether the value is a known member of the CandidateSuggestionsKind enum. -func (e CandidateSuggestionsKind) Valid() bool { - switch e { - case ExplorerCandidateSuggestions: - return true - default: - return false - } -} - // Defines values for ColumnSourceKind. const ( CodingBySystem ColumnSourceKind = "codingBySystem" @@ -530,20 +509,6 @@ type CandidateSearchResponse struct { // CandidateSearchResponseKind defines model for CandidateSearchResponse.Kind. type CandidateSearchResponseKind string -// CandidateSuggestions defines model for CandidateSuggestions. -type CandidateSuggestions struct { - ApiVersion APIVersion `json:"apiVersion"` - CandidateId string `json:"candidateId"` - Complete bool `json:"complete"` - Kind CandidateSuggestionsKind `json:"kind"` - SnapshotToken string `json:"snapshotToken"` - Truncated bool `json:"truncated"` - Values []string `json:"values"` -} - -// CandidateSuggestionsKind defines model for CandidateSuggestions.Kind. -type CandidateSuggestionsKind string - // Catalog defines model for Catalog. type Catalog struct { AuthorizationScopeDigest string `json:"authorizationScopeDigest"` @@ -635,12 +600,6 @@ type CommandResult struct { // CommandResultType defines model for CommandResult.Type. type CommandResultType string -// CompileRequest defines model for CompileRequest. -type CompileRequest struct { - SnapshotToken string `json:"snapshotToken"` - Workspace Workspace `json:"workspace"` -} - // CompileResponse defines model for CompileResponse. type CompileResponse struct { ApiVersion APIVersion `json:"apiVersion"` @@ -844,21 +803,6 @@ type RoutePolicy struct { MaxSteps *int `json:"maxSteps,omitempty"` } -// SaveDraftRequest defines model for SaveDraftRequest. -type SaveDraftRequest struct { - ExpectedDraftDigest *string `json:"expectedDraftDigest,omitempty"` - ExpectedDraftVersion int64 `json:"expectedDraftVersion"` - SnapshotToken string `json:"snapshotToken"` - Workspace Workspace `json:"workspace"` -} - -// SaveDraftResponse defines model for SaveDraftResponse. -type SaveDraftResponse struct { - DraftDigest string `json:"draftDigest"` - DraftVersion int64 `json:"draftVersion"` - Workspace Workspace `json:"workspace"` -} - // ServiceErrorBody defines model for ServiceErrorBody. type ServiceErrorBody struct { Code string `json:"code"` @@ -902,6 +846,9 @@ type TablePresentationCellRenderer string // Workspace defines model for Workspace. type Workspace = authoringv2.Workspace +// AuthResourcePath defines model for AuthResourcePath. +type AuthResourcePath = string + // ExplorerId defines model for ExplorerId. type ExplorerId = string @@ -911,12 +858,6 @@ type Generation = string // Project defines model for Project. type Project = string -// Release defines model for Release. -type Release = string - -// ResourceType defines model for ResourceType. -type ResourceType = string - // AuthoringBadRequest defines model for AuthoringBadRequest. type AuthoringBadRequest = ErrorResponse @@ -1007,6 +948,12 @@ type ServiceUnprocessable = ServiceErrorResponse // ServiceUnsupportedMediaType Stable error envelope emitted by Loom's shared HTTP error mapper and snapshot routes. type ServiceUnsupportedMediaType = ServiceErrorResponse +// GetDatasetGenerationStatusParams defines parameters for GetDatasetGenerationStatus. +type GetDatasetGenerationStatusParams struct { + // AuthResourcePath Optional Calypr resource scope used to authorize durable Explorer mutations. + AuthResourcePath *AuthResourcePath `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` +} + // CreateDatasetGenerationMultipartBody defines parameters for CreateDatasetGeneration. type CreateDatasetGenerationMultipartBody struct { AuthResourcePath *string `json:"auth_resource_path,omitempty"` @@ -1022,28 +969,34 @@ type ActivateDatasetGenerationParams struct { AuthResourcePath *string `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` } -// PublishRepositoryExplorerConfigParams defines parameters for PublishRepositoryExplorerConfig. -type PublishRepositoryExplorerConfigParams struct { - AuthResourcePath *string `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` - XLoomSourceCommit string `json:"X-Loom-Source-Commit"` +// CreateExplorerParams defines parameters for CreateExplorer. +type CreateExplorerParams struct { + // AuthResourcePath Optional Calypr resource scope used to authorize durable Explorer mutations. + AuthResourcePath *AuthResourcePath `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` } -// UploadSnapshotResourceParams defines parameters for UploadSnapshotResource. -type UploadSnapshotResourceParams struct { - XContentSHA256 *string `json:"X-Content-SHA256,omitempty"` +// ApplyExplorerBuilderCommandsParams defines parameters for ApplyExplorerBuilderCommands. +type ApplyExplorerBuilderCommandsParams struct { + // AuthResourcePath Optional Calypr resource scope used to authorize durable Explorer mutations. + AuthResourcePath *AuthResourcePath `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` } -// UploadProjectResourceMultipartBody defines parameters for UploadProjectResource. -type UploadProjectResourceMultipartBody struct { - AuthResourcePath *string `json:"auth_resource_path,omitempty"` - File openapi_types.File `json:"file"` +// PublishExplorerParams defines parameters for PublishExplorer. +type PublishExplorerParams struct { + // AuthResourcePath Optional Calypr resource scope used to authorize durable Explorer mutations. + AuthResourcePath *AuthResourcePath `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` } -// UploadRawNDJSONParams defines parameters for UploadRawNDJSON. -type UploadRawNDJSONParams struct { - Project *string `form:"project,omitempty" json:"project,omitempty"` - Generation *string `form:"generation,omitempty" json:"generation,omitempty"` - AuthResourcePath *string `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` +// ReconcileExplorerBuilderParams defines parameters for ReconcileExplorerBuilder. +type ReconcileExplorerBuilderParams struct { + // AuthResourcePath Optional Calypr resource scope used to authorize durable Explorer mutations. + AuthResourcePath *AuthResourcePath `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` +} + +// PublishRepositoryExplorerConfigParams defines parameters for PublishRepositoryExplorerConfig. +type PublishRepositoryExplorerConfigParams struct { + AuthResourcePath *string `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` + XLoomSourceCommit string `json:"X-Loom-Source-Commit"` } // CreateDatasetGenerationMultipartRequestBody defines body for CreateDatasetGeneration for multipart/form-data ContentType. @@ -1052,18 +1005,9 @@ type CreateDatasetGenerationMultipartRequestBody CreateDatasetGenerationMultipar // CreateExplorerJSONRequestBody defines body for CreateExplorer for application/json ContentType. type CreateExplorerJSONRequestBody = RawJSON -// CompileExplorerBuilderJSONRequestBody defines body for CompileExplorerBuilder for application/json ContentType. -type CompileExplorerBuilderJSONRequestBody = CompileRequest - // ApplyExplorerBuilderCommandsJSONRequestBody defines body for ApplyExplorerBuilderCommands for application/json ContentType. type ApplyExplorerBuilderCommandsJSONRequestBody = ApplyCommandsRequest -// CompileExplorerJSONRequestBody defines body for CompileExplorer for application/json ContentType. -type CompileExplorerJSONRequestBody = CompileRequest - -// SaveExplorerDraftJSONRequestBody defines body for SaveExplorerDraft for application/json ContentType. -type SaveExplorerDraftJSONRequestBody = SaveDraftRequest - // PreviewExplorerJSONRequestBody defines body for PreviewExplorer for application/json ContentType. type PreviewExplorerJSONRequestBody = PreviewRequest @@ -1076,26 +1020,9 @@ type ReconcileExplorerBuilderJSONRequestBody = ReconcileRequest // SearchExplorerCandidatesJSONRequestBody defines body for SearchExplorerCandidates for application/json ContentType. type SearchExplorerCandidatesJSONRequestBody = CandidateSearchRequest -// CreateSnapshotJSONRequestBody defines body for CreateSnapshot for application/json ContentType. -type CreateSnapshotJSONRequestBody = RawJSON - // PublishRepositoryExplorerConfigJSONRequestBody defines body for PublishRepositoryExplorerConfig for application/json ContentType. type PublishRepositoryExplorerConfigJSONRequestBody = Workspace -// CreateReleaseJSONRequestBody defines body for CreateRelease for application/json ContentType. -type CreateReleaseJSONRequestBody = RawJSON - -// ActivateReleaseCompatibilityJSONRequestBody defines body for ActivateReleaseCompatibility for application/json ContentType. -// -// Deprecated: this type has been marked as deprecated upstream, but no `x-deprecated-reason` was set -type ActivateReleaseCompatibilityJSONRequestBody = RawJSON - -// ActivateReleaseJSONRequestBody defines body for ActivateRelease for application/json ContentType. -type ActivateReleaseJSONRequestBody = RawJSON - -// UploadProjectResourceMultipartRequestBody defines body for UploadProjectResource for multipart/form-data ContentType. -type UploadProjectResourceMultipartRequestBody UploadProjectResourceMultipartBody - // ExecuteDataframeGraphQLJSONRequestBody defines body for ExecuteDataframeGraphQL for application/json ContentType. type ExecuteDataframeGraphQLJSONRequestBody = RawJSON @@ -1423,21 +1350,21 @@ type ServerInterface interface { // GetRecipeExecution Read public metadata for a dataframe recipe execution. // (GET /api/v1/dataframe/recipe-executions/{id}) GetRecipeExecution(c fiber.Ctx, id string) error + // GetDatasetGenerationStatus Read a dataset generation's persisted load state. + // (GET /api/v1/datasets/{project}/generations/{generation}) + GetDatasetGenerationStatus(c fiber.Ctx, project Project, generation Generation, params GetDatasetGenerationStatusParams) error // CreateDatasetGeneration Upload a multipart dataset generation. // (POST /api/v1/datasets/{project}/generations/{generation}) CreateDatasetGeneration(c fiber.Ctx, project Project, generation Generation) error // ActivateDatasetGeneration Activate a generation after dataframe publication. // (POST /api/v1/datasets/{project}/generations/{generation}/activate) ActivateDatasetGeneration(c fiber.Ctx, project Project, generation Generation, params ActivateDatasetGenerationParams) error - // GetRepositoryExplorerConfig Read the active repository-owned Explorer config. - // (GET /api/v1/projects/{project}/explorer-config) - GetRepositoryExplorerConfig(c fiber.Ctx, project Project) error // ListExplorers List Explorer summaries for a project. // (GET /api/v1/projects/{project}/explorers) ListExplorers(c fiber.Ctx, project Project) error // CreateExplorer Create an interactive Explorer. // (POST /api/v1/projects/{project}/explorers) - CreateExplorer(c fiber.Ctx, project Project) error + CreateExplorer(c fiber.Ctx, project Project, params CreateExplorerParams) error // GetExplorer Read an Explorer's active runtime projection. // (GET /api/v1/projects/{project}/explorers/{explorerId}) GetExplorer(c fiber.Ctx, project Project, explorerId ExplorerId) error @@ -1445,79 +1372,26 @@ type ServerInterface interface { // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/builder) GetExplorerBuilder(c fiber.Ctx, project Project, explorerId ExplorerId) error - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/builder) - CompileExplorerBuilder(c fiber.Ctx, project Project, explorerId ExplorerId) error - - // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/capabilities/{snapshotToken}/candidates/{candidateId}/suggestions) - GetExplorerCandidateSuggestions(c fiber.Ctx, project Project, explorerId ExplorerId, snapshotToken string, candidateId string) error - // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/capability) GetExplorerAuthoringCapability(c fiber.Ctx, project Project, explorerId ExplorerId) error // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/commands) - ApplyExplorerBuilderCommands(c fiber.Ctx, project Project, explorerId ExplorerId) error - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/compile) - CompileExplorer(c fiber.Ctx, project Project, explorerId ExplorerId) error - - // (PUT /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/draft) - SaveExplorerDraft(c fiber.Ctx, project Project, explorerId ExplorerId) error - - // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/export) - ExportExplorerWorkspace(c fiber.Ctx, project Project, explorerId ExplorerId) error + ApplyExplorerBuilderCommands(c fiber.Ctx, project Project, explorerId ExplorerId, params ApplyExplorerBuilderCommandsParams) error // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/preview) PreviewExplorer(c fiber.Ctx, project Project, explorerId ExplorerId) error // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/publish) - PublishExplorer(c fiber.Ctx, project Project, explorerId ExplorerId) error + PublishExplorer(c fiber.Ctx, project Project, explorerId ExplorerId, params PublishExplorerParams) error // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/reconcile) - ReconcileExplorerBuilder(c fiber.Ctx, project Project, explorerId ExplorerId) error + ReconcileExplorerBuilder(c fiber.Ctx, project Project, explorerId ExplorerId, params ReconcileExplorerBuilderParams) error // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/suggestions) SearchExplorerCandidates(c fiber.Ctx, project Project, explorerId ExplorerId) error - // AbortSnapshot Abort a generation snapshot. - // (DELETE /api/v1/projects/{project}/generations/{generation}) - AbortSnapshot(c fiber.Ctx, project Project, generation Generation) error - // GetSnapshot Read generation snapshot status. - // (GET /api/v1/projects/{project}/generations/{generation}) - GetSnapshot(c fiber.Ctx, project Project, generation Generation) error - // CreateSnapshot Create or resume an immutable generation snapshot. - // (POST /api/v1/projects/{project}/generations/{generation}) - CreateSnapshot(c fiber.Ctx, project Project, generation Generation) error // PublishRepositoryExplorerConfig Compile and publish a repository-owned Explorer workspace. // (POST /api/v1/projects/{project}/generations/{generation}/explorer-config) PublishRepositoryExplorerConfig(c fiber.Ctx, project Project, generation Generation, params PublishRepositoryExplorerConfigParams) error - // FinalizeSnapshot Validate and finalize an immutable generation snapshot. - // (POST /api/v1/projects/{project}/generations/{generation}/finalize) - FinalizeSnapshot(c fiber.Ctx, project Project, generation Generation) error - // UploadSnapshotResource Stream one resource type into an immutable snapshot. - // (PUT /api/v1/projects/{project}/generations/{generation}/resources/{resourceType}) - UploadSnapshotResource(c fiber.Ctx, project Project, generation Generation, resourceType ResourceType, params UploadSnapshotResourceParams) error - // CreateRelease Create a durable project release candidate. - // (POST /api/v1/projects/{project}/releases) - CreateRelease(c fiber.Ctx, project Project) error - // ActivateReleaseCompatibility Create and activate a release in one legacy operation. - // (POST /api/v1/projects/{project}/releases/activate) - // - // Deprecated: this operation has been marked as deprecated upstream, but no `x-deprecated-reason` was set - ActivateReleaseCompatibility(c fiber.Ctx, project Project) error - // GetActiveRelease Read the active project release. - // (GET /api/v1/projects/{project}/releases/active) - GetActiveRelease(c fiber.Ctx, project Project) error - // GetRelease Read a project release by ID. - // (GET /api/v1/projects/{project}/releases/{release}) - GetRelease(c fiber.Ctx, project Project, release Release) error - // ActivateRelease Activate an existing durable release. - // (POST /api/v1/projects/{project}/releases/{release}/activate) - ActivateRelease(c fiber.Ctx, project Project, release Release) error - // UploadProjectResource Upload one multipart NDJSON resource file. - // (PUT /api/v1/projects/{project}/resources/{resourceType}) - UploadProjectResource(c fiber.Ctx, project Project, resourceType ResourceType) error - // UploadRawNDJSON Stream raw NDJSON into a generation. - // (PUT /api/v1/raw) - UploadRawNDJSON(c fiber.Ctx, params UploadRawNDJSONParams) error // GetApolloSandbox Render the Apollo GraphQL sandbox. // (GET /apollo) GetApolloSandbox(c fiber.Ctx) error @@ -1579,6 +1453,59 @@ func (siw *ServerInterfaceWrapper) GetRecipeExecution(c fiber.Ctx) error { return handler(c) } +// GetDatasetGenerationStatus operation middleware +func (siw *ServerInterfaceWrapper) GetDatasetGenerationStatus(c fiber.Ctx) error { + + var err error + _ = err + + // ------------- Path parameter "project" ------------- + var project Project + + err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) + } + + // ------------- Path parameter "generation" ------------- + var generation Generation + + err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) + } + + // Parameter object where we will unmarshal all parameters from the context + var params GetDatasetGenerationStatusParams + + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) + } + + // ------------- Optional query parameter "auth_resource_path" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) + } + + handler := func(c fiber.Ctx) error { + return siw.Handler.GetDatasetGenerationStatus(c, project, generation, params) + } + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + m := siw.HandlerMiddlewares[i] + next := handler + handler = func(c fiber.Ctx) error { + return m(c, next) + } + } + + return handler(c) +} + // CreateDatasetGeneration operation middleware func (siw *ServerInterfaceWrapper) CreateDatasetGeneration(c fiber.Ctx) error { @@ -1676,8 +1603,8 @@ func (siw *ServerInterfaceWrapper) ActivateDatasetGeneration(c fiber.Ctx) error return handler(c) } -// GetRepositoryExplorerConfig operation middleware -func (siw *ServerInterfaceWrapper) GetRepositoryExplorerConfig(c fiber.Ctx) error { +// ListExplorers operation middleware +func (siw *ServerInterfaceWrapper) ListExplorers(c fiber.Ctx) error { var err error _ = err @@ -1691,7 +1618,7 @@ func (siw *ServerInterfaceWrapper) GetRepositoryExplorerConfig(c fiber.Ctx) erro } handler := func(c fiber.Ctx) error { - return siw.Handler.GetRepositoryExplorerConfig(c, project) + return siw.Handler.ListExplorers(c, project) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1705,8 +1632,8 @@ func (siw *ServerInterfaceWrapper) GetRepositoryExplorerConfig(c fiber.Ctx) erro return handler(c) } -// ListExplorers operation middleware -func (siw *ServerInterfaceWrapper) ListExplorers(c fiber.Ctx) error { +// CreateExplorer operation middleware +func (siw *ServerInterfaceWrapper) CreateExplorer(c fiber.Ctx) error { var err error _ = err @@ -1719,37 +1646,24 @@ func (siw *ServerInterfaceWrapper) ListExplorers(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) } - handler := func(c fiber.Ctx) error { - return siw.Handler.ListExplorers(c, project) - } + // Parameter object where we will unmarshal all parameters from the context + var params CreateExplorerParams - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) } - return handler(c) -} - -// CreateExplorer operation middleware -func (siw *ServerInterfaceWrapper) CreateExplorer(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project + // ------------- Optional query parameter "auth_resource_path" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) } handler := func(c fiber.Ctx) error { - return siw.Handler.CreateExplorer(c, project) + return siw.Handler.CreateExplorer(c, project, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1837,8 +1751,8 @@ func (siw *ServerInterfaceWrapper) GetExplorerBuilder(c fiber.Ctx) error { return handler(c) } -// CompileExplorerBuilder operation middleware -func (siw *ServerInterfaceWrapper) CompileExplorerBuilder(c fiber.Ctx) error { +// GetExplorerAuthoringCapability operation middleware +func (siw *ServerInterfaceWrapper) GetExplorerAuthoringCapability(c fiber.Ctx) error { var err error _ = err @@ -1860,7 +1774,7 @@ func (siw *ServerInterfaceWrapper) CompileExplorerBuilder(c fiber.Ctx) error { } handler := func(c fiber.Ctx) error { - return siw.Handler.CompileExplorerBuilder(c, project, explorerId) + return siw.Handler.GetExplorerAuthoringCapability(c, project, explorerId) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1874,8 +1788,8 @@ func (siw *ServerInterfaceWrapper) CompileExplorerBuilder(c fiber.Ctx) error { return handler(c) } -// GetExplorerCandidateSuggestions operation middleware -func (siw *ServerInterfaceWrapper) GetExplorerCandidateSuggestions(c fiber.Ctx) error { +// ApplyExplorerBuilderCommands operation middleware +func (siw *ServerInterfaceWrapper) ApplyExplorerBuilderCommands(c fiber.Ctx) error { var err error _ = err @@ -1896,24 +1810,24 @@ func (siw *ServerInterfaceWrapper) GetExplorerCandidateSuggestions(c fiber.Ctx) return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) } - // ------------- Path parameter "snapshotToken" ------------- - var snapshotToken string + // Parameter object where we will unmarshal all parameters from the context + var params ApplyExplorerBuilderCommandsParams - err = runtime.BindStyledParameterWithOptions("simple", "snapshotToken", c.Params("snapshotToken"), &snapshotToken, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter snapshotToken: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) } - // ------------- Path parameter "candidateId" ------------- - var candidateId string + // ------------- Optional query parameter "auth_resource_path" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "candidateId", c.Params("candidateId"), &candidateId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter candidateId: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) } handler := func(c fiber.Ctx) error { - return siw.Handler.GetExplorerCandidateSuggestions(c, project, explorerId, snapshotToken, candidateId) + return siw.Handler.ApplyExplorerBuilderCommands(c, project, explorerId, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1927,8 +1841,8 @@ func (siw *ServerInterfaceWrapper) GetExplorerCandidateSuggestions(c fiber.Ctx) return handler(c) } -// GetExplorerAuthoringCapability operation middleware -func (siw *ServerInterfaceWrapper) GetExplorerAuthoringCapability(c fiber.Ctx) error { +// PreviewExplorer operation middleware +func (siw *ServerInterfaceWrapper) PreviewExplorer(c fiber.Ctx) error { var err error _ = err @@ -1950,7 +1864,7 @@ func (siw *ServerInterfaceWrapper) GetExplorerAuthoringCapability(c fiber.Ctx) e } handler := func(c fiber.Ctx) error { - return siw.Handler.GetExplorerAuthoringCapability(c, project, explorerId) + return siw.Handler.PreviewExplorer(c, project, explorerId) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1964,8 +1878,8 @@ func (siw *ServerInterfaceWrapper) GetExplorerAuthoringCapability(c fiber.Ctx) e return handler(c) } -// ApplyExplorerBuilderCommands operation middleware -func (siw *ServerInterfaceWrapper) ApplyExplorerBuilderCommands(c fiber.Ctx) error { +// PublishExplorer operation middleware +func (siw *ServerInterfaceWrapper) PublishExplorer(c fiber.Ctx) error { var err error _ = err @@ -1986,45 +1900,24 @@ func (siw *ServerInterfaceWrapper) ApplyExplorerBuilderCommands(c fiber.Ctx) err return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) } - handler := func(c fiber.Ctx) error { - return siw.Handler.ApplyExplorerBuilderCommands(c, project, explorerId) - } - - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } - - return handler(c) -} - -// CompileExplorer operation middleware -func (siw *ServerInterfaceWrapper) CompileExplorer(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project + // Parameter object where we will unmarshal all parameters from the context + var params PublishExplorerParams - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) } - // ------------- Path parameter "explorerId" ------------- - var explorerId ExplorerId + // ------------- Optional query parameter "auth_resource_path" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "explorerId", c.Params("explorerId"), &explorerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) } handler := func(c fiber.Ctx) error { - return siw.Handler.CompileExplorer(c, project, explorerId) + return siw.Handler.PublishExplorer(c, project, explorerId, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2038,8 +1931,8 @@ func (siw *ServerInterfaceWrapper) CompileExplorer(c fiber.Ctx) error { return handler(c) } -// SaveExplorerDraft operation middleware -func (siw *ServerInterfaceWrapper) SaveExplorerDraft(c fiber.Ctx) error { +// ReconcileExplorerBuilder operation middleware +func (siw *ServerInterfaceWrapper) ReconcileExplorerBuilder(c fiber.Ctx) error { var err error _ = err @@ -2060,45 +1953,24 @@ func (siw *ServerInterfaceWrapper) SaveExplorerDraft(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) } - handler := func(c fiber.Ctx) error { - return siw.Handler.SaveExplorerDraft(c, project, explorerId) - } - - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } - - return handler(c) -} - -// ExportExplorerWorkspace operation middleware -func (siw *ServerInterfaceWrapper) ExportExplorerWorkspace(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project + // Parameter object where we will unmarshal all parameters from the context + var params ReconcileExplorerBuilderParams - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) } - // ------------- Path parameter "explorerId" ------------- - var explorerId ExplorerId + // ------------- Optional query parameter "auth_resource_path" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "explorerId", c.Params("explorerId"), &explorerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) } handler := func(c fiber.Ctx) error { - return siw.Handler.ExportExplorerWorkspace(c, project, explorerId) + return siw.Handler.ReconcileExplorerBuilder(c, project, explorerId, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2112,8 +1984,8 @@ func (siw *ServerInterfaceWrapper) ExportExplorerWorkspace(c fiber.Ctx) error { return handler(c) } -// PreviewExplorer operation middleware -func (siw *ServerInterfaceWrapper) PreviewExplorer(c fiber.Ctx) error { +// SearchExplorerCandidates operation middleware +func (siw *ServerInterfaceWrapper) SearchExplorerCandidates(c fiber.Ctx) error { var err error _ = err @@ -2135,7 +2007,7 @@ func (siw *ServerInterfaceWrapper) PreviewExplorer(c fiber.Ctx) error { } handler := func(c fiber.Ctx) error { - return siw.Handler.PreviewExplorer(c, project, explorerId) + return siw.Handler.SearchExplorerCandidates(c, project, explorerId) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2149,8 +2021,8 @@ func (siw *ServerInterfaceWrapper) PreviewExplorer(c fiber.Ctx) error { return handler(c) } -// PublishExplorer operation middleware -func (siw *ServerInterfaceWrapper) PublishExplorer(c fiber.Ctx) error { +// PublishRepositoryExplorerConfig operation middleware +func (siw *ServerInterfaceWrapper) PublishRepositoryExplorerConfig(c fiber.Ctx) error { var err error _ = err @@ -2163,53 +2035,53 @@ func (siw *ServerInterfaceWrapper) PublishExplorer(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) } - // ------------- Path parameter "explorerId" ------------- - var explorerId ExplorerId + // ------------- Path parameter "generation" ------------- + var generation Generation - err = runtime.BindStyledParameterWithOptions("simple", "explorerId", c.Params("explorerId"), &explorerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) } - handler := func(c fiber.Ctx) error { - return siw.Handler.PublishExplorer(c, project, explorerId) + // Parameter object where we will unmarshal all parameters from the context + var params PublishRepositoryExplorerConfigParams + + var query url.Values + query, err = url.ParseQuery(string(c.Request().URI().QueryString())) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) } - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } - - return handler(c) -} + // ------------- Optional query parameter "auth_resource_path" ------------- -// ReconcileExplorerBuilder operation middleware -func (siw *ServerInterfaceWrapper) ReconcileExplorerBuilder(c fiber.Ctx) error { + err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) + } - var err error - _ = err + headers := c.GetReqHeaders() - // ------------- Path parameter "project" ------------- - var project Project + // ------------- Required header parameter "X-Loom-Source-Commit" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("X-Loom-Source-Commit")]; found { + var XLoomSourceCommit string + n := len(valueList) + if n != 1 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for ParamName X-Loom-Source-Commit, 1 is required, but %d found", n)) + } - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } + err = runtime.BindStyledParameterWithOptions("simple", "X-Loom-Source-Commit", valueList[0], &XLoomSourceCommit, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter X-Loom-Source-Commit: %w", err).Error()) + } - // ------------- Path parameter "explorerId" ------------- - var explorerId ExplorerId + params.XLoomSourceCommit = XLoomSourceCommit - err = runtime.BindStyledParameterWithOptions("simple", "explorerId", c.Params("explorerId"), &explorerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) + } else { + return fiber.NewError(fiber.StatusBadRequest, "Header parameter X-Loom-Source-Commit is required, but not found") } handler := func(c fiber.Ctx) error { - return siw.Handler.ReconcileExplorerBuilder(c, project, explorerId) + return siw.Handler.PublishRepositoryExplorerConfig(c, project, generation, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2223,30 +2095,11 @@ func (siw *ServerInterfaceWrapper) ReconcileExplorerBuilder(c fiber.Ctx) error { return handler(c) } -// SearchExplorerCandidates operation middleware -func (siw *ServerInterfaceWrapper) SearchExplorerCandidates(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project - - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } - - // ------------- Path parameter "explorerId" ------------- - var explorerId ExplorerId - - err = runtime.BindStyledParameterWithOptions("simple", "explorerId", c.Params("explorerId"), &explorerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) - } +// GetApolloSandbox operation middleware +func (siw *ServerInterfaceWrapper) GetApolloSandbox(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.SearchExplorerCandidates(c, project, explorerId) + return siw.Handler.GetApolloSandbox(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2260,30 +2113,11 @@ func (siw *ServerInterfaceWrapper) SearchExplorerCandidates(c fiber.Ctx) error { return handler(c) } -// AbortSnapshot operation middleware -func (siw *ServerInterfaceWrapper) AbortSnapshot(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project - - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } - - // ------------- Path parameter "generation" ------------- - var generation Generation - - err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) - } +// ExecuteDataframeGraphQL operation middleware +func (siw *ServerInterfaceWrapper) ExecuteDataframeGraphQL(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.AbortSnapshot(c, project, generation) + return siw.Handler.ExecuteDataframeGraphQL(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2297,30 +2131,11 @@ func (siw *ServerInterfaceWrapper) AbortSnapshot(c fiber.Ctx) error { return handler(c) } -// GetSnapshot operation middleware -func (siw *ServerInterfaceWrapper) GetSnapshot(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project - - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } - - // ------------- Path parameter "generation" ------------- - var generation Generation - - err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) - } +// GetGraphQLPlayground operation middleware +func (siw *ServerInterfaceWrapper) GetGraphQLPlayground(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.GetSnapshot(c, project, generation) + return siw.Handler.GetGraphQLPlayground(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2334,30 +2149,11 @@ func (siw *ServerInterfaceWrapper) GetSnapshot(c fiber.Ctx) error { return handler(c) } -// CreateSnapshot operation middleware -func (siw *ServerInterfaceWrapper) CreateSnapshot(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project - - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } - - // ------------- Path parameter "generation" ------------- - var generation Generation - - err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) - } +// ExecuteGraphQL operation middleware +func (siw *ServerInterfaceWrapper) ExecuteGraphQL(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.CreateSnapshot(c, project, generation) + return siw.Handler.ExecuteGraphQL(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2371,67 +2167,29 @@ func (siw *ServerInterfaceWrapper) CreateSnapshot(c fiber.Ctx) error { return handler(c) } -// PublishRepositoryExplorerConfig operation middleware -func (siw *ServerInterfaceWrapper) PublishRepositoryExplorerConfig(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project - - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } - - // ------------- Path parameter "generation" ------------- - var generation Generation - - err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) - } - - // Parameter object where we will unmarshal all parameters from the context - var params PublishRepositoryExplorerConfigParams - - var query url.Values - query, err = url.ParseQuery(string(c.Request().URI().QueryString())) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) - } - - // ------------- Optional query parameter "auth_resource_path" ------------- +// GetHealth operation middleware +func (siw *ServerInterfaceWrapper) GetHealth(c fiber.Ctx) error { - err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) + handler := func(c fiber.Ctx) error { + return siw.Handler.GetHealth(c) } - headers := c.GetReqHeaders() - - // ------------- Required header parameter "X-Loom-Source-Commit" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("X-Loom-Source-Commit")]; found { - var XLoomSourceCommit string - n := len(valueList) - if n != 1 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for ParamName X-Loom-Source-Commit, 1 is required, but %d found", n)) - } - - err = runtime.BindStyledParameterWithOptions("simple", "X-Loom-Source-Commit", valueList[0], &XLoomSourceCommit, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter X-Loom-Source-Commit: %w", err).Error()) + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + m := siw.HandlerMiddlewares[i] + next := handler + handler = func(c fiber.Ctx) error { + return m(c, next) } + } - params.XLoomSourceCommit = XLoomSourceCommit + return handler(c) +} - } else { - return fiber.NewError(fiber.StatusBadRequest, "Header parameter X-Loom-Source-Commit is required, but not found") - } +// GetLiveness operation middleware +func (siw *ServerInterfaceWrapper) GetLiveness(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.PublishRepositoryExplorerConfig(c, project, generation, params) + return siw.Handler.GetLiveness(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2445,30 +2203,11 @@ func (siw *ServerInterfaceWrapper) PublishRepositoryExplorerConfig(c fiber.Ctx) return handler(c) } -// FinalizeSnapshot operation middleware -func (siw *ServerInterfaceWrapper) FinalizeSnapshot(c fiber.Ctx) error { - - var err error - _ = err - - // ------------- Path parameter "project" ------------- - var project Project - - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } - - // ------------- Path parameter "generation" ------------- - var generation Generation - - err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) - } +// GetReadiness operation middleware +func (siw *ServerInterfaceWrapper) GetReadiness(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.FinalizeSnapshot(c, project, generation) + return siw.Handler.GetReadiness(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2482,4190 +2221,1734 @@ func (siw *ServerInterfaceWrapper) FinalizeSnapshot(c fiber.Ctx) error { return handler(c) } -// UploadSnapshotResource operation middleware -func (siw *ServerInterfaceWrapper) UploadSnapshotResource(c fiber.Ctx) error { - - var err error - _ = err +// FiberServerOptions provides options for the Fiber server. +type FiberServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc + HandlerMiddlewares []HandlerMiddlewareFunc +} - // ------------- Path parameter "project" ------------- - var project Project +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router fiber.Router, si ServerInterface) { + RegisterHandlersWithOptions(router, si, FiberServerOptions{}) +} - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) +// RegisterHandlersWithOptions creates http.Handler with additional options +func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, options FiberServerOptions) { + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.HandlerMiddlewares, } - // ------------- Path parameter "generation" ------------- - var generation Generation - - err = runtime.BindStyledParameterWithOptions("simple", "generation", c.Params("generation"), &generation, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) + for _, m := range options.Middlewares { + router.Use(fiber.Handler(m)) } - // ------------- Path parameter "resourceType" ------------- - var resourceType ResourceType - - err = runtime.BindStyledParameterWithOptions("simple", "resourceType", c.Params("resourceType"), &resourceType, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter resourceType: %w", err).Error()) - } - - // Parameter object where we will unmarshal all parameters from the context - var params UploadSnapshotResourceParams + router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/capability", wrapper.GetExplorerAuthoringCapability) - headers := c.GetReqHeaders() + router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/suggestions", wrapper.SearchExplorerCandidates) - // ------------- Optional header parameter "X-Content-SHA256" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("X-Content-SHA256")]; found { - var XContentSHA256 string - n := len(valueList) - if n != 1 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Too many values for ParamName X-Content-SHA256, 1 is required, but %d found", n)) - } + router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/builder", wrapper.GetExplorerBuilder) - err = runtime.BindStyledParameterWithOptions("simple", "X-Content-SHA256", valueList[0], &XContentSHA256, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter X-Content-SHA256: %w", err).Error()) - } + router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/commands", wrapper.ApplyExplorerBuilderCommands) - params.XContentSHA256 = &XContentSHA256 + router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/reconcile", wrapper.ReconcileExplorerBuilder) - } + router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/preview", wrapper.PreviewExplorer) - handler := func(c fiber.Ctx) error { - return siw.Handler.UploadSnapshotResource(c, project, generation, resourceType, params) - } + router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/publish", wrapper.PublishExplorer) - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } + router.Get(options.BaseURL+"/health", wrapper.GetHealth) - return handler(c) -} + router.Get(options.BaseURL+"/livez", wrapper.GetLiveness) -// CreateRelease operation middleware -func (siw *ServerInterfaceWrapper) CreateRelease(c fiber.Ctx) error { + router.Get(options.BaseURL+"/readyz", wrapper.GetReadiness) - var err error - _ = err + router.Get(options.BaseURL+"/api/v1/datasets/:project/generations/:generation", wrapper.GetDatasetGenerationStatus) - // ------------- Path parameter "project" ------------- - var project Project + router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation", wrapper.CreateDatasetGeneration) - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } + router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation/activate", wrapper.ActivateDatasetGeneration) - handler := func(c fiber.Ctx) error { - return siw.Handler.CreateRelease(c, project) - } + router.Get(options.BaseURL+"/api/v1/dataframe/recipe-executions/:id", wrapper.GetRecipeExecution) - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } + router.Get(options.BaseURL+"/graphql/graph", wrapper.GetGraphQLPlayground) - return handler(c) -} + router.Post(options.BaseURL+"/graphql/graph", wrapper.ExecuteGraphQL) -// ActivateReleaseCompatibility operation middleware -func (siw *ServerInterfaceWrapper) ActivateReleaseCompatibility(c fiber.Ctx) error { + router.Post(options.BaseURL+"/graphql/dataframe", wrapper.ExecuteDataframeGraphQL) - var err error - _ = err + router.Get(options.BaseURL+"/apollo", wrapper.GetApolloSandbox) - // ------------- Path parameter "project" ------------- - var project Project + router.Get(options.BaseURL+"/api/v1/projects/:project/explorers", wrapper.ListExplorers) - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } + router.Post(options.BaseURL+"/api/v1/projects/:project/explorers", wrapper.CreateExplorer) - handler := func(c fiber.Ctx) error { - return siw.Handler.ActivateReleaseCompatibility(c, project) - } + router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId", wrapper.GetExplorer) - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } + router.Post(options.BaseURL+"/api/v1/projects/:project/generations/:generation/explorer-config", wrapper.PublishRepositoryExplorerConfig) - return handler(c) } -// GetActiveRelease operation middleware -func (siw *ServerInterfaceWrapper) GetActiveRelease(c fiber.Ctx) error { +type AuthoringBadRequestJSONResponse ErrorResponse - var err error - _ = err +type AuthoringClientClosedRequestJSONResponse ErrorResponse - // ------------- Path parameter "project" ------------- - var project Project +type AuthoringConflictJSONResponse ErrorResponse - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } +type AuthoringForbiddenJSONResponse ErrorResponse - handler := func(c fiber.Ctx) error { - return siw.Handler.GetActiveRelease(c, project) - } +type AuthoringGatewayTimeoutJSONResponse ErrorResponse - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +type AuthoringInternalErrorJSONResponse ErrorResponse - return handler(c) -} +type AuthoringNotFoundJSONResponse ErrorResponse -// GetRelease operation middleware -func (siw *ServerInterfaceWrapper) GetRelease(c fiber.Ctx) error { +type AuthoringPayloadTooLargeJSONResponse ErrorResponse - var err error - _ = err +type AuthoringTooManyRequestsJSONResponse ErrorResponse - // ------------- Path parameter "project" ------------- - var project Project +type AuthoringUnavailableJSONResponse ErrorResponse - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } +type AuthoringUnprocessableJSONResponse ErrorResponse - // ------------- Path parameter "release" ------------- - var release Release +type GraphQLBadRequestJSONResponse RawJSON - err = runtime.BindStyledParameterWithOptions("simple", "release", c.Params("release"), &release, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter release: %w", err).Error()) - } +type GraphQLInternalErrorJSONResponse RawJSON - handler := func(c fiber.Ctx) error { - return siw.Handler.GetRelease(c, project, release) - } +type GraphQLUnprocessableJSONResponse RawJSON - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +type HTMLTexthtmlResponse struct { + Body io.Reader - return handler(c) + ContentLength int64 } -// ActivateRelease operation middleware -func (siw *ServerInterfaceWrapper) ActivateRelease(c fiber.Ctx) error { +type LegacyBadRequestJSONResponse LegacyErrorResponse - var err error - _ = err +type LegacyConflictJSONResponse LegacyErrorResponse - // ------------- Path parameter "project" ------------- - var project Project +type LegacyForbiddenJSONResponse LegacyErrorResponse - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } +type LegacyInternalErrorJSONResponse LegacyErrorResponse - // ------------- Path parameter "release" ------------- - var release Release +type LegacyNotFoundJSONResponse LegacyErrorResponse - err = runtime.BindStyledParameterWithOptions("simple", "release", c.Params("release"), &release, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter release: %w", err).Error()) - } +type LegacyUnavailableJSONResponse LegacyErrorResponse - handler := func(c fiber.Ctx) error { - return siw.Handler.ActivateRelease(c, project, release) - } +type LegacyUnprocessableJSONResponse LegacyErrorResponse - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +type ServiceBadRequestJSONResponse ServiceErrorResponse - return handler(c) -} +type ServiceConflictJSONResponse ServiceErrorResponse -// UploadProjectResource operation middleware -func (siw *ServerInterfaceWrapper) UploadProjectResource(c fiber.Ctx) error { +type ServiceForbiddenJSONResponse ServiceErrorResponse - var err error - _ = err +type ServiceInternalErrorJSONResponse ServiceErrorResponse - // ------------- Path parameter "project" ------------- - var project Project +type ServiceNotFoundJSONResponse ServiceErrorResponse - err = runtime.BindStyledParameterWithOptions("simple", "project", c.Params("project"), &project, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } +type ServiceUnauthorizedJSONResponse ServiceErrorResponse - // ------------- Path parameter "resourceType" ------------- - var resourceType ResourceType +type ServiceUnavailableJSONResponse ServiceErrorResponse - err = runtime.BindStyledParameterWithOptions("simple", "resourceType", c.Params("resourceType"), &resourceType, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter resourceType: %w", err).Error()) - } +type ServiceUnprocessableJSONResponse ServiceErrorResponse - handler := func(c fiber.Ctx) error { - return siw.Handler.UploadProjectResource(c, project, resourceType) - } +type ServiceUnsupportedMediaTypeJSONResponse ServiceErrorResponse - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +type GetRecipeExecutionRequestObject struct { + Id string `json:"id"` +} - return handler(c) +type GetRecipeExecutionResponseObject interface { + VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error } -// UploadRawNDJSON operation middleware -func (siw *ServerInterfaceWrapper) UploadRawNDJSON(c fiber.Ctx) error { +type GetRecipeExecution200JSONResponse RawJSON - var err error - _ = err +func (response GetRecipeExecution200JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(200) - // Parameter object where we will unmarshal all parameters from the context - var params UploadRawNDJSONParams + return ctx.JSON(&response) +} - var query url.Values - query, err = url.ParseQuery(string(c.Request().URI().QueryString())) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for query string: %w", err).Error()) - } +type GetRecipeExecution401JSONResponse RawJSON - // ------------- Optional query parameter "project" ------------- +func (response GetRecipeExecution401JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(401) - err = runtime.BindQueryParameterWithOptions("form", true, false, "project", query, ¶ms.Project, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) - } + return ctx.JSON(&response) +} - // ------------- Optional query parameter "generation" ------------- +type GetRecipeExecution403JSONResponse struct{ LegacyForbiddenJSONResponse } - err = runtime.BindQueryParameterWithOptions("form", true, false, "generation", query, ¶ms.Generation, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter generation: %w", err).Error()) - } +func (response GetRecipeExecution403JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(403) - // ------------- Optional query parameter "auth_resource_path" ------------- + return ctx.JSON(&response) +} - err = runtime.BindQueryParameterWithOptions("form", true, false, "auth_resource_path", query, ¶ms.AuthResourcePath, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter auth_resource_path: %w", err).Error()) - } +type GetRecipeExecution404JSONResponse struct{ LegacyNotFoundJSONResponse } - handler := func(c fiber.Ctx) error { - return siw.Handler.UploadRawNDJSON(c, params) - } +func (response GetRecipeExecution404JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(404) - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } + return ctx.JSON(&response) +} - return handler(c) +type GetDatasetGenerationStatusRequestObject struct { + Project Project `json:"project"` + Generation Generation `json:"generation"` + Params GetDatasetGenerationStatusParams } -// GetApolloSandbox operation middleware -func (siw *ServerInterfaceWrapper) GetApolloSandbox(c fiber.Ctx) error { +type GetDatasetGenerationStatusResponseObject interface { + VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error +} - handler := func(c fiber.Ctx) error { - return siw.Handler.GetApolloSandbox(c) - } +type GetDatasetGenerationStatus200JSONResponse RawJSON - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +func (response GetDatasetGenerationStatus200JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(200) - return handler(c) + return ctx.JSON(&response) } -// ExecuteDataframeGraphQL operation middleware -func (siw *ServerInterfaceWrapper) ExecuteDataframeGraphQL(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.ExecuteDataframeGraphQL(c) - } +type GetDatasetGenerationStatus400JSONResponse struct{ ServiceBadRequestJSONResponse } - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +func (response GetDatasetGenerationStatus400JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(400) - return handler(c) + return ctx.JSON(&response) } -// GetGraphQLPlayground operation middleware -func (siw *ServerInterfaceWrapper) GetGraphQLPlayground(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetGraphQLPlayground(c) - } +type GetDatasetGenerationStatus401JSONResponse struct { + ServiceUnauthorizedJSONResponse +} - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +func (response GetDatasetGenerationStatus401JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(401) - return handler(c) + return ctx.JSON(&response) } -// ExecuteGraphQL operation middleware -func (siw *ServerInterfaceWrapper) ExecuteGraphQL(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.ExecuteGraphQL(c) - } +type GetDatasetGenerationStatus403JSONResponse struct{ ServiceForbiddenJSONResponse } - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +func (response GetDatasetGenerationStatus403JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(403) - return handler(c) + return ctx.JSON(&response) } -// GetHealth operation middleware -func (siw *ServerInterfaceWrapper) GetHealth(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetHealth(c) - } +type GetDatasetGenerationStatus404JSONResponse struct{ ServiceNotFoundJSONResponse } - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +func (response GetDatasetGenerationStatus404JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(404) - return handler(c) + return ctx.JSON(&response) } -// GetLiveness operation middleware -func (siw *ServerInterfaceWrapper) GetLiveness(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetLiveness(c) - } +type GetDatasetGenerationStatus500JSONResponse struct { + ServiceInternalErrorJSONResponse +} - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +func (response GetDatasetGenerationStatus500JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(500) - return handler(c) + return ctx.JSON(&response) } -// GetReadiness operation middleware -func (siw *ServerInterfaceWrapper) GetReadiness(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetReadiness(c) - } +type GetDatasetGenerationStatus503JSONResponse struct{ ServiceUnavailableJSONResponse } - for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { - m := siw.HandlerMiddlewares[i] - next := handler - handler = func(c fiber.Ctx) error { - return m(c, next) - } - } +func (response GetDatasetGenerationStatus503JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(503) - return handler(c) + return ctx.JSON(&response) } -// FiberServerOptions provides options for the Fiber server. -type FiberServerOptions struct { - BaseURL string - Middlewares []MiddlewareFunc - HandlerMiddlewares []HandlerMiddlewareFunc +type CreateDatasetGenerationRequestObject struct { + Project Project `json:"project"` + Generation Generation `json:"generation"` + Body *multipart.Reader } -// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. -func RegisterHandlers(router fiber.Router, si ServerInterface) { - RegisterHandlersWithOptions(router, si, FiberServerOptions{}) +type CreateDatasetGenerationResponseObject interface { + VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error } -// RegisterHandlersWithOptions creates http.Handler with additional options -func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, options FiberServerOptions) { - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.HandlerMiddlewares, - } +type CreateDatasetGeneration200JSONResponse RawJSON - for _, m := range options.Middlewares { - router.Use(fiber.Handler(m)) - } +func (response CreateDatasetGeneration200JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(200) - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/capability", wrapper.GetExplorerAuthoringCapability) + return ctx.JSON(&response) +} - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/capabilities/:snapshotToken/candidates/:candidateId/suggestions", wrapper.GetExplorerCandidateSuggestions) +type CreateDatasetGeneration400JSONResponse struct{ ServiceBadRequestJSONResponse } - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/suggestions", wrapper.SearchExplorerCandidates) +func (response CreateDatasetGeneration400JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(400) - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/builder", wrapper.GetExplorerBuilder) + return ctx.JSON(&response) +} - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/builder", wrapper.CompileExplorerBuilder) +type CreateDatasetGeneration401JSONResponse struct { + ServiceUnauthorizedJSONResponse +} - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/compile", wrapper.CompileExplorer) +func (response CreateDatasetGeneration401JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(401) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/commands", wrapper.ApplyExplorerBuilderCommands) + return ctx.JSON(&response) +} - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/reconcile", wrapper.ReconcileExplorerBuilder) +type CreateDatasetGeneration403JSONResponse struct{ ServiceForbiddenJSONResponse } - router.Put(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/draft", wrapper.SaveExplorerDraft) +func (response CreateDatasetGeneration403JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(403) - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/export", wrapper.ExportExplorerWorkspace) + return ctx.JSON(&response) +} - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/preview", wrapper.PreviewExplorer) +type CreateDatasetGeneration409JSONResponse struct{ ServiceConflictJSONResponse } - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/publish", wrapper.PublishExplorer) +func (response CreateDatasetGeneration409JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(409) - router.Get(options.BaseURL+"/health", wrapper.GetHealth) + return ctx.JSON(&response) +} - router.Get(options.BaseURL+"/livez", wrapper.GetLiveness) +type CreateDatasetGeneration415JSONResponse struct { + ServiceUnsupportedMediaTypeJSONResponse +} - router.Get(options.BaseURL+"/readyz", wrapper.GetReadiness) +func (response CreateDatasetGeneration415JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(415) - router.Put(options.BaseURL+"/api/v1/projects/:project/resources/:resourceType", wrapper.UploadProjectResource) + return ctx.JSON(&response) +} - router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation", wrapper.CreateDatasetGeneration) +type CreateDatasetGeneration422JSONResponse struct { + ServiceUnprocessableJSONResponse +} - router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation/activate", wrapper.ActivateDatasetGeneration) +func (response CreateDatasetGeneration422JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(422) - router.Put(options.BaseURL+"/api/v1/raw", wrapper.UploadRawNDJSON) + return ctx.JSON(&response) +} - router.Delete(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.AbortSnapshot) +type CreateDatasetGeneration500JSONResponse struct { + ServiceInternalErrorJSONResponse +} - router.Get(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.GetSnapshot) +func (response CreateDatasetGeneration500JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(500) - router.Post(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.CreateSnapshot) + return ctx.JSON(&response) +} - router.Put(options.BaseURL+"/api/v1/projects/:project/generations/:generation/resources/:resourceType", wrapper.UploadSnapshotResource) +type CreateDatasetGeneration503JSONResponse struct{ ServiceUnavailableJSONResponse } - router.Post(options.BaseURL+"/api/v1/projects/:project/generations/:generation/finalize", wrapper.FinalizeSnapshot) +func (response CreateDatasetGeneration503JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(503) - router.Post(options.BaseURL+"/api/v1/projects/:project/releases", wrapper.CreateRelease) + return ctx.JSON(&response) +} - router.Get(options.BaseURL+"/api/v1/projects/:project/releases/active", wrapper.GetActiveRelease) +type ActivateDatasetGenerationRequestObject struct { + Project Project `json:"project"` + Generation Generation `json:"generation"` + Params ActivateDatasetGenerationParams +} - router.Get(options.BaseURL+"/api/v1/projects/:project/releases/:release", wrapper.GetRelease) +type ActivateDatasetGenerationResponseObject interface { + VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error +} - router.Post(options.BaseURL+"/api/v1/projects/:project/releases/:release/activate", wrapper.ActivateRelease) +type ActivateDatasetGeneration200JSONResponse RawJSON - router.Post(options.BaseURL+"/api/v1/projects/:project/releases/activate", wrapper.ActivateReleaseCompatibility) +func (response ActivateDatasetGeneration200JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(200) - router.Get(options.BaseURL+"/api/v1/dataframe/recipe-executions/:id", wrapper.GetRecipeExecution) + return ctx.JSON(&response) +} - router.Get(options.BaseURL+"/graphql/graph", wrapper.GetGraphQLPlayground) +type ActivateDatasetGeneration400JSONResponse struct{ ServiceBadRequestJSONResponse } - router.Post(options.BaseURL+"/graphql/graph", wrapper.ExecuteGraphQL) +func (response ActivateDatasetGeneration400JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(400) - router.Post(options.BaseURL+"/graphql/dataframe", wrapper.ExecuteDataframeGraphQL) + return ctx.JSON(&response) +} - router.Get(options.BaseURL+"/apollo", wrapper.GetApolloSandbox) +type ActivateDatasetGeneration401JSONResponse struct { + ServiceUnauthorizedJSONResponse +} - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers", wrapper.ListExplorers) +func (response ActivateDatasetGeneration401JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(401) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers", wrapper.CreateExplorer) + return ctx.JSON(&response) +} - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId", wrapper.GetExplorer) +type ActivateDatasetGeneration403JSONResponse struct{ ServiceForbiddenJSONResponse } - router.Post(options.BaseURL+"/api/v1/projects/:project/generations/:generation/explorer-config", wrapper.PublishRepositoryExplorerConfig) - - router.Get(options.BaseURL+"/api/v1/projects/:project/explorer-config", wrapper.GetRepositoryExplorerConfig) +func (response ActivateDatasetGeneration403JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(403) + return ctx.JSON(&response) } -type AuthoringBadRequestJSONResponse ErrorResponse - -type AuthoringClientClosedRequestJSONResponse ErrorResponse - -type AuthoringConflictJSONResponse ErrorResponse - -type AuthoringForbiddenJSONResponse ErrorResponse - -type AuthoringGatewayTimeoutJSONResponse ErrorResponse - -type AuthoringInternalErrorJSONResponse ErrorResponse - -type AuthoringNotFoundJSONResponse ErrorResponse - -type AuthoringPayloadTooLargeJSONResponse ErrorResponse - -type AuthoringTooManyRequestsJSONResponse ErrorResponse - -type AuthoringUnavailableJSONResponse ErrorResponse - -type AuthoringUnprocessableJSONResponse ErrorResponse - -type GraphQLBadRequestJSONResponse RawJSON - -type GraphQLInternalErrorJSONResponse RawJSON - -type GraphQLUnprocessableJSONResponse RawJSON +type ActivateDatasetGeneration409JSONResponse struct{ ServiceConflictJSONResponse } -type HTMLTexthtmlResponse struct { - Body io.Reader +func (response ActivateDatasetGeneration409JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(409) - ContentLength int64 + return ctx.JSON(&response) } -type LegacyBadRequestJSONResponse LegacyErrorResponse - -type LegacyConflictJSONResponse LegacyErrorResponse - -type LegacyForbiddenJSONResponse LegacyErrorResponse - -type LegacyInternalErrorJSONResponse LegacyErrorResponse - -type LegacyNotFoundJSONResponse LegacyErrorResponse - -type LegacyUnavailableJSONResponse LegacyErrorResponse - -type LegacyUnprocessableJSONResponse LegacyErrorResponse - -type ServiceBadRequestJSONResponse ServiceErrorResponse - -type ServiceConflictJSONResponse ServiceErrorResponse - -type ServiceForbiddenJSONResponse ServiceErrorResponse - -type ServiceInternalErrorJSONResponse ServiceErrorResponse +type ActivateDatasetGeneration500JSONResponse struct { + ServiceInternalErrorJSONResponse +} -type ServiceNotFoundJSONResponse ServiceErrorResponse +func (response ActivateDatasetGeneration500JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(500) -type ServiceUnauthorizedJSONResponse ServiceErrorResponse + return ctx.JSON(&response) +} -type ServiceUnavailableJSONResponse ServiceErrorResponse +type ActivateDatasetGeneration503JSONResponse struct{ ServiceUnavailableJSONResponse } -type ServiceUnprocessableJSONResponse ServiceErrorResponse +func (response ActivateDatasetGeneration503JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(503) -type ServiceUnsupportedMediaTypeJSONResponse ServiceErrorResponse + return ctx.JSON(&response) +} -type GetRecipeExecutionRequestObject struct { - Id string `json:"id"` +type ListExplorersRequestObject struct { + Project Project `json:"project"` } -type GetRecipeExecutionResponseObject interface { - VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error +type ListExplorersResponseObject interface { + VisitListExplorersResponse(ctx fiber.Ctx) error } -type GetRecipeExecution200JSONResponse RawJSON +type ListExplorers200JSONResponse []map[string]interface{} -func (response GetRecipeExecution200JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { +func (response ListExplorers200JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type GetRecipeExecution401JSONResponse RawJSON +type ListExplorers401JSONResponse struct { + ServiceUnauthorizedJSONResponse +} -func (response GetRecipeExecution401JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { +func (response ListExplorers401JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type GetRecipeExecution403JSONResponse struct{ LegacyForbiddenJSONResponse } +type ListExplorers403JSONResponse struct{ LegacyForbiddenJSONResponse } -func (response GetRecipeExecution403JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { +func (response ListExplorers403JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type GetRecipeExecution404JSONResponse struct{ LegacyNotFoundJSONResponse } +type ListExplorers500JSONResponse struct { + LegacyInternalErrorJSONResponse +} -func (response GetRecipeExecution404JSONResponse) VisitGetRecipeExecutionResponse(ctx fiber.Ctx) error { +func (response ListExplorers500JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) + ctx.Status(500) return ctx.JSON(&response) } -type CreateDatasetGenerationRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` - Body *multipart.Reader +type CreateExplorerRequestObject struct { + Project Project `json:"project"` + Params CreateExplorerParams + Body *CreateExplorerJSONRequestBody } -type CreateDatasetGenerationResponseObject interface { - VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error +type CreateExplorerResponseObject interface { + VisitCreateExplorerResponse(ctx fiber.Ctx) error } -type CreateDatasetGeneration200JSONResponse RawJSON +type CreateExplorer201JSONResponse RawJSON -func (response CreateDatasetGeneration200JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response CreateExplorer201JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) + ctx.Status(201) return ctx.JSON(&response) } -type CreateDatasetGeneration400JSONResponse struct{ ServiceBadRequestJSONResponse } +type CreateExplorer400JSONResponse struct{ LegacyBadRequestJSONResponse } -func (response CreateDatasetGeneration400JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response CreateExplorer400JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(400) return ctx.JSON(&response) } -type CreateDatasetGeneration401JSONResponse struct { +type CreateExplorer401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response CreateDatasetGeneration401JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response CreateExplorer401JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type CreateDatasetGeneration403JSONResponse struct{ ServiceForbiddenJSONResponse } +type CreateExplorer403JSONResponse struct{ LegacyForbiddenJSONResponse } -func (response CreateDatasetGeneration403JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response CreateExplorer403JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type CreateDatasetGeneration409JSONResponse struct{ ServiceConflictJSONResponse } +type CreateExplorer409JSONResponse struct{ LegacyConflictJSONResponse } -func (response CreateDatasetGeneration409JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response CreateExplorer409JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(409) return ctx.JSON(&response) } -type CreateDatasetGeneration415JSONResponse struct { - ServiceUnsupportedMediaTypeJSONResponse -} - -func (response CreateDatasetGeneration415JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(415) - - return ctx.JSON(&response) -} - -type CreateDatasetGeneration422JSONResponse struct { - ServiceUnprocessableJSONResponse +type CreateExplorer422JSONResponse struct { + LegacyUnprocessableJSONResponse } -func (response CreateDatasetGeneration422JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response CreateExplorer422JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(422) return ctx.JSON(&response) } -type CreateDatasetGeneration500JSONResponse struct { - ServiceInternalErrorJSONResponse +type CreateExplorer500JSONResponse struct { + LegacyInternalErrorJSONResponse } -func (response CreateDatasetGeneration500JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response CreateExplorer500JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type CreateDatasetGeneration503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response CreateDatasetGeneration503JSONResponse) VisitCreateDatasetGenerationResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type ActivateDatasetGenerationRequestObject struct { +type GetExplorerRequestObject struct { Project Project `json:"project"` - Generation Generation `json:"generation"` - Params ActivateDatasetGenerationParams + ExplorerId ExplorerId `json:"explorerId"` } -type ActivateDatasetGenerationResponseObject interface { - VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error +type GetExplorerResponseObject interface { + VisitGetExplorerResponse(ctx fiber.Ctx) error } -type ActivateDatasetGeneration200JSONResponse RawJSON +type GetExplorer200JSONResponse RawJSON -func (response ActivateDatasetGeneration200JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response GetExplorer200JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type ActivateDatasetGeneration400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response ActivateDatasetGeneration400JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type ActivateDatasetGeneration401JSONResponse struct { +type GetExplorer401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response ActivateDatasetGeneration401JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response GetExplorer401JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type ActivateDatasetGeneration403JSONResponse struct{ ServiceForbiddenJSONResponse } +type GetExplorer403JSONResponse struct{ LegacyForbiddenJSONResponse } -func (response ActivateDatasetGeneration403JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response GetExplorer403JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type ActivateDatasetGeneration409JSONResponse struct{ ServiceConflictJSONResponse } +type GetExplorer404JSONResponse struct{ LegacyNotFoundJSONResponse } -func (response ActivateDatasetGeneration409JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response GetExplorer404JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) + ctx.Status(404) return ctx.JSON(&response) } -type ActivateDatasetGeneration500JSONResponse struct { - ServiceInternalErrorJSONResponse +type GetExplorer500JSONResponse struct { + LegacyInternalErrorJSONResponse } -func (response ActivateDatasetGeneration500JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { +func (response GetExplorer500JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type ActivateDatasetGeneration503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response ActivateDatasetGeneration503JSONResponse) VisitActivateDatasetGenerationResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type GetRepositoryExplorerConfigRequestObject struct { - Project Project `json:"project"` +type GetExplorerBuilderRequestObject struct { + Project Project `json:"project"` + ExplorerId ExplorerId `json:"explorerId"` } -type GetRepositoryExplorerConfigResponseObject interface { - VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error +type GetExplorerBuilderResponseObject interface { + VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error } -type GetRepositoryExplorerConfig200JSONResponse RawJSON +type GetExplorerBuilder200JSONResponse BuilderState -func (response GetRepositoryExplorerConfig200JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { +func (response GetExplorerBuilder200JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type GetRepositoryExplorerConfig401JSONResponse struct { +type GetExplorerBuilder401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response GetRepositoryExplorerConfig401JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { +func (response GetExplorerBuilder401JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type GetRepositoryExplorerConfig403JSONResponse struct{ LegacyForbiddenJSONResponse } +type GetExplorerBuilder403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response GetRepositoryExplorerConfig403JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { +func (response GetExplorerBuilder403JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type GetRepositoryExplorerConfig404JSONResponse struct{ LegacyNotFoundJSONResponse } +type GetExplorerBuilder404JSONResponse struct{ AuthoringNotFoundJSONResponse } -func (response GetRepositoryExplorerConfig404JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { +func (response GetExplorerBuilder404JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(404) return ctx.JSON(&response) } -type GetRepositoryExplorerConfig500JSONResponse struct { - LegacyInternalErrorJSONResponse -} +type GetExplorerBuilder409JSONResponse struct{ AuthoringConflictJSONResponse } -func (response GetRepositoryExplorerConfig500JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { +func (response GetExplorerBuilder409JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) + ctx.Status(409) return ctx.JSON(&response) } -type ListExplorersRequestObject struct { - Project Project `json:"project"` +type GetExplorerBuilder500JSONResponse struct { + AuthoringInternalErrorJSONResponse } -type ListExplorersResponseObject interface { - VisitListExplorersResponse(ctx fiber.Ctx) error -} +func (response GetExplorerBuilder500JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(500) -type ListExplorers200JSONResponse []map[string]interface{} + return ctx.JSON(&response) +} -func (response ListExplorers200JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { +type GetExplorerBuilder503JSONResponse struct { + AuthoringUnavailableJSONResponse +} + +func (response GetExplorerBuilder503JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(503) + + return ctx.JSON(&response) +} + +type GetExplorerAuthoringCapabilityRequestObject struct { + Project Project `json:"project"` + ExplorerId ExplorerId `json:"explorerId"` +} + +type GetExplorerAuthoringCapabilityResponseObject interface { + VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error +} + +type GetExplorerAuthoringCapability200JSONResponse AuthoringCapability + +func (response GetExplorerAuthoringCapability200JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type ListExplorers401JSONResponse struct { +type GetExplorerAuthoringCapability401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response ListExplorers401JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { +func (response GetExplorerAuthoringCapability401JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type ListExplorers403JSONResponse struct{ LegacyForbiddenJSONResponse } +type GetExplorerAuthoringCapability403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response ListExplorers403JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { +func (response GetExplorerAuthoringCapability403JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type ListExplorers500JSONResponse struct { - LegacyInternalErrorJSONResponse +type GetExplorerAuthoringCapability500JSONResponse struct { + AuthoringInternalErrorJSONResponse } -func (response ListExplorers500JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { +func (response GetExplorerAuthoringCapability500JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type CreateExplorerRequestObject struct { - Project Project `json:"project"` - Body *CreateExplorerJSONRequestBody +type ApplyExplorerBuilderCommandsRequestObject struct { + Project Project `json:"project"` + ExplorerId ExplorerId `json:"explorerId"` + Params ApplyExplorerBuilderCommandsParams + Body *ApplyExplorerBuilderCommandsJSONRequestBody } -type CreateExplorerResponseObject interface { - VisitCreateExplorerResponse(ctx fiber.Ctx) error +type ApplyExplorerBuilderCommandsResponseObject interface { + VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error } -type CreateExplorer201JSONResponse RawJSON +type ApplyExplorerBuilderCommands200JSONResponse ApplyCommandsResponse -func (response CreateExplorer201JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands200JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(201) + ctx.Status(200) return ctx.JSON(&response) } -type CreateExplorer400JSONResponse struct{ LegacyBadRequestJSONResponse } +type ApplyExplorerBuilderCommands400JSONResponse struct { + AuthoringBadRequestJSONResponse +} -func (response CreateExplorer400JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands400JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(400) return ctx.JSON(&response) } -type CreateExplorer401JSONResponse struct { +type ApplyExplorerBuilderCommands401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response CreateExplorer401JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands401JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type CreateExplorer403JSONResponse struct{ LegacyForbiddenJSONResponse } +type ApplyExplorerBuilderCommands403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response CreateExplorer403JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands403JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type CreateExplorer409JSONResponse struct{ LegacyConflictJSONResponse } +type ApplyExplorerBuilderCommands409JSONResponse struct{ AuthoringConflictJSONResponse } -func (response CreateExplorer409JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands409JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(409) return ctx.JSON(&response) } -type CreateExplorer422JSONResponse struct { - LegacyUnprocessableJSONResponse +type ApplyExplorerBuilderCommands422JSONResponse struct { + AuthoringUnprocessableJSONResponse } -func (response CreateExplorer422JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands422JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(422) return ctx.JSON(&response) } -type CreateExplorer500JSONResponse struct { - LegacyInternalErrorJSONResponse +type ApplyExplorerBuilderCommands500JSONResponse struct { + AuthoringInternalErrorJSONResponse } -func (response CreateExplorer500JSONResponse) VisitCreateExplorerResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands500JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type GetExplorerRequestObject struct { +type ApplyExplorerBuilderCommands503JSONResponse struct { + AuthoringUnavailableJSONResponse +} + +func (response ApplyExplorerBuilderCommands503JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(503) + + return ctx.JSON(&response) +} + +type PreviewExplorerRequestObject struct { Project Project `json:"project"` ExplorerId ExplorerId `json:"explorerId"` + Body *PreviewExplorerJSONRequestBody } -type GetExplorerResponseObject interface { - VisitGetExplorerResponse(ctx fiber.Ctx) error +type PreviewExplorerResponseObject interface { + VisitPreviewExplorerResponse(ctx fiber.Ctx) error } -type GetExplorer200JSONResponse RawJSON +type PreviewExplorer200JSONResponse PreviewResponse -func (response GetExplorer200JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer200JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type GetExplorer401JSONResponse struct { +type PreviewExplorer400JSONResponse struct { + AuthoringBadRequestJSONResponse +} + +func (response PreviewExplorer400JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(400) + + return ctx.JSON(&response) +} + +type PreviewExplorer401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response GetExplorer401JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer401JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type GetExplorer403JSONResponse struct{ LegacyForbiddenJSONResponse } +type PreviewExplorer403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response GetExplorer403JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer403JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type GetExplorer404JSONResponse struct{ LegacyNotFoundJSONResponse } +type PreviewExplorer404JSONResponse struct{ AuthoringNotFoundJSONResponse } -func (response GetExplorer404JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer404JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(404) return ctx.JSON(&response) } -type GetExplorer500JSONResponse struct { - LegacyInternalErrorJSONResponse -} +type PreviewExplorer409JSONResponse struct{ AuthoringConflictJSONResponse } -func (response GetExplorer500JSONResponse) VisitGetExplorerResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer409JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) + ctx.Status(409) return ctx.JSON(&response) } -type GetExplorerBuilderRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` -} - -type GetExplorerBuilderResponseObject interface { - VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error +type PreviewExplorer413JSONResponse struct { + AuthoringPayloadTooLargeJSONResponse } -type GetExplorerBuilder200JSONResponse BuilderState - -func (response GetExplorerBuilder200JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer413JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) + ctx.Status(413) return ctx.JSON(&response) } -type GetExplorerBuilder401JSONResponse struct { - ServiceUnauthorizedJSONResponse +type PreviewExplorer422JSONResponse struct { + AuthoringUnprocessableJSONResponse } -func (response GetExplorerBuilder401JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer422JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) + ctx.Status(422) return ctx.JSON(&response) } -type GetExplorerBuilder403JSONResponse struct{ AuthoringForbiddenJSONResponse } +type PreviewExplorer429JSONResponse struct { + AuthoringTooManyRequestsJSONResponse +} -func (response GetExplorerBuilder403JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer429JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) + ctx.Status(429) return ctx.JSON(&response) } -type GetExplorerBuilder404JSONResponse struct{ AuthoringNotFoundJSONResponse } +type PreviewExplorer499JSONResponse struct { + AuthoringClientClosedRequestJSONResponse +} -func (response GetExplorerBuilder404JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer499JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) + ctx.Status(499) return ctx.JSON(&response) } -type GetExplorerBuilder409JSONResponse struct{ AuthoringConflictJSONResponse } +type PreviewExplorer500JSONResponse struct { + AuthoringInternalErrorJSONResponse +} -func (response GetExplorerBuilder409JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer500JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) + ctx.Status(500) return ctx.JSON(&response) } -type GetExplorerBuilder500JSONResponse struct { - AuthoringInternalErrorJSONResponse +type PreviewExplorer503JSONResponse struct { + AuthoringUnavailableJSONResponse } -func (response GetExplorerBuilder500JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer503JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) + ctx.Status(503) return ctx.JSON(&response) } -type GetExplorerBuilder503JSONResponse struct { - AuthoringUnavailableJSONResponse +type PreviewExplorer504JSONResponse struct { + AuthoringGatewayTimeoutJSONResponse } -func (response GetExplorerBuilder503JSONResponse) VisitGetExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PreviewExplorer504JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) + ctx.Status(504) return ctx.JSON(&response) } -type CompileExplorerBuilderRequestObject struct { +type PublishExplorerRequestObject struct { Project Project `json:"project"` ExplorerId ExplorerId `json:"explorerId"` - Body *CompileExplorerBuilderJSONRequestBody + Params PublishExplorerParams + Body *PublishExplorerJSONRequestBody } -type CompileExplorerBuilderResponseObject interface { - VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error +type PublishExplorerResponseObject interface { + VisitPublishExplorerResponse(ctx fiber.Ctx) error } -type CompileExplorerBuilder200JSONResponse CompileResponse +type PublishExplorer200JSONResponse PublishResponse -func (response CompileExplorerBuilder200JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer200JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type CompileExplorerBuilder400JSONResponse struct { +type PublishExplorer400JSONResponse struct { AuthoringBadRequestJSONResponse } -func (response CompileExplorerBuilder400JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer400JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(400) return ctx.JSON(&response) } -type CompileExplorerBuilder401JSONResponse struct { +type PublishExplorer401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response CompileExplorerBuilder401JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer401JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type CompileExplorerBuilder403JSONResponse struct{ AuthoringForbiddenJSONResponse } +type PublishExplorer403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response CompileExplorerBuilder403JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer403JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type CompileExplorerBuilder409JSONResponse struct{ AuthoringConflictJSONResponse } +type PublishExplorer404JSONResponse struct{ AuthoringNotFoundJSONResponse } + +func (response PublishExplorer404JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(404) + + return ctx.JSON(&response) +} + +type PublishExplorer409JSONResponse struct{ AuthoringConflictJSONResponse } -func (response CompileExplorerBuilder409JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer409JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(409) return ctx.JSON(&response) } -type CompileExplorerBuilder422JSONResponse struct { +type PublishExplorer422JSONResponse struct { AuthoringUnprocessableJSONResponse } -func (response CompileExplorerBuilder422JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer422JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(422) return ctx.JSON(&response) } -type CompileExplorerBuilder500JSONResponse struct { +type PublishExplorer500JSONResponse struct { AuthoringInternalErrorJSONResponse } -func (response CompileExplorerBuilder500JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer500JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type CompileExplorerBuilder503JSONResponse struct { +type PublishExplorer503JSONResponse struct { AuthoringUnavailableJSONResponse } -func (response CompileExplorerBuilder503JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { +func (response PublishExplorer503JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(503) return ctx.JSON(&response) } -type GetExplorerCandidateSuggestionsRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - SnapshotToken string `json:"snapshotToken"` - CandidateId string `json:"candidateId"` +type ReconcileExplorerBuilderRequestObject struct { + Project Project `json:"project"` + ExplorerId ExplorerId `json:"explorerId"` + Params ReconcileExplorerBuilderParams + Body *ReconcileExplorerBuilderJSONRequestBody } -type GetExplorerCandidateSuggestionsResponseObject interface { - VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error +type ReconcileExplorerBuilderResponseObject interface { + VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error } -type GetExplorerCandidateSuggestions200JSONResponse CandidateSuggestions +type ReconcileExplorerBuilder200JSONResponse CompileResponse -func (response GetExplorerCandidateSuggestions200JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder200JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type GetExplorerCandidateSuggestions400JSONResponse struct { +type ReconcileExplorerBuilder400JSONResponse struct { AuthoringBadRequestJSONResponse } -func (response GetExplorerCandidateSuggestions400JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder400JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(400) return ctx.JSON(&response) } -type GetExplorerCandidateSuggestions401JSONResponse struct { +type ReconcileExplorerBuilder401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response GetExplorerCandidateSuggestions401JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder401JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type GetExplorerCandidateSuggestions403JSONResponse struct{ AuthoringForbiddenJSONResponse } +type ReconcileExplorerBuilder403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response GetExplorerCandidateSuggestions403JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder403JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type GetExplorerCandidateSuggestions404JSONResponse struct{ AuthoringNotFoundJSONResponse } +type ReconcileExplorerBuilder404JSONResponse struct{ AuthoringNotFoundJSONResponse } -func (response GetExplorerCandidateSuggestions404JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder404JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(404) return ctx.JSON(&response) } -type GetExplorerCandidateSuggestions409JSONResponse struct{ AuthoringConflictJSONResponse } +type ReconcileExplorerBuilder409JSONResponse struct{ AuthoringConflictJSONResponse } -func (response GetExplorerCandidateSuggestions409JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder409JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(409) return ctx.JSON(&response) } -type GetExplorerCandidateSuggestions500JSONResponse struct { - AuthoringInternalErrorJSONResponse +type ReconcileExplorerBuilder422JSONResponse struct { + AuthoringUnprocessableJSONResponse } -func (response GetExplorerCandidateSuggestions500JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder422JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type GetExplorerCandidateSuggestions503JSONResponse struct { - AuthoringUnavailableJSONResponse -} - -func (response GetExplorerCandidateSuggestions503JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type GetExplorerAuthoringCapabilityRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` -} - -type GetExplorerAuthoringCapabilityResponseObject interface { - VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error -} - -type GetExplorerAuthoringCapability200JSONResponse AuthoringCapability - -func (response GetExplorerAuthoringCapability200JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) + ctx.Status(422) return ctx.JSON(&response) } -type GetExplorerAuthoringCapability401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response GetExplorerAuthoringCapability401JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) +type ReconcileExplorerBuilder500JSONResponse struct { + AuthoringInternalErrorJSONResponse } -type GetExplorerAuthoringCapability403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response GetExplorerAuthoringCapability403JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder500JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) + ctx.Status(500) return ctx.JSON(&response) } -type GetExplorerAuthoringCapability500JSONResponse struct { - AuthoringInternalErrorJSONResponse +type ReconcileExplorerBuilder503JSONResponse struct { + AuthoringUnavailableJSONResponse } -func (response GetExplorerAuthoringCapability500JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { +func (response ReconcileExplorerBuilder503JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) + ctx.Status(503) return ctx.JSON(&response) } -type ApplyExplorerBuilderCommandsRequestObject struct { +type SearchExplorerCandidatesRequestObject struct { Project Project `json:"project"` ExplorerId ExplorerId `json:"explorerId"` - Body *ApplyExplorerBuilderCommandsJSONRequestBody + Body *SearchExplorerCandidatesJSONRequestBody } -type ApplyExplorerBuilderCommandsResponseObject interface { - VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error +type SearchExplorerCandidatesResponseObject interface { + VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error } -type ApplyExplorerBuilderCommands200JSONResponse ApplyCommandsResponse +type SearchExplorerCandidates200JSONResponse CandidateSearchResponse -func (response ApplyExplorerBuilderCommands200JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { +func (response SearchExplorerCandidates200JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type ApplyExplorerBuilderCommands400JSONResponse struct { +type SearchExplorerCandidates400JSONResponse struct { AuthoringBadRequestJSONResponse } -func (response ApplyExplorerBuilderCommands400JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { +func (response SearchExplorerCandidates400JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(400) return ctx.JSON(&response) } -type ApplyExplorerBuilderCommands401JSONResponse struct { +type SearchExplorerCandidates401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response ApplyExplorerBuilderCommands401JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { +func (response SearchExplorerCandidates401JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type ApplyExplorerBuilderCommands403JSONResponse struct{ AuthoringForbiddenJSONResponse } +type SearchExplorerCandidates403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response ApplyExplorerBuilderCommands403JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { +func (response SearchExplorerCandidates403JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type ApplyExplorerBuilderCommands409JSONResponse struct{ AuthoringConflictJSONResponse } +type SearchExplorerCandidates409JSONResponse struct{ AuthoringConflictJSONResponse } -func (response ApplyExplorerBuilderCommands409JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { +func (response SearchExplorerCandidates409JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(409) return ctx.JSON(&response) } -type ApplyExplorerBuilderCommands422JSONResponse struct { - AuthoringUnprocessableJSONResponse -} - -func (response ApplyExplorerBuilderCommands422JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type ApplyExplorerBuilderCommands500JSONResponse struct { +type SearchExplorerCandidates500JSONResponse struct { AuthoringInternalErrorJSONResponse } -func (response ApplyExplorerBuilderCommands500JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { +func (response SearchExplorerCandidates500JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type ApplyExplorerBuilderCommands503JSONResponse struct { +type SearchExplorerCandidates503JSONResponse struct { AuthoringUnavailableJSONResponse } -func (response ApplyExplorerBuilderCommands503JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { +func (response SearchExplorerCandidates503JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(503) return ctx.JSON(&response) } -type CompileExplorerRequestObject struct { +type PublishRepositoryExplorerConfigRequestObject struct { Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *CompileExplorerJSONRequestBody + Generation Generation `json:"generation"` + Params PublishRepositoryExplorerConfigParams + Body *PublishRepositoryExplorerConfigJSONRequestBody } -type CompileExplorerResponseObject interface { - VisitCompileExplorerResponse(ctx fiber.Ctx) error +type PublishRepositoryExplorerConfigResponseObject interface { + VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error } -type CompileExplorer200JSONResponse CompileResponse +type PublishRepositoryExplorerConfig200JSONResponse RawJSON -func (response CompileExplorer200JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig200JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type CompileExplorer400JSONResponse struct { - AuthoringBadRequestJSONResponse -} +type PublishRepositoryExplorerConfig400JSONResponse struct{ LegacyBadRequestJSONResponse } -func (response CompileExplorer400JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig400JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(400) return ctx.JSON(&response) } -type CompileExplorer401JSONResponse struct { +type PublishRepositoryExplorerConfig401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response CompileExplorer401JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig401JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type CompileExplorer403JSONResponse struct{ AuthoringForbiddenJSONResponse } +type PublishRepositoryExplorerConfig403JSONResponse struct{ LegacyForbiddenJSONResponse } -func (response CompileExplorer403JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig403JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) return ctx.JSON(&response) } -type CompileExplorer409JSONResponse struct{ AuthoringConflictJSONResponse } +type PublishRepositoryExplorerConfig409JSONResponse struct{ LegacyConflictJSONResponse } -func (response CompileExplorer409JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig409JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(409) return ctx.JSON(&response) } -type CompileExplorer422JSONResponse struct { - AuthoringUnprocessableJSONResponse +type PublishRepositoryExplorerConfig422JSONResponse struct { + LegacyUnprocessableJSONResponse } -func (response CompileExplorer422JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig422JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(422) return ctx.JSON(&response) } -type CompileExplorer500JSONResponse struct { - AuthoringInternalErrorJSONResponse +type PublishRepositoryExplorerConfig500JSONResponse struct { + LegacyInternalErrorJSONResponse } -func (response CompileExplorer500JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig500JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type CompileExplorer503JSONResponse struct { - AuthoringUnavailableJSONResponse -} +type PublishRepositoryExplorerConfig503JSONResponse struct{ LegacyUnavailableJSONResponse } -func (response CompileExplorer503JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig503JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(503) return ctx.JSON(&response) } -type SaveExplorerDraftRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *SaveExplorerDraftJSONRequestBody +type GetApolloSandboxRequestObject struct { } -type SaveExplorerDraftResponseObject interface { - VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error +type GetApolloSandboxResponseObject interface { + VisitGetApolloSandboxResponse(ctx fiber.Ctx) error } -type SaveExplorerDraft200JSONResponse SaveDraftResponse +type GetApolloSandbox200TexthtmlResponse struct{ HTMLTexthtmlResponse } -func (response SaveExplorerDraft200JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") +func (response GetApolloSandbox200TexthtmlResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "text/html") + if response.ContentLength != 0 { + ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) + } ctx.Status(200) - return ctx.JSON(&response) + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) + return err } -type SaveExplorerDraft400JSONResponse struct { - AuthoringBadRequestJSONResponse +type GetApolloSandbox401JSONResponse struct { + ServiceUnauthorizedJSONResponse } -func (response SaveExplorerDraft400JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response GetApolloSandbox401JSONResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) + ctx.Status(401) return ctx.JSON(&response) } -type SaveExplorerDraft401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} +type GetApolloSandbox404JSONResponse struct{ ServiceNotFoundJSONResponse } -func (response SaveExplorerDraft401JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response GetApolloSandbox404JSONResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) + ctx.Status(404) return ctx.JSON(&response) } -type SaveExplorerDraft403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response SaveExplorerDraft403JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) +type ExecuteDataframeGraphQLRequestObject struct { + Body *ExecuteDataframeGraphQLJSONRequestBody +} - return ctx.JSON(&response) +type ExecuteDataframeGraphQLResponseObject interface { + VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error } -type SaveExplorerDraft404JSONResponse struct{ AuthoringNotFoundJSONResponse } +type ExecuteDataframeGraphQL200JSONResponse RawJSON -func (response SaveExplorerDraft404JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response ExecuteDataframeGraphQL200JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) + ctx.Status(200) return ctx.JSON(&response) } -type SaveExplorerDraft409JSONResponse struct{ AuthoringConflictJSONResponse } +type ExecuteDataframeGraphQL400JSONResponse struct{ GraphQLBadRequestJSONResponse } -func (response SaveExplorerDraft409JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response ExecuteDataframeGraphQL400JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) + ctx.Status(400) return ctx.JSON(&response) } -type SaveExplorerDraft422JSONResponse struct { - AuthoringUnprocessableJSONResponse +type ExecuteDataframeGraphQL401JSONResponse struct { + ServiceUnauthorizedJSONResponse } -func (response SaveExplorerDraft422JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response ExecuteDataframeGraphQL401JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) + ctx.Status(401) return ctx.JSON(&response) } -type SaveExplorerDraft500JSONResponse struct { - AuthoringInternalErrorJSONResponse +type ExecuteDataframeGraphQL422JSONResponse struct { + GraphQLUnprocessableJSONResponse } -func (response SaveExplorerDraft500JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response ExecuteDataframeGraphQL422JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) + ctx.Status(422) return ctx.JSON(&response) } -type SaveExplorerDraft503JSONResponse struct { - AuthoringUnavailableJSONResponse +type ExecuteDataframeGraphQL500JSONResponse struct { + GraphQLInternalErrorJSONResponse } -func (response SaveExplorerDraft503JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response ExecuteDataframeGraphQL500JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) + ctx.Status(500) return ctx.JSON(&response) } -type ExportExplorerWorkspaceRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` +type GetGraphQLPlaygroundRequestObject struct { } -type ExportExplorerWorkspaceResponseObject interface { - VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error +type GetGraphQLPlaygroundResponseObject interface { + VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error } -type ExportExplorerWorkspace200JSONResponse Workspace +type GetGraphQLPlayground200TexthtmlResponse struct{ HTMLTexthtmlResponse } -func (response ExportExplorerWorkspace200JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") +func (response GetGraphQLPlayground200TexthtmlResponse) VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "text/html") + if response.ContentLength != 0 { + ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) + } ctx.Status(200) - return ctx.JSON(&response) + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) + return err } -type ExportExplorerWorkspace401JSONResponse struct { +type GetGraphQLPlayground401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response ExportExplorerWorkspace401JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response GetGraphQLPlayground401JSONResponse) VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type ExportExplorerWorkspace403JSONResponse struct{ AuthoringForbiddenJSONResponse } +type GetGraphQLPlayground404JSONResponse struct{ ServiceNotFoundJSONResponse } -func (response ExportExplorerWorkspace403JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response GetGraphQLPlayground404JSONResponse) VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) + ctx.Status(404) return ctx.JSON(&response) } -type ExportExplorerWorkspace404JSONResponse struct{ AuthoringNotFoundJSONResponse } +type ExecuteGraphQLRequestObject struct { + Body *ExecuteGraphQLJSONRequestBody +} + +type ExecuteGraphQLResponseObject interface { + VisitExecuteGraphQLResponse(ctx fiber.Ctx) error +} + +type ExecuteGraphQL200JSONResponse RawJSON -func (response ExportExplorerWorkspace404JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response ExecuteGraphQL200JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) + ctx.Status(200) return ctx.JSON(&response) } -type ExportExplorerWorkspace409JSONResponse struct{ AuthoringConflictJSONResponse } +type ExecuteGraphQL400JSONResponse struct{ GraphQLBadRequestJSONResponse } -func (response ExportExplorerWorkspace409JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response ExecuteGraphQL400JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) + ctx.Status(400) return ctx.JSON(&response) } -type ExportExplorerWorkspace500JSONResponse struct { - AuthoringInternalErrorJSONResponse +type ExecuteGraphQL401JSONResponse struct { + ServiceUnauthorizedJSONResponse } -func (response ExportExplorerWorkspace500JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response ExecuteGraphQL401JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) + ctx.Status(401) return ctx.JSON(&response) } -type PreviewExplorerRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *PreviewExplorerJSONRequestBody +type ExecuteGraphQL422JSONResponse struct { + GraphQLUnprocessableJSONResponse } -type PreviewExplorerResponseObject interface { - VisitPreviewExplorerResponse(ctx fiber.Ctx) error +func (response ExecuteGraphQL422JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(422) + + return ctx.JSON(&response) } -type PreviewExplorer200JSONResponse PreviewResponse - -func (response PreviewExplorer200JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type PreviewExplorer400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response PreviewExplorer400JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type PreviewExplorer401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response PreviewExplorer401JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type PreviewExplorer403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response PreviewExplorer403JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type PreviewExplorer404JSONResponse struct{ AuthoringNotFoundJSONResponse } - -func (response PreviewExplorer404JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type PreviewExplorer409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response PreviewExplorer409JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type PreviewExplorer413JSONResponse struct { - AuthoringPayloadTooLargeJSONResponse -} - -func (response PreviewExplorer413JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(413) - - return ctx.JSON(&response) -} - -type PreviewExplorer422JSONResponse struct { - AuthoringUnprocessableJSONResponse -} - -func (response PreviewExplorer422JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type PreviewExplorer429JSONResponse struct { - AuthoringTooManyRequestsJSONResponse -} - -func (response PreviewExplorer429JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(429) - - return ctx.JSON(&response) -} - -type PreviewExplorer499JSONResponse struct { - AuthoringClientClosedRequestJSONResponse -} - -func (response PreviewExplorer499JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(499) - - return ctx.JSON(&response) -} - -type PreviewExplorer500JSONResponse struct { - AuthoringInternalErrorJSONResponse +type ExecuteGraphQL500JSONResponse struct { + GraphQLInternalErrorJSONResponse } -func (response PreviewExplorer500JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { +func (response ExecuteGraphQL500JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(500) return ctx.JSON(&response) } -type PreviewExplorer503JSONResponse struct { - AuthoringUnavailableJSONResponse -} - -func (response PreviewExplorer503JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type PreviewExplorer504JSONResponse struct { - AuthoringGatewayTimeoutJSONResponse -} - -func (response PreviewExplorer504JSONResponse) VisitPreviewExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(504) - - return ctx.JSON(&response) -} - -type PublishExplorerRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *PublishExplorerJSONRequestBody +type GetHealthRequestObject struct { } -type PublishExplorerResponseObject interface { - VisitPublishExplorerResponse(ctx fiber.Ctx) error +type GetHealthResponseObject interface { + VisitGetHealthResponse(ctx fiber.Ctx) error } -type PublishExplorer200JSONResponse PublishResponse +type GetHealth200JSONResponse RawJSON -func (response PublishExplorer200JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { +func (response GetHealth200JSONResponse) VisitGetHealthResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type PublishExplorer400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response PublishExplorer400JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type PublishExplorer401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} +type GetHealth503JSONResponse RawJSON -func (response PublishExplorer401JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { +func (response GetHealth503JSONResponse) VisitGetHealthResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) + ctx.Status(503) return ctx.JSON(&response) } -type PublishExplorer403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response PublishExplorer403JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) +type GetLivenessRequestObject struct { } -type PublishExplorer404JSONResponse struct{ AuthoringNotFoundJSONResponse } - -func (response PublishExplorer404JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) +type GetLivenessResponseObject interface { + VisitGetLivenessResponse(ctx fiber.Ctx) error } -type PublishExplorer409JSONResponse struct{ AuthoringConflictJSONResponse } +type GetLiveness200JSONResponse RawJSON -func (response PublishExplorer409JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { +func (response GetLiveness200JSONResponse) VisitGetLivenessResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) + ctx.Status(200) return ctx.JSON(&response) } -type PublishExplorer422JSONResponse struct { - AuthoringUnprocessableJSONResponse +type GetReadinessRequestObject struct { } -func (response PublishExplorer422JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) +type GetReadinessResponseObject interface { + VisitGetReadinessResponse(ctx fiber.Ctx) error } -type PublishExplorer500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} +type GetReadiness200JSONResponse RawJSON -func (response PublishExplorer500JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { +func (response GetReadiness200JSONResponse) VisitGetReadinessResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) + ctx.Status(200) return ctx.JSON(&response) } -type PublishExplorer503JSONResponse struct { - AuthoringUnavailableJSONResponse -} +type GetReadiness503JSONResponse RawJSON -func (response PublishExplorer503JSONResponse) VisitPublishExplorerResponse(ctx fiber.Ctx) error { +func (response GetReadiness503JSONResponse) VisitGetReadinessResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(503) return ctx.JSON(&response) } -type ReconcileExplorerBuilderRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *ReconcileExplorerBuilderJSONRequestBody -} +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // GetRecipeExecution Read public metadata for a dataframe recipe execution. + // (GET /api/v1/dataframe/recipe-executions/{id}) + GetRecipeExecution(ctx context.Context, request GetRecipeExecutionRequestObject) (GetRecipeExecutionResponseObject, error) + // GetDatasetGenerationStatus Read a dataset generation's persisted load state. + // (GET /api/v1/datasets/{project}/generations/{generation}) + GetDatasetGenerationStatus(ctx context.Context, request GetDatasetGenerationStatusRequestObject) (GetDatasetGenerationStatusResponseObject, error) + // CreateDatasetGeneration Upload a multipart dataset generation. + // (POST /api/v1/datasets/{project}/generations/{generation}) + CreateDatasetGeneration(ctx context.Context, request CreateDatasetGenerationRequestObject) (CreateDatasetGenerationResponseObject, error) + // ActivateDatasetGeneration Activate a generation after dataframe publication. + // (POST /api/v1/datasets/{project}/generations/{generation}/activate) + ActivateDatasetGeneration(ctx context.Context, request ActivateDatasetGenerationRequestObject) (ActivateDatasetGenerationResponseObject, error) + // ListExplorers List Explorer summaries for a project. + // (GET /api/v1/projects/{project}/explorers) + ListExplorers(ctx context.Context, request ListExplorersRequestObject) (ListExplorersResponseObject, error) + // CreateExplorer Create an interactive Explorer. + // (POST /api/v1/projects/{project}/explorers) + CreateExplorer(ctx context.Context, request CreateExplorerRequestObject) (CreateExplorerResponseObject, error) + // GetExplorer Read an Explorer's active runtime projection. + // (GET /api/v1/projects/{project}/explorers/{explorerId}) + GetExplorer(ctx context.Context, request GetExplorerRequestObject) (GetExplorerResponseObject, error) -type ReconcileExplorerBuilderResponseObject interface { - VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error -} + // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/builder) + GetExplorerBuilder(ctx context.Context, request GetExplorerBuilderRequestObject) (GetExplorerBuilderResponseObject, error) -type ReconcileExplorerBuilder200JSONResponse CompileResponse + // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/capability) + GetExplorerAuthoringCapability(ctx context.Context, request GetExplorerAuthoringCapabilityRequestObject) (GetExplorerAuthoringCapabilityResponseObject, error) -func (response ReconcileExplorerBuilder200JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) + // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/commands) + ApplyExplorerBuilderCommands(ctx context.Context, request ApplyExplorerBuilderCommandsRequestObject) (ApplyExplorerBuilderCommandsResponseObject, error) - return ctx.JSON(&response) -} + // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/preview) + PreviewExplorer(ctx context.Context, request PreviewExplorerRequestObject) (PreviewExplorerResponseObject, error) -type ReconcileExplorerBuilder400JSONResponse struct { - AuthoringBadRequestJSONResponse -} + // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/publish) + PublishExplorer(ctx context.Context, request PublishExplorerRequestObject) (PublishExplorerResponseObject, error) -func (response ReconcileExplorerBuilder400JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type ReconcileExplorerBuilder401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response ReconcileExplorerBuilder401JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type ReconcileExplorerBuilder403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response ReconcileExplorerBuilder403JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type ReconcileExplorerBuilder404JSONResponse struct{ AuthoringNotFoundJSONResponse } - -func (response ReconcileExplorerBuilder404JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type ReconcileExplorerBuilder409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response ReconcileExplorerBuilder409JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type ReconcileExplorerBuilder422JSONResponse struct { - AuthoringUnprocessableJSONResponse -} - -func (response ReconcileExplorerBuilder422JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type ReconcileExplorerBuilder500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} - -func (response ReconcileExplorerBuilder500JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type ReconcileExplorerBuilder503JSONResponse struct { - AuthoringUnavailableJSONResponse -} - -func (response ReconcileExplorerBuilder503JSONResponse) VisitReconcileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type SearchExplorerCandidatesRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *SearchExplorerCandidatesJSONRequestBody -} - -type SearchExplorerCandidatesResponseObject interface { - VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error -} - -type SearchExplorerCandidates200JSONResponse CandidateSearchResponse - -func (response SearchExplorerCandidates200JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type SearchExplorerCandidates400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response SearchExplorerCandidates400JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type SearchExplorerCandidates401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response SearchExplorerCandidates401JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type SearchExplorerCandidates403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response SearchExplorerCandidates403JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type SearchExplorerCandidates409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response SearchExplorerCandidates409JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type SearchExplorerCandidates500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} - -func (response SearchExplorerCandidates500JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type SearchExplorerCandidates503JSONResponse struct { - AuthoringUnavailableJSONResponse -} - -func (response SearchExplorerCandidates503JSONResponse) VisitSearchExplorerCandidatesResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type AbortSnapshotRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` -} - -type AbortSnapshotResponseObject interface { - VisitAbortSnapshotResponse(ctx fiber.Ctx) error -} - -type AbortSnapshot200JSONResponse RawJSON - -func (response AbortSnapshot200JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type AbortSnapshot401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response AbortSnapshot401JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type AbortSnapshot403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response AbortSnapshot403JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type AbortSnapshot404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response AbortSnapshot404JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type AbortSnapshot409JSONResponse struct{ ServiceConflictJSONResponse } - -func (response AbortSnapshot409JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type AbortSnapshot500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response AbortSnapshot500JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type AbortSnapshot503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response AbortSnapshot503JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type GetSnapshotRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` -} - -type GetSnapshotResponseObject interface { - VisitGetSnapshotResponse(ctx fiber.Ctx) error -} - -type GetSnapshot200JSONResponse RawJSON - -func (response GetSnapshot200JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type GetSnapshot401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response GetSnapshot401JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type GetSnapshot403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response GetSnapshot403JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type GetSnapshot404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response GetSnapshot404JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type GetSnapshot500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response GetSnapshot500JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type GetSnapshot503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response GetSnapshot503JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type CreateSnapshotRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` - Body *CreateSnapshotJSONRequestBody -} - -type CreateSnapshotResponseObject interface { - VisitCreateSnapshotResponse(ctx fiber.Ctx) error -} - -type CreateSnapshot200JSONResponse RawJSON - -func (response CreateSnapshot200JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type CreateSnapshot400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response CreateSnapshot400JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type CreateSnapshot401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response CreateSnapshot401JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type CreateSnapshot403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response CreateSnapshot403JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type CreateSnapshot409JSONResponse struct{ ServiceConflictJSONResponse } - -func (response CreateSnapshot409JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type CreateSnapshot500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response CreateSnapshot500JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type CreateSnapshot503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response CreateSnapshot503JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfigRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` - Params PublishRepositoryExplorerConfigParams - Body *PublishRepositoryExplorerConfigJSONRequestBody -} - -type PublishRepositoryExplorerConfigResponseObject interface { - VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error -} - -type PublishRepositoryExplorerConfig200JSONResponse RawJSON - -func (response PublishRepositoryExplorerConfig200JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfig400JSONResponse struct{ LegacyBadRequestJSONResponse } - -func (response PublishRepositoryExplorerConfig400JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfig401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response PublishRepositoryExplorerConfig401JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfig403JSONResponse struct{ LegacyForbiddenJSONResponse } - -func (response PublishRepositoryExplorerConfig403JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfig409JSONResponse struct{ LegacyConflictJSONResponse } - -func (response PublishRepositoryExplorerConfig409JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfig422JSONResponse struct { - LegacyUnprocessableJSONResponse -} - -func (response PublishRepositoryExplorerConfig422JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfig500JSONResponse struct { - LegacyInternalErrorJSONResponse -} - -func (response PublishRepositoryExplorerConfig500JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type PublishRepositoryExplorerConfig503JSONResponse struct{ LegacyUnavailableJSONResponse } - -func (response PublishRepositoryExplorerConfig503JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type FinalizeSnapshotRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` -} - -type FinalizeSnapshotResponseObject interface { - VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error -} - -type FinalizeSnapshot200JSONResponse RawJSON - -func (response FinalizeSnapshot200JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response FinalizeSnapshot400JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response FinalizeSnapshot401JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response FinalizeSnapshot403JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response FinalizeSnapshot404JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot409JSONResponse struct{ ServiceConflictJSONResponse } - -func (response FinalizeSnapshot409JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot422JSONResponse struct { - ServiceUnprocessableJSONResponse -} - -func (response FinalizeSnapshot422JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response FinalizeSnapshot500JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type FinalizeSnapshot503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response FinalizeSnapshot503JSONResponse) VisitFinalizeSnapshotResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type UploadSnapshotResourceRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` - ResourceType ResourceType `json:"resourceType"` - Params UploadSnapshotResourceParams - Body io.Reader -} - -type UploadSnapshotResourceResponseObject interface { - VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error -} - -type UploadSnapshotResource200JSONResponse RawJSON - -func (response UploadSnapshotResource200JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response UploadSnapshotResource400JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response UploadSnapshotResource401JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response UploadSnapshotResource403JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response UploadSnapshotResource404JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource409JSONResponse struct{ ServiceConflictJSONResponse } - -func (response UploadSnapshotResource409JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource422JSONResponse struct { - ServiceUnprocessableJSONResponse -} - -func (response UploadSnapshotResource422JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response UploadSnapshotResource500JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type UploadSnapshotResource503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response UploadSnapshotResource503JSONResponse) VisitUploadSnapshotResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type CreateReleaseRequestObject struct { - Project Project `json:"project"` - Body *CreateReleaseJSONRequestBody -} - -type CreateReleaseResponseObject interface { - VisitCreateReleaseResponse(ctx fiber.Ctx) error -} - -type CreateRelease200JSONResponse RawJSON - -func (response CreateRelease200JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type CreateRelease400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response CreateRelease400JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type CreateRelease401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response CreateRelease401JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type CreateRelease403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response CreateRelease403JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type CreateRelease404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response CreateRelease404JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type CreateRelease409JSONResponse struct{ ServiceConflictJSONResponse } - -func (response CreateRelease409JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type CreateRelease422JSONResponse struct { - ServiceUnprocessableJSONResponse -} - -func (response CreateRelease422JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type CreateRelease500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response CreateRelease500JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type CreateRelease503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response CreateRelease503JSONResponse) VisitCreateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibilityRequestObject struct { - Project Project `json:"project"` - Body *ActivateReleaseCompatibilityJSONRequestBody -} - -type ActivateReleaseCompatibilityResponseObject interface { - VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error -} - -type ActivateReleaseCompatibility200JSONResponse RawJSON - -func (response ActivateReleaseCompatibility200JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response ActivateReleaseCompatibility400JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response ActivateReleaseCompatibility401JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response ActivateReleaseCompatibility403JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response ActivateReleaseCompatibility404JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility409JSONResponse struct{ ServiceConflictJSONResponse } - -func (response ActivateReleaseCompatibility409JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility422JSONResponse struct { - ServiceUnprocessableJSONResponse -} - -func (response ActivateReleaseCompatibility422JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response ActivateReleaseCompatibility500JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type ActivateReleaseCompatibility503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response ActivateReleaseCompatibility503JSONResponse) VisitActivateReleaseCompatibilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type GetActiveReleaseRequestObject struct { - Project Project `json:"project"` -} - -type GetActiveReleaseResponseObject interface { - VisitGetActiveReleaseResponse(ctx fiber.Ctx) error -} - -type GetActiveRelease200JSONResponse RawJSON - -func (response GetActiveRelease200JSONResponse) VisitGetActiveReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type GetActiveRelease401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response GetActiveRelease401JSONResponse) VisitGetActiveReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type GetActiveRelease403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response GetActiveRelease403JSONResponse) VisitGetActiveReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type GetActiveRelease404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response GetActiveRelease404JSONResponse) VisitGetActiveReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type GetActiveRelease500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response GetActiveRelease500JSONResponse) VisitGetActiveReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type GetActiveRelease503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response GetActiveRelease503JSONResponse) VisitGetActiveReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type GetReleaseRequestObject struct { - Project Project `json:"project"` - Release Release `json:"release"` -} - -type GetReleaseResponseObject interface { - VisitGetReleaseResponse(ctx fiber.Ctx) error -} - -type GetRelease200JSONResponse RawJSON - -func (response GetRelease200JSONResponse) VisitGetReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type GetRelease401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response GetRelease401JSONResponse) VisitGetReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type GetRelease403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response GetRelease403JSONResponse) VisitGetReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type GetRelease404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response GetRelease404JSONResponse) VisitGetReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type GetRelease500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response GetRelease500JSONResponse) VisitGetReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type GetRelease503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response GetRelease503JSONResponse) VisitGetReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type ActivateReleaseRequestObject struct { - Project Project `json:"project"` - Release Release `json:"release"` - Body *ActivateReleaseJSONRequestBody -} - -type ActivateReleaseResponseObject interface { - VisitActivateReleaseResponse(ctx fiber.Ctx) error -} - -type ActivateRelease200JSONResponse RawJSON - -func (response ActivateRelease200JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type ActivateRelease400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response ActivateRelease400JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type ActivateRelease401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response ActivateRelease401JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type ActivateRelease403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response ActivateRelease403JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type ActivateRelease404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response ActivateRelease404JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type ActivateRelease409JSONResponse struct{ ServiceConflictJSONResponse } - -func (response ActivateRelease409JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type ActivateRelease422JSONResponse struct { - ServiceUnprocessableJSONResponse -} - -func (response ActivateRelease422JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type ActivateRelease500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response ActivateRelease500JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type ActivateRelease503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response ActivateRelease503JSONResponse) VisitActivateReleaseResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type UploadProjectResourceRequestObject struct { - Project Project `json:"project"` - ResourceType ResourceType `json:"resourceType"` - Body *multipart.Reader -} - -type UploadProjectResourceResponseObject interface { - VisitUploadProjectResourceResponse(ctx fiber.Ctx) error -} - -type UploadProjectResource200JSONResponse RawJSON - -func (response UploadProjectResource200JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type UploadProjectResource400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response UploadProjectResource400JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type UploadProjectResource401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response UploadProjectResource401JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type UploadProjectResource403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response UploadProjectResource403JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type UploadProjectResource415JSONResponse struct { - ServiceUnsupportedMediaTypeJSONResponse -} - -func (response UploadProjectResource415JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(415) - - return ctx.JSON(&response) -} - -type UploadProjectResource422JSONResponse struct { - ServiceUnprocessableJSONResponse -} - -func (response UploadProjectResource422JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type UploadProjectResource500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response UploadProjectResource500JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type UploadProjectResource503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response UploadProjectResource503JSONResponse) VisitUploadProjectResourceResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type UploadRawNDJSONRequestObject struct { - Params UploadRawNDJSONParams - Body io.Reader -} - -type UploadRawNDJSONResponseObject interface { - VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error -} - -type UploadRawNDJSON200JSONResponse RawJSON - -func (response UploadRawNDJSON200JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type UploadRawNDJSON400JSONResponse struct{ ServiceBadRequestJSONResponse } - -func (response UploadRawNDJSON400JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type UploadRawNDJSON401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response UploadRawNDJSON401JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type UploadRawNDJSON403JSONResponse struct{ ServiceForbiddenJSONResponse } - -func (response UploadRawNDJSON403JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type UploadRawNDJSON415JSONResponse struct { - ServiceUnsupportedMediaTypeJSONResponse -} - -func (response UploadRawNDJSON415JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(415) - - return ctx.JSON(&response) -} - -type UploadRawNDJSON422JSONResponse struct { - ServiceUnprocessableJSONResponse -} - -func (response UploadRawNDJSON422JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type UploadRawNDJSON500JSONResponse struct { - ServiceInternalErrorJSONResponse -} - -func (response UploadRawNDJSON500JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type UploadRawNDJSON503JSONResponse struct{ ServiceUnavailableJSONResponse } - -func (response UploadRawNDJSON503JSONResponse) VisitUploadRawNDJSONResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type GetApolloSandboxRequestObject struct { -} - -type GetApolloSandboxResponseObject interface { - VisitGetApolloSandboxResponse(ctx fiber.Ctx) error -} - -type GetApolloSandbox200TexthtmlResponse struct{ HTMLTexthtmlResponse } - -func (response GetApolloSandbox200TexthtmlResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "text/html") - if response.ContentLength != 0 { - ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) - } - ctx.Status(200) - - if closer, ok := response.Body.(io.ReadCloser); ok { - defer closer.Close() - } - _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) - return err -} - -type GetApolloSandbox401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response GetApolloSandbox401JSONResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type GetApolloSandbox404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response GetApolloSandbox404JSONResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type ExecuteDataframeGraphQLRequestObject struct { - Body *ExecuteDataframeGraphQLJSONRequestBody -} - -type ExecuteDataframeGraphQLResponseObject interface { - VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error -} - -type ExecuteDataframeGraphQL200JSONResponse RawJSON - -func (response ExecuteDataframeGraphQL200JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type ExecuteDataframeGraphQL400JSONResponse struct{ GraphQLBadRequestJSONResponse } - -func (response ExecuteDataframeGraphQL400JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type ExecuteDataframeGraphQL401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response ExecuteDataframeGraphQL401JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type ExecuteDataframeGraphQL422JSONResponse struct { - GraphQLUnprocessableJSONResponse -} - -func (response ExecuteDataframeGraphQL422JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type ExecuteDataframeGraphQL500JSONResponse struct { - GraphQLInternalErrorJSONResponse -} - -func (response ExecuteDataframeGraphQL500JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type GetGraphQLPlaygroundRequestObject struct { -} - -type GetGraphQLPlaygroundResponseObject interface { - VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error -} - -type GetGraphQLPlayground200TexthtmlResponse struct{ HTMLTexthtmlResponse } - -func (response GetGraphQLPlayground200TexthtmlResponse) VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "text/html") - if response.ContentLength != 0 { - ctx.Response().Header.Set("Content-Length", fmt.Sprint(response.ContentLength)) - } - ctx.Status(200) - - if closer, ok := response.Body.(io.ReadCloser); ok { - defer closer.Close() - } - _, err := io.Copy(ctx.Response().BodyWriter(), response.Body) - return err -} - -type GetGraphQLPlayground401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response GetGraphQLPlayground401JSONResponse) VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type GetGraphQLPlayground404JSONResponse struct{ ServiceNotFoundJSONResponse } - -func (response GetGraphQLPlayground404JSONResponse) VisitGetGraphQLPlaygroundResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type ExecuteGraphQLRequestObject struct { - Body *ExecuteGraphQLJSONRequestBody -} - -type ExecuteGraphQLResponseObject interface { - VisitExecuteGraphQLResponse(ctx fiber.Ctx) error -} - -type ExecuteGraphQL200JSONResponse RawJSON - -func (response ExecuteGraphQL200JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type ExecuteGraphQL400JSONResponse struct{ GraphQLBadRequestJSONResponse } - -func (response ExecuteGraphQL400JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type ExecuteGraphQL401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response ExecuteGraphQL401JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type ExecuteGraphQL422JSONResponse struct { - GraphQLUnprocessableJSONResponse -} - -func (response ExecuteGraphQL422JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type ExecuteGraphQL500JSONResponse struct { - GraphQLInternalErrorJSONResponse -} - -func (response ExecuteGraphQL500JSONResponse) VisitExecuteGraphQLResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type GetHealthRequestObject struct { -} - -type GetHealthResponseObject interface { - VisitGetHealthResponse(ctx fiber.Ctx) error -} - -type GetHealth200JSONResponse RawJSON - -func (response GetHealth200JSONResponse) VisitGetHealthResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type GetHealth503JSONResponse RawJSON - -func (response GetHealth503JSONResponse) VisitGetHealthResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -type GetLivenessRequestObject struct { -} - -type GetLivenessResponseObject interface { - VisitGetLivenessResponse(ctx fiber.Ctx) error -} - -type GetLiveness200JSONResponse RawJSON - -func (response GetLiveness200JSONResponse) VisitGetLivenessResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type GetReadinessRequestObject struct { -} - -type GetReadinessResponseObject interface { - VisitGetReadinessResponse(ctx fiber.Ctx) error -} - -type GetReadiness200JSONResponse RawJSON - -func (response GetReadiness200JSONResponse) VisitGetReadinessResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type GetReadiness503JSONResponse RawJSON - -func (response GetReadiness503JSONResponse) VisitGetReadinessResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) - - return ctx.JSON(&response) -} - -// StrictServerInterface represents all server handlers. -type StrictServerInterface interface { - // GetRecipeExecution Read public metadata for a dataframe recipe execution. - // (GET /api/v1/dataframe/recipe-executions/{id}) - GetRecipeExecution(ctx context.Context, request GetRecipeExecutionRequestObject) (GetRecipeExecutionResponseObject, error) - // CreateDatasetGeneration Upload a multipart dataset generation. - // (POST /api/v1/datasets/{project}/generations/{generation}) - CreateDatasetGeneration(ctx context.Context, request CreateDatasetGenerationRequestObject) (CreateDatasetGenerationResponseObject, error) - // ActivateDatasetGeneration Activate a generation after dataframe publication. - // (POST /api/v1/datasets/{project}/generations/{generation}/activate) - ActivateDatasetGeneration(ctx context.Context, request ActivateDatasetGenerationRequestObject) (ActivateDatasetGenerationResponseObject, error) - // GetRepositoryExplorerConfig Read the active repository-owned Explorer config. - // (GET /api/v1/projects/{project}/explorer-config) - GetRepositoryExplorerConfig(ctx context.Context, request GetRepositoryExplorerConfigRequestObject) (GetRepositoryExplorerConfigResponseObject, error) - // ListExplorers List Explorer summaries for a project. - // (GET /api/v1/projects/{project}/explorers) - ListExplorers(ctx context.Context, request ListExplorersRequestObject) (ListExplorersResponseObject, error) - // CreateExplorer Create an interactive Explorer. - // (POST /api/v1/projects/{project}/explorers) - CreateExplorer(ctx context.Context, request CreateExplorerRequestObject) (CreateExplorerResponseObject, error) - // GetExplorer Read an Explorer's active runtime projection. - // (GET /api/v1/projects/{project}/explorers/{explorerId}) - GetExplorer(ctx context.Context, request GetExplorerRequestObject) (GetExplorerResponseObject, error) - - // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/builder) - GetExplorerBuilder(ctx context.Context, request GetExplorerBuilderRequestObject) (GetExplorerBuilderResponseObject, error) - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/builder) - CompileExplorerBuilder(ctx context.Context, request CompileExplorerBuilderRequestObject) (CompileExplorerBuilderResponseObject, error) - - // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/capabilities/{snapshotToken}/candidates/{candidateId}/suggestions) - GetExplorerCandidateSuggestions(ctx context.Context, request GetExplorerCandidateSuggestionsRequestObject) (GetExplorerCandidateSuggestionsResponseObject, error) - - // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/capability) - GetExplorerAuthoringCapability(ctx context.Context, request GetExplorerAuthoringCapabilityRequestObject) (GetExplorerAuthoringCapabilityResponseObject, error) - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/commands) - ApplyExplorerBuilderCommands(ctx context.Context, request ApplyExplorerBuilderCommandsRequestObject) (ApplyExplorerBuilderCommandsResponseObject, error) - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/compile) - CompileExplorer(ctx context.Context, request CompileExplorerRequestObject) (CompileExplorerResponseObject, error) - - // (PUT /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/draft) - SaveExplorerDraft(ctx context.Context, request SaveExplorerDraftRequestObject) (SaveExplorerDraftResponseObject, error) - - // (GET /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/export) - ExportExplorerWorkspace(ctx context.Context, request ExportExplorerWorkspaceRequestObject) (ExportExplorerWorkspaceResponseObject, error) - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/preview) - PreviewExplorer(ctx context.Context, request PreviewExplorerRequestObject) (PreviewExplorerResponseObject, error) - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/publish) - PublishExplorer(ctx context.Context, request PublishExplorerRequestObject) (PublishExplorerResponseObject, error) - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/reconcile) - ReconcileExplorerBuilder(ctx context.Context, request ReconcileExplorerBuilderRequestObject) (ReconcileExplorerBuilderResponseObject, error) - - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/suggestions) - SearchExplorerCandidates(ctx context.Context, request SearchExplorerCandidatesRequestObject) (SearchExplorerCandidatesResponseObject, error) - // AbortSnapshot Abort a generation snapshot. - // (DELETE /api/v1/projects/{project}/generations/{generation}) - AbortSnapshot(ctx context.Context, request AbortSnapshotRequestObject) (AbortSnapshotResponseObject, error) - // GetSnapshot Read generation snapshot status. - // (GET /api/v1/projects/{project}/generations/{generation}) - GetSnapshot(ctx context.Context, request GetSnapshotRequestObject) (GetSnapshotResponseObject, error) - // CreateSnapshot Create or resume an immutable generation snapshot. - // (POST /api/v1/projects/{project}/generations/{generation}) - CreateSnapshot(ctx context.Context, request CreateSnapshotRequestObject) (CreateSnapshotResponseObject, error) - // PublishRepositoryExplorerConfig Compile and publish a repository-owned Explorer workspace. - // (POST /api/v1/projects/{project}/generations/{generation}/explorer-config) - PublishRepositoryExplorerConfig(ctx context.Context, request PublishRepositoryExplorerConfigRequestObject) (PublishRepositoryExplorerConfigResponseObject, error) - // FinalizeSnapshot Validate and finalize an immutable generation snapshot. - // (POST /api/v1/projects/{project}/generations/{generation}/finalize) - FinalizeSnapshot(ctx context.Context, request FinalizeSnapshotRequestObject) (FinalizeSnapshotResponseObject, error) - // UploadSnapshotResource Stream one resource type into an immutable snapshot. - // (PUT /api/v1/projects/{project}/generations/{generation}/resources/{resourceType}) - UploadSnapshotResource(ctx context.Context, request UploadSnapshotResourceRequestObject) (UploadSnapshotResourceResponseObject, error) - // CreateRelease Create a durable project release candidate. - // (POST /api/v1/projects/{project}/releases) - CreateRelease(ctx context.Context, request CreateReleaseRequestObject) (CreateReleaseResponseObject, error) - // ActivateReleaseCompatibility Create and activate a release in one legacy operation. - // (POST /api/v1/projects/{project}/releases/activate) - // - // Deprecated: this operation has been marked as deprecated upstream, but no `x-deprecated-reason` was set - ActivateReleaseCompatibility(ctx context.Context, request ActivateReleaseCompatibilityRequestObject) (ActivateReleaseCompatibilityResponseObject, error) - // GetActiveRelease Read the active project release. - // (GET /api/v1/projects/{project}/releases/active) - GetActiveRelease(ctx context.Context, request GetActiveReleaseRequestObject) (GetActiveReleaseResponseObject, error) - // GetRelease Read a project release by ID. - // (GET /api/v1/projects/{project}/releases/{release}) - GetRelease(ctx context.Context, request GetReleaseRequestObject) (GetReleaseResponseObject, error) - // ActivateRelease Activate an existing durable release. - // (POST /api/v1/projects/{project}/releases/{release}/activate) - ActivateRelease(ctx context.Context, request ActivateReleaseRequestObject) (ActivateReleaseResponseObject, error) - // UploadProjectResource Upload one multipart NDJSON resource file. - // (PUT /api/v1/projects/{project}/resources/{resourceType}) - UploadProjectResource(ctx context.Context, request UploadProjectResourceRequestObject) (UploadProjectResourceResponseObject, error) - // UploadRawNDJSON Stream raw NDJSON into a generation. - // (PUT /api/v1/raw) - UploadRawNDJSON(ctx context.Context, request UploadRawNDJSONRequestObject) (UploadRawNDJSONResponseObject, error) - // GetApolloSandbox Render the Apollo GraphQL sandbox. - // (GET /apollo) - GetApolloSandbox(ctx context.Context, request GetApolloSandboxRequestObject) (GetApolloSandboxResponseObject, error) - // ExecuteDataframeGraphQL Execute a dataframe GraphQL operation. - // (POST /graphql/dataframe) - ExecuteDataframeGraphQL(ctx context.Context, request ExecuteDataframeGraphQLRequestObject) (ExecuteDataframeGraphQLResponseObject, error) - // GetGraphQLPlayground Render the GraphQL playground. - // (GET /graphql/graph) - GetGraphQLPlayground(ctx context.Context, request GetGraphQLPlaygroundRequestObject) (GetGraphQLPlaygroundResponseObject, error) - // ExecuteGraphQL Execute a graph or FHIR GraphQL operation. - // (POST /graphql/graph) - ExecuteGraphQL(ctx context.Context, request ExecuteGraphQLRequestObject) (ExecuteGraphQLResponseObject, error) - // GetHealth Report cached core and dataframe health. - // (GET /health) - GetHealth(ctx context.Context, request GetHealthRequestObject) (GetHealthResponseObject, error) - // GetLiveness Report process liveness without checking dependencies. - // (GET /livez) - GetLiveness(ctx context.Context, request GetLivenessRequestObject) (GetLivenessResponseObject, error) - // GetReadiness Report whether required dependencies are ready. - // (GET /readyz) - GetReadiness(ctx context.Context, request GetReadinessRequestObject) (GetReadinessResponseObject, error) -} - -type StrictHandlerFunc func(ctx fiber.Ctx, args any) (any, error) - -type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc - -func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares} -} - -type strictHandler struct { - ssi StrictServerInterface - middlewares []StrictMiddlewareFunc -} - -// GetRecipeExecution operation middleware -func (sh *strictHandler) GetRecipeExecution(ctx fiber.Ctx, id string) error { - var request GetRecipeExecutionRequestObject - - request.Id = id - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetRecipeExecution(ctx.Context(), request.(GetRecipeExecutionRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetRecipeExecution") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetRecipeExecutionResponseObject); ok { - if err := validResponse.VisitGetRecipeExecutionResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// CreateDatasetGeneration operation middleware -func (sh *strictHandler) CreateDatasetGeneration(ctx fiber.Ctx, project Project, generation Generation) error { - var request CreateDatasetGenerationRequestObject - - request.Project = project - request.Generation = generation - - request.Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), string(ctx.Request().Header.MultipartFormBoundary())) - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.CreateDatasetGeneration(ctx.Context(), request.(CreateDatasetGenerationRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "CreateDatasetGeneration") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(CreateDatasetGenerationResponseObject); ok { - if err := validResponse.VisitCreateDatasetGenerationResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// ActivateDatasetGeneration operation middleware -func (sh *strictHandler) ActivateDatasetGeneration(ctx fiber.Ctx, project Project, generation Generation, params ActivateDatasetGenerationParams) error { - var request ActivateDatasetGenerationRequestObject - - request.Project = project - request.Generation = generation - request.Params = params - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.ActivateDatasetGeneration(ctx.Context(), request.(ActivateDatasetGenerationRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "ActivateDatasetGeneration") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(ActivateDatasetGenerationResponseObject); ok { - if err := validResponse.VisitActivateDatasetGenerationResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetRepositoryExplorerConfig operation middleware -func (sh *strictHandler) GetRepositoryExplorerConfig(ctx fiber.Ctx, project Project) error { - var request GetRepositoryExplorerConfigRequestObject - - request.Project = project - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetRepositoryExplorerConfig(ctx.Context(), request.(GetRepositoryExplorerConfigRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetRepositoryExplorerConfig") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetRepositoryExplorerConfigResponseObject); ok { - if err := validResponse.VisitGetRepositoryExplorerConfigResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// ListExplorers operation middleware -func (sh *strictHandler) ListExplorers(ctx fiber.Ctx, project Project) error { - var request ListExplorersRequestObject - - request.Project = project - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.ListExplorers(ctx.Context(), request.(ListExplorersRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "ListExplorers") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(ListExplorersResponseObject); ok { - if err := validResponse.VisitListExplorersResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// CreateExplorer operation middleware -func (sh *strictHandler) CreateExplorer(ctx fiber.Ctx, project Project) error { - var request CreateExplorerRequestObject - - request.Project = project - - var body CreateExplorerJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.CreateExplorer(ctx.Context(), request.(CreateExplorerRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "CreateExplorer") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(CreateExplorerResponseObject); ok { - if err := validResponse.VisitCreateExplorerResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetExplorer operation middleware -func (sh *strictHandler) GetExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request GetExplorerRequestObject - - request.Project = project - request.ExplorerId = explorerId - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetExplorer(ctx.Context(), request.(GetExplorerRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetExplorer") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetExplorerResponseObject); ok { - if err := validResponse.VisitGetExplorerResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetExplorerBuilder operation middleware -func (sh *strictHandler) GetExplorerBuilder(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request GetExplorerBuilderRequestObject - - request.Project = project - request.ExplorerId = explorerId - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetExplorerBuilder(ctx.Context(), request.(GetExplorerBuilderRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetExplorerBuilder") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetExplorerBuilderResponseObject); ok { - if err := validResponse.VisitGetExplorerBuilderResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// CompileExplorerBuilder operation middleware -func (sh *strictHandler) CompileExplorerBuilder(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request CompileExplorerBuilderRequestObject - - request.Project = project - request.ExplorerId = explorerId - - var body CompileExplorerBuilderJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.CompileExplorerBuilder(ctx.Context(), request.(CompileExplorerBuilderRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "CompileExplorerBuilder") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(CompileExplorerBuilderResponseObject); ok { - if err := validResponse.VisitCompileExplorerBuilderResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetExplorerCandidateSuggestions operation middleware -func (sh *strictHandler) GetExplorerCandidateSuggestions(ctx fiber.Ctx, project Project, explorerId ExplorerId, snapshotToken string, candidateId string) error { - var request GetExplorerCandidateSuggestionsRequestObject - - request.Project = project - request.ExplorerId = explorerId - request.SnapshotToken = snapshotToken - request.CandidateId = candidateId - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetExplorerCandidateSuggestions(ctx.Context(), request.(GetExplorerCandidateSuggestionsRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetExplorerCandidateSuggestions") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetExplorerCandidateSuggestionsResponseObject); ok { - if err := validResponse.VisitGetExplorerCandidateSuggestionsResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetExplorerAuthoringCapability operation middleware -func (sh *strictHandler) GetExplorerAuthoringCapability(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request GetExplorerAuthoringCapabilityRequestObject - - request.Project = project - request.ExplorerId = explorerId - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetExplorerAuthoringCapability(ctx.Context(), request.(GetExplorerAuthoringCapabilityRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetExplorerAuthoringCapability") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetExplorerAuthoringCapabilityResponseObject); ok { - if err := validResponse.VisitGetExplorerAuthoringCapabilityResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// ApplyExplorerBuilderCommands operation middleware -func (sh *strictHandler) ApplyExplorerBuilderCommands(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request ApplyExplorerBuilderCommandsRequestObject - - request.Project = project - request.ExplorerId = explorerId - - var body ApplyExplorerBuilderCommandsJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.ApplyExplorerBuilderCommands(ctx.Context(), request.(ApplyExplorerBuilderCommandsRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "ApplyExplorerBuilderCommands") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(ApplyExplorerBuilderCommandsResponseObject); ok { - if err := validResponse.VisitApplyExplorerBuilderCommandsResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// CompileExplorer operation middleware -func (sh *strictHandler) CompileExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request CompileExplorerRequestObject - - request.Project = project - request.ExplorerId = explorerId - - var body CompileExplorerJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.CompileExplorer(ctx.Context(), request.(CompileExplorerRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "CompileExplorer") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(CompileExplorerResponseObject); ok { - if err := validResponse.VisitCompileExplorerResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// SaveExplorerDraft operation middleware -func (sh *strictHandler) SaveExplorerDraft(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request SaveExplorerDraftRequestObject - - request.Project = project - request.ExplorerId = explorerId - - var body SaveExplorerDraftJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.SaveExplorerDraft(ctx.Context(), request.(SaveExplorerDraftRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "SaveExplorerDraft") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(SaveExplorerDraftResponseObject); ok { - if err := validResponse.VisitSaveExplorerDraftResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// ExportExplorerWorkspace operation middleware -func (sh *strictHandler) ExportExplorerWorkspace(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request ExportExplorerWorkspaceRequestObject - - request.Project = project - request.ExplorerId = explorerId - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.ExportExplorerWorkspace(ctx.Context(), request.(ExportExplorerWorkspaceRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "ExportExplorerWorkspace") - } - - response, err := handler(ctx, request) + // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/reconcile) + ReconcileExplorerBuilder(ctx context.Context, request ReconcileExplorerBuilderRequestObject) (ReconcileExplorerBuilderResponseObject, error) - if err != nil { - return err - } else if validResponse, ok := response.(ExportExplorerWorkspaceResponseObject); ok { - if err := validResponse.VisitExportExplorerWorkspaceResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil + // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/suggestions) + SearchExplorerCandidates(ctx context.Context, request SearchExplorerCandidatesRequestObject) (SearchExplorerCandidatesResponseObject, error) + // PublishRepositoryExplorerConfig Compile and publish a repository-owned Explorer workspace. + // (POST /api/v1/projects/{project}/generations/{generation}/explorer-config) + PublishRepositoryExplorerConfig(ctx context.Context, request PublishRepositoryExplorerConfigRequestObject) (PublishRepositoryExplorerConfigResponseObject, error) + // GetApolloSandbox Render the Apollo GraphQL sandbox. + // (GET /apollo) + GetApolloSandbox(ctx context.Context, request GetApolloSandboxRequestObject) (GetApolloSandboxResponseObject, error) + // ExecuteDataframeGraphQL Execute a dataframe GraphQL operation. + // (POST /graphql/dataframe) + ExecuteDataframeGraphQL(ctx context.Context, request ExecuteDataframeGraphQLRequestObject) (ExecuteDataframeGraphQLResponseObject, error) + // GetGraphQLPlayground Render the GraphQL playground. + // (GET /graphql/graph) + GetGraphQLPlayground(ctx context.Context, request GetGraphQLPlaygroundRequestObject) (GetGraphQLPlaygroundResponseObject, error) + // ExecuteGraphQL Execute a graph or FHIR GraphQL operation. + // (POST /graphql/graph) + ExecuteGraphQL(ctx context.Context, request ExecuteGraphQLRequestObject) (ExecuteGraphQLResponseObject, error) + // GetHealth Report cached core and dataframe health. + // (GET /health) + GetHealth(ctx context.Context, request GetHealthRequestObject) (GetHealthResponseObject, error) + // GetLiveness Report process liveness without checking dependencies. + // (GET /livez) + GetLiveness(ctx context.Context, request GetLivenessRequestObject) (GetLivenessResponseObject, error) + // GetReadiness Report whether required dependencies are ready. + // (GET /readyz) + GetReadiness(ctx context.Context, request GetReadinessRequestObject) (GetReadinessResponseObject, error) } -// PreviewExplorer operation middleware -func (sh *strictHandler) PreviewExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request PreviewExplorerRequestObject - - request.Project = project - request.ExplorerId = explorerId - - var body PreviewExplorerJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.PreviewExplorer(ctx.Context(), request.(PreviewExplorerRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PreviewExplorer") - } +type StrictHandlerFunc func(ctx fiber.Ctx, args any) (any, error) - response, err := handler(ctx, request) +type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc - if err != nil { - return err - } else if validResponse, ok := response.(PreviewExplorerResponseObject); ok { - if err := validResponse.VisitPreviewExplorerResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} } -// PublishExplorer operation middleware -func (sh *strictHandler) PublishExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request PublishExplorerRequestObject +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} - request.Project = project - request.ExplorerId = explorerId +// GetRecipeExecution operation middleware +func (sh *strictHandler) GetRecipeExecution(ctx fiber.Ctx, id string) error { + var request GetRecipeExecutionRequestObject - var body PublishExplorerJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body + request.Id = id handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.PublishExplorer(ctx.Context(), request.(PublishExplorerRequestObject)) + return sh.ssi.GetRecipeExecution(ctx.Context(), request.(GetRecipeExecutionRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "PublishExplorer") + handler = middleware(handler, "GetRecipeExecution") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(PublishExplorerResponseObject); ok { - if err := validResponse.VisitPublishExplorerResponse(ctx); err != nil { + } else if validResponse, ok := response.(GetRecipeExecutionResponseObject); ok { + if err := validResponse.VisitGetRecipeExecutionResponse(ctx); err != nil { return err } } else if response != nil { @@ -6674,32 +3957,27 @@ func (sh *strictHandler) PublishExplorer(ctx fiber.Ctx, project Project, explore return nil } -// ReconcileExplorerBuilder operation middleware -func (sh *strictHandler) ReconcileExplorerBuilder(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request ReconcileExplorerBuilderRequestObject +// GetDatasetGenerationStatus operation middleware +func (sh *strictHandler) GetDatasetGenerationStatus(ctx fiber.Ctx, project Project, generation Generation, params GetDatasetGenerationStatusParams) error { + var request GetDatasetGenerationStatusRequestObject request.Project = project - request.ExplorerId = explorerId - - var body ReconcileExplorerBuilderJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body + request.Generation = generation + request.Params = params handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.ReconcileExplorerBuilder(ctx.Context(), request.(ReconcileExplorerBuilderRequestObject)) + return sh.ssi.GetDatasetGenerationStatus(ctx.Context(), request.(GetDatasetGenerationStatusRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "ReconcileExplorerBuilder") + handler = middleware(handler, "GetDatasetGenerationStatus") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(ReconcileExplorerBuilderResponseObject); ok { - if err := validResponse.VisitReconcileExplorerBuilderResponse(ctx); err != nil { + } else if validResponse, ok := response.(GetDatasetGenerationStatusResponseObject); ok { + if err := validResponse.VisitGetDatasetGenerationStatusResponse(ctx); err != nil { return err } } else if response != nil { @@ -6708,32 +3986,28 @@ func (sh *strictHandler) ReconcileExplorerBuilder(ctx fiber.Ctx, project Project return nil } -// SearchExplorerCandidates operation middleware -func (sh *strictHandler) SearchExplorerCandidates(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { - var request SearchExplorerCandidatesRequestObject +// CreateDatasetGeneration operation middleware +func (sh *strictHandler) CreateDatasetGeneration(ctx fiber.Ctx, project Project, generation Generation) error { + var request CreateDatasetGenerationRequestObject request.Project = project - request.ExplorerId = explorerId + request.Generation = generation - var body SearchExplorerCandidatesJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body + request.Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), string(ctx.Request().Header.MultipartFormBoundary())) handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.SearchExplorerCandidates(ctx.Context(), request.(SearchExplorerCandidatesRequestObject)) + return sh.ssi.CreateDatasetGeneration(ctx.Context(), request.(CreateDatasetGenerationRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "SearchExplorerCandidates") + handler = middleware(handler, "CreateDatasetGeneration") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(SearchExplorerCandidatesResponseObject); ok { - if err := validResponse.VisitSearchExplorerCandidatesResponse(ctx); err != nil { + } else if validResponse, ok := response.(CreateDatasetGenerationResponseObject); ok { + if err := validResponse.VisitCreateDatasetGenerationResponse(ctx); err != nil { return err } } else if response != nil { @@ -6742,26 +4016,27 @@ func (sh *strictHandler) SearchExplorerCandidates(ctx fiber.Ctx, project Project return nil } -// AbortSnapshot operation middleware -func (sh *strictHandler) AbortSnapshot(ctx fiber.Ctx, project Project, generation Generation) error { - var request AbortSnapshotRequestObject +// ActivateDatasetGeneration operation middleware +func (sh *strictHandler) ActivateDatasetGeneration(ctx fiber.Ctx, project Project, generation Generation, params ActivateDatasetGenerationParams) error { + var request ActivateDatasetGenerationRequestObject request.Project = project request.Generation = generation + request.Params = params handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.AbortSnapshot(ctx.Context(), request.(AbortSnapshotRequestObject)) + return sh.ssi.ActivateDatasetGeneration(ctx.Context(), request.(ActivateDatasetGenerationRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "AbortSnapshot") + handler = middleware(handler, "ActivateDatasetGeneration") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(AbortSnapshotResponseObject); ok { - if err := validResponse.VisitAbortSnapshotResponse(ctx); err != nil { + } else if validResponse, ok := response.(ActivateDatasetGenerationResponseObject); ok { + if err := validResponse.VisitActivateDatasetGenerationResponse(ctx); err != nil { return err } } else if response != nil { @@ -6770,26 +4045,25 @@ func (sh *strictHandler) AbortSnapshot(ctx fiber.Ctx, project Project, generatio return nil } -// GetSnapshot operation middleware -func (sh *strictHandler) GetSnapshot(ctx fiber.Ctx, project Project, generation Generation) error { - var request GetSnapshotRequestObject +// ListExplorers operation middleware +func (sh *strictHandler) ListExplorers(ctx fiber.Ctx, project Project) error { + var request ListExplorersRequestObject request.Project = project - request.Generation = generation handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetSnapshot(ctx.Context(), request.(GetSnapshotRequestObject)) + return sh.ssi.ListExplorers(ctx.Context(), request.(ListExplorersRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetSnapshot") + handler = middleware(handler, "ListExplorers") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(GetSnapshotResponseObject); ok { - if err := validResponse.VisitGetSnapshotResponse(ctx); err != nil { + } else if validResponse, ok := response.(ListExplorersResponseObject); ok { + if err := validResponse.VisitListExplorersResponse(ctx); err != nil { return err } } else if response != nil { @@ -6798,32 +4072,32 @@ func (sh *strictHandler) GetSnapshot(ctx fiber.Ctx, project Project, generation return nil } -// CreateSnapshot operation middleware -func (sh *strictHandler) CreateSnapshot(ctx fiber.Ctx, project Project, generation Generation) error { - var request CreateSnapshotRequestObject +// CreateExplorer operation middleware +func (sh *strictHandler) CreateExplorer(ctx fiber.Ctx, project Project, params CreateExplorerParams) error { + var request CreateExplorerRequestObject request.Project = project - request.Generation = generation + request.Params = params - var body CreateSnapshotJSONRequestBody + var body CreateExplorerJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.Body = &body handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.CreateSnapshot(ctx.Context(), request.(CreateSnapshotRequestObject)) + return sh.ssi.CreateExplorer(ctx.Context(), request.(CreateExplorerRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "CreateSnapshot") + handler = middleware(handler, "CreateExplorer") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(CreateSnapshotResponseObject); ok { - if err := validResponse.VisitCreateSnapshotResponse(ctx); err != nil { + } else if validResponse, ok := response.(CreateExplorerResponseObject); ok { + if err := validResponse.VisitCreateExplorerResponse(ctx); err != nil { return err } } else if response != nil { @@ -6832,33 +4106,26 @@ func (sh *strictHandler) CreateSnapshot(ctx fiber.Ctx, project Project, generati return nil } -// PublishRepositoryExplorerConfig operation middleware -func (sh *strictHandler) PublishRepositoryExplorerConfig(ctx fiber.Ctx, project Project, generation Generation, params PublishRepositoryExplorerConfigParams) error { - var request PublishRepositoryExplorerConfigRequestObject +// GetExplorer operation middleware +func (sh *strictHandler) GetExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { + var request GetExplorerRequestObject request.Project = project - request.Generation = generation - request.Params = params - - var body PublishRepositoryExplorerConfigJSONRequestBody - if err := ctx.Bind().Body(&body); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - request.Body = &body + request.ExplorerId = explorerId handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.PublishRepositoryExplorerConfig(ctx.Context(), request.(PublishRepositoryExplorerConfigRequestObject)) + return sh.ssi.GetExplorer(ctx.Context(), request.(GetExplorerRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "PublishRepositoryExplorerConfig") + handler = middleware(handler, "GetExplorer") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(PublishRepositoryExplorerConfigResponseObject); ok { - if err := validResponse.VisitPublishRepositoryExplorerConfigResponse(ctx); err != nil { + } else if validResponse, ok := response.(GetExplorerResponseObject); ok { + if err := validResponse.VisitGetExplorerResponse(ctx); err != nil { return err } } else if response != nil { @@ -6867,26 +4134,26 @@ func (sh *strictHandler) PublishRepositoryExplorerConfig(ctx fiber.Ctx, project return nil } -// FinalizeSnapshot operation middleware -func (sh *strictHandler) FinalizeSnapshot(ctx fiber.Ctx, project Project, generation Generation) error { - var request FinalizeSnapshotRequestObject +// GetExplorerBuilder operation middleware +func (sh *strictHandler) GetExplorerBuilder(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { + var request GetExplorerBuilderRequestObject request.Project = project - request.Generation = generation + request.ExplorerId = explorerId handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.FinalizeSnapshot(ctx.Context(), request.(FinalizeSnapshotRequestObject)) + return sh.ssi.GetExplorerBuilder(ctx.Context(), request.(GetExplorerBuilderRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "FinalizeSnapshot") + handler = middleware(handler, "GetExplorerBuilder") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(FinalizeSnapshotResponseObject); ok { - if err := validResponse.VisitFinalizeSnapshotResponse(ctx); err != nil { + } else if validResponse, ok := response.(GetExplorerBuilderResponseObject); ok { + if err := validResponse.VisitGetExplorerBuilderResponse(ctx); err != nil { return err } } else if response != nil { @@ -6895,30 +4162,26 @@ func (sh *strictHandler) FinalizeSnapshot(ctx fiber.Ctx, project Project, genera return nil } -// UploadSnapshotResource operation middleware -func (sh *strictHandler) UploadSnapshotResource(ctx fiber.Ctx, project Project, generation Generation, resourceType ResourceType, params UploadSnapshotResourceParams) error { - var request UploadSnapshotResourceRequestObject +// GetExplorerAuthoringCapability operation middleware +func (sh *strictHandler) GetExplorerAuthoringCapability(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { + var request GetExplorerAuthoringCapabilityRequestObject request.Project = project - request.Generation = generation - request.ResourceType = resourceType - request.Params = params - - request.Body = bytes.NewReader(ctx.Request().Body()) + request.ExplorerId = explorerId handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.UploadSnapshotResource(ctx.Context(), request.(UploadSnapshotResourceRequestObject)) + return sh.ssi.GetExplorerAuthoringCapability(ctx.Context(), request.(GetExplorerAuthoringCapabilityRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "UploadSnapshotResource") + handler = middleware(handler, "GetExplorerAuthoringCapability") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(UploadSnapshotResourceResponseObject); ok { - if err := validResponse.VisitUploadSnapshotResourceResponse(ctx); err != nil { + } else if validResponse, ok := response.(GetExplorerAuthoringCapabilityResponseObject); ok { + if err := validResponse.VisitGetExplorerAuthoringCapabilityResponse(ctx); err != nil { return err } } else if response != nil { @@ -6927,31 +4190,33 @@ func (sh *strictHandler) UploadSnapshotResource(ctx fiber.Ctx, project Project, return nil } -// CreateRelease operation middleware -func (sh *strictHandler) CreateRelease(ctx fiber.Ctx, project Project) error { - var request CreateReleaseRequestObject +// ApplyExplorerBuilderCommands operation middleware +func (sh *strictHandler) ApplyExplorerBuilderCommands(ctx fiber.Ctx, project Project, explorerId ExplorerId, params ApplyExplorerBuilderCommandsParams) error { + var request ApplyExplorerBuilderCommandsRequestObject request.Project = project + request.ExplorerId = explorerId + request.Params = params - var body CreateReleaseJSONRequestBody + var body ApplyExplorerBuilderCommandsJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.Body = &body handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.CreateRelease(ctx.Context(), request.(CreateReleaseRequestObject)) + return sh.ssi.ApplyExplorerBuilderCommands(ctx.Context(), request.(ApplyExplorerBuilderCommandsRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "CreateRelease") + handler = middleware(handler, "ApplyExplorerBuilderCommands") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(CreateReleaseResponseObject); ok { - if err := validResponse.VisitCreateReleaseResponse(ctx); err != nil { + } else if validResponse, ok := response.(ApplyExplorerBuilderCommandsResponseObject); ok { + if err := validResponse.VisitApplyExplorerBuilderCommandsResponse(ctx); err != nil { return err } } else if response != nil { @@ -6960,31 +4225,32 @@ func (sh *strictHandler) CreateRelease(ctx fiber.Ctx, project Project) error { return nil } -// ActivateReleaseCompatibility operation middleware -func (sh *strictHandler) ActivateReleaseCompatibility(ctx fiber.Ctx, project Project) error { - var request ActivateReleaseCompatibilityRequestObject +// PreviewExplorer operation middleware +func (sh *strictHandler) PreviewExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { + var request PreviewExplorerRequestObject request.Project = project + request.ExplorerId = explorerId - var body ActivateReleaseCompatibilityJSONRequestBody + var body PreviewExplorerJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.Body = &body handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.ActivateReleaseCompatibility(ctx.Context(), request.(ActivateReleaseCompatibilityRequestObject)) + return sh.ssi.PreviewExplorer(ctx.Context(), request.(PreviewExplorerRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "ActivateReleaseCompatibility") + handler = middleware(handler, "PreviewExplorer") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(ActivateReleaseCompatibilityResponseObject); ok { - if err := validResponse.VisitActivateReleaseCompatibilityResponse(ctx); err != nil { + } else if validResponse, ok := response.(PreviewExplorerResponseObject); ok { + if err := validResponse.VisitPreviewExplorerResponse(ctx); err != nil { return err } } else if response != nil { @@ -6993,53 +4259,33 @@ func (sh *strictHandler) ActivateReleaseCompatibility(ctx fiber.Ctx, project Pro return nil } -// GetActiveRelease operation middleware -func (sh *strictHandler) GetActiveRelease(ctx fiber.Ctx, project Project) error { - var request GetActiveReleaseRequestObject +// PublishExplorer operation middleware +func (sh *strictHandler) PublishExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId, params PublishExplorerParams) error { + var request PublishExplorerRequestObject request.Project = project + request.ExplorerId = explorerId + request.Params = params - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetActiveRelease(ctx.Context(), request.(GetActiveReleaseRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetActiveRelease") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetActiveReleaseResponseObject); ok { - if err := validResponse.VisitGetActiveReleaseResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) + var body PublishExplorerJSONRequestBody + if err := ctx.Bind().Body(&body); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - return nil -} - -// GetRelease operation middleware -func (sh *strictHandler) GetRelease(ctx fiber.Ctx, project Project, release Release) error { - var request GetReleaseRequestObject - - request.Project = project - request.Release = release + request.Body = &body handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetRelease(ctx.Context(), request.(GetReleaseRequestObject)) + return sh.ssi.PublishExplorer(ctx.Context(), request.(PublishExplorerRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetRelease") + handler = middleware(handler, "PublishExplorer") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(GetReleaseResponseObject); ok { - if err := validResponse.VisitGetReleaseResponse(ctx); err != nil { + } else if validResponse, ok := response.(PublishExplorerResponseObject); ok { + if err := validResponse.VisitPublishExplorerResponse(ctx); err != nil { return err } } else if response != nil { @@ -7048,32 +4294,33 @@ func (sh *strictHandler) GetRelease(ctx fiber.Ctx, project Project, release Rele return nil } -// ActivateRelease operation middleware -func (sh *strictHandler) ActivateRelease(ctx fiber.Ctx, project Project, release Release) error { - var request ActivateReleaseRequestObject +// ReconcileExplorerBuilder operation middleware +func (sh *strictHandler) ReconcileExplorerBuilder(ctx fiber.Ctx, project Project, explorerId ExplorerId, params ReconcileExplorerBuilderParams) error { + var request ReconcileExplorerBuilderRequestObject request.Project = project - request.Release = release + request.ExplorerId = explorerId + request.Params = params - var body ActivateReleaseJSONRequestBody + var body ReconcileExplorerBuilderJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } request.Body = &body handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.ActivateRelease(ctx.Context(), request.(ActivateReleaseRequestObject)) + return sh.ssi.ReconcileExplorerBuilder(ctx.Context(), request.(ReconcileExplorerBuilderRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "ActivateRelease") + handler = middleware(handler, "ReconcileExplorerBuilder") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(ActivateReleaseResponseObject); ok { - if err := validResponse.VisitActivateReleaseResponse(ctx); err != nil { + } else if validResponse, ok := response.(ReconcileExplorerBuilderResponseObject); ok { + if err := validResponse.VisitReconcileExplorerBuilderResponse(ctx); err != nil { return err } } else if response != nil { @@ -7082,28 +4329,32 @@ func (sh *strictHandler) ActivateRelease(ctx fiber.Ctx, project Project, release return nil } -// UploadProjectResource operation middleware -func (sh *strictHandler) UploadProjectResource(ctx fiber.Ctx, project Project, resourceType ResourceType) error { - var request UploadProjectResourceRequestObject +// SearchExplorerCandidates operation middleware +func (sh *strictHandler) SearchExplorerCandidates(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { + var request SearchExplorerCandidatesRequestObject request.Project = project - request.ResourceType = resourceType + request.ExplorerId = explorerId - request.Body = multipart.NewReader(bytes.NewReader(ctx.Request().Body()), string(ctx.Request().Header.MultipartFormBoundary())) + var body SearchExplorerCandidatesJSONRequestBody + if err := ctx.Bind().Body(&body); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + request.Body = &body handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.UploadProjectResource(ctx.Context(), request.(UploadProjectResourceRequestObject)) + return sh.ssi.SearchExplorerCandidates(ctx.Context(), request.(SearchExplorerCandidatesRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "UploadProjectResource") + handler = middleware(handler, "SearchExplorerCandidates") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(UploadProjectResourceResponseObject); ok { - if err := validResponse.VisitUploadProjectResourceResponse(ctx); err != nil { + } else if validResponse, ok := response.(SearchExplorerCandidatesResponseObject); ok { + if err := validResponse.VisitSearchExplorerCandidatesResponse(ctx); err != nil { return err } } else if response != nil { @@ -7112,27 +4363,33 @@ func (sh *strictHandler) UploadProjectResource(ctx fiber.Ctx, project Project, r return nil } -// UploadRawNDJSON operation middleware -func (sh *strictHandler) UploadRawNDJSON(ctx fiber.Ctx, params UploadRawNDJSONParams) error { - var request UploadRawNDJSONRequestObject +// PublishRepositoryExplorerConfig operation middleware +func (sh *strictHandler) PublishRepositoryExplorerConfig(ctx fiber.Ctx, project Project, generation Generation, params PublishRepositoryExplorerConfigParams) error { + var request PublishRepositoryExplorerConfigRequestObject + request.Project = project + request.Generation = generation request.Params = params - request.Body = bytes.NewReader(ctx.Request().Body()) + var body PublishRepositoryExplorerConfigJSONRequestBody + if err := ctx.Bind().Body(&body); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + request.Body = &body handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.UploadRawNDJSON(ctx.Context(), request.(UploadRawNDJSONRequestObject)) + return sh.ssi.PublishRepositoryExplorerConfig(ctx.Context(), request.(PublishRepositoryExplorerConfigRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "UploadRawNDJSON") + handler = middleware(handler, "PublishRepositoryExplorerConfig") } response, err := handler(ctx, request) if err != nil { return err - } else if validResponse, ok := response.(UploadRawNDJSONResponseObject); ok { - if err := validResponse.VisitUploadRawNDJSONResponse(ctx); err != nil { + } else if validResponse, ok := response.(PublishRepositoryExplorerConfigResponseObject); ok { + if err := validResponse.VisitPublishRepositoryExplorerConfigResponse(ctx); err != nil { return err } } else if response != nil { @@ -7333,117 +4590,106 @@ func (sh *strictHandler) GetReadiness(ctx fiber.Ctx) error { // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "7D39b9u4kv8KoXvAAgcnTrt9D9jcT2mStjmkaZ6T7t67Ra5gpLHNV4nUklQSt8j/fuCXRMn6dGwnu/VP", - "rWJKHA7ne8iZ70HIkpRRoFIEh9+DFHOcgASun04f0phx4GeReiI0OAxSLOfBKKA4geAwgGLAKODwR0Y4", - "RMGh5BmMAhHOIcHqzYTQc6AzOQ8OX40CuUjVu0JyQmfB4+MoeA8UOJaE0YZ5ZsWAp8xzydm/IZQNk6T2", - "16fMMIEYsICGGbj99WkzCJbxEK71Lw3TeENWn+tRvSpSRgVoajjK5Jyp397iaAJ/ZCA0JkNGJVD9X5ym", - "MQn1Po3/LcxuFrP9jcM0OAz+Y1yQ3Nj8KsannDM+sbOZuSMQISepoYrgeg7ov68+XaCIhVkCVCIiUILj", - "KeMJRCOkoMCECpTRr5TdUzQlEEdihBhHLCFSIIcGhN06EKFpJveDx1GxtuOYAJXHMRPwPIsMNQAoxDSE", - "GCKUcrgjcI/gAcJMjUO3MGUckJogBvWXygoYncYk3CLYZ0mSSXwbAwpxim9JTORC4Z1DCCSVSEgsQe2X", - "kDgGsyWpJAkRkoQo4ngqx2l268Cz48M5pjOIyot7x/gtiSKg290URTJApZpBbwmhIUlxrHaJMok44Eit", - "KmERmS6QnBOBnOxEVqyU1/EeS7jHi2uSAMu2uFWXS9QEDyFABBFSPCKA3wHfEyQCFAGOYkKhDPgZlcAp", - "jvVM24P7iKKMwkMKofQ5eIRS4IIICTQ0hOXYZYpJnHFALAwzzqtkdMHkO5bRaLtU5EhipOgmIhGWMNJs", - "TGJD+JZhDIfIOSg+sXpvT6QQkikJPfHl5DyKGAikCBEeiKhQ2iVexAxH14ydYz6D7a74ViHZE2IcRBZL", - "S3MCyTmgBD+QJEuQUzVIkG8Vmrtm7COmCyuOxXaXwM2s3iLSGNPSEgzX/CQKrtoLmZAoZTEJF+W1fKb4", - "DpNYScstck+h/AoJPfLkM+MwQgmWwAmOyTdFojiU5M7KY+B3xPLXLQ6/Ao2UNM+KpVTXmHIWghDbXaW3", - "Wwq8OxyTyNgMt5nU4u2e8a8ixaFhuwTTaIRYJtPMsJzPikRDrJUWJFiL/jheIEL1Z/V633Oczv95vgFr", - "aILvFdxNi7QTow/X15f5ioHeQcxSUAuRHFORMi5RYc8jzKEwmPwFbEakt6zBwV8oISWtIXKWDUaU8QTH", - "+UJz2RCyLFbDlFKNsrC8js2QXc+9yM1SA6ISxgpMzAVo00CTjbYepowXK9fwf7j+eF6BV8KDHM9lEpcB", - "rTHRyyCdKgWgdMMtZ/cCOPp8loOm5zqHGQ4XG6BZ8+FeTBrroYWF5DFtTqDanCJCGD1nhVdhr5vZ1m7q", - "DlhEDj2Olfm3MLpXGLs3ZYJIxhcogjRmC00YJatWDSMCERoyauwXf13rt3IHLKzD1sWhYrEWG9dMtRmh", - "0nMZE8CRIhxlDxr7UD1Jf9esxCmMynjhQb92+3AVwipR0h67pxAVP4aMTsksM8YhusfG/psqoL11bMLa", - "6L0HORN4etW3Muwf9DJ1XMS3OYhZkFum4ZiqxeEWuQm5v8qO5faFAtZjfWVFLKhUyzNmhLFNlFliueoW", - "LJYgKhlikXFqlHMs5lbdXRlzbAMy3H55iD2ZYjkfoT8y4IsRSrJYkhRzOdJ2l4b94kSbYHc4znQMwAl1", - "JQA9Y8pOvXaJ3ndJXgzDTFu2gKXx1TIuGHdPbmPKUQu7AIHuiZxrmWPVm7/M9Qv4vutsk/AxDr+KXMAz", - "H3g9Tst4w6/W8RQhS8Ff2GbEfm+6LEUJjFuGQL2VxwH+C0UgMYmNPcwhwiaiQCMUMs4h1ji5XbiFn5XI", - "c+16Yci2FVsRYYkFyBEq4uIjJ0S1W0dSKGxMI2JZJqHYuGWFYSH5TG184Rtsf5VHBWEqjgo5ROoR293C", - "t0JJ03rJsQlVt4I8dNizm6JcazyDESJ0BsL8zRMaemu08GxzrPMVbkLPrUCBbfpMudmKPMu06el4iyml", - "F4Wn/ezSoLKnIkuVHwvRR4gIdjmPrS782Ey2pyZ3ZkkOlpIU2hTO0pjhqIjX7etkjZ1LZ08uz35VtqgB", - "EWiWBIe/BzFjyX6I40XK9xmfjV1CbS+P8o3vXgc3S8mZUXAUunwZjiKi/o/jS67mlwREcDjFsYBRkHp/", - "UoiLs4Tq/xIJiejCkZnkWL+lJrVQYM7xQj1PSQwXOun0fRlCSaSh09Y0k/tDd+aryGP9bn51UxToYbc6", - "g5ejx0K+CpJ6AA4Pigg+AI6A13nnZZDtZ2uBTdN4cWwiUsIz6gYBrd82mdoOuO3YAWRQ5Jb0m+ojCaFn", - "5t1Xy3ThdPAJx1N5QmZ2PXUYLMZ57DFlPMEyOAwIlf94E+jZSKJY5iCfjFAJM+DqM4LiVMyZvGZfgQ4l", - "pQJz1Q81wOchsMdmWsGyud2MCJ5RJiQJ+2/oSf5OHVdH5V3rmr/33r2q2zuTEugPusXsRL9WB33uhHV9", - "6bd8YBtJFJ+rLLWMp2Ih5R2ppZCcnfJg/ED6wCnxMN7KuoXaUfIasMw49Gf4d+6Fx1HwlRjD1+ku5/Yu", - "L0dBWaezcu1Y3m73wduMxEqUagZTvrDiyGxmDSeFWZuCUf8zLrHbhcBJY5871Z6EjIakpCOqiqegHPv1", - "c5KQCkHaJFVw+Org4KCLosufrZCWt3MWoyW0VIHwtqydkqxgHihmXBqyp9roqRfNwF+Vx9/Nzc60gGjW", - "DwxCiXIJLjkobyA/NeSI6Pro7flpMArenZ1fn06CUXD84WhyXU+OxiekYb+JTaZo0NAKFfWxgzxyxByo", - "/DQMSOulE0Y/sqgT/Zfl0YpWGZMXLOo3mXEmP/XHy1CT0G3q8eT06Pr0i9vbk8+X52fHpb+cnp96j5PT", - "i6OP/uOnycnpxDxfBaPg6vTaPHyZfPp0HYyCo5OTL5NPn6/N6I+ffj3NH9VPx5/OP3+8CEbB58sTNW3+", - "bMfa55teZmsrI7/zJPQATo4gBglOInu21i1jMWDDYTr0xujxHHMj35rHvCOxOwS4PEhZ/ca+bhzwAFHr", - "J8Qc89YhFcRV4VpaTfWTFSDKQI+q+KrbkrdGG11JLGFL6jnEEsds1ik07bBlU+0JxlmtYd2k8ku4qROt", - "MZlCuAhjyNHnPnFx+pvmm6OTf9W+WTLfcBx/mgaHv/c25G5GAc1iGwWSPIM++rcCbIed5xuEbsPq6OfY", - "KdcrwDycr+bW0b6iWAeRakngSc5R1SGy8PRa70qez+qsY2cf4EmYzcvhrnWF1u5cdbBUgUXP8K3jk96U", - "8aT9r2OXBpoo7UG3E1S70C3TSt9oSaqURb0ia9rO3vs4bHtGgeQZ1Ymbenh0nq1Mqx2G5go77iMwn9LD", - "lA9l/d7nem7Idvuh9auQpdA7RrEh6dBOGsqjGTzfaTSrnWpWuiLRsVzFkYNnvrAeQHVmDoLFdxBd6eEt", - "toZOMV3qM4+dh6q8oU8VUaW7IY00UkPEBbUafLkdqwgyf1ktxFwQykZ9cGXtuvzPMslFMMVZLC+f6ATq", - "uxOXWAHRCdFUW9jNIMX4FuIe34nZjIQ4vu6TDBig/cr+cH+mWEbKMl+k0CSHq/HEkrzM9WWBZoemMhq8", - "OUp49slgeYVNVNBCu1roDCPb3uGaKWdJ74BCX2JJWZrFzUpQsp4zVr1Ms6gS0N7Xil0q5m/B6oXlvSFu", - "vD2qecwyk94c6K/1Z4t2BPLKJbOOr3F2/55jQhvUwv2EMXkakxlpkBKVbcgZpHqRrfKlzn1QXFKNEw7Y", - "DcZtYq8d6XlQa32ZzdrVrJTLVCjoNACW8FSK9qZYSuA0OAz+7/ejvf/Fe9++3Nj/HOz98uXmP/8WNOqF", - "rqlNfKY694o646kBXkNs/ULWV2as2mSn+9peulaDysusTw8XMqYEfA5cM2Vc5dAPoI+Spl9CSNW50aOD", - "UUD0sZwpAf52cbUQEhKdf5FAld/wdvGZx9rAigideSPYrQBuTtQdOxS9XRwrMZmrsVJwoYAkwTKsh3E5", - "4O2g/fXo/LPJBEyudIz3/DwYBSdnV9dnF8fX3cFavfp6fPspyFUPF3RSa2sCYlnW4NumXyrBdBP4NiH1", - "k2Dknj8cXbzXzzrw/eXo5EQ/mdC2fbx5gvTSGb3VomBDfeQ1pID9KF95+tbVbTXm5fKl/ReZZ1a5N+HW", - "zxcM8mXNRazenn5HeG1irrzVpwM1e/VfpP2WSX/Vewh6wPNE6YrJl13fkstcQvAygYy8tLzDUI/oHqOS", - "41CubrQ0e5S989C9XdMnOaNderzs0TW4cXU49JhoqI4xirBiuGmjrcViswejmycz5SOW4Py3YLTRhEhA", - "CDzr5US4A9f1WQy4A24PynSxkuwzYTXR4b7v3h8ZNBYrqN0i668NFfjh8iGY7mOY9RHIYQc6245ylnO2", - "vT73rnhphVxHjr1madwFQCF8OWNyMtRtzST0ClWa2GidfZgDWgOBm6HYpjoa0geQ37Jo0cV49UzeW3sP", - "09nr4dsl2djJUeXj2AOjJ2u3VMBdY+m8mK43cCmepF+vXablhI8gcYQlHnzcwzuhvvLR66rt3niY+l35", - "wMdqkq7+reb0VDF97lm2fqdvvmtpguqmFbONcvgbsFKNXQxDTrP90Svy9FgLUyGQN3TqvU+GseVweJO5", - "ZD9bh2jvKubqcrLNMnmy1Kq7LNoBZeXevr6kltdw4MoUo+Z6SfW2PKaRveK1VxRQ0IpG7AdLYXonwCrT", - "EV1dBruP27UhgwHE1E9C8iyU+uqtwsDYjTGrHqEIUqDmmjU1VwUVDAoERsGe2qkivfeVWyNOb/oL1E+5", - "uTCA5knUL7h93sioKwlaErVeXbE1mlYLlcQkIXLwkeUBR1v7O7WVVfsOaT5f6/q3e4BosCVd8nBfwtkh", - "i7gWc3rNO6yZI89VtWdJOLsvo2GQjzn4yEottRW7bOHxFtAd07hsjDRfHR+dH038UPNkcvQvL9j8xf3B", - "xaQ7Y9Gj4LK4JrqScKsUWlj3EXfhDnYOkQHeTgh71nIZzFrcm+seq8nENYisVpi2Kqe2L1UKOlxL2HSZ", - "rp8YOlVST/Sl8JXItkvAeBAUhN0/WOpKTA2yGHVVjfs5E2AqDylDTMyxuaOcKqeE30GEYiZEDELECxRl", - "pmqgrgQQZpLdAd8P6uApBbY3e8O4W40O0Vxt5xBWMtU8gWXe9yZpD+tM3NWz1WTWVu9grvWIeMvh+Vo8", - "5fG1oUkCEkfcgNsvXVME8mqIbNhJAV0nhDAq5iStP/QyJBJZpbly4r/0qUYMFucuh+igOGb3E3vG7NQd", - "V11OlOhxVxBPzxlLG8Yk+OFKQip6RC1KonUZhKX56hZ9he9A38pejb1e9AX1DSewG1bVgeXVIrLblGJr", - "RtpAOeaXE2mPUrXm7FbNypVvPdXA1xzu6kq9cZB8gfsd3esRK6utu9KFrLLtc2UqdUE5aAYJkbYkyzlj", - "yU8CmSuBpk6qGZzgNLUxNMcU3bGzvlVkBuYArrwLi2+JDqVtKmbb24ZqNn9aypdc49uVAm+rHSzseSqT", - "rf9WsjLy+7EB8Q1GD4kG9uJLDdisHBUcSBQQxxOgEdiLwMXhvSKJc7M6XlNCaePlo1YELa30t9JVzy24", - "zjbfPMBxdhnqx9oaN/ll69b0YDXVt3yDuuOEbD6021kvUFp716x667ozl9Yq+mrkV3e2TZ9U7D+Jki2r", - "xP/y3fG33c69xHWj4GFvxvbcJO4u/t3r/d88gyAfs0cSXWrk8LvruOK9otSIPgwTzIicZ7f7IUvGpsLX", - "OGYsGRNbIjGv9TX2337UFcgInTLNRUYyBUqfGTV2dHmm5Icj/+DV/sH+ga2tQnFKgsPg5/2D/Z8tGBq/", - "Y5yS8d2rsaK+KccJjKv5IzH+TqJHNXYGel15SRIlQIP3ICf6jVP3QlDpDfP64GAbFbevMl2ccprFeU1w", - "XS/uzcGrbUxfqU9YVDMduYqEpo58XtXO3Z3TRb/l3CvK+JNwVRy9IppvDn5ugi7H9rhaIlq/96bve3kF", - "S12eLksSzBe2YrItTIgSK6g00BjlVLNUWHLfVinJO0b9XtuOiLQ3h6paIDePoxLFCpBi/N0e834cF2cX", - "xfh78fBY7V7VUDOgGOKueDWmJb2hXoOqx5tRkDJRwyXHXDmxJwbk99WOVSCkcws8Ms0r5o6Vs7PnjoIU", - "2Fm+AvvFBQS+pE2H7iKYAv9S1K9tLuNRksO5v3VLqCKLHjVqykd5mw7nd+fYp8S/y1Y+orpsJJXf1mUe", - "Xpo8OuhmyOU6zoUk6/VmqVZrX/GxVIFYv/hL7xfzCs3qvVd/HwBsTS1P9Y3Xrwd8w6+A+jgK/j4Az+XS", - "xPrln4egOq8uW5acn03hT1wUv84Fe0HDphroClJtbJkYtibeRlaGm4IiuRDPtcCXXPx/GSjYG75cI9Na", - "1UOT+D2ymKoXwDvZsGXZ8AJY01EEwh4rIjyVwD2rxivHXGZTy5w+m+bleU1fgnaD2fU/yEuCmHdepOW8", - "OZpal7nak6Lquo/UmLrKFteiFTp7bNRZuX1F700/chKNhHROhDzNRz2RdNZy9OZFU9aaSERhvSAD83cC", - "wjpEfrObVemi3Yc4LYIXza7DmsRElx396sXryqWWVs8m1n7p+17JiO5jANd1ulkfvRuqQ9i0veNWMDoq", - "7K0Vxfh70RC6NZpUIvCdMnxmZYhpvtc/iVwtZlSSBFBxv/4pAq/bB/F6jfdWmyV6G/stE8be3eguInyb", - "32/dGC2W6mXW9ZjQBz6k10AKU9ORl3i9lXVVmTxvuR2Krem83Jdol7vt9hWSy72s+1J8Q5/ivi5FbZtW", - "vV/boftGs8Bcyq6j2PWbB5VSDVuOtlVLKXT06lpqntzfbqjrZ/+cLLUqY/QyIBqa8z4zXz1dzodez4Hx", - "99JBp8dxUcdv/N2rwfY4FuWao10aoraY5yZ5oG6+GkZ4a9trm7tz2j1hFIru4ohQbdTl/FLRHX9CJtnp", - "nQ3onVFt3q56arA50ttxpKj26+WaiKt++2aNEmTRRxTU9W7ZoCSom67WoXH9yPKlFa3IhDYmP58h10rk", - "OS3HJ3HT45/IAfGbbD2v8agbUlXLjPs9ctZvQtY2NNuyIVnfh6vO+cKUURLi2PVgtM33IPL9MZ01wBRh", - "yRISIru96BbLcL6zOX8sm9N2p3p2zq64hTt/cOcP/ui8abrBbZMzsxrGvMJ3OVee2P50m2DNpVtQW2bO", - "5ftBrfrVdOS2adZfX3vqVW/bzivdMXkvJretHpvctVP9s+M//wj3xhjBv1LWwgAKLq2ofOLfBfL/oi6g", - "61b67HaiLQyzYTuxUrdoy6qoWjWoJVxq9wWxqQ6XWkMRmXtaOyU0QAm9GgLuJV7EDEfXjJ1jPoM1aLE3", - "r4dAfs3YR0wXdqv0bao3vwxaekyAyuOYCfA3/Pl0qXp5yK6/xxLu8eKaJMAyuR5d7BoxP7+QM4BsWsiV", - "CxFtW8hVSg7V3VGyh169c4yuPM1OtO3s6148XbRNf3auzsvobOfsw1LVnpcc7XKGi41HRmjKWWKvGeJQ", - "ohS4IEKJgp17vWP//uxfOR3xvALAdBNeOomxqcxVQ9fmbYuBhl7K9cEFe9gjgimhxGR8db8kQmcII8oi", - "c5xQ37v68cLhL5cX264ymwb1NcncW8bllT2Z8aMdn66/n/am94uDdcMLvdemaKB8qa10sKvpDMuObJ5O", - "Ni9g+/WZ/Zq9R0JimYnNHtMfUAmhRG7PfItpd+P3h7jxay8yMa42IEugfPq1Xl6upqPrrgI/L9PlsaHG", - "e8d1pVqedhnfHe2cAzY+uf3M/+ydM5bsmZake8csSYgcVgRmMzKjkqb780uN3d3HFW7A9ZQ2bt5GYWPC", - "Ltq3slFwhFuuk/sJ3xWFzpRQHJNv8PzS5p2F5MXalH8ylbtdJ+bPXnTnVxybmIfiPccUG1T2TiOK8Xe/", - "HPfjNqvydIwudR5sPJhmqhU5pnXvNNgGy1r92LDz3tWHo9d//0ewFg3OQglyT0gOOCnLhM56ZDvLfyeG", - "nlMMXWmitYdIDCshRaKIUMkab961yyAOMWBLvZupLjIxM+zc8h1z/pWZ0xU1QVGm+227ShbIclhxTbY3", - "T66jGp/HnBGkHEIsC4e4vqKdZVjlb2BJvEuHO+7dce9fnHtphHBRxs+xLqFa6drupTnXDGRkaLvwq5mv", - "pCt3GZM/dcbEK/lX0QQbrPCXE9x3+7+OIus7avuLUBteMjduF+jsZLPJOUc/w6lyO3WGC/i6ivjufJSd", - "lfMDWDlFhWJadPF0/kqhnbq4+RmCk0PCjfazXrRx2w0RXKuD7ohiTVuCXf+BjYuNXR8B10dAuTVFJ4GL", - "E93mNw8uKnosywOOzf3CZu6b4HvzmX4nAIoWHIMr+c/8kvtb6APQzzR42KPRMsPtcgs7Hn+ufAHH946z", - "TaKgrlMIi2PWGp/QI64wjW7ZQ4PH2A7uh+uP508nh1UMvoq7RCMw3bHMmtB7jtP5P8+RMIuzKJmpv/4R", - "F13MtNyr9SJMmzLdCUSPtB/ceRN9yMIia40iow/X2llX41r7cluha0sSpX5mjs6q8UNHafrfNh60H7iM", - "8WLGNWn/NfjQISbNF2biFm3MtuOxHY/lPKY5BzGO3n04mzTy2RxwLFsZ7IMZ8bJ22NoCG282iRydI4Mo", - "FEGqeJSGC0QEokwiDjha7C8xsjKdUIjDOUQoZNykUQq5Zz5nNyEmd/CtbQ/OyR1QEOJF7ULdgi1Vo9gC", - "jO6JnLNMonAO4VcdWXEIJLoIqlq+xuC39qA8jsiLQ8DWyPDEwxnCHDrp7n4Ocq6rDljqjapfyN9Wr+uS", - "YMYdzXgcHAbj4PHm8f8DAAD//w==", + "7D1rc9s4kn8FxduqqbqSLWcme1Xr++TYTuIrx/bKysztTeVcENmSsCEBDgDaVlL+71d4kSDFpyzJnr18", + "Sijj0d3obnQ3Go3vQciSlFGgUgTH34MUc5yABK6/TjK5nIBgGQ/hBsul+i0CEXKSSsJocBxc6//gGJ3i", + "eJVyxG1rJEKWAsoEREgyhDO5ZJx8AxRlHM9iQOePacw4cJRkEqsxxGEwCoga848M+CoYBRQnEBwHqu+d", + "G/cuVWCMAhEuIcEKHrlKVSshOaGL4OlpFLihLyL1dz2k7WVHhKLBKODwR0Y4RMGx5Bn4IyeEXgJdKLTf", + "jGrm+QAUODaEqJ1nUTR4zjw3nP0TQtkwSWr/uvkMT6qrSBkVkK86U397h6MJ/JGB0HOHjEqg+r84TWMS", + "aszG/xQG/2K2v3CYB8fBv40L1hqbv4rxOeeMT+xsZu4yQ02XgP7r9voKRSzMEqASEYESHM8ZTyAaIQUF", + "JlSgjH6l7IGiOYE4EiPEOGIJkQI5MjimowtEaJrJw+BpVOB2GhOg8jRmAl4GyVADgEJMQ4ghQimHewIP", + "CB4hzFQ7NIM544DUBDGoXyoYMDqPSbhHsC8SJaxKekOc4hmJiVwpunMIgaQSCYklqPUSEsdgliSVJCFC", + "khBFHM/lOM1mDjzbPlxiuoCojNx7xmckioDud1EUywCVaga9JISGJMWxWiXKJOKAI4VVwiIyXyG5JKJQ", + "ZFYQy3h8wBIe8GpKEmDZHpfqZo2b4DEEiCBCSkYE8HvgB4JEgCLAUUwolAG/oBI4xbGeaX9wn1CUUXhM", + "IZS+BI9QClwQIYGGhrGcuMwxiTMOiIVhxnmVja6YfM8yGu2XixxLjBTfRCTCEkZajElsGN8KjJEQuQQl", + "J3anOBAphGROQk995ZtqxEAgxYjwSESF027wKmY4mjJ2ifkC9ovxTBHZU2IcRBZLy3MCySWgBD+SJEuQ", + "22qQIN8qPDdl7BOmK6uOxX5R4GZWD4k0xrSEgpGan0QhVQchExKlLCbhqozLZ4rvMYmVttyj9BSbX6Gh", + "R55+ZhxGKMESOMEx+aZYFIeS3Ft9DPyeWPma4fAr0Ehp86xApYpjylkIQuwXS2+1FHj3OCaRsRlmmdTq", + "7YHxryLFoRG7BNNohFgm08yInC+KREOsNy1IsFb9cbxChOphNb4fOE6Xf7/cgTU0wQ8K7iYk7cTo43R6", + "k2MM9B5iZVszjiTHVKSMS1TY7QhzKAwmH4HdqPQWHBz8xSaktDVEzrLBiDKe4DhHNNcNIcti1UxtqlEW", + "lvHYDdv1XIvcLDUgKmWswMRceTqMG27U1sOc8QJzDf/H6afLCrwSHuV4KZO4DGiNiV4G6VxtAGpvmHH2", + "IICjzxc5aHquS1jgcLUDnjUD9xLSWDctLCRPaHMG1eYUEcLsc1Z5Ffa6mW3rpu4AJHLocazMv5XZe4Wx", + "e1MmiGR8hSJIY7bSjFGyalUzIhChIaPGfvHx2r6VOwCxDlsXh0rEWmxcM9VulEpPNCaAI8U4yh409qH6", + "kv6qWY1TGJXxyoN+6/bhJoxV4qQD9kAhKv4YMjoni8wYh+gBG/tvroD28NiFtdF7DXIh8PZV38qwP2g0", + "Y8ACfJuDGIQcmkZiqhaHQ3IXen+TFcvtCwWsJ/rKilhRqdAzZoSxTZRZYqVqBpZKEJUMscg4Nco5Fku7", + "3d0ac2wHOtyOPMSeTLFcjpAOzY1QksWSpJjLkba7NOxXZ9oEu8dxpmMATqkrBegZU3bqrWv0vih5MQwz", + "bdkClsZXy7hg3H25hSlHLSwCAj0QudQ6x25vPprbV/B98WzT8DEOv4pcwTMfeN1O63gjr34010dsN2q/", + "N1+WogTGLUOgeuVxgP9EEUhMYmMPc4iwiSjQCIWMc4g1TWYrh/hFiT23vi8MWbZiKSIssQA5QkUkeeSU", + "qHbrSAqFjWlULMskFAu3vmFYSD7TPCa/fyxPCsZUEhVyiNQntquFZ0Jp03rNsYutbgN96KhnF0W51ngB", + "I0ToAoT5zVMaemm08mxzrHMMd7HPbcCBbfuZcrMVe5Z509vjLaXUvii83c+iBpU1FVmq/FiIPkFE8FS7", + "P3tG/NRMdqAmd2ZJDpbSFNoUztKY4aiI1x3qIxo7lz49ubn4VdmiBkSgWRIc/x7EjCWHoT4jO2R8MXZH", + "UAd5lG98/3PwZe1wZhSchO6ECUcRMadtN1zNLwmI4HiOYwGjIPV+UoSLs4Tq/xIJieiikZnkVPdSk1oo", + "MOd4pb7nJIYrfd70fR1CSaTh09ZjJvdD93lXcY71u/mrm6IgD5vpM6+cPBbyTYjUA3B4VEzwEXAEvP7I", + "0QfZDlsLbJrGq1MTkRKeUTcIaN3bnG12wG3bDmCD4mxJ91SDJIRemL5v1vnC7cFnHM/lGVlYfOooWLTz", + "xGPOeIJlcBwQKv/jbaBnI4kSmaN8MkIlLICrYQTFqVgyOWVfgQ5lpYJy1YEa4PMI2GMxrWLZ3WpGBC8o", + "E5KE/Rf0LO9TJ9VRedW65u+9dm/q1s4cCfQH3VJ2orvVQZ87YV0j/ZY3bGOJYrgKqmU6FYiUV6SWQ3Jx", + "yoPxA/kDp8SjeKvoFtuO0teAZcahv8C/dx2eRsFXYgxft3c5t3cdHQVl3Z6V747l5XYDzjISK1U6CkS2", + "sOaSoqc9eFH/M46wL4KK8CGjISltBNXdpWAPO9glSUiF6+xJVHD85ujoqItty8NW+MdbHku2Eu5VILx1", + "aWcXq30H6hJ31thzb+i5+ZmGvyq3vltknf0A0aIfGIQSZfffcFAmf55M4zhlevLu8jwYBe8vLqfnk2AU", + "nH48mUzrec44fjTsN7E5DhrUtMJFfYwdjx0xByqvhwFpXXHC6CcWdZL/ptxa8Spj8opF/SYzHuN1f7oM", + "tfvcop5Ozk+m53dubc8+31xenJZ+Ob889z4n51cnn/zP68nZ+cR83waj4PZ8aj7uJtfX02AUnJyd3U2u", + "P09V6883Z2po/Xl3fvbBjPDp+tfzvIlqfnp9+fnTVdE+/7Zt7feXXvZqq3C/91TzAOmOIAYJThV7RtaM", + "sRiwkTodc2P0dIm50XnNbd6T2GX5rTdS5r4xrBsbPELUOoRYYt7apEK4Klxr2FSHrABRBnpUpVfdkrwz", + "29CtxBL2tC+HWOKYLToVqW22bqM9wyqrtaib9voSberUbUzmEK7CGHLyuSGuzn/TcnNy9o/aniW7Dcfx", + "9Tw4/r23BfdlFNAstuEfyTPosydXgO0w8HxL0C1YHf+cug33FjAPl5v5c7SvejZZsXUs8CyvqOoJWXh6", + "4buRy7O56NjZB7gQZvFyuGt9oK17VR0iVVDRs33r5KQ3Zzxr/evEpYEnSmvQ7f2cFrpuCHv4cdXbkKXQ", + "20HdEYfY/Fto2E+jxfD5zqNF7VSLUkZ5B7pqVQbPfGUtw+rMHASL7yG61c1b9ht9vnCjE946M2q8ps9l", + "01IqfSOPrLNuvniOXm7FKszso9XCzAWj7NQ3UxaPC/6vs1wEc5zF8uaZzoFOnHe3OjogmmsrqxmkGM8g", + "7jFOzBYkxPG0TyR4gAYs+0n9hWKdKOtykQKW5oisw4L119jTmQWZHZnKZPDmKNHZZ4N1DJu4oIV3tdIZ", + "xra93fg5Z0lvR7Mvs6QszeIm4o8CyXrOWPU0DFIloL3RilUq5m+h6pWVvSGunM3TO2WZOdsaaLP3F4t2", + "ArpD4p7SyNnDB44JbdgWHiaMyfOYLEiDlqgsQy4gJTDWR+pcByUl1fjRgNVg3J7qtBM9D3Zs71irFpuN", + "DrIUCToNgDU6laKAKZYSOA2Og//9/eTgf/DBt7sv9j9HB3+7+/Lvfwka94WuqY2PXp17wz3juYE/w2z9", + "Qpm3pq1aZLf3tXWaqkZlNOvPBgsdUwI+B66ZM25z6AfwR2mnXyNI1V/RrYNRQHROxpwAf7e6XQkJiT4o", + "k0CVt/Bu9ZnH2sCKCF14LdhMADfpVKeORO9Wp0pN5ttYycEsIEmwDOthXA+EOmh/Pbn8bCLEk1sd+7u8", + "DEbB2cXt9OLqdNodsNPY19PbP3/a9GS5k1tbA9PrugbPmv5SCbKagKgJtZ4FI/f98eTqg/42AdGTszP9", + "ZcKb9vPLM7SXTm3cc2TAHSf1PwY0fgGJgXsT7v34dZC3Z+6p9PaFO4IQE3MjqP4gRTNgfyTtWObgoN6G", + "1g1eJpZRTL7uHJacyhKB1xlk5J1aOgr1iIEwKjkO5ebberPP1fsEr7fz9ix3rWunK/s8DY5OHQ09IRqq", + "hc1WUTFttFnTYtPYvNHmyczt+jU4/ykYbdxkExACL3qZ2S4ftT7WC/fAbR5BlyjJPhNWw8FufNd/ZMhY", + "YFC7RNajGarww/Ucge4stfoY3bB8t7ZMt/LJVq/h3hedNogI59Rr1sZdABTKlzMmJ0Mdu0xCr2CeiR7W", + "WVA5oDUQuBmKZarjIZ2f+Y5Fqy7Bqxfy3rv3sD17O3K7phs7JaqcrTowvrB1SwVcln/nvV29gGsRF929", + "Fk0rCZ9A4ghLPPhQ3Evg3TgztWrdNuaavi8fi2+m6ep7NVr03vS579U6Tr7cnSkplQmqi1bMNsrhb6BK", + "1bsfRpxm+6NXbOapFqZCIe8oKVhfdOqgeEvubJO5ZIetI7R3U21zPdlmmTxba9XdpeuAsnKtWd/hya+4", + "c2WKUZN9X71MjGlkb8AcFPfL9UajK0lVAtlOgVWmI7r4BnaDW9yQoQBi6k9C8iyU+maiosDYtTFYj1AE", + "KVBzC5Wam1QKBgUCo2BzG6pE730j0ajTL/0V6nVuLgzgeRL1C/9eNgrqRoqWRK2Z/baEzWZpFTFJiByc", + "7DkgKbC/U1vB2ndI8/la8d9vmsVgS7rk4b6GDAtLuBZzessrrIUjP81pP0fg7KFMhkE+5uBs5FpuK1bZ", + "wuMh0B3TuGmMxd6enlyeTPxg7GRy8g8vHHvnfnBR285o7Si4KW7RbaTcKvfQt50cLFz62xAd4K2EsBlp", + "62DW0t7kxW+mE7egslph2que2r9WKfhwK2HTdb5+ZuhUaT3Rl8M3YtsuBeNBUDB2/2Cpq8AzyGLURQce", + "lkyAKcyiDDGxxOYKZ6qcEn4PEYqZEDEIEa9QlJmiavqidJhJdg/8MKiDpxTY3u0FzO5tdMjO1XZSv5Gp", + "5iks09+bpD2sM3GXdjbTWXu9orbVRNqWFONaOuXxtaGHBCSOuAG333FNEcirYbJhZ+m6jAJhVCxJWp8W", + "MiQSWeW58tF4aahGChaZiUP2oDhmDxObhXXuEjrXD0p0u1uI55eMpQ1tEvx4KyEVPaIWJdW6DsLafHVI", + "+5fb24MCrUckmx6ClFPxa+Brji50nXRwkHyF++US9QhN1FYB6CJWeau5NXVjoByjgIRIWyDgkrHkJ4HM", + "PRVTtc80TnCa2pCFUxPdoYq+NQ0GhlxvvVs074iOXOwqRNZ7y2rebVou00/xbKM4x2aZTj3TxNj2r88p", + "m6qfGBB/f/aIaGAvRmqgZiV3aSBTQBxPgEZgb6cV2URFzPzL5nRNCaVNeYztBFrD9LfS/aM9eCr2eG+A", + "n+IOBJ9qKy7kNwBbT2OqJyvr1/o6Uvbypt2+UUHSuiVeuwrYeXTRqvpq9Ff34YZOneo/idItm4Rb8tXx", + "l93OvSZ1o+DxYMEO3CTuguj9z4e/eTfQ8jYHJEmZyfP0XkawXdQ2onMPggWRy2x2GLJkbOrNjGPGkjGx", + "BbvyyjNjv/eTrodD6JxpKTKaKVD7mdnGTm4ulP5w7B+8OTw6PLI3/SlOSXAc/HJ4dPiLBUPTd4xTMr5/", + "M1bcN+c4gXE1XC/G30n0pNouQOOV351XCjT4AHKie5y7DkHlpYKfj472Uf/1NtOl0uZZnFeo1dWL3h69", + "2cf0lWpZRW29kauPZaoa5zWW3GUeXYJWLr0SYT8JV1PMK+n29uiXJuhyao+rBUt1v7d9++X11HSxpCxJ", + "MF/Z+p22TBZKrKLSQGOUc81ambNDe50+f6fk99p3MUj74x5VC+TL06jEsQKkGH+3eadP4yJVTIy/Fx8+", + "81bLaJn4wzwmi6VEmQCBHjiRUK4chmYQ4kwAIhJFCp+EUNVyCfZkSmQzoexkKr1CW64KVYJXpqAWRId+", + "6QknPmcGkeKhkluJZaYLU5TpV7eGRZPx2jswT19eRhKnS8hfAoh8gpiijMrAdqQjuuamEYfM1Rt7a8Bs", + "Z9n1upuFrPfqWaqt11fA1ipG9pWwasnCp1Hw1wFolis56s6/DME0L8ZXI9o41zbFUv0kvBXUXKzXrk6q", + "O7jSvYvTdJ7qNfWe6lGsmzJRs9+ccuV9r8lMkHuqzsH2uDyvhDqeM54cuByWgtHXb7dW3jKq977nwO+K", + "uqTNVRpKFk0e7poRivUTSp05IOUc5Ka8++7kAA3LyHuMyBt43d0o99a3+F/bzv4n0xN/690xr7yr+r35", + "6wBga2o0qjF+/nnAGH5ly9ehqD6brRQXRY1rlJap8riBfTC2QgzVx9V2pt5G32tfUcvtqbvckLobaCI1", + "jDzwfbZG9XtiKVWvgH/ohj3rhlcgmo4jEPZNPTyXwD3/wCuzWxZTK5y+mDrnVzQ6nZdEyPO81TM5bytJ", + "HkPd0d2xV40P2ItL6l6KKK+0onqR2md+JyCsL+i/OrGhDu0w+s6LuM1WPKMmc3FLiqrLdnrz6vXj2vM0", + "L8S/vbRj5RGavkZP3asV2xMZw7gImyesuLYziicq+mvC8ffiOdTWWJwnI68wCLdnrtkg8rW1lTeuNc3X", + "+ieB7OrzjEqSACquS+/WrfZe2i0F0Hrz29gvfz72LvJ2MeG7/DLmznixVAKvrl68zk6Q3mMwmJrXNYn3", + "TqouEpKf+u6HY2teUe3LtOsvZ/ZVkuvv0vbl+IY3R/uakbVPLur1+rPwfVgqD93F+nVVpXcoBnXT1apn", + "91JC8W5pUQpZi8bnC+TqH7+kHDyLJ/9UbOWV/98PzI2ufZrGq2odxKKw9ys1uWtfa9hz7LL+kYG63QhT", + "RkmIY/fAjH1ZBCJ/g9KuM6YIS5aQ0D2OimZYhsv+xnvdA/Evua9tujv1suIbXrt94c3t+drBFdt/ceVg", + "r+uU3IvtC3PlNtmexbh6l6tGgN9VntBmc8Qo5G83m3SuP6+M7t/2fPtmCLjV59Ofrx7e/jwE8urb52qA", + "vw1CPSZA5WnMBPgL/nJKSnUesuofsIQHvJqSBFgmt6Tk7DsiL6/kDCCvP85YuWG2bz1ZuUtWlw1lDwUi", + "/11pc+/oh3b8Yfv0UgvFS0Ivrhjy+1Hrka3XehJRvdG1Zx1RLTLY+sivM5/co8tozllicyJxKL1UJH1B", + "64cG+aFBemkQ/+GyF9ch5j0O1/S0VNd+Bwqg4d2TfauBhtdI6mMjpimKYK5fHGNUIF1tltAFwoiyyETv", + "dWrL/79AyOuVxca8qvwB3ZDROVnsLb2qy8KfQMoEkYzn8c5TA2B9zvrzcqlcRtbSPE2bD/PfB5eMJQem", + "WPTBKUsSIodlw+9Gc/gvcf5LpID+SGPY4DC7p8Jw8zZlh1k7UOtt6+cjjHgufwfsgfpeYh4Gz3MjWByz", + "thO3E93iFtNoxh4bztjacfg4/XT5fJ7Y5BJAJWOARmCuAhmc0AeO0+XfL5EwyFmSLNSvf8TFlS2tVmvV", + "nbmTpZM1dUs7YPAKsp9ev9qwxNqi3ugjyHbWzSTZdm7LS7EsUbq85fis8l5+zmn63zYZtAPcxHi14Jq1", + "/zXk0BEmzREzqTptwvZDxn7IWC5jWnIQ4+j9x4tJo5wtAceyVcA+mhava4WtgbDzm7XI8TkyhLLVPIGG", + "K0QEokwiDjhaHa4Jcsq4RCEOl7ouKDdmSKH3zHB2EWJyD9/a1uCS3AMFIV7VKtQhbLkaxRZg9EDkkmUS", + "hUsIvypnOicg0Vk+Cn1NwW/t17xxRF4dAfbGhmcezRDm0Ml37o5rzr1RdYS8t34zgN87pzPjcXAcjIOn", + "L0//FwAA//8=", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/go.sum b/go.sum index d2b41bf4..97c20d35 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvA github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofiber/fiber/v3 v3.4.0 h1:F0aND4vwZF7dR7cbvSwFQQEpBU902XHKWxrLsFBkVqw= github.com/gofiber/fiber/v3 v3.4.0/go.mod h1:nAhJfdxUIJJph2tPWPmqWf8QDIN2iiqQiQf3lENZpdk= github.com/gofiber/schema v1.8.3 h1:06ZedxIYjngzc0095PYy7uWnFnbRflWFpikvZH61fDc= @@ -141,6 +143,8 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= +github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s= @@ -161,6 +165,8 @@ golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho= golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -169,6 +175,8 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/acceptance/fixture.go b/internal/acceptance/fixture.go new file mode 100644 index 00000000..4b5d3b61 --- /dev/null +++ b/internal/acceptance/fixture.go @@ -0,0 +1,762 @@ +package acceptance + +import ( + "bufio" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +const ( + DefaultFHIRBase = "https://google-fhir.fhir-aggregator.org" + DefaultStudyID = "638f6162-e000-5167-8216-962d86b74a98" + DefaultPatientLimit = 100 + DefaultResourceLimit = 50000 + DefaultBytesLimit int64 = 512 << 20 + DefaultPageLimit = 250 + DefaultFetchTimeout = 5 * time.Minute +) + +// FixtureDigest identifies both a fixture lock and its content-addressed cache directory. +type FixtureDigest string + +type RunID string + +type ResourceLock struct { + ID string `json:"id"` + Version string `json:"version"` + Digest string `json:"digest"` +} + +type SelectionRule struct { + ResourceType string `json:"resource_type"` + Search string `json:"search"` + Order string `json:"order"` + Limit int `json:"limit"` +} + +// FixtureLock is intentionally metadata-only. Resources contains the exact +// IDs, upstream version IDs, and canonical payload digests needed to detect +// drift without redistributing potentially restricted FHIR payloads. +type FixtureLock struct { + Version int `json:"version"` + Endpoint string `json:"endpoint"` + Study ResourceLock `json:"study"` + Selection SelectionRule `json:"selection"` + Resources map[string][]ResourceLock `json:"resources"` + Counts map[string]int `json:"counts"` + Format string `json:"format"` + Digest FixtureDigest `json:"lock_digest"` +} + +type Limits struct { + Patients int + Resources int + Bytes int64 + Pages int + Timeout time.Duration +} + +func DefaultLimits() Limits { + return Limits{Patients: DefaultPatientLimit, Resources: DefaultResourceLimit, Bytes: DefaultBytesLimit, Pages: DefaultPageLimit, Timeout: DefaultFetchTimeout} +} + +func (l Limits) normalize() Limits { + d := DefaultLimits() + if l.Patients <= 0 { + l.Patients = d.Patients + } + if l.Resources <= 0 { + l.Resources = d.Resources + } + if l.Bytes <= 0 { + l.Bytes = d.Bytes + } + if l.Pages <= 0 { + l.Pages = d.Pages + } + if l.Timeout <= 0 { + l.Timeout = d.Timeout + } + return l +} + +type ValidatedFixture struct { + Digest FixtureDigest + MetaDir string + Counts map[string]int +} + +type Fetcher struct { + HTTPClient *http.Client + CacheDir string + Lock FixtureLock + Limits Limits + mu sync.Mutex +} + +var ( + ErrFixtureDrift = errors.New("upstream fixture drift") + ErrFixtureCap = errors.New("fixture cap exceeded") + ErrFixtureLockInvalid = errors.New("invalid fixture lock") +) + +func LoadLock(path string) (FixtureLock, error) { + raw, err := os.ReadFile(path) + if err != nil { + return FixtureLock{}, fmt.Errorf("read fixture lock: %w", err) + } + var lock FixtureLock + dec := json.NewDecoder(strings.NewReader(string(raw))) + dec.DisallowUnknownFields() + if err := dec.Decode(&lock); err != nil { + return FixtureLock{}, fmt.Errorf("decode fixture lock: %w", err) + } + var extra any + if err := dec.Decode(&extra); err != io.EOF { + return FixtureLock{}, fmt.Errorf("fixture lock has trailing JSON") + } + if err := lock.Validate(); err != nil { + return FixtureLock{}, err + } + return lock, nil +} + +func (l FixtureLock) Validate() error { + if l.Version != 1 { + return fmt.Errorf("%w: unsupported version %d", ErrFixtureLockInvalid, l.Version) + } + if strings.TrimSpace(l.Endpoint) == "" || strings.TrimSpace(l.Study.ID) == "" || strings.TrimSpace(l.Study.Version) == "" || !strings.HasPrefix(l.Study.Digest, "sha256:") { + return fmt.Errorf("%w: endpoint and study identity are required", ErrFixtureLockInvalid) + } + if l.Selection.ResourceType != "Patient" || l.Selection.Limit <= 0 || strings.TrimSpace(l.Selection.Search) == "" { + return fmt.Errorf("%w: patient selection rule is incomplete", ErrFixtureLockInvalid) + } + if l.Format != "canonical-ndjson-v1" { + return fmt.Errorf("%w: unsupported fixture format %q", ErrFixtureLockInvalid, l.Format) + } + if len(l.Resources) == 0 || len(l.Counts) == 0 { + return fmt.Errorf("%w: resources and counts are required", ErrFixtureLockInvalid) + } + for typ, resources := range l.Resources { + if typ == "" || l.Counts[typ] != len(resources) { + return fmt.Errorf("%w: %s count does not match resource lock", ErrFixtureLockInvalid, typ) + } + seen := make(map[string]bool, len(resources)) + for _, resource := range resources { + if strings.TrimSpace(resource.ID) == "" || strings.TrimSpace(resource.Version) == "" || !strings.HasPrefix(resource.Digest, "sha256:") { + return fmt.Errorf("%w: %s contains incomplete resource identity", ErrFixtureLockInvalid, typ) + } + if seen[resource.ID] { + return fmt.Errorf("%w: %s contains duplicate id %q", ErrFixtureLockInvalid, typ, resource.ID) + } + seen[resource.ID] = true + } + } + if l.Counts["Patient"] != l.Selection.Limit { + return fmt.Errorf("%w: patient count %d != selection limit %d", ErrFixtureLockInvalid, l.Counts["Patient"], l.Selection.Limit) + } + if strings.TrimSpace(string(l.Digest)) == "" { + return fmt.Errorf("%w: lock_digest is required", ErrFixtureLockInvalid) + } + if got := l.ComputedDigest(); got != l.Digest { + return fmt.Errorf("%w: lock_digest %q != %q", ErrFixtureLockInvalid, l.Digest, got) + } + return nil +} + +func (l FixtureLock) ComputedDigest() FixtureDigest { + copy := l + copy.Digest = "" + raw, _ := json.Marshal(copy) + sum := sha256.Sum256(raw) + return FixtureDigest("sha256:" + hex.EncodeToString(sum[:])) +} + +func (f *Fetcher) Acquire(ctx context.Context) (ValidatedFixture, error) { + if f == nil { + return ValidatedFixture{}, errors.New("fixture fetcher is nil") + } + if err := f.Lock.Validate(); err != nil { + return ValidatedFixture{}, err + } + if strings.TrimSpace(f.CacheDir) == "" { + return ValidatedFixture{}, errors.New("fixture cache directory is required") + } + limits := f.Limits.normalize() + digest := f.Lock.Digest + cache := filepath.Join(f.CacheDir, strings.TrimPrefix(string(digest), "sha256:")) + f.mu.Lock() + defer f.mu.Unlock() + if fixture, err := validateCache(cache, f.Lock, limits); err == nil { + return fixture, nil + } + if err := os.MkdirAll(f.CacheDir, 0o755); err != nil { + return ValidatedFixture{}, fmt.Errorf("create fixture cache: %w", err) + } + // Every process writes a unique temporary directory. The lock is held only + // in-process; the rename is atomic across processes and an already-created + // valid directory wins a race harmlessly. + tmp, err := os.MkdirTemp(f.CacheDir, ".fixture-") + if err != nil { + return ValidatedFixture{}, fmt.Errorf("create fixture staging directory: %w", err) + } + defer os.RemoveAll(tmp) + if err := f.fetchInto(ctx, tmp, limits); err != nil { + return ValidatedFixture{}, err + } + if err := writeCacheMetadata(tmp, f.Lock); err != nil { + return ValidatedFixture{}, err + } + validated, err := validateCache(tmp, f.Lock, limits) + if err != nil { + return ValidatedFixture{}, err + } + if existing, err := validateCache(cache, f.Lock, limits); err == nil { + return existing, nil + } + if err := os.Rename(tmp, cache); err != nil { + if existing, checkErr := validateCache(cache, f.Lock, limits); checkErr == nil { + return existing, nil + } + return ValidatedFixture{}, fmt.Errorf("commit fixture cache: %w", err) + } + return ValidatedFixture{Digest: digest, MetaDir: filepath.Join(cache, "meta"), Counts: validated.Counts}, nil +} + +func validateCache(cache string, lock FixtureLock, limits Limits) (ValidatedFixture, error) { + if lock.Selection.Limit > limits.Patients { + return ValidatedFixture{}, fmt.Errorf("%w: patients=%d want at most %d", ErrFixtureCap, lock.Selection.Limit, limits.Patients) + } + meta := filepath.Join(cache, "meta") + if _, err := os.Stat(meta); err != nil { + return ValidatedFixture{}, err + } + marker, err := os.ReadFile(filepath.Join(cache, "fixture.json")) + if err != nil { + return ValidatedFixture{}, err + } + var got struct { + Digest FixtureDigest `json:"digest"` + Counts map[string]int `json:"counts"` + } + if err := json.Unmarshal(marker, &got); err != nil || got.Digest != lock.Digest { + return ValidatedFixture{}, fmt.Errorf("fixture cache marker mismatch: digest=%q want=%q err=%v", got.Digest, lock.Digest, err) + } + if !sameCounts(got.Counts, lock.Counts) { + return ValidatedFixture{}, fmt.Errorf("fixture cache marker counts mismatch: got=%v want=%v", got.Counts, lock.Counts) + } + total, totalBytes := 0, int64(0) + for typ, want := range lock.Counts { + path := filepath.Join(meta, typ+".ndjson") + count, bytes, err := validateCanonicalNDJSON(path, typ, lock.Resources[typ], &total, &totalBytes, limits) + if err != nil || count != want { + return ValidatedFixture{}, fmt.Errorf("fixture cache contents mismatch for %s: count=%d want=%d bytes=%d err=%v", typ, count, want, bytes, err) + } + } + entries, err := os.ReadDir(meta) + if err != nil { + return ValidatedFixture{}, err + } + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".ndjson") { + continue + } + typ := strings.TrimSuffix(name, ".ndjson") + if _, ok := lock.Counts[typ]; !ok { + return ValidatedFixture{}, fmt.Errorf("%w: unexpected cached resource type %s", ErrFixtureDrift, typ) + } + } + return ValidatedFixture{Digest: lock.Digest, MetaDir: meta, Counts: lock.Counts}, nil +} + +func validateCanonicalNDJSON(path, typ string, locks []ResourceLock, total *int, totalBytes *int64, limits Limits) (int, int64, error) { + file, err := os.Open(path) + if err != nil { + return 0, 0, err + } + defer file.Close() + byID := make(map[string]ResourceLock, len(locks)) + for _, lock := range locks { + byID[lock.ID] = lock + } + seen := make(map[string]bool, len(locks)) + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 64<<10), 64<<20) + count := 0 + var bytes int64 + for scanner.Scan() { + line := scanner.Text() + if line == "" { + return count, bytes, fmt.Errorf("%w: %s contains a blank NDJSON line", ErrFixtureDrift, typ) + } + canonical := canonicalJSON([]byte(line)) + if string(canonical) != line { + return count, bytes, fmt.Errorf("%w: %s contains non-canonical NDJSON", ErrFixtureDrift, typ) + } + var value struct { + ID string `json:"id"` + } + if err := json.Unmarshal(canonical, &value); err != nil { + return count, bytes, fmt.Errorf("%w: decode %s cache line: %v", ErrFixtureDrift, typ, err) + } + lock, ok := byID[value.ID] + if !ok { + return count, bytes, fmt.Errorf("%w: unexpected %s/%s in cache", ErrFixtureDrift, typ, value.ID) + } + if seen[value.ID] { + return count, bytes, fmt.Errorf("%w: duplicate %s/%s in cache", ErrFixtureDrift, typ, value.ID) + } + if err := (&Fetcher{}).checkLocked(typ, canonical, lock); err != nil { + return count, bytes, err + } + seen[value.ID] = true + lineBytes := int64(len(line)) + 1 + count++ + bytes += lineBytes + *total++ + *totalBytes += lineBytes + if *total > limits.Resources || *totalBytes > limits.Bytes { + return count, bytes, fmt.Errorf("%w: resources=%d bytes=%d", ErrFixtureCap, *total, *totalBytes) + } + } + if err := scanner.Err(); err != nil { + return count, bytes, err + } + if len(seen) != len(locks) { + return count, bytes, fmt.Errorf("%w: %s cache IDs=%d want=%d", ErrFixtureDrift, typ, len(seen), len(locks)) + } + return count, bytes, nil +} + +func sameCounts(got, want map[string]int) bool { + if len(got) != len(want) { + return false + } + for typ, count := range want { + if got[typ] != count { + return false + } + } + return true +} + +func writeCacheMetadata(dir string, lock FixtureLock) error { + marker := struct { + Digest FixtureDigest `json:"digest"` + Counts map[string]int `json:"counts"` + }{lock.Digest, lock.Counts} + raw, _ := json.Marshal(marker) + return os.WriteFile(filepath.Join(dir, "fixture.json"), append(raw, '\n'), 0o644) +} + +func (f *Fetcher) fetchInto(ctx context.Context, dir string, limits Limits) error { + ctx, cancel := context.WithTimeout(ctx, limits.Timeout) + defer cancel() + if f.HTTPClient == nil { + f.HTTPClient = &http.Client{Timeout: 30 * time.Second} + } + endpoint := strings.TrimRight(f.Lock.Endpoint, "/") + resources := make(map[string]map[string]json.RawMessage) + add := func(typ string, payload json.RawMessage) error { + if len(resources) == 0 { + resources = make(map[string]map[string]json.RawMessage) + } + var value map[string]any + if err := json.Unmarshal(payload, &value); err != nil { + return fmt.Errorf("decode %s resource: %w", typ, err) + } + id, _ := value["id"].(string) + if strings.TrimSpace(id) == "" { + return fmt.Errorf("%s resource is missing id", typ) + } + if actual, _ := value["resourceType"].(string); actual != "" && actual != typ { + return fmt.Errorf("resource %s/%s declares type %s", typ, id, actual) + } + if resources[typ] == nil { + resources[typ] = map[string]json.RawMessage{} + } + if _, exists := resources[typ][id]; exists { + return nil + } + resources[typ][id] = canonicalJSON(payload) + return nil + } + study, err := f.getResource(ctx, endpoint, "ResearchStudy", f.Lock.Study.ID) + if err != nil { + return err + } + if err := f.checkLocked("ResearchStudy", study, f.Lock.Study); err != nil { + return err + } + if err := add("ResearchStudy", study); err != nil { + return err + } + patients, err := f.search(ctx, endpoint, "Patient", url.Values{"part-of-study": {"ResearchStudy/" + f.Lock.Study.ID}, "_count": {strconv.Itoa(minInt(limits.Patients, 1000))}, "_total": {"accurate"}}, limits.Pages) + if err != nil { + return err + } + sort.Slice(patients, func(i, j int) bool { return resourceID(patients[i]) < resourceID(patients[j]) }) + if len(patients) < f.Lock.Selection.Limit { + return fmt.Errorf("%w: upstream returned %d patients, need %d", ErrFixtureDrift, len(patients), f.Lock.Selection.Limit) + } + selected := make(map[string]bool, f.Lock.Selection.Limit) + for _, patient := range patients[:f.Lock.Selection.Limit] { + selected[resourceID(patient)] = true + if err := add("Patient", patient); err != nil { + return err + } + } + for typ, parameter := range map[string]string{"Specimen": "subject", "Observation": "subject", "DocumentReference": "subject", "Condition": "subject"} { + ids := sortedKeys(selected) + for start := 0; start < len(ids); start += 25 { + end := minInt(start+25, len(ids)) + refs := make([]string, end-start) + for i := range refs { + refs[i] = "Patient/" + ids[start+i] + } + values := url.Values{parameter: {strings.Join(refs, ",")}, "_count": {"1000"}, "_total": {"accurate"}} + items, err := f.search(ctx, endpoint, typ, values, limits.Pages) + if err != nil { + return err + } + for _, item := range items { + if err := add(typ, item); err != nil { + return err + } + } + } + } + if err := f.writeCanonical(dir, resources, limits); err != nil { + return err + } + return nil +} + +func (f *Fetcher) checkLocked(typ string, payload json.RawMessage, expected ResourceLock) error { + canonical := canonicalJSON(payload) + sum := sha256.Sum256(canonical) + got := "sha256:" + hex.EncodeToString(sum[:]) + var value struct { + ID string `json:"id"` + Meta struct { + VersionID string `json:"versionId"` + } `json:"meta"` + } + if err := json.Unmarshal(canonical, &value); err != nil { + return err + } + if value.ID != expected.ID || value.Meta.VersionID != expected.Version || got != expected.Digest { + return fmt.Errorf("%w: %s/%s expected version=%s digest=%s got version=%s digest=%s", ErrFixtureDrift, typ, expected.ID, expected.Version, expected.Digest, value.Meta.VersionID, got) + } + return nil +} + +func (f *Fetcher) writeCanonical(dir string, resources map[string]map[string]json.RawMessage, limits Limits) error { + if len(resources) == 0 { + return errors.New("fixture contains no resources") + } + total, totalBytes := 0, int64(0) + for typ, locks := range f.Lock.Resources { + values := resources[typ] + if values == nil { + values = map[string]json.RawMessage{} + } + if len(values) != len(locks) { + return fmt.Errorf("%w: %s count expected %d got %d", ErrFixtureDrift, typ, len(locks), len(values)) + } + byID := make(map[string]ResourceLock, len(locks)) + for _, lock := range locks { + byID[lock.ID] = lock + } + ids := sortedRawKeys(values) + var payloadBytes int64 + for _, id := range ids { + lock, exists := byID[id] + if !exists { + return fmt.Errorf("%w: unexpected %s/%s", ErrFixtureDrift, typ, id) + } + if err := f.checkLocked(typ, values[id], lock); err != nil { + return err + } + payloadBytes += int64(len(values[id])) + 1 + total++ + totalBytes += int64(len(values[id])) + 1 + if total > limits.Resources || totalBytes > limits.Bytes { + return fmt.Errorf("%w: resource=%d bytes=%d", ErrFixtureCap, total, totalBytes) + } + } + if err := os.MkdirAll(filepath.Join(dir, "meta"), 0o755); err != nil { + return err + } + file, err := os.OpenFile(filepath.Join(dir, "meta", typ+".ndjson"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) + if err != nil { + return err + } + w := bufio.NewWriter(file) + for _, id := range ids { + if _, err := w.Write(append(values[id], '\n')); err != nil { + _ = file.Close() + return err + } + } + if err := w.Flush(); err != nil { + _ = file.Close() + return err + } + if err := file.Close(); err != nil { + return err + } + } + for typ := range resources { + if _, ok := f.Lock.Resources[typ]; !ok { + return fmt.Errorf("%w: lock has no %s resources", ErrFixtureDrift, typ) + } + } + if total > limits.Resources { + return fmt.Errorf("%w: resources=%d", ErrFixtureCap, total) + } + return nil +} + +func (f *Fetcher) getResource(ctx context.Context, endpoint, typ, id string) (json.RawMessage, error) { + u := strings.TrimRight(endpoint, "/") + "/" + url.PathEscape(typ) + "/" + url.PathEscape(id) + return f.request(ctx, u) +} + +func (f *Fetcher) search(ctx context.Context, endpoint, typ string, values url.Values, pageLimit int) ([]json.RawMessage, error) { + u := strings.TrimRight(endpoint, "/") + "/" + url.PathEscape(typ) + "?" + values.Encode() + var all []json.RawMessage + for page := 0; page < pageLimit; page++ { + body, err := f.request(ctx, u) + if err != nil { + return nil, err + } + var bundle struct { + Entry []struct { + Resource json.RawMessage `json:"resource"` + } `json:"entry"` + Link []struct { + Relation string `json:"relation"` + URL string `json:"url"` + } `json:"link"` + } + if err := json.Unmarshal(body, &bundle); err != nil { + return nil, fmt.Errorf("decode %s Bundle: %w", typ, err) + } + for _, entry := range bundle.Entry { + if len(entry.Resource) > 0 { + all = append(all, canonicalJSON(entry.Resource)) + } + } + next := "" + for _, link := range bundle.Link { + if link.Relation == "next" { + next = link.URL + break + } + } + if next == "" { + return all, nil + } + u = next + } + return nil, fmt.Errorf("%w: %s exceeded %d result pages", ErrFixtureCap, typ, pageLimit) +} + +func (f *Fetcher) request(ctx context.Context, target string) (json.RawMessage, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/fhir+json") + resp, err := f.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("FHIR request: %w", err) + } + defer resp.Body.Close() + body, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<20)) + if readErr != nil { + return nil, readErr + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("FHIR request %s: HTTP %s: %s", target, resp.Status, strings.TrimSpace(string(body))) + } + return body, nil +} + +func canonicalJSON(raw []byte) []byte { + var value any + if json.Unmarshal(raw, &value) != nil { + return append([]byte(nil), raw...) + } + out, _ := json.Marshal(value) + return out +} +func canonicalFileStats(path string) (int, int64, error) { + file, err := os.Open(path) + if err != nil { + return 0, 0, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 64<<10), 64<<20) + count := 0 + var bytes int64 + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + canonical := canonicalJSON([]byte(line)) + if string(canonical) != line { + return 0, 0, errors.New("non-canonical NDJSON") + } + count++ + bytes += int64(len(line)) + 1 + } + return count, bytes, scanner.Err() +} +func resourceID(raw []byte) string { + var value struct { + ID string `json:"id"` + } + _ = json.Unmarshal(raw, &value) + return value.ID +} +func sortedKeys(values map[string]bool) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} +func sortedRawKeys(values map[string]json.RawMessage) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// RefreshLock discovers a new metadata-only lock. It is intentionally an +// explicit operation: normal acceptance runs call LoadLock and never rewrite +// the checked-in oracle or lock after an upstream response changes. +func RefreshLock(ctx context.Context, endpoint, studyID, path string, client *http.Client, limits Limits) (FixtureLock, error) { + limits = limits.normalize() + endpoint = strings.TrimRight(strings.TrimSpace(endpoint), "/") + if endpoint == "" || strings.TrimSpace(studyID) == "" || strings.TrimSpace(path) == "" { + return FixtureLock{}, errors.New("fixture refresh requires endpoint, study ID, and output path") + } + f := &Fetcher{HTTPClient: client, CacheDir: filepath.Dir(path), Limits: limits} + if f.HTTPClient == nil { + f.HTTPClient = &http.Client{Timeout: 30 * time.Second} + } + study, err := f.getResource(ctx, endpoint, "ResearchStudy", studyID) + if err != nil { + return FixtureLock{}, err + } + resources := map[string][]json.RawMessage{"ResearchStudy": {canonicalJSON(study)}} + patients, err := f.search(ctx, endpoint, "Patient", url.Values{"part-of-study": {"ResearchStudy/" + studyID}, "_count": {strconv.Itoa(minInt(limits.Patients, 1000))}, "_total": {"accurate"}}, limits.Pages) + if err != nil { + return FixtureLock{}, err + } + sort.Slice(patients, func(i, j int) bool { return resourceID(patients[i]) < resourceID(patients[j]) }) + if len(patients) < limits.Patients { + return FixtureLock{}, fmt.Errorf("%w: upstream returned %d patients, need %d", ErrFixtureDrift, len(patients), limits.Patients) + } + resources["Patient"] = append(resources["Patient"], patients[:limits.Patients]...) + ids := make([]string, limits.Patients) + for i := range ids { + ids[i] = resourceID(patients[i]) + } + sort.Strings(ids) + for typ, parameter := range map[string]string{"Specimen": "subject", "Observation": "subject", "DocumentReference": "subject", "Condition": "subject"} { + if resources[typ] == nil { + resources[typ] = []json.RawMessage{} + } + for start := 0; start < len(ids); start += 25 { + end := minInt(start+25, len(ids)) + refs := make([]string, end-start) + for i := range refs { + refs[i] = "Patient/" + ids[start+i] + } + values := url.Values{parameter: {strings.Join(refs, ",")}, "_count": {"1000"}, "_total": {"accurate"}} + items, err := f.search(ctx, endpoint, typ, values, limits.Pages) + if err != nil { + return FixtureLock{}, err + } + resources[typ] = append(resources[typ], items...) + } + } + lock := FixtureLock{Version: 1, Endpoint: endpoint, Study: identityFor("ResearchStudy", study), Selection: SelectionRule{ResourceType: "Patient", Search: "part-of-study=ResearchStudy/" + studyID, Order: "id ascending", Limit: limits.Patients}, Resources: make(map[string][]ResourceLock, len(resources)), Counts: make(map[string]int, len(resources)), Format: "canonical-ndjson-v1"} + for typ, items := range resources { + lock.Resources[typ] = []ResourceLock{} + seen := map[string]bool{} + for _, item := range items { + identity := identityFor(typ, item) + if identity.ID == "" || seen[identity.ID] { + continue + } + seen[identity.ID] = true + lock.Resources[typ] = append(lock.Resources[typ], identity) + } + sort.Slice(lock.Resources[typ], func(i, j int) bool { return lock.Resources[typ][i].ID < lock.Resources[typ][j].ID }) + lock.Counts[typ] = len(lock.Resources[typ]) + } + lock.Digest = lock.ComputedDigest() + if err := WriteLock(path, lock); err != nil { + return FixtureLock{}, err + } + return lock, nil +} + +func identityFor(typ string, payload []byte) ResourceLock { + canonical := canonicalJSON(payload) + var value struct { + ID string `json:"id"` + Meta struct { + VersionID string `json:"versionId"` + } `json:"meta"` + } + _ = json.Unmarshal(canonical, &value) + sum := sha256.Sum256(canonical) + return ResourceLock{ID: value.ID, Version: value.Meta.VersionID, Digest: "sha256:" + hex.EncodeToString(sum[:])} +} + +func WriteLock(path string, lock FixtureLock) error { + if err := lock.Validate(); err != nil { + return err + } + raw, err := json.MarshalIndent(lock, "", " ") + if err != nil { + return err + } + tmp := path + ".tmp" + if err := os.WriteFile(tmp, append(raw, '\n'), 0o644); err != nil { + return err + } + return os.Rename(tmp, path) +} diff --git a/internal/acceptance/fixture_test.go b/internal/acceptance/fixture_test.go new file mode 100644 index 00000000..2ddc6e00 --- /dev/null +++ b/internal/acceptance/fixture_test.go @@ -0,0 +1,341 @@ +package acceptance + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" +) + +func testResource(typ, id, version string, extra map[string]any) []byte { + value := map[string]any{"resourceType": typ, "id": id, "meta": map[string]any{"versionId": version}} + for key, item := range extra { + value[key] = item + } + raw, _ := json.Marshal(value) + return canonicalJSON(raw) +} + +func lockResource(typ, id, version string, payload []byte) ResourceLock { + sum := sha256.Sum256(canonicalJSON(payload)) + return ResourceLock{ID: id, Version: version, Digest: "sha256:" + hex.EncodeToString(sum[:])} +} + +func testLock(endpoint string, patientIDs []string, resources map[string][]ResourceLock) FixtureLock { + counts := make(map[string]int, len(resources)) + for typ, values := range resources { + counts[typ] = len(values) + } + lock := FixtureLock{Version: 1, Endpoint: endpoint, Study: ResourceLock{ID: "study-1", Version: "v1", Digest: "sha256:" + strings.Repeat("0", 64)}, Selection: SelectionRule{ResourceType: "Patient", Search: "part-of-study=ResearchStudy/study-1", Order: "id ascending", Limit: len(patientIDs)}, Resources: resources, Counts: counts, Format: "canonical-ndjson-v1"} + if studies := resources["ResearchStudy"]; len(studies) == 1 { + lock.Study = studies[0] + } + lock.Digest = lock.ComputedDigest() + return lock +} + +type localTestServer struct { + URL string + *http.Server + Listener net.Listener +} + +type rejectRoundTripper func(*http.Request) (*http.Response, error) + +func (f rejectRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request) +} + +func newLocalTestServer(t *testing.T, handler http.Handler) *localTestServer { + t.Helper() + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + server := &http.Server{Handler: handler} + go func() { _ = server.Serve(listener) }() + return &localTestServer{URL: "http://" + listener.Addr().String(), Server: server, Listener: listener} +} + +func testServer(t *testing.T, patientIDs []string, byType map[string][][]byte, count *atomic.Int64) *localTestServer { + t.Helper() + return newLocalTestServer(t, fixtureHandler(byType, count)) +} + +func fixtureHandler(byType map[string][][]byte, count *atomic.Int64) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count.Add(1) + path := strings.Trim(r.URL.Path, "/") + parts := strings.Split(path, "/") + if len(parts) == 2 { + typ, id := parts[0], parts[1] + if typ == "ResearchStudy" && id == "study-1" { + w.Header().Set("Content-Type", "application/fhir+json") + _, _ = w.Write(byType[typ][0]) + return + } + } + if len(parts) != 1 { + http.NotFound(w, r) + return + } + typ := parts[0] + items := byType[typ] + if typ == "Patient" { + items = append([][]byte(nil), items...) + } + start, _ := strconv.Atoi(r.URL.Query().Get("page")) + if start < 0 { + start = 0 + } + pageSize := 2 + end := start + pageSize + if end > len(items) { + end = len(items) + } + entries := make([]map[string]any, 0, end-start) + for _, item := range items[start:end] { + var v any + _ = json.Unmarshal(item, &v) + entries = append(entries, map[string]any{"resource": v}) + } + bundle := map[string]any{"resourceType": "Bundle", "type": "searchset", "entry": entries} + if end < len(items) { + bundle["link"] = []map[string]string{{"relation": "next", "url": fmt.Sprintf("%s/%s?page=%d", countBase(r), typ, end)}} + } + w.Header().Set("Content-Type", "application/fhir+json") + _ = json.NewEncoder(w).Encode(bundle) + }) +} + +func countBase(r *http.Request) string { return "http://" + r.Host } + +func fixtureFixture(t *testing.T, serverURL string, n int) (FixtureLock, map[string][][]byte) { + patientIDs := make([]string, n) + byType := map[string][][]byte{"ResearchStudy": {testResource("ResearchStudy", "study-1", "v1", nil)}} + for i := range patientIDs { + patientIDs[i] = fmt.Sprintf("patient-%02d", n-i) + } + for _, typ := range []string{"Patient", "Specimen", "Observation", "DocumentReference", "Condition"} { + for i, patient := range patientIDs { + id := fmt.Sprintf("%s-%02d", strings.ToLower(typ), i) + extra := map[string]any{} + if typ != "Patient" { + extra["subject"] = map[string]any{"reference": "Patient/" + patient} + } + byType[typ] = append(byType[typ], testResource(typ, id, "v1", extra)) + } + } + resources := map[string][]ResourceLock{} + for typ, items := range byType { + for _, item := range items { + resources[typ] = append(resources[typ], lockResource(typ, resourceID(item), "v1", item)) + } + } + lock := testLock(serverURL, patientIDs, resources) + return lock, byType +} + +func TestFixtureColdMissWarmHitAndCanonicalNDJSON(t *testing.T) { + var requests atomic.Int64 + server := testServer(t, nil, nil, &requests) + defer server.Close() + lock, byType := fixtureFixture(t, server.URL, 4) + server.Close() + server = testServer(t, nil, byType, &requests) + defer server.Close() + lock.Endpoint = server.URL + lock.Study.Digest = lockResource("ResearchStudy", "study-1", "v1", byType["ResearchStudy"][0]).Digest + lock.Digest = lock.ComputedDigest() + cache := t.TempDir() + fetcher := &Fetcher{CacheDir: cache, Lock: lock, Limits: Limits{Patients: 4, Resources: 100, Bytes: 1 << 20, Pages: 10}} + got, err := fetcher.Acquire(context.Background()) + if err != nil { + t.Fatal(err) + } + if got.MetaDir == "" || got.Counts["Patient"] != 4 { + t.Fatalf("fixture=%#v", got) + } + coldRequests := requests.Load() + if coldRequests == 0 { + t.Fatal("cold acquire made no requests") + } + if _, err := fetcher.Acquire(context.Background()); err != nil { + t.Fatal(err) + } + if requests.Load() != coldRequests { + t.Fatalf("warm acquire made network requests: before=%d after=%d", coldRequests, requests.Load()) + } + for typ, want := range lock.Counts { + count, _, err := canonicalFileStats(filepath.Join(got.MetaDir, typ+".ndjson")) + if err != nil || count != want { + t.Fatalf("%s count=%d err=%v want=%d", typ, count, err, want) + } + } + first, _ := os.ReadFile(filepath.Join(got.MetaDir, "Patient.ndjson")) + second := append([]byte(nil), first...) + if string(first) != string(second) { + t.Fatal("canonical fixture changed between reads") + } +} + +func TestFixtureWarmCacheRejectsCorruptLockedLine(t *testing.T) { + var requests atomic.Int64 + server := testServer(t, nil, nil, &requests) + defer server.Close() + lock, byType := fixtureFixture(t, server.URL, 2) + server.Handler = fixtureHandler(byType, &requests) + fetcher := &Fetcher{CacheDir: t.TempDir(), Lock: lock, Limits: Limits{Patients: 2, Resources: 100, Bytes: 1 << 20, Pages: 10}} + fixture, err := fetcher.Acquire(context.Background()) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(fixture.MetaDir, "Patient.ndjson") + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + lineEnd := bytes.IndexByte(raw, '\n') + if lineEnd < 0 { + t.Fatal("patient fixture has no NDJSON line") + } + var value map[string]any + if err := json.Unmarshal(raw[:lineEnd], &value); err != nil { + t.Fatal(err) + } + value["id"] = "patient-corrupt" + corrupt := append(canonicalJSON(mustJSON(t, value)), raw[lineEnd:]...) + if err := os.WriteFile(path, corrupt, 0o644); err != nil { + t.Fatal(err) + } + fetcher.HTTPClient = &http.Client{Transport: rejectRoundTripper(func(*http.Request) (*http.Response, error) { + return nil, errors.New("network disabled for warm-cache verification") + })} + if _, err := fetcher.Acquire(context.Background()); err == nil || !strings.Contains(err.Error(), "network disabled for warm-cache verification") { + t.Fatalf("corrupt cache was accepted or wrong error returned: %v", err) + } +} + +func mustJSON(t *testing.T, value any) []byte { + t.Helper() + raw, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + return raw +} + +func TestFixtureLockInvalidationUsesDifferentCacheAddress(t *testing.T) { + var requests atomic.Int64 + server := testServer(t, nil, nil, &requests) + defer server.Close() + lock, byType := fixtureFixture(t, server.URL, 2) + server.Handler = fixtureHandler(byType, &requests) + fetcher := &Fetcher{CacheDir: t.TempDir(), Lock: lock, Limits: Limits{Patients: 2, Resources: 100, Bytes: 1 << 20, Pages: 10}} + if _, err := fetcher.Acquire(context.Background()); err != nil { + t.Fatal(err) + } + old := fetcher.Lock.Digest + fetcher.Lock.Endpoint = server.URL + "/different" + fetcher.Lock.Digest = fetcher.Lock.ComputedDigest() + if old == fetcher.Lock.Digest { + t.Fatal("lock invalidation did not change digest") + } +} + +func TestFixtureDriftIsRejectedAndNotCommitted(t *testing.T) { + var requests atomic.Int64 + var drift atomic.Bool + server := newLocalTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests.Add(1) + typ := strings.Trim(r.URL.Path, "/") + if typ == "ResearchStudy/study-1" { + payload := testResource("ResearchStudy", "study-1", "v2", nil) + _, _ = w.Write(payload) + return + } + if typ == "Patient" { + _, _ = fmt.Fprintf(w, `{"entry":[],"resourceType":"Bundle","type":"searchset"}`) + return + } + if drift.Load() { + _, _ = fmt.Fprintf(w, `{"entry":[],"resourceType":"Bundle","type":"searchset"}`) + return + } + _, _ = fmt.Fprintf(w, `{"entry":[],"resourceType":"Bundle","type":"searchset"}`) + })) + defer server.Close() + payload := testResource("ResearchStudy", "study-1", "v1", nil) + lock := testLock(server.URL, []string{"p1"}, map[string][]ResourceLock{"ResearchStudy": {lockResource("ResearchStudy", "study-1", "v1", payload)}, "Patient": {lockResource("Patient", "p1", "v1", testResource("Patient", "p1", "v1", nil))}}) + fetcher := &Fetcher{CacheDir: t.TempDir(), Lock: lock, Limits: Limits{Patients: 1, Resources: 10, Bytes: 1 << 20, Pages: 2}} + if _, err := fetcher.Acquire(context.Background()); !errors.Is(err, ErrFixtureDrift) { + t.Fatalf("error=%v want drift", err) + } + entries, _ := os.ReadDir(fetcher.CacheDir) + if len(entries) != 0 { + t.Fatalf("drift left cache entries: %v", entries) + } +} + +func TestFixtureConcurrentCreationIsAtomic(t *testing.T) { + var requests atomic.Int64 + server := testServer(t, nil, nil, &requests) + defer server.Close() + lock, byType := fixtureFixture(t, server.URL, 2) + server.Handler = fixtureHandler(byType, &requests) + cache := t.TempDir() + start := make(chan struct{}) + var wg sync.WaitGroup + errs := make([]error, 8) + for i := range errs { + wg.Add(1) + go func(i int) { + defer wg.Done() + <-start + f := &Fetcher{CacheDir: cache, Lock: lock, Limits: Limits{Patients: 2, Resources: 100, Bytes: 1 << 20, Pages: 10}} + _, errs[i] = f.Acquire(context.Background()) + }(i) + } + close(start) + wg.Wait() + for _, err := range errs { + if err != nil { + t.Fatal(err) + } + } + entries, _ := os.ReadDir(cache) + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), ".fixture-") { + t.Fatalf("temporary cache was left behind: %s", entry.Name()) + } + } +} + +func TestFixtureCapsFailBeforeCacheCommit(t *testing.T) { + var requests atomic.Int64 + server := testServer(t, nil, nil, &requests) + defer server.Close() + lock, byType := fixtureFixture(t, server.URL, 2) + server.Handler = fixtureHandler(byType, &requests) + lock.Digest = lock.ComputedDigest() + fetcher := &Fetcher{CacheDir: t.TempDir(), Lock: lock, Limits: Limits{Patients: 2, Resources: 1, Bytes: 1 << 20, Pages: 10}} + if _, err := fetcher.Acquire(context.Background()); !errors.Is(err, ErrFixtureCap) { + t.Fatalf("error=%v want cap", err) + } + entries, _ := os.ReadDir(fetcher.CacheDir) + if len(entries) != 0 { + t.Fatalf("cap failure left cache entries: %v", entries) + } +} diff --git a/internal/acceptance/performance.go b/internal/acceptance/performance.go new file mode 100644 index 00000000..20a8f1d1 --- /dev/null +++ b/internal/acceptance/performance.go @@ -0,0 +1,228 @@ +package acceptance + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +const ( + IngestionFloorSeconds = 5.0 + PublicationFloorSeconds = 5.0 + ProbeFloorSeconds = 0.1 +) + +// PerformanceStageNames is deliberately kept in the same order as the +// acceptance contract. These are end-to-end stages, so adding a new timed +// stage here is an explicit contract change rather than an accidental report +// field becoming a gate. +var PerformanceStageNames = []string{ + "generation_upload", + "explorer_publish", + "explorer_viewer", + "graphql", +} + +type PerformanceObservation struct { + Name string `json:"name"` + BaseSeconds float64 `json:"base_seconds"` + CurrentSeconds float64 `json:"current_seconds"` + AbsoluteFloor float64 `json:"absolute_floor"` + Ratio float64 `json:"ratio"` + Suspected bool `json:"suspected"` + RepeatBaseSeconds float64 `json:"repeat_base_seconds,omitempty"` + RepeatCurrentSeconds float64 `json:"repeat_current_seconds,omitempty"` + RepeatRatio float64 `json:"repeat_ratio,omitempty"` + RepeatSuspected bool `json:"repeat_suspected,omitempty"` + // ReverseSuspected is retained as a wire-compatible name for consumers of + // the initial comparator. It is always the result of comparing the repeat + // current run against the repeat base run when ComparePerformanceRuns is + // used. + ReverseSuspected bool `json:"reverse_suspected"` +} + +// PerformanceStageSeconds extracts only the four stages covered by the +// performance contract. A missing stage remains absent, allowing callers to +// report a malformed/unsupported base explicitly instead of treating zero as +// a very fast run. +func PerformanceStageSeconds(stages []StageReport) map[string]float64 { + allowed := make(map[string]struct{}, len(PerformanceStageNames)) + for _, name := range PerformanceStageNames { + allowed[name] = struct{}{} + } + result := make(map[string]float64, len(allowed)) + for _, stage := range stages { + if _, ok := allowed[stage.Name]; ok && stage.Seconds >= 0 { + result[stage.Name] = stage.Seconds + } + } + return result +} + +func performanceFloor(name string) (float64, bool) { + switch name { + case "generation_upload", "ingestion": + return IngestionFloorSeconds, true + case "explorer_publish", "publication": + return PublicationFloorSeconds, true + case "explorer_viewer", "graphql", "probe": + return ProbeFloorSeconds, true + default: + return 0, false + } +} + +// ComparePerformanceRuns applies the two-part regression gate. The first +// pair is the initial (ordered) base/current run. If that pair is suspected, +// callers run both variants again in the opposite order with fresh databases +// and pass that pair as repeatBase/repeatCurrent. Confirmation is therefore +// always against the repeat's base, not a value observed before the repeat. +func ComparePerformanceRuns(base, current, repeatBase, repeatCurrent map[string]float64) []PerformanceObservation { + names := map[string]bool{} + for _, name := range PerformanceStageNames { + if _, ok := base[name]; ok { + names[name] = true + } + if _, ok := current[name]; ok { + names[name] = true + } + } + ordered := make([]string, 0, len(names)) + for name := range names { + ordered = append(ordered, name) + } + sort.Strings(ordered) + result := make([]PerformanceObservation, 0, len(ordered)) + for _, name := range ordered { + floor, ok := performanceFloor(name) + if !ok { + continue + } + b, cok := base[name] + c, pok := current[name] + if !cok || !pok || b <= 0 || c < 0 { + continue + } + observation := PerformanceObservation{Name: name, BaseSeconds: b, CurrentSeconds: c, AbsoluteFloor: floor, Ratio: c / b, Suspected: c >= 2*b && c >= floor} + if repeatBaseSeconds, ok := repeatBase[name]; ok { + if repeatCurrentSeconds, ok := repeatCurrent[name]; ok && repeatBaseSeconds > 0 && repeatCurrentSeconds >= 0 { + observation.RepeatBaseSeconds = repeatBaseSeconds + observation.RepeatCurrentSeconds = repeatCurrentSeconds + observation.RepeatRatio = repeatCurrentSeconds / repeatBaseSeconds + observation.RepeatSuspected = repeatCurrentSeconds >= 2*repeatBaseSeconds && repeatCurrentSeconds >= floor + observation.ReverseSuspected = observation.RepeatSuspected + } + } + result = append(result, observation) + } + return result +} + +// ComparePerformance is retained for callers that already have a reverse +// current observation but no reverse base. New callers should use +// ComparePerformanceRuns so confirmation is based on the repeat's base. +func ComparePerformance(base, current, reverse map[string]float64) []PerformanceObservation { + return ComparePerformanceRuns(base, current, base, reverse) +} + +func PerformanceFailure(observations []PerformanceObservation) bool { + for _, observation := range observations { + if observation.Suspected && observation.ReverseSuspected { + return true + } + } + return false +} + +func PerformanceSuspicion(observations []PerformanceObservation) bool { + for _, observation := range observations { + if observation.Suspected { + return true + } + } + return false +} + +type PerformanceComparisonReport struct { + Status string `json:"status"` + BaseReport string `json:"base_report"` + CurrentReport string `json:"current_report"` + Observations []PerformanceObservation `json:"observations"` +} + +func ComparePerformanceReportFiles(basePath, currentPath, repeatBasePath, repeatCurrentPath string) (PerformanceComparisonReport, error) { + base, err := performanceMetricsFromReport(basePath) + if err != nil { + return PerformanceComparisonReport{}, fmt.Errorf("base performance report: %w", err) + } + current, err := performanceMetricsFromReport(currentPath) + if err != nil { + return PerformanceComparisonReport{}, fmt.Errorf("current performance report: %w", err) + } + if (strings.TrimSpace(repeatBasePath) == "") != (strings.TrimSpace(repeatCurrentPath) == "") { + return PerformanceComparisonReport{}, errors.New("repeat base and current reports must be provided together") + } + var repeatBase, repeatCurrent map[string]float64 + if repeatBasePath != "" { + repeatBase, err = performanceMetricsFromReport(repeatBasePath) + if err != nil { + return PerformanceComparisonReport{}, fmt.Errorf("repeat base performance report: %w", err) + } + repeatCurrent, err = performanceMetricsFromReport(repeatCurrentPath) + if err != nil { + return PerformanceComparisonReport{}, fmt.Errorf("repeat current performance report: %w", err) + } + } + observations := ComparePerformanceRuns(base, current, repeatBase, repeatCurrent) + status := "PASSED" + if repeatBasePath == "" && PerformanceSuspicion(observations) { + status = "SUSPECTED" + } + if repeatBasePath != "" && PerformanceFailure(observations) { + status = "FAILED" + } + return PerformanceComparisonReport{Status: status, BaseReport: basePath, CurrentReport: currentPath, Observations: observations}, nil +} + +func performanceMetricsFromReport(path string) (map[string]float64, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var report Report + if err := json.Unmarshal(raw, &report); err != nil { + return nil, err + } + if report.Status != "PASSED" { + return nil, fmt.Errorf("acceptance status is %q", report.Status) + } + metrics := PerformanceStageSeconds(report.Stages) + for _, name := range PerformanceStageNames { + if _, ok := metrics[name]; !ok { + return nil, fmt.Errorf("required stage %q is missing", name) + } + } + return metrics, nil +} + +func WritePerformanceComparison(path string, report PerformanceComparisonReport) error { + if strings.TrimSpace(path) == "" { + return errors.New("performance output path is required") + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + raw, err := json.MarshalIndent(report, "", " ") + if err != nil { + return err + } + tmp := path + ".tmp" + if err := os.WriteFile(tmp, append(raw, '\n'), 0o644); err != nil { + return err + } + return os.Rename(tmp, path) +} diff --git a/internal/acceptance/performance_test.go b/internal/acceptance/performance_test.go new file mode 100644 index 00000000..13b5f9e7 --- /dev/null +++ b/internal/acceptance/performance_test.go @@ -0,0 +1,152 @@ +package acceptance + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestComparePerformanceRequiresAbsoluteFloorAndRepeatBaseConfirmation(t *testing.T) { + base := map[string]float64{ + "generation_upload": 4, + "explorer_publish": 10, + "explorer_viewer": .2, + "graphql": .2, + } + current := map[string]float64{ + "generation_upload": 9, + "explorer_publish": 21, + "explorer_viewer": .5, + "graphql": .5, + } + repeatBase := map[string]float64{ + "generation_upload": 5, + "explorer_publish": 12, + "explorer_viewer": .3, + "graphql": .3, + } + repeatCurrent := map[string]float64{ + "generation_upload": 11, + "explorer_publish": 25, + "explorer_viewer": .7, + "graphql": .7, + } + got := ComparePerformanceRuns(base, current, repeatBase, repeatCurrent) + if len(got) != 4 { + t.Fatalf("observations=%d", len(got)) + } + for _, observation := range got { + if !observation.Suspected || !observation.RepeatSuspected || !observation.ReverseSuspected { + t.Fatalf("observation=%#v", observation) + } + } + if got[0].Name != "explorer_publish" { + t.Fatalf("observations are not sorted: %#v", got) + } + if !PerformanceFailure(got) { + t.Fatal("expected confirmed performance failure") + } +} + +func TestComparePerformanceRequiresRepeatFailureAgainstRepeatBase(t *testing.T) { + base := map[string]float64{"generation_upload": 4} + current := map[string]float64{"generation_upload": 9} + repeatBase := map[string]float64{"generation_upload": 5} + repeatCurrent := map[string]float64{"generation_upload": 9} + got := ComparePerformanceRuns(base, current, repeatBase, repeatCurrent) + if len(got) != 1 || !got[0].Suspected || got[0].ReverseSuspected || PerformanceFailure(got) { + t.Fatalf("got=%#v", got) + } +} + +func TestComparePerformanceAppliesMetricFloors(t *testing.T) { + base := map[string]float64{ + "generation_upload": 2.5, + "explorer_publish": 2.5, + "explorer_viewer": .05, + "graphql": .05, + } + current := map[string]float64{ + "generation_upload": 4.9, + "explorer_publish": 4.9, + "explorer_viewer": .099, + "graphql": .099, + } + for _, observation := range ComparePerformanceRuns(base, current, current, current) { + if observation.Suspected { + t.Fatalf("below floor should not be suspected: %#v", observation) + } + } + base["generation_upload"] = 2 + current["generation_upload"] = 5 + got := ComparePerformanceRuns(base, current, nil, nil) + var ingestion *PerformanceObservation + for i := range got { + if got[i].Name == "generation_upload" { + ingestion = &got[i] + } + } + if ingestion == nil || !ingestion.Suspected { + t.Fatalf("expected exact floor to be eligible: %#v", got) + } +} + +func TestPerformanceStageSecondsSelectsContractStages(t *testing.T) { + got := PerformanceStageSeconds([]StageReport{ + {Name: "generation_upload", Seconds: 1.2}, + {Name: "arango_counts", Seconds: 99}, + {Name: "graphql", Seconds: .4}, + }) + if len(got) != 2 || got["generation_upload"] != 1.2 || got["graphql"] != .4 { + t.Fatalf("metrics=%#v", got) + } +} + +func TestComparePerformanceDoesNotFailWithoutBase(t *testing.T) { + if got := ComparePerformanceRuns(nil, map[string]float64{"graphql": 8}, nil, nil); len(got) != 0 || PerformanceFailure(got) { + t.Fatalf("got=%#v", got) + } +} + +func TestComparePerformanceReportFilesRequiresRepeatToFail(t *testing.T) { + dir := t.TempDir() + base := writePerformanceTestReport(t, dir, "base.json", 2, 2, .05, .05) + current := writePerformanceTestReport(t, dir, "current.json", 5, 5, .1, .1) + first, err := ComparePerformanceReportFiles(base, current, "", "") + if err != nil || first.Status != "SUSPECTED" { + t.Fatalf("first comparison = %#v, %v", first, err) + } + repeatBase := writePerformanceTestReport(t, dir, "repeat-base.json", 4, 4, .1, .1) + repeatCurrent := writePerformanceTestReport(t, dir, "repeat-current.json", 5, 5, .11, .11) + confirmed, err := ComparePerformanceReportFiles(base, current, repeatBase, repeatCurrent) + if err != nil || confirmed.Status != "PASSED" { + t.Fatalf("confirmed comparison = %#v, %v", confirmed, err) + } + output := filepath.Join(dir, "performance.json") + if err := WritePerformanceComparison(output, confirmed); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(output); err != nil { + t.Fatal(err) + } +} + +func writePerformanceTestReport(t *testing.T, dir, name string, ingestion, publication, viewer, graphql float64) string { + t.Helper() + report := Report{Status: "PASSED", Stages: []StageReport{ + {Name: "generation_upload", Seconds: ingestion}, + {Name: "explorer_publish", Seconds: publication}, + {Name: "explorer_viewer", Seconds: viewer}, + {Name: "graphql", Seconds: graphql}, + }} + raw, err := json.Marshal(report) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(dir, name) + if err := os.WriteFile(path, raw, 0o644); err != nil { + t.Fatal(err) + } + return path +} diff --git a/internal/acceptance/runner.go b/internal/acceptance/runner.go new file mode 100644 index 00000000..4204ec9f --- /dev/null +++ b/internal/acceptance/runner.go @@ -0,0 +1,247 @@ +package acceptance + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "time" +) + +type Connections struct { + LoomURL string + ArangoURL string + ClickHouseURL string + ClickHouseUsername string + ClickHousePassword string +} + +type Namespace struct { + Run string `json:"run"` + Project string `json:"project"` + Generation string `json:"generation"` + ArangoDatabase string `json:"arango_database"` + ClickHouseDatabase string `json:"clickhouse_database"` +} + +type RunSpec struct { + RunID RunID + Project string + Generation string + ArangoDatabase string + ClickHouseDatabase string +} + +type Lease interface { + Connections() Connections + Namespace() Namespace + Close(context.Context) error +} + +type Target interface { + Acquire(context.Context, RunSpec) (Lease, error) +} + +type StaticTarget struct{ Conn Connections } + +func (t StaticTarget) Acquire(_ context.Context, spec RunSpec) (Lease, error) { + ns, err := namespaceFromSpec(spec) + if err != nil { + return nil, err + } + if strings.TrimSpace(t.Conn.LoomURL) == "" || strings.TrimSpace(t.Conn.ArangoURL) == "" || strings.TrimSpace(t.Conn.ClickHouseURL) == "" { + return nil, errors.New("static target requires Loom, ArangoDB, and ClickHouse URLs") + } + return staticLease{conn: t.Conn, ns: ns}, nil +} + +type staticLease struct { + conn Connections + ns Namespace +} + +func (l staticLease) Connections() Connections { return l.conn } +func (l staticLease) Namespace() Namespace { return l.ns } +func (l staticLease) Close(context.Context) error { return nil } + +type Config struct { + Target Target + Fixture Fetcher + Run RunSpec + ArtifactDir string + WorkspacePath string + OraclePath string + HTTPClient *http.Client + SourceCommit string + SkipPerformance bool +} + +type StageReport struct { + Name string `json:"name"` + Seconds float64 `json:"seconds"` + Details map[string]any `json:"details,omitempty"` + Error string `json:"error,omitempty"` +} + +type Report struct { + Run Namespace `json:"run"` + Fixture ValidatedFixture `json:"fixture"` + Status string `json:"status"` + BaseStatus string `json:"base_status,omitempty"` + Stages []StageReport `json:"stages"` + StartedAt time.Time `json:"started_at"` + FinishedAt time.Time `json:"finished_at"` + Error string `json:"error,omitempty"` +} + +type Runner struct{ cfg Config } + +var databaseRE = regexp.MustCompile(`^loom_acceptance_[a-f0-9]{16}$`) + +func New(cfg Config) (*Runner, error) { + if cfg.Target == nil { + return nil, errors.New("acceptance target is required") + } + if cfg.Fixture.CacheDir == "" { + return nil, errors.New("fixture cache directory is required") + } + if strings.TrimSpace(cfg.WorkspacePath) == "" { + return nil, errors.New("workspace path is required") + } + if strings.TrimSpace(cfg.ArtifactDir) == "" { + return nil, errors.New("artifact directory is required") + } + if cfg.Run.Project == "" { + cfg.Run.Project = "NCPI_ACCEPTANCE" + } + if cfg.Run.Generation == "" { + cfg.Run.Generation = "tcga-brca-locked" + } + if cfg.Run.RunID == "" { + cfg.Run.RunID = randomRunID() + } + if cfg.Run.ArangoDatabase == "" { + cfg.Run.ArangoDatabase = databaseName(cfg.Run.RunID) + } + if cfg.Run.ClickHouseDatabase == "" { + cfg.Run.ClickHouseDatabase = databaseName(cfg.Run.RunID) + } + if _, err := namespaceFromSpec(cfg.Run); err != nil { + return nil, err + } + if cfg.SourceCommit == "" { + cfg.SourceCommit = "acceptance" + } + return &Runner{cfg: cfg}, nil +} + +func randomRunID() RunID { + var bytes [8]byte + if _, err := rand.Read(bytes[:]); err != nil { + return RunID("0000000000000000") + } + return RunID(hex.EncodeToString(bytes[:])) +} +func databaseName(run RunID) string { + value := string(run) + if !regexp.MustCompile(`^[a-f0-9]{16}$`).MatchString(value) { + sum := sha256.Sum256([]byte(value)) + value = hex.EncodeToString(sum[:8]) + } + return "loom_acceptance_" + value +} + +func namespaceFromSpec(spec RunSpec) (Namespace, error) { + if !databaseRE.MatchString(spec.ArangoDatabase) || !databaseRE.MatchString(spec.ClickHouseDatabase) { + return Namespace{}, fmt.Errorf("run-specific database names must match %s", databaseRE.String()) + } + if strings.TrimSpace(string(spec.RunID)) == "" || strings.TrimSpace(spec.Project) == "" || strings.TrimSpace(spec.Generation) == "" { + return Namespace{}, errors.New("run, project, and generation are required") + } + return Namespace{Run: string(spec.RunID), Project: spec.Project, Generation: spec.Generation, ArangoDatabase: spec.ArangoDatabase, ClickHouseDatabase: spec.ClickHouseDatabase}, nil +} + +func (r *Runner) Run(ctx context.Context) (report Report, runErr error) { + started := time.Now().UTC() + report.StartedAt = started + lease, err := r.cfg.Target.Acquire(ctx, r.cfg.Run) + if err != nil { + report.Status = "TARGET_UNAVAILABLE" + report.Error = err.Error() + r.writeEvidence(report) + return report, err + } + ns := lease.Namespace() + report.Run = ns + defer func() { + report.FinishedAt = time.Now().UTC() + if runErr != nil { + report.Status = "FAILED" + } else if report.Status == "" { + report.Status = "PASSED" + } + if err := r.writeEvidence(report); err != nil { + runErr = errors.Join(runErr, err) + } + if closeErr := lease.Close(context.WithoutCancel(ctx)); closeErr != nil { + runErr = errors.Join(runErr, closeErr) + report.Status = "FAILED" + report.Error = closeErr.Error() + _ = r.writeEvidence(report) + } + }() + fixture := r.cfg.Fixture + validated, err := fixture.Acquire(ctx) + report.Fixture = validated + if err != nil { + report.Status = "FIXTURE_UNAVAILABLE" + report.Error = err.Error() + runErr = err + return report, err + } + if err := validateFixtureClosure(validated, fixture.Lock.Counts); err != nil { + report.Status = "FIXTURE_INVALID" + report.Error = err.Error() + runErr = err + return report, err + } + startedStage := time.Now() + scenario := ScenarioConfig{Connections: lease.Connections(), Namespace: ns, Fixture: validated, WorkspacePath: r.cfg.WorkspacePath, OraclePath: r.cfg.OraclePath, SourceCommit: r.cfg.SourceCommit, ArtifactDir: r.cfg.ArtifactDir, HTTPClient: r.cfg.HTTPClient} + result, err := RunScenario(ctx, scenario) + report.Stages = append(report.Stages, result.Stages...) + report.BaseStatus = result.BaseStatus + report.Status = result.Status + report.Stages = append(report.Stages, StageReport{Name: "acceptance", Seconds: time.Since(startedStage).Seconds()}) + if err != nil { + report.Error = err.Error() + runErr = err + return report, err + } + return report, nil +} + +func (r *Runner) writeEvidence(report Report) error { + if strings.TrimSpace(r.cfg.ArtifactDir) == "" { + return nil + } + if err := os.MkdirAll(r.cfg.ArtifactDir, 0o755); err != nil { + return err + } + raw, err := json.MarshalIndent(report, "", " ") + if err != nil { + return err + } + tmp := filepath.Join(r.cfg.ArtifactDir, ".report.json.tmp") + if err := os.WriteFile(tmp, append(raw, '\n'), 0o644); err != nil { + return err + } + return os.Rename(tmp, filepath.Join(r.cfg.ArtifactDir, "report.json")) +} diff --git a/internal/acceptance/scenario.go b/internal/acceptance/scenario.go new file mode 100644 index 00000000..954b0929 --- /dev/null +++ b/internal/acceptance/scenario.go @@ -0,0 +1,735 @@ +package acceptance + +import ( + "bufio" + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + dfpublication "github.com/calypr/loom/internal/dataframe/publication" + publicationarango "github.com/calypr/loom/internal/dataframe/publication/arango" + arangostore "github.com/calypr/loom/internal/store/arango" + clickhousestore "github.com/calypr/loom/internal/store/clickhouse" +) + +type ScenarioConfig struct { + Connections Connections + Namespace Namespace + Fixture ValidatedFixture + WorkspacePath string + OraclePath string + SourceCommit string + ArtifactDir string + HTTPClient *http.Client +} + +type ScenarioResult struct { + Status string + BaseStatus string + Stages []StageReport +} + +type Oracle struct { + RowCount int `json:"row_count"` + UniquePatientCount int `json:"unique_patient_count"` + Columns []string `json:"columns"` + RowDigest string `json:"row_digest"` + NonNullCounts map[string]int `json:"non_null_counts,omitempty"` + TrueCounts map[string]int `json:"true_counts,omitempty"` +} + +type publication struct { + Project string `json:"project"` + Generation string `json:"generation"` + ExecutionID string `json:"executionId"` + Recipe string `json:"recipe"` + TranslationVersion string `json:"translationVersion"` + Output string `json:"output"` +} + +func RunScenario(ctx context.Context, cfg ScenarioConfig) (result ScenarioResult, err error) { + if cfg.HTTPClient == nil { + cfg.HTTPClient = &http.Client{Timeout: 2 * time.Minute} + } + oracle, err := loadOracle(cfg.OraclePath) + if err != nil { + return result, err + } + workspace, err := os.ReadFile(cfg.WorkspacePath) + if err != nil { + return result, fmt.Errorf("read acceptance workspace: %w", err) + } + if !json.Valid(workspace) { + return result, errors.New("acceptance workspace is not valid JSON") + } + stage := func(name string, started time.Time, details map[string]any) { + result.Stages = append(result.Stages, StageReport{Name: name, Seconds: time.Since(started).Seconds(), Details: details}) + } + started := time.Now() + load, err := uploadGeneration(ctx, cfg, cfg.HTTPClient) + if err != nil { + return result, err + } + if err := writeArtifact(cfg.ArtifactDir, "generation.json", load); err != nil { + return result, err + } + stage("generation_upload", started, load) + if err := compareSummary(load, cfg.Fixture.Counts); err != nil { + return result, err + } + started = time.Now() + counts, err := directArangoCounts(ctx, cfg) + if err != nil { + return result, err + } + stage("arango_counts", started, map[string]any{"counts": counts}) + if err := compareCounts(counts, cfg.Fixture.Counts); err != nil { + return result, err + } + started = time.Now() + pub, err := publishWorkspace(ctx, cfg, cfg.HTTPClient, workspace) + if err != nil { + return result, err + } + stage("explorer_publish", started, map[string]any{"execution_id": pub.ExecutionID}) + if pub.ExecutionID == "" { + return result, errors.New("Explorer publication returned no execution ID") + } + started = time.Now() + execution, err := getJSON(ctx, cfg.HTTPClient, cfg.Connections.LoomURL+"/api/v1/dataframe/recipe-executions/"+url.PathEscape(pub.ExecutionID)) + if err != nil { + return result, err + } + if err := writeArtifact(cfg.ArtifactDir, "execution.json", execution); err != nil { + return result, err + } + stage("execution_registry", started, map[string]any{"execution_id": pub.ExecutionID, "state": execution["state"]}) + if err := successfulExecution(execution, pub); err != nil { + return result, err + } + started = time.Now() + physical, err := verifyClickHouse(ctx, cfg, pub, oracle) + if err != nil { + return result, err + } + stage("clickhouse_physical", started, physical) + started = time.Now() + viewer, err := getJSON(ctx, cfg.HTTPClient, cfg.Connections.LoomURL+"/api/v1/projects/"+url.PathEscape(cfg.Namespace.Project)+"/explorers/default") + if err != nil { + return result, err + } + if err := writeArtifact(cfg.ArtifactDir, "viewer.json", viewer); err != nil { + return result, err + } + stage("explorer_viewer", started, map[string]any{"keys": sortedMapKeys(viewer)}) + if err := verifyViewer(viewer, pub); err != nil { + return result, err + } + started = time.Now() + graphql, err := verifyGraphQL(ctx, cfg, pub, oracle) + if err != nil { + return result, err + } + if err := writeArtifact(cfg.ArtifactDir, "graphql.json", graphql); err != nil { + return result, err + } + stage("graphql", started, graphql) + started = time.Now() + second, err := publishWorkspace(ctx, cfg, cfg.HTTPClient, workspace) + if err != nil { + return result, fmt.Errorf("idempotent second publication: %w", err) + } + stage("idempotent_publish", started, map[string]any{"execution_id": second.ExecutionID}) + if second.ExecutionID != pub.ExecutionID || second.Recipe != pub.Recipe || second.TranslationVersion != pub.TranslationVersion { + return result, errors.New("idempotent publication changed execution or selector") + } + result.Status = "PASSED" + return result, nil +} + +func loadOracle(path string) (Oracle, error) { + if strings.TrimSpace(path) == "" { + return Oracle{RowCount: 100, UniquePatientCount: 100}, nil + } + raw, err := os.ReadFile(path) + if err != nil { + return Oracle{}, fmt.Errorf("read acceptance oracle: %w", err) + } + var oracle Oracle + if err := json.Unmarshal(raw, &oracle); err != nil { + return Oracle{}, fmt.Errorf("decode acceptance oracle: %w", err) + } + if oracle.RowCount <= 0 || oracle.UniquePatientCount <= 0 { + return Oracle{}, errors.New("acceptance oracle requires positive row counts") + } + return oracle, nil +} + +func uploadGeneration(ctx context.Context, cfg ScenarioConfig, client *http.Client) (map[string]any, error) { + var body bytes.Buffer + writer := multipart.NewWriter(&body) + files, err := filepath.Glob(filepath.Join(cfg.Fixture.MetaDir, "*.ndjson")) + if err != nil { + return nil, err + } + sort.Strings(files) + if len(files) == 0 { + return nil, errors.New("fixture contains no NDJSON files") + } + for _, path := range files { + file, err := os.Open(path) + if err != nil { + return nil, err + } + part, err := writer.CreateFormFile("file", filepath.Base(path)) + if err == nil { + _, err = io.Copy(part, file) + } + closeErr := file.Close() + if err == nil { + err = closeErr + } + if err != nil { + return nil, fmt.Errorf("attach fixture %s: %w", filepath.Base(path), err) + } + } + _ = writer.WriteField("project", cfg.Namespace.Project) + _ = writer.WriteField("generation", cfg.Namespace.Generation) + _ = writer.WriteField("defer_activation", "false") + if err := writer.Close(); err != nil { + return nil, err + } + path := cfg.Connections.LoomURL + "/api/v1/datasets/" + url.PathEscape(cfg.Namespace.Project) + "/generations/" + url.PathEscape(cfg.Namespace.Generation) + return doJSON(ctx, client, http.MethodPost, path, &body, writer.FormDataContentType(), nil) +} + +func compareSummary(payload map[string]any, want map[string]int) error { + if reused, _ := payload["reused"].(bool); reused { + return nil + } + summary, _ := payload["summary"].(map[string]any) + if summary == nil { + return errors.New("generation response omitted summary") + } + resources, _ := summary["resources"].(map[string]any) + if resources == nil { + return errors.New("generation response omitted resource counts") + } + for typ, count := range want { + if got, ok := numeric(resources[typ]); !ok || got != int64(count) { + return fmt.Errorf("generation count %s=%d want %d", typ, got, count) + } + } + return nil +} + +func directArangoCounts(ctx context.Context, cfg ScenarioConfig) (map[string]int, error) { + client, err := arangostore.Open(ctx, cfg.Connections.ArangoURL, cfg.Namespace.ArangoDatabase) + if err != nil { + return nil, fmt.Errorf("open acceptance ArangoDB: %w", err) + } + defer client.Close(ctx) + result := map[string]int{} + for typ := range cfg.Fixture.Counts { + count := 0 + query := "FOR doc IN @@collection FILTER doc.project == @project AND doc.dataset_generation == @generation RETURN {present: true}" + err := client.QueryRows(ctx, query, 1000, map[string]any{"@collection": typ, "project": cfg.Namespace.Project, "generation": cfg.Namespace.Generation}, func(map[string]any) error { count++; return nil }) + if err != nil { + return nil, fmt.Errorf("count Arango %s: %w", typ, err) + } + result[typ] = count + } + return result, nil +} + +func compareCounts(got, want map[string]int) error { + for typ, count := range want { + if got[typ] != count { + return fmt.Errorf("Arango count %s=%d want %d", typ, got[typ], count) + } + } + return nil +} + +func validateFixtureClosure(fixture ValidatedFixture, counts map[string]int) error { + patients := make(map[string]bool) + file, err := os.Open(filepath.Join(fixture.MetaDir, "Patient.ndjson")) + if err != nil { + return fmt.Errorf("open fixture patients: %w", err) + } + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 64<<10), 64<<20) + for scanner.Scan() { + var value struct { + ID string `json:"id"` + } + if err := json.Unmarshal(scanner.Bytes(), &value); err != nil { + _ = file.Close() + return err + } + patients[value.ID] = true + } + if err := scanner.Err(); err != nil { + _ = file.Close() + return err + } + if err := file.Close(); err != nil { + return err + } + for typ, count := range counts { + if typ == "Patient" || typ == "ResearchStudy" { + continue + } + file, err := os.Open(filepath.Join(fixture.MetaDir, typ+".ndjson")) + if err != nil { + return fmt.Errorf("open fixture %s: %w", typ, err) + } + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 64<<10), 64<<20) + seen := 0 + for scanner.Scan() { + var value any + if err := json.Unmarshal(scanner.Bytes(), &value); err != nil { + _ = file.Close() + return err + } + refs := []string{} + collectPatientRefs(value, &refs) + for _, ref := range refs { + id := strings.TrimPrefix(ref, "Patient/") + if !patients[id] { + _ = file.Close() + return fmt.Errorf("fixture closure: %s references patient outside selected cohort: %s", typ, ref) + } + } + seen++ + } + closeErr := file.Close() + if err := scanner.Err(); err != nil { + return err + } + if closeErr != nil { + return closeErr + } + if seen != count { + return fmt.Errorf("fixture closure: %s rows=%d want %d", typ, seen, count) + } + } + return nil +} + +func collectPatientRefs(value any, refs *[]string) { + switch item := value.(type) { + case map[string]any: + for key, child := range item { + if key == "reference" { + if ref, ok := child.(string); ok && strings.HasPrefix(ref, "Patient/") { + *refs = append(*refs, ref) + } + } + collectPatientRefs(child, refs) + } + case []any: + for _, child := range item { + collectPatientRefs(child, refs) + } + } +} + +func publishWorkspace(ctx context.Context, cfg ScenarioConfig, client *http.Client, workspace []byte) (publication, error) { + path := cfg.Connections.LoomURL + "/api/v1/projects/" + url.PathEscape(cfg.Namespace.Project) + "/generations/" + url.PathEscape(cfg.Namespace.Generation) + "/explorer-config" + value, err := doJSON(ctx, client, http.MethodPost, path, bytes.NewReader(workspace), "application/json", map[string]string{"X-Loom-Source-Commit": cfg.SourceCommit}) + if err != nil { + return publication{}, err + } + var result publication + raw, _ := json.Marshal(value) + if err := json.Unmarshal(raw, &result); err != nil { + return publication{}, err + } + if result.Recipe == "" { + result.Recipe = "tcga_brca_cohort" + } + if result.Output == "" { + result.Output = "tcga_brca_cohort" + } + return result, nil +} + +func successfulExecution(value map[string]any, pub publication) error { + state, _ := value["state"].(string) + if !strings.EqualFold(state, "PUBLISHED") && !strings.EqualFold(state, "READY") { + return fmt.Errorf("execution %s state %q is not successful", pub.ExecutionID, state) + } + outputs, _ := value["outputs"].([]any) + if len(outputs) == 0 { + return errors.New("execution registry omitted outputs") + } + return nil +} + +func verifyClickHouse(ctx context.Context, cfg ScenarioConfig, pub publication, oracle Oracle) (map[string]any, error) { + arangoClient, err := arangostore.Open(ctx, cfg.Connections.ArangoURL, cfg.Namespace.ArangoDatabase) + if err != nil { + return nil, fmt.Errorf("open publication registry: %w", err) + } + defer arangoClient.Close(ctx) + registry, err := publicationarango.New(arangoClient) + if err != nil { + return nil, err + } + record, err := registry.GetExecution(ctx, pub.ExecutionID) + if err != nil { + return nil, fmt.Errorf("resolve publication execution %s: %w", pub.ExecutionID, err) + } + output, err := publicationOutput(record, pub.Output) + if err != nil { + return nil, err + } + table := output.PhysicalTable + if err := compareColumns(output.Columns, oracle.Columns); err != nil { + return nil, err + } + client, err := clickhousestore.New(clickhousestore.Options{URL: cfg.Connections.ClickHouseURL, Database: cfg.Namespace.ClickHouseDatabase, Username: cfg.Connections.ClickHouseUsername, Password: cfg.Connections.ClickHousePassword}) + if err != nil { + return nil, err + } + defer client.Close() + rows, err := client.QueryRowsArgs(ctx, "SELECT count() AS row_count FROM `"+table+"`", []string{"row_count"}) + if err != nil { + return nil, err + } + if len(rows) != 1 { + return nil, fmt.Errorf("ClickHouse count returned %d rows", len(rows)) + } + count, ok := numeric(rows[0]["row_count"]) + if !ok || count != int64(oracle.RowCount) { + return nil, fmt.Errorf("ClickHouse row count=%d want %d", count, oracle.RowCount) + } + patientColumn := executionPatientColumn(output.Columns) + if patientColumn == "" { + return nil, errors.New("execution output has no patient identity column") + } + unique, err := client.QueryRowsArgs(ctx, "SELECT uniqExact(`"+patientColumn+"`) AS unique_patients FROM `"+table+"`", []string{"unique_patients"}) + if err != nil { + return nil, err + } + uniqueCount := int64(0) + if len(unique) > 0 { + uniqueCount, _ = numeric(unique[0]["unique_patients"]) + } + if uniqueCount != int64(oracle.UniquePatientCount) { + return nil, fmt.Errorf("ClickHouse unique patient count=%d want %d", uniqueCount, oracle.UniquePatientCount) + } + return map[string]any{"table": table, "row_count": count, "unique_patients": uniqueCount}, nil +} + +func compareColumns(got []dfpublication.PhysicalColumn, want []string) error { + available := make(map[string]bool, len(got)) + for _, column := range got { + available[column.Name] = true + } + for _, name := range want { + if !available[name] { + return fmt.Errorf("ClickHouse publication omitted expected column %q", name) + } + } + return nil +} + +func publicationOutput(execution dfpublication.BundleExecution, name string) (dfpublication.BundleOutputRecord, error) { + for _, output := range execution.Outputs { + if output.Name == name { + if !output.Queryable() { + return dfpublication.BundleOutputRecord{}, fmt.Errorf("publication output %q is not queryable", name) + } + return output, nil + } + } + return dfpublication.BundleOutputRecord{}, fmt.Errorf("publication execution %s omitted output %q", execution.ID, name) + +} + +func executionPatientColumn(columns []dfpublication.PhysicalColumn) string { + for _, column := range columns { + lower := strings.ToLower(column.Name) + if strings.Contains(lower, "patient") || lower == "id" { + return column.Name + } + } + return "" +} + +func verifyViewer(viewer map[string]any, pub publication) error { + if len(viewer) == 0 { + return errors.New("Explorer viewer returned an empty state") + } + raw, _ := json.Marshal(viewer) + if pub.Recipe != "" && !bytes.Contains(bytes.ToLower(raw), []byte(strings.ToLower(pub.Recipe))) { + return fmt.Errorf("Explorer viewer does not contain publication recipe %q", pub.Recipe) + } + return nil +} + +func verifyGraphQL(ctx context.Context, cfg ScenarioConfig, pub publication, oracle Oracle) (map[string]any, error) { + selector := map[string]any{"recipe": pub.Recipe, "translationVersion": pub.TranslationVersion, "output": pub.Output} + query := `query($input:DataframeDatasetInput!){dataframeDataset(input:$input){id name projectId datasetGeneration state rowCount selector{recipe translationVersion output} columns{name}}}` + dataset, err := graphql(ctx, cfg, query, map[string]any{"input": map[string]any{"projectId": cfg.Namespace.Project, "selector": selector}}) + if err != nil { + return nil, err + } + data, _ := dataset["data"].(map[string]any) + materialization, _ := data["dataframeDataset"].(map[string]any) + if materialization == nil { + return nil, errors.New("GraphQL dataset metadata is null") + } + if state, _ := materialization["state"].(string); state != "READY" { + return nil, fmt.Errorf("GraphQL dataset state=%q want READY", state) + } + if err := compareGraphQLColumns(materialization, oracle.Columns); err != nil { + return nil, err + } + count, ok := numeric(materialization["rowCount"]) + if !ok { + return nil, errors.New("GraphQL dataset rowCount is missing or has the wrong shape") + } + if count != int64(oracle.RowCount) { + return nil, fmt.Errorf("GraphQL dataset rowCount=%d want %d", count, oracle.RowCount) + } + rowsQuery := `query($input:DataframeRowsInput!){dataframeRows(input:$input){columns rows totalCount pageInfo{hasNextPage}}}` + rows, err := graphql(ctx, cfg, rowsQuery, map[string]any{"input": map[string]any{"projectId": cfg.Namespace.Project, "selector": selector, "first": oracle.RowCount, "sort": map[string]any{"column": patientColumn(materialization), "desc": false}}}) + if err != nil { + return nil, err + } + rowsData, ok := rows["data"].(map[string]any) + if !ok { + return nil, errors.New("GraphQL rows data is missing or has the wrong shape") + } + connection, ok := rowsData["dataframeRows"].(map[string]any) + if !ok { + return nil, errors.New("GraphQL dataframeRows is missing or has the wrong shape") + } + total, ok := numeric(connection["totalCount"]) + if !ok { + return nil, errors.New("GraphQL dataframeRows totalCount is missing or has the wrong shape") + } + if total != int64(oracle.RowCount) { + return nil, fmt.Errorf("GraphQL rows totalCount=%d want %d", total, oracle.RowCount) + } + rowValues, ok := connection["rows"].([]any) + if !ok { + return nil, errors.New("GraphQL dataframeRows rows is missing or has the wrong shape") + } + if err := verifyRowProfiles(rowValues, oracle); err != nil { + return nil, err + } + if oracle.RowDigest != "" { + if got := normalizedRowsDigest(rowValues); got != oracle.RowDigest { + return nil, fmt.Errorf("GraphQL normalized row digest=%s want %s", got, oracle.RowDigest) + } + } + countQuery := `query($input:DataframeAggregateInput!){dataframeAggregate(input:$input){columns rows}}` + _, err = graphql(ctx, cfg, countQuery, map[string]any{"input": map[string]any{"projectId": cfg.Namespace.Project, "selector": selector, "operation": "COUNT"}}) + if err != nil { + return nil, err + } + facetQuery := `query($input:DataframeAggregationsInput!){dataframeAggregations(input:$input){aggregations}}` + _, err = graphql(ctx, cfg, facetQuery, map[string]any{"input": map[string]any{"projectId": cfg.Namespace.Project, "selector": selector, "specs": []any{map[string]any{"name": "patient_values", "kind": "TERMS", "column": firstColumn(materialization), "size": 10}}}}) + if err != nil { + return nil, err + } + return map[string]any{"dataset": materialization, "rows": rows["data"], "row_digest": graphqlRowsDigest(rows)}, nil +} + +func verifyRowProfiles(rows []any, oracle Oracle) error { + nonNull := make(map[string]int, len(oracle.NonNullCounts)) + trueValues := make(map[string]int, len(oracle.TrueCounts)) + for index, raw := range rows { + row, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("GraphQL row %d has the wrong shape", index) + } + for column := range oracle.NonNullCounts { + if value, exists := row[column]; exists && value != nil { + nonNull[column]++ + } + } + for column := range oracle.TrueCounts { + if value, exists := row[column]; exists { + if boolean, ok := value.(bool); ok && boolean { + trueValues[column]++ + } + } + } + } + for column, want := range oracle.NonNullCounts { + if got := nonNull[column]; got != want { + return fmt.Errorf("GraphQL column %s non-null rows=%d want %d", column, got, want) + } + } + for column, want := range oracle.TrueCounts { + if got := trueValues[column]; got != want { + return fmt.Errorf("GraphQL column %s true rows=%d want %d", column, got, want) + } + } + return nil +} + +func graphqlRowsDigest(response map[string]any) string { + data, _ := response["data"].(map[string]any) + connection, _ := data["dataframeRows"].(map[string]any) + return normalizedRowsDigest(connection["rows"]) +} + +func firstColumn(materialization map[string]any) string { + cols, _ := materialization["columns"].([]any) + for _, raw := range cols { + if col, ok := raw.(map[string]any); ok { + name, _ := col["name"].(string) + if !strings.HasPrefix(name, "__loom_") { + return name + } + } + } + return "patient_id" +} + +func patientColumn(materialization map[string]any) string { + cols, _ := materialization["columns"].([]any) + for _, raw := range cols { + if col, ok := raw.(map[string]any); ok { + name, _ := col["name"].(string) + if name == "patient_id" { + return name + } + } + } + return firstColumn(materialization) +} + +func compareGraphQLColumns(materialization map[string]any, want []string) error { + cols, _ := materialization["columns"].([]any) + available := make(map[string]bool, len(cols)) + for _, raw := range cols { + if col, ok := raw.(map[string]any); ok { + name, _ := col["name"].(string) + available[name] = true + } + } + for _, name := range want { + if !available[name] { + return fmt.Errorf("GraphQL dataset omitted expected column %q", name) + } + } + return nil +} + +func graphql(ctx context.Context, cfg ScenarioConfig, query string, variables map[string]any) (map[string]any, error) { + payload := map[string]any{"query": query, "variables": variables} + raw, _ := json.Marshal(payload) + value, err := doJSON(ctx, cfg.HTTPClient, http.MethodPost, cfg.Connections.LoomURL+"/graphql/graph", bytes.NewReader(raw), "application/json", nil) + if err != nil { + return nil, err + } + if errorsValue, ok := value["errors"]; ok && errorsValue != nil { + return nil, fmt.Errorf("GraphQL errors: %v", errorsValue) + } + return value, nil +} + +func getJSON(ctx context.Context, client *http.Client, target string) (map[string]any, error) { + return doJSON(ctx, client, http.MethodGet, target, nil, "", nil) +} + +func doJSON(ctx context.Context, client *http.Client, method, target string, body io.Reader, contentType string, headers map[string]string) (map[string]any, error) { + req, err := http.NewRequestWithContext(ctx, method, target, body) + if err != nil { + return nil, err + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + for key, value := range headers { + req.Header.Set(key, value) + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<20)) + if readErr != nil { + return nil, readErr + } + var value map[string]any + if err := json.Unmarshal(raw, &value); err != nil { + return nil, fmt.Errorf("decode HTTP %s response (%s): %w", method, resp.Status, err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("HTTP %s %s: %s", method, resp.Status, strings.TrimSpace(string(raw))) + } + return value, nil +} + +func numeric(value any) (int64, bool) { + switch item := value.(type) { + case int: + return int64(item), true + case int64: + return item, true + case float64: + return int64(item), item == float64(int64(item)) + case json.Number: + n, err := item.Int64() + return n, err == nil + case string: + n, err := strconv.ParseInt(item, 10, 64) + return n, err == nil + } + return 0, false +} +func sortedMapKeys(value map[string]any) []string { + result := make([]string, 0, len(value)) + for key := range value { + result = append(result, key) + } + sort.Strings(result) + return result +} +func normalizedRowsDigest(rows any) string { + raw, _ := json.Marshal(rows) + sum := sha256.Sum256(raw) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +func writeArtifact(dir, name string, value any) error { + if strings.TrimSpace(dir) == "" { + return nil + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + raw, err := json.MarshalIndent(value, "", " ") + if err != nil { + return err + } + tmp := filepath.Join(dir, "."+name+".tmp") + if err := os.WriteFile(tmp, append(raw, '\n'), 0o644); err != nil { + return err + } + return os.Rename(tmp, filepath.Join(dir, name)) +} diff --git a/internal/acceptance/scenario_test.go b/internal/acceptance/scenario_test.go new file mode 100644 index 00000000..abf2339c --- /dev/null +++ b/internal/acceptance/scenario_test.go @@ -0,0 +1,162 @@ +package acceptance + +import ( + "context" + "encoding/json" + "net/http" + "os" + "strings" + "testing" + "time" + + dfpublication "github.com/calypr/loom/internal/dataframe/publication" + "github.com/calypr/loom/internal/explorer/authoringv2" +) + +func TestAcceptanceWorkspaceDefinesPatientCohortContract(t *testing.T) { + raw, err := os.ReadFile("../../testdata/acceptance/ncpi-tcga-brca/workspace.json") + if err != nil { + t.Fatal(err) + } + workspace, err := authoringv2.DecodeWorkspace(raw) + if err != nil { + t.Fatal(err) + } + if err := workspace.ValidateForPublication(); err != nil { + t.Fatal(err) + } + want := []string{ + "patient_id", "submitter_id", "birth_sex", "race", "ethnicity", + "primary_histology", "pathological_stage", "age_at_diagnosis_days", "diagnostic_method", "days_to_death", + "condition_count", "specimen_count", "tumor_specimen_count", "normal_specimen_count", "has_paired_tumor_normal", + "earliest_collection_day", "latest_collection_day", + } + if len(workspace.Documents) != 1 || len(workspace.Documents[0].Columns) != len(want) { + t.Fatalf("workspace documents/columns = %d/%d", len(workspace.Documents), len(workspace.Documents[0].Columns)) + } + for index, column := range workspace.Documents[0].Columns { + if column.Column != want[index] { + t.Fatalf("column[%d] = %q, want %q", index, column.Column, want[index]) + } + } + route := workspace.Documents[0].Route + if len(route.Children) != 3 || route.Children[1].OccurrenceID != "specimen" || len(route.Children[1].Children) != 1 || route.Children[1].Children[0].Relationship != "specimen_Specimen" { + t.Fatalf("workspace lost patient-to-specimen-to-observation traversal: %#v", route) + } +} + +func TestPublicationOutputRequiresNamedQueryableOutput(t *testing.T) { + verified := time.Now().UTC() + execution := dfpublication.BundleExecution{ID: "execution-a", Outputs: []dfpublication.BundleOutputRecord{{ + Name: "cohort", PhysicalTable: "loom_bundle_a_cohort", State: dfpublication.BundlePublished, VerifiedAt: &verified, + }}} + output, err := publicationOutput(execution, "cohort") + if err != nil || output.PhysicalTable != "loom_bundle_a_cohort" { + t.Fatalf("publication output = %#v, %v", output, err) + } + if _, err := publicationOutput(execution, "missing"); err == nil { + t.Fatal("missing publication output was accepted") + } +} + +func TestPatientColumnPrefersStableIdentity(t *testing.T) { + materialization := map[string]any{"columns": []any{ + map[string]any{"name": "project_id"}, + map[string]any{"name": "patient_id"}, + }} + if got := patientColumn(materialization); got != "patient_id" { + t.Fatalf("patient sort column = %q", got) + } +} + +func TestCompareColumnsRejectsMissingOracleColumn(t *testing.T) { + columns := []dfpublication.PhysicalColumn{{Name: "patient_id"}} + if err := compareColumns(columns, []string{"patient_id"}); err != nil { + t.Fatal(err) + } + if err := compareColumns(columns, []string{"patient_id", "diagnosis"}); err == nil { + t.Fatal("missing oracle column was accepted") + } +} + +func TestCompareGenerationSummaryAcceptsReusedReadyGeneration(t *testing.T) { + counts := map[string]int{"Patient": 100} + if err := compareSummary(map[string]any{"reused": true}, counts); err != nil { + t.Fatalf("reused generation = %v", err) + } + if err := compareSummary(map[string]any{}, counts); err == nil { + t.Fatal("fresh generation without a load summary was accepted") + } +} + +func TestVerifyRowProfilesExplainsCohortDrift(t *testing.T) { + rows := []any{ + map[string]any{"patient_id": "p1", "stage": "I", "paired": true}, + map[string]any{"patient_id": "p2", "stage": nil, "paired": false}, + } + oracle := Oracle{NonNullCounts: map[string]int{"patient_id": 2, "stage": 1}, TrueCounts: map[string]int{"paired": 1}} + if err := verifyRowProfiles(rows, oracle); err != nil { + t.Fatal(err) + } + oracle.NonNullCounts["stage"] = 2 + if err := verifyRowProfiles(rows, oracle); err == nil || !strings.Contains(err.Error(), "stage non-null rows=1 want 2") { + t.Fatalf("profile mismatch = %v", err) + } +} + +func TestVerifyGraphQLRejectsMalformedRequiredFields(t *testing.T) { + cases := []struct { + name string + mutateDataset func(map[string]any) + mutateRows func(map[string]any) + }{ + {name: "dataset rowCount missing", mutateDataset: func(dataset map[string]any) { delete(dataset, "rowCount") }}, + {name: "dataset rowCount wrong shape", mutateDataset: func(dataset map[string]any) { dataset["rowCount"] = map[string]any{} }}, + {name: "dataframeRows missing", mutateRows: func(rows map[string]any) { delete(rows, "dataframeRows") }}, + {name: "dataframeRows wrong shape", mutateRows: func(rows map[string]any) { rows["dataframeRows"] = "wrong" }}, + {name: "totalCount missing", mutateRows: func(rows map[string]any) { delete(rows["dataframeRows"].(map[string]any), "totalCount") }}, + {name: "totalCount wrong shape", mutateRows: func(rows map[string]any) { rows["dataframeRows"].(map[string]any)["totalCount"] = map[string]any{} }}, + {name: "rows missing", mutateRows: func(rows map[string]any) { delete(rows["dataframeRows"].(map[string]any), "rows") }}, + {name: "rows wrong shape", mutateRows: func(rows map[string]any) { rows["dataframeRows"].(map[string]any)["rows"] = map[string]any{} }}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + server := newLocalTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request struct { + Query string `json:"query"` + } + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Errorf("decode request: %v", err) + return + } + response := map[string]any{"data": map[string]any{}} + switch { + case strings.Contains(request.Query, "dataframeDataset"): + response["data"].(map[string]any)["dataframeDataset"] = map[string]any{ + "state": "READY", "rowCount": 1, "columns": []any{}, + } + if test.mutateDataset != nil { + test.mutateDataset(response["data"].(map[string]any)["dataframeDataset"].(map[string]any)) + } + case strings.Contains(request.Query, "dataframeRows"): + response["data"].(map[string]any)["dataframeRows"] = map[string]any{ + "columns": []any{}, "rows": []any{}, "totalCount": 1, + } + if test.mutateRows != nil { + test.mutateRows(response["data"].(map[string]any)) + } + default: + response["data"].(map[string]any)["ok"] = true + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(response) + })) + defer server.Close() + cfg := ScenarioConfig{Connections: Connections{LoomURL: server.URL}, Namespace: Namespace{Project: "project"}, HTTPClient: &http.Client{}} + _, err := verifyGraphQL(context.Background(), cfg, publication{Recipe: "recipe", TranslationVersion: "v1", Output: "output"}, Oracle{RowCount: 1, UniquePatientCount: 1}) + if err == nil { + t.Fatal("malformed GraphQL response was accepted") + } + }) + } +} diff --git a/internal/api/README.md b/internal/api/README.md index 4a8ef2b4..8099615b 100644 --- a/internal/api/README.md +++ b/internal/api/README.md @@ -6,9 +6,7 @@ they call domain services but do not define storage or ingestion behavior. - `http` provides shared Fiber transport middleware, authentication, errors, and health checks. -- `bulk/load` owns import and raw/generation load endpoints. -- `bulk/dump` owns raw, generation, and dataframe export endpoints. -- `graphql/graph` owns the graph and FHIR/dataframe GraphQL surface. +- `bulk/load` owns the multipart generation upload and activation endpoints. - `graphql/graph` owns both the graph and ClickHouse-backed published-dataframe fields. `internal/server` composes these adapters and supplies their dependencies. diff --git a/internal/api/bulk/load/errors.go b/internal/api/bulk/load/errors.go new file mode 100644 index 00000000..66bf52aa --- /dev/null +++ b/internal/api/bulk/load/errors.go @@ -0,0 +1,54 @@ +package load + +import ( + "errors" + + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + "github.com/calypr/loom/internal/ingest" +) + +// normalizeError translates ingest-owned failures at the ingest HTTP boundary +// so the shared HTTP package does not depend on the loader implementation. +func normalizeError(err error) error { + if err == nil { + return nil + } + var preflight *ingest.PreflightError + if errors.As(err, &preflight) { + return dataframeerrors.Wrap(err, dataframeerrors.CodeIngestPreflightFailed, "", dataframeerrors.WithDetails(preflightErrorDetails(preflight))) + } + var incomplete *ingest.GenerationLoadIncompleteError + if errors.As(err, &incomplete) { + return dataframeerrors.Wrap(err, dataframeerrors.CodeGenerationLoadIncomplete, "", dataframeerrors.WithDetails(map[string]any{ + "validationErrors": incomplete.ValidationErrors, + "generationErrors": incomplete.GenerationErrors, + "edgeErrors": incomplete.EdgeErrors, + })) + } + var activation *ingest.ActivationOutcomeError + if errors.As(err, &activation) { + return dataframeerrors.Wrap(err, dataframeerrors.CodeGenerationActivationUnknown, "") + } + return err +} + +func preflightErrorDetails(err *ingest.PreflightError) map[string]any { + if err == nil || len(err.Report.Issues) == 0 { + return nil + } + issues := make([]any, 0, len(err.Report.Issues)) + for _, issue := range err.Report.Issues { + item := map[string]any{"code": issue.Code} + if issue.File != "" { + item["file"] = issue.File + } + if issue.ResourceType != "" { + item["resourceType"] = issue.ResourceType + } + if issue.Row > 0 { + item["row"] = issue.Row + } + issues = append(issues, item) + } + return map[string]any{"issues": issues} +} diff --git a/internal/api/bulk/load/errors_test.go b/internal/api/bulk/load/errors_test.go new file mode 100644 index 00000000..ecfaaf5e --- /dev/null +++ b/internal/api/bulk/load/errors_test.go @@ -0,0 +1,32 @@ +package load + +import ( + "errors" + "testing" + + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + "github.com/calypr/loom/internal/ingest" +) + +func TestNormalizeIngestionFailures(t *testing.T) { + preflight := &ingest.PreflightError{Report: ingest.PreflightReport{Issues: []ingest.PreflightIssue{{Code: "invalid_json", File: "Patient.ndjson", ResourceType: "Patient", Row: 2, Message: "raw parser detail"}}}} + userErr, ok := dataframeerrors.AsUserError(normalizeError(preflight)) + if !ok || userErr.Code() != "INGEST_PREFLIGHT_FAILED" || userErr.Retryable() { + t.Fatalf("preflight normalized = %#v", userErr) + } + if got := userErr.Details()["issues"].([]any)[0].(map[string]any); got["message"] != nil { + t.Fatal("preflight parser message leaked into details") + } + + incomplete := &ingest.GenerationLoadIncompleteError{ValidationErrors: 2, GenerationErrors: 1, EdgeErrors: 3} + userErr, ok = dataframeerrors.AsUserError(normalizeError(incomplete)) + if !ok || userErr.Code() != "GENERATION_LOAD_INCOMPLETE" || userErr.Details()["edgeErrors"] != 3 { + t.Fatalf("incomplete normalized = %#v", userErr) + } + + activation := &ingest.ActivationOutcomeError{Err: errors.New("pointer storage timeout")} + userErr, ok = dataframeerrors.AsUserError(normalizeError(activation)) + if !ok || userErr.Code() != "GENERATION_ACTIVATION_UNKNOWN" || userErr.Retryable() { + t.Fatalf("activation normalized = %#v", userErr) + } +} diff --git a/internal/api/bulk/load/generation_route.go b/internal/api/bulk/load/generation_route.go deleted file mode 100644 index 9dda82db..00000000 --- a/internal/api/bulk/load/generation_route.go +++ /dev/null @@ -1,126 +0,0 @@ -package load - -import ( - "errors" - "io" - "mime/multipart" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/calypr/loom/internal/authscope" - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/gofiber/fiber/v3" -) - -func (s *Handler) HandleCreateGeneration(c fiber.Ctx) error { - if !c.IsMultipart() { - return fiber.NewError(fiber.StatusUnsupportedMediaType) - } - form, err := c.Req().MultipartForm() - if err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidRequest, "") - } - project := strings.TrimSpace(c.Req().FormValue("project")) - generation := strings.TrimSpace(c.Req().FormValue("generation")) - if project == "" || generation == "" { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - files := form.File["file"] - if len(files) == 0 { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - authResourcePath := strings.TrimSpace(c.Req().FormValue("auth_resource_path")) - deferActivation := false - if raw := strings.TrimSpace(c.Req().FormValue("defer_activation")); raw != "" { - deferActivation, err = strconv.ParseBool(raw) - if err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidRequest, "") - } - } - principal, _ := c.Locals("principal").(*authscope.Principal) - if err := s.authz.AuthorizeWrite(c.Context(), principal, project, authResourcePath); err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") - } - authResourcePath = authscope.NormalizeAuthResourcePath(authResourcePath) - stagedDir, err := stageGenerationFiles(files) - if err != nil { - if errors.Is(err, os.ErrInvalid) { - return dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidGenerationFile, "") - } - if errors.Is(err, os.ErrExist) { - return dataframeerrors.Wrap(err, dataframeerrors.CodeDuplicateGenerationFile, "") - } - return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - } - defer os.RemoveAll(stagedDir) - req := GenerationLoadRequest{Project: project, Generation: generation, AuthResourcePath: authResourcePath, StagedDir: stagedDir, DeferActivation: deferActivation} - if principal != nil { - req.SubmittedBy = principal.Subject - } - result, err := s.service.RunGeneration(c.Context(), req) - if err != nil { - return err - } - return c.Status(fiber.StatusOK).JSON(result) -} - -func (s *Handler) HandleActivateGeneration(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - generation := strings.TrimSpace(c.Params("generation")) - executionID := strings.TrimSpace(c.Query("dataframe_execution_id")) - if project == "" || generation == "" || executionID == "" { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - authResourcePath := strings.TrimSpace(c.Query("auth_resource_path")) - principal, _ := c.Locals("principal").(*authscope.Principal) - if err := s.authz.AuthorizeWrite(c.Context(), principal, project, authResourcePath); err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") - } - if err := s.service.ActivateGeneration(c.Context(), project, generation, executionID); err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodePublicationConflict, "") - } - return c.Status(fiber.StatusOK).JSON(map[string]any{"project": project, "generation": generation, "dataframeExecutionId": executionID, "activated": true}) -} - -func stageGenerationFiles(headers []*multipart.FileHeader) (string, error) { - dir, err := os.MkdirTemp("", "loom-generation-") - if err != nil { - return "", err - } - cleanup := func() { _ = os.RemoveAll(dir) } - seen := make(map[string]struct{}, len(headers)) - for _, header := range headers { - name := filepath.Base(header.Filename) - if name == "." || name == "" || name != header.Filename || filepath.Ext(name) != ".ndjson" { - cleanup() - return "", os.ErrInvalid - } - if _, ok := seen[name]; ok { - cleanup() - return "", os.ErrExist - } - seen[name] = struct{}{} - src, err := header.Open() - if err != nil { - cleanup() - return "", err - } - dst, err := os.Create(filepath.Join(dir, name)) - if err == nil { - _, err = io.Copy(dst, src) - } - _ = src.Close() - if dst != nil { - if closeErr := dst.Close(); err == nil { - err = closeErr - } - } - if err != nil { - cleanup() - return "", err - } - } - return dir, nil -} diff --git a/internal/api/bulk/load/generation_route_test.go b/internal/api/bulk/load/generation_route_test.go deleted file mode 100644 index 94caef31..00000000 --- a/internal/api/bulk/load/generation_route_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package load - -import ( - "bytes" - "context" - "mime/multipart" - "net/http" - "net/http/httptest" - "testing" - - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/ingest" -) - -type fakeGenerationRunner struct { - summary ingest.LoadSummary - req GenerationLoadRequest -} - -func (r *fakeGenerationRunner) RunGeneration(_ context.Context, req GenerationLoadRequest, _ ingest.EventSink) (ingest.LoadSummary, error) { - r.req = req - return r.summary, nil -} - -func TestCreateGenerationStagesCompleteBundle(t *testing.T) { - runner := &fakeGenerationRunner{summary: ingest.LoadSummary{Files: 2, VerticesInserted: 4}} - svc, err := NewService(ServiceConfig{GenerationRunner: runner}) - if err != nil { - t.Fatal(err) - } - server, err := NewHTTPServer(HTTPConfig{Service: svc, Authorizer: authscope.AllowAllAuthorizer{}}) - if err != nil { - t.Fatal(err) - } - req := newGenerationMultipartRequest(t, "P1", "generation-1", map[string][]byte{ - "Patient.ndjson": []byte(`{"resourceType":"Patient","id":"1"}` + "\n"), - "Specimen.ndjson": []byte(`{"resourceType":"Specimen","id":"2"}` + "\n"), - }) - resp, err := server.App().Test(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) - } - if runner.req.Project != "P1" || runner.req.Generation != "generation-1" { - t.Fatalf("request = %#v", runner.req) - } -} - -func TestCreateGenerationPropagatesDeferredActivation(t *testing.T) { - runner := &fakeGenerationRunner{} - svc, err := NewService(ServiceConfig{GenerationRunner: runner}) - if err != nil { - t.Fatal(err) - } - server, err := NewHTTPServer(HTTPConfig{Service: svc, Authorizer: authscope.AllowAllAuthorizer{}}) - if err != nil { - t.Fatal(err) - } - req := newGenerationMultipartRequestWithDefer(t, "P1", "generation-1", true, map[string][]byte{ - "Patient.ndjson": []byte(`{"resourceType":"Patient","id":"1"}` + "\n"), - }) - resp, err := server.App().Test(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusOK) - } - if !runner.req.DeferActivation { - t.Fatalf("generation request = %#v, want deferred activation", runner.req) - } -} - -func newGenerationMultipartRequest(t *testing.T, project, generation string, files map[string][]byte) *http.Request { - return newGenerationMultipartRequestWithDefer(t, project, generation, false, files) -} - -func newGenerationMultipartRequestWithDefer(t *testing.T, project, generation string, deferActivation bool, files map[string][]byte) *http.Request { - t.Helper() - var body bytes.Buffer - writer := multipart.NewWriter(&body) - _ = writer.WriteField("project", project) - _ = writer.WriteField("generation", generation) - if deferActivation { - _ = writer.WriteField("defer_activation", "true") - } - for name, content := range files { - part, err := writer.CreateFormFile("file", name) - if err != nil { - t.Fatal(err) - } - if _, err := part.Write(content); err != nil { - t.Fatal(err) - } - } - if err := writer.Close(); err != nil { - t.Fatal(err) - } - req := httptest.NewRequest(http.MethodPost, "/api/v1/datasets/P1/generations/generation-1", &body) - req.Header.Set("Content-Type", writer.FormDataContentType()) - return req -} diff --git a/internal/api/bulk/load/generation_staging.go b/internal/api/bulk/load/generation_staging.go new file mode 100644 index 00000000..9bba0e56 --- /dev/null +++ b/internal/api/bulk/load/generation_staging.go @@ -0,0 +1,49 @@ +package load + +import ( + "io" + "mime/multipart" + "os" + "path/filepath" +) + +func stageGenerationFiles(headers []*multipart.FileHeader) (string, error) { + dir, err := os.MkdirTemp("", "loom-generation-") + if err != nil { + return "", err + } + cleanup := func() { _ = os.RemoveAll(dir) } + seen := make(map[string]struct{}, len(headers)) + for _, header := range headers { + name := filepath.Base(header.Filename) + if name == "." || name == "" || name != header.Filename || filepath.Ext(name) != ".ndjson" { + cleanup() + return "", os.ErrInvalid + } + if _, ok := seen[name]; ok { + cleanup() + return "", os.ErrExist + } + seen[name] = struct{}{} + src, err := header.Open() + if err != nil { + cleanup() + return "", err + } + dst, err := os.Create(filepath.Join(dir, name)) + if err == nil { + _, err = io.Copy(dst, src) + } + _ = src.Close() + if dst != nil { + if closeErr := dst.Close(); err == nil { + err = closeErr + } + } + if err != nil { + cleanup() + return "", err + } + } + return dir, nil +} diff --git a/internal/api/bulk/load/handler.go b/internal/api/bulk/load/handler.go index e6f9dee5..66260ef4 100644 --- a/internal/api/bulk/load/handler.go +++ b/internal/api/bulk/load/handler.go @@ -2,22 +2,18 @@ package load import ( "fmt" + "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataset" ) type Handler struct { - service *Service - authz authscope.Authorizer - snapshots *SnapshotService - releases *dataset.ReleaseService + service *Service + authz authscope.Authorizer } type Config struct { Service *Service Authorizer authscope.Authorizer - Snapshots *SnapshotService - Releases *dataset.ReleaseService } func NewHandler(cfg Config) (*Handler, error) { @@ -27,5 +23,5 @@ func NewHandler(cfg Config) (*Handler, error) { if cfg.Authorizer == nil { return nil, fmt.Errorf("load authorizer is required") } - return &Handler{service: cfg.Service, authz: cfg.Authorizer, snapshots: cfg.Snapshots, releases: cfg.Releases}, nil + return &Handler{service: cfg.Service, authz: cfg.Authorizer}, nil } diff --git a/internal/api/bulk/load/memory_lifecycle_store_test.go b/internal/api/bulk/load/memory_lifecycle_store_test.go deleted file mode 100644 index ffacf30a..00000000 --- a/internal/api/bulk/load/memory_lifecycle_store_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package load - -import ( - "context" - "fmt" - "reflect" - "sort" - "sync" - "time" - - "github.com/calypr/loom/internal/dataset" -) - -type memoryLifecycleStore struct { - mu sync.RWMutex - snapshots map[dataset.Ref]dataset.SnapshotGeneration - releases map[string]dataset.ActiveRelease - releaseRecords map[string]dataset.ProjectRelease -} - -func newMemoryLifecycleStore() *memoryLifecycleStore { - return &memoryLifecycleStore{ - snapshots: make(map[dataset.Ref]dataset.SnapshotGeneration), - releases: make(map[string]dataset.ActiveRelease), - releaseRecords: make(map[string]dataset.ProjectRelease), - } -} - -func (s *memoryLifecycleStore) SaveRelease(_ context.Context, release dataset.ProjectRelease) (dataset.ProjectRelease, error) { - s.mu.Lock() - defer s.mu.Unlock() - key := release.Project + "\x00" + release.ID - if existing, ok := s.releaseRecords[key]; ok { - if !reflect.DeepEqual(existing, release) { - return dataset.ProjectRelease{}, dataset.ErrSnapshotConflict - } - return cloneTestRelease(existing), nil - } - s.releaseRecords[key] = cloneTestRelease(release) - return cloneTestRelease(release), nil -} - -func (s *memoryLifecycleStore) ReadRelease(_ context.Context, project, releaseID string) (dataset.ProjectRelease, error) { - s.mu.RLock() - defer s.mu.RUnlock() - release, ok := s.releaseRecords[project+"\x00"+releaseID] - if !ok { - return dataset.ProjectRelease{}, dataset.ErrReleaseNotFound - } - return cloneTestRelease(release), nil -} - -func (s *memoryLifecycleStore) CreateOrResumeSnapshot(_ context.Context, candidate dataset.SnapshotGeneration) (dataset.SnapshotGeneration, error) { - if err := candidate.Validate(); err != nil { - return dataset.SnapshotGeneration{}, err - } - s.mu.Lock() - defer s.mu.Unlock() - if existing, ok := s.snapshots[candidate.Dataset]; ok { - if existing.GitCommit != candidate.GitCommit || existing.AuthResourcePath != candidate.AuthResourcePath || !reflect.DeepEqual(existing.ExpectedResourceTypes, candidate.ExpectedResourceTypes) { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotConflict - } - return cloneTestSnapshot(existing), nil - } - s.snapshots[candidate.Dataset] = cloneTestSnapshot(candidate) - return cloneTestSnapshot(candidate), nil -} - -func (s *memoryLifecycleStore) ReadSnapshot(_ context.Context, ref dataset.Ref) (dataset.SnapshotGeneration, error) { - s.mu.RLock() - defer s.mu.RUnlock() - snapshot, ok := s.snapshots[ref] - if !ok { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotNotFound - } - return cloneTestSnapshot(snapshot), nil -} - -func (s *memoryLifecycleStore) RecordSnapshotUpload(_ context.Context, ref dataset.Ref, upload dataset.ResourceUpload) (dataset.SnapshotGeneration, error) { - s.mu.Lock() - defer s.mu.Unlock() - snapshot, ok := s.snapshots[ref] - if !ok { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotNotFound - } - if snapshot.State != dataset.StateLoading { - if prior, found := snapshot.Upload(upload.ResourceType); found && prior.SHA256 == upload.SHA256 && prior.Size == upload.Size { - return cloneTestSnapshot(snapshot), nil - } - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotFinalized - } - if !includesResource(snapshot.ExpectedResourceTypes, upload.ResourceType) { - return dataset.SnapshotGeneration{}, fmt.Errorf("%w: resource type %q was not declared", dataset.ErrSnapshotConflict, upload.ResourceType) - } - for _, prior := range snapshot.Uploads { - if prior.ResourceType != upload.ResourceType { - continue - } - if prior.SHA256 != upload.SHA256 || prior.Size != upload.Size { - return dataset.SnapshotGeneration{}, dataset.ErrChecksumConflict - } - return cloneTestSnapshot(snapshot), nil - } - snapshot.Uploads = append(snapshot.Uploads, upload) - sort.Slice(snapshot.Uploads, func(i, j int) bool { return snapshot.Uploads[i].ResourceType < snapshot.Uploads[j].ResourceType }) - snapshot.UpdatedAt = upload.UploadedAt - s.snapshots[ref] = snapshot - return cloneTestSnapshot(snapshot), nil -} - -func (s *memoryLifecycleStore) TransitionSnapshot(_ context.Context, ref dataset.Ref, expected, next dataset.State, now time.Time) (dataset.SnapshotGeneration, error) { - s.mu.Lock() - defer s.mu.Unlock() - snapshot, ok := s.snapshots[ref] - if !ok { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotNotFound - } - if snapshot.State == next { - return cloneTestSnapshot(snapshot), nil - } - if snapshot.State != expected || expected != dataset.StateLoading || (next != dataset.StateStaged && next != dataset.StateFailed) { - return dataset.SnapshotGeneration{}, fmt.Errorf("%w: %s -> %s", dataset.ErrSnapshotConflict, snapshot.State, next) - } - if next == dataset.StateStaged && len(snapshot.MissingResourceTypes()) != 0 { - return dataset.SnapshotGeneration{}, dataset.ErrGenerationIncomplete - } - snapshot.State = next - snapshot.UpdatedAt = now.UTC() - if next == dataset.StateFailed { - aborted := now.UTC() - snapshot.AbortedAt = &aborted - } - s.snapshots[ref] = snapshot - return cloneTestSnapshot(snapshot), nil -} - -func (s *memoryLifecycleStore) ListSnapshotProjects(context.Context) ([]string, error) { - s.mu.RLock() - defer s.mu.RUnlock() - set := make(map[string]struct{}) - for ref := range s.snapshots { - set[ref.Project] = struct{}{} - } - return sortedTestProjects(set), nil -} - -func (s *memoryLifecycleStore) ReadActiveRelease(_ context.Context, project string) (dataset.ActiveRelease, error) { - s.mu.RLock() - defer s.mu.RUnlock() - release, ok := s.releases[project] - if !ok { - return dataset.ActiveRelease{}, dataset.ErrNoActiveRelease - } - return cloneTestActiveRelease(release), nil -} - -func (s *memoryLifecycleStore) CompareAndSwapActivateRelease(_ context.Context, release dataset.ProjectRelease, expectedRevision int64) (dataset.ActiveRelease, error) { - s.mu.Lock() - defer s.mu.Unlock() - current := s.releases[release.Project] - if current.Revision == expectedRevision+1 && current.Release.ID == release.ID { - return cloneTestActiveRelease(current), nil - } - if current.Revision != expectedRevision { - return dataset.ActiveRelease{}, dataset.ErrReleaseActivationConflict - } - active := dataset.ActiveRelease{Release: cloneTestRelease(release), Revision: expectedRevision + 1} - s.releases[release.Project] = active - return cloneTestActiveRelease(active), nil -} - -func (s *memoryLifecycleStore) ListReleaseProjects(context.Context) ([]string, error) { - s.mu.RLock() - defer s.mu.RUnlock() - set := make(map[string]struct{}, len(s.releases)) - for project := range s.releases { - set[project] = struct{}{} - } - return sortedTestProjects(set), nil -} - -func cloneTestSnapshot(value dataset.SnapshotGeneration) dataset.SnapshotGeneration { - value.ExpectedResourceTypes = append([]string(nil), value.ExpectedResourceTypes...) - value.Uploads = append([]dataset.ResourceUpload(nil), value.Uploads...) - if value.AbortedAt != nil { - copied := *value.AbortedAt - value.AbortedAt = &copied - } - return value -} - -func cloneTestRelease(value dataset.ProjectRelease) dataset.ProjectRelease { - value.Publications = append([]dataset.ReleasePublication(nil), value.Publications...) - value.RequiredVerifications = append([]dataset.ContractVerification(nil), value.RequiredVerifications...) - return value -} - -func cloneTestActiveRelease(value dataset.ActiveRelease) dataset.ActiveRelease { - value.Release = cloneTestRelease(value.Release) - return value -} -func includesResource(values []string, target string) bool { - for _, value := range values { - if value == target { - return true - } - } - return false -} -func sortedTestProjects(values map[string]struct{}) []string { - result := make([]string, 0, len(values)) - for value := range values { - result = append(result, value) - } - sort.Strings(result) - return result -} - -var _ dataset.SnapshotRepository = (*memoryLifecycleStore)(nil) -var _ dataset.ReleaseRepository = (*memoryLifecycleStore)(nil) diff --git a/internal/api/bulk/load/operations.go b/internal/api/bulk/load/operations.go new file mode 100644 index 00000000..3ec7a840 --- /dev/null +++ b/internal/api/bulk/load/operations.go @@ -0,0 +1,103 @@ +package load + +import ( + "context" + "errors" + "mime/multipart" + "os" + "strconv" + "strings" + + "github.com/calypr/loom/internal/authscope" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + publication "github.com/calypr/loom/internal/dataset" + "github.com/gofiber/fiber/v3" +) + +func (h *Handler) GetDatasetGenerationStatus(ctx context.Context, project, generation, authResourcePath string, principal *authscope.Principal) (*GenerationStatusResult, error) { + project, generation, authResourcePath = strings.TrimSpace(project), strings.TrimSpace(generation), strings.TrimSpace(authResourcePath) + if project == "" || generation == "" { + return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + if err := h.authz.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") + } + status, err := h.service.generationStatus(ctx, project, generation) + if err != nil { + if errors.Is(err, publication.ErrManifestNotFound) { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeDatasetNotFound, "") + } + return nil, normalizeError(err) + } + return status, nil +} + +// CreateDatasetGeneration parses and stages a generated multipart request, +// then runs the generation import operation. +func (h *Handler) CreateDatasetGeneration(ctx context.Context, project, generation string, body *multipart.Reader, principal *authscope.Principal) (*GenerationLoadResult, error) { + if body == nil { + return nil, fiber.NewError(fiber.StatusUnsupportedMediaType) + } + form, err := body.ReadForm(32 << 20) + if err != nil { + if strings.Contains(err.Error(), "boundary is empty") { + return nil, fiber.NewError(fiber.StatusUnsupportedMediaType) + } + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidRequest, "") + } + project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) + if project == "" || generation == "" || len(form.File["file"]) == 0 { + return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + authResourcePath := strings.TrimSpace(firstFormValue(form.Value, "auth_resource_path")) + deferActivation := false + if raw := strings.TrimSpace(firstFormValue(form.Value, "defer_activation")); raw != "" { + deferActivation, err = strconv.ParseBool(raw) + if err != nil { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidRequest, "") + } + } + if err := h.authz.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") + } + authResourcePath = authscope.NormalizeAuthResourcePath(authResourcePath) + stagedDir, err := stageGenerationFiles(form.File["file"]) + if err != nil { + switch { + case errors.Is(err, os.ErrInvalid): + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidGenerationFile, "") + case errors.Is(err, os.ErrExist): + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeDuplicateGenerationFile, "") + default: + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) + } + } + defer os.RemoveAll(stagedDir) + req := GenerationLoadRequest{Project: project, Generation: generation, AuthResourcePath: authResourcePath, StagedDir: stagedDir, DeferActivation: deferActivation} + if principal != nil { + req.SubmittedBy = principal.Subject + } + result, err := h.service.runGeneration(ctx, req) + return result, normalizeError(err) +} + +func (h *Handler) ActivateDatasetGeneration(ctx context.Context, project, generation, executionID, authResourcePath string, principal *authscope.Principal) (map[string]any, error) { + project, generation, executionID = strings.TrimSpace(project), strings.TrimSpace(generation), strings.TrimSpace(executionID) + if project == "" || generation == "" || executionID == "" { + return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + if err := h.authz.AuthorizeWrite(ctx, principal, project, strings.TrimSpace(authResourcePath)); err != nil { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") + } + if err := h.service.activateGeneration(ctx, project, generation, executionID); err != nil { + return nil, normalizeError(err) + } + return map[string]any{"project": project, "generation": generation, "dataframeExecutionId": executionID, "activated": true}, nil +} + +func firstFormValue(values map[string][]string, key string) string { + if items := values[key]; len(items) != 0 { + return items[0] + } + return "" +} diff --git a/internal/api/bulk/load/operations_test.go b/internal/api/bulk/load/operations_test.go new file mode 100644 index 00000000..b5088d9f --- /dev/null +++ b/internal/api/bulk/load/operations_test.go @@ -0,0 +1,116 @@ +package load + +import ( + "bytes" + "context" + "errors" + "mime/multipart" + "testing" + + "github.com/calypr/loom/internal/authscope" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + publication "github.com/calypr/loom/internal/dataset" + "github.com/calypr/loom/internal/ingest" +) + +type fakeGenerationRunner struct{ req GenerationLoadRequest } + +func (r *fakeGenerationRunner) RunGeneration(_ context.Context, req GenerationLoadRequest, _ ingest.EventSink) (ingest.LoadSummary, error) { + r.req = req + return ingest.LoadSummary{Files: 2, VerticesInserted: 4}, nil +} + +func TestCreateDatasetGenerationStagesCompleteBundle(t *testing.T) { + runner := &fakeGenerationRunner{} + service, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration}) + if err != nil { + t.Fatal(err) + } + handler, err := NewHandler(Config{Service: service, Authorizer: authscope.AllowAllAuthorizer{}}) + if err != nil { + t.Fatal(err) + } + body, boundary := generationMultipart(t, true, map[string][]byte{ + "Patient.ndjson": []byte("{\"resourceType\":\"Patient\",\"id\":\"1\"}\n"), + "Specimen.ndjson": []byte("{\"resourceType\":\"Specimen\",\"id\":\"2\"}\n"), + }) + result, err := handler.CreateDatasetGeneration(context.Background(), "P1", "generation-1", multipart.NewReader(bytes.NewReader(body), boundary), nil) + if err != nil { + t.Fatal(err) + } + if result.Project != "P1" || result.Generation != "generation-1" || runner.req.Project != "P1" || runner.req.Generation != "generation-1" || !runner.req.DeferActivation { + t.Fatalf("result=%#v request=%#v", result, runner.req) + } +} + +func TestGetDatasetGenerationStatusMapsMissingManifest(t *testing.T) { + service, err := NewService(ServiceConfig{ + LoadGeneration: activationGenerationRunner{}.RunGeneration, + GenerationActivator: &activationManifestStore{readErr: publication.ErrManifestNotFound}, + }) + if err != nil { + t.Fatal(err) + } + handler, err := NewHandler(Config{Service: service, Authorizer: authscope.AllowAllAuthorizer{}}) + if err != nil { + t.Fatal(err) + } + + _, err = handler.GetDatasetGenerationStatus(context.Background(), "project-a", "generation-a", "", nil) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodeDatasetNotFound) { + t.Fatalf("status error = %v, want DATASET_NOT_FOUND", err) + } +} + +func TestGetDatasetGenerationStatusAuthorizesBeforeManifestLookup(t *testing.T) { + store := &activationManifestStore{manifest: activationManifest(t, "project-a", "generation-a")} + service, err := NewService(ServiceConfig{ + LoadGeneration: activationGenerationRunner{}.RunGeneration, + GenerationActivator: store, + }) + if err != nil { + t.Fatal(err) + } + handler, err := NewHandler(Config{Service: service, Authorizer: rejectingAuthorizer{}}) + if err != nil { + t.Fatal(err) + } + + _, err = handler.GetDatasetGenerationStatus(context.Background(), "project-a", "generation-a", "", nil) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodeForbidden) { + t.Fatalf("status error = %v, want FORBIDDEN", err) + } + if store.readCalls != 0 { + t.Fatalf("manifest reads = %d, want 0", store.readCalls) + } +} + +type rejectingAuthorizer struct{} + +func (rejectingAuthorizer) AuthorizeWrite(context.Context, *authscope.Principal, string, string) error { + return errors.New("denied") +} + +func generationMultipart(t *testing.T, deferActivation bool, files map[string][]byte) ([]byte, string) { + t.Helper() + var body bytes.Buffer + writer := multipart.NewWriter(&body) + if deferActivation { + _ = writer.WriteField("defer_activation", "true") + } + for name, content := range files { + part, err := writer.CreateFormFile("file", name) + if err != nil { + t.Fatal(err) + } + if _, err := part.Write(content); err != nil { + t.Fatal(err) + } + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + return body.Bytes(), writer.Boundary() +} diff --git a/internal/api/bulk/load/raw_load.go b/internal/api/bulk/load/raw_load.go deleted file mode 100644 index 340dc07a..00000000 --- a/internal/api/bulk/load/raw_load.go +++ /dev/null @@ -1,57 +0,0 @@ -package load - -import ( - "mime" - "os" - "strings" - - "github.com/calypr/loom/internal/authscope" - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/calypr/loom/internal/ingest" - "github.com/gofiber/fiber/v3" -) - -const defaultRawProject = "default" - -func (s *Handler) HandleLoadRaw(c fiber.Ctx) error { - contentType, _, err := mime.ParseMediaType(c.Get(fiber.HeaderContentType)) - if err != nil || (contentType != "application/x-ndjson" && contentType != "application/ndjson") { - return fiber.NewError(fiber.StatusUnsupportedMediaType) - } - project := strings.TrimSpace(c.Query("project")) - if project == "" { - project = defaultRawProject - } - authResourcePath := authscope.NormalizeAuthResourcePath(strings.TrimSpace(c.Query("auth_resource_path"))) - principal, _ := c.Locals("principal").(*authscope.Principal) - if err := s.authz.AuthorizeWrite(c.Context(), principal, project, authResourcePath); err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") - } - - dir, err := os.MkdirTemp("", "loom-raw-") - if err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - } - defer os.RemoveAll(dir) - rows, err := ingest.PartitionNDJSON(c.Request().BodyStream(), dir) - if err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidData, "") - } - req := GenerationLoadRequest{ - Project: project, - Generation: strings.TrimSpace(c.Query("generation")), - AuthResourcePath: authResourcePath, - StagedDir: dir, - } - if principal != nil { - req.SubmittedBy = principal.Subject - } - if req.Generation == "" { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - result, err := s.service.RunGeneration(c.Context(), req) - if err != nil { - return err - } - return c.Status(fiber.StatusOK).JSON(fiber.Map{"rows": rows, "result": result}) -} diff --git a/internal/api/bulk/load/resource_route.go b/internal/api/bulk/load/resource_route.go deleted file mode 100644 index 8fdd7ab5..00000000 --- a/internal/api/bulk/load/resource_route.go +++ /dev/null @@ -1,95 +0,0 @@ -package load - -import ( - "io" - "mime/multipart" - "os" - "path/filepath" - "strings" - - "github.com/calypr/loom/internal/authscope" - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/gofiber/fiber/v3" -) - -// HandleResource implements the project resource upload transport operation. -func (h *Handler) HandleResource(c fiber.Ctx) error { - if !c.IsMultipart() { - return fiber.NewError(fiber.StatusUnsupportedMediaType) - } - project := strings.TrimSpace(c.Params("project")) - resourceType := strings.TrimSpace(c.Params("resourceType")) - if project == "" { - return dataframeerrors.NewError(dataframeerrors.CodeProjectRequired, "") - } - if resourceType == "" { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidResourceType, "") - } - authResourcePath := strings.TrimSpace(c.Req().FormValue("auth_resource_path")) - principal, _ := c.Locals("principal").(*authscope.Principal) - if err := h.authz.AuthorizeWrite(c.Context(), principal, project, authResourcePath); err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") - } - authResourcePath = authscope.NormalizeAuthResourcePath(authResourcePath) - - fileHeader, err := c.Req().FormFile("file") - if err != nil { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - stagedPath, err := stageUploadedFile(fileHeader) - if err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - } - defer os.Remove(stagedPath) - - req := ImportRequest{ - Project: project, - ResourceType: resourceType, - AuthResourcePath: authResourcePath, - StagedFilePath: stagedPath, - OriginalFilename: fileHeader.Filename, - } - if principal != nil { - req.SubmittedBy = principal.Subject - } - result, err := h.service.Run(c.Context(), req) - if err != nil { - return dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidData, "") - } - - return c.Status(fiber.StatusOK).JSON(fiber.Map{ - "project": result.Project, - "resource_type": result.ResourceType, - "auth_resource_path": result.AuthResourcePath, - "original_filename": result.OriginalFilename, - "submitted_by": result.SubmittedBy, - "summary": result.Summary, - }) -} - -func stageUploadedFile(fileHeader *multipart.FileHeader) (string, error) { - src, err := fileHeader.Open() - if err != nil { - return "", err - } - defer src.Close() - - ext := filepath.Ext(fileHeader.Filename) - if ext == "" { - ext = ".ndjson" - } - dst, err := os.CreateTemp("", "arango-fhir-upload-*"+ext) - if err != nil { - return "", err - } - if _, err := io.Copy(dst, src); err != nil { - _ = dst.Close() - _ = os.Remove(dst.Name()) - return "", err - } - if err := dst.Close(); err != nil { - _ = os.Remove(dst.Name()) - return "", err - } - return dst.Name(), nil -} diff --git a/internal/api/bulk/load/resource_route_test.go b/internal/api/bulk/load/resource_route_test.go deleted file mode 100644 index 792c0451..00000000 --- a/internal/api/bulk/load/resource_route_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package load - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "mime/multipart" - "net/http" - "net/http/httptest" - "os" - "testing" - - httpapi "github.com/calypr/loom/internal/api/http" - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/ingest" -) - -type capturingResourceRunner struct { - req ImportRequest - content []byte - summary ingest.LoadSummary -} - -func (r *capturingResourceRunner) Run(_ context.Context, req ImportRequest, _ ingest.EventSink) (ingest.LoadSummary, error) { - r.req = req - content, err := os.ReadFile(req.StagedFilePath) - if err != nil { - return ingest.LoadSummary{}, err - } - r.content = content - return r.summary, nil -} - -func TestResourceRouteUsesPathIdentityAndMultipartFile(t *testing.T) { - runner := &capturingResourceRunner{summary: ingest.LoadSummary{Files: 1, VerticesInserted: 2}} - service, err := NewService(ServiceConfig{Runner: runner}) - if err != nil { - t.Fatal(err) - } - server, err := httpapi.NewHTTPServer(httpapi.HTTPConfig{Authenticator: authscope.StaticAuthenticator{}, Authorizer: authscope.AllowAllAuthorizer{}}) - if err != nil { - t.Fatal(err) - } - handler, err := NewHandler(Config{Service: service, Authorizer: authscope.AllowAllAuthorizer{}}) - if err != nil { - t.Fatal(err) - } - server.App().Put("/api/v1/projects/:project/resources/:resourceType", handler.HandleResource) - - req := resourceMultipartRequest(t, map[string]string{"auth_resource_path": "/programs/p1/projects/p1"}, "file", "Patient.ndjson", []byte("{\"resourceType\":\"Patient\",\"id\":\"1\"}\n")) - req.Method = http.MethodPut - req.URL.Path = "/api/v1/projects/P1/resources/Patient" - resp, err := server.App().Test(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d", resp.StatusCode) - } - if runner.req.Project != "P1" || runner.req.ResourceType != "Patient" { - t.Fatalf("path identity not passed to loader: %+v", runner.req) - } - if runner.req.AuthResourcePath != "p1-p1" { - t.Fatalf("auth resource path = %q, want normalized project key", runner.req.AuthResourcePath) - } - if runner.req.Truncate { - t.Fatal("resource route must use overwrite mode, not truncate mode") - } - if string(runner.content) != "{\"resourceType\":\"Patient\",\"id\":\"1\"}\n" { - t.Fatalf("unexpected staged content: %q", runner.content) - } - if _, err := os.Stat(runner.req.StagedFilePath); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("staged upload still exists after request: %v", err) - } - var payload map[string]any - if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { - t.Fatal(err) - } - if payload["project"] != "P1" || payload["resource_type"] != "Patient" { - t.Fatalf("unexpected response: %#v", payload) - } -} - -func resourceMultipartRequest(t *testing.T, fields map[string]string, fileField, fileName string, content []byte) *http.Request { - t.Helper() - var body bytes.Buffer - writer := multipart.NewWriter(&body) - for key, value := range fields { - if err := writer.WriteField(key, value); err != nil { - t.Fatal(err) - } - } - part, err := writer.CreateFormFile(fileField, fileName) - if err != nil { - t.Fatal(err) - } - if _, err := part.Write(content); err != nil { - t.Fatal(err) - } - if err := writer.Close(); err != nil { - t.Fatal(err) - } - req := httptest.NewRequest(http.MethodPut, "/", &body) - req.Header.Set("Content-Type", writer.FormDataContentType()) - return req -} diff --git a/internal/api/bulk/load/service.go b/internal/api/bulk/load/service.go index 420fd8df..fc3e4124 100644 --- a/internal/api/bulk/load/service.go +++ b/internal/api/bulk/load/service.go @@ -17,30 +17,9 @@ type GenerationLoadRequest struct { AuthResourcePath string StagedDir string SubmittedBy string - StageOnly bool DeferActivation bool } -type ImportRequest struct { - Project string `json:"project"` - ResourceType string `json:"resource_type"` - AuthResourcePath string `json:"auth_resource_path,omitempty"` - Truncate bool `json:"truncate"` - UseGeneric bool `json:"use_generic"` - StagedFilePath string `json:"-"` - OriginalFilename string `json:"original_filename"` - SubmittedBy string `json:"submitted_by,omitempty"` -} - -type ImportResult struct { - Project string `json:"project"` - ResourceType string `json:"resource_type"` - AuthResourcePath string `json:"auth_resource_path,omitempty"` - OriginalFilename string `json:"original_filename"` - SubmittedBy string `json:"submitted_by,omitempty"` - Summary *ingest.LoadSummary `json:"summary,omitempty"` -} - type GenerationLoadResult struct { Project string `json:"project"` Generation string `json:"generation"` @@ -51,6 +30,13 @@ type GenerationLoadResult struct { Reused bool `json:"reused,omitempty"` } +type GenerationStatusResult struct { + Project string `json:"project"` + Generation string `json:"generation"` + State publication.State `json:"state"` + Reusable bool `json:"reusable"` +} + type GenerationActivator interface { ReadManifest(context.Context, publication.Ref) (publication.Manifest, error) Activate(context.Context, publication.Manifest) error @@ -61,28 +47,12 @@ type DataframeReleaseStore interface { GetPointer(context.Context, string) (dataframepublication.BundlePointer, error) } -type GenerationRunner interface { - RunGeneration(ctx context.Context, req GenerationLoadRequest, sink ingest.EventSink) (ingest.LoadSummary, error) -} - -type Runner interface { - Run(ctx context.Context, req ImportRequest, sink ingest.EventSink) (ingest.LoadSummary, error) -} +type GenerationLoader func(context.Context, GenerationLoadRequest, ingest.EventSink) (ingest.LoadSummary, error) type IngestRunner struct { BaseOptions ingest.LoadOptions } -func (r IngestRunner) Run(ctx context.Context, req ImportRequest, sink ingest.EventSink) (ingest.LoadSummary, error) { - opts := r.BaseOptions - opts.Project = req.Project - opts.AuthResourcePath = req.AuthResourcePath - opts.Truncate = req.Truncate - opts.UseGeneric = req.UseGeneric - opts.EventSink = sink - return ingest.LoadSingleResourceFile(ctx, opts, req.ResourceType, req.StagedFilePath) -} - func (r IngestRunner) RunGeneration(ctx context.Context, req GenerationLoadRequest, sink ingest.EventSink) (ingest.LoadSummary, error) { ref, err := publication.NewRef(req.Project, req.Generation) if err != nil { @@ -96,13 +66,13 @@ func (r IngestRunner) RunGeneration(ctx context.Context, req GenerationLoadReque opts.DeferActivation = req.DeferActivation opts.Truncate = false opts.EventSink = sink - opts.StageOnly = req.StageOnly + // StageOnly is a CLI mode. The HTTP API stages through DeferActivation. + opts.StageOnly = false return ingest.Load(ctx, opts) } type ServiceConfig struct { - Runner Runner - GenerationRunner GenerationRunner + LoadGeneration GenerationLoader Logger *slog.Logger OnSuccess func(project string) GenerationActivator GenerationActivator @@ -110,8 +80,7 @@ type ServiceConfig struct { } type Service struct { - runner Runner - generationRunner GenerationRunner + loadGeneration GenerationLoader logger *slog.Logger onSuccess func(project string) generationActivator GenerationActivator @@ -119,15 +88,14 @@ type Service struct { } func NewService(cfg ServiceConfig) (*Service, error) { - if cfg.Runner == nil && cfg.GenerationRunner == nil { + if cfg.LoadGeneration == nil { return nil, errors.New("load runner is required") } if cfg.Logger == nil { cfg.Logger = slog.Default() } return &Service{ - runner: cfg.Runner, - generationRunner: cfg.GenerationRunner, + loadGeneration: cfg.LoadGeneration, logger: cfg.Logger, onSuccess: cfg.OnSuccess, generationActivator: cfg.GenerationActivator, @@ -135,44 +103,28 @@ func NewService(cfg ServiceConfig) (*Service, error) { }, nil } -func (s *Service) Run(ctx context.Context, req ImportRequest) (*ImportResult, error) { - if s.runner == nil { - return nil, errors.New("resource runner is not configured") +func (s *Service) generationStatus(ctx context.Context, project, generation string) (*GenerationStatusResult, error) { + if project == "" || generation == "" { + return nil, errors.New("project and generation are required") } - if req.Project == "" { - return nil, errors.New("project is required") + if s.generationActivator == nil { + return nil, errors.New("generation manifest reader is not configured") } - if req.ResourceType == "" { - return nil, errors.New("resource_type is required") - } - if req.StagedFilePath == "" { - return nil, errors.New("staged file path is required") - } - - summary, err := s.runner.Run(ctx, req, nil) + ref, err := publication.NewRef(project, generation) if err != nil { - s.logger.Error("resource load failed", "project", req.Project, "resource_type", req.ResourceType, "error", err.Error()) return nil, err } - if s.onSuccess != nil { - s.onSuccess(req.Project) + manifest, err := s.generationActivator.ReadManifest(ctx, ref) + if err != nil { + return nil, err } - s.logger.Info("resource load succeeded", "project", req.Project, "resource_type", req.ResourceType, "vertices", summary.VerticesInserted, "edges", summary.EdgesInserted) - summaryCopy := summary - return &ImportResult{ - Project: req.Project, - ResourceType: req.ResourceType, - AuthResourcePath: req.AuthResourcePath, - OriginalFilename: req.OriginalFilename, - SubmittedBy: req.SubmittedBy, - Summary: &summaryCopy, + return &GenerationStatusResult{ + Project: ref.Project, Generation: ref.Generation, + State: manifest.State, Reusable: manifest.IsStaged(), }, nil } -func (s *Service) RunGeneration(ctx context.Context, req GenerationLoadRequest) (*GenerationLoadResult, error) { - if s.generationRunner == nil { - return nil, errors.New("generation runner is not configured") - } +func (s *Service) runGeneration(ctx context.Context, req GenerationLoadRequest) (*GenerationLoadResult, error) { if req.Project == "" || req.Generation == "" || req.StagedDir == "" { return nil, errors.New("project, generation, and staged directory are required") } @@ -196,7 +148,7 @@ func (s *Service) RunGeneration(ctx context.Context, req GenerationLoadRequest) return nil, fmt.Errorf("inspect existing generation: %w", err) } } - summary, err := s.generationRunner.RunGeneration(ctx, req, nil) + summary, err := s.loadGeneration(ctx, req, nil) if err != nil { s.logger.Error("generation load failed", "project", req.Project, "generation", req.Generation, "error", err.Error()) return nil, err @@ -205,13 +157,13 @@ func (s *Service) RunGeneration(ctx context.Context, req GenerationLoadRequest) s.onSuccess(req.Project) } s.logger.Info("generation load succeeded", "project", req.Project, "generation", req.Generation, "vertices", summary.VerticesInserted, "edges", summary.EdgesInserted) - return &GenerationLoadResult{Project: req.Project, Generation: req.Generation, AuthResourcePath: req.AuthResourcePath, SubmittedBy: req.SubmittedBy, Summary: &summary, Activated: !req.DeferActivation && !req.StageOnly}, nil + return &GenerationLoadResult{Project: req.Project, Generation: req.Generation, AuthResourcePath: req.AuthResourcePath, SubmittedBy: req.SubmittedBy, Summary: &summary, Activated: !req.DeferActivation}, nil } -// ActivateGeneration performs the release switch only after the exact +// activateGeneration performs the release switch only after the exact // dataframe bundle supplied by the caller is durably successful and remains // the active pointer for its recipe/generation namespace. -func (s *Service) ActivateGeneration(ctx context.Context, project, generation, executionID string) error { +func (s *Service) activateGeneration(ctx context.Context, project, generation, executionID string) error { fail := func(err error) error { if err != nil { s.logger.Error("generation release activation failed", "project", project, "generation", generation, "dataframe_execution_id", executionID, "error", err) diff --git a/internal/api/bulk/load/service_test.go b/internal/api/bulk/load/service_test.go index bed03bf0..4026f835 100644 --- a/internal/api/bulk/load/service_test.go +++ b/internal/api/bulk/load/service_test.go @@ -64,12 +64,12 @@ func (s *activationReleaseStore) GetPointer(context.Context, string) (dataframep func TestRunGenerationPropagatesDeferActivationAndResult(t *testing.T) { runner := &activationCapturingRunner{} - svc, err := NewService(ServiceConfig{GenerationRunner: runner}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration}) if err != nil { t.Fatal(err) } for _, deferred := range []bool{false, true} { - result, err := svc.RunGeneration(context.Background(), GenerationLoadRequest{Project: "project-a", Generation: "generation-a", StagedDir: t.TempDir(), DeferActivation: deferred}) + result, err := svc.runGeneration(context.Background(), GenerationLoadRequest{Project: "project-a", Generation: "generation-a", StagedDir: t.TempDir(), DeferActivation: deferred}) if err != nil { t.Fatal(err) } @@ -97,11 +97,11 @@ func TestRunGenerationReusesReadyManifest(t *testing.T) { runner := &activationCapturingRunner{} manifest := activationManifest(t, "project-a", "generation-a") store := &activationManifestStore{manifest: manifest} - svc, err := NewService(ServiceConfig{GenerationRunner: runner, GenerationActivator: store}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: store}) if err != nil { t.Fatal(err) } - result, err := svc.RunGeneration(context.Background(), GenerationLoadRequest{ + result, err := svc.runGeneration(context.Background(), GenerationLoadRequest{ Project: "project-a", Generation: "generation-a", StagedDir: t.TempDir(), DeferActivation: true, }) if err != nil { @@ -115,14 +115,53 @@ func TestRunGenerationReusesReadyManifest(t *testing.T) { } } +func TestGenerationStatusReportsReusableManifest(t *testing.T) { + manifest := activationManifest(t, "project-a", "generation-a") + store := &activationManifestStore{manifest: manifest} + svc, err := NewService(ServiceConfig{ + LoadGeneration: activationGenerationRunner{}.RunGeneration, + GenerationActivator: store, + }) + if err != nil { + t.Fatal(err) + } + + status, err := svc.generationStatus(context.Background(), "project-a", "generation-a") + if err != nil { + t.Fatal(err) + } + if status.Project != "project-a" || status.Generation != "generation-a" || status.State != publication.StateReady || !status.Reusable { + t.Fatalf("status = %#v", status) + } + if store.readCalls != 1 { + t.Fatalf("manifest reads = %d, want 1", store.readCalls) + } +} + +func TestGenerationStatusPreservesManifestNotFound(t *testing.T) { + store := &activationManifestStore{readErr: fmt.Errorf("lookup: %w", publication.ErrManifestNotFound)} + svc, err := NewService(ServiceConfig{ + LoadGeneration: activationGenerationRunner{}.RunGeneration, + GenerationActivator: store, + }) + if err != nil { + t.Fatal(err) + } + + _, err = svc.generationStatus(context.Background(), "project-a", "generation-a") + if !errors.Is(err, publication.ErrManifestNotFound) { + t.Fatalf("generationStatus error = %v, want ErrManifestNotFound", err) + } +} + func TestRunGenerationLoadsWhenManifestDoesNotExist(t *testing.T) { runner := &activationCapturingRunner{} store := &activationManifestStore{readErr: fmt.Errorf("lookup: %w", publication.ErrManifestNotFound)} - svc, err := NewService(ServiceConfig{GenerationRunner: runner, GenerationActivator: store}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: store}) if err != nil { t.Fatal(err) } - result, err := svc.RunGeneration(context.Background(), GenerationLoadRequest{ + result, err := svc.runGeneration(context.Background(), GenerationLoadRequest{ Project: "project-a", Generation: "generation-a", StagedDir: t.TempDir(), DeferActivation: true, }) if err != nil { @@ -136,11 +175,11 @@ func TestRunGenerationLoadsWhenManifestDoesNotExist(t *testing.T) { func TestRunGenerationRejectsManifestLookupFailure(t *testing.T) { runner := &activationCapturingRunner{} store := &activationManifestStore{readErr: errors.New("database unavailable")} - svc, err := NewService(ServiceConfig{GenerationRunner: runner, GenerationActivator: store}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: store}) if err != nil { t.Fatal(err) } - _, err = svc.RunGeneration(context.Background(), GenerationLoadRequest{ + _, err = svc.runGeneration(context.Background(), GenerationLoadRequest{ Project: "project-a", Generation: "generation-a", StagedDir: t.TempDir(), DeferActivation: true, }) if err == nil || runner.calls != 0 { @@ -186,11 +225,12 @@ func TestActivateGenerationRejectsInvalidDataframeRelease(t *testing.T) { test.mutate(&execution, &pointer) activator := &activationManifestStore{manifest: readyManifest} releases := &activationReleaseStore{execution: execution, pointer: pointer} - svc, err := NewService(ServiceConfig{GenerationRunner: activationGenerationRunner{}, GenerationActivator: activator, DataframeReleases: releases}) + runner := activationGenerationRunner{} + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: activator, DataframeReleases: releases}) if err != nil { t.Fatal(err) } - if err := svc.ActivateGeneration(context.Background(), "project-a", "generation-a", execution.ID); err == nil { + if err := svc.activateGeneration(context.Background(), "project-a", "generation-a", execution.ID); err == nil { t.Fatal("ActivateGeneration() unexpectedly succeeded") } if activator.readCalls != 0 || activator.activateCalls != 0 { @@ -213,7 +253,7 @@ func TestActivateGenerationActivatesPublishedAllOutputBundle(t *testing.T) { activator := &activationManifestStore{manifest: manifest} calledSuccess := false svc, err := NewService(ServiceConfig{ - GenerationRunner: activationGenerationRunner{}, + LoadGeneration: activationGenerationRunner{}.RunGeneration, GenerationActivator: activator, DataframeReleases: &activationReleaseStore{execution: execution, pointer: dataframepublication.BundlePointer{Name: execution.PointerName(), ExecutionID: execution.ID}}, OnSuccess: func(string) { calledSuccess = true }, @@ -221,7 +261,7 @@ func TestActivateGenerationActivatesPublishedAllOutputBundle(t *testing.T) { if err != nil { t.Fatal(err) } - if err := svc.ActivateGeneration(context.Background(), "project-a", "generation-a", execution.ID); err != nil { + if err := svc.activateGeneration(context.Background(), "project-a", "generation-a", execution.ID); err != nil { t.Fatal(err) } if activator.readCalls != 1 || activator.activateCalls != 1 || !calledSuccess { diff --git a/internal/api/bulk/load/snapshot_route.go b/internal/api/bulk/load/snapshot_route.go deleted file mode 100644 index fef38007..00000000 --- a/internal/api/bulk/load/snapshot_route.go +++ /dev/null @@ -1,231 +0,0 @@ -package load - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - httpapi "github.com/calypr/loom/internal/api/http" - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataset" - "github.com/gofiber/fiber/v3" -) - -type createSnapshotRequest struct { - GitCommit string `json:"gitCommit"` - ExpectedResourceTypes []string `json:"expectedResourceTypes"` - AuthResourcePath string `json:"authResourcePath,omitempty"` -} - -func (h *Handler) HandleCreateSnapshot(c fiber.Ctx) error { - project, generation := strings.TrimSpace(c.Params("project")), strings.TrimSpace(c.Params("generation")) - var request createSnapshotRequest - if err := json.Unmarshal(c.Body(), &request); err != nil { - return writeSnapshotError(c, err) - } - if request.GitCommit == "" { - request.GitCommit = generation - } - if request.GitCommit != generation { - return writeSnapshotError(c, dataset.ErrSnapshotConflict) - } - if err := h.authorizeSnapshot(c, project, request.AuthResourcePath); err != nil { - return err - } - result, err := h.snapshots.CreateOrResume(c.Context(), project, generation, request.AuthResourcePath, request.ExpectedResourceTypes) - if err != nil { - return writeSnapshotError(c, err) - } - return c.Status(http.StatusOK).JSON(result) -} - -func (h *Handler) HandleSnapshotStatus(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - result, err := h.snapshots.Status(c.Context(), project, strings.TrimSpace(c.Params("generation"))) - if err != nil { - return writeSnapshotError(c, err) - } - if err := h.authorizeSnapshot(c, project, result.AuthResourcePath); err != nil { - return err - } - return c.Status(http.StatusOK).JSON(result) -} - -func (h *Handler) HandleUploadSnapshotResource(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - generation := strings.TrimSpace(c.Params("generation")) - status, err := h.snapshots.Status(c.Context(), project, generation) - if err != nil { - return writeSnapshotError(c, err) - } - if err := h.authorizeSnapshot(c, project, status.AuthResourcePath); err != nil { - return err - } - result, err := h.snapshots.Upload(c.Context(), project, generation, strings.TrimSpace(c.Params("resourceType")), c.Get("X-Content-SHA256"), c.Body()) - if err != nil { - return writeSnapshotError(c, err) - } - return c.Status(http.StatusOK).JSON(result) -} - -func (h *Handler) HandleFinalizeSnapshot(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - generationID := strings.TrimSpace(c.Params("generation")) - status, err := h.snapshots.Status(c.Context(), project, generationID) - if err != nil { - return writeSnapshotError(c, err) - } - if err := h.authorizeSnapshot(c, project, status.AuthResourcePath); err != nil { - return err - } - principal, _ := c.Locals("principal").(*authscope.Principal) - submittedBy := "" - if principal != nil { - submittedBy = principal.Subject - } - generation, load, err := h.snapshots.Finalize(c.Context(), project, generationID, submittedBy) - if err != nil { - return writeSnapshotError(c, err) - } - return c.Status(http.StatusOK).JSON(fiber.Map{"generation": generation, "load": load}) -} - -func (h *Handler) HandleAbortSnapshot(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - generationID := strings.TrimSpace(c.Params("generation")) - status, err := h.snapshots.Status(c.Context(), project, generationID) - if err != nil { - return writeSnapshotError(c, err) - } - if err := h.authorizeSnapshot(c, project, status.AuthResourcePath); err != nil { - return err - } - result, err := h.snapshots.Abort(c.Context(), project, generationID) - if err != nil { - return writeSnapshotError(c, err) - } - return c.Status(http.StatusOK).JSON(result) -} - -func (h *Handler) HandleActivateReleaseCompatibility(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - var request dataset.ActivationRequest - if err := json.Unmarshal(c.Body(), &request); err != nil { - return writeSnapshotError(c, err) - } - request.Project = project - if err := h.authorizeReleaseGeneration(c, project, request.Generation); err != nil { - return err - } - result, err := h.releases.Activate(c.Context(), request) - if err != nil { - return writeSnapshotError(c, err) - } - return c.Status(http.StatusOK).JSON(result) -} - -func (h *Handler) HandleCreateRelease(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - var request dataset.ActivationRequest - if err := json.Unmarshal(c.Body(), &request); err != nil { - return writeSnapshotError(c, err) - } - request.Project = project - if err := h.authorizeReleaseGeneration(c, project, request.Generation); err != nil { - return err - } - release, err := h.releases.Create(c.Context(), request) - if err != nil { - return writeSnapshotError(c, err) - } - return c.Status(http.StatusOK).JSON(release) -} - -func (h *Handler) HandleActivateRelease(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - releaseID := strings.TrimSpace(c.Params("release")) - release, err := h.releases.Releases.ReadRelease(c.Context(), project, releaseID) - if err != nil { - return writeSnapshotError(c, err) - } - if err := h.authorizeReleaseGeneration(c, project, release.Generation); err != nil { - return err - } - var request struct { - ExpectedRevision int64 `json:"expectedRevision"` - } - if err := json.Unmarshal(c.Body(), &request); err != nil { - return writeSnapshotError(c, err) - } - active, err := h.releases.ActivateExisting(c.Context(), project, releaseID, request.ExpectedRevision) - if err != nil { - return writeSnapshotError(c, err) - } - return c.Status(http.StatusOK).JSON(active) -} - -func (h *Handler) HandleActiveRelease(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - active, err := h.releases.Active(c.Context(), project) - if err != nil { - return writeSnapshotError(c, err) - } - if err := h.authorizeReleaseGeneration(c, project, active.Release.Generation); err != nil { - return err - } - return c.Status(http.StatusOK).JSON(active) -} - -func (h *Handler) HandleReleaseStatus(c fiber.Ctx) error { - project := strings.TrimSpace(c.Params("project")) - release, err := h.releases.Releases.ReadRelease(c.Context(), project, strings.TrimSpace(c.Params("release"))) - if err != nil { - return writeSnapshotError(c, err) - } - if err := h.authorizeReleaseGeneration(c, project, release.Generation); err != nil { - return err - } - return c.Status(http.StatusOK).JSON(release) -} - -func (h *Handler) authorizeSnapshot(c fiber.Ctx, project, authResourcePath string) error { - principal, _ := c.Locals("principal").(*authscope.Principal) - return h.authz.AuthorizeWrite(c.Context(), principal, project, authResourcePath) -} - -func (h *Handler) authorizeReleaseGeneration(c fiber.Ctx, project, generation string) error { - status, err := h.snapshots.Status(c.Context(), project, generation) - if err != nil { - return writeSnapshotError(c, err) - } - return h.authorizeSnapshot(c, project, status.AuthResourcePath) -} - -func writeSnapshotError(c fiber.Ctx, err error) error { - status, code, message, retryable := http.StatusBadRequest, "INVALID_REQUEST", "the request is invalid", false - switch { - case errors.Is(err, dataset.ErrSnapshotNotFound), errors.Is(err, dataset.ErrReleaseNotFound), errors.Is(err, dataset.ErrNoActiveRelease): - status, code, message = http.StatusNotFound, "DATASET_NOT_FOUND", "the requested dataset was not found" - case errors.Is(err, dataset.ErrChecksumConflict), errors.Is(err, dataset.ErrSnapshotConflict): - status, code, message = http.StatusConflict, "CHECKSUM_CONFLICT", "immutable snapshot content conflicts with an existing upload" - case errors.Is(err, dataset.ErrGenerationIncomplete): - status, code, message = http.StatusUnprocessableEntity, "GENERATION_INCOMPLETE", "the generation is missing required resource uploads" - case errors.Is(err, dataset.ErrSnapshotFinalized), errors.Is(err, dataset.ErrSnapshotAborted): - status, code, message = http.StatusConflict, "CHECKSUM_CONFLICT", "the immutable generation can no longer be changed" - case errors.Is(err, dataset.ErrReleaseRequirementsUnmet): - status, code, message = http.StatusUnprocessableEntity, "RELEASE_REQUIREMENTS_UNMET", "required dataframe publications are not ready for activation" - case errors.Is(err, dataset.ErrReleaseActivationConflict): - status, code, message = http.StatusConflict, "RELEASE_ACTIVATION_CONFLICT", "the active project release changed; reload it before retrying" - default: - if err == nil { - return nil - } - } - details := map[string]any(nil) - var requirements *dataset.ReleaseRequirementsError - if errors.As(err, &requirements) { - details = map[string]any{"verifications": requirements.Verifications} - } - return c.Status(status).JSON(httpapi.ErrorResponse{Error: httpapi.HTTPErrorBody{Code: code, Message: message, Details: details, Retryable: retryable, RequestID: httpapi.RequestIDFromContext(c.Context())}}) -} diff --git a/internal/api/bulk/load/snapshot_route_test.go b/internal/api/bulk/load/snapshot_route_test.go deleted file mode 100644 index ae3ac229..00000000 --- a/internal/api/bulk/load/snapshot_route_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package load - -import ( - "bytes" - "context" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "testing" - "time" - - httpapi "github.com/calypr/loom/internal/api/http" - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataset" -) - -type routePublicationVerifier struct { - result dataset.PublicationVerification -} - -func (v routePublicationVerifier) VerifyPublication(context.Context, string, string, dataset.DataframeSelector) (dataset.PublicationVerification, error) { - return v.result, nil -} - -func TestSnapshotHTTPContractCreateUploadFinalizeStatusAndAbort(t *testing.T) { - store := newMemoryLifecycleStore() - runner := &snapshotRunnerFixture{} - snapshots := &SnapshotService{Repository: store, Blobs: LocalSnapshotBlobs{Root: t.TempDir()}, Runner: runner} - selector := dataset.DataframeSelector{Recipe: "core", TranslationVersion: "v1", Output: "Patient"} - releases := &dataset.ReleaseService{Snapshots: store, Releases: store, Verifier: routePublicationVerifier{result: dataset.PublicationVerification{ - Selector: selector, ExecutionID: "execution-a", Generation: "commit-a", State: "PUBLISHED", Queryable: true, VerifiedAt: time.Now().UTC(), - }}, Required: []dataset.DataframeSelector{selector}} - baseService, err := NewService(ServiceConfig{GenerationRunner: runner}) - if err != nil { - t.Fatal(err) - } - server, err := httpapi.NewHTTPServer(httpapi.HTTPConfig{Authenticator: authscope.StaticAuthenticator{}, Authorizer: authscope.AllowAllAuthorizer{}}) - if err != nil { - t.Fatal(err) - } - handler, err := NewHandler(Config{Service: baseService, Authorizer: authscope.AllowAllAuthorizer{}, Snapshots: snapshots, Releases: releases}) - if err != nil { - t.Fatal(err) - } - registerTestSnapshotRoutes(server.App(), handler) - - createBody := []byte(`{"gitCommit":"commit-a","expectedResourceTypes":["Patient"]}`) - for range 2 { - response := requestSnapshot(t, server, http.MethodPost, "/api/v1/projects/project-a/generations/commit-a", createBody, "") - if response.StatusCode != http.StatusOK { - t.Fatalf("create status = %d, body=%s", response.StatusCode, readBody(response)) - } - _ = response.Body.Close() - } - body := []byte("{\"resourceType\":\"Patient\",\"id\":\"p1\"}\n") - response := requestSnapshot(t, server, http.MethodPut, "/api/v1/projects/project-a/generations/commit-a/resources/Patient", body, checksumFor([]byte("different"))) - if response.StatusCode != http.StatusConflict { - t.Fatalf("checksum conflict status = %d, body=%s", response.StatusCode, readBody(response)) - } - var conflict httpapi.ErrorResponse - if err := json.NewDecoder(response.Body).Decode(&conflict); err != nil { - t.Fatal(err) - } - _ = response.Body.Close() - if conflict.Error.Code != "CHECKSUM_CONFLICT" || conflict.Error.Retryable { - t.Fatalf("checksum error = %#v", conflict) - } - - response = requestSnapshot(t, server, http.MethodPut, "/api/v1/projects/project-a/generations/commit-a/resources/Patient", body, checksumFor(body)) - if response.StatusCode != http.StatusOK { - t.Fatalf("upload status = %d, body=%s", response.StatusCode, readBody(response)) - } - _ = response.Body.Close() - response = requestSnapshot(t, server, http.MethodPost, "/api/v1/projects/project-a/generations/commit-a/finalize", nil, "") - if response.StatusCode != http.StatusOK || runner.calls != 1 || !runner.requests[0].StageOnly { - t.Fatalf("finalize status=%d calls=%d requests=%#v body=%s", response.StatusCode, runner.calls, runner.requests, readBody(response)) - } - _ = response.Body.Close() - response = requestSnapshot(t, server, http.MethodGet, "/api/v1/projects/project-a/generations/commit-a", nil, "") - if response.StatusCode != http.StatusOK { - t.Fatalf("status = %d, body=%s", response.StatusCode, readBody(response)) - } - _ = response.Body.Close() - - releaseBody := []byte(`{"generation":"commit-a"}`) - response = requestSnapshot(t, server, http.MethodPost, "/api/v1/projects/project-a/releases", releaseBody, "") - if response.StatusCode != http.StatusOK { - t.Fatalf("create release status = %d, body=%s", response.StatusCode, readBody(response)) - } - var release dataset.ProjectRelease - if err := json.NewDecoder(response.Body).Decode(&release); err != nil { - t.Fatal(err) - } - _ = response.Body.Close() - response = requestSnapshot(t, server, http.MethodGet, "/api/v1/projects/project-a/releases/active", nil, "") - if response.StatusCode != http.StatusNotFound { - t.Fatalf("inactive release became visible: status=%d body=%s", response.StatusCode, readBody(response)) - } - _ = response.Body.Close() - activatePath := "/api/v1/projects/project-a/releases/" + release.ID + "/activate" - for range 2 { - response = requestSnapshot(t, server, http.MethodPost, activatePath, []byte(`{"expectedRevision":0}`), "") - if response.StatusCode != http.StatusOK { - t.Fatalf("activate release status = %d, body=%s", response.StatusCode, readBody(response)) - } - _ = response.Body.Close() - } - if status, err := store.ReadSnapshot(context.Background(), dataset.Ref{Project: "project-a", Generation: "commit-a"}); err != nil || status.State != dataset.StateStaged { - t.Fatalf("snapshot after activation = %#v, %v", status, err) - } - response = requestSnapshot(t, server, http.MethodDelete, "/api/v1/projects/project-a/generations/commit-a", nil, "") - if response.StatusCode != http.StatusConflict { - t.Fatalf("abort finalized status = %d, body=%s", response.StatusCode, readBody(response)) - } - _ = response.Body.Close() -} - -func requestSnapshot(t *testing.T, server *httpapi.HTTPServer, method, path string, body []byte, checksum string) *http.Response { - t.Helper() - request := httptest.NewRequest(method, path, bytes.NewReader(body)) - if checksum != "" { - request.Header.Set("X-Content-SHA256", checksum) - } - response, err := server.App().Test(request) - if err != nil { - t.Fatal(err) - } - return response -} - -func readBody(response *http.Response) string { - body, _ := io.ReadAll(response.Body) - return string(body) -} diff --git a/internal/api/bulk/load/snapshot_service.go b/internal/api/bulk/load/snapshot_service.go deleted file mode 100644 index 04ae0eb0..00000000 --- a/internal/api/bulk/load/snapshot_service.go +++ /dev/null @@ -1,267 +0,0 @@ -package load - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/calypr/loom/internal/dataset" -) - -type SnapshotBlobStore interface { - Put(context.Context, dataset.Ref, dataset.ResourceUpload, []byte) error - Directory(context.Context, dataset.Ref) (string, error) - DeleteGeneration(context.Context, dataset.Ref) error -} - -type SnapshotService struct { - Repository dataset.SnapshotRepository - Blobs SnapshotBlobStore - Runner GenerationRunner - Now func() time.Time - locks sync.Map -} - -func (s *SnapshotService) CreateOrResume(ctx context.Context, project, gitCommit, authResourcePath string, resourceTypes []string) (dataset.SnapshotGeneration, error) { - if s == nil || s.Repository == nil || s.Blobs == nil || s.Runner == nil { - return dataset.SnapshotGeneration{}, fmt.Errorf("snapshot service dependencies are required") - } - now := s.now() - generation, err := dataset.NewSnapshotGeneration(project, gitCommit, authResourcePath, resourceTypes, now) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - return s.Repository.CreateOrResumeSnapshot(ctx, generation) -} - -func (s *SnapshotService) Status(ctx context.Context, project, generation string) (dataset.SnapshotGeneration, error) { - ref, err := dataset.NewRef(project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - return s.Repository.ReadSnapshot(ctx, ref) -} - -func (s *SnapshotService) Upload(ctx context.Context, project, generation, resourceType, declaredChecksum string, body []byte) (dataset.SnapshotGeneration, error) { - ref, err := dataset.NewRef(project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - digest := sha256.Sum256(body) - actualChecksum := hex.EncodeToString(digest[:]) - if strings.ToLower(strings.TrimSpace(declaredChecksum)) != actualChecksum { - return dataset.SnapshotGeneration{}, fmt.Errorf("%w: declared checksum does not match request body", dataset.ErrChecksumConflict) - } - upload, err := dataset.NewResourceUpload(resourceType, actualChecksum, int64(len(body)), s.now()) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - snapshot, err := s.Repository.ReadSnapshot(ctx, ref) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - if prior, ok := snapshot.Upload(resourceType); ok { - if prior.SHA256 != upload.SHA256 || prior.Size != upload.Size { - return dataset.SnapshotGeneration{}, dataset.ErrChecksumConflict - } - return snapshot, nil - } - if snapshot.State != dataset.StateLoading { - if snapshot.State == dataset.StateFailed { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotAborted - } - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotFinalized - } - declared := false - for _, expected := range snapshot.ExpectedResourceTypes { - if expected == resourceType { - declared = true - break - } - } - if !declared { - return dataset.SnapshotGeneration{}, fmt.Errorf("%w: resource type %q was not declared", dataset.ErrSnapshotConflict, resourceType) - } - if err := s.Blobs.Put(ctx, ref, upload, body); err != nil { - return dataset.SnapshotGeneration{}, err - } - return s.Repository.RecordSnapshotUpload(ctx, ref, upload) -} - -func (s *SnapshotService) Finalize(ctx context.Context, project, generation, submittedBy string) (dataset.SnapshotGeneration, *GenerationLoadResult, error) { - ref, err := dataset.NewRef(project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, nil, err - } - lockValue, _ := s.locks.LoadOrStore(ref, &sync.Mutex{}) - lock := lockValue.(*sync.Mutex) - lock.Lock() - defer lock.Unlock() - - snapshot, err := s.Repository.ReadSnapshot(ctx, ref) - if err != nil { - return dataset.SnapshotGeneration{}, nil, err - } - if snapshot.State == dataset.StateStaged { - return snapshot, nil, nil - } - if snapshot.State == dataset.StateFailed { - return dataset.SnapshotGeneration{}, nil, dataset.ErrSnapshotAborted - } - if missing := snapshot.MissingResourceTypes(); len(missing) != 0 { - return dataset.SnapshotGeneration{}, nil, fmt.Errorf("%w: missing %s", dataset.ErrGenerationIncomplete, strings.Join(missing, ", ")) - } - directory, err := s.Blobs.Directory(ctx, ref) - if err != nil { - return dataset.SnapshotGeneration{}, nil, err - } - result, err := runGeneration(s.Runner, ctx, GenerationLoadRequest{ - Project: project, Generation: generation, AuthResourcePath: snapshot.AuthResourcePath, - StagedDir: directory, SubmittedBy: submittedBy, StageOnly: true, - }) - if err != nil { - if reader, ok := s.Repository.(interface { - ReadManifest(context.Context, dataset.Ref) (dataset.Manifest, error) - }); ok { - if manifest, readErr := reader.ReadManifest(ctx, ref); readErr == nil && manifest.IsStaged() { - staged, transitionErr := s.Repository.TransitionSnapshot(ctx, ref, dataset.StateLoading, dataset.StateStaged, s.now()) - return staged, nil, transitionErr - } - } - _, transitionErr := s.Repository.TransitionSnapshot(context.WithoutCancel(ctx), ref, dataset.StateLoading, dataset.StateFailed, s.now()) - return dataset.SnapshotGeneration{}, nil, errors.Join(err, transitionErr) - } - staged, err := s.Repository.TransitionSnapshot(ctx, ref, dataset.StateLoading, dataset.StateStaged, s.now()) - if err != nil { - return dataset.SnapshotGeneration{}, nil, err - } - return staged, result, nil -} - -func (s *SnapshotService) Abort(ctx context.Context, project, generation string) (dataset.SnapshotGeneration, error) { - ref, err := dataset.NewRef(project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - snapshot, err := s.Repository.ReadSnapshot(ctx, ref) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - if snapshot.State == dataset.StateStaged { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotFinalized - } - if snapshot.State == dataset.StateFailed { - if err := s.Blobs.DeleteGeneration(ctx, ref); err != nil { - return dataset.SnapshotGeneration{}, err - } - return snapshot, nil - } - aborted, err := s.Repository.TransitionSnapshot(ctx, ref, dataset.StateLoading, dataset.StateFailed, s.now()) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - if err := s.Blobs.DeleteGeneration(ctx, ref); err != nil { - return dataset.SnapshotGeneration{}, err - } - return aborted, nil -} - -func runGeneration(runner GenerationRunner, ctx context.Context, request GenerationLoadRequest) (*GenerationLoadResult, error) { - summary, err := runner.RunGeneration(ctx, request, nil) - if err != nil { - return nil, err - } - return &GenerationLoadResult{Project: request.Project, Generation: request.Generation, AuthResourcePath: request.AuthResourcePath, SubmittedBy: request.SubmittedBy, Summary: &summary}, nil -} - -func (s *SnapshotService) now() time.Time { - if s.Now != nil { - return s.Now().UTC() - } - return time.Now().UTC() -} - -type LocalSnapshotBlobs struct{ Root string } - -func (b LocalSnapshotBlobs) Put(_ context.Context, ref dataset.Ref, upload dataset.ResourceUpload, body []byte) error { - directory := b.generationDirectory(ref) - if err := os.MkdirAll(directory, 0o750); err != nil { - return err - } - path := filepath.Join(directory, upload.ResourceType+".ndjson") - if existing, err := os.ReadFile(path); err == nil { - digest := sha256.Sum256(existing) - if hex.EncodeToString(digest[:]) != upload.SHA256 { - return dataset.ErrChecksumConflict - } - return nil - } else if !errors.Is(err, os.ErrNotExist) { - return err - } - temporary, err := os.CreateTemp(directory, ".upload-*") - if err != nil { - return err - } - temporaryPath := temporary.Name() - defer os.Remove(temporaryPath) - if _, err := temporary.Write(body); err != nil { - _ = temporary.Close() - return err - } - if err := temporary.Sync(); err != nil { - _ = temporary.Close() - return err - } - if err := temporary.Close(); err != nil { - return err - } - if err := os.Link(temporaryPath, path); err != nil { - if !errors.Is(err, os.ErrExist) { - return err - } - existing, readErr := os.ReadFile(path) - if readErr != nil { - return readErr - } - digest := sha256.Sum256(existing) - if hex.EncodeToString(digest[:]) != upload.SHA256 { - return dataset.ErrChecksumConflict - } - } - return nil -} - -func (b LocalSnapshotBlobs) Directory(_ context.Context, ref dataset.Ref) (string, error) { - directory := b.generationDirectory(ref) - info, err := os.Stat(directory) - if err != nil { - return "", err - } - if !info.IsDir() { - return "", fmt.Errorf("snapshot path is not a directory") - } - return directory, nil -} - -func (b LocalSnapshotBlobs) DeleteGeneration(_ context.Context, ref dataset.Ref) error { - return os.RemoveAll(b.generationDirectory(ref)) -} - -func (b LocalSnapshotBlobs) generationDirectory(ref dataset.Ref) string { - root := b.Root - if strings.TrimSpace(root) == "" { - root = filepath.Join(os.TempDir(), "loom-snapshots") - } - digest := sha256.Sum256([]byte(ref.Project + "\x00" + ref.Generation)) - return filepath.Join(root, hex.EncodeToString(digest[:])) -} - -var _ SnapshotBlobStore = LocalSnapshotBlobs{} -var _ dataset.GenerationBlobCleaner = LocalSnapshotBlobs{} diff --git a/internal/api/bulk/load/snapshot_service_test.go b/internal/api/bulk/load/snapshot_service_test.go deleted file mode 100644 index 63fef276..00000000 --- a/internal/api/bulk/load/snapshot_service_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package load - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "os" - "path/filepath" - "testing" - "time" - - "github.com/calypr/loom/internal/dataset" - "github.com/calypr/loom/internal/ingest" -) - -type snapshotRunnerFixture struct { - calls int - requests []GenerationLoadRequest - err error -} - -type stagedManifestRepository struct { - *memoryLifecycleStore - manifest dataset.Manifest -} - -func (r *stagedManifestRepository) ReadManifest(context.Context, dataset.Ref) (dataset.Manifest, error) { - return r.manifest, nil -} - -func (f *snapshotRunnerFixture) RunGeneration(_ context.Context, request GenerationLoadRequest, _ ingest.EventSink) (ingest.LoadSummary, error) { - f.calls++ - f.requests = append(f.requests, request) - return ingest.LoadSummary{Files: 1}, f.err -} - -func TestSnapshotServiceSafeRetryFinalizeAndNoImplicitActivation(t *testing.T) { - store := newMemoryLifecycleStore() - runner := &snapshotRunnerFixture{} - now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC) - blobs := LocalSnapshotBlobs{Root: t.TempDir()} - service := &SnapshotService{Repository: store, Blobs: blobs, Runner: runner, Now: func() time.Time { return now }} - ctx := context.Background() - - created, err := service.CreateOrResume(ctx, "project-a", "commit-a", "", []string{"Patient", "Observation"}) - if err != nil { - t.Fatal(err) - } - resumed, err := service.CreateOrResume(ctx, "project-a", "commit-a", "", []string{"Observation", "Patient"}) - if err != nil || resumed.Dataset != created.Dataset { - t.Fatalf("resume = %#v, %v", resumed, err) - } - undeclared := []byte("{}\n") - if _, err := service.Upload(ctx, "project-a", "commit-a", "Encounter", checksumFor(undeclared), undeclared); !errors.Is(err, dataset.ErrSnapshotConflict) { - t.Fatalf("undeclared resource upload = %v", err) - } - if _, err := os.Stat(filepath.Join(blobs.generationDirectory(created.Dataset), "Encounter.ndjson")); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("undeclared resource left a blob: %v", err) - } - patient := []byte("{\"resourceType\":\"Patient\",\"id\":\"p1\"}\n") - checksum := checksumFor(patient) - if _, err := service.Upload(ctx, "project-a", "commit-a", "Patient", checksum, patient); err != nil { - t.Fatal(err) - } - if _, err := service.Upload(ctx, "project-a", "commit-a", "Patient", checksum, patient); err != nil { - t.Fatalf("upload retry = %v", err) - } - if _, _, err := service.Finalize(ctx, "project-a", "commit-a", "operator"); !errors.Is(err, dataset.ErrGenerationIncomplete) { - t.Fatalf("partial finalize = %v", err) - } - if runner.calls != 0 { - t.Fatalf("partial finalize invoked loader %d times", runner.calls) - } - observation := []byte("{\"resourceType\":\"Observation\",\"id\":\"o1\"}\n") - if _, err := service.Upload(ctx, "project-a", "commit-a", "Observation", checksumFor(observation), observation); err != nil { - t.Fatal(err) - } - staged, result, err := service.Finalize(ctx, "project-a", "commit-a", "operator") - if err != nil || staged.State != dataset.StateStaged || result == nil { - t.Fatalf("finalize = %#v, %#v, %v", staged, result, err) - } - if runner.calls != 1 || len(runner.requests) != 1 || !runner.requests[0].StageOnly { - t.Fatalf("runner requests = %#v", runner.requests) - } - if _, err := service.Upload(ctx, "project-a", "commit-a", "Patient", checksum, patient); err != nil { - t.Fatalf("idempotent upload after finalize = %v", err) - } - stagedAgain, resultAgain, err := service.Finalize(ctx, "project-a", "commit-a", "operator") - if err != nil || stagedAgain.State != dataset.StateStaged || resultAgain != nil || runner.calls != 1 { - t.Fatalf("idempotent finalize = %#v, %#v, calls=%d, err=%v", stagedAgain, resultAgain, runner.calls, err) - } - if _, err := store.ReadActiveRelease(ctx, "project-a"); !errors.Is(err, dataset.ErrNoActiveRelease) { - t.Fatalf("finalize activated release: %v", err) - } -} - -func TestSnapshotServiceChecksumConflictAndFailurePreservePriorRelease(t *testing.T) { - ctx := context.Background() - store := newMemoryLifecycleStore() - now := time.Now().UTC() - runner := &snapshotRunnerFixture{err: errors.New("invalid rows")} - service := &SnapshotService{Repository: store, Blobs: LocalSnapshotBlobs{Root: t.TempDir()}, Runner: runner, Now: func() time.Time { return now }} - _, _ = service.CreateOrResume(ctx, "project-a", "commit-b", "", []string{"Patient"}) - body := []byte("{}\n") - if _, err := service.Upload(ctx, "project-a", "commit-b", "Patient", checksumFor([]byte("different")), body); !errors.Is(err, dataset.ErrChecksumConflict) { - t.Fatalf("declared checksum mismatch = %v", err) - } - if _, err := service.Upload(ctx, "project-a", "commit-b", "Patient", checksumFor(body), body); err != nil { - t.Fatal(err) - } - if _, _, err := service.Finalize(ctx, "project-a", "commit-b", "operator"); err == nil || !errors.Is(err, runner.err) { - t.Fatalf("loader failure = %v", err) - } - status, err := service.Status(ctx, "project-a", "commit-b") - if err != nil || status.State != dataset.StateFailed { - t.Fatalf("failed status = %#v, %v", status, err) - } - if _, err := store.ReadActiveRelease(ctx, "project-a"); !errors.Is(err, dataset.ErrNoActiveRelease) { - t.Fatalf("failure activated release: %v", err) - } -} - -func TestSnapshotFinalizeRecoversWhenLoadCommittedBeforeResponseWasLost(t *testing.T) { - ctx := context.Background() - memory := newMemoryLifecycleStore() - now := time.Now().UTC() - ref, _ := dataset.NewRef("project-a", "commit-a") - schema, _ := dataset.NewSchemaSnapshot("urn:test", "R5", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", []string{"Patient"}) - manifest, _ := dataset.NewManifest(ref, schema) - manifest, _ = manifest.Transition(dataset.StateStaged) - repository := &stagedManifestRepository{memoryLifecycleStore: memory, manifest: manifest} - runner := &snapshotRunnerFixture{err: errors.New("connection lost after commit")} - service := &SnapshotService{Repository: repository, Blobs: LocalSnapshotBlobs{Root: t.TempDir()}, Runner: runner, Now: func() time.Time { return now }} - _, _ = service.CreateOrResume(ctx, "project-a", "commit-a", "", []string{"Patient"}) - body := []byte("{}\n") - _, _ = service.Upload(ctx, "project-a", "commit-a", "Patient", checksumFor(body), body) - staged, result, err := service.Finalize(ctx, "project-a", "commit-a", "operator") - if err != nil || staged.State != dataset.StateStaged || result != nil { - t.Fatalf("recovered finalize = %#v, %#v, %v", staged, result, err) - } -} - -func checksumFor(body []byte) string { - digest := sha256.Sum256(body) - return hex.EncodeToString(digest[:]) -} diff --git a/internal/api/bulk/load/test_server_helpers_test.go b/internal/api/bulk/load/test_server_helpers_test.go deleted file mode 100644 index 3bedb15b..00000000 --- a/internal/api/bulk/load/test_server_helpers_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package load - -import ( - httpapi "github.com/calypr/loom/internal/api/http" - "github.com/calypr/loom/internal/authscope" - "github.com/gofiber/fiber/v3" -) - -type HTTPConfig struct { - Service *Service - Authenticator authscope.Authenticator - Authorizer authscope.Authorizer -} - -func NewHTTPServer(cfg HTTPConfig) (*httpapi.HTTPServer, error) { - s, err := httpapi.NewHTTPServer(httpapi.HTTPConfig{Authenticator: cfg.Authenticator, Authorizer: cfg.Authorizer}) - if err != nil { - return nil, err - } - h, err := NewHandler(Config{Service: cfg.Service, Authorizer: cfg.Authorizer}) - if err != nil { - return nil, err - } - registerTestRoutes(s.App(), h) - return s, nil -} - -// registerTestRoutes keeps package-level handler tests focused on transport -// behavior. Production route ownership lives exclusively in generated/loomapi. -func registerTestRoutes(router fiber.Router, h *Handler) { - router.Put("/api/v1/projects/:project/resources/:resourceType", h.HandleResource) - router.Post("/api/v1/datasets/:project/generations/:generation", h.HandleCreateGeneration) - router.Post("/api/v1/datasets/:project/generations/:generation/activate", h.HandleActivateGeneration) - router.Put("/api/v1/raw", h.HandleLoadRaw) - registerTestSnapshotRoutes(router, h) -} - -func registerTestSnapshotRoutes(router fiber.Router, h *Handler) { - if h.snapshots != nil { - router.Post("/api/v1/projects/:project/generations/:generation", h.HandleCreateSnapshot) - router.Get("/api/v1/projects/:project/generations/:generation", h.HandleSnapshotStatus) - router.Put("/api/v1/projects/:project/generations/:generation/resources/:resourceType", h.HandleUploadSnapshotResource) - router.Post("/api/v1/projects/:project/generations/:generation/finalize", h.HandleFinalizeSnapshot) - router.Delete("/api/v1/projects/:project/generations/:generation", h.HandleAbortSnapshot) - } - if h.releases != nil { - router.Post("/api/v1/projects/:project/releases/activate", h.HandleActivateReleaseCompatibility) - router.Post("/api/v1/projects/:project/releases", h.HandleCreateRelease) - router.Post("/api/v1/projects/:project/releases/:release/activate", h.HandleActivateRelease) - router.Get("/api/v1/projects/:project/releases/active", h.HandleActiveRelease) - router.Get("/api/v1/projects/:project/releases/:release", h.HandleReleaseStatus) - } -} diff --git a/internal/api/graphql/errors_test.go b/internal/api/graphql/errors_test.go deleted file mode 100644 index b84779c2..00000000 --- a/internal/api/graphql/errors_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package graphqlerrors - -import ( - "errors" - "testing" - - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" -) - -func TestPresentErrorUsesStableExtensions(t *testing.T) { - err := dataframeerrors.NewError( - dataframeerrors.CodeUnknownField, - "secret internal selector detail", - dataframeerrors.WithFieldPath("rootFields", "2", "fieldRef"), - dataframeerrors.WithDetails(map[string]any{"fieldRef": "Patient.missing", "aql": "FOR p IN Patient RETURN p"}), - ) - graphqlErr := PresentError(err, "request-123") - if graphqlErr == nil { - t.Fatal("PresentError returned nil") - } - if graphqlErr.Message != "the selected field is not recognized" { - t.Fatalf("message = %q", graphqlErr.Message) - } - if graphqlErr.Extensions["code"] != string(dataframeerrors.CodeUnknownField) { - t.Fatalf("extensions = %#v", graphqlErr.Extensions) - } - if graphqlErr.Extensions["requestId"] != "request-123" { - t.Fatalf("request ID missing: %#v", graphqlErr.Extensions) - } - if _, ok := graphqlErr.Extensions["aql"]; ok { - t.Fatalf("AQL extension leaked: %#v", graphqlErr.Extensions) - } - if !errors.Is(graphqlErr, err) { - t.Fatal("GraphQL error did not preserve original cause") - } -} - -func TestPresentErrorRedactsUnknownErrors(t *testing.T) { - graphqlErr := PresentError(errors.New("AQL FOR p IN Patient bind=@token"), "request-456") - if graphqlErr.Message != "internal server error" { - t.Fatalf("unknown message = %q", graphqlErr.Message) - } - if graphqlErr.Extensions["code"] != string(dataframeerrors.CodeInternalError) { - t.Fatalf("unknown extensions = %#v", graphqlErr.Extensions) - } - if graphqlErr.Extensions["details"] != nil { - t.Fatalf("unknown details leaked: %#v", graphqlErr.Extensions) - } -} - -func TestPresentRecipeResolutionErrorKeepsActionableMessage(t *testing.T) { - err := dataframeerrors.NewError( - dataframeerrors.CodeRecipeResolutionFailed, - `outputs[0] DocumentReference: field "project_id" is not in the active FHIR schema`, - ) - graphqlErr := PresentError(err, "request-789") - if graphqlErr.Message != err.Error() { - t.Fatalf("recipe resolution message = %q", graphqlErr.Message) - } - if graphqlErr.Extensions["code"] != string(dataframeerrors.CodeRecipeResolutionFailed) { - t.Fatalf("recipe resolution extensions = %#v", graphqlErr.Extensions) - } -} - -func TestExtensionsForErrorOmitsEmptyRequestID(t *testing.T) { - extensions := ExtensionsForError(dataframeerrors.NewError(dataframeerrors.CodeInvalidCursor, ""), "") - if _, ok := extensions["requestId"]; ok { - t.Fatalf("empty request ID should be omitted: %#v", extensions) - } - if extensions["fieldPath"] == nil { - t.Fatal("fieldPath extension should remain present for stable shape") - } -} diff --git a/internal/api/graphql/graph/aggregate_http_integration_test.go b/internal/api/graphql/graph/aggregate_http_integration_test.go index 26fd2925..fc6d8b31 100644 --- a/internal/api/graphql/graph/aggregate_http_integration_test.go +++ b/internal/api/graphql/graph/aggregate_http_integration_test.go @@ -58,6 +58,72 @@ func (q *aggregateHTTPQueryer) QueryRowsArgsVisit(ctx context.Context, query str return nil } +func TestGraphQLDataframeCanonicalProjectResolvesLegacyBundle(t *testing.T) { + now := time.Now().UTC() + catalog := &aggregateHTTPCatalog{execution: dfpublication.BundleExecution{ + ID: "execution", BundleIdentity: dfpublication.BundleIdentity{ + Name: "recipe", TranslationVersion: "v1", Project: "HTAN_INT-BForePC", DatasetGeneration: "generation", + }, + State: dfpublication.BundlePublished, UpdatedAt: now, + Outputs: []dfpublication.BundleOutputRecord{{ + Name: "Patient", PhysicalTable: "physical_patient", Columns: []dfpublication.PhysicalColumn{{Name: "__loom_row_id", ClickHouse: "String"}}, + State: dfpublication.BundlePublished, VerifiedAt: &now, + }}, + }} + root := graphresolver.NewResolver(graphresolver.ResolverConfig{ + MaterializationReader: &dfpublished.Reader{ClickHouse: &aggregateHTTPQueryer{}, Catalog: catalog}, + }) + server, err := newGraphServer(root, authscope.StaticAuthenticator{Principal: authscope.Principal{Subject: "user", Projects: []string{"HTAN_INT/BForePC"}}}) + if err != nil { + t.Fatal(err) + } + + query := `query CanonicalProject($selector: DataframeSelectorInput!) { + aggregate: dataframeAggregate(input: {projectId: "HTAN_INT/BForePC", selector: $selector, operation: "COUNT"}) { + materialization { id projectId } + } + }` + body, err := json.Marshal(map[string]any{ + "query": query, + "variables": map[string]any{ + "selector": map[string]any{"recipe": "recipe", "translationVersion": "v1", "output": "Patient"}, + }, + }) + if err != nil { + t.Fatal(err) + } + request := httptest.NewRequest(http.MethodPost, "/graphql/graph", strings.NewReader(string(body))) + request.Header.Set("Content-Type", "application/json") + response, err := server.App().Test(request) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + + var payload map[string]any + if err := json.NewDecoder(response.Body).Decode(&payload); err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200 (payload %#v)", response.StatusCode, payload) + } + if errorsValue, ok := payload["errors"].([]any); ok && len(errorsValue) != 0 { + t.Fatalf("unexpected GraphQL errors: %#v", errorsValue) + } + data, ok := payload["data"].(map[string]any) + if !ok { + t.Fatalf("response has no data: %#v", payload) + } + aggregate, ok := data["aggregate"].(map[string]any) + if !ok { + t.Fatalf("response has no aggregate: %#v", payload) + } + materialization, ok := aggregate["materialization"].(map[string]any) + if !ok || materialization["id"] != "execution:Patient" || materialization["projectId"] != "HTAN_INT/BForePC" { + t.Fatalf("unexpected materialization: %#v", aggregate) + } +} + func TestGraphQLAggregateAliasesUseOneGroupingStatement(t *testing.T) { columns := []dfpublication.PhysicalColumn{{Name: "__loom_row_id", ClickHouse: "String"}} for i := 0; i < 156; i++ { @@ -71,7 +137,7 @@ func TestGraphQLAggregateAliasesUseOneGroupingStatement(t *testing.T) { State: dfpublication.BundlePublished, UpdatedAt: now, Outputs: []dfpublication.BundleOutputRecord{{ Name: "Patient", PhysicalTable: "physical_patient", Columns: columns, - State: dfpublication.BundlePublished, + State: dfpublication.BundlePublished, VerifiedAt: &now, }}, }} queryer := &aggregateHTTPQueryer{} @@ -89,14 +155,14 @@ func TestGraphQLAggregateAliasesUseOneGroupingStatement(t *testing.T) { if i == 0 { directive = " @include(if: $include)" } - fmt.Fprintf(&fields, " a%03d: dataframeAggregate(input: {selector: $selector, groupBy: [\"facet_%03d\"], operation: \"COUNT\"})%s { columns rows materialization { id } }", i, i, directive) + fmt.Fprintf(&fields, " a%03d: dataframeAggregate(input: {projectId: \"P1\", selector: $selector, groupBy: [\"facet_%03d\"], operation: \"COUNT\"})%s { columns rows materialization { id } }", i, i, directive) } - fields.WriteString(" totalA: dataframeAggregate(input: {selector: $selector, operation: \"COUNT\"}) { columns rows materialization { id } }") - fields.WriteString(" totalB: dataframeAggregate(input: {selector: $selector, operation: \"COUNT\"}) { columns rows materialization { id } }") + fields.WriteString(" totalA: dataframeAggregate(input: {projectId: \"P1\", selector: $selector, operation: \"COUNT\"}) { columns rows materialization { id } }") + fields.WriteString(" totalB: dataframeAggregate(input: {projectId: \"P1\", selector: $selector, operation: \"COUNT\"}) { columns rows materialization { id } }") query := "query AggregateTable($selector: DataframeSelectorInput!, $include: Boolean!) {" + - " page: dataframeRows(input: {selector: $selector, columns: [\"facet_000\"], first: 1}) { columns rows }" + + " page: dataframeRows(input: {projectId: \"P1\", selector: $selector, columns: [\"facet_000\"], first: 1}) { columns rows }" + " ...FacetFields" + - " skipped: dataframeAggregate(input: {selector: $selector, operation: \"COUNT\"}) @skip(if: true) { columns }" + + " skipped: dataframeAggregate(input: {projectId: \"P1\", selector: $selector, operation: \"COUNT\"}) @skip(if: true) { columns }" + "} fragment FacetFields on Query {" + fields.String() + "}" body, err := json.Marshal(map[string]any{ "query": query, @@ -174,7 +240,7 @@ func TestGraphQLTableRenderCombinesRowsAndTermsFacets(t *testing.T) { State: dfpublication.BundlePublished, UpdatedAt: now, Outputs: []dfpublication.BundleOutputRecord{{ Name: "Patient", PhysicalTable: "physical_patient", Columns: columns, - State: dfpublication.BundlePublished, + State: dfpublication.BundlePublished, VerifiedAt: &now, }}, }} queryer := &aggregateHTTPQueryer{} @@ -208,8 +274,8 @@ func TestGraphQLTableRenderCombinesRowsAndTermsFacets(t *testing.T) { body, err := json.Marshal(map[string]any{ "query": query, "variables": map[string]any{ - "rows": map[string]any{"selector": selector, "columns": []string{"facet_000", "facet_001", "facet_002"}, "filters": []any{}, "first": 10}, - "facets": map[string]any{"selector": selector, "filters": []any{}, "specs": specs}, + "rows": map[string]any{"projectId": "P1", "selector": selector, "columns": []string{"facet_000", "facet_001", "facet_002"}, "filters": []any{}, "first": 10}, + "facets": map[string]any{"projectId": "P1", "selector": selector, "filters": []any{}, "specs": specs}, }, }) if err != nil { diff --git a/internal/api/graphql/graph/materialization/aggregate_batch.go b/internal/api/graphql/graph/dataframe/aggregates.go similarity index 69% rename from internal/api/graphql/graph/materialization/aggregate_batch.go rename to internal/api/graphql/graph/dataframe/aggregates.go index 14b4a5ad..8d4e3483 100644 --- a/internal/api/graphql/graph/materialization/aggregate_batch.go +++ b/internal/api/graphql/graph/dataframe/aggregates.go @@ -1,7 +1,8 @@ -package materializationapi +package dataframe import ( "context" + "encoding/json" "fmt" "sort" "strings" @@ -14,7 +15,41 @@ import ( dfmaterialization "github.com/calypr/loom/internal/dataframe/published" ) -const aggregateFallbackDelay = 2 * time.Millisecond +func (s *Service) AggregateInput(ctx context.Context, input model.DataframeAggregateInput) (dfmaterialization.AggregateResult, error) { + selector, err := resolveSelector(input.Selector) + if err != nil { + return dfmaterialization.AggregateResult{}, err + } + call := &aggregateCall{project: canonicalProjectID(input.ProjectID), selector: selector, filters: convertFilters(input.Filters), legacy: input} + result := s.submitAggregateCall(ctx, call) + if result.err != nil { + s.logReadFailure(ctx, "clickhouse_aggregate", selector.Output, result.err, "project", input.ProjectID) + } + return result.legacy, result.err +} + +func (s *Service) AggregationsInput(ctx context.Context, input model.DataframeAggregationsInput) (dfmaterialization.AggregationsResult, error) { + selector, err := resolveSelector(input.Selector) + if err != nil { + return dfmaterialization.AggregationsResult{}, err + } + call := &aggregateCall{project: canonicalProjectID(input.ProjectID), selector: selector, filters: convertFilters(input.Filters), rich: input, kind: aggregateCallRich} + result := s.submitAggregateCall(ctx, call) + if result.err != nil { + s.logReadFailure(ctx, "clickhouse_aggregations", selector.Output, result.err, "project", input.ProjectID) + } + return result.rich, result.err +} + +func AggregationsJSON(result dfmaterialization.AggregationsResult) (json.RawMessage, error) { + return json.Marshal(dfmaterialization.NormalizeAggregationResults(result.Aggregations)) +} + +// GraphQL launches sibling resolvers concurrently, but a large selection set +// can take several milliseconds to enqueue its final aggregate call. The +// fallback exists only for directive-skipped fields where expectedCalls cannot +// be reached; normal operations dispatch immediately at the exact count. +const aggregateFallbackDelay = 10 * time.Millisecond type aggregateOperationStateKey struct{} @@ -28,10 +63,16 @@ type aggregateOperationState struct { dispatching bool dispatchCalls func(context.Context, []*aggregateCall) lastEnqueue time.Time + datasets map[string]*projectDatasetCacheEntry + + mu sync.Mutex +} - mu sync.Mutex - projects *projectsCacheEntry - federations map[string]*federationCacheEntry +type projectDatasetCacheEntry struct { + ready chan struct{} + value dfmaterialization.Materialization + access projectAccess + err error } type aggregateCallKind uint8 @@ -43,6 +84,7 @@ const ( type aggregateCall struct { kind aggregateCallKind + project string selector dfmaterialization.DataframeSelector filters []dfmaterialization.Filter legacy model.DataframeAggregateInput @@ -58,8 +100,7 @@ type aggregateCallResult struct { func (s *Service) WithOperationContext(ctx context.Context, expectedCalls int) context.Context { state := &aggregateOperationState{ - service: s, expectedCalls: expectedCalls, - federations: make(map[string]*federationCacheEntry), + service: s, expectedCalls: expectedCalls, datasets: make(map[string]*projectDatasetCacheEntry), } result := context.WithValue(ctx, aggregateOperationStateKey{}, state) state.operationCtx = result @@ -78,6 +119,35 @@ func (s *Service) WithOperationContext(ctx context.Context, expectedCalls int) c return result } +func (s *aggregateOperationState) projectDataset( + ctx context.Context, + project string, + selector dfmaterialization.DataframeSelector, + resolve func() (dfmaterialization.Materialization, projectAccess, error), +) (dfmaterialization.Materialization, projectAccess, error) { + key := canonicalProjectID(project) + "\x00" + selector.Key() + s.mu.Lock() + if s.datasets == nil { + s.datasets = make(map[string]*projectDatasetCacheEntry) + } + if cached := s.datasets[key]; cached != nil { + s.mu.Unlock() + select { + case <-cached.ready: + return cached.value, cached.access, cached.err + case <-ctx.Done(): + return dfmaterialization.Materialization{}, projectAccess{}, ctx.Err() + } + } + entry := &projectDatasetCacheEntry{ready: make(chan struct{})} + s.datasets[key] = entry + s.mu.Unlock() + + entry.value, entry.access, entry.err = resolve() + close(entry.ready) + return entry.value, entry.access, entry.err +} + func aggregateStateFromContext(ctx context.Context) *aggregateOperationState { state, _ := ctx.Value(aggregateOperationStateKey{}).(*aggregateOperationState) return state @@ -194,9 +264,9 @@ func (s *aggregateOperationState) cancelPending(err error) { } type aggregateExecutionGroup struct { - dataset dfmaterialization.FederatedDataset - access map[string]dfmaterialization.SourceAccess - calls []*aggregateCall + materialization dfmaterialization.Materialization + access projectAccess + calls []*aggregateCall } func (s *Service) dispatchAggregateCalls(ctx context.Context, calls []*aggregateCall) { @@ -219,24 +289,33 @@ func (s *Service) dispatchAggregateCalls(ctx context.Context, calls []*aggregate } groups := make(map[string]*aggregateExecutionGroup) + resolved := make(map[string]*aggregateExecutionGroup) + resolutionErrors := make(map[string]error) groupKeys := make([]string, 0) for _, call := range calls { - dataset, access, resolveErr := s.authorizedFederation(ctx, principal, call.selector, call.filters) - if resolveErr != nil { + requestKey := canonicalProjectID(call.project) + "\x00" + call.selector.Key() + if group := resolved[requestKey]; group != nil { + group.calls = append(group.calls, call) + continue + } + if resolveErr := resolutionErrors[requestKey]; resolveErr != nil { call.result <- aggregateCallResult{err: mapReaderError(resolveErr)} continue } - if len(dataset.Sources) == 0 { - call.result <- aggregateCallResult{err: dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "")} + materialization, access, resolveErr := s.currentProjectDatasetForPrincipal(ctx, principal, call.project, call.selector) + if resolveErr != nil { + resolutionErrors[requestKey] = resolveErr + call.result <- aggregateCallResult{err: mapReaderError(resolveErr)} continue } - key := aggregateDatasetKey(dataset) + key := materialization.ID group := groups[key] if group == nil { - group = &aggregateExecutionGroup{dataset: dataset, access: access} + group = &aggregateExecutionGroup{materialization: materialization, access: access} groups[key] = group groupKeys = append(groupKeys, key) } + resolved[requestKey] = group group.calls = append(group.calls, call) } sort.Strings(groupKeys) @@ -245,7 +324,7 @@ func (s *Service) dispatchAggregateCalls(ctx context.Context, calls []*aggregate clickhouseStarted := time.Now() for _, key := range groupKeys { group := groups[key] - sourceCount += len(group.dataset.Sources) + sourceCount++ jobs := make([]dfmaterialization.AggregateJob, 0) callIDs := make(map[*aggregateCall][]int) nextID := 0 @@ -267,8 +346,8 @@ func (s *Service) dispatchAggregateCalls(ctx context.Context, calls []*aggregate continue } logicalJobs += len(jobs) - batch, batchErr := s.reader.ExecuteAggregateBatch(ctx, group.dataset, dfmaterialization.AggregateBatchRequest{ - Jobs: jobs, AccessByProject: group.access, + batch, batchErr := s.reader.ExecuteAggregateBatch(ctx, group.materialization, dfmaterialization.AggregateBatchRequest{ + Jobs: jobs, AuthResourcePaths: group.access.authResourcePaths, Unrestricted: group.access.unrestricted, }) deduplicated += batch.DeduplicatedJobs statements += batch.Statements @@ -287,7 +366,7 @@ func (s *Service) dispatchAggregateCalls(ctx context.Context, calls []*aggregate resultRows += len(job.Rows) } for call, ids := range callIDs { - s.completeAggregateCall(group.dataset, call, ids, byID) + s.completeAggregateCall(group.materialization, call, ids, byID) } } clickhouseElapsed := time.Since(clickhouseStarted) @@ -307,18 +386,6 @@ func (s *Service) dispatchAggregateCalls(ctx context.Context, calls []*aggregate } } -func aggregateDatasetKey(dataset dfmaterialization.FederatedDataset) string { - ids := make([]string, 0, len(dataset.Sources)+len(dataset.ProjectStatuses)+2) - for _, source := range dataset.Sources { - ids = append(ids, "source:"+source.ID) - } - for _, status := range dataset.ProjectStatuses { - ids = append(ids, "project:"+status.ProjectID+":"+string(status.State)) - } - sort.Strings(ids) - return dataset.Selector.Key() + "\x00" + dataset.Revision + "\x00" + fmt.Sprint(dataset.ExpectedProjects) + "\x00" + strings.Join(ids, "\x1f") -} - func aggregateJobsForCall(call *aggregateCall, firstID int) ([]dfmaterialization.AggregateJob, error) { if call.kind == aggregateCallLegacy { return []dfmaterialization.AggregateJob{{ @@ -368,7 +435,7 @@ func aggregateJobsForCall(call *aggregateCall, firstID int) ([]dfmaterialization return jobs, nil } -func (s *Service) completeAggregateCall(dataset dfmaterialization.FederatedDataset, call *aggregateCall, ids []int, byID map[int]dfmaterialization.AggregateJobResult) { +func (s *Service) completeAggregateCall(materialization dfmaterialization.Materialization, call *aggregateCall, ids []int, byID map[int]dfmaterialization.AggregateJobResult) { if call.kind == aggregateCallLegacy { job := byID[ids[0]] if job.Err != nil { @@ -376,7 +443,7 @@ func (s *Service) completeAggregateCall(dataset dfmaterialization.FederatedDatas return } call.result <- aggregateCallResult{legacy: dfmaterialization.AggregateResult{ - Materialization: federatedMaterialization(dataset), Columns: job.Columns, Rows: job.Rows, + Materialization: materialization, Columns: job.Columns, Rows: job.Rows, }} return } @@ -394,6 +461,6 @@ func (s *Service) completeAggregateCall(dataset dfmaterialization.FederatedDatas }) } call.result <- aggregateCallResult{rich: dfmaterialization.AggregationsResult{ - Dataset: dataset, Aggregations: dfmaterialization.NormalizeAggregationResults(results), + Materialization: materialization, Aggregations: dfmaterialization.NormalizeAggregationResults(results), }} } diff --git a/internal/api/graphql/graph/materialization/aggregate_batch_test.go b/internal/api/graphql/graph/dataframe/aggregates_test.go similarity index 98% rename from internal/api/graphql/graph/materialization/aggregate_batch_test.go rename to internal/api/graphql/graph/dataframe/aggregates_test.go index 6ce31e7e..c7a260ef 100644 --- a/internal/api/graphql/graph/materialization/aggregate_batch_test.go +++ b/internal/api/graphql/graph/dataframe/aggregates_test.go @@ -1,4 +1,4 @@ -package materializationapi +package dataframe import ( "context" diff --git a/internal/api/graphql/graph/materialization/export.go b/internal/api/graphql/graph/dataframe/export.go similarity index 86% rename from internal/api/graphql/graph/materialization/export.go rename to internal/api/graphql/graph/dataframe/export.go index fb45e9b6..2ceed854 100644 --- a/internal/api/graphql/graph/materialization/export.go +++ b/internal/api/graphql/graph/dataframe/export.go @@ -1,4 +1,4 @@ -package materializationapi +package dataframe import ( "context" @@ -11,30 +11,25 @@ import ( dfmaterialization "github.com/calypr/loom/internal/dataframe/published" ) -// ExportDataframe streams a principal-scoped published dataframe through the -// same federated reader contract used by interactive rows. -func (s *Service) ExportDataframe(ctx context.Context, request dfmaterialization.ExportRequest, out io.Writer) error { +// ExportDataframe streams one authorized project's published dataframe. +func (s *Service) ExportDataframe(ctx context.Context, project string, request dfmaterialization.ExportRequest, out io.Writer) error { if s.reader == nil { return dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) } if out == nil { return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") } - principal, err := s.principal(ctx) - if err != nil { - return err - } selector, err := resolvePublishedSelector(request.Selector) if err != nil { return err } - dataset, access, err := s.authorizedFederation(ctx, principal, selector, request.Filters) + if strings.TrimSpace(project) == "" { + return dataframeerrors.NewError(dataframeerrors.CodeProjectRequired, "") + } + dataset, access, err := s.currentProjectDataset(ctx, project, selector) if err != nil { return err } - if len(dataset.Sources) == 0 { - return dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } format := request.Format.Normalize() if format != dfmaterialization.ExportCSV && format != dfmaterialization.ExportTSV && format != dfmaterialization.ExportJSON && format != dfmaterialization.ExportJSONL { return dataframeerrors.NewError(dataframeerrors.CodeUnsupportedExportFormat, "") @@ -54,9 +49,9 @@ func (s *Service) ExportDataframe(ctx context.Context, request dfmaterialization if err := state.begin(); err != nil { return err } - _, err = s.reader.StreamFederatedDataset(ctx, dataset, dfmaterialization.FederatedStreamRequest{ + err = s.reader.Stream(ctx, dataset, dfmaterialization.StreamRequest{ Columns: columns, Filters: request.Filters, Sort: request.Sort, - AccessByProject: access, + AuthResourcePaths: access.authResourcePaths, Unrestricted: access.unrestricted, }, state.visit) if err != nil { return err diff --git a/internal/api/graphql/graph/materialization/export_test.go b/internal/api/graphql/graph/dataframe/export_test.go similarity index 98% rename from internal/api/graphql/graph/materialization/export_test.go rename to internal/api/graphql/graph/dataframe/export_test.go index 6e9677d5..6c31c308 100644 --- a/internal/api/graphql/graph/materialization/export_test.go +++ b/internal/api/graphql/graph/dataframe/export_test.go @@ -1,4 +1,4 @@ -package materializationapi +package dataframe import ( "bytes" diff --git a/internal/api/graphql/graph/materialization/filter_test.go b/internal/api/graphql/graph/dataframe/filter_test.go similarity index 97% rename from internal/api/graphql/graph/materialization/filter_test.go rename to internal/api/graphql/graph/dataframe/filter_test.go index 918c3570..f3a78011 100644 --- a/internal/api/graphql/graph/materialization/filter_test.go +++ b/internal/api/graphql/graph/dataframe/filter_test.go @@ -1,4 +1,4 @@ -package materializationapi +package dataframe import ( "encoding/json" diff --git a/internal/api/graphql/graph/materialization/output.go b/internal/api/graphql/graph/dataframe/model.go similarity index 68% rename from internal/api/graphql/graph/materialization/output.go rename to internal/api/graphql/graph/dataframe/model.go index 6812c2e0..b09a062d 100644 --- a/internal/api/graphql/graph/materialization/output.go +++ b/internal/api/graphql/graph/dataframe/model.go @@ -1,4 +1,4 @@ -package materializationapi +package dataframe import ( "encoding/json" @@ -8,6 +8,7 @@ import ( dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" publication "github.com/calypr/loom/internal/dataframe/publication" materialization "github.com/calypr/loom/internal/dataframe/published" + "github.com/calypr/loom/internal/projectid" ) func Model(value materialization.Materialization) *model.DataframeMaterialization { @@ -35,54 +36,15 @@ func Model(value materialization.Materialization) *model.DataframeMaterializatio } result := &model.DataframeMaterialization{ ID: value.ID, Name: value.Name, Revision: revision, - ProjectID: value.Project, DatasetGeneration: value.DatasetGeneration, + ProjectID: projectid.Canonical(value.Project), DatasetGeneration: value.DatasetGeneration, State: model.DataframeMaterializationState(value.State), Columns: columns, RowCount: rowCount, CreatedAt: value.CreatedAt.UTC().Format("2006-01-02T15:04:05.999Z07:00"), ReadyAt: readyAt, Error: failure, ErrorCode: failureCode, ErrorRetryable: failureRetryable, - ProjectStatuses: []*model.DataframeProjectStatus{}, } if value.Selector.Valid() { result.Selector = &model.DataframeSelector{Recipe: value.Selector.Recipe, TranslationVersion: value.Selector.TranslationVersion, Output: value.Selector.Output} } - if value.Availability != "" { - availability := model.DataframeAvailability(value.Availability) - result.Availability = &availability - included, expected := value.IncludedProjects, value.ExpectedProjects - result.IncludedProjectCount, result.ExpectedProjectCount = &included, &expected - completeness := 0.0 - if expected > 0 { - completeness = float64(included) / float64(expected) - } - result.Completeness = &completeness - result.ProjectStatuses = projectStatusModels(value.ProjectStatuses) - } - return result -} - -func projectStatusModels(values []materialization.ProjectStatus) []*model.DataframeProjectStatus { - result := make([]*model.DataframeProjectStatus, 0, len(values)) - for _, value := range values { - item := &model.DataframeProjectStatus{Project: value.ProjectID, State: model.DataframeProjectState(value.State), Retryable: value.Retryable} - if value.Generation != "" { - item.Generation = &value.Generation - } - if value.ExecutionID != "" { - item.ExecutionID = &value.ExecutionID - } - if !value.CreatedAt.IsZero() { - formatted := value.CreatedAt.UTC().Format("2006-01-02T15:04:05.999Z07:00") - item.CreatedAt = &formatted - } - if !value.UpdatedAt.IsZero() { - formatted := value.UpdatedAt.UTC().Format("2006-01-02T15:04:05.999Z07:00") - item.UpdatedAt = &formatted - } - if value.ErrorCode != "" { - item.ErrorCode = &value.ErrorCode - } - result = append(result, item) - } return result } @@ -120,15 +82,6 @@ func PersistedFailure(raw, code string, retryable bool) (message, failureCode *s return &publicMessage, &publicCode, &retryable } -func FederatedMaterialization(dataset materialization.FederatedDataset) *model.DataframeMaterialization { - return Model(materialization.Materialization{ - ID: "federated:" + dataset.Selector.Key(), Name: dataset.Name, Revision: dataset.Revision, - DatasetGeneration: "federated:" + dataset.Revision, State: materialization.StateReady, - Columns: dataset.Columns, RowCount: dataset.RowCount, RowCountKnown: dataset.RowCountComplete, - Selector: dataset.Selector, Availability: dataset.Availability, ExpectedProjects: dataset.ExpectedProjects, ProjectStatuses: dataset.ProjectStatuses, - }) -} - func columnCapabilities(clickHouseType string) (logical string, nullable, repeated, filterable, sortable, aggregatable bool) { typ := clickHouseType if strings.HasPrefix(typ, "Nullable(") && strings.HasSuffix(typ, ")") { diff --git a/internal/api/graphql/graph/materialization/output_test.go b/internal/api/graphql/graph/dataframe/model_test.go similarity index 60% rename from internal/api/graphql/graph/materialization/output_test.go rename to internal/api/graphql/graph/dataframe/model_test.go index a31a2981..105e0ad9 100644 --- a/internal/api/graphql/graph/materialization/output_test.go +++ b/internal/api/graphql/graph/dataframe/model_test.go @@ -1,4 +1,4 @@ -package materializationapi +package dataframe import ( "errors" @@ -15,21 +15,16 @@ func TestPersistedFailureRedactsLegacyText(t *testing.T) { } } -func TestFederationMetadataMapping(t *testing.T) { +func TestProjectMetadataMapping(t *testing.T) { value := Model(dfpublished.Materialization{ - ID: "federated", Name: "DocumentReference", Selector: dfpublished.DataframeSelector{Recipe: "documents", TranslationVersion: "v2", Output: "DocumentReference"}, - Availability: dfpublished.FederationDegraded, ExpectedProjects: 2, - IncludedProjects: 1, - ProjectStatuses: []dfpublished.ProjectStatus{{ProjectID: "allowed", State: dfpublished.ProjectCurrent}, {ProjectID: "missing", State: dfpublished.ProjectMissing}}, + ID: "execution:DocumentReference", Name: "DocumentReference", Project: "P1", DatasetGeneration: "generation", State: dfpublished.StateReady, + Selector: dfpublished.DataframeSelector{Recipe: "documents", TranslationVersion: "v2", Output: "DocumentReference"}, }) if value.Selector == nil || value.Selector.TranslationVersion != "v2" { t.Fatalf("selector metadata = %#v", value) } - if value.Availability == nil || *value.Availability != "DEGRADED" || value.Completeness == nil || *value.Completeness != .5 { - t.Fatalf("availability metadata = %#v", value) - } - if len(value.ProjectStatuses) != 2 { - t.Fatalf("statuses = %#v", value.ProjectStatuses) + if value.ProjectID != "P1" || value.DatasetGeneration != "generation" { + t.Fatalf("project metadata = %#v", value) } } diff --git a/internal/api/graphql/graph/dataframe/rows.go b/internal/api/graphql/graph/dataframe/rows.go new file mode 100644 index 00000000..12164ee3 --- /dev/null +++ b/internal/api/graphql/graph/dataframe/rows.go @@ -0,0 +1,139 @@ +package dataframe + +import ( + "context" + "encoding/json" + "strings" + + "github.com/calypr/loom/generated/graphql/graph/model" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dfmaterialization "github.com/calypr/loom/internal/dataframe/published" +) + +// ProjectDatasets exposes the current output dataframes for one authorized +// project. It is the only dataset-discovery surface. +func (s *Service) ProjectDatasets(ctx context.Context, projectID string) ([]dfmaterialization.Materialization, error) { + if s.reader == nil { + return nil, readerUnavailable() + } + principal, err := s.principal(ctx) + if err != nil { + return nil, err + } + projectID = canonicalProjectID(projectID) + if _, err := s.authorizeProject(ctx, principal, projectID); err != nil { + return nil, err + } + values, err := s.reader.CurrentProjectDatasets(ctx, legacyProjectID(projectID)) + if err != nil { + return nil, mapReaderError(err) + } + result := make([]dfmaterialization.Materialization, 0, len(values)) + for _, value := range values { + if _, scopeErr := s.authorizeMaterialization(ctx, principal, value); scopeErr == nil { + result = append(result, value) + } else if normalized := dataframeerrors.Normalize(scopeErr); normalized.Code() != string(dataframeerrors.CodeDatasetNotFound) { + return nil, scopeErr + } + } + if len(result) == 0 { + return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + return result, nil +} + +func (s *Service) Dataset(ctx context.Context, input model.DataframeDatasetInput) (*dfmaterialization.Materialization, error) { + selector, err := resolveSelector(input.Selector) + if err != nil { + return nil, err + } + value, _, err := s.currentProjectDataset(ctx, input.ProjectID, selector) + if err != nil { + return nil, err + } + return &value, nil +} + +func (s *Service) Rows(ctx context.Context, input model.DataframeRowsInput) (dfmaterialization.Page, error) { + selector, err := resolveSelector(input.Selector) + if err != nil { + return dfmaterialization.Page{}, err + } + value, access, err := s.currentProjectDataset(ctx, input.ProjectID, selector) + if err != nil { + s.logReadFailure(ctx, "published_dataset_resolution", selector.Output, err, "project", input.ProjectID) + return dfmaterialization.Page{}, err + } + page, err := s.reader.Page(ctx, value, dfmaterialization.PageRequest{ + Columns: input.Columns, Filters: convertFilters(input.Filters), Sort: convertSort(input), First: intValue(input.First), After: stringValue(input.After), + AuthResourcePaths: access.authResourcePaths, Unrestricted: access.unrestricted, + }) + if err != nil { + s.logReadFailure(ctx, "clickhouse_rows", selector.Output, err, "project", input.ProjectID) + return dfmaterialization.Page{}, mapReaderError(err) + } + return page, nil +} + +func resolveSelector(input *model.DataframeSelectorInput) (dfmaterialization.DataframeSelector, error) { + if input == nil { + return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "", dataframeerrors.WithFieldPath("input", "selector")) + } + selector := dfmaterialization.DataframeSelector{Recipe: strings.TrimSpace(input.Recipe), TranslationVersion: strings.TrimSpace(input.TranslationVersion), Output: strings.TrimSpace(input.Output)} + if !selector.Valid() { + return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "", dataframeerrors.WithFieldPath("input", "selector")) + } + return selector, nil +} + +func resolvePublishedSelector(input *dfmaterialization.DataframeSelector) (dfmaterialization.DataframeSelector, error) { + if input == nil { + return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "") + } + selector := *input + selector.Recipe = strings.TrimSpace(selector.Recipe) + selector.TranslationVersion = strings.TrimSpace(selector.TranslationVersion) + selector.Output = strings.TrimSpace(selector.Output) + if !selector.Valid() { + return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "") + } + return selector, nil +} + +func convertFilters(filters []*model.DataframeFilterInput) []dfmaterialization.Filter { + converted := make([]dfmaterialization.Filter, 0, len(filters)) + for _, filter := range filters { + if filter == nil { + continue + } + var value any + if len(filter.Value) > 0 { + if err := json.Unmarshal(filter.Value, &value); err != nil { + value = filter.Value + } + } + converted = append(converted, dfmaterialization.Filter{Column: filter.Column, Op: filter.Op, Value: value}) + } + return converted +} + +func convertSort(input model.DataframeRowsInput) *dfmaterialization.Sort { + if input.Sort == nil { + return nil + } + return &dfmaterialization.Sort{Column: input.Sort.Column, Desc: input.Sort.Desc != nil && *input.Sort.Desc} +} + +func intValue(value *int) int { + if value == nil { + return 0 + } + return *value +} + +func stringValue(value *string) string { + if value == nil { + return "" + } + return *value +} diff --git a/internal/api/graphql/graph/dataframe/service.go b/internal/api/graphql/graph/dataframe/service.go new file mode 100644 index 00000000..4e86d118 --- /dev/null +++ b/internal/api/graphql/graph/dataframe/service.go @@ -0,0 +1,182 @@ +// Package dataframe adapts one project's published ClickHouse +// dataframe outputs to GraphQL. Project identity is explicit on every read; +// this package never discovers or combines data from other projects. +package dataframe + +import ( + "context" + "errors" + "log/slog" + "strings" + + httpapi "github.com/calypr/loom/internal/api/http" + "github.com/calypr/loom/internal/authscope" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dfmaterialization "github.com/calypr/loom/internal/dataframe/published" + "github.com/calypr/loom/internal/projectid" +) + +type Service struct { + reader *dfmaterialization.Reader + scopeResolver *authscope.ScopeResolver + logger *slog.Logger + maxExportRows int64 + maxExportBytes int64 +} + +type Config struct { + Reader *dfmaterialization.Reader + ScopeResolver *authscope.ScopeResolver + Logger *slog.Logger + MaxExportRows int64 + MaxExportBytes int64 +} + +func NewService(cfg Config) *Service { + if cfg.MaxExportRows <= 0 { + cfg.MaxExportRows = 1_000_000 + } + if cfg.MaxExportBytes <= 0 { + cfg.MaxExportBytes = 1 << 30 + } + if cfg.Logger == nil { + cfg.Logger = slog.Default() + } + return &Service{reader: cfg.Reader, scopeResolver: cfg.ScopeResolver, logger: cfg.Logger, maxExportRows: cfg.MaxExportRows, maxExportBytes: cfg.MaxExportBytes} +} + +func (s *Service) principal(ctx context.Context) (*authscope.Principal, error) { + principal, ok := authscope.PrincipalFromContext(ctx) + if !ok || principal == nil { + return nil, dataframeerrors.NewError(dataframeerrors.CodeUnauthenticated, "") + } + return principal, nil +} + +type projectAccess struct { + authResourcePaths []string + unrestricted bool +} + +func (s *Service) authorizeProject(_ context.Context, principal *authscope.Principal, project string) (projectAccess, error) { + project = canonicalProjectID(project) + if project == "" { + return projectAccess{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + if err := authscope.AuthorizeProject(principal, project, false); err != nil { + return projectAccess{}, mapReaderError(err) + } + return projectAccess{}, nil +} + +func (s *Service) authorizeMaterialization(ctx context.Context, principal *authscope.Principal, value dfmaterialization.Materialization) (projectAccess, error) { + project := canonicalProjectID(value.Project) + if err := authscope.AuthorizeProject(principal, project, false); err != nil { + return projectAccess{}, mapReaderError(err) + } + if s.scopeResolver == nil { + if value.ScopeUnrestricted { + return projectAccess{unrestricted: true}, nil + } + if len(principal.AuthResourcePaths) == 0 { + return projectAccess{}, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + return projectAccess{authResourcePaths: append([]string(nil), principal.AuthResourcePaths...)}, nil + } + scope, err := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, legacyProjectID(project), value.DatasetGeneration, nil) + if err != nil { + return projectAccess{}, mapReaderError(err) + } + access := projectAccess{authResourcePaths: append([]string(nil), scope.AuthResourcePaths...), unrestricted: scope.Unrestricted()} + if !access.unrestricted && len(access.authResourcePaths) == 0 { + return projectAccess{}, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + return access, nil +} + +func (s *Service) currentProjectDataset(ctx context.Context, project string, selector dfmaterialization.DataframeSelector) (dfmaterialization.Materialization, projectAccess, error) { + if s.reader == nil { + return dfmaterialization.Materialization{}, projectAccess{}, readerUnavailable() + } + principal, err := s.principal(ctx) + if err != nil { + return dfmaterialization.Materialization{}, projectAccess{}, err + } + return s.currentProjectDatasetForPrincipal(ctx, principal, project, selector) +} + +func (s *Service) currentProjectDatasetForPrincipal(ctx context.Context, principal *authscope.Principal, project string, selector dfmaterialization.DataframeSelector) (dfmaterialization.Materialization, projectAccess, error) { + if state := aggregateStateFromContext(ctx); state != nil && state.service == s { + return state.projectDataset(ctx, project, selector, func() (dfmaterialization.Materialization, projectAccess, error) { + return s.resolveCurrentProjectDatasetForPrincipal(ctx, principal, project, selector) + }) + } + return s.resolveCurrentProjectDatasetForPrincipal(ctx, principal, project, selector) +} + +func (s *Service) resolveCurrentProjectDatasetForPrincipal(ctx context.Context, principal *authscope.Principal, project string, selector dfmaterialization.DataframeSelector) (dfmaterialization.Materialization, projectAccess, error) { + project = canonicalProjectID(project) + if _, err := s.authorizeProject(ctx, principal, project); err != nil { + return dfmaterialization.Materialization{}, projectAccess{}, err + } + value, err := s.reader.CurrentProjectDataset(ctx, legacyProjectID(project), selector) + if err != nil { + return dfmaterialization.Materialization{}, projectAccess{}, mapReaderError(err) + } + access, err := s.authorizeMaterialization(ctx, principal, value) + if err != nil { + return dfmaterialization.Materialization{}, projectAccess{}, err + } + return value, access, nil +} + +func (s *Service) logReadFailure(ctx context.Context, phase, resourceType string, err error, attrs ...any) { + if s.logger == nil { + return + } + fields := []any{"request_id", httpapi.RequestIDFromContext(ctx), "phase", phase, "resource_type", resourceType, "error_chain", errorChain(err)} + fields = append(fields, attrs...) + s.logger.Error("dataframe read failed", fields...) +} + +func errorChain(err error) string { + if err == nil { + return "" + } + parts := make([]string, 0, 3) + for current := err; current != nil; current = errors.Unwrap(current) { + parts = append(parts, current.Error()) + } + return strings.Join(parts, " <- ") +} + +func mapReaderError(err error) error { + if err == nil { + return nil + } + if _, ok := dataframeerrors.AsUserError(err); ok { + return err + } + if errors.Is(err, authscope.ErrUnauthenticated) { + return dataframeerrors.NewError(dataframeerrors.CodeUnauthenticated, "") + } + if errors.Is(err, authscope.ErrForbidden) { + return dataframeerrors.NewError(dataframeerrors.CodeForbidden, "") + } + if errors.Is(err, authscope.ErrAuthorizationBackendUnavailable) { + return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) + } + return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) +} + +func readerUnavailable() error { + return dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) +} + +func canonicalProjectID(raw string) string { + return projectid.Canonical(strings.TrimSpace(raw)) +} + +func legacyProjectID(raw string) string { + return projectid.Legacy(canonicalProjectID(raw)) +} diff --git a/internal/api/graphql/graph/dataframe/service_test.go b/internal/api/graphql/graph/dataframe/service_test.go new file mode 100644 index 00000000..8f607a02 --- /dev/null +++ b/internal/api/graphql/graph/dataframe/service_test.go @@ -0,0 +1,71 @@ +package dataframe + +import ( + "context" + "testing" + + "github.com/calypr/loom/generated/graphql/graph/model" + "github.com/calypr/loom/internal/authscope" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dfmaterialization "github.com/calypr/loom/internal/dataframe/published" +) + +func TestResolveSelectorRequiresCompleteExplicitSelector(t *testing.T) { + selector, err := resolveSelector(&model.DataframeSelectorInput{Recipe: "documents", TranslationVersion: "v2", Output: "Patient"}) + if err != nil { + t.Fatal(err) + } + if selector.Recipe != "documents" || selector.TranslationVersion != "v2" || selector.Output != "Patient" { + t.Fatalf("selector = %#v", selector) + } +} + +func TestResolveSelectorRejectsMissingSelector(t *testing.T) { + _, err := resolveSelector(nil) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodeInvalidSelector) { + t.Fatalf("error = %v", err) + } +} + +func TestAuthorizeProjectUsesPrincipalProjectGrant(t *testing.T) { + service := NewService(Config{}) + principal := &authscope.Principal{Subject: "user", Projects: []string{"P1"}} + if _, err := service.authorizeProject(context.Background(), principal, "P1"); err != nil { + t.Fatal(err) + } + if _, err := service.authorizeProject(context.Background(), principal, "P2"); err == nil { + t.Fatal("unauthorized project was accepted") + } +} + +func TestAuthorizeProjectRequiresProject(t *testing.T) { + service := NewService(Config{}) + _, err := service.authorizeProject(context.Background(), &authscope.Principal{}, " ") + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodeInvalidRequest) { + t.Fatalf("error = %v", err) + } +} + +func TestAuthorizeRestrictedMaterializationRequiresResourcePathsWithoutResolver(t *testing.T) { + service := NewService(Config{}) + principal := &authscope.Principal{Subject: "user", Projects: []string{"P1"}} + _, err := service.authorizeMaterialization(context.Background(), principal, dfmaterialization.Materialization{Project: "P1"}) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodeDatasetNotFound) { + t.Fatalf("error = %v", err) + } +} + +func TestAuthorizeUnrestrictedMaterializationWithoutResolver(t *testing.T) { + service := NewService(Config{}) + principal := &authscope.Principal{Subject: "user", Projects: []string{"P1"}} + access, err := service.authorizeMaterialization(context.Background(), principal, dfmaterialization.Materialization{Project: "P1", ScopeUnrestricted: true}) + if err != nil { + t.Fatal(err) + } + if !access.unrestricted || len(access.authResourcePaths) != 0 { + t.Fatalf("access = %#v", access) + } +} diff --git a/internal/api/graphql/graph/doc.go b/internal/api/graphql/graph/doc.go index 1239a293..6a9b0d3c 100644 --- a/internal/api/graphql/graph/doc.go +++ b/internal/api/graphql/graph/doc.go @@ -1,7 +1,7 @@ // Package graphqlapi owns Loom's primary GraphQL transport. // // HTTP wiring and error presentation live here. Query construction lives in -// query, dataframe response mapping in materialization, and the flat endpoint -// handler in the sibling flat package. Generated GraphQL types, executors, and resolver -// bindings live outside internal under generated/graphql/graph. +// query, dataframe access and response mapping in dataframe, resolver wiring in +// resolver, and the schema source in schema. Generated GraphQL types, executors, +// and resolver bindings live outside internal under generated/graphql/graph. package graphqlapi diff --git a/internal/api/graphql/errors.go b/internal/api/graphql/graph/errors.go similarity index 72% rename from internal/api/graphql/errors.go rename to internal/api/graphql/graph/errors.go index 8201121d..dc2b4241 100644 --- a/internal/api/graphql/errors.go +++ b/internal/api/graphql/graph/errors.go @@ -1,4 +1,4 @@ -package graphqlerrors +package graphqlapi import ( "errors" @@ -7,10 +7,10 @@ import ( "github.com/vektah/gqlparser/v2/gqlerror" ) -// PresentError converts a service error to the GraphQL error shape used by +// presentError converts a service error to the GraphQL error shape used by // the frontend. The original error remains attached to gqlerror.Error.Err so // server logging and errors.Is/errors.As retain their normal behavior. -func PresentError(err error, requestID string) *gqlerror.Error { +func presentError(err error, requestID string) *gqlerror.Error { if err == nil { return nil } @@ -18,7 +18,7 @@ func PresentError(err error, requestID string) *gqlerror.Error { result := &gqlerror.Error{ Err: err, Message: dataframeerrors.PublicMessage(userErr), - Extensions: ExtensionsForError(userErr, requestID), + Extensions: extensionsForError(userErr, requestID), } var original *gqlerror.Error if errors.As(err, &original) && original != nil { @@ -27,7 +27,7 @@ func PresentError(err error, requestID string) *gqlerror.Error { return result } -func PresentGraphQLError(err error, requestID string) *gqlerror.Error { +func presentGraphQLError(err error, requestID string) *gqlerror.Error { var gqlErr *gqlerror.Error if errors.As(err, &gqlErr) && gqlErr != nil { // Parser and validation errors are already safe and actionable. Preserve @@ -36,22 +36,28 @@ func PresentGraphQLError(err error, requestID string) *gqlerror.Error { copy := *gqlErr if len(copy.Extensions) == 0 { copy.Extensions = map[string]any{"code": "GRAPHQL_VALIDATION_FAILED", "retryable": false} + } else { + extensions := make(map[string]any, len(copy.Extensions)+1) + for key, value := range copy.Extensions { + extensions[key] = value + } + copy.Extensions = extensions } if requestID != "" { copy.Extensions["requestId"] = requestID } return © } - presented := PresentError(gqlErr.Err, requestID) + presented := presentError(gqlErr.Err, requestID) presented.Path, presented.Locations = gqlErr.Path, gqlErr.Locations return presented } - return PresentError(err, requestID) + return presentError(err, requestID) } -// ExtensionsForError returns a fresh map suitable for a GraphQL error +// extensionsForError returns a fresh map suitable for a GraphQL error // response. It deliberately exposes only the stable semantic contract. -func ExtensionsForError(err error, requestID string) map[string]any { +func extensionsForError(err error, requestID string) map[string]any { userErr := dataframeerrors.Normalize(err) if userErr == nil { return nil diff --git a/internal/api/graphql/graph/errors_test.go b/internal/api/graphql/graph/errors_test.go new file mode 100644 index 00000000..2d7fca64 --- /dev/null +++ b/internal/api/graphql/graph/errors_test.go @@ -0,0 +1,189 @@ +package graphqlapi + +import ( + "errors" + "reflect" + "testing" + + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + "github.com/vektah/gqlparser/v2/ast" + "github.com/vektah/gqlparser/v2/gqlerror" +) + +func TestPresentErrorUsesStableExtensions(t *testing.T) { + err := dataframeerrors.NewError( + dataframeerrors.CodeUnknownField, + "secret internal selector detail", + dataframeerrors.WithFieldPath("rootFields", "2", "fieldRef"), + dataframeerrors.WithDetails(map[string]any{"fieldRef": "Patient.missing", "aql": "FOR p IN Patient RETURN p"}), + ) + graphqlErr := presentError(err, "request-123") + if graphqlErr == nil { + t.Fatal("presentError returned nil") + } + if graphqlErr.Message != "the selected field is not recognized" { + t.Fatalf("message = %q", graphqlErr.Message) + } + if graphqlErr.Extensions["code"] != string(dataframeerrors.CodeUnknownField) { + t.Fatalf("extensions = %#v", graphqlErr.Extensions) + } + if graphqlErr.Extensions["requestId"] != "request-123" { + t.Fatalf("request ID missing: %#v", graphqlErr.Extensions) + } + if _, ok := graphqlErr.Extensions["aql"]; ok { + t.Fatalf("AQL extension leaked: %#v", graphqlErr.Extensions) + } + if !errors.Is(graphqlErr, err) { + t.Fatal("GraphQL error did not preserve original cause") + } +} + +func TestPresentErrorRedactsUnknownErrors(t *testing.T) { + graphqlErr := presentError(errors.New("AQL FOR p IN Patient bind=@token"), "request-456") + if graphqlErr.Message != "internal server error" { + t.Fatalf("unknown message = %q", graphqlErr.Message) + } + if graphqlErr.Extensions["code"] != string(dataframeerrors.CodeInternalError) { + t.Fatalf("unknown extensions = %#v", graphqlErr.Extensions) + } + if graphqlErr.Extensions["details"] != nil { + t.Fatalf("unknown details leaked: %#v", graphqlErr.Extensions) + } +} + +func TestPresentRecipeResolutionErrorKeepsActionableMessage(t *testing.T) { + err := dataframeerrors.NewError( + dataframeerrors.CodeRecipeResolutionFailed, + `outputs[0] DocumentReference: field "project_id" is not in the active FHIR schema`, + ) + graphqlErr := presentError(err, "request-789") + if graphqlErr.Message != err.Error() { + t.Fatalf("recipe resolution message = %q", graphqlErr.Message) + } + if graphqlErr.Extensions["code"] != string(dataframeerrors.CodeRecipeResolutionFailed) { + t.Fatalf("recipe resolution extensions = %#v", graphqlErr.Extensions) + } +} + +func TestExtensionsForErrorOmitsEmptyRequestID(t *testing.T) { + extensions := extensionsForError(dataframeerrors.NewError(dataframeerrors.CodeInvalidCursor, ""), "") + if _, ok := extensions["requestId"]; ok { + t.Fatalf("empty request ID should be omitted: %#v", extensions) + } + if extensions["fieldPath"] == nil { + t.Fatal("fieldPath extension should remain present for stable shape") + } +} + +func TestPresentGraphQLErrorAddsDefaultsToValidationError(t *testing.T) { + original := &gqlerror.Error{ + Message: `Cannot query field "missing" on type "Patient".`, + Path: ast.Path{ast.PathName("patient")}, + Locations: []gqlerror.Location{{Line: 4, Column: 9}}, + } + + presented := presentGraphQLError(original, "request-validation") + if presented == nil { + t.Fatal("presentGraphQLError returned nil") + } + if presented.Message != original.Message { + t.Fatalf("message = %q, want %q", presented.Message, original.Message) + } + if !reflect.DeepEqual(presented.Path, original.Path) { + t.Fatalf("path = %#v, want %#v", presented.Path, original.Path) + } + if !reflect.DeepEqual(presented.Locations, original.Locations) { + t.Fatalf("locations = %#v, want %#v", presented.Locations, original.Locations) + } + if presented.Extensions["code"] != "GRAPHQL_VALIDATION_FAILED" { + t.Fatalf("code = %#v", presented.Extensions["code"]) + } + if presented.Extensions["retryable"] != false { + t.Fatalf("retryable = %#v", presented.Extensions["retryable"]) + } + if presented.Extensions["requestId"] != "request-validation" { + t.Fatalf("request ID = %#v", presented.Extensions["requestId"]) + } +} + +func TestPresentGraphQLErrorPreservesValidationExtensions(t *testing.T) { + original := &gqlerror.Error{ + Message: `Variable "$selector" got invalid value.`, + Extensions: map[string]any{ + "code": "CUSTOM_VALIDATION", + "retryable": true, + "hint": "use a valid selector", + }, + } + + presented := presentGraphQLError(original, "request-validation-custom") + if presented == nil { + t.Fatal("presentGraphQLError returned nil") + } + if presented.Extensions["code"] != "CUSTOM_VALIDATION" { + t.Fatalf("code = %#v", presented.Extensions["code"]) + } + if presented.Extensions["retryable"] != true { + t.Fatalf("retryable = %#v", presented.Extensions["retryable"]) + } + if presented.Extensions["hint"] != "use a valid selector" { + t.Fatalf("hint = %#v", presented.Extensions["hint"]) + } + if presented.Extensions["requestId"] != "request-validation-custom" { + t.Fatalf("request ID = %#v", presented.Extensions["requestId"]) + } +} + +func TestPresentGraphQLErrorDoesNotMutateValidationExtensions(t *testing.T) { + original := &gqlerror.Error{ + Message: `Variable "$selector" got invalid value.`, + Extensions: map[string]any{ + "code": "CUSTOM_VALIDATION", + }, + } + + presented := presentGraphQLError(original, "request-validation-custom") + if presented == nil { + t.Fatal("presentGraphQLError returned nil") + } + if _, ok := original.Extensions["requestId"]; ok { + t.Fatalf("presentGraphQLError mutated the original extensions: %#v", original.Extensions) + } + if presented.Extensions["requestId"] != "request-validation-custom" { + t.Fatalf("presented request ID = %#v", presented.Extensions["requestId"]) + } +} + +func TestPresentGraphQLErrorPresentsWrappedSemanticCause(t *testing.T) { + semantic := dataframeerrors.NewError( + dataframeerrors.CodeUnknownField, + "secret internal selector detail", + dataframeerrors.WithFieldPath("input", "field"), + ) + original := &gqlerror.Error{ + Err: semantic, + Message: "internal selector failure", + Path: ast.Path{ast.PathName("patient")}, + Locations: []gqlerror.Location{{Line: 8, Column: 12}}, + } + + presented := presentGraphQLError(original, "request-semantic") + if presented == nil { + t.Fatal("presentGraphQLError returned nil") + } + if presented.Message != "the selected field is not recognized" { + t.Fatalf("message = %q", presented.Message) + } + if presented.Extensions["code"] != string(dataframeerrors.CodeUnknownField) { + t.Fatalf("code = %#v", presented.Extensions["code"]) + } + if !errors.Is(presented, semantic) { + t.Fatal("GraphQL error did not preserve semantic cause") + } + if !reflect.DeepEqual(presented.Path, original.Path) { + t.Fatalf("path = %#v, want %#v", presented.Path, original.Path) + } + if !reflect.DeepEqual(presented.Locations, original.Locations) { + t.Fatalf("locations = %#v, want %#v", presented.Locations, original.Locations) + } +} diff --git a/internal/api/graphql/graph/handler.go b/internal/api/graphql/graph/handler.go index bd112ae3..5afd80f6 100644 --- a/internal/api/graphql/graph/handler.go +++ b/internal/api/graphql/graph/handler.go @@ -14,7 +14,6 @@ import ( gqlhandler "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/calypr/loom/generated/graphql/graph/executor" - graphqlerrors "github.com/calypr/loom/internal/api/graphql" "github.com/calypr/loom/internal/api/graphql/graph/resolver" httpapi "github.com/calypr/loom/internal/api/http" fiberadaptor "github.com/gofiber/fiber/v3/middleware/adaptor" @@ -33,7 +32,7 @@ func NewHandler(root *resolver.Resolver, loggers ...*slog.Logger) http.Handler { })) server.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error { requestID := httpapi.RequestIDFromContext(ctx) - presented := graphqlerrors.PresentGraphQLError(err, requestID) + presented := presentGraphQLError(err, requestID) if presented != nil { code, _ := presented.Extensions["code"].(string) cause := presented.Err diff --git a/internal/api/graphql/graph/http_integration_test.go b/internal/api/graphql/graph/http_integration_test.go index e8a2769c..f2143f20 100644 --- a/internal/api/graphql/graph/http_integration_test.go +++ b/internal/api/graphql/graph/http_integration_test.go @@ -14,7 +14,7 @@ import ( api "github.com/calypr/loom/internal/api/http" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/catalog" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" publication "github.com/calypr/loom/internal/dataset" fiberadaptor "github.com/gofiber/fiber/v3/middleware/adaptor" ) @@ -110,7 +110,7 @@ func TestProjectDataframeDatasetsResolverIsWired(t *testing.T) { } func TestGraphQLRunDataframeMutation(t *testing.T) { - dfService := runtime.NewService(runtime.ServiceConfig{ + dfService := dataframeexecution.NewService(dataframeexecution.ServiceConfig{ QueryRows: func(ctx context.Context, query string, _ int, bindVars map[string]any, visit func(map[string]any) error) error { if !strings.Contains(query, "LET child_set_") || !strings.Contains(query, "LENGTH(child_set_") { t.Fatalf("expected physical child-set query, got:\n%s", query) @@ -199,7 +199,7 @@ func TestGraphQLRunDataframeMutation(t *testing.T) { } func TestGraphQLRunDataframeTraversalBuilder(t *testing.T) { - dfService := runtime.NewService(runtime.ServiceConfig{ + dfService := dataframeexecution.NewService(dataframeexecution.ServiceConfig{ QueryRows: func(ctx context.Context, query string, _ int, bindVars map[string]any, visit func(map[string]any) error) error { if !strings.Contains(query, "LET child_set_") || !strings.Contains(query, "LENGTH(child_set_") { t.Fatalf("expected physical child-set query, got:\n%s", query) diff --git a/internal/api/graphql/graph/materialization/operation_cache.go b/internal/api/graphql/graph/materialization/operation_cache.go deleted file mode 100644 index 528eb98c..00000000 --- a/internal/api/graphql/graph/materialization/operation_cache.go +++ /dev/null @@ -1,107 +0,0 @@ -package materializationapi - -import ( - "context" - "sort" - "strings" - - httpapi "github.com/calypr/loom/internal/api/http" - "github.com/calypr/loom/internal/authscope" - dfmaterialization "github.com/calypr/loom/internal/dataframe/published" -) - -type projectsCacheEntry struct { - ready chan struct{} - projects []string - err error -} - -type federationCacheEntry struct { - ready chan struct{} - dataset dfmaterialization.FederatedDataset - access map[string]dfmaterialization.SourceAccess - err error -} - -func (s *Service) projects(ctx context.Context, principal *authscope.Principal) ([]string, error) { - state := aggregateStateFromContext(ctx) - if state == nil || state.service != s { - return s.projectsUncached(ctx, principal) - } - state.mu.Lock() - if entry := state.projects; entry != nil { - state.mu.Unlock() - if s.logger != nil { - s.logger.Debug("dataframe project discovery cache wait", "request_id", httpapi.RequestIDFromContext(ctx)) - } - select { - case <-entry.ready: - return append([]string(nil), entry.projects...), entry.err - case <-ctx.Done(): - return nil, ctx.Err() - } - } - entry := &projectsCacheEntry{ready: make(chan struct{})} - state.projects = entry - state.mu.Unlock() - if s.logger != nil { - s.logger.Debug("dataframe project discovery cache miss", "request_id", httpapi.RequestIDFromContext(ctx)) - } - - entry.projects, entry.err = s.projectsUncached(ctx, principal) - entry.projects = append([]string(nil), entry.projects...) - close(entry.ready) - if s.logger != nil { - s.logger.Debug("dataframe project discovery resolved", "request_id", httpapi.RequestIDFromContext(ctx), "project_count", len(entry.projects), "error", entry.err) - } - return append([]string(nil), entry.projects...), entry.err -} - -func (s *Service) authorizedFederation(ctx context.Context, principal *authscope.Principal, selector dfmaterialization.DataframeSelector, filters []dfmaterialization.Filter) (dfmaterialization.FederatedDataset, map[string]dfmaterialization.SourceAccess, error) { - candidates, err := s.projects(ctx, principal) - if err != nil { - s.logReadFailure(ctx, "authorized_project_discovery", selector.Output, err) - return dfmaterialization.FederatedDataset{}, nil, mapReaderError(err) - } - candidates = filterProjects(candidates, filters) - key := federationCacheKey(selector, candidates) - state := aggregateStateFromContext(ctx) - if state == nil || state.service != s { - return s.authorizedFederationUncached(ctx, principal, selector, candidates) - } - state.mu.Lock() - if entry := state.federations[key]; entry != nil { - state.mu.Unlock() - if s.logger != nil { - s.logger.Debug("dataframe federation cache wait", "request_id", httpapi.RequestIDFromContext(ctx), "selector", selector.Key(), "candidate_project_count", len(candidates)) - } - select { - case <-entry.ready: - return entry.dataset, entry.access, entry.err - case <-ctx.Done(): - return dfmaterialization.FederatedDataset{}, nil, ctx.Err() - } - } - entry := &federationCacheEntry{ready: make(chan struct{})} - state.federations[key] = entry - state.mu.Unlock() - if s.logger != nil { - s.logger.Debug("dataframe federation cache miss", "request_id", httpapi.RequestIDFromContext(ctx), "selector", selector.Key(), "candidate_project_count", len(candidates)) - } - - entry.dataset, entry.access, entry.err = s.authorizedFederationUncached(ctx, principal, selector, candidates) - close(entry.ready) - if s.logger != nil { - s.logger.Debug("dataframe federation resolved", "request_id", httpapi.RequestIDFromContext(ctx), "selector", selector.Key(), "source_count", len(entry.dataset.Sources), "authorized_project_count", len(entry.access), "error", entry.err) - } - return entry.dataset, entry.access, entry.err -} - -func federationCacheKey(selector dfmaterialization.DataframeSelector, projects []string) string { - values := append([]string(nil), projects...) - for i := range values { - values[i] = strings.TrimSpace(values[i]) - } - sort.Strings(values) - return selector.Key() + "\x00" + strings.Join(values, "\x1f") -} diff --git a/internal/api/graphql/graph/materialization/operation_cache_test.go b/internal/api/graphql/graph/materialization/operation_cache_test.go deleted file mode 100644 index 0b864f35..00000000 --- a/internal/api/graphql/graph/materialization/operation_cache_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package materializationapi - -import ( - "context" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/calypr/loom/internal/authscope" -) - -func TestProjectsCacheSharesInflightResolutionPerOperation(t *testing.T) { - var calls atomic.Int32 - service := NewService(Config{CandidateProjects: func(context.Context) ([]string, error) { - calls.Add(1) - time.Sleep(time.Millisecond) - return []string{"project"}, nil - }}) - ctx := service.WithOperationContext(context.Background(), 0) - principal := &authscope.Principal{Subject: "subject"} - var wait sync.WaitGroup - for range 32 { - wait.Add(1) - go func() { - defer wait.Done() - projects, err := service.projects(ctx, principal) - if err != nil || len(projects) != 1 || projects[0] != "project" { - t.Errorf("projects = %#v, err = %v", projects, err) - } - }() - } - wait.Wait() - if calls.Load() != 1 { - t.Fatalf("candidate project calls = %d, want 1", calls.Load()) - } - - second := service.WithOperationContext(context.Background(), 0) - if _, err := service.projects(second, principal); err != nil { - t.Fatal(err) - } - if calls.Load() != 2 { - t.Fatalf("cache crossed operation contexts: calls = %d", calls.Load()) - } -} diff --git a/internal/api/graphql/graph/materialization/service.go b/internal/api/graphql/graph/materialization/service.go deleted file mode 100644 index 395bdf4b..00000000 --- a/internal/api/graphql/graph/materialization/service.go +++ /dev/null @@ -1,528 +0,0 @@ -// Package materializationapi adapts published ClickHouse dataframe outputs to -// GraphQL. Dataset discovery and reads are principal-scoped federations; the -// browser supplies only the FHIR resource type. -package materializationapi - -import ( - "context" - "encoding/json" - "errors" - "log/slog" - "strings" - "time" - - "github.com/calypr/loom/generated/graphql/graph/model" - httpapi "github.com/calypr/loom/internal/api/http" - "github.com/calypr/loom/internal/authscope" - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - dfmaterialization "github.com/calypr/loom/internal/dataframe/published" -) - -type Service struct { - reader *dfmaterialization.Reader - scopeResolver *authscope.ScopeResolver - logger *slog.Logger - maxExportRows int64 - maxExportBytes int64 - candidateProjects func(context.Context) ([]string, error) -} - -type Config struct { - Reader *dfmaterialization.Reader - ScopeResolver *authscope.ScopeResolver - Logger *slog.Logger - MaxExportRows int64 - MaxExportBytes int64 - CandidateProjects func(context.Context) ([]string, error) -} - -func NewService(cfg Config) *Service { - if cfg.MaxExportRows <= 0 { - cfg.MaxExportRows = 1_000_000 - } - if cfg.MaxExportBytes <= 0 { - cfg.MaxExportBytes = 1 << 30 - } - if cfg.Logger == nil { - cfg.Logger = slog.Default() - } - return &Service{reader: cfg.Reader, scopeResolver: cfg.ScopeResolver, logger: cfg.Logger, maxExportRows: cfg.MaxExportRows, maxExportBytes: cfg.MaxExportBytes, candidateProjects: cfg.CandidateProjects} -} - -func (s *Service) principal(ctx context.Context) (*authscope.Principal, error) { - principal, ok := authscope.PrincipalFromContext(ctx) - if !ok || principal == nil { - return nil, dataframeerrors.NewError(dataframeerrors.CodeUnauthenticated, "") - } - return principal, nil -} - -func (s *Service) projectsUncached(ctx context.Context, principal *authscope.Principal) ([]string, error) { - if len(principal.Projects) > 0 { - return append([]string(nil), principal.Projects...), nil - } - if s.candidateProjects != nil { - projects, err := s.candidateProjects(ctx) - if err != nil { - return nil, mapReaderError(err) - } - if len(projects) == 0 { - return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } - return projects, nil - } - projects, err := s.reader.PublishedProjects(ctx) - if err != nil { - return nil, mapReaderError(err) - } - if len(projects) == 0 { - // An empty publication catalog is an expected data state, not an - // authorization failure. Callers can present a useful "not published" - // state while real source-level access checks still fail closed below. - return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } - return projects, nil -} - -func (s *Service) Datasets(ctx context.Context) ([]dfmaterialization.Materialization, error) { - if s.reader == nil { - return nil, readerUnavailable() - } - principal, err := s.principal(ctx) - if err != nil { - return nil, err - } - projects, err := s.projects(ctx, principal) - if err != nil { - return nil, mapReaderError(err) - } - selectors, err := s.reader.CurrentFederatedSelectors(ctx, projects) - if err != nil { - return nil, mapReaderError(err) - } - result := make([]dfmaterialization.Materialization, 0, len(selectors)) - for _, selector := range selectors { - if dataset, _, resolveErr := s.authorizedFederation(ctx, principal, selector, nil); resolveErr == nil { - result = append(result, federatedMaterialization(dataset)) - } else if normalized := dataframeerrors.Normalize(resolveErr); normalized.Code() != string(dataframeerrors.CodeDatasetNotFound) { - return nil, resolveErr - } - } - return result, nil -} - -// ProjectDatasets exposes the exact active-generation schemas for one -// authorized project. It intentionally does not federate across projects. -func (s *Service) ProjectDatasets(ctx context.Context, projectID string) ([]dfmaterialization.Materialization, error) { - if s.reader == nil { - return nil, readerUnavailable() - } - projectID = strings.TrimSpace(projectID) - if projectID == "" { - return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - principal, err := s.principal(ctx) - if err != nil { - return nil, err - } - if len(principal.Projects) > 0 && !containsProject(principal.Projects, projectID) { - return nil, dataframeerrors.NewError(dataframeerrors.CodeUnauthorizedProject, "") - } - values, err := s.reader.CurrentProjectDatasets(ctx, projectID) - if err != nil { - return nil, mapReaderError(err) - } - for _, value := range values { - if s.scopeResolver != nil { - if _, scopeErr := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, value.Project, value.DatasetGeneration, nil); scopeErr != nil { - return nil, mapReaderError(scopeErr) - } - } - } - return values, nil -} - -func containsProject(projects []string, project string) bool { - for _, value := range projects { - if strings.TrimSpace(value) == project { - return true - } - } - return false -} - -func (s *Service) Dataset(ctx context.Context, input model.DataframeDatasetInput) (*dfmaterialization.Materialization, error) { - if s.reader == nil { - return nil, readerUnavailable() - } - principal, err := s.principal(ctx) - if err != nil { - return nil, err - } - selector, err := resolveSelector(input.Selector) - if err != nil { - return nil, err - } - dataset, _, err := s.authorizedFederation(ctx, principal, selector, nil) - if err != nil { - return nil, mapReaderError(err) - } - value := federatedMaterialization(dataset) - return &value, nil -} - -func (s *Service) Rows(ctx context.Context, input model.DataframeRowsInput) (dfmaterialization.Page, error) { - if s.reader == nil { - return dfmaterialization.Page{}, readerUnavailable() - } - selector, err := resolveSelector(input.Selector) - if err != nil { - return dfmaterialization.Page{}, err - } - principal, err := s.principal(ctx) - if err != nil { - s.logReadFailure(ctx, "principal_resolution", selector.Output, err) - return dfmaterialization.Page{}, mapReaderError(err) - } - dataset, access, err := s.authorizedFederation(ctx, principal, selector, convertFilters(input.Filters)) - if err != nil { - s.logReadFailure(ctx, "federated_publication_resolution", selector.Output, err) - return dfmaterialization.Page{}, mapReaderError(err) - } - if len(dataset.Sources) == 0 { - return dfmaterialization.Page{}, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } - page, err := s.reader.PageFederatedDataset(ctx, dataset, dfmaterialization.FederatedPageRequest{ - Columns: input.Columns, Filters: convertFilters(input.Filters), Sort: convertSort(input), First: intValue(input.First), After: stringValue(input.After), - AccessByProject: access, - }) - if err != nil { - s.logReadFailure(ctx, "federated_clickhouse_rows", selector.Output, err, "source_ids", materializationIDs(dataset.Sources), "physical_tables", physicalTables(dataset.Sources)) - return dfmaterialization.Page{}, mapReaderError(err) - } - return dfmaterialization.Page{Materialization: federatedMaterialization(dataset), Columns: page.Columns, Rows: page.Rows, TotalCount: page.TotalCount, HasNext: page.HasNext, NextCursor: page.NextCursor}, nil -} - -func (s *Service) AggregateInput(ctx context.Context, input model.DataframeAggregateInput) (dfmaterialization.AggregateResult, error) { - selector, err := resolveSelector(input.Selector) - if err != nil { - return dfmaterialization.AggregateResult{}, err - } - result := s.submitAggregateCall(ctx, &aggregateCall{kind: aggregateCallLegacy, selector: selector, filters: convertFilters(input.Filters), legacy: input}) - if result.err != nil { - s.logReadFailure(ctx, "federated_clickhouse_aggregate", selector.Output, result.err) - } - return result.legacy, result.err -} - -func (s *Service) AggregationsInput(ctx context.Context, input model.DataframeAggregationsInput) (dfmaterialization.AggregationsResult, error) { - selector, err := resolveSelector(input.Selector) - if err != nil { - return dfmaterialization.AggregationsResult{}, err - } - result := s.submitAggregateCall(ctx, &aggregateCall{kind: aggregateCallRich, selector: selector, filters: convertFilters(input.Filters), rich: input}) - return result.rich, result.err -} - -func AggregationsJSON(result dfmaterialization.AggregationsResult) (json.RawMessage, error) { - return json.Marshal(dfmaterialization.NormalizeAggregationResults(result.Aggregations)) -} - -func (s *Service) authorizedFederationUncached(ctx context.Context, principal *authscope.Principal, selector dfmaterialization.DataframeSelector, candidates []string) (dfmaterialization.FederatedDataset, map[string]dfmaterialization.SourceAccess, error) { - started := time.Now() - sources, statuses, err := s.reader.CurrentFederatedSnapshot(ctx, candidates, selector) - snapshotElapsed := time.Since(started) - if err != nil { - s.logReadFailure(ctx, "published_source_discovery", selector.Output, err, "projects", candidates) - return dfmaterialization.FederatedDataset{}, nil, mapReaderError(err) - } - if len(sources) == 0 { - dataset, reconcileErr := dfmaterialization.ReconcileFederatedDataset(selector, candidates, nil) - if len(statuses) > 0 { - dataset.ProjectStatuses = filterAuthorizedStatuses(candidates, statuses) - } - return dataset, map[string]dfmaterialization.SourceAccess{}, reconcileErr - } - s.logger.Debug("dataframe publication sources discovered", "request_id", httpapi.RequestIDFromContext(ctx), "selector", selector.Key(), "projects", candidates, "source_count", len(sources), "source_ids", materializationIDs(sources), "physical_tables", physicalTables(sources)) - access := make(map[string]dfmaterialization.SourceAccess, len(sources)) - authorizedSources := make([]dfmaterialization.Materialization, 0, len(sources)) - rowCountComplete := true - authorizationStarted := time.Now() - for _, source := range sources { - if s.scopeResolver != nil { - scope, err := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, source.Project, source.DatasetGeneration, nil) - if err != nil { - s.logReadFailure(ctx, "read_scope_resolution", selector.Output, err, "project", source.Project, "dataset_generation", source.DatasetGeneration, "materialization_id", source.ID) - return dfmaterialization.FederatedDataset{}, nil, mapReaderError(err) - } - if !scope.Unrestricted() && len(scope.AuthResourcePaths) == 0 { - continue - } - if !scope.Unrestricted() { - rowCountComplete = false - } - access[source.Project] = dfmaterialization.SourceAccess{ResourcePaths: append([]string(nil), scope.AuthResourcePaths...), Unrestricted: scope.Unrestricted()} - authorizedSources = append(authorizedSources, source) - continue - } - if source.ScopeUnrestricted { - access[source.Project] = dfmaterialization.SourceAccess{Unrestricted: true} - authorizedSources = append(authorizedSources, source) - continue - } - if len(principal.AuthResourcePaths) == 0 { - continue - } - rowCountComplete = false - access[source.Project] = dfmaterialization.SourceAccess{ResourcePaths: append([]string(nil), principal.AuthResourcePaths...)} - authorizedSources = append(authorizedSources, source) - } - if len(access) == 0 { - return dfmaterialization.FederatedDataset{}, nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } - authorizationElapsed := time.Since(authorizationStarted) - reconcileStarted := time.Now() - dataset, err := dfmaterialization.ReconcileFederatedDataset(selector, candidates, authorizedSources) - if err != nil { - s.logReadFailure(ctx, "published_schema_reconciliation", selector.Output, err, "source_ids", materializationIDs(authorizedSources), "physical_tables", physicalTables(authorizedSources)) - return dfmaterialization.FederatedDataset{}, nil, err - } - if len(statuses) > 0 { - dataset.ProjectStatuses = filterAuthorizedStatuses(candidates, statuses) - } - reconciliationElapsed := time.Since(reconcileStarted) - discovered := make(map[string]struct{}, len(sources)) - for _, source := range sources { - discovered[source.Project] = struct{}{} - } - for index := range dataset.ProjectStatuses { - status := &dataset.ProjectStatuses[index] - if _, ok := discovered[status.ProjectID]; !ok { - continue - } - if _, ok := access[status.ProjectID]; !ok { - status.State = dfmaterialization.ProjectExcluded - } - } - dataset.RowCountComplete = rowCountComplete - refreshAvailability(&dataset) - s.logger.Info("dataframe federation metadata resolved", - "request_id", httpapi.RequestIDFromContext(ctx), "selector", selector.Key(), - "candidate_project_count", len(candidates), "source_count", len(sources), - "snapshot_ms", snapshotElapsed.Milliseconds(), - "authorization_ms", authorizationElapsed.Milliseconds(), - "reconciliation_ms", reconciliationElapsed.Milliseconds(), - ) - return dataset, access, nil -} - -func refreshAvailability(dataset *dfmaterialization.FederatedDataset) { - if dataset == nil || len(dataset.Sources) == 0 { - if dataset != nil { - dataset.Availability = dfmaterialization.FederationUnavailable - } - return - } - dataset.Availability = dfmaterialization.FederationAvailable - for _, status := range dataset.ProjectStatuses { - if status.State != dfmaterialization.ProjectCurrent { - dataset.Availability = dfmaterialization.FederationDegraded - return - } - } -} - -func filterAuthorizedStatuses(projects []string, statuses []dfmaterialization.ProjectStatus) []dfmaterialization.ProjectStatus { - allowed := make(map[string]struct{}, len(projects)) - for _, project := range projects { - allowed[project] = struct{}{} - } - result := make([]dfmaterialization.ProjectStatus, 0, len(statuses)) - for _, status := range statuses { - if _, ok := allowed[status.ProjectID]; ok { - result = append(result, status) - } - } - return result -} - -func resolveSelector(input *model.DataframeSelectorInput) (dfmaterialization.DataframeSelector, error) { - if input == nil { - return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "", dataframeerrors.WithFieldPath("input", "selector")) - } - selector := dfmaterialization.DataframeSelector{Recipe: strings.TrimSpace(input.Recipe), TranslationVersion: strings.TrimSpace(input.TranslationVersion), Output: strings.TrimSpace(input.Output)} - if !selector.Valid() { - return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "", dataframeerrors.WithFieldPath("input", "selector")) - } - return selector, nil -} - -func resolvePublishedSelector(input *dfmaterialization.DataframeSelector) (dfmaterialization.DataframeSelector, error) { - if input == nil { - return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "") - } - selector := *input - selector.Recipe, selector.TranslationVersion, selector.Output = strings.TrimSpace(selector.Recipe), strings.TrimSpace(selector.TranslationVersion), strings.TrimSpace(selector.Output) - if !selector.Valid() { - return dfmaterialization.DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "") - } - return selector, nil -} - -// filterProjects applies authoritative project_id equality/IN constraints -// before source discovery and schema reconciliation. Other predicates remain -// in ClickHouse where clauses. -func filterProjects(projects []string, filters []dfmaterialization.Filter) []string { - var selected map[string]struct{} - for _, filter := range filters { - if filter.Column != "project_id" { - continue - } - op := strings.ToUpper(strings.TrimSpace(filter.Op)) - values := make([]string, 0) - switch op { - case "=", "EQ": - if value, ok := filter.Value.(string); ok { - values = append(values, value) - } - case "IN": - switch typed := filter.Value.(type) { - case []string: - values = append(values, typed...) - case []any: - for _, item := range typed { - if value, ok := item.(string); ok { - values = append(values, value) - } - } - } - default: - continue - } - current := make(map[string]struct{}, len(values)) - for _, value := range values { - current[strings.TrimSpace(value)] = struct{}{} - } - if selected == nil { - selected = current - } else { - for value := range selected { - if _, ok := current[value]; !ok { - delete(selected, value) - } - } - } - } - if selected == nil { - return append([]string(nil), projects...) - } - result := make([]string, 0, len(projects)) - for _, project := range projects { - if _, ok := selected[project]; ok { - result = append(result, project) - } - } - return result -} - -func (s *Service) logReadFailure(ctx context.Context, phase, resourceType string, err error, attrs ...any) { - fields := []any{"request_id", httpapi.RequestIDFromContext(ctx), "phase", phase, "resource_type", resourceType, "error_chain", errorChain(err)} - fields = append(fields, attrs...) - s.logger.Error("dataframe read failed", fields...) -} - -func errorChain(err error) string { - if err == nil { - return "" - } - parts := make([]string, 0, 3) - for current := err; current != nil; current = errors.Unwrap(current) { - parts = append(parts, current.Error()) - } - return strings.Join(parts, " <- ") -} - -func materializationIDs(values []dfmaterialization.Materialization) []string { - result := make([]string, 0, len(values)) - for _, value := range values { - result = append(result, value.ID) - } - return result -} - -func physicalTables(values []dfmaterialization.Materialization) []string { - result := make([]string, 0, len(values)) - for _, value := range values { - result = append(result, value.PhysicalTable) - } - return result -} - -func mapReaderError(err error) error { - if err == nil { - return nil - } - if _, ok := dataframeerrors.AsUserError(err); ok { - return err - } - if errors.Is(err, authscope.ErrUnauthenticated) { - return dataframeerrors.NewError(dataframeerrors.CodeUnauthenticated, "") - } - if errors.Is(err, authscope.ErrForbidden) { - return dataframeerrors.NewError(dataframeerrors.CodeForbidden, "") - } - if errors.Is(err, authscope.ErrAuthorizationBackendUnavailable) { - return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - } - return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) -} - -func readerUnavailable() error { - return dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) -} - -func federatedMaterialization(dataset dfmaterialization.FederatedDataset) dfmaterialization.Materialization { - return dfmaterialization.Materialization{ID: "federated:" + dataset.Selector.Key(), Name: dataset.Name, Revision: dataset.Revision, DatasetGeneration: "federated:" + dataset.Revision, State: dfmaterialization.StateReady, Columns: dataset.Columns, RowCount: dataset.RowCount, RowCountKnown: dataset.RowCountComplete, Selector: dataset.Selector, Availability: dataset.Availability, IncludedProjects: len(dataset.Sources), ExpectedProjects: dataset.ExpectedProjects, ProjectStatuses: append([]dfmaterialization.ProjectStatus(nil), dataset.ProjectStatuses...)} -} - -func convertFilters(filters []*model.DataframeFilterInput) []dfmaterialization.Filter { - converted := make([]dfmaterialization.Filter, 0, len(filters)) - for _, filter := range filters { - if filter != nil { - var value any - if len(filter.Value) > 0 { - if err := json.Unmarshal(filter.Value, &value); err != nil { - // DataframeFilterInput.value is a GraphQL JSON scalar, so - // gqlgen should only provide valid JSON here. Preserve the - // original value if a custom caller bypasses GraphQL with - // malformed input; the reader will surface the resulting - // parameter error instead of silently dropping the filter. - value = filter.Value - } - } - converted = append(converted, dfmaterialization.Filter{Column: filter.Column, Op: filter.Op, Value: value}) - } - } - return converted -} - -func convertSort(input model.DataframeRowsInput) *dfmaterialization.Sort { - if input.Sort == nil { - return nil - } - return &dfmaterialization.Sort{Column: input.Sort.Column, Desc: input.Sort.Desc != nil && *input.Sort.Desc} -} - -func intValue(value *int) int { - if value == nil { - return 0 - } - return *value -} - -func stringValue(value *string) string { - if value == nil { - return "" - } - return *value -} diff --git a/internal/api/graphql/graph/materialization/service_test.go b/internal/api/graphql/graph/materialization/service_test.go deleted file mode 100644 index a2cae642..00000000 --- a/internal/api/graphql/graph/materialization/service_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package materializationapi - -import ( - "context" - "testing" - "time" - - "github.com/calypr/loom/generated/graphql/graph/model" - "github.com/calypr/loom/internal/authscope" - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - bundlepublication "github.com/calypr/loom/internal/dataframe/publication" - dfpublished "github.com/calypr/loom/internal/dataframe/published" -) - -type emptyPublicationCatalog struct { - bundlepublication.BundleCatalog -} - -func (emptyPublicationCatalog) ListExecutions(context.Context, bundlepublication.BundleState, time.Time) ([]bundlepublication.BundleExecution, error) { - return []bundlepublication.BundleExecution{}, nil -} - -func TestNoAuthCandidateProjectsIncludeObservedUnpublishedProjects(t *testing.T) { - service := NewService(Config{CandidateProjects: func(context.Context) ([]string, error) { - return []string{"staged", "failed"}, nil - }}) - projects, err := service.projects(context.Background(), &authscope.Principal{Subject: "anonymous"}) - if err != nil { - t.Fatal(err) - } - if len(projects) != 2 || projects[0] != "staged" || projects[1] != "failed" { - t.Fatalf("candidate projects = %#v", projects) - } -} - -func TestProjectsReportsEmptyPublicationCatalogAsDatasetNotFound(t *testing.T) { - service := &Service{reader: &dfpublished.Reader{Catalog: emptyPublicationCatalog{}}} - - _, err := service.projects(context.Background(), &authscope.Principal{}) - userErr, ok := dataframeerrors.AsUserError(err) - if !ok || userErr.Code() != string(dataframeerrors.CodeDatasetNotFound) { - t.Fatalf("projects() error = %v, want DATASET_NOT_FOUND", err) - } -} - -func TestResolveSelectorRequiresCompleteExplicitSelector(t *testing.T) { - selector, err := resolveSelector(&model.DataframeSelectorInput{Recipe: "documents", TranslationVersion: "v2", Output: "Patient"}) - if err != nil { - t.Fatal(err) - } - if selector.Recipe != "documents" || selector.TranslationVersion != "v2" || selector.Output != "Patient" { - t.Fatalf("selector = %#v", selector) - } -} - -func TestProjectFilterNarrowsBeforeFederation(t *testing.T) { - projects := []string{"a", "b", "c"} - filtered := filterProjects(projects, []dfpublished.Filter{{Column: "project_id", Op: "IN", Value: []any{"b", "c"}}}) - if len(filtered) != 2 || filtered[0] != "b" || filtered[1] != "c" { - t.Fatalf("filtered = %#v", filtered) - } -} - -func TestProjectStatusesNeverExposeUnauthorizedProjects(t *testing.T) { - statuses := filterAuthorizedStatuses([]string{"allowed"}, []dfpublished.ProjectStatus{{ProjectID: "allowed"}, {ProjectID: "secret"}}) - if len(statuses) != 1 || statuses[0].ProjectID != "allowed" { - t.Fatalf("statuses = %#v", statuses) - } -} diff --git a/internal/api/graphql/graph/query/active_generation_test.go b/internal/api/graphql/graph/query/active_generation_test.go index 83b686fc..52fb1016 100644 --- a/internal/api/graphql/graph/query/active_generation_test.go +++ b/internal/api/graphql/graph/query/active_generation_test.go @@ -7,7 +7,7 @@ import ( "github.com/calypr/loom/generated/graphql/graph/model" "github.com/calypr/loom/internal/catalog" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" publication "github.com/calypr/loom/internal/dataset" ) @@ -81,7 +81,7 @@ func TestActiveGenerationPropagatesThroughBuilderCatalogEntrypoints(t *testing.T referenceCalls = append(referenceCalls, options) return []catalog.PopulatedReference{{FromType: "Patient", Label: "subject_Patient", ToType: "Specimen", EdgeCount: 1}}, nil } - dataframes := runtime.NewService(runtime.ServiceConfig{ + dataframes := dataframeexecution.NewService(dataframeexecution.ServiceConfig{ QueryRows: func(_ context.Context, _ string, _ int, bindVars map[string]any, _ func(map[string]any) error) error { if got := bindVars["dataset_generation"]; got != generation { t.Fatalf("dataframe execution generation = %#v, want %q", got, generation) diff --git a/internal/api/graphql/graph/query/auth_scope_test.go b/internal/api/graphql/graph/query/auth_scope_test.go index 70895d07..f6726209 100644 --- a/internal/api/graphql/graph/query/auth_scope_test.go +++ b/internal/api/graphql/graph/query/auth_scope_test.go @@ -7,7 +7,7 @@ import ( "github.com/calypr/loom/generated/graphql/graph/model" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/catalog" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" ) type dataframeBuilderRestrictedEmptyResourceAccess struct{} @@ -35,10 +35,10 @@ func TestRunPreservesRestrictedEmptyScopeIntoDataframeService(t *testing.T) { resolver := dataframeBuilderRestrictedEmptyScopeResolver() preparedCatalogCalls := 0 - dataframes := runtime.NewService(runtime.ServiceConfig{ + dataframes := dataframeexecution.NewService(dataframeexecution.ServiceConfig{ // This intentionally has no ScopeResolver. The one-shot GraphQL recipe // is already catalog- and scope-resolved before reaching execution, so - // the runtime must not perform a second recipe preparation pass. + // execution must not perform a second recipe preparation pass. QueryRows: func(_ context.Context, _ string, _ int, bindVars map[string]any, _ func(map[string]any) error) error { if got, ok := bindVars["auth_resource_paths_unrestricted"].(bool); !ok || got { t.Fatalf("dataframe AQL unrestricted bind = %#v, want false", bindVars["auth_resource_paths_unrestricted"]) diff --git a/internal/api/graphql/graph/query/doc.go b/internal/api/graphql/graph/query/doc.go index 6eb9bc3f..5d8d064e 100644 --- a/internal/api/graphql/graph/query/doc.go +++ b/internal/api/graphql/graph/query/doc.go @@ -1,4 +1,4 @@ // Package queryapi adapts GraphQL dataframe requests to Loom's dataframe -// runtime. It owns input mapping, catalog discovery, validation, templates, -// and direct Arango-backed dataframe execution. +// execution layer. It owns input mapping, catalog discovery, validation, +// templates, and direct Arango-backed dataframe execution. package queryapi diff --git a/internal/api/graphql/graph/query/fhir_service.go b/internal/api/graphql/graph/query/fhir_service.go index 1c082b74..d0d465e1 100644 --- a/internal/api/graphql/graph/query/fhir_service.go +++ b/internal/api/graphql/graph/query/fhir_service.go @@ -2,7 +2,7 @@ package queryapi // This file contains the transport-neutral FHIR document query facade. It // deliberately delegates planning, authorization, generation resolution, and -// execution to the same dataframe recipe/compiler/runtime path as the rest of +// execution to the same dataframe recipe/compiler/execution path as the rest of // GraphQL; no AQL or Arango client is constructed here. import ( diff --git a/internal/api/graphql/graph/query/fhir_service_test.go b/internal/api/graphql/graph/query/fhir_service_test.go index 13460730..d029e307 100644 --- a/internal/api/graphql/graph/query/fhir_service_test.go +++ b/internal/api/graphql/graph/query/fhir_service_test.go @@ -7,7 +7,7 @@ import ( "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/catalog" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" ) type fhirLimitResourceAccess struct { @@ -51,7 +51,7 @@ func TestFHIRLimitRequiresProjectWriteAccessAboveReadCap(t *testing.T) { DiscoverFields: func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { return nil, nil }, - Dataframes: runtime.NewService(runtime.ServiceConfig{ + Dataframes: dataframeexecution.NewService(dataframeexecution.ServiceConfig{ QueryRows: func(context.Context, string, int, map[string]any, func(map[string]any) error) error { return nil }, diff --git a/internal/api/graphql/graph/query/service.go b/internal/api/graphql/graph/query/service.go index 2975fdd7..4019568f 100644 --- a/internal/api/graphql/graph/query/service.go +++ b/internal/api/graphql/graph/query/service.go @@ -10,8 +10,8 @@ import ( "github.com/calypr/loom/internal/dataframe/compiler" "github.com/calypr/loom/internal/dataframe/compiler/ir" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/runtime" "github.com/calypr/loom/internal/dataframe/semantic" publication "github.com/calypr/loom/internal/dataset" ) @@ -19,22 +19,22 @@ import ( type Service struct { discoverReferencesFn func(context.Context, catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) discoverFieldsFn func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) - dataframes *runtime.Service + dataframes *dataframeexecution.Service scopeResolver *authscope.ScopeResolver activeManifestResolver publication.ActiveResolver - explain func(context.Context, runtime.CompiledQuery) error + explain func(context.Context, dataframeexecution.CompiledQuery) error } type Config struct { DiscoverReferences func(context.Context, catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) DiscoverFields func(context.Context, catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) - Dataframes *runtime.Service + Dataframes *dataframeexecution.Service ScopeResolver *authscope.ScopeResolver // ActiveManifestResolver is optional. When present, builder catalog // discovery and recipe preparation resolve one READY active generation // before inspecting any fields or relationship routes. ActiveManifestResolver publication.ActiveResolver - Explain func(context.Context, runtime.CompiledQuery) error + Explain func(context.Context, dataframeexecution.CompiledQuery) error } func NewService(cfg Config) *Service { @@ -52,7 +52,7 @@ func NewService(cfg Config) *Service { if cfg.Dataframes != nil { service.dataframes = cfg.Dataframes } else { - service.dataframes = runtime.NewService(runtime.ServiceConfig{}) + service.dataframes = dataframeexecution.NewService(dataframeexecution.ServiceConfig{}) } return service } @@ -87,18 +87,18 @@ func (s *Service) resolveRecipe(ctx context.Context, bundle recipe.Bundle, bindi return semantic.ResolveRecipePlan(plan, "", bindings.DatasetGeneration) } -func compileSingleQuery(resolved semantic.ResolvedRecipePlan, limit int) (runtime.CompiledQuery, error) { +func compileSingleQuery(resolved semantic.ResolvedRecipePlan, limit int) (dataframeexecution.CompiledQuery, error) { queries, err := compiler.CompileResolvedRecipePlanWithPolicy(resolved, limit, ir.DefaultPhysicalOptimizationPolicy()) if err != nil { - return runtime.CompiledQuery{}, err + return dataframeexecution.CompiledQuery{}, err } if len(queries) != 1 { - return runtime.CompiledQuery{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + return dataframeexecution.CompiledQuery{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") } return queries[0], nil } -func (s *Service) Run(ctx context.Context, input model.FhirDataframeInput, limit *int) (*runtime.Result, error) { +func (s *Service) Run(ctx context.Context, input model.FhirDataframeInput, limit *int) (*dataframeexecution.Result, error) { started := time.Now() normalizedInput, scope, generation, err := s.prepareRunInput(ctx, input) if err != nil { diff --git a/internal/api/graphql/graph/resolver/dataframe_diagnostics.go b/internal/api/graphql/graph/resolver/dataframe_diagnostics.go index 4579702a..2c90e78c 100644 --- a/internal/api/graphql/graph/resolver/dataframe_diagnostics.go +++ b/internal/api/graphql/graph/resolver/dataframe_diagnostics.go @@ -5,10 +5,10 @@ import ( "github.com/calypr/loom/generated/graphql/graph/model" "github.com/calypr/loom/internal/dataframe/compiler/ir" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" ) -func dataframeDiagnostics(in runtime.QueryDiagnostics) *model.DataframeQueryDiagnostics { +func dataframeDiagnostics(in dataframeexecution.QueryDiagnostics) *model.DataframeQueryDiagnostics { return &model.DataframeQueryDiagnostics{ InputResolutionMs: milliseconds(in.InputResolution), RequestPreparationMs: milliseconds(in.RequestPreparation), diff --git a/internal/api/graphql/graph/resolver/graph_resolvers.go b/internal/api/graphql/graph/resolver/graph_resolvers.go index 20499c09..888f90d2 100644 --- a/internal/api/graphql/graph/resolver/graph_resolvers.go +++ b/internal/api/graphql/graph/resolver/graph_resolvers.go @@ -7,7 +7,7 @@ import ( "github.com/calypr/loom/generated/graphql/graph/model" queryapi "github.com/calypr/loom/internal/api/graphql/graph/query" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" ) func (r *queryResolver) resolveFHIRGraph(ctx context.Context, input model.FhirGraphQueryInput) (*model.FhirGraphQueryResult, error) { @@ -31,7 +31,7 @@ func (r *queryResolver) resolveFHIRGraph(ctx context.Context, input model.FhirGr paths = append(paths, mapped) } diagnostics := (*model.DataframeQueryDiagnostics)(nil) - if value, ok := result.Diagnostics.(runtime.QueryDiagnostics); ok { + if value, ok := result.Diagnostics.(dataframeexecution.QueryDiagnostics); ok { diagnostics = dataframeDiagnostics(value) } return &model.FhirGraphQueryResult{SourceGeneration: result.SourceGeneration, Paths: paths, ReturnedCount: result.ReturnedCount, PageInfo: &model.FhirGraphPageInfo{HasMore: result.PageInfo.HasMore}, Diagnostics: diagnostics}, nil diff --git a/internal/api/graphql/graph/resolver/operation_context.go b/internal/api/graphql/graph/resolver/operation_context.go index 8b961a73..250cebab 100644 --- a/internal/api/graphql/graph/resolver/operation_context.go +++ b/internal/api/graphql/graph/resolver/operation_context.go @@ -11,8 +11,8 @@ import ( // scheduling state at gqlgen's operation boundary. func (r *Resolver) WithOperationContext(ctx context.Context) context.Context { ctx = withFHIRReferenceLoader(ctx, r) - if r.materializations != nil { - ctx = r.materializations.WithOperationContext(ctx, aggregateRootFieldCount(ctx)) + if r.dataframes != nil { + ctx = r.dataframes.WithOperationContext(ctx, aggregateRootFieldCount(ctx)) } return ctx } diff --git a/internal/api/graphql/graph/resolver/recipe_control_graphql.go b/internal/api/graphql/graph/resolver/recipe_control_graphql.go index e5f1302a..0bab172e 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_graphql.go +++ b/internal/api/graphql/graph/resolver/recipe_control_graphql.go @@ -11,11 +11,11 @@ import ( "strconv" "github.com/calypr/loom/generated/graphql/graph/model" - materializationapi "github.com/calypr/loom/internal/api/graphql/graph/materialization" + dataframeapi "github.com/calypr/loom/internal/api/graphql/graph/dataframe" "github.com/calypr/loom/internal/authscope" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" recipeexec "github.com/calypr/loom/internal/dataframe/recipe/exec" "github.com/calypr/loom/internal/dataframe/semantic" ) @@ -33,7 +33,7 @@ func recipeGraphQLError(err error) error { } if _, ok := dataframeerrors.AsUserError(err); !ok { var validation *recipe.ValidationError - var resolution *engine.ResolutionError + var resolution *dataframeexecution.ResolutionError switch { case errors.As(err, &resolution): return dataframeerrors.Wrap(err, dataframeerrors.CodeRecipeResolutionFailed, resolution.Error()) @@ -68,11 +68,9 @@ func graphqlRecipeBindings(bindings *model.DataframeRecipeBindingsInput) (recipe } func outputValidation(output semantic.OutputPlan) *model.DataframeRecipeOutputValidation { - fields := append([]string(nil), output.DeclaredOrder...) - if len(fields) == 0 { - for _, field := range output.Fields { - fields = append(fields, field.Name) - } + fields := make([]string, 0, len(output.Root.Fields)) + for _, field := range output.Root.Fields { + fields = append(fields, field.Name) } dynamic := make([]string, 0, len(output.DynamicMaps)) for _, value := range output.DynamicMaps { @@ -115,7 +113,7 @@ func planExplanation(plan semantic.RecipePlanExplanation, name string) *model.Da return &model.DataframeRecipeExplanation{Name: name, RecipeDigest: plan.RecipeDigest, TranslationVersion: plan.TranslationVersion, Outputs: outputs} } -func physicalExplanation(value engine.PhysicalExplanation) *model.DataframeRecipePhysicalExplanation { +func physicalExplanation(value dataframeexecution.PhysicalExplanation) *model.DataframeRecipePhysicalExplanation { outputs := make([]*model.DataframeRecipePhysicalOutputExplanation, 0, len(value.Outputs)) for _, output := range value.Outputs { diagnostics := output.Diagnostics @@ -149,7 +147,7 @@ func physicalExplanation(value engine.PhysicalExplanation) *model.DataframeRecip return &model.DataframeRecipePhysicalExplanation{Outputs: outputs} } -func arangoAssessment(value engine.ExplainAssessment) *model.DataframeRecipeArangoAssessment { +func arangoAssessment(value dataframeexecution.ExplainAssessment) *model.DataframeRecipeArangoAssessment { result := &model.DataframeRecipeArangoAssessment{ Plans: make([]*model.DataframeRecipeExplainPlanEstimate, 0, len(value.Plans)), FullCollectionScans: make([]*model.DataframeRecipeExplainCollectionScan, 0, len(value.FullCollectionScans)), @@ -179,7 +177,7 @@ func arangoAssessment(value engine.ExplainAssessment) *model.DataframeRecipeAran func preflightResult(plan semantic.ResolvedRecipePlan, name string) *model.DataframeRecipePreflight { columns := make([]*model.DataframeRecipeColumn, 0) for _, output := range plan.SemanticPlan.Outputs { - for _, field := range output.Fields { + for _, field := range output.Root.Fields { columns = append(columns, &model.DataframeRecipeColumn{ Output: output.Name, Name: field.Name, LogicalType: string(field.Expr.Type.Kind), Repeated: field.Expr.Type.Cardinality == "many", @@ -315,17 +313,17 @@ func logicalPreviewRows(columns []string, rows []map[string]any) []map[string]an type recipePreviewView struct { plan semantic.ResolvedRecipePlan - outputs []engine.OutputRows + outputs []dataframeexecution.OutputRows } func executionModel(value RecipeExecution) *model.DataframeRecipeExecution { outputs := make([]*model.DataframeRecipeExecutionOutput, 0, len(value.Outputs)) for _, output := range value.Outputs { - errorText, errorCode, errorRetryable := materializationapi.PersistedFailure(output.Error, output.ErrorCode, output.ErrorRetryable) + errorText, errorCode, errorRetryable := dataframeapi.PersistedFailure(output.Error, output.ErrorCode, output.ErrorRetryable) item := &model.DataframeRecipeExecutionOutput{Name: output.Name, State: model.DataframeRecipeExecutionState(output.State), RowCount: output.RowCount, Error: errorText, ErrorCode: errorCode, ErrorRetryable: errorRetryable} columns := make([]*model.DataframeColumn, 0, len(output.Columns)) for _, column := range output.Columns { - columns = append(columns, materializationapi.ColumnFromPhysical(column)) + columns = append(columns, dataframeapi.ColumnFromPhysical(column)) } item.Columns = columns if output.Selector.Valid() { @@ -336,7 +334,7 @@ func executionModel(value RecipeExecution) *model.DataframeRecipeExecution { } outputs = append(outputs, item) } - errorText, errorCode, errorRetryable := materializationapi.PersistedFailure(value.Error, value.ErrorCode, value.ErrorRetryable) + errorText, errorCode, errorRetryable := dataframeapi.PersistedFailure(value.Error, value.ErrorCode, value.ErrorRetryable) result := &model.DataframeRecipeExecution{ID: value.ID, Name: value.Name, RecipeDigest: value.RecipeDigest, ResolvedSchemaDigest: value.ResolvedSchemaDigest, SourceGeneration: value.SourceGeneration, State: model.DataframeRecipeExecutionState(value.State), Outputs: outputs, Error: errorText, ErrorCode: errorCode, ErrorRetryable: errorRetryable} if value.TranslationVersion != "" { result.TranslationVersion = &value.TranslationVersion diff --git a/internal/api/graphql/graph/resolver/recipe_control_test.go b/internal/api/graphql/graph/resolver/recipe_control_test.go index 827c426e..7f2de407 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_test.go +++ b/internal/api/graphql/graph/resolver/recipe_control_test.go @@ -5,30 +5,29 @@ import ( "testing" "github.com/calypr/loom/generated/graphql/graph/model" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" materialization "github.com/calypr/loom/internal/dataframe/publication" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/dataframe/semantic" "github.com/calypr/loom/internal/dataframe/spec" - dataset "github.com/calypr/loom/internal/dataset" ) type fakeRecipeControl struct { - validation engine.Validation + validation dataframeexecution.Validation plan semantic.ResolvedRecipePlan resolveCalls *int } -func (f fakeRecipeControl) ExplainPhysical(context.Context, string, recipe.RuntimeBindings, bool) (engine.PhysicalExplanation, error) { - return engine.PhysicalExplanation{}, nil +func (f fakeRecipeControl) ExplainPhysical(context.Context, string, recipe.RuntimeBindings, bool) (dataframeexecution.PhysicalExplanation, error) { + return dataframeexecution.PhysicalExplanation{}, nil } -func (f fakeRecipeControl) Run(context.Context, string, recipe.RuntimeBindings) (engine.Preview, error) { +func (f fakeRecipeControl) Run(context.Context, string, recipe.RuntimeBindings) (dataframeexecution.Preview, error) { rows := []map[string]any{{"id": "p1"}, {"id": "p2"}} - return engine.Preview{Plan: f.plan, Outputs: []engine.OutputRows{{Name: "rows", Columns: []string{"id"}, Rows: rows}}}, nil + return dataframeexecution.Preview{Plan: f.plan, Outputs: []dataframeexecution.OutputRows{{Name: "rows", Columns: []string{"id"}, Rows: rows}}}, nil } -func (f fakeRecipeControl) Validate(context.Context, string, recipe.RuntimeBindings) (engine.Validation, error) { +func (f fakeRecipeControl) Validate(context.Context, string, recipe.RuntimeBindings) (dataframeexecution.Validation, error) { return f.validation, nil } func (f fakeRecipeControl) Explain(context.Context, string, recipe.RuntimeBindings) (semantic.RecipePlanExplanation, error) { @@ -40,17 +39,17 @@ func (f fakeRecipeControl) Resolve(context.Context, string, recipe.RuntimeBindin } return f.plan, nil } -func (f fakeRecipeControl) Preview(context.Context, string, recipe.RuntimeBindings) (engine.Preview, error) { +func (f fakeRecipeControl) Preview(context.Context, string, recipe.RuntimeBindings) (dataframeexecution.Preview, error) { rows := []map[string]any{{"id": "p1"}} - return engine.Preview{Plan: f.plan, Outputs: []engine.OutputRows{{Name: "rows", Columns: []string{"id"}, Rows: rows}}}, nil + return dataframeexecution.Preview{Plan: f.plan, Outputs: []dataframeexecution.OutputRows{{Name: "rows", Columns: []string{"id"}, Rows: rows}}}, nil } -func testRecipeValidation() engine.Validation { +func testRecipeValidation() dataframeexecution.Validation { plan := semantic.RecipePlan{Version: 1, RecipeDigest: "recipe", TranslationVersion: "legacy", Outputs: []semantic.OutputPlan{{ Name: "rows", RootResourceType: "Patient", RowGrain: spec.RowGrainResource, - DeclaredOrder: []string{"id"}, Fields: []semantic.SemanticProjection{{Name: "id", Expr: semantic.SemanticExpression{SourcePath: "$.outputs[0].fields[0]", Type: semanticTypeString().Type}}}, + Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Fields: []semantic.SemanticField{{Name: "id", Expr: semantic.SemanticExpression{SourcePath: "$.outputs[0].fields[0]", Type: semanticTypeString().Type}}}}, }}} - return engine.Validation{Plan: plan} + return dataframeexecution.Validation{Plan: plan} } func semanticTypeString() (value semantic.SemanticExpression) { @@ -178,45 +177,3 @@ func TestRecipeExecutionReaderMapsCanonicalAsyncStates(t *testing.T) { } } } - -func TestMaterializeRecipeDoesNotPreResolveInGraphQL(t *testing.T) { - validation := testRecipeValidation() - plan := semantic.ResolvedRecipePlan{SemanticPlan: validation.Plan, ResolvedSchemaDigest: "schema", SourceGeneration: "g", ScopeDigest: "scope"} - resolveCalls := 0 - materializeCalls := 0 - resolver := NewResolver(ResolverConfig{ - RecipeControl: fakeRecipeControl{validation: validation, plan: plan, resolveCalls: &resolveCalls}, - RecipeMaterialize: func(context.Context, string, recipe.RuntimeBindings) (RecipeExecution, error) { - materializeCalls++ - return RecipeExecution{ID: "execution-1", Name: "default", State: "READY"}, nil - }, - }) - if _, err := resolver.Mutation().MaterializeDataframeRecipeBundle(context.Background(), model.MaterializeDataframeRecipeInput{ - Name: "default", Bindings: &model.DataframeRecipeBindingsInput{Project: "p", DatasetGeneration: stringPtr("g")}, - }); err != nil { - t.Fatal(err) - } - if resolveCalls != 0 || materializeCalls != 1 { - t.Fatalf("GraphQL pre-resolve counts = resolve:%d materialize:%d", resolveCalls, materializeCalls) - } -} - -func TestStartDataframeMaterializationPassesExactSelector(t *testing.T) { - var got dataset.DataframeSelector - resolver := NewResolver(ResolverConfig{ExactMaterializationStarter: func(_ context.Context, selector dataset.DataframeSelector, bindings recipe.RuntimeBindings) (RecipeExecution, error) { - got = selector - if bindings.Project != "p" || bindings.DatasetGeneration != "git-sha" { - t.Fatalf("target = %s/%s", bindings.Project, bindings.DatasetGeneration) - } - return RecipeExecution{ID: "execution-1", Name: selector.Recipe, TranslationVersion: selector.TranslationVersion, State: "QUEUED", Outputs: []RecipeExecutionOutput{{Name: selector.Output, Selector: selector, State: "QUEUED"}}}, nil - }}) - result, err := resolver.Mutation().StartDataframeMaterialization(context.Background(), model.StartDataframeMaterializationInput{Project: "p", Generation: "git-sha", Selector: &model.DataframeSelectorInput{Recipe: "documents", TranslationVersion: "v2", Output: "DocumentReference"}}) - if err != nil { - t.Fatal(err) - } - if !got.Valid() || result.TranslationVersion == nil || *result.TranslationVersion != "v2" || result.Outputs[0].Selector == nil { - t.Fatalf("result = %#v, selector = %#v", result, got) - } -} - -func stringPtr(value string) *string { return &value } diff --git a/internal/api/graphql/graph/resolver/resolver.go b/internal/api/graphql/graph/resolver/resolver.go index 3c04fdce..08eee183 100644 --- a/internal/api/graphql/graph/resolver/resolver.go +++ b/internal/api/graphql/graph/resolver/resolver.go @@ -5,12 +5,12 @@ import ( "log/slog" "github.com/calypr/loom/generated/graphql/graph/model" - materializationapi "github.com/calypr/loom/internal/api/graphql/graph/materialization" + dataframeapi "github.com/calypr/loom/internal/api/graphql/graph/dataframe" queryapi "github.com/calypr/loom/internal/api/graphql/graph/query" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" publication "github.com/calypr/loom/internal/dataframe/publication" materialization "github.com/calypr/loom/internal/dataframe/published" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/dataframe/semantic" dataset "github.com/calypr/loom/internal/dataset" ) @@ -19,12 +19,12 @@ import ( // recipe GraphQL fields. It intentionally returns logical plans only; no // AQL, SQL, table names, or credentials cross this boundary. type RecipeControl interface { - Validate(context.Context, string, recipe.RuntimeBindings) (engine.Validation, error) + Validate(context.Context, string, recipe.RuntimeBindings) (dataframeexecution.Validation, error) Explain(context.Context, string, recipe.RuntimeBindings) (semantic.RecipePlanExplanation, error) - ExplainPhysical(context.Context, string, recipe.RuntimeBindings, bool) (engine.PhysicalExplanation, error) + ExplainPhysical(context.Context, string, recipe.RuntimeBindings, bool) (dataframeexecution.PhysicalExplanation, error) Resolve(context.Context, string, recipe.RuntimeBindings) (semantic.ResolvedRecipePlan, error) - Preview(context.Context, string, recipe.RuntimeBindings) (engine.Preview, error) - Run(context.Context, string, recipe.RuntimeBindings) (engine.Preview, error) + Preview(context.Context, string, recipe.RuntimeBindings) (dataframeexecution.Preview, error) + Run(context.Context, string, recipe.RuntimeBindings) (dataframeexecution.Preview, error) } // RecipeExecution is the stable logical execution record exposed by GraphQL. @@ -58,20 +58,7 @@ type RecipeExecutionOutput struct { Phase string } -// RecipeMaterializeFunc owns the complete materialization operation. The -// callback resolves the recipe exactly once and publishes the resulting -// physical plan; GraphQL does not pre-resolve it and then ask the callback to -// resolve it again. -type RecipeMaterializeFunc func(context.Context, string, recipe.RuntimeBindings) (RecipeExecution, error) type RecipeExecutionReader func(context.Context, string) (*RecipeExecution, error) -type ExactMaterializationStarter func(context.Context, dataset.DataframeSelector, recipe.RuntimeBindings) (RecipeExecution, error) -type ProjectRelease struct{ ID, Project, Generation, Revision, State string } -type ProjectReleaseActivator func(context.Context, string, string, string) (ProjectRelease, error) - -// ExplorerBundleMaterializer is retained as the shared publication capability -// used by the REST V2 lifecycle. GraphQL no longer owns Explorer lifecycle -// mutations. -type ExplorerBundleMaterializer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (RecipeExecution, error) // RecipeAuthorizer resolves request bindings against the caller's project // grants. GraphQL operation type is intentionally irrelevant: preview/run are @@ -82,47 +69,36 @@ type RecipeAuthorizer interface { } type Resolver struct { - query *queryapi.Service - materializations *materializationapi.Service - recipeControl RecipeControl - recipeMaterialize RecipeMaterializeFunc - recipeExecutions RecipeExecutionReader - recipeAuthorizer RecipeAuthorizer - exactMaterializationStarter ExactMaterializationStarter - projectReleaseActivator ProjectReleaseActivator - recipeRevisions recipe.RevisionStore + query *queryapi.Service + dataframes *dataframeapi.Service + recipeControl RecipeControl + recipeExecutions RecipeExecutionReader + recipeAuthorizer RecipeAuthorizer + recipeRevisions recipe.RevisionStore } type ResolverConfig struct { - DataframeQuery queryapi.Config - MaterializationReader *materialization.Reader - Logger *slog.Logger - RecipeControl RecipeControl - RecipeMaterialize RecipeMaterializeFunc - RecipeExecutions RecipeExecutionReader - RecipeAuthorizer RecipeAuthorizer - ExactMaterializationStarter ExactMaterializationStarter - ProjectReleaseActivator ProjectReleaseActivator - CandidateProjects func(context.Context) ([]string, error) - RecipeRevisions recipe.RevisionStore + DataframeQuery queryapi.Config + MaterializationReader *materialization.Reader + Logger *slog.Logger + RecipeControl RecipeControl + RecipeExecutions RecipeExecutionReader + RecipeAuthorizer RecipeAuthorizer + RecipeRevisions recipe.RevisionStore } func NewResolver(cfg ResolverConfig) *Resolver { return &Resolver{ query: queryapi.NewService(cfg.DataframeQuery), - materializations: materializationapi.NewService(materializationapi.Config{ - Reader: cfg.MaterializationReader, - ScopeResolver: cfg.DataframeQuery.ScopeResolver, - Logger: cfg.Logger, - CandidateProjects: cfg.CandidateProjects, + dataframes: dataframeapi.NewService(dataframeapi.Config{ + Reader: cfg.MaterializationReader, + ScopeResolver: cfg.DataframeQuery.ScopeResolver, + Logger: cfg.Logger, }), - recipeControl: cfg.RecipeControl, - recipeMaterialize: cfg.RecipeMaterialize, - recipeExecutions: cfg.RecipeExecutions, - recipeAuthorizer: cfg.RecipeAuthorizer, - exactMaterializationStarter: cfg.ExactMaterializationStarter, - projectReleaseActivator: cfg.ProjectReleaseActivator, - recipeRevisions: cfg.RecipeRevisions, + recipeControl: cfg.RecipeControl, + recipeExecutions: cfg.RecipeExecutions, + recipeAuthorizer: cfg.RecipeAuthorizer, + recipeRevisions: cfg.RecipeRevisions, } } diff --git a/internal/api/graphql/graph/resolver/schema.resolvers.go b/internal/api/graphql/graph/resolver/schema.resolvers.go index 9b65af65..ecfdc39c 100644 --- a/internal/api/graphql/graph/resolver/schema.resolvers.go +++ b/internal/api/graphql/graph/resolver/schema.resolvers.go @@ -9,14 +9,12 @@ import ( "context" "encoding/json" "fmt" - "strings" "github.com/calypr/loom/generated/graphql/graph/executor" "github.com/calypr/loom/generated/graphql/graph/model" - materializationapi "github.com/calypr/loom/internal/api/graphql/graph/materialization" + dataframeapi "github.com/calypr/loom/internal/api/graphql/graph/dataframe" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataset" ) // RunFhirDataframe is the resolver for the runFhirDataframe field. @@ -112,73 +110,6 @@ func (r *mutationResolver) PreviewDataframeRecipe(ctx context.Context, input mod return previewResult(recipePreviewView{plan: preview.Plan, outputs: preview.Outputs}, input.Name) } -// MaterializeDataframeRecipeBundle is the resolver for the materializeDataframeRecipeBundle field. -func (r *mutationResolver) MaterializeDataframeRecipeBundle(ctx context.Context, input model.MaterializeDataframeRecipeInput) (*model.DataframeRecipeExecution, error) { - bindings, err := graphqlRecipeBindings(input.Bindings) - if err != nil { - return nil, recipeGraphQLError(err) - } - bindings, err = r.authorizeRecipeWrite(ctx, bindings) - if err != nil { - return nil, recipeGraphQLError(err) - } - if err := requireRecipeControl(r.recipeControl); err != nil { - return nil, recipeGraphQLError(err) - } - if r.recipeMaterialize == nil { - return nil, recipeGraphQLError(dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true))) - } - execution, err := r.recipeMaterialize(ctx, input.Name, bindings) - if err != nil { - return nil, recipeGraphQLError(err) - } - return executionModel(execution), nil -} - -// StartDataframeMaterialization is the resolver for the startDataframeMaterialization field. -func (r *mutationResolver) StartDataframeMaterialization(ctx context.Context, input model.StartDataframeMaterializationInput) (*model.DataframeRecipeExecution, error) { - selector := dataset.DataframeSelector{Recipe: input.Selector.Recipe, TranslationVersion: input.Selector.TranslationVersion, Output: input.Selector.Output} - if !selector.Valid() || input.Project == "" || input.Generation == "" { - return nil, recipeGraphQLError(dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "")) - } - bindings, err := r.authorizeRecipeWrite(ctx, recipe.RuntimeBindings{Project: input.Project, DatasetGeneration: input.Generation}) - if err != nil { - return nil, recipeGraphQLError(err) - } - if r.exactMaterializationStarter == nil { - return nil, recipeGraphQLError(dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true))) - } - execution, err := r.exactMaterializationStarter(ctx, selector, bindings) - if err != nil { - return nil, recipeGraphQLError(err) - } - return executionModel(execution), nil -} - -// ActivateProjectRelease is the resolver for the activateProjectRelease field. -func (r *mutationResolver) ActivateProjectRelease(ctx context.Context, input model.ActivateProjectReleaseInput) (*model.ProjectRelease, error) { - project, releaseID := strings.TrimSpace(input.Project), strings.TrimSpace(input.ReleaseID) - if project == "" || releaseID == "" { - return nil, recipeGraphQLError(dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "")) - } - bindings, err := r.authorizeRecipeWrite(ctx, recipe.RuntimeBindings{Project: project}) - if err != nil { - return nil, recipeGraphQLError(err) - } - if r.projectReleaseActivator == nil { - return nil, recipeGraphQLError(dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true))) - } - expected := "" - if input.ExpectedActiveRevision != nil { - expected = strings.TrimSpace(*input.ExpectedActiveRevision) - } - release, err := r.projectReleaseActivator(ctx, bindings.Project, releaseID, expected) - if err != nil { - return nil, recipeGraphQLError(err) - } - return &model.ProjectRelease{ID: release.ID, Project: release.Project, Generation: release.Generation, Revision: release.Revision, State: model.ProjectReleaseState(release.State)}, nil -} - // RegisterDataframeRecipeRevision is the resolver for the registerDataframeRecipeRevision field. func (r *mutationResolver) RegisterDataframeRecipeRevision(ctx context.Context, input model.RegisterDataframeRecipeRevisionInput) (*model.DataframeRecipeRevision, error) { if r.recipeRevisions == nil { @@ -202,44 +133,31 @@ func (r *mutationResolver) RegisterDataframeRecipeRevision(ctx context.Context, return recipeRevisionModel(value), nil } -// DataframeDatasets is the resolver for the dataframeDatasets field. -func (r *queryResolver) DataframeDatasets(ctx context.Context) ([]*model.DataframeMaterialization, error) { - values, err := r.materializations.Datasets(ctx) - if err != nil { - return nil, err - } - result := make([]*model.DataframeMaterialization, 0, len(values)) - for _, value := range values { - result = append(result, materializationapi.Model(value)) - } - return result, nil -} - // ProjectDataframeDatasets is the resolver for the projectDataframeDatasets field. func (r *queryResolver) ProjectDataframeDatasets(ctx context.Context, projectID string) ([]*model.DataframeMaterialization, error) { - values, err := r.materializations.ProjectDatasets(ctx, projectID) + values, err := r.dataframes.ProjectDatasets(ctx, projectID) if err != nil { return nil, err } result := make([]*model.DataframeMaterialization, 0, len(values)) for _, value := range values { - result = append(result, materializationapi.Model(value)) + result = append(result, dataframeapi.Model(value)) } return result, nil } // DataframeDataset is the resolver for the dataframeDataset field. func (r *queryResolver) DataframeDataset(ctx context.Context, input model.DataframeDatasetInput) (*model.DataframeMaterialization, error) { - value, err := r.materializations.Dataset(ctx, input) + value, err := r.dataframes.Dataset(ctx, input) if err != nil { return nil, err } - return materializationapi.Model(*value), nil + return dataframeapi.Model(*value), nil } // DataframeRows is the resolver for the dataframeRows field. func (r *queryResolver) DataframeRows(ctx context.Context, input model.DataframeRowsInput) (*model.DataframeRowConnection, error) { - page, err := r.materializations.Rows(ctx, input) + page, err := r.dataframes.Rows(ctx, input) if err != nil { return nil, err } @@ -253,7 +171,7 @@ func (r *queryResolver) DataframeRows(ctx context.Context, input model.Dataframe } totalCount := int(page.TotalCount) return &model.DataframeRowConnection{ - Materialization: materializationapi.Model(page.Materialization), + Materialization: dataframeapi.Model(page.Materialization), Columns: append([]string(nil), page.Columns...), Rows: rows, TotalCount: &totalCount, PageInfo: &model.DataframePageInfo{HasNextPage: page.HasNext, EndCursor: cursor}, }, nil @@ -261,28 +179,28 @@ func (r *queryResolver) DataframeRows(ctx context.Context, input model.Dataframe // DataframeAggregate is the resolver for the dataframeAggregate field. func (r *queryResolver) DataframeAggregate(ctx context.Context, input model.DataframeAggregateInput) (*model.DataframeAggregateResult, error) { - result, err := r.materializations.AggregateInput(ctx, input) + result, err := r.dataframes.AggregateInput(ctx, input) if err != nil { return nil, err } - rows, err := materializationapi.AggregateRowsResult(result.Rows) + rows, err := dataframeapi.AggregateRowsResult(result.Rows) if err != nil { return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeOutputEncodingFailed, "") } - return &model.DataframeAggregateResult{Materialization: materializationapi.Model(result.Materialization), Columns: result.Columns, Rows: rows}, nil + return &model.DataframeAggregateResult{Materialization: dataframeapi.Model(result.Materialization), Columns: result.Columns, Rows: rows}, nil } // DataframeAggregations is the resolver for the dataframeAggregations field. func (r *queryResolver) DataframeAggregations(ctx context.Context, input model.DataframeAggregationsInput) (*model.DataframeAggregationsResult, error) { - result, err := r.materializations.AggregationsInput(ctx, input) + result, err := r.dataframes.AggregationsInput(ctx, input) if err != nil { return nil, err } - aggregations, err := materializationapi.AggregationsJSON(result) + aggregations, err := dataframeapi.AggregationsJSON(result) if err != nil { return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeOutputEncodingFailed, "") } - return &model.DataframeAggregationsResult{Materialization: materializationapi.FederatedMaterialization(result.Dataset), Aggregations: aggregations}, nil + return &model.DataframeAggregationsResult{Materialization: dataframeapi.Model(result.Materialization), Aggregations: aggregations}, nil } // DataframeRecipeExecution is the resolver for the dataframeRecipeExecution field. diff --git a/internal/api/graphql/graph/schema/schema.graphqls b/internal/api/graphql/graph/schema/schema.graphqls index df72c513..0298a519 100644 --- a/internal/api/graphql/graph/schema/schema.graphqls +++ b/internal/api/graphql/graph/schema/schema.graphqls @@ -1,5 +1,4 @@ type Query { - dataframeDatasets: [DataframeMaterialization!]! projectDataframeDatasets(projectId: String!): [DataframeMaterialization!]! dataframeDataset(input: DataframeDatasetInput!): DataframeMaterialization dataframeRows(input: DataframeRowsInput!): DataframeRowConnection! @@ -24,34 +23,9 @@ type Mutation { runDataframeRecipe(input: RunDataframeRecipeInput!): DataframeRecipeResult! validateDataframeRecipe(input: ValidateDataframeRecipeInput!): DataframeRecipeValidation! previewDataframeRecipe(input: PreviewDataframeRecipeInput!): DataframeRecipePreview! - materializeDataframeRecipeBundle(input: MaterializeDataframeRecipeInput!): DataframeRecipeExecution! - startDataframeMaterialization(input: StartDataframeMaterializationInput!): DataframeRecipeExecution! - activateProjectRelease(input: ActivateProjectReleaseInput!): ProjectRelease! registerDataframeRecipeRevision(input: RegisterDataframeRecipeRevisionInput!): DataframeRecipeRevision! } -input StartDataframeMaterializationInput { - project: String! - generation: String! - selector: DataframeSelectorInput! -} - -input ActivateProjectReleaseInput { - project: String! - releaseId: ID! - expectedActiveRevision: String -} - -type ProjectRelease { - id: ID! - project: String! - generation: String! - revision: String! - state: ProjectReleaseState! -} - -enum ProjectReleaseState { ACTIVE } - input RegisterDataframeRecipeRevisionInput { projectId: String! name: String! @@ -105,11 +79,6 @@ input RunDataframeRecipeInput { outputs: [String!] } -input MaterializeDataframeRecipeInput { - name: String! - bindings: DataframeRecipeBindingsInput! -} - type DataframeRecipeValidation { name: String! recipeDigest: String! @@ -360,11 +329,6 @@ type DataframeMaterialization { errorCode: String errorRetryable: Boolean selector: DataframeSelector - availability: DataframeAvailability - completeness: Float - includedProjectCount: Int - expectedProjectCount: Int - projectStatuses: [DataframeProjectStatus!]! } input DataframeSelectorInput { @@ -379,33 +343,8 @@ type DataframeSelector { output: String! } -enum DataframeAvailability { - AVAILABLE - DEGRADED - UNAVAILABLE -} - -enum DataframeProjectState { - CURRENT - STALE - BUILDING - FAILED - MISSING - EXCLUDED -} - -type DataframeProjectStatus { - project: String! - state: DataframeProjectState! - generation: String - executionId: ID - createdAt: String - updatedAt: String - errorCode: String - retryable: Boolean! -} - input DataframeDatasetInput { + projectId: String! selector: DataframeSelectorInput! } @@ -421,6 +360,7 @@ input DataframeSortInput { } input DataframeRowsInput { + projectId: String! selector: DataframeSelectorInput! columns: [String!] filters: [DataframeFilterInput!] @@ -443,6 +383,7 @@ type DataframeRowConnection { } input DataframeAggregateInput { + projectId: String! selector: DataframeSelectorInput! groupBy: [String!] filters: [DataframeFilterInput!] @@ -467,6 +408,7 @@ input DataframeAggregationSpecInput { } input DataframeAggregationsInput { + projectId: String! selector: DataframeSelectorInput! filters: [DataframeFilterInput!] specs: [DataframeAggregationSpecInput!]! diff --git a/internal/api/http/errors.go b/internal/api/http/errors.go index 12872b5f..ee220a32 100644 --- a/internal/api/http/errors.go +++ b/internal/api/http/errors.go @@ -9,7 +9,6 @@ import ( "github.com/calypr/loom/internal/authscope" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/calypr/loom/internal/ingest" "github.com/gofiber/fiber/v3" ) @@ -53,28 +52,6 @@ func MapDataframeError(err error, requestID string) MappedError { if errors.Is(err, authscope.ErrAuthorizationBackendUnavailable) { err = dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) } - var preflightErr *ingest.PreflightError - if errors.As(err, &preflightErr) { - return MappedError{Status: http.StatusUnprocessableEntity, Body: ErrorResponse{Error: HTTPErrorBody{ - Code: "INGEST_PREFLIGHT_FAILED", Message: messageForCode("INGEST_PREFLIGHT_FAILED"), Details: preflightDetails(preflightErr), RequestID: requestID, - }}, Cause: err} - } - var incompleteErr *ingest.GenerationLoadIncompleteError - if errors.As(err, &incompleteErr) { - return MappedError{Status: http.StatusUnprocessableEntity, Body: ErrorResponse{Error: HTTPErrorBody{ - Code: "GENERATION_LOAD_INCOMPLETE", Message: messageForCode("GENERATION_LOAD_INCOMPLETE"), Details: map[string]any{ - "validationErrors": incompleteErr.ValidationErrors, - "generationErrors": incompleteErr.GenerationErrors, - "edgeErrors": incompleteErr.EdgeErrors, - }, RequestID: requestID, - }}, Cause: err} - } - var activationErr *ingest.ActivationOutcomeError - if errors.As(err, &activationErr) { - return MappedError{Status: http.StatusConflict, Body: ErrorResponse{Error: HTTPErrorBody{ - Code: "GENERATION_ACTIVATION_UNKNOWN", Message: messageForCode("GENERATION_ACTIVATION_UNKNOWN"), Retryable: false, RequestID: requestID, - }}, Cause: err} - } if errors.Is(err, os.ErrInvalid) { return MappedError{Status: http.StatusBadRequest, Body: ErrorResponse{Error: HTTPErrorBody{ Code: "INVALID_REQUEST", Message: messageForCode("INVALID_REQUEST"), RequestID: requestID, @@ -129,32 +106,11 @@ func StatusForErrorCode(code string) int { return statusForCode(normalizeHTTPCode(code), 0) } -func preflightDetails(err *ingest.PreflightError) map[string]any { - if err == nil || len(err.Report.Issues) == 0 { - return nil - } - issues := make([]map[string]any, 0, len(err.Report.Issues)) - for _, issue := range err.Report.Issues { - item := map[string]any{"code": issue.Code} - if issue.File != "" { - item["file"] = issue.File - } - if issue.ResourceType != "" { - item["resourceType"] = issue.ResourceType - } - if issue.Row > 0 { - item["row"] = issue.Row - } - issues = append(issues, item) - } - return map[string]any{"issues": issues} -} - func normalizeHTTPCode(code string) string { code = strings.ToUpper(strings.TrimSpace(code)) code = strings.ReplaceAll(code, "-", "_") switch code { - case "PROJECT_REQUIRED", "ROOT_RESOURCE_TYPE_REQUIRED", "UNAUTHORIZED_PROJECT", "UNKNOWN_FIELD", "FIELD_NOT_POPULATED", "INVALID_TRAVERSAL", "UNSAFE_TRAVERSAL_ROUTE", "INVALID_FILTER", "UNBOUNDED_PIVOT", "INVALID_PIVOT_COLUMN", "INVALID_SLICE", "PLAN_TOO_EXPENSIVE", "INVALID_CURSOR", "STALE_CURSOR", "DATASET_GENERATION_CHANGED", "UNSUPPORTED_EXPORT_FORMAT", "CLIENT_CANCELED", "BACKEND_UNAVAILABLE", "RECEIPT_STORE_UNAVAILABLE", "PREVIEW_TIMEOUT", "PREVIEW_RESPONSE_TOO_LARGE", "DYNAMIC_SCHEMA_DRIFT", "RECIPE_CONTRACT_VIOLATION", "DATASET_NOT_FOUND", "SCHEMA_CONFLICT", "INVALID_RESOURCE_TYPE", "NO_ACTIVE_GENERATION", "RESOURCE_DECODE_FAILED", "REFERENCE_NOT_RESOLVED", "QUERY_DEPTH_EXCEEDED", "INVALID_REQUEST", "INVALID_DATA", "INVALID_SELECTOR", "UNAUTHENTICATED", "FORBIDDEN", "RECIPE_NOT_FOUND", "RECIPE_RESOLUTION_FAILED", "RECIPE_EXECUTION_NOT_FOUND", "EXPORT_LIMIT_EXCEEDED", "INGEST_PREFLIGHT_FAILED", "GENERATION_LOAD_INCOMPLETE", "GENERATION_ACTIVATION_UNKNOWN", "INVALID_GENERATION_FILE", "DUPLICATE_GENERATION_FILE", "PUBLICATION_IN_PROGRESS", "PUBLICATION_CONFLICT", "PUBLICATION_LEASE_LOST", "OUTPUT_ENCODING_FAILED", "NOT_FOUND", "METHOD_NOT_ALLOWED", "PAYLOAD_TOO_LARGE", "GRAPHQL_VALIDATION_FAILED": + case "PROJECT_REQUIRED", "ROOT_RESOURCE_TYPE_REQUIRED", "UNAUTHORIZED_PROJECT", "UNKNOWN_FIELD", "FIELD_NOT_POPULATED", "INVALID_TRAVERSAL", "UNSAFE_TRAVERSAL_ROUTE", "INVALID_FILTER", "UNBOUNDED_PIVOT", "INVALID_PIVOT_COLUMN", "INVALID_SLICE", "PLAN_TOO_EXPENSIVE", "INVALID_CURSOR", "STALE_CURSOR", "DATASET_GENERATION_CHANGED", "UNSUPPORTED_EXPORT_FORMAT", "CLIENT_CANCELED", "BACKEND_UNAVAILABLE", "RECEIPT_STORE_UNAVAILABLE", "PREVIEW_TIMEOUT", "PREVIEW_RESPONSE_TOO_LARGE", "QUERY_MEMORY_LIMIT_EXCEEDED", "QUERY_RESOURCE_LIMIT_EXCEEDED", "QUERY_BACKEND_OUT_OF_MEMORY", "DYNAMIC_SCHEMA_DRIFT", "RECIPE_CONTRACT_VIOLATION", "DATASET_NOT_FOUND", "SCHEMA_CONFLICT", "INVALID_RESOURCE_TYPE", "INVALID_LIMIT", "NO_ACTIVE_GENERATION", "RESOURCE_DECODE_FAILED", "REFERENCE_NOT_RESOLVED", "QUERY_DEPTH_EXCEEDED", "INVALID_REQUEST", "INVALID_DATA", "INVALID_SELECTOR", "UNAUTHENTICATED", "FORBIDDEN", "RECIPE_NOT_FOUND", "RECIPE_RESOLUTION_FAILED", "RECIPE_EXECUTION_NOT_FOUND", "EXPORT_LIMIT_EXCEEDED", "INGEST_PREFLIGHT_FAILED", "GENERATION_LOAD_INCOMPLETE", "GENERATION_ACTIVATION_UNKNOWN", "INVALID_GENERATION_FILE", "DUPLICATE_GENERATION_FILE", "PUBLICATION_IN_PROGRESS", "PUBLICATION_CONFLICT", "PUBLICATION_LEASE_LOST", "PUBLICATION_FAILED", "OUTPUT_ENCODING_FAILED", "NOT_FOUND", "METHOD_NOT_ALLOWED", "PAYLOAD_TOO_LARGE", "GRAPHQL_VALIDATION_FAILED": return code default: return "INTERNAL_ERROR" @@ -179,7 +135,9 @@ func statusForCode(code string, fallback int) int { return http.StatusConflict case "PUBLICATION_IN_PROGRESS", "PUBLICATION_CONFLICT": return http.StatusConflict - case "PUBLICATION_LEASE_LOST": + case "PUBLICATION_LEASE_LOST", "QUERY_MEMORY_LIMIT_EXCEEDED", "QUERY_RESOURCE_LIMIT_EXCEEDED", "QUERY_BACKEND_OUT_OF_MEMORY": + return http.StatusServiceUnavailable + case "PUBLICATION_FAILED": return http.StatusServiceUnavailable case "OUTPUT_ENCODING_FAILED": return http.StatusInternalServerError diff --git a/internal/api/http/errors_test.go b/internal/api/http/errors_test.go index dffa6659..7fccff2f 100644 --- a/internal/api/http/errors_test.go +++ b/internal/api/http/errors_test.go @@ -6,7 +6,6 @@ import ( "testing" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/calypr/loom/internal/ingest" "github.com/gofiber/fiber/v3" ) @@ -23,32 +22,6 @@ func TestMapDataframeErrorRedactsUnknownCause(t *testing.T) { } } -func TestMapIngestionFailures(t *testing.T) { - preflight := &ingest.PreflightError{Report: ingest.PreflightReport{Issues: []ingest.PreflightIssue{{Code: "invalid_json", File: "Patient.ndjson", ResourceType: "Patient", Row: 2, Message: "raw parser detail"}}}} - mapped := MapDataframeError(preflight, "req-preflight") - if mapped.Status != http.StatusUnprocessableEntity || mapped.Body.Error.Code != "INGEST_PREFLIGHT_FAILED" || mapped.Body.Error.Retryable { - t.Fatalf("preflight mapped = %#v", mapped) - } - if got := mapped.Body.Error.Details["issues"].([]map[string]any)[0]; got["message"] != nil { - t.Fatal("preflight parser message leaked into details") - } - - incomplete := &ingest.GenerationLoadIncompleteError{ValidationErrors: 2, GenerationErrors: 1, EdgeErrors: 3} - mapped = MapDataframeError(incomplete, "req-incomplete") - if mapped.Status != http.StatusUnprocessableEntity || mapped.Body.Error.Code != "GENERATION_LOAD_INCOMPLETE" { - t.Fatalf("incomplete mapped = %#v", mapped) - } - if mapped.Body.Error.Details["edgeErrors"] != 3 { - t.Fatalf("incomplete details = %#v", mapped.Body.Error.Details) - } - - activation := &ingest.ActivationOutcomeError{Err: errors.New("pointer storage timeout")} - mapped = MapDataframeError(activation, "req-activation") - if mapped.Status != http.StatusConflict || mapped.Body.Error.Code != "GENERATION_ACTIVATION_UNKNOWN" || mapped.Body.Error.Retryable { - t.Fatalf("activation mapped = %#v", mapped) - } -} - func TestMapDataframeErrorBackendIsRetryable(t *testing.T) { err := dataframeerrors.Wrap(errors.New("clickhouse tcp://secret"), dataframeerrors.CodeBackendUnavailable, "") mapped := MapDataframeError(err, "req-2") @@ -70,6 +43,9 @@ func TestMapDataframeErrorPreviewClassifications(t *testing.T) { {dataframeerrors.CodeReceiptStoreUnavailable, http.StatusServiceUnavailable, true}, {dataframeerrors.CodePreviewTimeout, http.StatusGatewayTimeout, true}, {dataframeerrors.CodePreviewResponseTooLarge, http.StatusRequestEntityTooLarge, false}, + {dataframeerrors.CodeQueryMemoryLimitExceeded, http.StatusServiceUnavailable, false}, + {dataframeerrors.CodeQueryResourceLimitExceeded, http.StatusServiceUnavailable, false}, + {dataframeerrors.CodeQueryBackendOutOfMemory, http.StatusServiceUnavailable, false}, {dataframeerrors.CodeDynamicSchemaDrift, http.StatusConflict, false}, {dataframeerrors.CodeRecipeContractViolation, http.StatusConflict, false}, } { @@ -114,3 +90,19 @@ func TestGenerationFileErrors(t *testing.T) { } } } + +func TestMapDataframeErrorPreservesLimitAndPublicationCodes(t *testing.T) { + for _, test := range []struct { + code dataframeerrors.ErrorCode + status int + retryable bool + }{ + {dataframeerrors.CodeInvalidLimit, http.StatusBadRequest, false}, + {dataframeerrors.CodePublicationFailed, http.StatusServiceUnavailable, true}, + } { + mapped := MapDataframeError(dataframeerrors.NewError(test.code, ""), "req-publication") + if mapped.Status != test.status || mapped.Body.Error.Code != string(test.code) || mapped.Body.Error.Retryable != test.retryable { + t.Errorf("%s mapped = %#v, want %s/%d retryable=%t", test.code, mapped, test.code, test.status, test.retryable) + } + } +} diff --git a/internal/api/http/http_test.go b/internal/api/http/http_test.go index 0cc5f622..a99b25dd 100644 --- a/internal/api/http/http_test.go +++ b/internal/api/http/http_test.go @@ -3,7 +3,6 @@ package httpapi import ( "bytes" "context" - "encoding/json" "errors" "log/slog" "net/http" @@ -15,78 +14,30 @@ import ( "github.com/gofiber/fiber/v3" ) -func TestHealthDoesNotRequireAuthentication(t *testing.T) { - server, err := newTestHTTPServer(HTTPConfig{Authenticator: authscope.BasicAuthenticator{Username: "u", Password: "p"}, Authorizer: authscope.AllowAllAuthorizer{}}) - if err != nil { - t.Fatal(err) - } - resp, err := server.App().Test(httptest.NewRequest(http.MethodGet, "/health", nil)) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("health status = %d", resp.StatusCode) - } -} - -func TestHealthRoutesDoNotRequireAuthentication(t *testing.T) { - server, err := newTestHTTPServer(HTTPConfig{ - Authenticator: authscope.BasicAuthenticator{Username: "u", Password: "p"}, - Authorizer: authscope.AllowAllAuthorizer{}, - }) - if err != nil { - t.Fatal(err) - } - for _, path := range []string{"/health", "/livez", "/readyz"} { - resp, testErr := server.App().Test(httptest.NewRequest(http.MethodGet, path, nil)) - if testErr != nil { - t.Fatalf("GET %s: %v", path, testErr) - } - resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("GET %s status = %d", path, resp.StatusCode) - } - } -} - func TestLivenessDoesNotCheckDependencies(t *testing.T) { checks := 0 - server, err := newTestHTTPServer(HTTPConfig{ - Authorizer: authscope.AllowAllAuthorizer{}, - CoreReadyCheck: func(_ context.Context) error { - checks++ - return errors.New("unavailable") - }, - ClickHouseEnabled: true, - ClickHouseReadyCheck: func(_ context.Context) error { - checks++ - return errors.New("unavailable") - }, + server, err := NewHTTPServer(HTTPConfig{ + Authorizer: authscope.AllowAllAuthorizer{}, + CoreReadyCheck: func(context.Context) error { checks++; return errors.New("unavailable") }, + ClickHouseEnabled: true, + ClickHouseReadyCheck: func(context.Context) error { checks++; return errors.New("unavailable") }, }) if err != nil { t.Fatal(err) } - resp, err := server.App().Test(httptest.NewRequest(http.MethodGet, "/livez", nil)) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK || checks != 0 { - t.Fatalf("liveness status = %d, dependency checks = %d", resp.StatusCode, checks) + body, status := server.Liveness(context.Background()) + if status != http.StatusOK || checks != 0 || body["status"] != "live" { + t.Fatalf("liveness status=%d checks=%d body=%#v", status, checks, body) } } func TestReadinessRequiresCoreAndClickHouse(t *testing.T) { tests := []struct { - name string - coreErr error - clickHouseErr error - clickHouseEnabled bool - wantStatus int - wantOverall string - wantCore string - wantDataframe string + name string + coreErr, clickHouseErr error + clickHouseEnabled bool + wantStatus int + wantOverall, wantCore, wantDataframe string }{ {name: "ready", clickHouseEnabled: true, wantStatus: http.StatusOK, wantOverall: "ready", wantCore: "ready", wantDataframe: "ready"}, {name: "core unavailable", coreErr: errors.New("core unavailable"), clickHouseEnabled: true, wantStatus: http.StatusServiceUnavailable, wantOverall: "not_ready", wantCore: "not_ready"}, @@ -95,57 +46,36 @@ func TestReadinessRequiresCoreAndClickHouse(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - server, err := newTestHTTPServer(HTTPConfig{ - Authorizer: authscope.AllowAllAuthorizer{}, - CoreReadyCheck: func(_ context.Context) error { - return tt.coreErr - }, - ClickHouseEnabled: tt.clickHouseEnabled, - ClickHouseReadyCheck: func(_ context.Context) error { - return tt.clickHouseErr - }, + server, err := NewHTTPServer(HTTPConfig{ + Authorizer: authscope.AllowAllAuthorizer{}, + CoreReadyCheck: func(context.Context) error { return tt.coreErr }, + ClickHouseEnabled: tt.clickHouseEnabled, + ClickHouseReadyCheck: func(context.Context) error { return tt.clickHouseErr }, }) if err != nil { t.Fatal(err) } - resp, err := server.App().Test(httptest.NewRequest(http.MethodGet, "/readyz", nil)) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - var body map[string]string - if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { - t.Fatal(err) - } - if resp.StatusCode != tt.wantStatus || body["status"] != tt.wantOverall || body["core"] != tt.wantCore || body["dataframe"] != tt.wantDataframe { - t.Fatalf("status = %d, body = %#v", resp.StatusCode, body) + body, status := server.Readiness(context.Background()) + dataframe, _ := body["dataframe"].(string) + if status != tt.wantStatus || body["status"] != tt.wantOverall || body["core"] != tt.wantCore || dataframe != tt.wantDataframe { + t.Fatalf("status=%d body=%#v", status, body) } }) } } func TestHealthRetainsDegradedClickHouseCompatibility(t *testing.T) { - server, err := newTestHTTPServer(HTTPConfig{ - Authorizer: authscope.AllowAllAuthorizer{}, - ClickHouseEnabled: true, - ClickHouseReadyCheck: func(_ context.Context) error { - return errors.New("ClickHouse unavailable") - }, + server, err := NewHTTPServer(HTTPConfig{ + Authorizer: authscope.AllowAllAuthorizer{}, + ClickHouseEnabled: true, + ClickHouseReadyCheck: func(context.Context) error { return errors.New("ClickHouse unavailable") }, }) if err != nil { t.Fatal(err) } - resp, err := server.App().Test(httptest.NewRequest(http.MethodGet, "/health", nil)) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - var body map[string]string - if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { - t.Fatal(err) - } - if resp.StatusCode != http.StatusOK || body["status"] != "degraded" || body["dataframe"] != "backend_unavailable" { - t.Fatalf("status = %d, body = %#v", resp.StatusCode, body) + body, status := server.Health(context.Background()) + if status != http.StatusOK || body["status"] != "degraded" || body["dataframe"] != "backend_unavailable" { + t.Fatalf("status=%d body=%#v", status, body) } } @@ -155,17 +85,6 @@ func TestNewHTTPServerRequiresAuthorizer(t *testing.T) { } } -func newTestHTTPServer(cfg HTTPConfig) (*HTTPServer, error) { - server, err := NewHTTPServer(cfg) - if err != nil { - return nil, err - } - server.App().Get("/health", server.HandleHealth) - server.App().Get("/livez", server.HandleLiveness) - server.App().Get("/readyz", server.HandleReadiness) - return server, nil -} - func TestLoggingMiddlewareEmitsStructuredResponseDiagnostics(t *testing.T) { var logs bytes.Buffer logger := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo})) diff --git a/internal/api/http/recipe_execution.go b/internal/api/http/recipe_execution.go deleted file mode 100644 index b4ce8984..00000000 --- a/internal/api/http/recipe_execution.go +++ /dev/null @@ -1,99 +0,0 @@ -package httpapi - -import ( - "errors" - "net/http" - "strings" - - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataframe/publication" - "github.com/gofiber/fiber/v3" -) - -// RecipeExecutionHandler exposes candidate schema metadata without physical -// table names. Generated OpenAPI routing calls Handle directly. -type RecipeExecutionHandler struct { - Catalog publication.BundleCatalog - Scopes *authscope.ScopeResolver -} - -func (h RecipeExecutionHandler) Handle(c fiber.Ctx) error { - if h.Catalog == nil { - return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "recipe execution not found"}) - } - id := strings.TrimSpace(c.Params("id")) - execution, err := h.Catalog.GetExecution(c.Context(), id) - if err != nil || execution.ID == "" { - return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "recipe execution not found"}) - } - if h.Scopes != nil { - principal, _ := authscope.PrincipalFromContext(c.Context()) - if _, scopeErr := h.Scopes.ResolveReadScopeForGeneration(c.Context(), principal, execution.Project, execution.DatasetGeneration, execution.AuthResourcePaths); scopeErr != nil { - status := http.StatusForbidden - if errors.Is(scopeErr, authscope.ErrUnauthenticated) { - status = http.StatusUnauthorized - } - return c.Status(status).JSON(fiber.Map{"error": "recipe execution not found"}) - } - } - outputs := make([]fiber.Map, 0, len(execution.Outputs)) - for _, output := range execution.Outputs { - columns := make([]fiber.Map, 0, len(output.Columns)) - for _, column := range output.Columns { - logical, nullable, repeated, filterable, sortable, aggregatable := columnCapabilities(column.ClickHouse) - if column.LogicalType != "" { - logical = column.LogicalType - } - if column.Nullable { - nullable = true - } - if column.Repeated { - repeated = true - } - columns = append(columns, fiber.Map{"name": column.Name, "semanticPath": column.SemanticPath, "clickhouseType": column.ClickHouse, "logicalType": logical, "nullable": nullable, "repeated": repeated, "filterable": filterable, "sortable": sortable, "aggregatable": aggregatable}) - } - outputs = append(outputs, fiber.Map{"name": output.Name, "state": recipeExecutionHTTPState(output.State), "rowCount": output.RowCount, "columns": columns}) - } - return c.JSON(fiber.Map{"id": execution.ID, "projectId": execution.Project, "datasetGeneration": execution.DatasetGeneration, "recipeDigest": execution.RecipeDigest, "schemaDigest": execution.SchemaDigest, "resolvedSchemaDigest": execution.SchemaDigest, "state": recipeExecutionHTTPState(execution.State), "outputs": outputs}) -} - -// recipeExecutionHTTPState preserves the READY spelling used by the legacy -// Explorer integration. Internally, a successfully committed bundle is -// PUBLISHED; the HTTP execution contract historically exposed that same -// successful state as READY and Gecko still validates that spelling. -func recipeExecutionHTTPState(state publication.BundleState) string { - if state == publication.BundlePublished { - return string(publication.BundleReady) - } - return string(state) -} - -func columnCapabilities(clickHouseType string) (logical string, nullable, repeated, filterable, sortable, aggregatable bool) { - typ := clickHouseType - if strings.HasPrefix(typ, "Nullable(") && strings.HasSuffix(typ, ")") { - nullable = true - typ = strings.TrimSuffix(strings.TrimPrefix(typ, "Nullable("), ")") - } - if strings.HasPrefix(typ, "Array(") && strings.HasSuffix(typ, ")") { - repeated = true - typ = strings.TrimSuffix(strings.TrimPrefix(typ, "Array("), ")") - } - switch { - case strings.HasPrefix(typ, "Bool"): - logical = "boolean" - case strings.HasPrefix(typ, "Int") || strings.HasPrefix(typ, "UInt"): - logical = "integer" - case strings.HasPrefix(typ, "Float") || strings.HasPrefix(typ, "Decimal"): - logical = "number" - case strings.HasPrefix(typ, "Date"): - logical = "date" - case strings.HasPrefix(typ, "String") || strings.HasPrefix(typ, "FixedString") || strings.HasPrefix(typ, "UUID"): - logical = "string" - default: - logical = "json" - } - filterable = true - sortable = !repeated - aggregatable = !repeated && logical != "json" - return -} diff --git a/internal/api/http/routes.go b/internal/api/http/routes.go index 0b3bba88..b4f47b11 100644 --- a/internal/api/http/routes.go +++ b/internal/api/http/routes.go @@ -11,32 +11,34 @@ func (s *HTTPServer) register() { s.app.Use(s.requestIDMiddleware, s.recoveryMiddleware, s.loggingMiddleware, s.authenticationMiddleware) } -func (s *HTTPServer) HandleHealth(c fiber.Ctx) error { +// Health computes the cached dependency health response for generated HTTP +// routes without requiring a Fiber context. +func (s *HTTPServer) Health(parent context.Context) (map[string]any, int) { s.healthMu.Lock() defer s.healthMu.Unlock() if time.Since(s.lastHealth) < 30*time.Second { - return s.writeHealth(c, s.lastHealthResult) + return healthBody(s.lastHealthResult), s.lastHealthResult.httpStatus } - ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second) + ctx, cancel := context.WithTimeout(parent, 2*time.Second) defer cancel() result := s.checkDependencies(ctx) s.lastHealth, s.lastHealthResult = time.Now(), result - return s.writeHealth(c, result) + return healthBody(result), result.httpStatus } -func (s *HTTPServer) HandleLiveness(c fiber.Ctx) error { - return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "live"}) +func (s *HTTPServer) Liveness(context.Context) (map[string]any, int) { + return map[string]any{"status": "live"}, fiber.StatusOK } -func (s *HTTPServer) HandleReadiness(c fiber.Ctx) error { - ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second) +func (s *HTTPServer) Readiness(parent context.Context) (map[string]any, int) { + ctx, cancel := context.WithTimeout(parent, 2*time.Second) defer cancel() result := s.checkDependencies(ctx) if result.core != "ready" || result.dataframe == "backend_unavailable" { result.status = "not_ready" result.httpStatus = fiber.StatusServiceUnavailable } - return s.writeHealth(c, result) + return healthBody(result), result.httpStatus } func (s *HTTPServer) checkDependencies(ctx context.Context) healthResult { @@ -59,10 +61,10 @@ func (s *HTTPServer) checkDependencies(ctx context.Context) healthResult { return result } -func (s *HTTPServer) writeHealth(c fiber.Ctx, result healthResult) error { - body := fiber.Map{"status": result.status, "core": result.core} +func healthBody(result healthResult) map[string]any { + body := map[string]any{"status": result.status, "core": result.core} if result.dataframe != "" { body["dataframe"] = result.dataframe } - return c.Status(result.httpStatus).JSON(body) + return body } diff --git a/internal/catalog/arango/codec.go b/internal/catalog/arango/codec.go new file mode 100644 index 00000000..45fc321f --- /dev/null +++ b/internal/catalog/arango/codec.go @@ -0,0 +1,153 @@ +package arango + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/calypr/loom/internal/catalog" +) + +func generation(value string) any { + value = catalog.NormalizeDatasetGeneration(value) + if value == "" { + return nil + } + return value +} + +func stringValue(value any) string { + if value == nil { + return "" + } + return fmt.Sprint(value) +} +func decodeInt64(value any) (int64, error) { + switch typed := value.(type) { + case int: + return int64(typed), nil + case int32: + return int64(typed), nil + case int64: + return typed, nil + case float64: + return int64(typed), nil + case float32: + return int64(typed), nil + case json.Number: + n, err := typed.Int64() + return n, err + case string: + n, err := strconv.ParseInt(typed, 10, 64) + return n, err + case nil: + return 0, nil + } + return 0, fmt.Errorf("unsupported numeric type %T", value) +} + +func decodeBool(value any) (bool, error) { + if value == nil { + return false, nil + } + result, ok := value.(bool) + if !ok { + return false, fmt.Errorf("unsupported boolean type %T", value) + } + return result, nil +} + +func decodeStrings(value any) ([]string, error) { + if value == nil { + return nil, nil + } + switch values := value.(type) { + case []string: + return append([]string(nil), values...), nil + case []any: + result := make([]string, len(values)) + for i, value := range values { + text, ok := value.(string) + if !ok { + return nil, fmt.Errorf("unsupported slice item type %T at index %d", value, i) + } + result[i] = text + } + return result, nil + default: + return nil, fmt.Errorf("unsupported string slice type %T", value) + } +} + +func decodeExtensionValues(value any) ([]catalog.ExtensionValueObservation, error) { + if value == nil { + return nil, nil + } + items, ok := value.([]any) + if !ok { + if typed, ok := value.([]map[string]any); ok { + items = make([]any, len(typed)) + for i := range typed { + items[i] = typed[i] + } + } else { + return nil, fmt.Errorf("unsupported extension observation slice type %T", value) + } + } + result := make([]catalog.ExtensionValueObservation, 0, len(items)) + for i, item := range items { + row, ok := item.(map[string]any) + if !ok { + return nil, fmt.Errorf("unsupported extension observation type %T at index %d", item, i) + } + urlPath, err := decodeStrings(row["url_path"]) + if err != nil { + return nil, fmt.Errorf("extension observation URL path: %w", err) + } + result = append(result, catalog.ExtensionValueObservation{URL: stringValue(row["url"]), SourcePath: stringValue(row["source_path"]), ValuePath: stringValue(row["value_path"]), ValueType: stringValue(row["value_type"]), URLPath: urlPath}) + } + return result, nil +} + +func decode(row map[string]any, out *catalog.PopulatedField) error { + out.Project = stringValue(row["project"]) + out.DatasetGeneration = stringValue(row["dataset_generation"]) + out.AuthResourcePath = stringValue(row["auth_resource_path"]) + out.ResourceType = stringValue(row["resource_type"]) + out.Path = stringValue(row["path"]) + out.Kind = stringValue(row["kind"]) + var err error + if out.DocCount, err = decodeInt64(row["doc_count"]); err != nil { + return fmt.Errorf("decode field row %s/%s doc_count: %w", out.ResourceType, out.Path, err) + } + var sampleCount int64 + if sampleCount, err = decodeInt64(row["sample_count"]); err != nil { + return fmt.Errorf("decode field row %s/%s sample_count: %w", out.ResourceType, out.Path, err) + } + out.SampleCount = int(sampleCount) + if out.DistinctTruncated, err = decodeBool(row["distinct_truncated"]); err != nil { + return fmt.Errorf("decode field row %s/%s distinct_truncated: %w", out.ResourceType, out.Path, err) + } + if out.PivotCandidate, err = decodeBool(row["pivot_candidate"]); err != nil { + return fmt.Errorf("decode field row %s/%s pivot_candidate: %w", out.ResourceType, out.Path, err) + } + if out.DistinctValues, err = decodeStrings(row["distinct_values"]); err != nil { + return fmt.Errorf("decode field row %s/%s distinct_values: %w", out.ResourceType, out.Path, err) + } + if out.PivotColumns, err = decodeStrings(row["pivot_columns"]); err != nil { + return fmt.Errorf("decode field row %s/%s pivot_columns: %w", out.ResourceType, out.Path, err) + } + if out.PivotValueSelectors, err = decodeStrings(row["pivot_value_selectors"]); err != nil { + return fmt.Errorf("decode field row %s/%s pivot_value_selectors: %w", out.ResourceType, out.Path, err) + } + if out.ExtensionValues, err = decodeExtensionValues(row["extension_values"]); err != nil { + return fmt.Errorf("decode field row %s/%s extension_values: %w", out.ResourceType, out.Path, err) + } + out.PivotKind = stringValue(row["pivot_kind"]) + out.PivotFamily = stringValue(row["pivot_family"]) + out.PivotColumnSelect = stringValue(row["pivot_column_selector"]) + out.PivotValueSelect = stringValue(row["pivot_value_selector"]) + out.PivotItemSource = stringValue(row["pivot_item_source"]) + out.PivotItemResourceType = stringValue(row["pivot_item_resource_type"]) + return nil +} diff --git a/internal/catalog/arango/evidence.go b/internal/catalog/arango/evidence.go new file mode 100644 index 00000000..ff683f72 --- /dev/null +++ b/internal/catalog/arango/evidence.go @@ -0,0 +1,114 @@ +package arango + +import ( + "context" + "sort" + + "github.com/calypr/loom/internal/catalog" + fhirschema "github.com/calypr/loom/internal/fhir/schema" +) + +// Read* aliases make the capability boundary explicit while retaining the +// Discover* naming used by the pre-existing catalog reader. +func (s *Store) ReadResourceInventory(ctx context.Context, opts catalog.ResourceInventoryOptions) (catalog.ResourceInventoryResult, error) { + return s.DiscoverResourceInventory(ctx, opts) +} +func (s *Store) ReadRelationshipObservations(ctx context.Context, opts catalog.RelationshipObservationOptions) (catalog.RelationshipObservationResult, error) { + return s.DiscoverRelationshipObservations(ctx, opts) +} +func (s *Store) ReadFieldEnrichment(ctx context.Context, opts catalog.FieldEnrichmentOptions) (catalog.FieldEnrichmentResult, error) { + return s.DiscoverFieldEnrichment(ctx, opts) +} + +type relationshipObservationKey struct { + project, generation, auth string + storageFrom, label, storageTo string + builderFrom, builderTo, builderDirection string +} + +func evidenceResourceTypes(requested []string) ([]string, []catalog.EvidenceDiagnostic) { + if len(requested) == 0 { + return fhirschema.ResourceTypes(), nil + } + seen := make(map[string]struct{}, len(requested)) + diagnostics := make([]catalog.EvidenceDiagnostic, 0) + for _, requestedType := range requested { + canonical, ok := fhirschema.ConcreteResourceType(requestedType) + if !ok { + diagnostics = append(diagnostics, catalog.EvidenceDiagnostic{Code: "INVALID_RESOURCE_TYPE", Message: "requested resource type is not a concrete generated FHIR resource", ResourceType: requestedType}) + continue + } + seen[canonical] = struct{}{} + } + result := make([]string, 0, len(seen)) + for resourceType := range seen { + result = append(result, resourceType) + } + sort.Strings(result) + return result, diagnostics +} + +func inventoryObservationLess(a, b catalog.ResourceInventoryObservation) bool { + for _, pair := range [][2]string{{a.Project, b.Project}, {a.DatasetGeneration, b.DatasetGeneration}, {a.AuthResourcePath, b.AuthResourcePath}, {a.ResourceType, b.ResourceType}} { + if pair[0] != pair[1] { + return pair[0] < pair[1] + } + } + return a.DocumentCount < b.DocumentCount +} + +func relationshipLess(a, b catalog.RelationshipObservation) bool { + for _, pair := range [][2]string{{a.Project, b.Project}, {a.DatasetGeneration, b.DatasetGeneration}, {a.AuthResourcePath, b.AuthResourcePath}, {a.FromType, b.FromType}, {a.Label, b.Label}, {a.ToType, b.ToType}, {a.StorageFromType, b.StorageFromType}, {a.StorageToType, b.StorageToType}} { + if pair[0] != pair[1] { + return pair[0] < pair[1] + } + } + return a.EdgeCount < b.EdgeCount +} + +func fieldLess(a, b catalog.FieldEnrichmentObservation) bool { + for _, pair := range [][2]string{{a.Project, b.Project}, {a.DatasetGeneration, b.DatasetGeneration}, {a.AuthResourcePath, b.AuthResourcePath}, {a.ResourceType, b.ResourceType}, {a.Path, b.Path}, {a.Kind, b.Kind}} { + if pair[0] != pair[1] { + return pair[0] < pair[1] + } + } + return a.DocCount < b.DocCount +} + +func relationshipObservation(project, datasetGeneration, auth, storageFrom, label, storageTo string, count int64) (catalog.RelationshipObservation, bool, catalog.EvidenceDiagnostic) { + diagnostic := catalog.EvidenceDiagnostic{StorageFromType: storageFrom, Label: label, StorageToType: storageTo, Count: count} + if count <= 0 { + diagnostic.Code = "INVALID_RELATIONSHIP_COUNT" + diagnostic.Message = "relationship evidence count must be positive" + return catalog.RelationshipObservation{}, false, diagnostic + } + // Stored fhir_edge is always _from -> _to. Builder's normal projection is + // the inverse generated traversal, which is physically INBOUND from the + // stored target. Keep a direct generated traversal as a valid fallback for + // labels whose generated schema only exposes the storage orientation. + if _, found, err := fhirschema.ResolveCompilerTraversal(storageTo, label, storageFrom); found && err == nil { + return catalog.RelationshipObservation{Project: project, DatasetGeneration: datasetGeneration, AuthResourcePath: auth, FromType: storageTo, Label: label, ToType: storageFrom, EdgeCount: count, StorageFromType: storageFrom, StorageToType: storageTo, StorageDirection: "OUTBOUND", BuilderFromType: storageTo, BuilderToType: storageFrom, BuilderDirection: "INBOUND"}, true, diagnostic + } + if _, found, err := fhirschema.ResolveCompilerTraversal(storageFrom, label, storageTo); found && err == nil { + return catalog.RelationshipObservation{Project: project, DatasetGeneration: datasetGeneration, AuthResourcePath: auth, FromType: storageFrom, Label: label, ToType: storageTo, EdgeCount: count, StorageFromType: storageFrom, StorageToType: storageTo, StorageDirection: "OUTBOUND", BuilderFromType: storageFrom, BuilderToType: storageTo, BuilderDirection: "OUTBOUND"}, true, diagnostic + } + diagnostic.Code = "UNSUPPORTED_GENERATED_TRAVERSAL" + diagnostic.Message = "relationship endpoints and label are not a supported generated traversal" + return catalog.RelationshipObservation{}, false, diagnostic +} + +func evidenceStatus(available, complete, truncated bool, count int) catalog.EvidenceStatus { + if !available { + return catalog.EvidenceUnavailable + } + if truncated { + return catalog.EvidenceTruncated + } + if !complete { + return catalog.EvidenceIncomplete + } + if count == 0 { + return catalog.EvidenceEmpty + } + return catalog.EvidenceAvailable +} diff --git a/internal/catalog/arango/fields.go b/internal/catalog/arango/fields.go new file mode 100644 index 00000000..952b575a --- /dev/null +++ b/internal/catalog/arango/fields.go @@ -0,0 +1,189 @@ +package arango + +import ( + "context" + "encoding/json" + "sort" + "time" + + "github.com/bytedance/sonic" + "github.com/calypr/loom/internal/catalog" + fhirschema "github.com/calypr/loom/internal/fhir/schema" +) + +func (s *Store) DiscoverFields(ctx context.Context, opts catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + vars := map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration), "resource_type": nil, "pivot_only": opts.PivotOnly, "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted)} + if opts.ResourceType != "" { + vars["resource_type"] = opts.ResourceType + } + rows := []catalog.PopulatedField{} + err := s.client.QueryRows(ctx, populatedFieldsAQL, opts.CursorBatch, vars, func(row map[string]any) error { + var out catalog.PopulatedField + if err := decode(row, &out); err != nil { + return err + } + resourceType, ok := fhirschema.ConcreteResourceType(out.ResourceType) + if !ok { + return nil + } + out.ResourceType = resourceType + rows = append(rows, out) + return nil + }) + return rows, err +} + +// DiscoverFieldEnrichment reads profiler rows as optional, fail-closed +// enrichment. A successful empty result is valid evidence; a query failure or +// bounded distinct-value set is explicitly unavailable/incomplete. +func (s *Store) DiscoverFieldEnrichment(ctx context.Context, opts catalog.FieldEnrichmentOptions) (catalog.FieldEnrichmentResult, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + resourceTypes, diagnostics := evidenceResourceTypes(opts.ResourceTypes) + result := catalog.FieldEnrichmentResult{Values: []catalog.FieldEnrichmentObservation{}, Available: true, Complete: true, Diagnostics: diagnostics} + vars := map[string]any{ + "project": opts.Project, + "dataset_generation": generation(opts.DatasetGeneration), + "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), + "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), + } + if len(opts.ResourceTypes) > 0 { + vars["resource_types"] = resourceTypes + } else { + vars["resource_types"] = nil + } + err := s.client.QueryRows(ctx, fieldEnrichmentAQL, opts.CursorBatch, vars, func(row map[string]any) error { + var field catalog.PopulatedField + if err := decode(row, &field); err != nil { + return err + } + resourceType, ok := fhirschema.ConcreteResourceType(field.ResourceType) + if !ok { + result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "INVALID_FIELD_RESOURCE_TYPE", Message: "field enrichment resource type is not concrete/generated", ResourceType: field.ResourceType}) + return nil + } + field.ResourceType = resourceType + result.Values = append(result.Values, catalog.FieldEnrichmentObservation{Project: field.Project, DatasetGeneration: field.DatasetGeneration, AuthResourcePath: field.AuthResourcePath, ResourceType: field.ResourceType, Path: field.Path, Kind: field.Kind, DocCount: field.DocCount, SampleCount: field.SampleCount, DistinctValues: append([]string(nil), field.DistinctValues...), DistinctTruncated: field.DistinctTruncated, ExtensionValues: append([]catalog.ExtensionValueObservation(nil), field.ExtensionValues...), PivotCandidate: field.PivotCandidate, PivotKind: field.PivotKind, PivotColumns: append([]string(nil), field.PivotColumns...), PivotFamily: field.PivotFamily, PivotColumnSelect: field.PivotColumnSelect, PivotValueSelect: field.PivotValueSelect, PivotItemSource: field.PivotItemSource, PivotItemResourceType: field.PivotItemResourceType, PivotValueSelectors: append([]string(nil), field.PivotValueSelectors...)}) + // DistinctTruncated is per-field suggestion metadata. It is retained + // on the observation but does not make the complete enrichment artifact + // unusable; only an artifact-level marker can fail the snapshot. + if truncated, ok := row["enrichment_truncated"].(bool); ok && truncated { + result.Truncated = true + } + if complete, ok := row["enrichment_complete"].(bool); ok && !complete { + result.Complete = false + } + return nil + }) + if err != nil { + result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable + result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "FIELD_ENRICHMENT_UNAVAILABLE", Message: err.Error()}) + return result, err + } + if result.Truncated { + result.Complete = false + } + sort.Slice(result.Values, func(i, j int) bool { return fieldLess(result.Values[i], result.Values[j]) }) + result.Digest, _ = catalog.FieldEnrichmentDigest(result.Values) + result.Status = evidenceStatus(result.Available, result.Complete, result.Truncated, len(result.Values)) + return result, nil +} + +func (s *Store) WriteFieldCatalog(ctx context.Context, collection string, docs []catalog.FieldCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { + if len(docs) == 0 { + return nil + } + if batchSize <= 0 { + batchSize = 1000 + } + started := time.Now() + raw := make([]json.RawMessage, 0, len(docs)) + for _, doc := range docs { + encoded, err := sonic.ConfigFastest.Marshal(&doc) + if err != nil { + return err + } + raw = append(raw, encoded) + } + if timings != nil { + timings["field_catalog_marshal"] += time.Since(started).Seconds() + } + for i := 0; i < len(raw); i += batchSize { + end := min(i+batchSize, len(raw)) + inserted := time.Now() + if err := s.client.InsertBatchRaw(ctx, collection, raw[i:end], overwrite, writeAPI); err != nil { + return err + } + if timings != nil { + timings["field_catalog_insert"] += time.Since(inserted).Seconds() + } + } + return nil +} + +const populatedFieldsAQL = ` +FOR d IN fhir_field_catalog + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths + FILTER @resource_type == null OR d.resource_type == @resource_type + FILTER @pivot_only == false OR d.pivot_candidate == true + SORT d.resource_type, d.doc_count DESC, d.path + RETURN { + project: d.project, + dataset_generation: d.dataset_generation, + auth_resource_path: d.auth_resource_path, + resource_type: d.resource_type, + path: d.path, + kind: d.kind, + doc_count: d.doc_count, + sample_count: d.sample_count, + distinct_values: d.distinct_values, + distinct_truncated: d.distinct_truncated, + extension_values: d.extension_values, + pivot_candidate: d.pivot_candidate, + pivot_kind: d.pivot_kind, + pivot_columns: d.pivot_columns, + pivot_family: d.pivot_family, + pivot_column_selector: d.pivot_column_selector, + pivot_value_selector: d.pivot_value_selector, + pivot_item_source: d.pivot_item_source, + pivot_item_resource_type: d.pivot_item_resource_type, + pivot_value_selectors: d.pivot_value_selectors + }` + +const fieldEnrichmentAQL = ` +FOR d IN fhir_field_catalog + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths + FILTER @resource_types == null OR d.resource_type IN @resource_types + SORT d.resource_type, d.path, d.auth_resource_path, d.doc_count + RETURN { + project: d.project, + dataset_generation: d.dataset_generation, + auth_resource_path: d.auth_resource_path, + resource_type: d.resource_type, + path: d.path, + kind: d.kind, + doc_count: d.doc_count, + sample_count: d.sample_count, + distinct_values: d.distinct_values, + distinct_truncated: d.distinct_truncated, + extension_values: d.extension_values, + pivot_candidate: d.pivot_candidate, + pivot_kind: d.pivot_kind, + pivot_columns: d.pivot_columns, + pivot_family: d.pivot_family, + pivot_column_selector: d.pivot_column_selector, + pivot_value_selector: d.pivot_value_selector, + pivot_item_source: d.pivot_item_source, + pivot_item_resource_type: d.pivot_item_resource_type, + pivot_value_selectors: d.pivot_value_selectors, + enrichment_complete: d.enrichment_complete, + enrichment_truncated: d.enrichment_truncated + }` diff --git a/internal/catalog/arango/relationships.go b/internal/catalog/arango/relationships.go new file mode 100644 index 00000000..5b3d4831 --- /dev/null +++ b/internal/catalog/arango/relationships.go @@ -0,0 +1,316 @@ +package arango + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/bytedance/sonic" + "github.com/calypr/loom/internal/catalog" + fhirschema "github.com/calypr/loom/internal/fhir/schema" + store "github.com/calypr/loom/internal/store/arango" +) + +func (s *Store) DiscoverReferences(ctx context.Context, opts catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + mode := opts.Mode + if mode == "" { + mode = catalog.TraversalModeStorage + } + vars := map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration), "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted)} + query := relationshipCatalogStorageAQL + if mode == catalog.TraversalModeBuilder { + query = relationshipCatalogBuilderAQL + vars["node_type"] = nil + if opts.NodeType != "" { + vars["node_type"] = opts.NodeType + } + } else { + vars["from_type"] = nil + if opts.FromType != "" { + vars["from_type"] = opts.FromType + } + } + rows := make([]catalog.PopulatedReference, 0, 64) + err := s.client.QueryRows(ctx, query, opts.CursorBatch, vars, func(row map[string]any) error { + fromType, fromOK := fhirschema.ConcreteResourceType(stringValue(row["from_type"])) + toType, toOK := fhirschema.ConcreteResourceType(stringValue(row["to_type"])) + if !fromOK || !toOK { + return nil + } + if _, ok := fhirschema.LookupTraversal(fromType, stringValue(row["label"]), toType); !ok { + return nil + } + count, err := decodeInt64(row["edge_count"]) + if err != nil { + return fmt.Errorf("decode edge_count for %s/%s/%s: %w", fromType, stringValue(row["label"]), toType, err) + } + rows = append(rows, catalog.PopulatedReference{DatasetGeneration: stringValue(row["dataset_generation"]), FromType: fromType, Label: stringValue(row["label"]), ToType: toType, EdgeCount: count}) + return nil + }) + return rows, err +} + +func (s *Store) AuditRelationshipEdges(ctx context.Context, opts catalog.RelationshipAuditOptions) (catalog.RelationshipAuditSummary, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + vars := map[string]any{ + "project": opts.Project, + "dataset_generation": generation(opts.DatasetGeneration), + "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), + "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), + "resource_types": fhirschema.ResourceTypes(), + } + summary := catalog.RelationshipAuditSummary{Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration)} + err := s.client.QueryRows(ctx, relationshipAuditAQL, opts.CursorBatch, vars, func(row map[string]any) error { + count, err := decodeInt64(row["edge_count"]) + if err != nil { + return err + } + summary.InvalidEdgeCount += count + summary.InvalidRelations = append(summary.InvalidRelations, catalog.InvalidRelationship{ + FromType: stringValue(row["from_type"]), + Label: stringValue(row["label"]), + ToType: stringValue(row["to_type"]), + EdgeCount: count, + }) + return nil + }) + if err != nil { + return catalog.RelationshipAuditSummary{}, err + } + return summary, nil +} + +// DiscoverRelationshipObservations returns both storage and Builder views of +// each aggregate. The catalog stores physical fhir_edge direction; Builder +// usually traverses its inverse, so dropping the storage provenance here is a +// correctness bug rather than a presentation detail. +func (s *Store) DiscoverRelationshipObservations(ctx context.Context, opts catalog.RelationshipObservationOptions) (catalog.RelationshipObservationResult, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + resourceTypes, diagnostics := evidenceResourceTypes(opts.ResourceTypes) + result := catalog.RelationshipObservationResult{Values: []catalog.RelationshipObservation{}, Available: true, Complete: true, Diagnostics: diagnostics} + vars := map[string]any{ + "project": opts.Project, + "dataset_generation": generation(opts.DatasetGeneration), + "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), + "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), + } + if len(opts.ResourceTypes) > 0 { + vars["resource_types"] = resourceTypes + } else { + vars["resource_types"] = nil + } + counts := make(map[relationshipObservationKey]int64) + var queryErr error + err := s.client.QueryRows(ctx, relationshipObservationsAQL, opts.CursorBatch, vars, func(row map[string]any) error { + storageFrom := stringValue(row["storage_from_type"]) + label := stringValue(row["label"]) + storageTo := stringValue(row["storage_to_type"]) + count, err := decodeInt64(row["edge_count"]) + if err != nil { + return fmt.Errorf("decode relationship count for %s/%s/%s: %w", storageFrom, label, storageTo, err) + } + from, fromOK := fhirschema.ConcreteResourceType(storageFrom) + to, toOK := fhirschema.ConcreteResourceType(storageTo) + if !fromOK || !toOK { + result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "INVALID_RELATIONSHIP_ENDPOINT", Message: "relationship endpoint is not a concrete generated FHIR resource", StorageFromType: storageFrom, Label: label, StorageToType: storageTo, Count: count}) + return nil + } + observation, ok, diagnostic := relationshipObservation(stringValue(row["project"]), stringValue(row["dataset_generation"]), stringValue(row["auth_resource_path"]), from, label, to, count) + if !ok { + result.Diagnostics = append(result.Diagnostics, diagnostic) + return nil + } + key := relationshipObservationKey{project: observation.Project, generation: observation.DatasetGeneration, auth: observation.AuthResourcePath, storageFrom: observation.StorageFromType, label: observation.Label, storageTo: observation.StorageToType, builderFrom: observation.BuilderFromType, builderTo: observation.BuilderToType, builderDirection: observation.BuilderDirection} + counts[key] += observation.EdgeCount + return nil + }) + queryErr = err + if queryErr != nil { + result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable + result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "RELATIONSHIP_EVIDENCE_UNAVAILABLE", Message: queryErr.Error()}) + return result, queryErr + } + for key, count := range counts { + result.Values = append(result.Values, catalog.RelationshipObservation{Project: key.project, DatasetGeneration: key.generation, AuthResourcePath: key.auth, FromType: key.builderFrom, Label: key.label, ToType: key.builderTo, EdgeCount: count, StorageFromType: key.storageFrom, StorageToType: key.storageTo, StorageDirection: "OUTBOUND", BuilderFromType: key.builderFrom, BuilderToType: key.builderTo, BuilderDirection: key.builderDirection}) + } + sort.Slice(result.Values, func(i, j int) bool { return relationshipLess(result.Values[i], result.Values[j]) }) + result.Digest, _ = catalog.RelationshipObservationDigest(result.Values) + result.Status = evidenceStatus(result.Available, result.Complete, result.Truncated, len(result.Values)) + return result, nil +} + +func (s *Store) WriteRelationshipCatalog(ctx context.Context, docs []catalog.RelationshipCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { + if len(docs) == 0 { + return nil + } + if batchSize <= 0 { + batchSize = 1000 + } + started := time.Now() + raw := make([]json.RawMessage, 0, len(docs)) + for _, doc := range docs { + encoded, err := sonic.ConfigFastest.Marshal(&doc) + if err != nil { + return err + } + raw = append(raw, encoded) + } + if timings != nil { + timings["relationship_catalog_marshal"] += time.Since(started).Seconds() + } + for i := 0; i < len(raw); i += batchSize { + end := min(i+batchSize, len(raw)) + inserted := time.Now() + if err := s.client.InsertBatchRaw(ctx, catalog.RelationshipCatalogCollection, raw[i:end], overwrite, writeAPI); err != nil { + return err + } + if timings != nil { + timings["relationship_catalog_insert"] += time.Since(inserted).Seconds() + } + } + return nil +} +func (s *Store) AccumulateRelationshipCatalog(ctx context.Context, docs []catalog.RelationshipCatalogDocument, timings map[string]float64) error { + if len(docs) == 0 { + return nil + } + rows := make([]map[string]any, 0, len(docs)) + for _, doc := range docs { + rows = append(rows, map[string]any{"_key": doc.Key, "project": doc.Project, "dataset_generation": generation(doc.DatasetGeneration), "auth_resource_path": doc.AuthResourcePath, "from_type": doc.FromType, "label": doc.Label, "to_type": doc.ToType, "edge_count": doc.EdgeCount}) + } + started := time.Now() + const query = ` +FOR d IN @docs + UPSERT { _key: d._key } + INSERT d + UPDATE { edge_count: OLD.edge_count + d.edge_count } + IN fhir_relationship_catalog +` + if err := s.client.ExecuteAQL(ctx, query, map[string]any{"docs": rows}); err != nil { + return err + } + if timings != nil { + timings["relationship_catalog_accumulate"] += time.Since(started).Seconds() + } + return nil +} +func (s *Store) RebuildRelationshipCatalog(ctx context.Context, opts catalog.RelationshipRebuildOptions) (catalog.RelationshipRebuildSummary, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + if opts.BatchSize <= 0 { + opts.BatchSize = 1000 + } + if opts.WriteAPI == "" { + opts.WriteAPI = "import" + } + started := time.Now() + if err := s.client.Bootstrap(ctx, store.BootstrapSpec{Collections: []store.CollectionSpec{{ + Name: catalog.RelationshipCatalogCollection, + Indexes: [][]string{ + {"project", "dataset_generation", "to_type"}, + {"project", "dataset_generation", "auth_resource_path", "to_type"}, + {"project", "dataset_generation", "from_type"}, + {"project", "dataset_generation", "auth_resource_path", "from_type"}, + }, + }}}); err != nil { + return catalog.RelationshipRebuildSummary{}, err + } + audit, err := s.AuditRelationshipEdges(ctx, catalog.RelationshipAuditOptions{Project: opts.Project, DatasetGeneration: opts.DatasetGeneration, AuthResourcePaths: opts.AuthResourcePaths, AuthResourcePathsUnrestricted: opts.AuthResourcePathsUnrestricted, CursorBatch: opts.CursorBatch}) + if err != nil { + return catalog.RelationshipRebuildSummary{}, err + } + vars := map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration), "auth_resource_paths": opts.AuthResourcePaths, "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), "resource_types": fhirschema.ResourceTypes()} + if err := s.client.QueryRows(ctx, relationshipClearAQL, opts.CursorBatch, vars, func(map[string]any) error { return nil }); err != nil { + return catalog.RelationshipRebuildSummary{}, err + } + counts := map[catalog.RelationshipKey]int64{} + if err := s.client.QueryRows(ctx, relationshipRebuildAQL, opts.CursorBatch, vars, func(row map[string]any) error { + count, err := decodeInt64(row["edge_count"]) + if err != nil { + return err + } + key := catalog.RelationshipKey{Project: stringValue(row["project"]), DatasetGeneration: stringValue(row["dataset_generation"]), AuthResourcePath: stringValue(row["auth_resource_path"]), FromType: stringValue(row["from_type"]), Label: stringValue(row["label"]), ToType: stringValue(row["to_type"])} + counts[key] = count + return nil + }); err != nil { + return catalog.RelationshipRebuildSummary{}, err + } + docs := catalog.RelationshipCatalogDocuments(counts) + if err := s.WriteRelationshipCatalog(ctx, docs, opts.BatchSize, true, opts.WriteAPI, nil); err != nil { + return catalog.RelationshipRebuildSummary{}, err + } + var total int64 + for _, count := range counts { + total += count + } + return catalog.RelationshipRebuildSummary{Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration), Rows: len(docs), EdgeCount: total, InvalidEdgeCount: audit.InvalidEdgeCount, Seconds: time.Since(started).Seconds()}, nil +} + +const relationshipObservationsAQL = ` +FOR d IN fhir_relationship_catalog + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths + FILTER @resource_types == null OR (d.from_type IN @resource_types AND d.to_type IN @resource_types) + COLLECT project = d.project, dataset_generation = d.dataset_generation, auth_resource_path = d.auth_resource_path, + storage_from_type = d.from_type, label = d.label, storage_to_type = d.to_type + AGGREGATE edge_count = SUM(d.edge_count) + SORT project, dataset_generation, auth_resource_path, storage_from_type, label, storage_to_type, edge_count + RETURN { project, dataset_generation, auth_resource_path, storage_from_type, label, storage_to_type, edge_count }` + +const relationshipCatalogStorageAQL = ` +FOR d IN fhir_relationship_catalog + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths + FILTER @from_type == null OR d.from_type == @from_type + COLLECT from_type = d.from_type, label = d.label, to_type = d.to_type + AGGREGATE edge_count = SUM(d.edge_count) + SORT from_type, edge_count DESC, label, to_type + RETURN { dataset_generation: @dataset_generation, from_type, label, to_type, edge_count }` +const relationshipCatalogBuilderAQL = ` +FOR d IN fhir_relationship_catalog + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths + FILTER @node_type == null OR d.to_type == @node_type + COLLECT from_type = d.to_type, label = d.label, to_type = d.from_type + AGGREGATE edge_count = SUM(d.edge_count) + SORT from_type, edge_count DESC, label, to_type + RETURN { dataset_generation: @dataset_generation, from_type, label, to_type, edge_count }` + +const relationshipClearAQL = ` +FOR d IN fhir_relationship_catalog + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths + REMOVE d IN fhir_relationship_catalog + RETURN 1` +const relationshipRebuildAQL = ` +FOR e IN fhir_edge + FILTER e.project == @project + FILTER e.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR e.auth_resource_path IN @auth_resource_paths + FILTER e.from_type IN @resource_types AND e.to_type IN @resource_types + COLLECT project = e.project, dataset_generation = e.dataset_generation, auth_resource_path = e.auth_resource_path, + from_type = e.from_type, label = e.label, to_type = e.to_type WITH COUNT INTO edge_count + RETURN { project, dataset_generation, auth_resource_path, from_type, label, to_type, edge_count }` +const relationshipAuditAQL = ` +FOR e IN fhir_edge + FILTER e.project == @project + FILTER e.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR e.auth_resource_path IN @auth_resource_paths + FILTER e.from_type NOT IN @resource_types OR e.to_type NOT IN @resource_types + COLLECT from_type = e.from_type, label = e.label, to_type = e.to_type WITH COUNT INTO edge_count + SORT edge_count DESC, from_type, label, to_type + RETURN { from_type, label, to_type, edge_count }` diff --git a/internal/catalog/arango/resources.go b/internal/catalog/arango/resources.go new file mode 100644 index 00000000..45bfd398 --- /dev/null +++ b/internal/catalog/arango/resources.go @@ -0,0 +1,117 @@ +package arango + +import ( + "context" + "fmt" + "sort" + + "github.com/calypr/loom/internal/catalog" +) + +func (s *Store) DiscoverExistingAuthResourcePaths(ctx context.Context, opts catalog.AuthResourcePathOptions) ([]string, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + result := make([]string, 0, 16) + err := s.client.QueryRows(ctx, existingAuthResourcePathsAQL, opts.CursorBatch, map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration)}, func(row map[string]any) error { + if value := stringValue(row["auth_resource_path"]); value != "" { + result = append(result, value) + } + return nil + }) + if err != nil { + return nil, err + } + sort.Strings(result) + return result, nil +} + +// DiscoverResourceInventory reads the concrete resource collections directly. +// Field profiler rows are deliberately not consulted: a populated field is +// not evidence that a resource collection is a valid row root. +func (s *Store) DiscoverResourceInventory(ctx context.Context, opts catalog.ResourceInventoryOptions) (catalog.ResourceInventoryResult, error) { + if opts.CursorBatch <= 0 { + opts.CursorBatch = 1000 + } + resourceTypes, diagnostics := evidenceResourceTypes(opts.ResourceTypes) + result := catalog.ResourceInventoryResult{Values: make([]catalog.ResourceInventoryObservation, 0, len(resourceTypes)), Available: true, Complete: true, Diagnostics: diagnostics} + vars := map[string]any{ + "project": opts.Project, + "dataset_generation": generation(opts.DatasetGeneration), + "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), + "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), + } + for _, resourceType := range resourceTypes { + collectionExists, err := s.client.CollectionExists(ctx, resourceType) + if err != nil { + err = fmt.Errorf("check resource collection %s: %w", resourceType, err) + result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable + result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "RESOURCE_INVENTORY_UNAVAILABLE", Message: err.Error(), ResourceType: resourceType}) + return result, err + } + if !collectionExists { + result.Values = append(result.Values, catalog.ResourceInventoryObservation{ + Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration), ResourceType: resourceType, DocumentCount: 0, + }) + continue + } + // Arango collection bind parameters use @@name in AQL and require the + // corresponding bind-var map key to retain one leading @. + vars["@resource_collection"] = resourceType + vars["resource_type"] = resourceType + var rowSeen bool + err = s.client.QueryRows(ctx, resourceInventoryAQL, opts.CursorBatch, vars, func(row map[string]any) error { + rowSeen = true + count, err := decodeInt64(row["document_count"]) + if err != nil { + return fmt.Errorf("decode inventory count for %s: %w", resourceType, err) + } + result.Values = append(result.Values, catalog.ResourceInventoryObservation{ + Project: stringValue(row["project"]), DatasetGeneration: stringValue(row["dataset_generation"]), AuthResourcePath: stringValue(row["auth_resource_path"]), ResourceType: resourceType, DocumentCount: count, + }) + return nil + }) + if err != nil { + result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable + result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "RESOURCE_INVENTORY_UNAVAILABLE", Message: err.Error(), ResourceType: resourceType}) + return result, err + } + // Arango's COUNT query always returns one row. A defensive fallback + // preserves a valid zero count when a compatible client omits it. + if !rowSeen { + result.Values = append(result.Values, catalog.ResourceInventoryObservation{Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration), ResourceType: resourceType, DocumentCount: 0}) + } + } + sort.Slice(result.Values, func(i, j int) bool { + return inventoryObservationLess(result.Values[i], result.Values[j]) + }) + result.Digest, _ = catalog.ResourceInventoryDigest(result.Values) + result.Status = evidenceStatus(result.Available, result.Complete, result.Truncated, len(result.Values)) + return result, nil +} + +// resourceInventoryAQL is executed once per concrete generated resource +// collection because Arango collection bind parameters cannot be safely +// interpolated from a caller-supplied type. Every identity predicate remains +// a bind variable, including the explicit restricted-empty authorization mode. +const resourceInventoryAQL = ` +FOR d IN @@resource_collection + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths + COLLECT WITH COUNT INTO document_count + RETURN { + project: @project, + dataset_generation: @dataset_generation, + resource_type: @resource_type, + document_count + }` + +const existingAuthResourcePathsAQL = ` +FOR d IN fhir_field_catalog + FILTER d.project == @project + FILTER d.dataset_generation == @dataset_generation + FILTER d.auth_resource_path != null AND d.auth_resource_path != "" + COLLECT auth_resource_path = d.auth_resource_path + SORT auth_resource_path + RETURN { auth_resource_path: auth_resource_path }` diff --git a/internal/catalog/arango/store.go b/internal/catalog/arango/store.go index bd327dbf..b9c9972c 100644 --- a/internal/catalog/arango/store.go +++ b/internal/catalog/arango/store.go @@ -3,22 +3,16 @@ package arango import ( "context" - "encoding/json" "fmt" - "sort" - "strconv" - "time" - "github.com/calypr/loom/internal/catalog" - fhirschema "github.com/calypr/loom/internal/fhir/schema" store "github.com/calypr/loom/internal/store/arango" ) type client interface { + store.RowQueryer + store.BatchInserter + store.AQLExecutor CollectionExists(context.Context, string) (bool, error) - QueryRows(context.Context, string, int, map[string]any, store.RowVisitor) error - InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error - ExecuteAQL(context.Context, string, map[string]any) error Bootstrap(context.Context, store.BootstrapSpec) error } @@ -32,758 +26,3 @@ func New(client client) (*Store, error) { } return &Store{client: client}, nil } - -func (s *Store) DiscoverFields(ctx context.Context, opts catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - vars := map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration), "resource_type": nil, "pivot_only": opts.PivotOnly, "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted)} - if opts.ResourceType != "" { - vars["resource_type"] = opts.ResourceType - } - rows := []catalog.PopulatedField{} - err := s.client.QueryRows(ctx, populatedFieldsAQL, opts.CursorBatch, vars, func(row map[string]any) error { - var out catalog.PopulatedField - if err := decode(row, &out); err != nil { - return err - } - resourceType, ok := fhirschema.ConcreteResourceType(out.ResourceType) - if !ok { - return nil - } - out.ResourceType = resourceType - rows = append(rows, out) - return nil - }) - return rows, err -} - -func (s *Store) DiscoverReferences(ctx context.Context, opts catalog.PopulatedReferenceOptions) ([]catalog.PopulatedReference, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - mode := opts.Mode - if mode == "" { - mode = catalog.TraversalModeStorage - } - vars := map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration), "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted)} - query := relationshipCatalogStorageAQL - if mode == catalog.TraversalModeBuilder { - query = relationshipCatalogBuilderAQL - vars["node_type"] = nil - if opts.NodeType != "" { - vars["node_type"] = opts.NodeType - } - } else { - vars["from_type"] = nil - if opts.FromType != "" { - vars["from_type"] = opts.FromType - } - } - rows := make([]catalog.PopulatedReference, 0, 64) - err := s.client.QueryRows(ctx, query, opts.CursorBatch, vars, func(row map[string]any) error { - fromType, fromOK := fhirschema.ConcreteResourceType(stringValue(row["from_type"])) - toType, toOK := fhirschema.ConcreteResourceType(stringValue(row["to_type"])) - if !fromOK || !toOK { - return nil - } - if _, ok := fhirschema.LookupTraversal(fromType, stringValue(row["label"]), toType); !ok { - return nil - } - count, err := decodeInt64(row["edge_count"]) - if err != nil { - return fmt.Errorf("decode edge_count for %s/%s/%s: %w", fromType, stringValue(row["label"]), toType, err) - } - rows = append(rows, catalog.PopulatedReference{DatasetGeneration: stringValue(row["dataset_generation"]), FromType: fromType, Label: stringValue(row["label"]), ToType: toType, EdgeCount: count}) - return nil - }) - return rows, err -} - -func (s *Store) AuditRelationshipEdges(ctx context.Context, opts catalog.RelationshipAuditOptions) (catalog.RelationshipAuditSummary, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - vars := map[string]any{ - "project": opts.Project, - "dataset_generation": generation(opts.DatasetGeneration), - "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), - "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), - "resource_types": fhirschema.ResourceTypes(), - } - summary := catalog.RelationshipAuditSummary{Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration)} - err := s.client.QueryRows(ctx, relationshipAuditAQL, opts.CursorBatch, vars, func(row map[string]any) error { - count, err := decodeInt64(row["edge_count"]) - if err != nil { - return err - } - summary.InvalidEdgeCount += count - summary.InvalidRelations = append(summary.InvalidRelations, catalog.InvalidRelationship{ - FromType: stringValue(row["from_type"]), - Label: stringValue(row["label"]), - ToType: stringValue(row["to_type"]), - EdgeCount: count, - }) - return nil - }) - if err != nil { - return catalog.RelationshipAuditSummary{}, err - } - return summary, nil -} - -func (s *Store) DiscoverExistingAuthResourcePaths(ctx context.Context, opts catalog.AuthResourcePathOptions) ([]string, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - result := make([]string, 0, 16) - err := s.client.QueryRows(ctx, existingAuthResourcePathsAQL, opts.CursorBatch, map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration)}, func(row map[string]any) error { - if value := stringValue(row["auth_resource_path"]); value != "" { - result = append(result, value) - } - return nil - }) - if err != nil { - return nil, err - } - sort.Strings(result) - return result, nil -} - -// DiscoverResourceInventory reads the concrete resource collections directly. -// Field profiler rows are deliberately not consulted: a populated field is -// not evidence that a resource collection is a valid row root. -func (s *Store) DiscoverResourceInventory(ctx context.Context, opts catalog.ResourceInventoryOptions) (catalog.ResourceInventoryResult, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - resourceTypes, diagnostics := evidenceResourceTypes(opts.ResourceTypes) - result := catalog.ResourceInventoryResult{Values: make([]catalog.ResourceInventoryObservation, 0, len(resourceTypes)), Available: true, Complete: true, Diagnostics: diagnostics} - vars := map[string]any{ - "project": opts.Project, - "dataset_generation": generation(opts.DatasetGeneration), - "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), - "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), - } - for _, resourceType := range resourceTypes { - collectionExists, err := s.client.CollectionExists(ctx, resourceType) - if err != nil { - err = fmt.Errorf("check resource collection %s: %w", resourceType, err) - result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable - result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "RESOURCE_INVENTORY_UNAVAILABLE", Message: err.Error(), ResourceType: resourceType}) - return result, err - } - if !collectionExists { - result.Values = append(result.Values, catalog.ResourceInventoryObservation{ - Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration), ResourceType: resourceType, DocumentCount: 0, - }) - continue - } - // Arango collection bind parameters use @@name in AQL and require the - // corresponding bind-var map key to retain one leading @. - vars["@resource_collection"] = resourceType - vars["resource_type"] = resourceType - var rowSeen bool - err = s.client.QueryRows(ctx, resourceInventoryAQL, opts.CursorBatch, vars, func(row map[string]any) error { - rowSeen = true - count, err := decodeInt64(row["document_count"]) - if err != nil { - return fmt.Errorf("decode inventory count for %s: %w", resourceType, err) - } - result.Values = append(result.Values, catalog.ResourceInventoryObservation{ - Project: stringValue(row["project"]), DatasetGeneration: stringValue(row["dataset_generation"]), AuthResourcePath: stringValue(row["auth_resource_path"]), ResourceType: resourceType, DocumentCount: count, - }) - return nil - }) - if err != nil { - result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable - result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "RESOURCE_INVENTORY_UNAVAILABLE", Message: err.Error(), ResourceType: resourceType}) - return result, err - } - // Arango's COUNT query always returns one row. A defensive fallback - // preserves a valid zero count when a compatible client omits it. - if !rowSeen { - result.Values = append(result.Values, catalog.ResourceInventoryObservation{Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration), ResourceType: resourceType, DocumentCount: 0}) - } - } - sort.Slice(result.Values, func(i, j int) bool { - return inventoryObservationLess(result.Values[i], result.Values[j]) - }) - result.Digest, _ = catalog.ResourceInventoryDigest(result.Values) - result.Status = evidenceStatus(result.Available, result.Complete, result.Truncated, len(result.Values)) - return result, nil -} - -// DiscoverRelationshipObservations returns both storage and Builder views of -// each aggregate. The catalog stores physical fhir_edge direction; Builder -// usually traverses its inverse, so dropping the storage provenance here is a -// correctness bug rather than a presentation detail. -func (s *Store) DiscoverRelationshipObservations(ctx context.Context, opts catalog.RelationshipObservationOptions) (catalog.RelationshipObservationResult, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - resourceTypes, diagnostics := evidenceResourceTypes(opts.ResourceTypes) - result := catalog.RelationshipObservationResult{Values: []catalog.RelationshipObservation{}, Available: true, Complete: true, Diagnostics: diagnostics} - vars := map[string]any{ - "project": opts.Project, - "dataset_generation": generation(opts.DatasetGeneration), - "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), - "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), - } - if len(opts.ResourceTypes) > 0 { - vars["resource_types"] = resourceTypes - } else { - vars["resource_types"] = nil - } - counts := make(map[relationshipObservationKey]int64) - var queryErr error - err := s.client.QueryRows(ctx, relationshipObservationsAQL, opts.CursorBatch, vars, func(row map[string]any) error { - storageFrom := stringValue(row["storage_from_type"]) - label := stringValue(row["label"]) - storageTo := stringValue(row["storage_to_type"]) - count, err := decodeInt64(row["edge_count"]) - if err != nil { - return fmt.Errorf("decode relationship count for %s/%s/%s: %w", storageFrom, label, storageTo, err) - } - from, fromOK := fhirschema.ConcreteResourceType(storageFrom) - to, toOK := fhirschema.ConcreteResourceType(storageTo) - if !fromOK || !toOK { - result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "INVALID_RELATIONSHIP_ENDPOINT", Message: "relationship endpoint is not a concrete generated FHIR resource", StorageFromType: storageFrom, Label: label, StorageToType: storageTo, Count: count}) - return nil - } - observation, ok, diagnostic := relationshipObservation(stringValue(row["project"]), stringValue(row["dataset_generation"]), stringValue(row["auth_resource_path"]), from, label, to, count) - if !ok { - result.Diagnostics = append(result.Diagnostics, diagnostic) - return nil - } - key := relationshipObservationKey{project: observation.Project, generation: observation.DatasetGeneration, auth: observation.AuthResourcePath, storageFrom: observation.StorageFromType, label: observation.Label, storageTo: observation.StorageToType, builderFrom: observation.BuilderFromType, builderTo: observation.BuilderToType, builderDirection: observation.BuilderDirection} - counts[key] += observation.EdgeCount - return nil - }) - queryErr = err - if queryErr != nil { - result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable - result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "RELATIONSHIP_EVIDENCE_UNAVAILABLE", Message: queryErr.Error()}) - return result, queryErr - } - for key, count := range counts { - result.Values = append(result.Values, catalog.RelationshipObservation{Project: key.project, DatasetGeneration: key.generation, AuthResourcePath: key.auth, FromType: key.builderFrom, Label: key.label, ToType: key.builderTo, EdgeCount: count, StorageFromType: key.storageFrom, StorageToType: key.storageTo, StorageDirection: "OUTBOUND", BuilderFromType: key.builderFrom, BuilderToType: key.builderTo, BuilderDirection: key.builderDirection}) - } - sort.Slice(result.Values, func(i, j int) bool { return relationshipLess(result.Values[i], result.Values[j]) }) - result.Digest, _ = catalog.RelationshipObservationDigest(result.Values) - result.Status = evidenceStatus(result.Available, result.Complete, result.Truncated, len(result.Values)) - return result, nil -} - -// DiscoverFieldEnrichment reads profiler rows as optional, fail-closed -// enrichment. A successful empty result is valid evidence; a query failure or -// bounded distinct-value set is explicitly unavailable/incomplete. -func (s *Store) DiscoverFieldEnrichment(ctx context.Context, opts catalog.FieldEnrichmentOptions) (catalog.FieldEnrichmentResult, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - resourceTypes, diagnostics := evidenceResourceTypes(opts.ResourceTypes) - result := catalog.FieldEnrichmentResult{Values: []catalog.FieldEnrichmentObservation{}, Available: true, Complete: true, Diagnostics: diagnostics} - vars := map[string]any{ - "project": opts.Project, - "dataset_generation": generation(opts.DatasetGeneration), - "auth_resource_paths": append([]string(nil), opts.AuthResourcePaths...), - "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), - } - if len(opts.ResourceTypes) > 0 { - vars["resource_types"] = resourceTypes - } else { - vars["resource_types"] = nil - } - err := s.client.QueryRows(ctx, fieldEnrichmentAQL, opts.CursorBatch, vars, func(row map[string]any) error { - var field catalog.PopulatedField - if err := decode(row, &field); err != nil { - return err - } - resourceType, ok := fhirschema.ConcreteResourceType(field.ResourceType) - if !ok { - result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "INVALID_FIELD_RESOURCE_TYPE", Message: "field enrichment resource type is not concrete/generated", ResourceType: field.ResourceType}) - return nil - } - field.ResourceType = resourceType - result.Values = append(result.Values, catalog.FieldEnrichmentObservation{Project: field.Project, DatasetGeneration: field.DatasetGeneration, AuthResourcePath: field.AuthResourcePath, ResourceType: field.ResourceType, Path: field.Path, Kind: field.Kind, DocCount: field.DocCount, SampleCount: field.SampleCount, DistinctValues: append([]string(nil), field.DistinctValues...), DistinctTruncated: field.DistinctTruncated, ExtensionValues: append([]catalog.ExtensionValueObservation(nil), field.ExtensionValues...), PivotCandidate: field.PivotCandidate, PivotKind: field.PivotKind, PivotColumns: append([]string(nil), field.PivotColumns...), PivotFamily: field.PivotFamily, PivotColumnSelect: field.PivotColumnSelect, PivotValueSelect: field.PivotValueSelect, PivotItemSource: field.PivotItemSource, PivotItemResourceType: field.PivotItemResourceType, PivotValueSelectors: append([]string(nil), field.PivotValueSelectors...)}) - // DistinctTruncated is per-field suggestion metadata. It is retained - // on the observation but does not make the complete enrichment artifact - // unusable; only an artifact-level marker can fail the snapshot. - if truncated, ok := row["enrichment_truncated"].(bool); ok && truncated { - result.Truncated = true - } - if complete, ok := row["enrichment_complete"].(bool); ok && !complete { - result.Complete = false - } - return nil - }) - if err != nil { - result.Available, result.Complete, result.Status = false, false, catalog.EvidenceUnavailable - result.Diagnostics = append(result.Diagnostics, catalog.EvidenceDiagnostic{Code: "FIELD_ENRICHMENT_UNAVAILABLE", Message: err.Error()}) - return result, err - } - if result.Truncated { - result.Complete = false - } - sort.Slice(result.Values, func(i, j int) bool { return fieldLess(result.Values[i], result.Values[j]) }) - result.Digest, _ = catalog.FieldEnrichmentDigest(result.Values) - result.Status = evidenceStatus(result.Available, result.Complete, result.Truncated, len(result.Values)) - return result, nil -} - -// Read* aliases make the capability boundary explicit while retaining the -// Discover* naming used by the pre-existing catalog reader. -func (s *Store) ReadResourceInventory(ctx context.Context, opts catalog.ResourceInventoryOptions) (catalog.ResourceInventoryResult, error) { - return s.DiscoverResourceInventory(ctx, opts) -} -func (s *Store) ReadRelationshipObservations(ctx context.Context, opts catalog.RelationshipObservationOptions) (catalog.RelationshipObservationResult, error) { - return s.DiscoverRelationshipObservations(ctx, opts) -} -func (s *Store) ReadFieldEnrichment(ctx context.Context, opts catalog.FieldEnrichmentOptions) (catalog.FieldEnrichmentResult, error) { - return s.DiscoverFieldEnrichment(ctx, opts) -} - -type relationshipObservationKey struct { - project, generation, auth string - storageFrom, label, storageTo string - builderFrom, builderTo, builderDirection string -} - -func evidenceResourceTypes(requested []string) ([]string, []catalog.EvidenceDiagnostic) { - if len(requested) == 0 { - return fhirschema.ResourceTypes(), nil - } - seen := make(map[string]struct{}, len(requested)) - diagnostics := make([]catalog.EvidenceDiagnostic, 0) - for _, requestedType := range requested { - canonical, ok := fhirschema.ConcreteResourceType(requestedType) - if !ok { - diagnostics = append(diagnostics, catalog.EvidenceDiagnostic{Code: "INVALID_RESOURCE_TYPE", Message: "requested resource type is not a concrete generated FHIR resource", ResourceType: requestedType}) - continue - } - seen[canonical] = struct{}{} - } - result := make([]string, 0, len(seen)) - for resourceType := range seen { - result = append(result, resourceType) - } - sort.Strings(result) - return result, diagnostics -} - -func inventoryObservationLess(a, b catalog.ResourceInventoryObservation) bool { - for _, pair := range [][2]string{{a.Project, b.Project}, {a.DatasetGeneration, b.DatasetGeneration}, {a.AuthResourcePath, b.AuthResourcePath}, {a.ResourceType, b.ResourceType}} { - if pair[0] != pair[1] { - return pair[0] < pair[1] - } - } - return a.DocumentCount < b.DocumentCount -} - -func relationshipLess(a, b catalog.RelationshipObservation) bool { - for _, pair := range [][2]string{{a.Project, b.Project}, {a.DatasetGeneration, b.DatasetGeneration}, {a.AuthResourcePath, b.AuthResourcePath}, {a.FromType, b.FromType}, {a.Label, b.Label}, {a.ToType, b.ToType}, {a.StorageFromType, b.StorageFromType}, {a.StorageToType, b.StorageToType}} { - if pair[0] != pair[1] { - return pair[0] < pair[1] - } - } - return a.EdgeCount < b.EdgeCount -} - -func fieldLess(a, b catalog.FieldEnrichmentObservation) bool { - for _, pair := range [][2]string{{a.Project, b.Project}, {a.DatasetGeneration, b.DatasetGeneration}, {a.AuthResourcePath, b.AuthResourcePath}, {a.ResourceType, b.ResourceType}, {a.Path, b.Path}, {a.Kind, b.Kind}} { - if pair[0] != pair[1] { - return pair[0] < pair[1] - } - } - return a.DocCount < b.DocCount -} - -func relationshipObservation(project, datasetGeneration, auth, storageFrom, label, storageTo string, count int64) (catalog.RelationshipObservation, bool, catalog.EvidenceDiagnostic) { - diagnostic := catalog.EvidenceDiagnostic{StorageFromType: storageFrom, Label: label, StorageToType: storageTo, Count: count} - if count <= 0 { - diagnostic.Code = "INVALID_RELATIONSHIP_COUNT" - diagnostic.Message = "relationship evidence count must be positive" - return catalog.RelationshipObservation{}, false, diagnostic - } - // Stored fhir_edge is always _from -> _to. Builder's normal projection is - // the inverse generated traversal, which is physically INBOUND from the - // stored target. Keep a direct generated traversal as a valid fallback for - // labels whose generated schema only exposes the storage orientation. - if _, found, err := fhirschema.ResolveCompilerTraversal(storageTo, label, storageFrom); found && err == nil { - return catalog.RelationshipObservation{Project: project, DatasetGeneration: datasetGeneration, AuthResourcePath: auth, FromType: storageTo, Label: label, ToType: storageFrom, EdgeCount: count, StorageFromType: storageFrom, StorageToType: storageTo, StorageDirection: "OUTBOUND", BuilderFromType: storageTo, BuilderToType: storageFrom, BuilderDirection: "INBOUND"}, true, diagnostic - } - if _, found, err := fhirschema.ResolveCompilerTraversal(storageFrom, label, storageTo); found && err == nil { - return catalog.RelationshipObservation{Project: project, DatasetGeneration: datasetGeneration, AuthResourcePath: auth, FromType: storageFrom, Label: label, ToType: storageTo, EdgeCount: count, StorageFromType: storageFrom, StorageToType: storageTo, StorageDirection: "OUTBOUND", BuilderFromType: storageFrom, BuilderToType: storageTo, BuilderDirection: "OUTBOUND"}, true, diagnostic - } - diagnostic.Code = "UNSUPPORTED_GENERATED_TRAVERSAL" - diagnostic.Message = "relationship endpoints and label are not a supported generated traversal" - return catalog.RelationshipObservation{}, false, diagnostic -} - -func evidenceStatus(available, complete, truncated bool, count int) catalog.EvidenceStatus { - if !available { - return catalog.EvidenceUnavailable - } - if truncated { - return catalog.EvidenceTruncated - } - if !complete { - return catalog.EvidenceIncomplete - } - if count == 0 { - return catalog.EvidenceEmpty - } - return catalog.EvidenceAvailable -} - -func (s *Store) WriteFieldCatalog(ctx context.Context, collection string, docs []catalog.FieldCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { - return catalog.WriteFieldCatalog(ctx, s.client, collection, docs, batchSize, overwrite, writeAPI, timings) -} -func (s *Store) WriteRelationshipCatalog(ctx context.Context, docs []catalog.RelationshipCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { - return catalog.WriteRelationshipCatalog(ctx, s.client, docs, batchSize, overwrite, writeAPI, timings) -} -func (s *Store) AccumulateRelationshipCatalog(ctx context.Context, docs []catalog.RelationshipCatalogDocument, timings map[string]float64) error { - return catalog.AccumulateRelationshipCatalog(ctx, s.client, docs, timings) -} -func (s *Store) RebuildRelationshipCatalog(ctx context.Context, opts catalog.RelationshipRebuildOptions) (catalog.RelationshipRebuildSummary, error) { - if opts.CursorBatch <= 0 { - opts.CursorBatch = 1000 - } - if opts.BatchSize <= 0 { - opts.BatchSize = 1000 - } - if opts.WriteAPI == "" { - opts.WriteAPI = "import" - } - started := time.Now() - if err := s.client.Bootstrap(ctx, store.BootstrapSpec{Collections: []store.CollectionSpec{{ - Name: catalog.RelationshipCatalogCollection, - Indexes: [][]string{ - {"project", "dataset_generation", "to_type"}, - {"project", "dataset_generation", "auth_resource_path", "to_type"}, - {"project", "dataset_generation", "from_type"}, - {"project", "dataset_generation", "auth_resource_path", "from_type"}, - }, - }}}); err != nil { - return catalog.RelationshipRebuildSummary{}, err - } - audit, err := s.AuditRelationshipEdges(ctx, catalog.RelationshipAuditOptions{Project: opts.Project, DatasetGeneration: opts.DatasetGeneration, AuthResourcePaths: opts.AuthResourcePaths, AuthResourcePathsUnrestricted: opts.AuthResourcePathsUnrestricted, CursorBatch: opts.CursorBatch}) - if err != nil { - return catalog.RelationshipRebuildSummary{}, err - } - vars := map[string]any{"project": opts.Project, "dataset_generation": generation(opts.DatasetGeneration), "auth_resource_paths": opts.AuthResourcePaths, "auth_resource_paths_unrestricted": catalog.EffectiveAuthResourcePathsUnrestricted(opts.AuthResourcePaths, opts.AuthResourcePathsUnrestricted), "resource_types": fhirschema.ResourceTypes()} - if err := s.client.QueryRows(ctx, relationshipClearAQL, opts.CursorBatch, vars, func(map[string]any) error { return nil }); err != nil { - return catalog.RelationshipRebuildSummary{}, err - } - counts := map[catalog.RelationshipKey]int64{} - if err := s.client.QueryRows(ctx, relationshipRebuildAQL, opts.CursorBatch, vars, func(row map[string]any) error { - count, err := decodeInt64(row["edge_count"]) - if err != nil { - return err - } - key := catalog.RelationshipKey{Project: stringValue(row["project"]), DatasetGeneration: stringValue(row["dataset_generation"]), AuthResourcePath: stringValue(row["auth_resource_path"]), FromType: stringValue(row["from_type"]), Label: stringValue(row["label"]), ToType: stringValue(row["to_type"])} - counts[key] = count - return nil - }); err != nil { - return catalog.RelationshipRebuildSummary{}, err - } - docs := catalog.RelationshipCatalogDocuments(counts) - if err := s.WriteRelationshipCatalog(ctx, docs, opts.BatchSize, true, opts.WriteAPI, nil); err != nil { - return catalog.RelationshipRebuildSummary{}, err - } - var total int64 - for _, count := range counts { - total += count - } - return catalog.RelationshipRebuildSummary{Project: opts.Project, DatasetGeneration: catalog.NormalizeDatasetGeneration(opts.DatasetGeneration), Rows: len(docs), EdgeCount: total, InvalidEdgeCount: audit.InvalidEdgeCount, Seconds: time.Since(started).Seconds()}, nil -} - -func generation(value string) any { - value = catalog.NormalizeDatasetGeneration(value) - if value == "" { - return nil - } - return value -} - -func stringValue(value any) string { - if value == nil { - return "" - } - return fmt.Sprint(value) -} -func decodeInt64(value any) (int64, error) { - switch typed := value.(type) { - case int: - return int64(typed), nil - case int32: - return int64(typed), nil - case int64: - return typed, nil - case float64: - return int64(typed), nil - case float32: - return int64(typed), nil - case json.Number: - n, err := typed.Int64() - return n, err - case string: - n, err := strconv.ParseInt(typed, 10, 64) - return n, err - case nil: - return 0, nil - } - return 0, fmt.Errorf("unsupported numeric type %T", value) -} - -func decodeBool(value any) (bool, error) { - if value == nil { - return false, nil - } - result, ok := value.(bool) - if !ok { - return false, fmt.Errorf("unsupported boolean type %T", value) - } - return result, nil -} - -func decodeStrings(value any) ([]string, error) { - if value == nil { - return nil, nil - } - switch values := value.(type) { - case []string: - return append([]string(nil), values...), nil - case []any: - result := make([]string, len(values)) - for i, value := range values { - text, ok := value.(string) - if !ok { - return nil, fmt.Errorf("unsupported slice item type %T at index %d", value, i) - } - result[i] = text - } - return result, nil - default: - return nil, fmt.Errorf("unsupported string slice type %T", value) - } -} - -func decodeExtensionValues(value any) ([]catalog.ExtensionValueObservation, error) { - if value == nil { - return nil, nil - } - items, ok := value.([]any) - if !ok { - if typed, ok := value.([]map[string]any); ok { - items = make([]any, len(typed)) - for i := range typed { - items[i] = typed[i] - } - } else { - return nil, fmt.Errorf("unsupported extension observation slice type %T", value) - } - } - result := make([]catalog.ExtensionValueObservation, 0, len(items)) - for i, item := range items { - row, ok := item.(map[string]any) - if !ok { - return nil, fmt.Errorf("unsupported extension observation type %T at index %d", item, i) - } - urlPath, err := decodeStrings(row["url_path"]) - if err != nil { - return nil, fmt.Errorf("extension observation URL path: %w", err) - } - result = append(result, catalog.ExtensionValueObservation{URL: stringValue(row["url"]), SourcePath: stringValue(row["source_path"]), ValuePath: stringValue(row["value_path"]), ValueType: stringValue(row["value_type"]), URLPath: urlPath}) - } - return result, nil -} - -func decode(row map[string]any, out *catalog.PopulatedField) error { - out.Project = stringValue(row["project"]) - out.DatasetGeneration = stringValue(row["dataset_generation"]) - out.AuthResourcePath = stringValue(row["auth_resource_path"]) - out.ResourceType = stringValue(row["resource_type"]) - out.Path = stringValue(row["path"]) - out.Kind = stringValue(row["kind"]) - var err error - if out.DocCount, err = decodeInt64(row["doc_count"]); err != nil { - return fmt.Errorf("decode field row %s/%s doc_count: %w", out.ResourceType, out.Path, err) - } - var sampleCount int64 - if sampleCount, err = decodeInt64(row["sample_count"]); err != nil { - return fmt.Errorf("decode field row %s/%s sample_count: %w", out.ResourceType, out.Path, err) - } - out.SampleCount = int(sampleCount) - if out.DistinctTruncated, err = decodeBool(row["distinct_truncated"]); err != nil { - return fmt.Errorf("decode field row %s/%s distinct_truncated: %w", out.ResourceType, out.Path, err) - } - if out.PivotCandidate, err = decodeBool(row["pivot_candidate"]); err != nil { - return fmt.Errorf("decode field row %s/%s pivot_candidate: %w", out.ResourceType, out.Path, err) - } - if out.DistinctValues, err = decodeStrings(row["distinct_values"]); err != nil { - return fmt.Errorf("decode field row %s/%s distinct_values: %w", out.ResourceType, out.Path, err) - } - if out.PivotColumns, err = decodeStrings(row["pivot_columns"]); err != nil { - return fmt.Errorf("decode field row %s/%s pivot_columns: %w", out.ResourceType, out.Path, err) - } - if out.PivotValueSelectors, err = decodeStrings(row["pivot_value_selectors"]); err != nil { - return fmt.Errorf("decode field row %s/%s pivot_value_selectors: %w", out.ResourceType, out.Path, err) - } - if out.ExtensionValues, err = decodeExtensionValues(row["extension_values"]); err != nil { - return fmt.Errorf("decode field row %s/%s extension_values: %w", out.ResourceType, out.Path, err) - } - out.PivotKind = stringValue(row["pivot_kind"]) - out.PivotFamily = stringValue(row["pivot_family"]) - out.PivotColumnSelect = stringValue(row["pivot_column_selector"]) - out.PivotValueSelect = stringValue(row["pivot_value_selector"]) - out.PivotItemSource = stringValue(row["pivot_item_source"]) - out.PivotItemResourceType = stringValue(row["pivot_item_resource_type"]) - return nil -} - -const populatedFieldsAQL = ` -FOR d IN fhir_field_catalog - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths - FILTER @resource_type == null OR d.resource_type == @resource_type - FILTER @pivot_only == false OR d.pivot_candidate == true - SORT d.resource_type, d.doc_count DESC, d.path - RETURN { - project: d.project, - dataset_generation: d.dataset_generation, - auth_resource_path: d.auth_resource_path, - resource_type: d.resource_type, - path: d.path, - kind: d.kind, - doc_count: d.doc_count, - sample_count: d.sample_count, - distinct_values: d.distinct_values, - distinct_truncated: d.distinct_truncated, - extension_values: d.extension_values, - pivot_candidate: d.pivot_candidate, - pivot_kind: d.pivot_kind, - pivot_columns: d.pivot_columns, - pivot_family: d.pivot_family, - pivot_column_selector: d.pivot_column_selector, - pivot_value_selector: d.pivot_value_selector, - pivot_item_source: d.pivot_item_source, - pivot_item_resource_type: d.pivot_item_resource_type, - pivot_value_selectors: d.pivot_value_selectors - }` - -// resourceInventoryAQL is executed once per concrete generated resource -// collection because Arango collection bind parameters cannot be safely -// interpolated from a caller-supplied type. Every identity predicate remains -// a bind variable, including the explicit restricted-empty authorization mode. -const resourceInventoryAQL = ` -FOR d IN @@resource_collection - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths - COLLECT WITH COUNT INTO document_count - RETURN { - project: @project, - dataset_generation: @dataset_generation, - resource_type: @resource_type, - document_count - }` - -const relationshipObservationsAQL = ` -FOR d IN fhir_relationship_catalog - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths - FILTER @resource_types == null OR (d.from_type IN @resource_types AND d.to_type IN @resource_types) - COLLECT project = d.project, dataset_generation = d.dataset_generation, auth_resource_path = d.auth_resource_path, - storage_from_type = d.from_type, label = d.label, storage_to_type = d.to_type - AGGREGATE edge_count = SUM(d.edge_count) - SORT project, dataset_generation, auth_resource_path, storage_from_type, label, storage_to_type, edge_count - RETURN { project, dataset_generation, auth_resource_path, storage_from_type, label, storage_to_type, edge_count }` - -const fieldEnrichmentAQL = ` -FOR d IN fhir_field_catalog - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths - FILTER @resource_types == null OR d.resource_type IN @resource_types - SORT d.resource_type, d.path, d.auth_resource_path, d.doc_count - RETURN { - project: d.project, - dataset_generation: d.dataset_generation, - auth_resource_path: d.auth_resource_path, - resource_type: d.resource_type, - path: d.path, - kind: d.kind, - doc_count: d.doc_count, - sample_count: d.sample_count, - distinct_values: d.distinct_values, - distinct_truncated: d.distinct_truncated, - extension_values: d.extension_values, - pivot_candidate: d.pivot_candidate, - pivot_kind: d.pivot_kind, - pivot_columns: d.pivot_columns, - pivot_family: d.pivot_family, - pivot_column_selector: d.pivot_column_selector, - pivot_value_selector: d.pivot_value_selector, - pivot_item_source: d.pivot_item_source, - pivot_item_resource_type: d.pivot_item_resource_type, - pivot_value_selectors: d.pivot_value_selectors, - enrichment_complete: d.enrichment_complete, - enrichment_truncated: d.enrichment_truncated - }` -const relationshipCatalogStorageAQL = ` -FOR d IN fhir_relationship_catalog - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths - FILTER @from_type == null OR d.from_type == @from_type - COLLECT from_type = d.from_type, label = d.label, to_type = d.to_type - AGGREGATE edge_count = SUM(d.edge_count) - SORT from_type, edge_count DESC, label, to_type - RETURN { dataset_generation: @dataset_generation, from_type, label, to_type, edge_count }` -const relationshipCatalogBuilderAQL = ` -FOR d IN fhir_relationship_catalog - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths - FILTER @node_type == null OR d.to_type == @node_type - COLLECT from_type = d.to_type, label = d.label, to_type = d.from_type - AGGREGATE edge_count = SUM(d.edge_count) - SORT from_type, edge_count DESC, label, to_type - RETURN { dataset_generation: @dataset_generation, from_type, label, to_type, edge_count }` -const existingAuthResourcePathsAQL = ` -FOR d IN fhir_field_catalog - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER d.auth_resource_path != null AND d.auth_resource_path != "" - COLLECT auth_resource_path = d.auth_resource_path - SORT auth_resource_path - RETURN { auth_resource_path: auth_resource_path }` -const relationshipClearAQL = ` -FOR d IN fhir_relationship_catalog - FILTER d.project == @project - FILTER d.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR d.auth_resource_path IN @auth_resource_paths - REMOVE d IN fhir_relationship_catalog - RETURN 1` -const relationshipRebuildAQL = ` -FOR e IN fhir_edge - FILTER e.project == @project - FILTER e.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR e.auth_resource_path IN @auth_resource_paths - FILTER e.from_type IN @resource_types AND e.to_type IN @resource_types - COLLECT project = e.project, dataset_generation = e.dataset_generation, auth_resource_path = e.auth_resource_path, - from_type = e.from_type, label = e.label, to_type = e.to_type WITH COUNT INTO edge_count - RETURN { project, dataset_generation, auth_resource_path, from_type, label, to_type, edge_count }` -const relationshipAuditAQL = ` -FOR e IN fhir_edge - FILTER e.project == @project - FILTER e.dataset_generation == @dataset_generation - FILTER @auth_resource_paths_unrestricted == true OR e.auth_resource_path IN @auth_resource_paths - FILTER e.from_type NOT IN @resource_types OR e.to_type NOT IN @resource_types - COLLECT from_type = e.from_type, label = e.label, to_type = e.to_type WITH COUNT INTO edge_count - SORT edge_count DESC, from_type, label, to_type - RETURN { from_type, label, to_type, edge_count }` diff --git a/internal/catalog/write_persist.go b/internal/catalog/write_persist.go deleted file mode 100644 index ea45daaf..00000000 --- a/internal/catalog/write_persist.go +++ /dev/null @@ -1,126 +0,0 @@ -package catalog - -import ( - "context" - "encoding/json" - "time" - - "github.com/bytedance/sonic" -) - -type aqlExecutor interface { - ExecuteAQL(context.Context, string, map[string]interface{}) error -} - -func WriteFieldCatalog(ctx context.Context, client interface { - InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error -}, collection string, docs []FieldCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { - if len(docs) == 0 { - return nil - } - - start := time.Now() - rawDocs := make([]json.RawMessage, 0, len(docs)) - for _, doc := range docs { - data, err := sonic.ConfigFastest.Marshal(&doc) - if err != nil { - return err - } - rawDocs = append(rawDocs, json.RawMessage(data)) - } - timings["field_catalog_marshal"] += time.Since(start).Seconds() - - for i := 0; i < len(rawDocs); i += batchSize { - end := i + batchSize - if end > len(rawDocs) { - end = len(rawDocs) - } - insertStart := time.Now() - if err := client.InsertBatchRaw(ctx, collection, rawDocs[i:end], overwrite, writeAPI); err != nil { - return err - } - timings["field_catalog_insert"] += time.Since(insertStart).Seconds() - } - return nil -} - -// WriteRelationshipCatalog persists committed edge cardinalities for a load. -func WriteRelationshipCatalog(ctx context.Context, client interface { - InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error -}, docs []RelationshipCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { - if len(docs) == 0 { - return nil - } - if batchSize <= 0 { - batchSize = 1000 - } - start := time.Now() - rawDocs := make([]json.RawMessage, 0, len(docs)) - for _, doc := range docs { - data, err := sonic.ConfigFastest.Marshal(&doc) - if err != nil { - return err - } - rawDocs = append(rawDocs, json.RawMessage(data)) - } - if timings != nil { - timings["relationship_catalog_marshal"] += time.Since(start).Seconds() - } - for i := 0; i < len(rawDocs); i += batchSize { - end := i + batchSize - if end > len(rawDocs) { - end = len(rawDocs) - } - insertStart := time.Now() - if err := client.InsertBatchRaw(ctx, RelationshipCatalogCollection, rawDocs[i:end], overwrite, writeAPI); err != nil { - return err - } - if timings != nil { - timings["relationship_catalog_insert"] += time.Since(insertStart).Seconds() - } - } - return nil -} - -// AccumulateRelationshipCatalog adds edge counts to the existing unversioned -// resource catalog without replacing counts written by other resource files. -func AccumulateRelationshipCatalog(ctx context.Context, client aqlExecutor, docs []RelationshipCatalogDocument, timings map[string]float64) error { - if len(docs) == 0 { - return nil - } - rows := make([]map[string]any, 0, len(docs)) - for _, doc := range docs { - rows = append(rows, map[string]any{ - "_key": doc.Key, - "project": doc.Project, - "dataset_generation": datasetGenerationBindValue(doc.DatasetGeneration), - "auth_resource_path": doc.AuthResourcePath, - "from_type": doc.FromType, - "label": doc.Label, - "to_type": doc.ToType, - "edge_count": doc.EdgeCount, - }) - } - start := time.Now() - const query = ` -FOR d IN @docs - UPSERT { _key: d._key } - INSERT d - UPDATE { edge_count: OLD.edge_count + d.edge_count } - IN fhir_relationship_catalog -` - if err := client.ExecuteAQL(ctx, query, map[string]any{"docs": rows}); err != nil { - return err - } - if timings != nil { - timings["relationship_catalog_accumulate"] += time.Since(start).Seconds() - } - return nil -} - -func datasetGenerationBindValue(generation string) any { - if generation == "" { - return nil - } - return generation -} diff --git a/internal/dataframe/compiler/capability/probe.go b/internal/dataframe/compiler/capability/probe.go index ab7dc7e5..8f6e32a0 100644 --- a/internal/dataframe/compiler/capability/probe.go +++ b/internal/dataframe/compiler/capability/probe.go @@ -91,27 +91,10 @@ type CandidateRequest struct { Chart *Chart } -// CostEstimate is intentionally descriptive rather than a hidden limit. A -// caller may install a policy that rejects an expensive finite request while -// still allowing arbitrary route depth when its own budget permits it. -type CostEstimate struct { - TraversalCount int - CandidateCount int -} - -// CostPolicy is an optional request-scoped safety hook. It is called before -// compilation and receives the context supplied to the probe. -type CostPolicy interface { - Allow(context.Context, CostEstimate) error -} - -// Options controls the explicitly authorized physical rewrite and cost -// policy. The default is the same conservative policy used by normal recipe -// compilation. +// Options controls the explicitly authorized physical rewrite. The default is +// the same conservative policy used by normal recipe compilation. type Options struct { - Policy ir.PhysicalOptimizationPolicy - Cost CostPolicy - CostLimit int + Policy ir.PhysicalOptimizationPolicy } func (o Options) normalized() Options { @@ -186,7 +169,8 @@ func ProbeRoot(ctx context.Context, request RootRequest, options ...Options) (Re if !ok { return Result{}, fmt.Errorf("resource type %q has no supported row grain", canonical) } - physical, rendered, err := compile(ctx, request.Scope, semantic.SemanticPlan{Version: 1, Project: request.Project, DatasetGeneration: request.DatasetGeneration, AuthResourcePaths: cloneStrings(request.AuthResourcePaths), AuthScopeMode: request.AuthScopeMode, Root: semantic.SemanticNode{Alias: "root", ResourceType: canonical}}, optionsFor(options)) + output := semantic.OutputPlan{RootResourceType: canonical, Root: semantic.SemanticNode{Alias: "root", ResourceType: canonical}} + physical, rendered, err := compile(ctx, request.Scope, output, optionsFor(options)) _ = physical if err != nil { return Result{}, err @@ -210,8 +194,8 @@ func ProbeTraversal(ctx context.Context, request TraversalRequest, options ...Op } toType, _ := fhirschema.ConcreteResourceType(request.ToResourceType) child := semantic.SemanticNode{Alias: routeAlias(request.Alias, 1), ResourceType: toType, EdgeLabel: request.EdgeLabel, MatchMode: request.MatchMode} - plan := semantic.SemanticPlan{Version: 1, Project: request.Project, DatasetGeneration: request.DatasetGeneration, AuthResourcePaths: cloneStrings(request.AuthResourcePaths), AuthScopeMode: request.AuthScopeMode, Root: semantic.SemanticNode{Alias: "root", ResourceType: rootType, Children: []semantic.SemanticNode{child}}} - physical, rendered, err := compile(ctx, request.Scope, plan, optionsFor(options)) + output := semantic.OutputPlan{RootResourceType: rootType, Root: semantic.SemanticNode{Alias: "root", ResourceType: rootType, Children: []semantic.SemanticNode{child}}} + physical, rendered, err := compile(ctx, request.Scope, output, optionsFor(options)) if err != nil { return Result{}, err } @@ -242,11 +226,6 @@ func ProbeCandidate(ctx context.Context, request CandidateRequest, options ...Op if err := contextErr(ctx); err != nil { return Result{}, err } - if optionsValue.Cost != nil { - if err := optionsValue.Cost.Allow(ctx, CostEstimate{TraversalCount: len(request.Route), CandidateCount: 1}); err != nil { - return Result{}, err - } - } fieldKind := prepared.fieldKind metadata := CandidateCapability{ResourceType: prepared.resourceType, FieldRef: prepared.fieldRef, Selector: prepared.selector.CanonicalPath(), FieldKind: fieldKind, Primitive: prepared.primitive, Cardinality: prepared.cardinality, Repeated: prepared.repeated} projectionModes := []spec.ProjectionMode{spec.ProjectionScalar, spec.ProjectionFirst, spec.ProjectionArray, spec.ProjectionDistinctArray} @@ -374,12 +353,17 @@ func compileCandidate(ctx context.Context, prepared preparedCandidate, mode spec if err := validateCandidateProjection(prepared, mode); err != nil { return ir.PhysicalPlan{}, Rendered{}, err } - field := semantic.SemanticField{Name: "candidate", FieldRef: prepared.fieldRef, Selector: prepared.selector, ValueMode: valueMode(mode), ExprType: expression.Type{Kind: expression.KindObject, Cardinality: expression.OptionalOne}} + fieldType := expression.Type{Kind: expression.KindObject, Cardinality: expression.OptionalOne} if prepared.primitive != fhirschema.PrimitiveUnknown { - field.ExprType.Kind = expressionKind(prepared.primitive) + fieldType.Kind = expressionKind(prepared.primitive) } if prepared.repeated { - field.ExprType.Cardinality = expression.Many + fieldType.Cardinality = expression.Many + } + field := semantic.SemanticField{ + Name: "candidate", FieldRef: prepared.fieldRef, + Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: prepared.selector.CanonicalPath()}), Type: fieldType}, + Projection: mode, } root := prepared.root target := &root @@ -401,8 +385,8 @@ func compileCandidate(ctx context.Context, prepared preparedCandidate, mode spec selector := prepared.selector target.Aggregates = []semantic.SemanticAggregate{{Name: "chart", Operation: string(chart.Operation), FieldRef: prepared.fieldRef, Selector: &selector}} } - plan := semantic.SemanticPlan{Version: 1, Root: root} - physical, rendered, err := compile(ctx, prepared.scope, plan, options) + output := semantic.OutputPlan{RootResourceType: root.ResourceType, Root: root} + physical, rendered, err := compile(ctx, prepared.scope, output, options) return physical, rendered, err } @@ -416,21 +400,18 @@ func validateCandidateProjection(prepared preparedCandidate, mode spec.Projectio return nil } -func compile(ctx context.Context, scope Scope, plan semantic.SemanticPlan, options Options) (ir.PhysicalPlan, Rendered, error) { +func compile(ctx context.Context, scope Scope, output semantic.OutputPlan, options Options) (ir.PhysicalPlan, Rendered, error) { if err := contextErr(ctx); err != nil { return ir.PhysicalPlan{}, Rendered{}, err } options = options.normalized() - if options.Cost != nil { - if err := options.Cost.Allow(ctx, CostEstimate{TraversalCount: countSemanticTraversals(plan.Root)}); err != nil { - return ir.PhysicalPlan{}, Rendered{}, err - } + context := semantic.ExecutionContext{ + Project: scope.Project, + DatasetGeneration: scope.DatasetGeneration, + AuthResourcePaths: cloneStrings(scope.AuthResourcePaths), + AuthScopeMode: scope.AuthScopeMode, } - plan.Project = scope.Project - plan.DatasetGeneration = scope.DatasetGeneration - plan.AuthResourcePaths = cloneStrings(scope.AuthResourcePaths) - plan.AuthScopeMode = scope.AuthScopeMode - physical, err := lower.BuildGenericPhysicalPlanWithPolicy(plan, options.Policy) + physical, err := lower.BuildGenericPhysicalPlanWithPolicy(output, context, options.Policy) if err != nil { return ir.PhysicalPlan{}, Rendered{}, err } @@ -606,19 +587,6 @@ func expressionKind(primitive fhirschema.PrimitiveKind) expression.ValueKind { } } -func valueMode(mode spec.ProjectionMode) string { - switch mode { - case spec.ProjectionFirst: - return "FIRST" - case spec.ProjectionArray: - return "ALL" - case spec.ProjectionDistinctArray: - return "DISTINCT" - default: - return "AUTO" - } -} - func defaultProjection(repeated bool) spec.ProjectionMode { if repeated { return spec.ProjectionFirst @@ -644,14 +612,6 @@ func findTraversal(plan ir.PhysicalPlan) (ir.PhysicalTraversal, bool) { return walk(plan.Operations) } -func countSemanticTraversals(node semantic.SemanticNode) int { - total := len(node.Children) - for _, child := range node.Children { - total += countSemanticTraversals(child) - } - return total -} - func contextErr(ctx context.Context) error { if ctx == nil { return nil diff --git a/internal/dataframe/compiler/compiled_query.go b/internal/dataframe/compiler/compiled_query.go index f661036e..4e94ec4a 100644 --- a/internal/dataframe/compiler/compiled_query.go +++ b/internal/dataframe/compiler/compiled_query.go @@ -17,8 +17,6 @@ type CompiledQuery struct { PlanMode string PlanProfile string TraversalCount int - FileSummaries bool - StudyLookup bool OptimizationRules []string RowIdentity *spec.RowIdentity Query string diff --git a/internal/dataframe/compiler/generic_physical_plan_test.go b/internal/dataframe/compiler/generic_physical_plan_test.go index 9980710a..27678b4f 100644 --- a/internal/dataframe/compiler/generic_physical_plan_test.go +++ b/internal/dataframe/compiler/generic_physical_plan_test.go @@ -7,15 +7,14 @@ import ( "github.com/calypr/loom/internal/dataframe/compiler/ir" "github.com/calypr/loom/internal/dataframe/compiler/lower" "github.com/calypr/loom/internal/dataframe/compiler/render/aql" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataframe/semantic" "github.com/calypr/loom/internal/dataframe/spec" ) func TestBuildGenericPhysicalPlanNavigationSkeleton(t *testing.T) { - semantic := semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - AuthResourcePaths: []string{"/programs/p1"}, + output := semantic.OutputPlan{ Root: semantic.SemanticNode{ Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ @@ -24,7 +23,7 @@ func TestBuildGenericPhysicalPlanNavigationSkeleton(t *testing.T) { }}, }, } - plan, err := buildGenericPhysicalPlan(semantic) + plan, err := lower.BuildGenericPhysicalPlanWithPolicy(output, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}, ir.DefaultPhysicalOptimizationPolicy()) if err != nil { t.Fatal(err) } @@ -67,7 +66,7 @@ func TestBuildGenericPhysicalPlanNavigationSkeleton(t *testing.T) { } func TestBuildGenericPhysicalPlanEmptyAuthScopeIsExplicit(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{Version: 1, Project: "p", Root: semantic.SemanticNode{Alias: "root", ResourceType: "Observation"}}) + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Observation"}}) if err != nil { t.Fatal(err) } @@ -78,12 +77,11 @@ func TestBuildGenericPhysicalPlanEmptyAuthScopeIsExplicit(t *testing.T) { func TestBuildAndRenderGenericPhysicalPlanOptionalChildFieldsAndFilters(t *testing.T) { wantID := "file-1" - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ - Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Patient", - Fields: []semantic.SemanticField{{Name: "file_id", FieldRef: "DocumentReference.id", Selector: spec.Selector{Steps: []spec.SelectorStep{{Field: "id"}}}, ValueMode: "FIRST"}}, - Filters: []spec.TypedFilter{{FieldRef: "DocumentReference.id", Selector: "id", FieldKind: spec.FilterString, Operator: spec.FilterEquals, Values: []spec.FilterValue{{Kind: spec.FilterString, String: &wantID}}}}, - }}}, + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ + Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Patient", + Fields: []semantic.SemanticField{{Name: "file_id", FieldRef: "DocumentReference.id", Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "id"})}, Projection: spec.ProjectionFirst}}, + Filters: []spec.TypedFilter{{FieldRef: "DocumentReference.id", Selector: "id", FieldKind: spec.FilterString, Operator: spec.FilterEquals, Values: []spec.FilterValue{{Kind: spec.FilterString, String: &wantID}}}}, + }}}, }) if err != nil { t.Fatal(err) @@ -109,21 +107,87 @@ func TestBuildAndRenderGenericPhysicalPlanOptionalChildFieldsAndFilters(t *testi } } +func TestBuildAndRenderGenericPhysicalPlanReducesDirectChildFieldsOnce(t *testing.T) { + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ + Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Patient", + Fields: []semantic.SemanticField{ + {Name: "file_id", FieldRef: "DocumentReference.id", Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "id"})}, Projection: spec.ProjectionFirst}, + {Name: "description", FieldRef: "DocumentReference.description", Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "description"})}, Projection: spec.ProjectionFirst}, + {Name: "titles", FieldRef: "DocumentReference.content[].attachment.title", Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "content[].attachment.title"})}, Projection: spec.ProjectionArray}, + {Name: "distinct_titles", FieldRef: "DocumentReference.content[].attachment.title", Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "content[].attachment.title"})}, Projection: spec.ProjectionDistinctArray}, + }, + }}}}) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(rendered.Query, "LET child_set_1_reduced = {") { + t.Fatalf("direct child fields were not reduced beside their set:\n%s", rendered.Query) + } + if strings.Contains(rendered.Query, "FOR __loom_prepared_value IN child_set_1") { + t.Fatalf("final child fields still rescan the relationship set:\n%s", rendered.Query) + } + if strings.Count(rendered.Query, "child_set_1_reduced.__loom_reduced_") != 4 { + t.Fatalf("final child fields do not read the reduction object exactly four times:\n%s", rendered.Query) + } + if !strings.Contains(rendered.Query, "SORTED_UNIQUE(FLATTEN(child_set_1[*].__loom_projection_") { + t.Fatalf("distinct child array was not reduced with sorted uniqueness:\n%s", rendered.Query) + } +} + +func TestCompileRecipeOutputPreservesMixedChildDependencyOrder(t *testing.T) { + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", + Fields: []semantic.SemanticField{{ + Name: "specimen_id", FieldRef: "Specimen.id", + Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "id"})}, + Projection: spec.ProjectionFirst, + }}, + Children: []semantic.SemanticNode{{ + Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", + }}, + }}, + }}) + if err != nil { + t.Fatal(err) + } + + compiled, err := CompileRecipeOutputWithPolicy(lower.CompiledRecipeOutput{ + Name: "patients", + RootResourceType: "Patient", + RowGrain: "patient", + Plan: plan, + }, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}, 25, ir.DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + + parentSet := strings.Index(compiled.Query, "LET child_set_1 =") + nestedTraversal := strings.Index(compiled.Query, "FOR __loom_physical_parent_1 IN child_set_1") + if parentSet < 0 || nestedTraversal < 0 || parentSet >= nestedTraversal { + t.Fatalf("mixed child dependencies rendered out of order:\n%s", compiled.Query) + } +} + func TestBuildAndRenderGenericPhysicalPlanNestedOptionalFieldsAndAggregates(t *testing.T) { id := "file-1" - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", Children: []semantic.SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - Children: []semantic.SemanticNode{{ - Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", - Fields: []semantic.SemanticField{{Name: "id", Selector: spec.Selector{Steps: []spec.SelectorStep{{Field: "id"}}}, ValueMode: "FIRST"}}, - Filters: []spec.TypedFilter{{FieldRef: "DocumentReference.id", Selector: "id", FieldKind: spec.FilterString, Operator: spec.FilterEquals, Values: []spec.FilterValue{{Kind: spec.FilterString, String: &id}}}}, - Aggregates: []semantic.SemanticAggregate{{Name: "count", Operation: "COUNT"}}, - }}, + Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", + Fields: []semantic.SemanticField{{Name: "id", Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "id"})}, Projection: spec.ProjectionFirst}}, + Filters: []spec.TypedFilter{{FieldRef: "DocumentReference.id", Selector: "id", FieldKind: spec.FilterString, Operator: spec.FilterEquals, Values: []spec.FilterValue{{Kind: spec.FilterString, String: &id}}}}, + Aggregates: []semantic.SemanticAggregate{{Name: "count", Operation: "COUNT"}}, }}, - }, + }}, + }, }) if err != nil { t.Fatal(err) @@ -137,7 +201,7 @@ func TestBuildAndRenderGenericPhysicalPlanNestedOptionalFieldsAndAggregates(t *t } for _, want := range []string{ "LET child_set_1 = UNIQUE((", - "@child_set_2_filter_1_value", "LENGTH(child_set_2)", "FOR __loom_prepared_value IN child_set_2", + "@child_set_2_filter_1_value", "LENGTH(child_set_2)", "LET child_set_2_reduced = {", } { if !strings.Contains(rendered.Query, want) { t.Fatalf("nested physical query missing %q:\n%s", want, rendered.Query) @@ -159,18 +223,17 @@ func TestBuildAndRenderGenericPhysicalPlanNestedOptionalFieldsAndAggregates(t *t func TestBuildAndRenderGenericPhysicalPlanAggregates(t *testing.T) { gender := spec.Selector{Steps: []spec.SelectorStep{{Field: "gender"}}} - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Aggregates: []semantic.SemanticAggregate{ - {Name: "patient_count", Operation: "COUNT"}, - {Name: "genders", Operation: "DISTINCT_VALUES", Selector: &gender}, - }, - Children: []semantic.SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - Aggregates: []semantic.SemanticAggregate{{Name: "count", Operation: "COUNT"}}, - }}, + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Aggregates: []semantic.SemanticAggregate{ + {Name: "patient_count", Operation: "COUNT"}, + {Name: "genders", Operation: "DISTINCT_VALUES", Selector: &gender}, }, + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", + Aggregates: []semantic.SemanticAggregate{{Name: "count", Operation: "COUNT"}}, + }}, + }, }) if err != nil { t.Fatal(err) @@ -192,12 +255,14 @@ func TestBuildAndRenderGenericPhysicalPlanAggregates(t *testing.T) { func TestBuildAndRenderGenericPhysicalPlanRepresentativeSlices(t *testing.T) { gender := spec.Selector{Steps: []spec.SelectorStep{{Field: "gender"}}} title := spec.Selector{Steps: []spec.SelectorStep{{Field: "content", Iterate: true}, {Field: "attachment"}, {Field: "title"}}} - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Slices: []semantic.SemanticSlice{{Name: "representative", Limit: 2, Predicate: &gender, PredicateEquals: "female", Fields: []semantic.SemanticField{{Name: "gender", Selector: gender, ValueMode: "FIRST"}}}}, - Children: []semantic.SemanticNode{{Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Patient", Slices: []semantic.SemanticSlice{{Name: "representative_files", Limit: 1, Fields: []semantic.SemanticField{{Name: "title", Selector: title, ValueMode: "FIRST"}}}}}}, - }, + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Slices: []semantic.SemanticSlice{{Name: "representative", Limit: 2, Predicate: &gender, PredicateEquals: "female", Fields: []semantic.SemanticField{testSemanticField("gender", gender, spec.ProjectionFirst)}}}, + Children: []semantic.SemanticNode{{ + Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Patient", + Slices: []semantic.SemanticSlice{{Name: "representative_files", Limit: 1, Fields: []semantic.SemanticField{testSemanticField("title", title, spec.ProjectionFirst)}}}, + }}, + }, }) if err != nil { t.Fatal(err) @@ -222,15 +287,14 @@ func TestBuildAndRenderGenericPhysicalPlanRepresentativeSlices(t *testing.T) { func TestBuildAndRenderGenericPhysicalPlanAggregatePredicates(t *testing.T) { status := spec.Selector{Steps: []spec.SelectorStep{{Field: "id"}}} gender := spec.Selector{Steps: []spec.SelectorStep{{Field: "gender"}}} - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Aggregates: []semantic.SemanticAggregate{{Name: "female_count", Operation: "COUNT", Predicate: &gender, PredicateEquals: "female"}}, - Children: []semantic.SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - Aggregates: []semantic.SemanticAggregate{{Name: "available_count", Operation: "COUNT_DISTINCT", Selector: &status, Predicate: &status, PredicateEquals: "available"}}, - }}, - }, + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Aggregates: []semantic.SemanticAggregate{{Name: "female_count", Operation: "COUNT", Predicate: &gender, PredicateEquals: "female"}}, + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", + Aggregates: []semantic.SemanticAggregate{{Name: "available_count", Operation: "COUNT_DISTINCT", Selector: &status, Predicate: &status, PredicateEquals: "available"}}, + }}, + }, }) if err != nil { t.Fatal(err) @@ -247,20 +311,50 @@ func TestBuildAndRenderGenericPhysicalPlanAggregatePredicates(t *testing.T) { } } +func TestBuildAndRenderGenericPhysicalPlanContainsAll(t *testing.T) { + typeCode := spec.Selector{Steps: []spec.SelectorStep{{Field: "type"}, {Field: "coding", Iterate: true}, {Field: "code"}}} + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", + Aggregates: []semantic.SemanticAggregate{{Name: "paired", Operation: "CONTAINS_ALL", Selector: &typeCode, RequiredValues: []string{"Tumor", "Normal"}}}, + }}, + }}) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(rendered.Query, "FILTER POSITION(FLATTEN(") || strings.Contains(rendered.Query, "POSITION(FLATTEN(") && strings.Contains(rendered.Query, ") != -1") { + t.Fatalf("CONTAINS_ALL does not use boolean POSITION semantics:\n%s", rendered.Query) + } + found := false + for key, value := range rendered.BindVars { + values, ok := value.([]string) + if strings.Contains(key, "required_values") && ok && len(values) == 2 && values[0] == "Tumor" && values[1] == "Normal" { + found = true + } + } + if !found { + t.Fatalf("required values were not bind-backed: %#v", rendered.BindVars) + } +} + func TestBuildGenericPhysicalPlanPreparesSelectorsAcrossRichConsumers(t *testing.T) { status := spec.Selector{Steps: []spec.SelectorStep{{Field: "id"}}} policy := ir.DefaultPhysicalOptimizationPolicy().WithRule(ir.PhysicalOptimizationRuleCompactProjection, false).WithRule(ir.PhysicalOptimizationRulePreparedSelectors, true) - plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []semantic.SemanticNode{{ - Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", - Aggregates: []semantic.SemanticAggregate{{Name: "status_count", Operation: "COUNT_DISTINCT", Selector: &status}}, - Pivots: []semantic.SemanticPivot{{Name: "status_values", ColumnSelector: status, ValueSelector: status, Columns: []string{"active", "resolved"}}}, - Slices: []semantic.SemanticSlice{{Name: "representative", Limit: 1, Fields: []semantic.SemanticField{{Name: "status", Selector: status}}}}, - }}, - }, - }, policy) + plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", + Aggregates: []semantic.SemanticAggregate{{Name: "status_count", Operation: "COUNT_DISTINCT", Selector: &status}}, + Pivots: []semantic.SemanticPivot{{Name: "status_values", ColumnSelector: status, ValueSelector: status, Columns: []string{"active", "resolved"}}}, + Slices: []semantic.SemanticSlice{{Name: "representative", Limit: 1, Fields: []semantic.SemanticField{testSemanticField("status", status, "")}}}, + }}, + }, + }, semantic.ExecutionContext{Project: "p"}, policy) if err != nil { t.Fatal(err) } @@ -336,16 +430,15 @@ func TestPreparedSelectorsPreserveFallbackExtraction(t *testing.T) { primary := spec.Selector{Steps: []spec.SelectorStep{{Field: "id"}}} fallback := spec.Selector{Steps: []spec.SelectorStep{{Field: "code"}}} policy := ir.DefaultPhysicalOptimizationPolicy().WithRule(ir.PhysicalOptimizationRulePreparedSelectors, true) - plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []semantic.SemanticNode{{ - Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", - Fields: []semantic.SemanticField{{Name: "status_with_fallback", Selector: primary, Fallbacks: []spec.Selector{fallback}}}, - Aggregates: []semantic.SemanticAggregate{{Name: "status_count", Operation: "COUNT_DISTINCT", Selector: &primary}}, - }}, - }, - }, policy) + plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", + Fields: []semantic.SemanticField{testSemanticFieldWithFallback("status_with_fallback", primary, fallback)}, + Aggregates: []semantic.SemanticAggregate{{Name: "status_count", Operation: "COUNT_DISTINCT", Selector: &primary}}, + }}, + }, + }, semantic.ExecutionContext{Project: "p"}, policy) if err != nil { t.Fatal(err) } @@ -379,9 +472,7 @@ func TestPreparedSelectorsPreserveFallbackExtraction(t *testing.T) { func TestBuildAndRenderGenericPhysicalPlanPivots(t *testing.T) { key := spec.Selector{Steps: []spec.SelectorStep{{Field: "code"}, {Field: "coding", Iterate: true}, {Field: "display"}}} value := spec.Selector{Steps: []spec.SelectorStep{{Field: "text"}}} - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{Alias: "root", ResourceType: "Condition", Pivots: []semantic.SemanticPivot{{Name: "lab_values", ColumnSelector: key, ValueSelector: value, Columns: []string{"female", "male"}}}}, - }) + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Condition", Pivots: []semantic.SemanticPivot{{Name: "lab_values", ColumnSelector: key, ValueSelector: value, Columns: []string{"female", "male"}}}}}) if err != nil { t.Fatal(err) } @@ -406,9 +497,7 @@ func TestBuildAndRenderGenericPhysicalPlanPivots(t *testing.T) { func TestChildPivotRetainsPayloadForDeferredEvaluation(t *testing.T) { code := spec.Selector{Steps: []spec.SelectorStep{{Field: "code"}, {Field: "coding", Iterate: true}, {Field: "display"}}} - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "p", + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{ Root: semantic.SemanticNode{ Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ @@ -458,11 +547,10 @@ func TestRenderGenericPivotFlattensRepeatedItemSource(t *testing.T) { codeText := spec.Selector{Steps: []spec.SelectorStep{{Field: "code"}, {Field: "text"}}} valueString := spec.Selector{Steps: []spec.SelectorStep{{Field: "valueString"}}} componentItems := spec.Selector{Steps: []spec.SelectorStep{{Field: "component", Iterate: true}}} - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{Alias: "root", ResourceType: "Observation", Pivots: []semantic.SemanticPivot{{ - Name: "component_values", ColumnSelector: codeText, ValueSelector: valueString, - ItemSource: componentItems, ItemResourceType: "ObservationComponent", Columns: []string{"Component"}, - }}}, + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Observation", Pivots: []semantic.SemanticPivot{{ + Name: "component_values", ColumnSelector: codeText, ValueSelector: valueString, + ItemSource: componentItems, ItemResourceType: "ObservationComponent", Columns: []string{"Component"}, + }}}, }) if err != nil { t.Fatal(err) @@ -488,7 +576,7 @@ func TestBuildGenericPhysicalPlanRejectsUnsupportedSemanticFeatures(t *testing.T } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - _, err := buildGenericPhysicalPlan(semantic.SemanticPlan{Version: 1, Project: "p", Root: test.root}) + _, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: test.root}) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("error = %v; want substring %q", err, test.want) } diff --git a/internal/dataframe/compiler/ir/clone.go b/internal/dataframe/compiler/ir/clone.go index 9a88701b..cdb59abb 100644 --- a/internal/dataframe/compiler/ir/clone.go +++ b/internal/dataframe/compiler/ir/clone.go @@ -1,10 +1,6 @@ package ir -import ( - "encoding/json" - - "github.com/calypr/loom/internal/dataframe/spec" -) +import "github.com/calypr/loom/internal/dataframe/spec" func cloneStrings(in []string) []string { if in == nil { @@ -62,6 +58,108 @@ func ClonePhysicalSubplan(subplan PhysicalSubplan) PhysicalSubplan { return clonePhysicalSubplan(subplan) } +// CanonicalExecutionPhysicalPlan returns the renderer-relevant plan contract. +// Compiler diagnostics and semantic source locations are deliberately removed: +// they explain a plan but do not change what the backend executes. +func CanonicalExecutionPhysicalPlan(plan PhysicalPlan) PhysicalPlan { + out := clonePhysicalPlan(plan) + out.Source = PhysicalSource{} + out.DeferredExpressionLets = nil + out.AppliedRules = nil + out.SharedTraversalCount = 0 + out.OptimizationPolicy = PhysicalOptimizationReport{} + out.RequiredMatchReuseCount = 0 + canonicalizePhysicalOperations(out.Operations) + return out +} + +func canonicalizePhysicalOperations(operations []PhysicalOperation) { + for index := range operations { + operation := &operations[index] + operation.Source = PhysicalSource{} + if operation.Set != nil { + canonicalizePhysicalSubplan(&operation.Set.Subplan) + } + if operation.Filter != nil && operation.Filter.Expression != nil { + canonicalizePhysicalPredicateExpression(operation.Filter.Expression) + } + if operation.ExpressionLet != nil { + canonicalizePhysicalExpression(&operation.ExpressionLet.Expression) + } + if operation.Unnest != nil { + canonicalizePhysicalExpression(&operation.Unnest.Expression) + } + if operation.Return != nil { + for projection := range operation.Return.Projections { + canonicalizePhysicalExpression(operation.Return.Projections[projection].Expression) + } + } + if operation.PathExtend != nil { + canonicalizePhysicalOperations(operation.PathExtend.Scope) + } + } +} + +func canonicalizePhysicalSubplan(subplan *PhysicalSubplan) { + if subplan == nil { + return + } + canonicalizePhysicalOperations(subplan.Operations) + canonicalizePhysicalExpression(&subplan.Return) +} + +func canonicalizePhysicalPredicateExpression(predicate *PhysicalPredicateExpression) { + if predicate == nil { + return + } + if predicate.Comparison != nil { + canonicalizePhysicalExpression(predicate.Comparison.LeftExpression) + } + canonicalizePhysicalSubplan(predicate.Exists) + for index := range predicate.Children { + canonicalizePhysicalPredicateExpression(&predicate.Children[index]) + } +} + +func canonicalizePhysicalExpression(expression *PhysicalExpression) { + if expression == nil { + return + } + if expression.Aggregate != nil { + canonicalizePhysicalExpression(expression.Aggregate.Value) + canonicalizePhysicalPredicateExpression(expression.Aggregate.Predicate) + } + if expression.Slice != nil { + canonicalizePhysicalPredicateExpression(expression.Slice.Predicate) + canonicalizePhysicalExpression(expression.Slice.Sort) + for index := range expression.Slice.Projections { + canonicalizePhysicalExpression(&expression.Slice.Projections[index].Expression) + } + } + if expression.KeyedMap != nil { + canonicalizePhysicalExpression(&expression.KeyedMap.Source) + canonicalizePhysicalExpression(&expression.KeyedMap.ItemKey) + canonicalizePhysicalExpression(&expression.KeyedMap.ItemValue) + for index := range expression.KeyedMap.ValueFallbacks { + canonicalizePhysicalExpression(&expression.KeyedMap.ValueFallbacks[index]) + } + } + if expression.KeySet != nil { + canonicalizePhysicalExpression(&expression.KeySet.Source) + canonicalizePhysicalExpression(&expression.KeySet.ItemKey) + } + if expression.Object != nil { + for index := range expression.Object.Fields { + canonicalizePhysicalExpression(&expression.Object.Fields[index].Expression) + } + } + if expression.Call != nil { + for index := range expression.Call.Args { + canonicalizePhysicalExpression(&expression.Call.Args[index]) + } + } +} + func clonePhysicalBindVars(bindVars map[string]any) map[string]any { if bindVars == nil { return nil @@ -105,6 +203,11 @@ func clonePhysicalOperation(operation PhysicalOperation) PhysicalOperation { projectionCopy.Fields = append([]PhysicalSetProjectionField(nil), operation.Set.Projection.Fields...) setCopy.Projection = &projectionCopy } + if operation.Set.Reduction != nil { + reductionCopy := *operation.Set.Reduction + reductionCopy.Fields = append([]PhysicalSetReductionField(nil), operation.Set.Reduction.Fields...) + setCopy.Reduction = &reductionCopy + } if operation.Set.Prepared != nil { preparedCopy := *operation.Set.Prepared preparedCopy.Fields = append([]PhysicalPreparedField(nil), operation.Set.Prepared.Fields...) @@ -123,10 +226,6 @@ func clonePhysicalOperation(operation PhysicalOperation) PhysicalOperation { for index, input := range operation.DerivedLet.Inputs { derivedCopy.Inputs[index] = clonePhysicalValue(input) } - if operation.DerivedLet.Expression != nil { - expression := clonePhysicalExpression(*operation.DerivedLet.Expression) - derivedCopy.Expression = &expression - } copy.DerivedLet = &derivedCopy } if operation.ExpressionLet != nil { @@ -269,13 +368,6 @@ func clonePhysicalExpression(expression PhysicalExpression) PhysicalExpression { } copy.Slice = &slice } - if expression.Lookup != nil { - lookup := *expression.Lookup - lookup.Source = clonePhysicalExpression(expression.Lookup.Source) - lookup.ItemKey = clonePhysicalExpression(expression.Lookup.ItemKey) - lookup.ItemValue = clonePhysicalExpression(expression.Lookup.ItemValue) - copy.Lookup = &lookup - } if expression.ObjectLookup != nil { lookup := *expression.ObjectLookup copy.ObjectLookup = &lookup @@ -314,20 +406,6 @@ func clonePhysicalExpression(expression PhysicalExpression) PhysicalExpression { return copy } -// PhysicalExpressionFingerprint returns a deterministic structural identity -// for optimizer reuse decisions. It intentionally includes typed bind keys -// and cardinality/null contracts but never bind values. -func PhysicalExpressionFingerprint(expression PhysicalExpression) (string, error) { - if err := validatePhysicalExpressionObjectCycles(expression); err != nil { - return "", err - } - encoded, err := json.Marshal(expression) - if err != nil { - return "", err - } - return string(encoded), nil -} - func clonePhysicalSubplan(subplan PhysicalSubplan) PhysicalSubplan { copy := subplan copy.Captures = cloneStrings(subplan.Captures) diff --git a/internal/dataframe/compiler/ir/lookup_test.go b/internal/dataframe/compiler/ir/lookup_test.go deleted file mode 100644 index e754fc47..00000000 --- a/internal/dataframe/compiler/ir/lookup_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package ir - -import "testing" - -func lookupExpression() PhysicalExpression { - value := func(path ...string) PhysicalExpression { - return PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalScalarCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "dynamic_item", Path: path}} - } - return PhysicalExpression{ - Kind: PhysicalLookupExpression, - Cardinality: PhysicalScalarCardinality, - NullBehavior: PhysicalPreserveNull, - Lookup: &PhysicalLookup{ - Source: PhysicalExpression{Kind: PhysicalValueExpression, Cardinality: PhysicalArrayCardinality, NullBehavior: PhysicalPreserveNull, Value: &PhysicalValue{Variable: "root", Path: []string{"items"}}}, - ItemVariable: "dynamic_item", - ItemKey: value("url"), - ItemValue: value("value"), - MatchBindKey: "dynamic_key", - }, - } -} - -func TestPhysicalLookupValidatesScopeAndBind(t *testing.T) { - plan := PhysicalPlan{Version: 1, BindVars: map[string]any{"collection": "Patient", "dynamic_key": "x"}, Operations: []PhysicalOperation{ - {Kind: PhysicalRootScanOp, RootScan: &PhysicalRootScan{Variable: "root", CollectionBindKey: "collection"}}, - {Kind: PhysicalReturnOp, Return: &PhysicalReturn{Projections: []PhysicalProjection{{Name: "value", Expression: func() *PhysicalExpression { value := lookupExpression(); return &value }()}}}}, - }} - if err := plan.Validate(); err != nil { - t.Fatal(err) - } - bad := lookupExpression() - bad.Lookup.ItemKey.Cardinality = PhysicalArrayCardinality - plan.Operations[1].Return.Projections[0].Expression = &bad - if err := plan.Validate(); err == nil { - t.Fatal("array-valued lookup key was accepted") - } -} - -func TestPhysicalLookupCloneAndFingerprint(t *testing.T) { - original := lookupExpression() - clone := ClonePhysicalExpression(original) - if clone.Lookup == original.Lookup || clone.Lookup.Source.Value == original.Lookup.Source.Value { - t.Fatal("lookup clone shares mutable payload") - } - left, err := PhysicalExpressionFingerprint(original) - if err != nil { - t.Fatal(err) - } - right, err := PhysicalExpressionFingerprint(clone) - if err != nil { - t.Fatal(err) - } - if left != right { - t.Fatalf("clone fingerprint differs: %s != %s", left, right) - } -} diff --git a/internal/dataframe/compiler/ir/physical_operations.go b/internal/dataframe/compiler/ir/physical_operations.go index b4138166..fdd3f0e5 100644 --- a/internal/dataframe/compiler/ir/physical_operations.go +++ b/internal/dataframe/compiler/ir/physical_operations.go @@ -30,6 +30,10 @@ type PhysicalSet struct { // SortByKey makes the set's node order part of physical semantics. Optional // relationship materialization must not rely on Arango traversal order. SortByKey bool + // Reduction contains set-level reductions for direct child projections. It + // is intentionally separate from the identity-bearing set because nested + // traversals still consume every matching child. + Reduction *PhysicalSetReduction Prepared *PhysicalPreparedSet } @@ -120,8 +124,43 @@ type PhysicalSetProjectionField struct { ResourceType string Selector spec.Selector ExecutionMode PhysicalSelectorExecutionMode + Demand PhysicalSelectorValueDemand } +// PhysicalSelectorValueDemand describes how much of a selector's result a +// projected set must retain. The zero value preserves every value so plans +// built without demand analysis remain conservative. +type PhysicalSelectorValueDemand string + +const ( + PhysicalSelectorAllValues PhysicalSelectorValueDemand = "" + PhysicalSelectorFirstValue PhysicalSelectorValueDemand = "FIRST_ONLY" +) + +// PhysicalSetReduction is the typed contract for reducing selector slots +// after a set has been materialized. The renderer owns the AQL spelling; the +// IR only describes which projected slot supplies each result and which +// cardinality-preserving reduction applies. +type PhysicalSetReduction struct { + Variable string + SourceSetVariable string + Fields []PhysicalSetReductionField +} + +type PhysicalSetReductionField struct { + Name string + SourceField string + Mode PhysicalSetReductionMode +} + +type PhysicalSetReductionMode string + +const ( + PhysicalSetReductionFirst PhysicalSetReductionMode = "FIRST" + PhysicalSetReductionAll PhysicalSetReductionMode = "ALL" + PhysicalSetReductionDistinct PhysicalSetReductionMode = "DISTINCT" +) + // PhysicalSetOutputField names the only stored properties that may survive a // compact set projection. The graph identity fields preserve nested traversal // and duplicate-edge semantics; payload is retained only when a downstream @@ -189,10 +228,9 @@ type PhysicalFilter struct { // PhysicalDerivedLet names a derived value. Operator is a compiler-owned // symbolic operation (for example UNIQUE or LENGTH), never raw AQL. type PhysicalDerivedLet struct { - Variable string - Operator string - Inputs []PhysicalValue - Expression *PhysicalExpression + Variable string + Operator string + Inputs []PhysicalValue } // PhysicalExpressionLet binds a deterministic expression once in the diff --git a/internal/dataframe/compiler/ir/physical_validation.go b/internal/dataframe/compiler/ir/physical_validation.go index e0d39ac9..1ada708a 100644 --- a/internal/dataframe/compiler/ir/physical_validation.go +++ b/internal/dataframe/compiler/ir/physical_validation.go @@ -2,7 +2,6 @@ package ir import ( "fmt" - "strings" ) @@ -106,6 +105,11 @@ func (p PhysicalPlan) Validate() error { if err := definePhysicalVariable(defined, set.Variable); err != nil { return fmt.Errorf("operation %d: %w", i, err) } + if set.Reduction != nil { + if err := definePhysicalVariable(defined, set.Reduction.Variable); err != nil { + return fmt.Errorf("operation %d set reduction: %w", i, err) + } + } if set.Prepared != nil { if err := definePhysicalVariable(defined, set.Prepared.Variable); err != nil { return fmt.Errorf("operation %d prepared set: %w", i, err) @@ -318,6 +322,11 @@ func validatePhysicalSet(set PhysicalSet, parent map[string]bool, bindVars map[s if err := validatePhysicalSelector(field.ResourceType, field.Selector); err != nil { return fmt.Errorf("set %q projection field %q selector: %w", set.Variable, field.Name, err) } + switch field.Demand { + case PhysicalSelectorAllValues, PhysicalSelectorFirstValue: + default: + return fmt.Errorf("set %q projection field %q has unsupported value demand %q", set.Variable, field.Name, field.Demand) + } } } if set.Output != nil { @@ -340,6 +349,39 @@ func validatePhysicalSet(set PhysicalSet, parent map[string]bool, bindVars map[s return fmt.Errorf("set %q compact output must retain _id and _key", set.Variable) } } + if set.Reduction != nil { + if !physicalVariablePattern.MatchString(set.Reduction.Variable) || set.Reduction.Variable == set.Variable { + return fmt.Errorf("set %q reduction variable %q is unsafe or shadows the set", set.Variable, set.Reduction.Variable) + } + if !physicalVariablePattern.MatchString(set.Reduction.SourceSetVariable) || set.Reduction.SourceSetVariable != set.Variable { + return fmt.Errorf("set %q reduction source %q must equal the owning set", set.Variable, set.Reduction.SourceSetVariable) + } + if set.Projection == nil { + return fmt.Errorf("set %q reduction requires a selector projection", set.Variable) + } + if len(set.Reduction.Fields) == 0 { + return fmt.Errorf("set %q reduction requires at least one field", set.Variable) + } + projectedFields := make(map[string]bool, len(set.Projection.Fields)) + for _, field := range set.Projection.Fields { + projectedFields[field.Name] = true + } + seen := make(map[string]bool, len(set.Reduction.Fields)) + for _, field := range set.Reduction.Fields { + if !physicalVariablePattern.MatchString(field.Name) || seen[field.Name] { + return fmt.Errorf("set %q reduction field %q is unsafe or duplicated", set.Variable, field.Name) + } + seen[field.Name] = true + if !physicalVariablePattern.MatchString(field.SourceField) || !projectedFields[field.SourceField] { + return fmt.Errorf("set %q reduction source field %q is not a projected selector slot", set.Variable, field.SourceField) + } + switch field.Mode { + case PhysicalSetReductionFirst, PhysicalSetReductionAll, PhysicalSetReductionDistinct: + default: + return fmt.Errorf("set %q reduction field %q has unsupported mode %q", set.Variable, field.Name, field.Mode) + } + } + } if set.Prepared != nil { prepared := set.Prepared if !physicalVariablePattern.MatchString(prepared.Variable) || !physicalVariablePattern.MatchString(prepared.SourceSetVariable) { @@ -582,14 +624,6 @@ func validatePhysicalFilter(filter PhysicalFilter, defined map[string]bool, bind } func validatePhysicalDerivedLet(derived PhysicalDerivedLet, defined map[string]bool, bindVars map[string]any) error { - legacy := strings.TrimSpace(derived.Operator) != "" || len(derived.Inputs) != 0 - rich := derived.Expression != nil - if legacy == rich { - return fmt.Errorf("derived LET requires exactly one legacy operation or expression") - } - if rich { - return validatePhysicalExpression(*derived.Expression, defined, bindVars) - } if strings.TrimSpace(derived.Operator) == "" { return fmt.Errorf("derived LET operator is required") } diff --git a/internal/dataframe/compiler/ir/physical_validation_expr.go b/internal/dataframe/compiler/ir/physical_validation_expr.go index f70fd660..cc3416ac 100644 --- a/internal/dataframe/compiler/ir/physical_validation_expr.go +++ b/internal/dataframe/compiler/ir/physical_validation_expr.go @@ -35,9 +35,6 @@ func validatePhysicalExpression(expression PhysicalExpression, defined map[strin if expression.Slice != nil { payloads++ } - if expression.Lookup != nil { - payloads++ - } if expression.ObjectLookup != nil { payloads++ } @@ -93,11 +90,6 @@ func validatePhysicalExpression(expression PhysicalExpression, defined map[strin return fmt.Errorf("expression payload does not match kind") } return validatePhysicalSlice(*expression.Slice, defined, bindVars) - case PhysicalLookupExpression: - if expression.Lookup == nil { - return fmt.Errorf("expression payload does not match kind") - } - return validatePhysicalLookup(*expression.Lookup, defined, bindVars) case PhysicalObjectLookupExpression: if expression.ObjectLookup == nil { return fmt.Errorf("expression payload does not match kind") @@ -139,39 +131,6 @@ func validatePhysicalExpression(expression PhysicalExpression, defined map[strin } } -func validatePhysicalLookup(lookup PhysicalLookup, defined map[string]bool, bindVars map[string]any) error { - if !physicalVariablePattern.MatchString(lookup.ItemVariable) { - return fmt.Errorf("lookup item variable %q is unsafe", lookup.ItemVariable) - } - if lookup.Source.Cardinality != PhysicalArrayCardinality { - return fmt.Errorf("lookup source expression must be array-valued, got %q", lookup.Source.Cardinality) - } - if err := validatePhysicalExpression(lookup.Source, defined, bindVars); err != nil { - return fmt.Errorf("lookup source: %w", err) - } - if lookup.ItemKey.Cardinality == PhysicalArrayCardinality { - return fmt.Errorf("lookup item key expression must be scalar") - } - itemDefined := make(map[string]bool, len(defined)+1) - for name, value := range defined { - itemDefined[name] = value - } - itemDefined[lookup.ItemVariable] = true - if err := validatePhysicalExpression(lookup.ItemKey, itemDefined, bindVars); err != nil { - return fmt.Errorf("lookup item key: %w", err) - } - if err := validatePhysicalExpression(lookup.ItemValue, itemDefined, bindVars); err != nil { - return fmt.Errorf("lookup item value: %w", err) - } - if strings.TrimSpace(lookup.MatchBindKey) == "" { - return fmt.Errorf("lookup match bind key is required") - } - if err := requireBind(bindVars, lookup.MatchBindKey); err != nil { - return fmt.Errorf("lookup match bind: %w", err) - } - return nil -} - func validatePhysicalKeyedMap(keyed PhysicalKeyedMap, defined map[string]bool, bindVars map[string]any) error { if keyed.Source.Cardinality != PhysicalArrayCardinality { return fmt.Errorf("keyed map source expression must be array-valued, got %q", keyed.Source.Cardinality) @@ -319,17 +278,6 @@ func validatePhysicalExpressionObjectCycles(expression PhysicalExpression) error } } } - if current.Lookup != nil { - if err := visitExpression(current.Lookup.Source); err != nil { - return err - } - if err := visitExpression(current.Lookup.ItemKey); err != nil { - return err - } - if err := visitExpression(current.Lookup.ItemValue); err != nil { - return err - } - } if current.KeyedMap != nil { if err := visitExpression(current.KeyedMap.Source); err != nil { return err @@ -376,12 +324,6 @@ func validatePhysicalExpressionObjectCycles(expression PhysicalExpression) error return err } } - case PhysicalDerivedLetOp: - if operation.DerivedLet != nil && operation.DerivedLet.Expression != nil { - if err := visitExpression(*operation.DerivedLet.Expression); err != nil { - return err - } - } case PhysicalSetOp: if operation.Set != nil { if err := visitSubplan(operation.Set.Subplan); err != nil { diff --git a/internal/dataframe/compiler/ir/physical_validation_refs.go b/internal/dataframe/compiler/ir/physical_validation_refs.go index 1ac96ab3..374b91a2 100644 --- a/internal/dataframe/compiler/ir/physical_validation_refs.go +++ b/internal/dataframe/compiler/ir/physical_validation_refs.go @@ -77,7 +77,7 @@ func validatePhysicalAggregate(aggregate PhysicalAggregate, defined map[string]b return err } switch aggregate.Operation { - case PhysicalCountAggregate, PhysicalCountDistinctAggregate, PhysicalExistsAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate: + case PhysicalCountAggregate, PhysicalCountDistinctAggregate, PhysicalExistsAggregate, PhysicalDistinctValuesAggregate, PhysicalMinAggregate, PhysicalMaxAggregate, PhysicalFirstAggregate, PhysicalContainsAllAggregate: default: return fmt.Errorf("unknown aggregate operation %q", aggregate.Operation) } @@ -95,6 +95,27 @@ func validatePhysicalAggregate(aggregate PhysicalAggregate, defined map[string]b return fmt.Errorf("aggregate predicate: %w", err) } } + if aggregate.Operation == PhysicalContainsAllAggregate { + if strings.TrimSpace(aggregate.RequiredValuesBindKey) == "" { + return fmt.Errorf("CONTAINS_ALL requires required values bind") + } + if err := requireBind(bindVars, aggregate.RequiredValuesBindKey); err != nil { + return err + } + values, ok := bindVars[aggregate.RequiredValuesBindKey].([]string) + if !ok || len(values) == 0 { + return fmt.Errorf("required values bind %q must be a non-empty []string", aggregate.RequiredValuesBindKey) + } + seen := map[string]bool{} + for _, value := range values { + if strings.TrimSpace(value) == "" || seen[value] { + return fmt.Errorf("required values bind %q contains an empty or duplicate value", aggregate.RequiredValuesBindKey) + } + seen[value] = true + } + } else if strings.TrimSpace(aggregate.RequiredValuesBindKey) != "" { + return fmt.Errorf("aggregate operation %q does not accept required values", aggregate.Operation) + } return nil } diff --git a/internal/dataframe/compiler/ir/physical_values.go b/internal/dataframe/compiler/ir/physical_values.go index 9cdd7209..6ead73af 100644 --- a/internal/dataframe/compiler/ir/physical_values.go +++ b/internal/dataframe/compiler/ir/physical_values.go @@ -36,16 +36,11 @@ const ( PhysicalValueExpression PhysicalExpressionKind = "VALUE" // PhysicalLiteralExpression is a typed bind-backed constant. Literals are // kept out of generated AQL so recipe authors cannot inject query source. - PhysicalLiteralExpression PhysicalExpressionKind = "LITERAL" - PhysicalExtractExpression PhysicalExpressionKind = "EXTRACT" - PhysicalAggregateExpression PhysicalExpressionKind = "AGGREGATE" - PhysicalPivotExpression PhysicalExpressionKind = "PIVOT_MAP" - PhysicalSliceExpression PhysicalExpressionKind = "SLICE" - // PhysicalLookupExpression performs one bounded key lookup over a - // collection-valued expression. It is backend-neutral and is useful for - // any frontend that freezes a finite set of key/value columns before - // execution; it is not a recipe-specific dynamic-map operation. - PhysicalLookupExpression PhysicalExpressionKind = "LOOKUP" + PhysicalLiteralExpression PhysicalExpressionKind = "LITERAL" + PhysicalExtractExpression PhysicalExpressionKind = "EXTRACT" + PhysicalAggregateExpression PhysicalExpressionKind = "AGGREGATE" + PhysicalPivotExpression PhysicalExpressionKind = "PIVOT_MAP" + PhysicalSliceExpression PhysicalExpressionKind = "SLICE" PhysicalObjectLookupExpression PhysicalExpressionKind = "OBJECT_LOOKUP" PhysicalKeyedMapExpression PhysicalExpressionKind = "KEYED_MAP" PhysicalObjectKeysExpression PhysicalExpressionKind = "OBJECT_KEYS" @@ -83,7 +78,6 @@ type PhysicalExpression struct { Aggregate *PhysicalAggregate Pivot *PhysicalPivotMap Slice *PhysicalSlice - Lookup *PhysicalLookup ObjectLookup *PhysicalObjectLookup KeyedMap *PhysicalKeyedMap ObjectKeys *PhysicalObjectKeys @@ -156,13 +150,15 @@ const ( PhysicalMinAggregate PhysicalAggregateOperation = "MIN" PhysicalMaxAggregate PhysicalAggregateOperation = "MAX" PhysicalFirstAggregate PhysicalAggregateOperation = "FIRST" + PhysicalContainsAllAggregate PhysicalAggregateOperation = "CONTAINS_ALL" ) type PhysicalAggregate struct { - Source PhysicalValue - Operation PhysicalAggregateOperation - Value *PhysicalExpression - Predicate *PhysicalPredicateExpression + Source PhysicalValue + Operation PhysicalAggregateOperation + Value *PhysicalExpression + Predicate *PhysicalPredicateExpression + RequiredValuesBindKey string } type PhysicalPivotMap struct { @@ -185,23 +181,6 @@ type PhysicalPivotMap struct { PreparedValue *PhysicalPreparedReference } -// PhysicalLookup is a typed, bounded lookup over one array-valued source. -// ItemVariable is introduced only while evaluating ItemKey and ItemValue; -// neither expression contains backend query text. MatchBindKey names the -// scalar bind containing the frozen key for this output projection. -// -// Source is intentionally an expression rather than a PhysicalValue because -// dynamic sources commonly select repeated values from a root payload. The -// optimizer can fingerprint identical sources and the renderer can keep the -// lookup shape identical across frontends. -type PhysicalLookup struct { - Source PhysicalExpression - ItemVariable string - ItemKey PhysicalExpression - ItemValue PhysicalExpression - MatchBindKey string -} - type PhysicalObjectLookup struct { ObjectVariable string KeyBindKey string diff --git a/internal/dataframe/compiler/ir/policy.go b/internal/dataframe/compiler/ir/policy.go index 60f12442..4e0a2cca 100644 --- a/internal/dataframe/compiler/ir/policy.go +++ b/internal/dataframe/compiler/ir/policy.go @@ -33,23 +33,17 @@ type PhysicalOptimizationPolicy struct { type PhysicalOptimizationRule string const ( - PhysicalOptimizationRuleTraversalSharing PhysicalOptimizationRule = "traversal_sharing" - PhysicalOptimizationRulePreparedSelectors PhysicalOptimizationRule = "prepared_selectors" - PhysicalOptimizationRuleNestedSharing PhysicalOptimizationRule = "nested_traversal_sharing" - PhysicalOptimizationRuleRichConsumerFusion PhysicalOptimizationRule = "rich_consumer_fusion" - PhysicalOptimizationRuleCompactProjection PhysicalOptimizationRule = "compact_set_projection" - PhysicalOptimizationRuleEndpointTraversal PhysicalOptimizationRule = "endpoint_traversal" - PhysicalOptimizationRuleKeyedMapSharing PhysicalOptimizationRule = "keyed_map_sharing" + PhysicalOptimizationRuleTraversalSharing PhysicalOptimizationRule = "traversal_sharing" + PhysicalOptimizationRulePreparedSelectors PhysicalOptimizationRule = "prepared_selectors" + PhysicalOptimizationRuleCompactProjection PhysicalOptimizationRule = "compact_set_projection" + PhysicalOptimizationRuleEndpointTraversal PhysicalOptimizationRule = "endpoint_traversal" ) var allPhysicalOptimizationRules = []PhysicalOptimizationRule{ PhysicalOptimizationRuleTraversalSharing, PhysicalOptimizationRulePreparedSelectors, - PhysicalOptimizationRuleNestedSharing, - PhysicalOptimizationRuleRichConsumerFusion, PhysicalOptimizationRuleCompactProjection, PhysicalOptimizationRuleEndpointTraversal, - PhysicalOptimizationRuleKeyedMapSharing, } // RuleEnabled resolves one rule without mutating the caller's policy. A @@ -64,7 +58,7 @@ func (policy PhysicalOptimizationPolicy) RuleEnabled(rule PhysicalOptimizationRu } } switch rule { - case PhysicalOptimizationRuleTraversalSharing, PhysicalOptimizationRuleCompactProjection, PhysicalOptimizationRuleEndpointTraversal, PhysicalOptimizationRuleKeyedMapSharing: + case PhysicalOptimizationRuleTraversalSharing, PhysicalOptimizationRuleCompactProjection, PhysicalOptimizationRuleEndpointTraversal: return true default: return false @@ -149,11 +143,8 @@ func DefaultPhysicalOptimizationPolicy() PhysicalOptimizationPolicy { }{ {name: "LOOM_PHYSICAL_RULE_TRAVERSAL_SHARING", rule: PhysicalOptimizationRuleTraversalSharing}, {name: "LOOM_PHYSICAL_RULE_PREPARED_SELECTORS", rule: PhysicalOptimizationRulePreparedSelectors}, - {name: "LOOM_PHYSICAL_RULE_NESTED_SHARING", rule: PhysicalOptimizationRuleNestedSharing}, - {name: "LOOM_PHYSICAL_RULE_RICH_FUSION", rule: PhysicalOptimizationRuleRichConsumerFusion}, {name: "LOOM_PHYSICAL_RULE_COMPACT_PROJECTION", rule: PhysicalOptimizationRuleCompactProjection}, {name: "LOOM_PHYSICAL_RULE_ENDPOINT_TRAVERSAL", rule: PhysicalOptimizationRuleEndpointTraversal}, - {name: "LOOM_PHYSICAL_RULE_KEYED_MAP_SHARING", rule: PhysicalOptimizationRuleKeyedMapSharing}, } { switch strings.ToLower(strings.TrimSpace(os.Getenv(setting.name))) { case "on", "1", "true", "enabled": diff --git a/internal/dataframe/compiler/lower/auth_scope.go b/internal/dataframe/compiler/lower/auth_scope.go index e91cc936..ae1645eb 100644 --- a/internal/dataframe/compiler/lower/auth_scope.go +++ b/internal/dataframe/compiler/lower/auth_scope.go @@ -20,6 +20,6 @@ func effectiveAuthScopeUnrestricted(paths []string, mode authscope.ReadScopeMode } } -func semanticAuthScopeUnrestricted(plan semantic.SemanticPlan) bool { - return effectiveAuthScopeUnrestricted(plan.AuthResourcePaths, plan.AuthScopeMode) +func semanticAuthScopeUnrestricted(context semantic.ExecutionContext) bool { + return effectiveAuthScopeUnrestricted(context.AuthResourcePaths, context.AuthScopeMode) } diff --git a/internal/dataframe/compiler/lower/child_set_selector_demand_test.go b/internal/dataframe/compiler/lower/child_set_selector_demand_test.go new file mode 100644 index 00000000..3e902162 --- /dev/null +++ b/internal/dataframe/compiler/lower/child_set_selector_demand_test.go @@ -0,0 +1,65 @@ +package lower + +import ( + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/spec" + fhirschema "github.com/calypr/loom/internal/fhir/schema" +) + +func TestProjectPhysicalChildSetRequiresProofBeforeFirstOnlyDemand(t *testing.T) { + selector := spec.Selector{Steps: []spec.SelectorStep{{Field: "content", Iterate: true}, {Field: "attachment"}, {Field: "title"}}} + extract := func(cardinality ir.PhysicalCardinality, selected spec.Selector) ir.PhysicalExpression { + return ir.PhysicalExpression{ + Kind: ir.PhysicalExtractExpression, Cardinality: cardinality, NullBehavior: ir.PhysicalPreserveNull, + Extract: &ir.PhysicalExtract{Source: ir.PhysicalValue{Variable: "children"}, ResourceType: "DocumentReference", Selector: selected}, + } + } + + tests := []struct { + name string + expression ir.PhysicalExpression + want ir.PhysicalSelectorValueDemand + }{ + {name: "first", expression: extract(ir.PhysicalScalarCardinality, selector), want: ir.PhysicalSelectorFirstValue}, + {name: "array", expression: extract(ir.PhysicalArrayCardinality, selector), want: ir.PhysicalSelectorAllValues}, + {name: "aggregate", expression: ir.PhysicalExpression{ + Kind: ir.PhysicalAggregateExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalEmptyOnNull, + Aggregate: &ir.PhysicalAggregate{Source: ir.PhysicalValue{Variable: "children"}, Operation: ir.PhysicalCountDistinctAggregate, Value: expressionPtr(extract(ir.PhysicalArrayCardinality, selector))}, + }, want: ir.PhysicalSelectorAllValues}, + {name: "filtered", expression: extract(ir.PhysicalScalarCardinality, spec.Selector{ + Steps: selector.Steps, Filter: &fhirschema.ContainsFilter{Field: "url", Needle: "proof"}, + }), want: ir.PhysicalSelectorAllValues}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + set := ir.PhysicalSet{Variable: "children"} + projectPhysicalChildSet(&set, "DocumentReference", []ir.PhysicalProjection{{Name: "value", Expression: &test.expression}}) + if set.Projection == nil || len(set.Projection.Fields) != 1 { + t.Fatalf("selector projection = %#v, want one field", set.Projection) + } + if got := set.Projection.Fields[0].Demand; got != test.want { + t.Fatalf("selector demand = %q, want %q", got, test.want) + } + }) + } + + t.Run("opaque rich consumer", func(t *testing.T) { + set := ir.PhysicalSet{Variable: "children"} + expression := ir.PhysicalExpression{ + Kind: ir.PhysicalCallExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, + Call: &ir.PhysicalCall{Name: "coalesce", Args: []ir.PhysicalExpression{extract(ir.PhysicalScalarCardinality, selector)}}, + } + projectPhysicalChildSet(&set, "DocumentReference", []ir.PhysicalProjection{ + {Name: "first", Expression: expressionPtr(extract(ir.PhysicalScalarCardinality, selector))}, + {Name: "derived", Expression: &expression}, + }) + if set.Projection != nil { + t.Fatalf("opaque rich consumer was compacted without a rewrite proof: %#v", set.Projection) + } + }) +} + +func expressionPtr(value ir.PhysicalExpression) *ir.PhysicalExpression { return &value } diff --git a/internal/dataframe/compiler/lower/child_sets.go b/internal/dataframe/compiler/lower/child_sets.go index c4d466d0..6f9f3d4c 100644 --- a/internal/dataframe/compiler/lower/child_sets.go +++ b/internal/dataframe/compiler/lower/child_sets.go @@ -10,7 +10,7 @@ import ( "github.com/calypr/loom/internal/dataframe/spec" ) -func buildOptionalChildPhysicalSet(physical *ir.PhysicalPlan, setIndex int, parent semantic.SemanticNode, parentVariable string, child semantic.SemanticNode, projectionPrefix string, policy ir.PhysicalOptimizationPolicy) (ir.PhysicalSet, []ir.PhysicalProjection, error) { +func buildOptionalChildPhysicalSet(physical *ir.PhysicalPlan, setIndex int, parent semantic.SemanticNode, parentVariable string, child semantic.SemanticNode, projectionPrefix string, policy ir.PhysicalOptimizationPolicy, bindings map[string]physicalSemanticBinding, lowerer semanticFieldProjectionLowerer) (ir.PhysicalSet, []ir.PhysicalProjection, error) { prefix := fmt.Sprintf("child_set_%d", setIndex) targetVariable := fmt.Sprintf("%s_node", prefix) edgeVariable := fmt.Sprintf("%s_edge", prefix) @@ -76,29 +76,20 @@ func buildOptionalChildPhysicalSet(physical *ir.PhysicalPlan, setIndex int, pare set := ir.PhysicalSet{Variable: prefix, Subplan: subplan, Unique: true, SortByKey: true, Output: output} projections := make([]ir.PhysicalProjection, 0, len(child.Fields)) for index, field := range child.Fields { - selection, err := semantic.ResolveSemanticField(child.ResourceType, child.Alias, index, field) + projection, err := lowerSemanticFieldProjection(physical, child, index, field, ir.PhysicalValue{Variable: set.Variable}, bindings, lowerer) if err != nil { return ir.PhysicalSet{}, nil, err } - cardinality, distinct := ir.PhysicalScalarCardinality, false - nullBehavior := ir.PhysicalPreserveNull - switch selection.Projection { - case spec.ProjectionArray: - cardinality, nullBehavior = ir.PhysicalArrayCardinality, ir.PhysicalEmptyOnNull - case spec.ProjectionDistinctArray: - cardinality, distinct, nullBehavior = ir.PhysicalArrayCardinality, true, ir.PhysicalEmptyOnNull - case spec.ProjectionScalar, spec.ProjectionFirst: - default: - return ir.PhysicalSet{}, nil, fmt.Errorf("child field %q has unsupported projection %q", field.Name, selection.Projection) - } - projections = append(projections, ir.PhysicalProjection{Name: projectionPrefix + "__" + field.Name, Expression: &ir.PhysicalExpression{Kind: ir.PhysicalExtractExpression, Cardinality: cardinality, NullBehavior: nullBehavior, Extract: &ir.PhysicalExtract{Source: ir.PhysicalValue{Variable: set.Variable}, ResourceType: child.ResourceType, Selector: selection.Selector, Fallbacks: append([]spec.Selector(nil), selection.Fallbacks...), Distinct: distinct, ExecutionMode: selectorExecutionMode(child.ResourceType, selection.Selector, selection.Fallbacks...)}}}) + projection.Name = projectionPrefix + "__" + field.Name + projections = append(projections, projection) } for _, aggregate := range child.Aggregates { expression, err := physicalAggregateExpression(physical, child.ResourceType, ir.PhysicalValue{Variable: set.Variable}, aggregate, true) if err != nil { return ir.PhysicalSet{}, nil, err } - projections = append(projections, ir.PhysicalProjection{Name: projectionPrefix + "__" + aggregate.Name, Expression: &expression}) + name := aggregateProjectionName(aggregate, projectionPrefix+"__") + projections = append(projections, ir.PhysicalProjection{Name: name, Expression: &expression}) } for _, pivot := range child.Pivots { pivotProjections, err := physicalPivotProjections(physical, child.ResourceType, ir.PhysicalValue{Variable: set.Variable}, pivot, projectionPrefix+"__") @@ -324,9 +315,22 @@ func projectPhysicalChildSet(set *ir.PhysicalSet, resourceType string, projectio return } selectors := map[string]spec.Selector{} + allValues := map[string]bool{} fallback := false - var collect func(*ir.PhysicalExpression) - collect = func(expression *ir.PhysicalExpression) { + var collect func(*ir.PhysicalExpression, bool) + var collectPredicate func(*ir.PhysicalPredicateExpression) + collectPredicate = func(predicate *ir.PhysicalPredicateExpression) { + if predicate == nil { + return + } + if predicate.Comparison != nil { + collect(predicate.Comparison.LeftExpression, false) + } + for index := range predicate.Children { + collectPredicate(&predicate.Children[index]) + } + } + collect = func(expression *ir.PhysicalExpression, firstValueConsumer bool) { if expression == nil { return } @@ -339,38 +343,53 @@ func projectPhysicalChildSet(set *ir.PhysicalSet, resourceType string, projectio fallback = true return } - selectors[physicalSelectorIdentity(expression.Extract.Selector)] = expression.Extract.Selector + key := physicalSelectorIdentity(expression.Extract.Selector) + selectors[key] = expression.Extract.Selector + if !firstValueConsumer || expression.Cardinality == ir.PhysicalArrayCardinality || expression.Extract.Selector.Filter != nil { + allValues[key] = true + } case ir.PhysicalAggregateExpression: if expression.Aggregate != nil { - collect(expression.Aggregate.Value) - if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { - collect(expression.Aggregate.Predicate.Comparison.LeftExpression) - } + collect(expression.Aggregate.Value, false) + collectPredicate(expression.Aggregate.Predicate) } case ir.PhysicalPivotExpression: if expression.Pivot != nil && expression.Pivot.Source.Variable == set.Variable { - selectors[physicalSelectorIdentity(expression.Pivot.KeySelector)] = expression.Pivot.KeySelector - selectors[physicalSelectorIdentity(expression.Pivot.ValueSelector)] = expression.Pivot.ValueSelector + if len(expression.Pivot.ValueFallbacks) != 0 { + fallback = true + return + } + key := physicalSelectorIdentity(expression.Pivot.KeySelector) + value := physicalSelectorIdentity(expression.Pivot.ValueSelector) + selectors[key] = expression.Pivot.KeySelector + selectors[value] = expression.Pivot.ValueSelector + allValues[key] = true + allValues[value] = true } case ir.PhysicalSliceExpression: if expression.Slice != nil { - if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { - collect(expression.Slice.Predicate.Comparison.LeftExpression) - } + collectPredicate(expression.Slice.Predicate) + collect(expression.Slice.Sort, false) for index := range expression.Slice.Projections { - collect(&expression.Slice.Projections[index].Expression) + collect(&expression.Slice.Projections[index].Expression, false) } } case ir.PhysicalObjectExpression: if expression.Object != nil { for index := range expression.Object.Fields { - collect(&expression.Object.Fields[index].Expression) + collect(&expression.Object.Fields[index].Expression, false) } } + case ir.PhysicalCallExpression: + fallback = true + case ir.PhysicalKeyedMapExpression: + fallback = true + case ir.PhysicalKeySetExpression: + fallback = true } } for index := range projections { - collect(projections[index].Expression) + collect(projections[index].Expression, true) } if fallback || len(selectors) == 0 { return @@ -384,10 +403,52 @@ func projectPhysicalChildSet(set *ir.PhysicalSet, resourceType string, projectio fieldBySelector := make(map[string]string, len(keys)) for index, key := range keys { name := fmt.Sprintf("__loom_projection_%d", index) - fields = append(fields, ir.PhysicalSetProjectionField{Name: name, ResourceType: resourceType, Selector: selectors[key], ExecutionMode: selectorExecutionMode(resourceType, selectors[key])}) fieldBySelector[key] = name + demand := ir.PhysicalSelectorFirstValue + if allValues[key] { + demand = ir.PhysicalSelectorAllValues + } + fields = append(fields, ir.PhysicalSetProjectionField{Name: name, ResourceType: resourceType, Selector: selectors[key], ExecutionMode: selectorExecutionMode(resourceType, selectors[key]), Demand: demand}) + } + reductionFields := make([]ir.PhysicalSetReductionField, 0) + for index := range projections { + projection := &projections[index] + if projection.Expression == nil || projection.Expression.Kind != ir.PhysicalExtractExpression || projection.Expression.Extract == nil { + continue + } + extract := projection.Expression.Extract + if extract.Source.Variable != set.Variable || extract.Source.BindKey != "" || len(extract.Source.Path) != 0 || len(extract.Fallbacks) != 0 { + continue + } + slot, ok := fieldBySelector[physicalSelectorIdentity(extract.Selector)] + if !ok { + continue + } + mode := ir.PhysicalSetReductionFirst + if projection.Expression.Cardinality == ir.PhysicalArrayCardinality { + mode = ir.PhysicalSetReductionAll + if projection.Expression.Extract.Distinct { + mode = ir.PhysicalSetReductionDistinct + } + } + name := fmt.Sprintf("__loom_reduced_%d", len(reductionFields)) + reductionFields = append(reductionFields, ir.PhysicalSetReductionField{Name: name, SourceField: slot, Mode: mode}) + projection.Value = ir.PhysicalValue{Variable: set.Variable + "_reduced", Path: []string{name}} + projection.Expression = nil } var rewrite func(*ir.PhysicalExpression) + var rewritePredicate func(*ir.PhysicalPredicateExpression) + rewritePredicate = func(predicate *ir.PhysicalPredicateExpression) { + if predicate == nil { + return + } + if predicate.Comparison != nil { + rewrite(predicate.Comparison.LeftExpression) + } + for index := range predicate.Children { + rewritePredicate(&predicate.Children[index]) + } + } rewrite = func(expression *ir.PhysicalExpression) { if expression == nil { return @@ -402,9 +463,7 @@ func projectPhysicalChildSet(set *ir.PhysicalSet, resourceType string, projectio case ir.PhysicalAggregateExpression: if expression.Aggregate != nil { rewrite(expression.Aggregate.Value) - if expression.Aggregate.Predicate != nil && expression.Aggregate.Predicate.Comparison != nil { - rewrite(expression.Aggregate.Predicate.Comparison.LeftExpression) - } + rewritePredicate(expression.Aggregate.Predicate) } case ir.PhysicalPivotExpression: if expression.Pivot != nil && expression.Pivot.Source.Variable == set.Variable { @@ -413,9 +472,8 @@ func projectPhysicalChildSet(set *ir.PhysicalSet, resourceType string, projectio } case ir.PhysicalSliceExpression: if expression.Slice != nil { - if expression.Slice.Predicate != nil && expression.Slice.Predicate.Comparison != nil { - rewrite(expression.Slice.Predicate.Comparison.LeftExpression) - } + rewritePredicate(expression.Slice.Predicate) + rewrite(expression.Slice.Sort) for index := range expression.Slice.Projections { rewrite(&expression.Slice.Projections[index].Expression) } @@ -432,6 +490,9 @@ func projectPhysicalChildSet(set *ir.PhysicalSet, resourceType string, projectio rewrite(projections[index].Expression) } set.Projection = &ir.PhysicalSetProjection{Fields: fields} + if len(reductionFields) != 0 { + set.Reduction = &ir.PhysicalSetReduction{Variable: set.Variable + "_reduced", SourceSetVariable: set.Variable, Fields: reductionFields} + } set.Output = nil } diff --git a/internal/dataframe/compiler/lower/graph_plan.go b/internal/dataframe/compiler/lower/graph_plan.go index 5c2c7cd9..bafe9c3f 100644 --- a/internal/dataframe/compiler/lower/graph_plan.go +++ b/internal/dataframe/compiler/lower/graph_plan.go @@ -17,57 +17,57 @@ const ( // BuildGraphPhysicalPlan lowers an explicit graph frontend onto the same // scoped root/traversal lowering used by dataframe recipes. Each route gets // its own path set, so sibling branches form independent unions. -func BuildGraphPhysicalPlan(plan semantic.SemanticPlan, limit int, policy ir.PhysicalOptimizationPolicy) (ir.PhysicalPlan, error) { +func BuildGraphPhysicalPlan(output semantic.OutputPlan, context semantic.ExecutionContext, limit int, policy ir.PhysicalOptimizationPolicy) (ir.PhysicalPlan, error) { if limit < 1 || limit > 10000 { return ir.PhysicalPlan{}, fmt.Errorf("graph limit must be between 1 and 10000") } - if strings.TrimSpace(plan.Project) == "" { + if strings.TrimSpace(context.Project) == "" { return ir.PhysicalPlan{}, fmt.Errorf("semantic plan project is required") } - plan.Root = normalizeGraphMatchModes(plan.Root) - if err := semantic.ValidateSemanticGraph(plan); err != nil { + output.Root = normalizeGraphMatchModes(output.Root) + if err := semantic.ValidateSemanticGraph(output.Root); err != nil { return ir.PhysicalPlan{}, err } - if countGraphTraversals(plan.Root) > maxGraphTraversalDeclarations { + if countGraphTraversals(output.Root) > maxGraphTraversalDeclarations { return ir.PhysicalPlan{}, fmt.Errorf("graph traversal declaration count exceeds %d", maxGraphTraversalDeclarations) } - if err := validateGraphNode(plan.Root, true); err != nil { + if err := validateGraphNode(output.Root, true); err != nil { return ir.PhysicalPlan{}, err } physical := ir.PhysicalPlan{ Version: 1, - Source: ir.PhysicalSource{SemanticNode: plan.Root.Alias, ResourceType: plan.Root.ResourceType}, + Source: ir.PhysicalSource{SemanticNode: output.Root.Alias, ResourceType: output.Root.ResourceType}, BindVars: map[string]any{ - "root_collection": plan.Root.ResourceType, - "project": plan.Project, - datasetGenerationBindKey: datasetGenerationBindValue(plan.DatasetGeneration), - "auth_resource_paths": append([]string(nil), plan.AuthResourcePaths...), - "auth_resource_paths_unrestricted": semanticAuthScopeUnrestricted(plan), + "root_collection": output.Root.ResourceType, + "project": context.Project, + datasetGenerationBindKey: datasetGenerationBindValue(context.DatasetGeneration), + "auth_resource_paths": append([]string(nil), context.AuthResourcePaths...), + "auth_resource_paths_unrestricted": semanticAuthScopeUnrestricted(context), "scope_allowed": true, graphLookaheadBindKey: limit + 1, }, Operations: []ir.PhysicalOperation{{ Kind: ir.PhysicalRootScanOp, - Source: ir.PhysicalSource{SemanticNode: plan.Root.Alias, ResourceType: plan.Root.ResourceType}, + Source: ir.PhysicalSource{SemanticNode: output.Root.Alias, ResourceType: output.Root.ResourceType}, RootScan: &ir.PhysicalRootScan{Variable: "root", CollectionBindKey: "root_collection"}, }}, } - physical.Operations = appendProjectScope(physical.Operations, []string{"root"}, "", plan.Root) - physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{"root"}, "", plan.Root) - physical.Operations = appendAuthScope(physical.Operations, []ir.PhysicalValue{{Variable: "root", Path: []string{"auth_resource_path"}}}, "root_scope_allowed", plan.Root) - if err := appendRootPhysicalFilters(&physical, plan.Root); err != nil { + physical.Operations = appendProjectScope(physical.Operations, []string{"root"}, "", output.Root) + physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{"root"}, "", output.Root) + physical.Operations = appendAuthScope(physical.Operations, []ir.PhysicalValue{{Variable: "root", Path: []string{"auth_resource_path"}}}, "root_scope_allowed", output.Root) + if err := appendRootPhysicalFilters(&physical, output.Root); err != nil { return ir.PhysicalPlan{}, err } - if err := appendRequiredTraversalMatchFilters(&physical, graphRequiredSemiJoinRoot(plan.Root)); err != nil { + if err := appendRequiredTraversalMatchFilters(&physical, graphRequiredSemiJoinRoot(output.Root)); err != nil { return ir.PhysicalPlan{}, err } physical.Operations = append(physical.Operations, ir.PhysicalOperation{ Kind: ir.PhysicalPathSeedOp, - Source: ir.PhysicalSource{SemanticNode: plan.Root.Alias, ResourceType: plan.Root.ResourceType}, + Source: ir.PhysicalSource{SemanticNode: output.Root.Alias, ResourceType: output.Root.ResourceType}, PathSeed: &ir.PhysicalPathSeed{Variable: "path_root", RouteOrder: 0, Node: ir.PhysicalPathNode{ - Alias: graphAlias(plan.Root, "root"), ResourceType: plan.Root.ResourceType, + Alias: graphAlias(output.Root, "root"), ResourceType: output.Root.ResourceType, Value: ir.PhysicalValue{Variable: "root"}, }}, }) @@ -77,8 +77,8 @@ func BuildGraphPhysicalPlan(plan semantic.SemanticPlan, limit int, policy ir.Phy // child. Once any REQUIRED route is declared, however, returning the root // itself would leak a path the caller did not ask for; required semi-joins // below still gate root membership. - pathSets := make([]string, 0, countGraphTraversals(plan.Root)+1) - if !graphHasRequiredTraversal(plan.Root) { + pathSets := make([]string, 0, countGraphTraversals(output.Root)+1) + if !graphHasRequiredTraversal(output.Root) { pathSets = append(pathSets, "path_root") } routeOrder := 1 @@ -132,7 +132,7 @@ func BuildGraphPhysicalPlan(plan semantic.SemanticPlan, limit int, policy ir.Phy } return nil } - if err := walk(plan.Root, "path_root"); err != nil { + if err := walk(output.Root, "path_root"); err != nil { return ir.PhysicalPlan{}, err } physical.Operations = append(physical.Operations, ir.PhysicalOperation{Kind: ir.PhysicalGraphReturnOp, GraphReturn: &ir.PhysicalGraphReturn{PathSets: pathSets, LimitBindKey: graphLookaheadBindKey}}) @@ -190,15 +190,13 @@ func CompileResolvedGraphPlan(resolved semantic.ResolvedRecipePlan, limit int, p return ir.PhysicalPlan{}, fmt.Errorf("graph query requires exactly one output, got %d", len(resolved.SemanticPlan.Outputs)) } output := resolved.SemanticPlan.Outputs[0] - plan := semantic.SemanticPlan{ - Version: 1, + context := semantic.ExecutionContext{ Project: resolved.SemanticPlan.Bindings.Project, DatasetGeneration: resolved.SemanticPlan.Bindings.DatasetGeneration, AuthResourcePaths: append([]string(nil), resolved.SemanticPlan.Bindings.AuthResourcePaths...), AuthScopeMode: resolved.SemanticPlan.Bindings.AuthScopeMode, - Root: output.Root, } - return BuildGraphPhysicalPlan(plan, limit, policy) + return BuildGraphPhysicalPlan(output, context, limit, policy) } func graphAlias(node semantic.SemanticNode, fallback string) string { diff --git a/internal/dataframe/compiler/lower/graph_plan_test.go b/internal/dataframe/compiler/lower/graph_plan_test.go index 79c7da9f..8890e971 100644 --- a/internal/dataframe/compiler/lower/graph_plan_test.go +++ b/internal/dataframe/compiler/lower/graph_plan_test.go @@ -11,7 +11,7 @@ import ( ) func TestBuildGraphPhysicalPlanRootOnly(t *testing.T) { - plan, err := BuildGraphPhysicalPlan(semantic.SemanticPlan{Version: 1, Project: "p", Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, 100, ir.DefaultPhysicalOptimizationPolicy()) + plan, err := BuildGraphPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "p"}, 100, ir.DefaultPhysicalOptimizationPolicy()) if err != nil { t.Fatalf("BuildGraphPhysicalPlan() error = %v", err) } @@ -25,7 +25,7 @@ func TestBuildGraphPhysicalPlanRootOnly(t *testing.T) { } func TestBuildGraphPhysicalPlanOneHopRequired(t *testing.T) { - plan, err := BuildGraphPhysicalPlan(semantic.SemanticPlan{Version: 1, Project: "p", Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", MatchMode: spec.TraversalMatchRequired}}}}, 100, ir.DefaultPhysicalOptimizationPolicy()) + plan, err := BuildGraphPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", MatchMode: spec.TraversalMatchRequired}}}}, semantic.ExecutionContext{Project: "p"}, 100, ir.DefaultPhysicalOptimizationPolicy()) if err != nil { t.Fatalf("BuildGraphPhysicalPlan() error = %v", err) } @@ -40,7 +40,7 @@ func TestBuildGraphPhysicalPlanOneHopRequired(t *testing.T) { func TestBuildGraphPhysicalPlanRootFallbackOnlyForOptionalBranches(t *testing.T) { base := semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", MatchMode: spec.TraversalMatchOptional}}} - optional, err := BuildGraphPhysicalPlan(semantic.SemanticPlan{Version: 1, Project: "p", Root: base}, 100, ir.DefaultPhysicalOptimizationPolicy()) + optional, err := BuildGraphPhysicalPlan(semantic.OutputPlan{Root: base}, semantic.ExecutionContext{Project: "p"}, 100, ir.DefaultPhysicalOptimizationPolicy()) if err != nil { t.Fatal(err) } @@ -48,7 +48,7 @@ func TestBuildGraphPhysicalPlanRootFallbackOnlyForOptionalBranches(t *testing.T) t.Fatalf("optional path sets = %#v, want root fallback", optional.Operations[len(optional.Operations)-1].GraphReturn.PathSets) } base.Children[0].MatchMode = spec.TraversalMatchRequired - required, err := BuildGraphPhysicalPlan(semantic.SemanticPlan{Version: 1, Project: "p", Root: base}, 100, ir.DefaultPhysicalOptimizationPolicy()) + required, err := BuildGraphPhysicalPlan(semantic.OutputPlan{Root: base}, semantic.ExecutionContext{Project: "p"}, 100, ir.DefaultPhysicalOptimizationPolicy()) if err != nil { t.Fatal(err) } diff --git a/internal/dataframe/compiler/lower/plan_orchestration.go b/internal/dataframe/compiler/lower/plan_orchestration.go index de0cf5ce..e5b14dd9 100644 --- a/internal/dataframe/compiler/lower/plan_orchestration.go +++ b/internal/dataframe/compiler/lower/plan_orchestration.go @@ -5,64 +5,69 @@ import ( "strings" "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/recipe" semanticpkg "github.com/calypr/loom/internal/dataframe/semantic" - fhirschema "github.com/calypr/loom/internal/fhir/schema" ) -// BuildGenericPhysicalPlanWithPolicy threads an explicit optimizer policy -// through physical construction so prepared-selector ablations happen before -// references to prepared variables are attached to projections. -func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, policy ir.PhysicalOptimizationPolicy) (ir.PhysicalPlan, error) { - if strings.TrimSpace(semantic.Project) == "" { +// BuildGenericPhysicalPlanWithPolicy lowers one canonical output with an +// explicit request execution context. Keeping these values separate prevents +// runtime provenance from being mistaken for semantic output structure. +func BuildGenericPhysicalPlanWithPolicy(output semanticpkg.OutputPlan, context semanticpkg.ExecutionContext, policy ir.PhysicalOptimizationPolicy) (ir.PhysicalPlan, error) { + return buildGenericPhysicalPlanWithPolicy(output, context, policy, nil) +} + +// buildGenericPhysicalPlanWithPolicy is the shared physical-plan builder. A +// field lowerer is supplied by frontends whose canonical semantic fields are +// richer than selector metadata (for example persisted recipe expressions). +// A nil lowerer retains the selector-only GraphQL/dataframe behavior. +func buildGenericPhysicalPlanWithPolicy(output semanticpkg.OutputPlan, context semanticpkg.ExecutionContext, policy ir.PhysicalOptimizationPolicy, lowerer semanticFieldProjectionLowerer) (ir.PhysicalPlan, error) { + if strings.TrimSpace(context.Project) == "" { return ir.PhysicalPlan{}, fmt.Errorf("semantic plan project is required") } - if err := semanticpkg.ValidateSemanticGraph(semantic); err != nil { - return ir.PhysicalPlan{}, err - } - if !fhirschema.ResourceExists(semantic.Root.ResourceType) { - return ir.PhysicalPlan{}, fmt.Errorf("root resource type %q is not represented by the generated FHIR schema", semantic.Root.ResourceType) - } - if err := validateGenericPhysicalNode(semantic.Root, true); err != nil { + if err := semanticpkg.ValidateSemanticGraph(output.Root); err != nil { return ir.PhysicalPlan{}, err } - physical := ir.PhysicalPlan{ Version: 1, Source: ir.PhysicalSource{ - SemanticNode: semantic.Root.Alias, - ResourceType: semantic.Root.ResourceType, + SemanticNode: output.Root.Alias, + ResourceType: output.Root.ResourceType, }, BindVars: map[string]any{ - "root_collection": semantic.Root.ResourceType, - "project": semantic.Project, - datasetGenerationBindKey: datasetGenerationBindValue(semantic.DatasetGeneration), - "auth_resource_paths": append([]string(nil), semantic.AuthResourcePaths...), - "auth_resource_paths_unrestricted": semanticAuthScopeUnrestricted(semantic), + "root_collection": output.Root.ResourceType, + "project": context.Project, + datasetGenerationBindKey: datasetGenerationBindValue(context.DatasetGeneration), + "auth_resource_paths": append([]string(nil), context.AuthResourcePaths...), + "auth_resource_paths_unrestricted": semanticAuthScopeUnrestricted(context), "scope_allowed": true, }, Operations: []ir.PhysicalOperation{ { Kind: ir.PhysicalRootScanOp, - Source: ir.PhysicalSource{SemanticNode: semantic.Root.Alias, ResourceType: semantic.Root.ResourceType}, + Source: ir.PhysicalSource{SemanticNode: output.Root.Alias, ResourceType: output.Root.ResourceType}, RootScan: &ir.PhysicalRootScan{Variable: "root", CollectionBindKey: "root_collection"}, }, }, } - physical.Operations = appendProjectScope(physical.Operations, []string{"root"}, "", semantic.Root) - physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{"root"}, "", semantic.Root) - physical.Operations = appendAuthScope(physical.Operations, []ir.PhysicalValue{{Variable: "root", Path: []string{"auth_resource_path"}}}, "root_scope_allowed", semantic.Root) - if err := appendRootPhysicalFilters(&physical, semantic.Root); err != nil { + physical.Operations = appendProjectScope(physical.Operations, []string{"root"}, "", output.Root) + physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{"root"}, "", output.Root) + physical.Operations = appendAuthScope(physical.Operations, []ir.PhysicalValue{{Variable: "root", Path: []string{"auth_resource_path"}}}, "root_scope_allowed", output.Root) + if err := appendRootPhysicalFilters(&physical, output.Root); err != nil { return ir.PhysicalPlan{}, err } - if err := appendRequiredTraversalMatchFilters(&physical, semantic.Root); err != nil { + if err := appendRequiredTraversalMatchFilters(&physical, output.Root); err != nil { return ir.PhysicalPlan{}, err } childSetIndex := 0 returnProjections := []ir.PhysicalProjection{} - var walk func(parent semanticpkg.SemanticNode, parentVariable, projectionPrefix string) error - walk = func(parent semanticpkg.SemanticNode, parentVariable, projectionPrefix string) error { + rootBindings := map[string]physicalSemanticBinding{ + "root": {ResourceType: output.Root.ResourceType, Source: ir.PhysicalValue{Variable: "root", Path: []string{"payload"}}}, + } + var walk func(parent semanticpkg.SemanticNode, parentVariable, projectionPrefix string, bindings map[string]physicalSemanticBinding) error + walk = func(parent semanticpkg.SemanticNode, parentVariable, projectionPrefix string, bindings map[string]physicalSemanticBinding) error { for _, child := range parent.Children { + childBindings := clonePhysicalSemanticBindings(bindings) if child.MatchMode.Required() { // Required routes are represented by the root semi-join emitted // above for membership, but they may still need a materialized @@ -72,16 +77,17 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic if physicalNodeNeedsMaterializedSet(child) { childSetIndex++ childProjectionPrefix := child.Alias - if projectionPrefix != "" { + if output.TraversalColumnNaming != recipe.TraversalColumnNamingAlias && projectionPrefix != "" { childProjectionPrefix = projectionPrefix + "__" + child.Alias } - set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) + childBindings[child.Alias] = physicalSemanticBinding{ResourceType: child.ResourceType, Source: ir.PhysicalValue{Variable: fmt.Sprintf("child_set_%d", childSetIndex)}} + set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy, childBindings, lowerer) if err != nil { return err } physical.Operations = append(physical.Operations, ir.PhysicalOperation{Kind: ir.PhysicalSetOp, Source: ir.PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, Set: &set}) returnProjections = append(returnProjections, projections...) - if err := walk(child, set.Variable, childProjectionPrefix); err != nil { + if err := walk(child, set.Variable, childProjectionPrefix, childBindings); err != nil { return err } } @@ -113,7 +119,8 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic physical.Operations = appendProjectScope(physical.Operations, []string{edgeVariable, nodeVariable}, child.EdgeLabel, child) physical.Operations = appendDatasetGenerationScope(physical.Operations, []string{edgeVariable, nodeVariable}, child.EdgeLabel, child) physical.Operations = appendAuthScope(physical.Operations, []ir.PhysicalValue{{Variable: edgeVariable, Path: []string{"auth_resource_path"}}, {Variable: nodeVariable, Path: []string{"auth_resource_path"}}}, fmt.Sprintf("traversal_%d_scope_allowed", traversalIndex), child) - if err := walk(child, nodeVariable, projectionPrefix); err != nil { + childBindings[child.Alias] = physicalSemanticBinding{ResourceType: child.ResourceType, Source: ir.PhysicalValue{Variable: nodeVariable, Path: []string{"payload"}}} + if err := walk(child, nodeVariable, projectionPrefix, childBindings); err != nil { return err } continue @@ -123,25 +130,26 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic // filters and projections to be applied before materialization. childSetIndex++ childProjectionPrefix := child.Alias - if projectionPrefix != "" { + if output.TraversalColumnNaming != recipe.TraversalColumnNamingAlias && projectionPrefix != "" { childProjectionPrefix = projectionPrefix + "__" + child.Alias } - set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) + childBindings[child.Alias] = physicalSemanticBinding{ResourceType: child.ResourceType, Source: ir.PhysicalValue{Variable: fmt.Sprintf("child_set_%d", childSetIndex)}} + set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy, childBindings, lowerer) if err != nil { return err } physical.Operations = append(physical.Operations, ir.PhysicalOperation{Kind: ir.PhysicalSetOp, Source: ir.PhysicalSource{SemanticNode: child.Alias, ResourceType: child.ResourceType, Relationship: child.EdgeLabel}, Set: &set}) returnProjections = append(returnProjections, projections...) - if err := walk(child, set.Variable, childProjectionPrefix); err != nil { + if err := walk(child, set.Variable, childProjectionPrefix, childBindings); err != nil { return err } } return nil } - if err := walk(semantic.Root, "root", ""); err != nil { + if err := walk(output.Root, "root", "", rootBindings); err != nil { return ir.PhysicalPlan{}, err } - projections, err := rootPhysicalProjections(&physical, semantic.Root) + projections, err := rootPhysicalProjections(&physical, output.Root, nil, rootBindings, lowerer) if err != nil { return ir.PhysicalPlan{}, err } @@ -150,7 +158,7 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic physical.DeferredExpressionLets = nil physical.Operations = append(physical.Operations, ir.PhysicalOperation{ Kind: ir.PhysicalReturnOp, - Source: ir.PhysicalSource{SemanticNode: semantic.Root.Alias, ResourceType: semantic.Root.ResourceType, SemanticField: "_key"}, + Source: ir.PhysicalSource{SemanticNode: output.Root.Alias, ResourceType: output.Root.ResourceType, SemanticField: "_key"}, Return: &ir.PhysicalReturn{Projections: projections}, }) if err := physical.Validate(); err != nil { @@ -228,12 +236,3 @@ func appendAuthScope(operations []ir.PhysicalOperation, scopedValues []ir.Physic Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "EQUALS", Left: ir.PhysicalValue{Variable: resultVariable}, Right: &right}}, }) } - -func validateGenericPhysicalNode(node semanticpkg.SemanticNode, root bool) error { - for _, child := range node.Children { - if err := validateGenericPhysicalNode(child, false); err != nil { - return err - } - } - return nil -} diff --git a/internal/dataframe/compiler/lower/recipe_compile.go b/internal/dataframe/compiler/lower/recipe_compile.go index dcd4a617..090788e8 100644 --- a/internal/dataframe/compiler/lower/recipe_compile.go +++ b/internal/dataframe/compiler/lower/recipe_compile.go @@ -34,6 +34,7 @@ type CompiledRecipeOutput struct { Name string RootResourceType string RowGrain spec.RowGrain + RootColumnNaming recipe.RootColumnNaming Columns []string // OutputSchema is the compiler-owned ordered projection schema. It is // captured from the finalized physical RETURN projections rather than @@ -125,31 +126,20 @@ func CompileResolvedRecipePlan(resolved semantic.ResolvedRecipePlan, policy ir.P } func compileRecipeOutput(output semantic.OutputPlan, bindings recipe.RuntimeBindings, resolvedColumns map[string][]semantic.ResolvedColumn, policy ir.PhysicalOptimizationPolicy) (CompiledRecipeOutput, error) { - root := cloneRecipeNodeForPhysical(output.Root) identity, ok := spec.DefaultRowIdentity(spec.RowGrain(output.RowGrain)) if !ok { return CompiledRecipeOutput{}, fmt.Errorf("row grain %q has no canonical identity", output.RowGrain) } - semanticInput := semantic.SemanticPlan{ - Version: 1, + context := semantic.ExecutionContext{ Project: bindings.Project, DatasetGeneration: bindings.DatasetGeneration, AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), AuthScopeMode: bindings.AuthScopeMode, - Root: root, - RowIdentity: &identity, } - physical, err := BuildGenericPhysicalPlanWithPolicy(semanticInput, policy) + physical, err := buildGenericPhysicalPlanWithPolicy(output, context, policy, recipeFieldProjectionLowerer(output)) if err != nil { return CompiledRecipeOutput{}, err } - - // Recipe expressions are richer than selector-only GraphQL fields. The - // generic plan supplies the complete scoped traversal/set structure; patch - // only the expression payloads using the already checked recipe AST. - if err := patchRecipeExpressions(&physical, output); err != nil { - return CompiledRecipeOutput{}, err - } if err := appendRecipeIdentity(&physical, output); err != nil { return CompiledRecipeOutput{}, err } @@ -171,7 +161,7 @@ func compileRecipeOutput(output semantic.OutputPlan, bindings recipe.RuntimeBind } return CompiledRecipeOutput{ Name: output.Name, RootResourceType: output.RootResourceType, - RowGrain: output.RowGrain, Columns: physicalOutputColumns(outputSchema), OutputSchema: outputSchema, + RowGrain: output.RowGrain, RootColumnNaming: output.RootColumnNaming, Columns: physicalOutputColumns(outputSchema), OutputSchema: outputSchema, RowIdentity: (&identity).Clone(), DynamicColumns: dynamicMetadata, Plan: physical, }, nil } diff --git a/internal/dataframe/compiler/lower/recipe_expression_lowering.go b/internal/dataframe/compiler/lower/recipe_expression_lowering.go index 752eb660..05145706 100644 --- a/internal/dataframe/compiler/lower/recipe_expression_lowering.go +++ b/internal/dataframe/compiler/lower/recipe_expression_lowering.go @@ -14,89 +14,26 @@ import ( fhirschema "github.com/calypr/loom/internal/fhir/schema" ) -// CompiledRecipe is orchestration metadata around one canonical physical plan -// per output. It deliberately contains no recipe-specific traversal, -// projection, or renderer structures. Output order is the persisted recipe -// order and therefore part of the stable materialization contract. -func patchRecipeExpressions(plan *ir.PhysicalPlan, output semantic.OutputPlan) error { - if plan == nil { - return fmt.Errorf("physical plan is nil") - } - fields := map[string]semantic.SemanticExpression{} - for _, field := range output.Fields { - fields[field.Name] = field.Expr - } - for _, operation := range plan.Operations { - if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { - continue +// recipeFieldProjectionLowerer lowers rich recipe expressions at the generic +// plan boundary, where the traversal walk has established the physical value +// for every lexical alias. Selector-bearing fields deliberately delegate to +// the existing selector lowerer so projection modes, fallbacks, and prepared +// selector behavior remain unchanged. +func recipeFieldProjectionLowerer(output semantic.OutputPlan) semanticFieldProjectionLowerer { + contexts := recipeExpressionContexts(output) + return func(physical *ir.PhysicalPlan, node semantic.SemanticNode, index int, field semantic.SemanticField, source ir.PhysicalValue, bindings map[string]physicalSemanticBinding) (ir.PhysicalProjection, error) { + if field.Expr.Expression.Selector != nil { + return lowerSemanticFieldProjection(physical, node, index, field, source, bindings, nil) } - for index := range operation.Return.Projections { - projection := &operation.Return.Projections[index] - field, ok := fields[projection.Name] - if !ok { - continue - } - // Selector expressions are already lowered by the generic semantic - // node, including AUTO/FIRST/ALL/DISTINCT, fallbacks, and prepared - // selector metadata. Replacing that physical extract with the raw - // recipe expression would drop the legacy AUTO cardinality contract - // (for example, repeated selectors become FIRST). Only richer - // expression ASTs need a post-lowering patch. - if field.Expression.Selector != nil { - continue - } - physical, err := lowerRecipeExpressionScoped(field.Expression, plan.BindVars, output.RootResourceType, recipeExpressionContexts(output)) - if err != nil { - return fmt.Errorf("field expression: %w", err) - } - projection.Expression = &physical - projection.Value = ir.PhysicalValue{} + lowered, err := lowerRecipeExpressionScoped(field.Expr.Expression, physical.BindVars, output.RootResourceType, contexts) + if err != nil { + return ir.PhysicalProjection{}, fmt.Errorf("field %q expression: %w", field.Name, err) } - } - setVariables := recipeSetVariables(*plan) - - // Child fields are returned as flattened projections named alias__field by - // the generic lowerer. Match those names deterministically and retain the - // child resource type for selector provenance. - var walk func(semantic.SemanticNode) error - walk = func(node semantic.SemanticNode) error { - for _, child := range node.Children { - for _, field := range child.Fields { - name := child.Alias + "__" + field.Name - for index := range plan.Operations { - op := &plan.Operations[index] - if op.Kind != ir.PhysicalReturnOp || op.Return == nil { - continue - } - for projectionIndex := range op.Return.Projections { - projection := &op.Return.Projections[projectionIndex] - if projection.Name != name { - continue - } - if field.Expr != nil && field.Expr.Selector != nil { - continue - } - physical, err := lowerRecipeExpressionScoped(*field.Expr, plan.BindVars, output.RootResourceType, recipeExpressionContexts(output)) - if err != nil { - return fmt.Errorf("child field %q: %w", name, err) - } - setVariable, ok := setVariables[child.Alias] - if !ok { - return fmt.Errorf("child field %q has no canonical set variable for alias %q", name, child.Alias) - } - rewriteRecipeExpressionVariable(&physical, child.Alias, setVariable) - projection.Expression = &physical - projection.Value = ir.PhysicalValue{} - } - } - } - if err := walk(child); err != nil { - return err - } + for alias, binding := range bindings { + rewriteRecipeExpressionBinding(&lowered, alias, binding.Source) } - return nil + return ir.PhysicalProjection{Name: field.Name, Expression: &lowered}, nil } - return walk(output.Root) } func recipeSetVariables(plan ir.PhysicalPlan) map[string]string { @@ -145,6 +82,9 @@ func rewriteRecipeExpressionVariable(value *ir.PhysicalExpression, from, to stri if value == nil { return } + if value.Value != nil && value.Value.Variable == from { + value.Value.Variable = to + } if value.Extract != nil && value.Extract.Source.Variable == from { value.Extract.Source.Variable = to } @@ -153,6 +93,40 @@ func rewriteRecipeExpressionVariable(value *ir.PhysicalExpression, from, to stri rewriteRecipeExpressionVariable(&value.Call.Args[index], from, to) } } + if value.Object != nil { + for index := range value.Object.Fields { + rewriteRecipeExpressionVariable(&value.Object.Fields[index].Expression, from, to) + } + } +} + +// rewriteRecipeExpressionBinding applies a physical lexical binding to all +// value-bearing nodes. Extracts also inherit a payload path when the binding +// is a traversed document variable (as opposed to a materialized child set, +// whose renderer intentionally handles set items through item.payload). +func rewriteRecipeExpressionBinding(value *ir.PhysicalExpression, from string, source ir.PhysicalValue) { + if value == nil { + return + } + if value.Value != nil && value.Value.Variable == from { + value.Value.Variable = source.Variable + } + if value.Extract != nil && value.Extract.Source.Variable == from { + value.Extract.Source.Variable = source.Variable + if len(source.Path) != 0 { + value.Extract.Source.Path = append([]string(nil), source.Path...) + } + } + if value.Call != nil { + for index := range value.Call.Args { + rewriteRecipeExpressionBinding(&value.Call.Args[index], from, source) + } + } + if value.Object != nil { + for index := range value.Object.Fields { + rewriteRecipeExpressionBinding(&value.Object.Fields[index].Expression, from, source) + } + } } func recipeExpressionContexts(output semantic.OutputPlan) map[string]string { diff --git a/internal/dataframe/compiler/lower/recipe_output_test.go b/internal/dataframe/compiler/lower/recipe_output_test.go index 2df20acd..37f84a6a 100644 --- a/internal/dataframe/compiler/lower/recipe_output_test.go +++ b/internal/dataframe/compiler/lower/recipe_output_test.go @@ -1,6 +1,7 @@ package lower import ( + "encoding/json" "strconv" "strings" "testing" @@ -13,6 +14,56 @@ import ( "github.com/calypr/loom/internal/dataframe/semantic" ) +func TestCompileResolvedRecipePlanLowersNonSelectorFieldAtGenericBoundary(t *testing.T) { + literal := func(value string) recipe.Expression { + return recipe.Expression{Literal: json.RawMessage(strconv.Quote(value))} + } + bundle := recipe.Bundle{ + RecipeSchemaVersion: 1, + Name: "direct-expression", + TranslationVersion: "test", + Outputs: []recipe.Output{{ + Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", + Fields: []recipe.Field{{Name: "label", Expr: recipe.Expression{Call: "concat", Args: []recipe.Expression{literal("a"), literal("b")}}}}, + }}, + } + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, ir.DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + var label *ir.PhysicalExpression + for _, operation := range compiled.Outputs[0].Plan.Operations { + if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { + continue + } + for _, projection := range operation.Return.Projections { + if projection.Name == "label" { + label = projection.Expression + } + } + } + if label == nil || label.Kind != ir.PhysicalCallExpression || label.Call == nil || label.Call.Name != "concat" { + t.Fatalf("label projection = %#v, want direct concat call", label) + } + if len(label.Call.Args) != 2 || label.Call.Args[0].Kind != ir.PhysicalLiteralExpression || label.Call.Args[1].Kind != ir.PhysicalLiteralExpression { + t.Fatalf("label call args = %#v, want two physical literals", label.Call.Args) + } + if _, ok := compiled.Outputs[0].Plan.BindVars[label.Call.Args[0].Literal.BindKey]; !ok { + t.Fatalf("first literal bind %q is missing: %#v", label.Call.Args[0].Literal.BindKey, compiled.Outputs[0].Plan.BindVars) + } + if _, ok := compiled.Outputs[0].Plan.BindVars[label.Call.Args[1].Literal.BindKey]; !ok { + t.Fatalf("second literal bind %q is missing: %#v", label.Call.Args[1].Literal.BindKey, compiled.Outputs[0].Plan.BindVars) + } +} + func TestCompileResolvedRecipePlanProducesCanonicalPhysicalPlans(t *testing.T) { bundle := compilerFixtureBundle(t) plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) @@ -148,6 +199,98 @@ func TestCompiledRecipeOutputSchemaMatchesFinalReturnProjectionOrder(t *testing. } } +func TestTraversalColumnNamingAliasKeepsNestedPublicColumnsGloballyScoped(t *testing.T) { + bundle := recipe.Bundle{ + RecipeSchemaVersion: recipe.CurrentSchemaVersion, + Name: "alias-scoped-traversals", + TranslationVersion: "test", + Outputs: []recipe.Output{{ + Name: "ResearchSubject", RootResourceType: "ResearchSubject", RowGrain: "study_enrollment", + TraversalColumnNaming: recipe.TraversalColumnNamingAlias, + Traversals: []recipe.Traversal{{ + Name: "subject_Patient", Alias: "occ_parent", ToResourceType: "Patient", MatchMode: recipe.MatchOptional, + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "occ_parent.id"}}}, + Traversals: []recipe.Traversal{{ + Name: "subject_Patient", Alias: "occ_child", ToResourceType: "Condition", MatchMode: recipe.MatchOptional, + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "occ_child.id"}}}, + }}, + }}, + }}, + } + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, ir.DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + public := map[string]bool{} + for _, column := range compiled.Outputs[0].OutputSchema { + if !column.Internal { + public[column.Name] = true + } + } + if !public["occ_parent__id"] || !public["occ_child__id"] { + t.Fatalf("alias-scoped public columns = %#v", public) + } + if public["occ_parent__occ_child__id"] { + t.Fatalf("nested column unexpectedly used a path-scoped name: %#v", public) + } + rendered, err := aql.RenderPhysicalPlan(compiled.Outputs[0].Plan) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{"LET child_set_1_reduced = {", "LET child_set_2_reduced = {"} { + if !strings.Contains(rendered.Query, want) { + t.Fatalf("recipe traversal field missing %q:\n%s", want, rendered.Query) + } + } + if strings.Contains(rendered.Query, "FOR __loom_prepared_value IN child_set_") { + t.Fatalf("recipe traversal fields still rescan their child sets:\n%s", rendered.Query) + } +} + +func TestTraversalColumnNamingDefaultsToPathForExistingRecipes(t *testing.T) { + bundle := recipe.Bundle{ + RecipeSchemaVersion: recipe.CurrentSchemaVersion, + Name: "path-scoped-traversals", + TranslationVersion: "test", + Outputs: []recipe.Output{{ + Name: "ResearchSubject", RootResourceType: "ResearchSubject", RowGrain: "study_enrollment", + Traversals: []recipe.Traversal{{ + Name: "subject_Patient", Alias: "parent", ToResourceType: "Patient", MatchMode: recipe.MatchOptional, + Traversals: []recipe.Traversal{{ + Name: "subject_Patient", Alias: "child", ToResourceType: "Condition", MatchMode: recipe.MatchOptional, + Fields: []recipe.Field{{Name: "id", Expr: recipe.Expression{Select: "child.id"}}}, + }}, + }}, + }}, + } + plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) + if err != nil { + t.Fatal(err) + } + resolved, err := semantic.ResolveRecipePlan(plan, "scope", "generation") + if err != nil { + t.Fatal(err) + } + compiled, err := CompileResolvedRecipePlan(resolved, ir.DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + for _, column := range compiled.Outputs[0].OutputSchema { + if column.Name == "parent__child__id" { + return + } + } + t.Fatalf("path-scoped output schema = %#v", compiled.Outputs[0].OutputSchema) +} + func TestCompileResolvedRecipePlanUsesCanonicalUnnest(t *testing.T) { bundle := compilerFixtureBundle(t) plan, err := semantic.BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "project", DatasetGeneration: "generation"}) diff --git a/internal/dataframe/compiler/lower/recipe_schema.go b/internal/dataframe/compiler/lower/recipe_schema.go index ee492b87..c9f4275f 100644 --- a/internal/dataframe/compiler/lower/recipe_schema.go +++ b/internal/dataframe/compiler/lower/recipe_schema.go @@ -10,40 +10,14 @@ import ( "github.com/calypr/loom/internal/dataframe/compiler/ir" "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataframe/semantic" - "github.com/calypr/loom/internal/dataframe/spec" ) // CompiledRecipe is orchestration metadata around one canonical physical plan // per output. It deliberately contains no recipe-specific traversal, // projection, or renderer structures. Output order is the persisted recipe // order and therefore part of the stable materialization contract. -func cloneRecipeNodeForPhysical(node semantic.SemanticNode) semantic.SemanticNode { - copy := node - copy.Fields = append([]semantic.SemanticField(nil), node.Fields...) - copy.Children = make([]semantic.SemanticNode, len(node.Children)) - for index := range node.Children { - copy.Children[index] = cloneRecipeNodeForPhysical(node.Children[index]) - } - // BuildGenericPhysicalPlan currently consumes selector metadata for its - // structural projections. Recipe expressions are patched after the - // generic plan is built, so provide a schema-valid selector seed for call - // expressions that contain no selector (for example a literal-only call). - for index := range copy.Fields { - // The generic lowerer uses SemanticField.Selector only to construct a - // valid traversal/set skeleton. Recipe expressions may refer to an - // expanded item (for example member.entity.reference), which is not a - // selector relative to the root resource. The checked recipe expression - // replaces this seed immediately after the skeleton is built. - if copy.Fields[index].Expr == nil || copy.Fields[index].Expr.Selector == nil { - if selector, err := spec.ParseSelector("id"); err == nil { - copy.Fields[index].Selector = selector - } - } - } - return copy -} - // recipeOutputSchema is the single schema capture point for recipe outputs. // Names and order come from the physical RETURN operation after all recipe // identity and bounded dynamic projections have been appended. Semantic @@ -82,21 +56,31 @@ func recipeOutputSchema(plan ir.PhysicalPlan, output semantic.OutputPlan, dynami } addLogical(name, semanticPath, kind, cardinality, typ.Cardinality.Optional(), discovered) } - for _, field := range output.Fields { - addType(field.Name, recipeSemanticPath(output.RootResourceType, output.RootResourceType, field.FieldRef, field.Expr.Expression), field.Expr.Type, field.Discovered) - } var addNode func(semantic.SemanticNode, string) addNode = func(node semantic.SemanticNode, prefix string) { for _, field := range node.Fields { name := prefix + field.Name - if field.Expr != nil { - addType(name, recipeSemanticPath(output.RootResourceType, node.ResourceType, field.FieldRef, *field.Expr), field.ExprType, field.Discovered) - } else { - addLogical(name, recipeSemanticPath(output.RootResourceType, node.ResourceType, field.FieldRef, expression.Expression{}), string(expression.KindString), string(expression.RequiredOne), true, field.Discovered) - } + addType(name, recipeSemanticPath(output.RootResourceType, node.ResourceType, field.FieldRef, field.Expr.Expression), field.Expr.Type, field.Discovered) } for _, aggregate := range node.Aggregates { - addLogical(prefix+aggregate.Name, recipeSemanticPath(output.RootResourceType, node.ResourceType, aggregate.FieldRef, expression.Expression{}), string(expression.KindInteger), string(expression.RequiredOne), true, false) + name := aggregate.Name + if aggregate.OutputName != "" { + name = aggregate.OutputName + } else if prefix != "" { + name = prefix + aggregate.Name + } + kind := aggregate.ValueKind + if kind == "" { + kind = expression.KindInteger + } + cardinality := expression.RequiredOne + if aggregate.Operation == string(recipe.AggregateDistinctValues) { + cardinality = expression.Many + } + if aggregate.Operation == string(recipe.AggregateMin) || aggregate.Operation == string(recipe.AggregateMax) { + cardinality = expression.OptionalOne + } + addLogical(name, recipeSemanticPath(output.RootResourceType, node.ResourceType, aggregate.FieldRef, expression.Expression{}), string(kind), string(cardinality), true, false) } for _, pivot := range node.Pivots { kind := pivot.ValueKind @@ -112,7 +96,7 @@ func recipeOutputSchema(plan ir.PhysicalPlan, output semantic.OutputPlan, dynami } for _, child := range node.Children { childPrefix := child.Alias - if prefix != "" { + if output.TraversalColumnNaming != recipe.TraversalColumnNamingAlias && prefix != "" { childPrefix = strings.TrimSuffix(prefix, "__") + "__" + child.Alias } addNode(child, childPrefix+"__") diff --git a/internal/dataframe/compiler/lower/root_projection.go b/internal/dataframe/compiler/lower/root_projection.go index b87010fe..886c83d3 100644 --- a/internal/dataframe/compiler/lower/root_projection.go +++ b/internal/dataframe/compiler/lower/root_projection.go @@ -5,51 +5,106 @@ import ( "strings" "github.com/calypr/loom/internal/dataframe/compiler/ir" - "github.com/calypr/loom/internal/dataframe/expression" "github.com/calypr/loom/internal/dataframe/semantic" "github.com/calypr/loom/internal/dataframe/spec" ) -func rootPhysicalProjections(physical *ir.PhysicalPlan, root semantic.SemanticNode) ([]ir.PhysicalProjection, error) { - projections := []ir.PhysicalProjection{{Name: "_key", Value: ir.PhysicalValue{Variable: "root", Path: []string{"_key"}}}} - for index, field := range root.Fields { - // Whole-document projections are lowered from the canonical expression - // after the generic semantic graph has established root scope. They do - // not have a FHIR selector to resolve here. - if field.Expr != nil && field.Expr.Kind == expression.DocumentRefNode { - placeholder := lowerDocumentRef(*field.Expr.Document, ir.PhysicalPreserveNull) - projections = append(projections, ir.PhysicalProjection{Name: field.Name, Expression: &placeholder}) - continue - } - selection, err := semantic.ResolveSemanticField(root.ResourceType, root.Alias, index, field) +// physicalSemanticBinding records the physical value and schema resource +// associated with one semantic lexical binding. Recipe expressions use this +// while lowering so a selector is rooted at the binding that is actually in +// scope (root document, traversal item, set item, or unnest item). +type physicalSemanticBinding struct { + ResourceType string + Source ir.PhysicalValue +} + +func clonePhysicalSemanticBindings(bindings map[string]physicalSemanticBinding) map[string]physicalSemanticBinding { + if len(bindings) == 0 { + return map[string]physicalSemanticBinding{} + } + copy := make(map[string]physicalSemanticBinding, len(bindings)+1) + for alias, binding := range bindings { + copy[alias] = binding + } + return copy +} + +// semanticFieldProjectionLowerer is shared by root and child projection +// construction. The generic dataframe compiler supplies nil and uses the +// selector-only lowering below; recipe compilation supplies a direct checked +// expression lowerer. Keeping this at the projection boundary means rich +// expressions are lowered once, while the traversal/set walk still owns +// lexical physical variables. +type semanticFieldProjectionLowerer func( + physical *ir.PhysicalPlan, + node semantic.SemanticNode, + index int, + field semantic.SemanticField, + source ir.PhysicalValue, + bindings map[string]physicalSemanticBinding, +) (ir.PhysicalProjection, error) + +func lowerSemanticFieldProjection(physical *ir.PhysicalPlan, node semantic.SemanticNode, index int, field semantic.SemanticField, source ir.PhysicalValue, bindings map[string]physicalSemanticBinding, lowerer semanticFieldProjectionLowerer) (ir.PhysicalProjection, error) { + if lowerer != nil { + return lowerer(physical, node, index, field, source, bindings) + } + selection, err := semantic.ResolveSemanticField(node.ResourceType, node.Alias, index, field) + if err != nil { + return ir.PhysicalProjection{}, err + } + cardinality, distinct := ir.PhysicalScalarCardinality, false + switch selection.Projection { + case spec.ProjectionArray: + cardinality = ir.PhysicalArrayCardinality + case spec.ProjectionDistinctArray: + cardinality, distinct = ir.PhysicalArrayCardinality, true + case spec.ProjectionScalar, spec.ProjectionFirst: + default: + return ir.PhysicalProjection{}, fmt.Errorf("field %q has unsupported projection %q", field.Name, selection.Projection) + } + fallbacks, err := field.FallbackSelectors() + if err != nil { + return ir.PhysicalProjection{}, err + } + expression := ir.PhysicalExpression{ + Kind: ir.PhysicalExtractExpression, Cardinality: cardinality, NullBehavior: ir.PhysicalPreserveNull, + Extract: &ir.PhysicalExtract{Source: source, ResourceType: node.ResourceType, Selector: selection.Selector, Fallbacks: fallbacks, Distinct: distinct, ExecutionMode: selectorExecutionMode(node.ResourceType, selection.Selector, fallbacks...)}, + } + if cardinality == ir.PhysicalArrayCardinality { + expression.NullBehavior = ir.PhysicalEmptyOnNull + } + return ir.PhysicalProjection{Name: field.Name, Expression: &expression}, nil +} + +func lowerSemanticFieldProjections(physical *ir.PhysicalPlan, node semantic.SemanticNode, source ir.PhysicalValue, bindings map[string]physicalSemanticBinding, lowerer semanticFieldProjectionLowerer) ([]ir.PhysicalProjection, error) { + projections := make([]ir.PhysicalProjection, 0, len(node.Fields)) + for index, field := range node.Fields { + projection, err := lowerSemanticFieldProjection(physical, node, index, field, source, bindings, lowerer) if err != nil { return nil, err } - cardinality, distinct := ir.PhysicalScalarCardinality, false - switch selection.Projection { - case spec.ProjectionArray: - cardinality = ir.PhysicalArrayCardinality - case spec.ProjectionDistinctArray: - cardinality, distinct = ir.PhysicalArrayCardinality, true - case spec.ProjectionScalar, spec.ProjectionFirst: - default: - return nil, fmt.Errorf("root field %q has unsupported projection %q", field.Name, selection.Projection) - } - expression := ir.PhysicalExpression{ - Kind: ir.PhysicalExtractExpression, Cardinality: cardinality, NullBehavior: ir.PhysicalPreserveNull, - Extract: &ir.PhysicalExtract{Source: ir.PhysicalValue{Variable: "root", Path: []string{"payload"}}, ResourceType: root.ResourceType, Selector: field.Selector, Fallbacks: append([]spec.Selector(nil), field.Fallbacks...), Distinct: distinct, ExecutionMode: selectorExecutionMode(root.ResourceType, field.Selector, field.Fallbacks...)}, - } - if cardinality == ir.PhysicalArrayCardinality { - expression.NullBehavior = ir.PhysicalEmptyOnNull + projections = append(projections, projection) + } + return projections, nil +} + +func rootPhysicalProjections(physical *ir.PhysicalPlan, root semantic.SemanticNode, fields []ir.PhysicalProjection, bindings map[string]physicalSemanticBinding, lowerer semanticFieldProjectionLowerer) ([]ir.PhysicalProjection, error) { + projections := []ir.PhysicalProjection{{Name: "_key", Value: ir.PhysicalValue{Variable: "root", Path: []string{"_key"}}}} + if fields != nil { + projections = append(projections, fields...) + } else { + fields, err := lowerSemanticFieldProjections(physical, root, ir.PhysicalValue{Variable: "root", Path: []string{"payload"}}, bindings, lowerer) + if err != nil { + return nil, err } - projections = append(projections, ir.PhysicalProjection{Name: field.Name, Expression: &expression}) + projections = append(projections, fields...) } for _, aggregate := range root.Aggregates { expression, err := physicalAggregateExpression(physical, root.ResourceType, ir.PhysicalValue{Variable: "root"}, aggregate, false) if err != nil { return nil, err } - projections = append(projections, ir.PhysicalProjection{Name: aggregate.Name, Expression: &expression}) + projections = append(projections, ir.PhysicalProjection{Name: aggregateProjectionName(aggregate, ""), Expression: &expression}) } for _, pivot := range root.Pivots { pivotProjections, err := physicalPivotProjections(physical, root.ResourceType, ir.PhysicalValue{Variable: "root"}, pivot, "") @@ -113,11 +168,16 @@ func deferredExpressionVariableExists(physical ir.PhysicalPlan, variable string) func physicalAggregateExpression(physical *ir.PhysicalPlan, resourceType string, source ir.PhysicalValue, aggregate semantic.SemanticAggregate, sourceIsSet bool) (ir.PhysicalExpression, error) { op := ir.PhysicalAggregateOperation(strings.ToUpper(strings.TrimSpace(aggregate.Operation))) switch op { - case ir.PhysicalCountAggregate, ir.PhysicalCountDistinctAggregate, ir.PhysicalExistsAggregate, ir.PhysicalDistinctValuesAggregate, ir.PhysicalMinAggregate, ir.PhysicalMaxAggregate, ir.PhysicalFirstAggregate: + case ir.PhysicalCountAggregate, ir.PhysicalCountDistinctAggregate, ir.PhysicalExistsAggregate, ir.PhysicalDistinctValuesAggregate, ir.PhysicalMinAggregate, ir.PhysicalMaxAggregate, ir.PhysicalFirstAggregate, ir.PhysicalContainsAllAggregate: default: return ir.PhysicalExpression{}, fmt.Errorf("aggregate %q uses unsupported operation %q", aggregate.Name, aggregate.Operation) } aggregatePhysical := ir.PhysicalAggregate{Source: source, Operation: op} + if len(aggregate.RequiredValues) > 0 { + key := "aggregate_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(aggregate.Name) + "_required_values" + physical.BindVars[key] = append([]string(nil), aggregate.RequiredValues...) + aggregatePhysical.RequiredValuesBindKey = key + } if aggregate.Selector != nil { valueSource := source if !sourceIsSet && source.Variable != "" && len(source.Path) == 0 { @@ -137,7 +197,10 @@ func physicalAggregateExpression(physical *ir.PhysicalPlan, resourceType string, comparison := &ir.PhysicalPredicate{Operator: "EXISTS", LeftExpression: &left} if aggregate.PredicateEquals != "" { comparison.Operator = "EQUALS" - comparison.ValueKind = spec.FilterString + comparison.ValueKind = aggregate.PredicateKind + if comparison.ValueKind == "" { + comparison.ValueKind = spec.FilterString + } key := "aggregate_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(aggregate.Name) + "_predicate_equals" physical.BindVars[key] = aggregate.PredicateEquals comparison.Right = &ir.PhysicalValue{BindKey: key} @@ -149,9 +212,19 @@ func physicalAggregateExpression(physical *ir.PhysicalPlan, resourceType string, if op == ir.PhysicalDistinctValuesAggregate { cardinality = ir.PhysicalArrayCardinality } + if op == ir.PhysicalContainsAllAggregate { + nullBehavior = ir.PhysicalPreserveNull + } return ir.PhysicalExpression{Kind: ir.PhysicalAggregateExpression, Cardinality: cardinality, NullBehavior: nullBehavior, Aggregate: &aggregatePhysical}, nil } +func aggregateProjectionName(aggregate semantic.SemanticAggregate, prefix string) string { + if strings.TrimSpace(aggregate.OutputName) != "" { + return aggregate.OutputName + } + return prefix + aggregate.Name +} + // physicalSliceExpression lowers a representative slice into a typed, // bounded projection over either the singleton root document or a correlated // child set. The source set is already materialized in stable _key order; @@ -180,7 +253,10 @@ func physicalSliceExpression(physical *ir.PhysicalPlan, resourceType string, sou key := "slice_" + sanitizeColumnName(source.Variable) + "_" + sanitizeColumnName(slice.Name) + "_predicate_equals" physical.BindVars[key] = slice.PredicateEquals comparison.Operator = "EQUALS" - comparison.ValueKind = spec.FilterString + comparison.ValueKind = slice.PredicateKind + if comparison.ValueKind == "" { + comparison.ValueKind = spec.FilterString + } comparison.Right = &ir.PhysicalValue{BindKey: key} } physicalSlice.Predicate = &ir.PhysicalPredicateExpression{Kind: ir.PhysicalComparisonPredicate, Comparison: comparison} @@ -190,10 +266,18 @@ func physicalSliceExpression(physical *ir.PhysicalPlan, resourceType string, sou if selection.Name == "" { return ir.PhysicalExpression{}, fmt.Errorf("slice %q field %d requires name", slice.Name, index) } + selector, err := selection.PrimarySelector() + if err != nil { + return ir.PhysicalExpression{}, err + } + fallbacks, err := selection.FallbackSelectors() + if err != nil { + return ir.PhysicalExpression{}, err + } physicalSlice.Projections = append(physicalSlice.Projections, ir.PhysicalExpressionProjection{ Name: selection.Name, Expression: ir.PhysicalExpression{Kind: ir.PhysicalExtractExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, - Extract: &ir.PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: selection.Selector, Fallbacks: append([]spec.Selector(nil), selection.Fallbacks...), ExecutionMode: selectorExecutionMode(resourceType, selection.Selector, selection.Fallbacks...)}}, + Extract: &ir.PhysicalExtract{Source: leftSource, ResourceType: resourceType, Selector: selector, Fallbacks: fallbacks, ExecutionMode: selectorExecutionMode(resourceType, selector, fallbacks...)}}, }) } return ir.PhysicalExpression{Kind: ir.PhysicalSliceExpression, Cardinality: ir.PhysicalArrayCardinality, NullBehavior: ir.PhysicalEmptyOnNull, Slice: &physicalSlice}, nil diff --git a/internal/dataframe/compiler/lower/traversal.go b/internal/dataframe/compiler/lower/traversal.go index a83a621d..a68aa56e 100644 --- a/internal/dataframe/compiler/lower/traversal.go +++ b/internal/dataframe/compiler/lower/traversal.go @@ -28,11 +28,8 @@ type TraversalLoweringRequest struct { } // TraversalLoweringResult contains the canonical physical traversal and the -// bind values required by its renderer. The route is retained as provenance -// for callers that need to inspect the schema-derived contract; callers must -// not mutate it or substitute a direction after construction. +// bind values required by its renderer. type TraversalLoweringResult struct { - Route storageRoute Traversal ir.PhysicalTraversal BindVars map[string]any } @@ -88,7 +85,6 @@ func BuildPhysicalTraversal(request TraversalLoweringRequest) (TraversalLowering typeBind := prefix + "_target_type" edgeCollectionBind := prefix + "_edge_collection" return TraversalLoweringResult{ - Route: route, Traversal: ir.PhysicalTraversal{ SourceVariable: request.SourceVariable, TargetVariable: request.TargetVariable, diff --git a/internal/dataframe/compiler/lower/traversal_test.go b/internal/dataframe/compiler/lower/traversal_test.go index 37255c4a..d169a5f0 100644 --- a/internal/dataframe/compiler/lower/traversal_test.go +++ b/internal/dataframe/compiler/lower/traversal_test.go @@ -21,9 +21,6 @@ func TestBuildPhysicalTraversalUsesSchemaDerivedInboundRoute(t *testing.T) { if err != nil { t.Fatalf("BuildPhysicalTraversal() error = %v", err) } - if got.Route.Direction != ir.PhysicalInbound { - t.Fatalf("route direction = %q, want %q", got.Route.Direction, ir.PhysicalInbound) - } if got.Traversal.Direction != ir.PhysicalInbound || got.Traversal.EdgeTargetTypeField != "from_type" { t.Fatalf("traversal route contract = %#v", got.Traversal) } @@ -59,8 +56,8 @@ func TestBuildPhysicalTraversalUsesSchemaDerivedOutboundRoute(t *testing.T) { if err != nil { t.Fatalf("BuildPhysicalTraversal() error = %v", err) } - if got.Route.Direction != ir.PhysicalOutbound || got.Traversal.Direction != ir.PhysicalOutbound { - t.Fatalf("outbound route contract = route %#v traversal %#v", got.Route, got.Traversal) + if got.Traversal.Direction != ir.PhysicalOutbound { + t.Fatalf("outbound traversal direction = %q", got.Traversal.Direction) } if got.Traversal.EdgeTargetTypeField != "to_type" { t.Fatalf("target discriminator = %q, want to_type", got.Traversal.EdgeTargetTypeField) @@ -79,8 +76,8 @@ func TestBuildPhysicalTraversalUsesGeneratedOutboundReferenceForAnyFHIRType(t *t if err != nil { t.Fatalf("BuildPhysicalTraversal() error = %v", err) } - if got.Route.Direction != ir.PhysicalOutbound || got.Traversal.Direction != ir.PhysicalOutbound { - t.Fatalf("generated outbound route contract = route %#v traversal %#v", got.Route, got.Traversal) + if got.Traversal.Direction != ir.PhysicalOutbound { + t.Fatalf("generated outbound traversal direction = %q", got.Traversal.Direction) } if got.Traversal.EndpointField != "_from" || got.Traversal.EndpointJoinField != "_to" { t.Fatalf("generated outbound endpoint contract = %#v", got.Traversal) diff --git a/internal/dataframe/compiler/optimize/keyed_map.go b/internal/dataframe/compiler/optimize/keyed_map.go deleted file mode 100644 index 25a44776..00000000 --- a/internal/dataframe/compiler/optimize/keyed_map.go +++ /dev/null @@ -1,134 +0,0 @@ -package optimize - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "sort" - - "github.com/calypr/loom/internal/dataframe/compiler/ir" -) - -type keyedMapCandidate struct { - Signature string - Uses []*ir.PhysicalExpression - Lookup ir.PhysicalLookup -} - -type keyedMapBinding struct { - Variable string - Lookup ir.PhysicalLookup -} - -// sharePhysicalLookupFamilies converts repeated source-bearing lookup -// expressions in one RETURN scope into one keyed map and constant-time object -// lookups. The pass is deliberately lexical: candidates are collected per -// RETURN operation, so an optional child scope or an unnest can never leak a -// binding into a different row grain. -func sharePhysicalLookupFamilies(plan *ir.PhysicalPlan, policy ir.PhysicalOptimizationPolicy) { - for returnIndex := range plan.Operations { - operation := &plan.Operations[returnIndex] - if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { - continue - } - candidates := map[string]*keyedMapCandidate{} - for projectionIndex := range operation.Return.Projections { - collectLookupCandidates(operation.Return.Projections[projectionIndex].Expression, candidates) - } - keys := make([]string, 0, len(candidates)) - for key, candidate := range candidates { - if len(candidate.Uses) >= 2 { - keys = append(keys, key) - } - } - sort.Strings(keys) - if len(keys) == 0 { - continue - } - bindings := make(map[string]keyedMapBinding, len(keys)) - lets := make([]ir.PhysicalOperation, 0, len(keys)) - for _, key := range keys { - candidate := candidates[key] - baseline := len(candidate.Uses) * 3 - optimized := len(candidate.Uses) + 1 - decision := ir.PhysicalOptimizationDecision{Rule: string(ir.PhysicalOptimizationRuleKeyedMapSharing), CandidateSets: len(candidate.Uses), EstimatedBaselineWork: baseline, EstimatedOptimizedWork: optimized, EstimatedSavings: baseline - optimized} - if !policy.RuleEnabled(ir.PhysicalOptimizationRuleKeyedMapSharing) { - decision.Reason = "keyed map sharing disabled by policy" - plan.OptimizationPolicy.AddDecision(decision) - continue - } - if decision.EstimatedSavings < policy.MinimumSavings { - decision.Reason = fmt.Sprintf("estimated savings %d is below policy minimum %d", decision.EstimatedSavings, policy.MinimumSavings) - plan.OptimizationPolicy.AddDecision(decision) - continue - } - digest := sha256.Sum256([]byte(key)) - variable := "__loom_shared_lookup_" + hex.EncodeToString(digest[:])[:12] - bindings[key] = keyedMapBinding{Variable: variable, Lookup: candidate.Lookup} - lets = append(lets, ir.PhysicalOperation{Kind: ir.PhysicalExpressionLetOp, Source: ir.PhysicalSource{SemanticField: "shared_lookup_family"}, ExpressionLet: &ir.PhysicalExpressionLet{Variable: variable, Expression: ir.PhysicalExpression{Kind: ir.PhysicalKeyedMapExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, KeyedMap: &ir.PhysicalKeyedMap{Source: ir.ClonePhysicalExpression(candidate.Lookup.Source), ItemVariable: candidate.Lookup.ItemVariable, ItemKey: ir.ClonePhysicalExpression(candidate.Lookup.ItemKey), ItemValue: ir.ClonePhysicalExpression(candidate.Lookup.ItemValue), Reduction: ir.PhysicalMapFirst}}}}) - decision.Enabled = true - decision.Reason = "repeated lookup source and selectors share one lexical keyed map" - plan.OptimizationPolicy.AddDecision(decision) - } - if len(bindings) == 0 { - continue - } - for projectionIndex := range operation.Return.Projections { - replaceLookupCandidates(operation.Return.Projections[projectionIndex].Expression, bindings) - } - operations := make([]ir.PhysicalOperation, 0, len(plan.Operations)+len(lets)) - operations = append(operations, plan.Operations[:returnIndex]...) - operations = append(operations, lets...) - operations = append(operations, plan.Operations[returnIndex:]...) - plan.Operations = operations - returnIndex += len(lets) - } -} - -func collectLookupCandidates(expression *ir.PhysicalExpression, candidates map[string]*keyedMapCandidate) { - if expression == nil { - return - } - if expression.Kind == ir.PhysicalLookupExpression && expression.Lookup != nil && expression.Lookup.ItemVariable != "" && expression.Lookup.MatchBindKey != "" { - keyed := ir.PhysicalKeyedMap{Source: ir.ClonePhysicalExpression(expression.Lookup.Source), ItemVariable: expression.Lookup.ItemVariable, ItemKey: ir.ClonePhysicalExpression(expression.Lookup.ItemKey), ItemValue: ir.ClonePhysicalExpression(expression.Lookup.ItemValue), Reduction: ir.PhysicalMapFirst} - candidateExpression := ir.PhysicalExpression{Kind: ir.PhysicalKeyedMapExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, KeyedMap: &keyed} - signature, err := ir.PhysicalExpressionFingerprint(candidateExpression) - if err == nil { - candidate := candidates[signature] - if candidate == nil { - candidate = &keyedMapCandidate{Signature: signature, Lookup: *expression.Lookup} - candidates[signature] = candidate - } - candidate.Uses = append(candidate.Uses, expression) - } - } - if expression.Object != nil { - for index := range expression.Object.Fields { - collectLookupCandidates(&expression.Object.Fields[index].Expression, candidates) - } - } -} - -func replaceLookupCandidates(expression *ir.PhysicalExpression, bindings map[string]keyedMapBinding) { - if expression == nil { - return - } - if expression.Kind == ir.PhysicalLookupExpression && expression.Lookup != nil { - keyed := ir.PhysicalKeyedMap{Source: ir.ClonePhysicalExpression(expression.Lookup.Source), ItemVariable: expression.Lookup.ItemVariable, ItemKey: ir.ClonePhysicalExpression(expression.Lookup.ItemKey), ItemValue: ir.ClonePhysicalExpression(expression.Lookup.ItemValue), Reduction: ir.PhysicalMapFirst} - candidateExpression := ir.PhysicalExpression{Kind: ir.PhysicalKeyedMapExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, KeyedMap: &keyed} - if signature, err := ir.PhysicalExpressionFingerprint(candidateExpression); err == nil { - if binding, ok := bindings[signature]; ok { - matchBindKey := expression.Lookup.MatchBindKey - expression.Kind = ir.PhysicalObjectLookupExpression - expression.Lookup = nil - expression.KeyedMap = nil - expression.ObjectLookup = &ir.PhysicalObjectLookup{ObjectVariable: binding.Variable, KeyBindKey: matchBindKey} - } - } - } - if expression.Object != nil { - for index := range expression.Object.Fields { - replaceLookupCandidates(&expression.Object.Fields[index].Expression, bindings) - } - } -} diff --git a/internal/dataframe/compiler/optimize/optimize.go b/internal/dataframe/compiler/optimize/optimize.go index 5df6a82c..7a93c57e 100644 --- a/internal/dataframe/compiler/optimize/optimize.go +++ b/internal/dataframe/compiler/optimize/optimize.go @@ -91,9 +91,6 @@ func OptimizePhysicalPlanWithPolicy(plan ir.PhysicalPlan, policy ir.PhysicalOpti decision.Reason = "estimated prefix work reduction exceeds policy minimum" out.OptimizationPolicy.AddDecision(decision) } - if policy.RuleEnabled(ir.PhysicalOptimizationRuleKeyedMapSharing) { - sharePhysicalLookupFamilies(&out, policy) - } if err := out.Validate(); err != nil { return ir.PhysicalPlan{}, fmt.Errorf("validate optimized physical plan: %w", err) } @@ -185,6 +182,10 @@ func sharePhysicalSetGroup(plan *ir.PhysicalPlan, indices []int, types map[strin // selectors against the wrong resource payload and leaves typed subsets // with empty values. base.Projection = nil + // The heterogeneous shared neighbor set has no consumer-specific selector + // slots, so it must not retain a reduction belonging to the original typed + // set. + base.Reduction = nil base.Subplan.Return = ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: t.TargetVariable}} // Rebuild operation stream, inserting the broad traversal immediately before // the first consumer and replacing each consumer with a typed subset. @@ -225,6 +226,11 @@ func sharePhysicalSetGroup(plan *ir.PhysicalPlan, indices []int, types map[strin prepared.SourceSetVariable = set.Variable set.Prepared = &prepared } + if set.Reduction != nil { + reduction := *set.Reduction + reduction.SourceSetVariable = set.Variable + set.Reduction = &reduction + } newOps = append(newOps, ir.PhysicalOperation{Kind: ir.PhysicalSetOp, Source: op.Source, Set: &set}) } plan.Operations = newOps @@ -301,10 +307,6 @@ func rewritePhysicalOperationVariables(op ir.PhysicalOperation, fromTarget, toTa for i := range d.Inputs { d.Inputs[i] = rewritePhysicalValue(d.Inputs[i], fromTarget, toTarget, fromEdge, toEdge) } - if d.Expression != nil { - expression := rewritePhysicalExpressionVariables(*d.Expression, fromTarget, toTarget, fromEdge, toEdge) - d.Expression = &expression - } op.DerivedLet = &d } if op.Unnest != nil { diff --git a/internal/dataframe/compiler/optimizer.go b/internal/dataframe/compiler/optimizer.go index 5312d3c8..458e781e 100644 --- a/internal/dataframe/compiler/optimizer.go +++ b/internal/dataframe/compiler/optimizer.go @@ -3,10 +3,5 @@ package compiler // Stable optimizer-rule identifiers appear in compiler explain output and // conformance evidence. They are deliberately independent of AQL text. const ( - OptimizerRuleFilterPushdown = "push_down_typed_filters" - OptimizerRuleTraversalSharing = "share_identical_traversals" - // OptimizerRuleRelationshipSemiJoin records that REQUIRED relationship - // matches became root-scoped bounded existence predicates rather than - // post-projection filters. - OptimizerRuleRelationshipSemiJoin = "root_relationship_semi_join" + OptimizerRuleFilterPushdown = "push_down_typed_filters" ) diff --git a/internal/dataframe/compiler/physical_diagnostics_test.go b/internal/dataframe/compiler/physical_diagnostics_test.go index f21d2e1b..ba1468a9 100644 --- a/internal/dataframe/compiler/physical_diagnostics_test.go +++ b/internal/dataframe/compiler/physical_diagnostics_test.go @@ -10,20 +10,17 @@ import ( func TestRichConsumerDiagnosticsClassifiesOnlyIdenticalExpressions(t *testing.T) { selector := spec.Selector{Steps: []spec.SelectorStep{{Field: "id"}}} - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "p", - Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []semantic.SemanticNode{{ - Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", - Aggregates: []semantic.SemanticAggregate{ - {Name: "count_a", Operation: "COUNT"}, - {Name: "count_b", Operation: "COUNT"}, - {Name: "distinct_ids", Operation: "DISTINCT_VALUES", Selector: &selector}, - }, - }}, - }, + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", + Aggregates: []semantic.SemanticAggregate{ + {Name: "count_a", Operation: "COUNT"}, + {Name: "count_b", Operation: "COUNT"}, + {Name: "distinct_ids", Operation: "DISTINCT_VALUES", Selector: &selector}, + }, + }}, + }, }) if err != nil { t.Fatal(err) @@ -47,9 +44,4 @@ func TestRichConsumerDiagnosticsClassifiesOnlyIdenticalExpressions(t *testing.T) if singletonGroups != 1 { t.Fatalf("non-identical aggregate singleton count = %d, diagnostics=%#v", singletonGroups, diagnostics.RichConsumerGroups) } - for _, state := range diagnostics.OptimizationPolicy.RuleStates { - if state.Rule == ir.PhysicalOptimizationRuleRichConsumerFusion && state.Enabled { - t.Fatalf("rich-consumer fusion unexpectedly enabled: %#v", state) - } - } } diff --git a/internal/dataframe/compiler/physical_lowering_test.go b/internal/dataframe/compiler/physical_lowering_test.go index 55006091..4e60b7cd 100644 --- a/internal/dataframe/compiler/physical_lowering_test.go +++ b/internal/dataframe/compiler/physical_lowering_test.go @@ -8,19 +8,16 @@ import ( "github.com/calypr/loom/internal/dataframe/spec" ) -func TestBuildPhysicalPlanUsesSemanticPlanDirectly(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - Root: semantic.SemanticNode{ - Alias: "root", - ResourceType: "Patient", - Children: []semantic.SemanticNode{{ - Alias: "specimen", - ResourceType: "Specimen", - EdgeLabel: "subject_Patient", - }}, - }, +func TestBuildPhysicalPlanUsesOutputPlanDirectly(t *testing.T) { + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", + ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", + ResourceType: "Specimen", + EdgeLabel: "subject_Patient", + }}, + }, }) if err != nil { t.Fatalf("BuildPhysicalPlan() error = %v", err) @@ -35,18 +32,11 @@ func TestBuildPhysicalPlanLowersRootSelection(t *testing.T) { if err != nil { t.Fatal(err) } - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - Root: semantic.SemanticNode{ - Alias: "root", - ResourceType: "Patient", - Fields: []semantic.SemanticField{{ - Name: "gender", - FieldRef: "gender", - Selector: selector, - }}, - }, + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", + ResourceType: "Patient", + Fields: []semantic.SemanticField{testSemanticField("gender", selector, "")}, + }, }) if err != nil { t.Fatalf("BuildPhysicalPlan() error = %v", err) diff --git a/internal/dataframe/compiler/physical_optimize_test.go b/internal/dataframe/compiler/physical_optimize_test.go index e0f598e4..e4d687e2 100644 --- a/internal/dataframe/compiler/physical_optimize_test.go +++ b/internal/dataframe/compiler/physical_optimize_test.go @@ -23,7 +23,7 @@ func TestOptimizePhysicalPlanSharesEquivalentTypedPrefixes(t *testing.T) { if optimized.SharedTraversalCount != 1 { t.Fatalf("shared traversal count = %d, want 1", optimized.SharedTraversalCount) } - if !slices.Contains(optimized.AppliedRules, OptimizerRuleTraversalSharing) { + if !slices.Contains(optimized.AppliedRules, optimize.OptimizerRuleTraversalSharing) { t.Fatalf("missing sharing rule: %#v", optimized.AppliedRules) } if len(optimized.Operations) != len(plan.Operations)+1 { @@ -48,44 +48,6 @@ func TestOptimizePhysicalPlanSharesEquivalentTypedPrefixes(t *testing.T) { } } -func TestOptimizePhysicalPlanSharesRepeatedLookups(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{Version: 1, Project: "p", Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}) - if err != nil { - t.Fatal(err) - } - plan.BindVars["lookup_a"] = "a" - plan.BindVars["lookup_b"] = "b" - source := ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalArrayCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "root", Path: []string{"payload", "identifier"}}} - key := ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "lookup_item", Path: []string{"value"}}} - value := key - returnOp := &plan.Operations[len(plan.Operations)-1] - for _, item := range []struct{ name, bind string }{{"a", "lookup_a"}, {"b", "lookup_b"}} { - expression := ir.PhysicalExpression{Kind: ir.PhysicalLookupExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, Lookup: &ir.PhysicalLookup{Source: source, ItemVariable: "lookup_item", ItemKey: key, ItemValue: value, MatchBindKey: item.bind}} - returnOp.Return.Projections = append(returnOp.Return.Projections, ir.PhysicalProjection{Name: item.name, Expression: &expression}) - } - optimized, err := optimize.OptimizePhysicalPlanWithPolicy(plan, ir.DefaultPhysicalOptimizationPolicy()) - if err != nil { - t.Fatal(err) - } - var lets, lookups int - for _, operation := range optimized.Operations { - if operation.Kind == ir.PhysicalExpressionLetOp { - lets++ - } - if operation.Kind != ir.PhysicalReturnOp || operation.Return == nil { - continue - } - for _, projection := range operation.Return.Projections { - if projection.Expression != nil && projection.Expression.Kind == ir.PhysicalObjectLookupExpression { - lookups++ - } - } - } - if lets != 1 || lookups != 2 { - t.Fatalf("shared lookup plan has %d lets and %d object lookups, want 1/2", lets, lookups) - } -} - func TestOptimizePhysicalPlanKeepsConsumerProjectionOffBroadSharedSet(t *testing.T) { plan := physicalScopedSiblingPlan(t) for _, set := range physicalSets(plan) { @@ -126,7 +88,7 @@ func TestOptimizePhysicalPlanReportsStructuralCostDecision(t *testing.T) { t.Fatalf("cost-policy decisions = %#v, want one decision", decisions) } decision := decisions[0] - if !decision.Enabled || decision.Rule != OptimizerRuleTraversalSharing { + if !decision.Enabled || decision.Rule != optimize.OptimizerRuleTraversalSharing { t.Fatalf("cost-policy decision = %#v, want enabled traversal sharing", decision) } if decision.EstimatedBaselineWork <= decision.EstimatedOptimizedWork || decision.EstimatedSavings <= 0 { @@ -197,14 +159,6 @@ func TestPhysicalOptimizationPolicyResolvesIndependentRules(t *testing.T) { if !policy.RuleEnabled(ir.PhysicalOptimizationRuleTraversalSharing) || !policy.RuleEnabled(ir.PhysicalOptimizationRuleCompactProjection) || policy.RuleEnabled(ir.PhysicalOptimizationRulePreparedSelectors) { t.Fatalf("default active rules = %#v", policy) } - for _, rule := range []ir.PhysicalOptimizationRule{ - ir.PhysicalOptimizationRuleNestedSharing, - ir.PhysicalOptimizationRuleRichConsumerFusion, - } { - if policy.RuleEnabled(rule) { - t.Fatalf("unimplemented rule %q was enabled by default", rule) - } - } policy = policy.WithRule(ir.PhysicalOptimizationRuleTraversalSharing, false).WithRule(ir.PhysicalOptimizationRulePreparedSelectors, false) if policy.RuleEnabled(ir.PhysicalOptimizationRuleTraversalSharing) || policy.RuleEnabled(ir.PhysicalOptimizationRulePreparedSelectors) { t.Fatalf("explicit rule overrides were ignored: %#v", policy.RuleOverrides) @@ -232,18 +186,17 @@ func TestOptimizePhysicalPlanReportsExplicitRuleDisable(t *testing.T) { func TestBuildGenericPhysicalPlanPolicyDisablesPreparedSelectors(t *testing.T) { status := spec.Selector{Steps: []spec.SelectorStep{{Field: "id"}}} - semantic := semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []semantic.SemanticNode{{ - Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", - Aggregates: []semantic.SemanticAggregate{{Name: "status_count", Operation: "COUNT_DISTINCT", Selector: &status}}, - Slices: []semantic.SemanticSlice{{Name: "representative", Limit: 1, Fields: []semantic.SemanticField{{Name: "status", Selector: status}}}}, - }}, - }, + output := semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", + Aggregates: []semantic.SemanticAggregate{{Name: "status_count", Operation: "COUNT_DISTINCT", Selector: &status}}, + Slices: []semantic.SemanticSlice{{Name: "representative", Limit: 1, Fields: []semantic.SemanticField{testSemanticField("status", status, "")}}}, + }}, + }, } policy := ir.DefaultPhysicalOptimizationPolicy().WithRule(ir.PhysicalOptimizationRuleCompactProjection, false).WithRule(ir.PhysicalOptimizationRulePreparedSelectors, false) - plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic, policy) + plan, err := lower.BuildGenericPhysicalPlanWithPolicy(output, semantic.ExecutionContext{Project: "p"}, policy) if err != nil { t.Fatal(err) } @@ -262,19 +215,18 @@ func TestBuildGenericPhysicalPlanPolicyDisablesPreparedSelectors(t *testing.T) { } func TestBuildGenericPhysicalPlanCompactOutputPolicy(t *testing.T) { - semantic := semantic.SemanticPlan{ - Version: 1, Project: "p", Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []semantic.SemanticNode{{Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", Aggregates: []semantic.SemanticAggregate{{Name: "count", Operation: "COUNT"}}}}, - }, + output := semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", Aggregates: []semantic.SemanticAggregate{{Name: "count", Operation: "COUNT"}}}}, + }, } fullPolicy := ir.DefaultPhysicalOptimizationPolicy().WithRule(ir.PhysicalOptimizationRuleCompactProjection, false) - full, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic, fullPolicy) + full, err := lower.BuildGenericPhysicalPlanWithPolicy(output, semantic.ExecutionContext{Project: "p"}, fullPolicy) if err != nil { t.Fatal(err) } compactPolicy := ir.DefaultPhysicalOptimizationPolicy() - compact, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic, compactPolicy) + compact, err := lower.BuildGenericPhysicalPlanWithPolicy(output, semantic.ExecutionContext{Project: "p"}, compactPolicy) if err != nil { t.Fatal(err) } @@ -409,13 +361,10 @@ func physicalSetIndex(plan ir.PhysicalPlan, set ir.PhysicalSet) int { func physicalScopedSiblingPlan(t *testing.T) ir.PhysicalPlan { t.Helper() - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{ - {Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", Fields: []semantic.SemanticField{{Name: "id", FieldRef: "Condition.id", Selector: mustPhysicalSelector(t, "id")}}}, - {Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", Fields: []semantic.SemanticField{{Name: "id", FieldRef: "Specimen.id", Selector: mustPhysicalSelector(t, "id")}}}, - }}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{ + {Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", Fields: []semantic.SemanticField{testSemanticField("id", mustPhysicalSelector(t, "id"), "")}}, + {Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", Fields: []semantic.SemanticField{testSemanticField("id", mustPhysicalSelector(t, "id"), "")}}, + }}}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } diff --git a/internal/dataframe/compiler/physical_plan_test.go b/internal/dataframe/compiler/physical_plan_test.go index 1394590f..14c9a339 100644 --- a/internal/dataframe/compiler/physical_plan_test.go +++ b/internal/dataframe/compiler/physical_plan_test.go @@ -167,7 +167,7 @@ func TestPhysicalPlanValidateRichExpressionRejectsUnsafeScopeAndShape(t *testing Variable: "files", Subplan: ir.PhysicalSubplan{ Captures: []string{"future"}, - Operations: []ir.PhysicalOperation{{Kind: ir.PhysicalDerivedLetOp, DerivedLet: &ir.PhysicalDerivedLet{Variable: "x", Expression: physicalExpressionPtr(physicalValueExpression(ir.PhysicalValue{Variable: "future"}, ir.PhysicalScalarCardinality))}}}, + Operations: []ir.PhysicalOperation{{Kind: ir.PhysicalExpressionLetOp, ExpressionLet: &ir.PhysicalExpressionLet{Variable: "x", Expression: physicalValueExpression(ir.PhysicalValue{Variable: "future"}, ir.PhysicalScalarCardinality)}}}, Return: physicalValueExpression(ir.PhysicalValue{Variable: "x"}, ir.PhysicalScalarCardinality), }, }} @@ -177,9 +177,9 @@ func TestPhysicalPlanValidateRichExpressionRejectsUnsafeScopeAndShape(t *testing plan = validPhysicalPlan() plan.Version = 2 - plan.Operations[3] = ir.PhysicalOperation{Kind: ir.PhysicalDerivedLetOp, DerivedLet: &ir.PhysicalDerivedLet{ + plan.Operations[3] = ir.PhysicalOperation{Kind: ir.PhysicalExpressionLetOp, ExpressionLet: &ir.PhysicalExpressionLet{ Variable: "pivot", - Expression: &ir.PhysicalExpression{Kind: ir.PhysicalPivotExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalEmptyOnNull, Pivot: &ir.PhysicalPivotMap{ + Expression: ir.PhysicalExpression{Kind: ir.PhysicalPivotExpression, Cardinality: ir.PhysicalObjectCardinality, NullBehavior: ir.PhysicalEmptyOnNull, Pivot: &ir.PhysicalPivotMap{ Source: ir.PhysicalValue{Variable: "specimen"}, ResourceType: "Specimen", KeySelector: mustPhysicalSelector(t, "type.coding[].display"), ValueSelector: mustPhysicalSelector(t, "type.coding[].display"), ColumnsBindKey: "edge_label", }}, diff --git a/internal/dataframe/compiler/physical_render_test.go b/internal/dataframe/compiler/physical_render_test.go index a95fd659..5b646684 100644 --- a/internal/dataframe/compiler/physical_render_test.go +++ b/internal/dataframe/compiler/physical_render_test.go @@ -11,20 +11,15 @@ import ( ) func TestRenderPhysicalPlanGenericNavigation(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{ - Alias: "root", - ResourceType: "Patient", - Children: []semantic.SemanticNode{{ - Alias: "specimen", - ResourceType: "Specimen", - EdgeLabel: "subject_Patient", - }}, - }, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", + ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", + ResourceType: "Specimen", + EdgeLabel: "subject_Patient", + }}, + }}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } @@ -77,20 +72,15 @@ func TestRenderPhysicalPlanGenericNavigation(t *testing.T) { } func TestRenderPhysicalPlanTraversalSetsPreserveRootRowGrain(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", Children: []semantic.SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - Children: []semantic.SemanticNode{{ - Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", - }}, + Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", }}, - }, - }) + }}, + }}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } @@ -122,12 +112,7 @@ func TestRenderPhysicalPlanTraversalSetsPreserveRootRowGrain(t *testing.T) { } func TestRenderPhysicalPlanIsDeterministicAndCopiesBindVars(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } @@ -157,10 +142,7 @@ func TestRenderPhysicalPlanIsDeterministicAndCopiesBindVars(t *testing.T) { } func TestRenderPhysicalPlanNestedObjectExpression(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } @@ -236,10 +218,7 @@ func TestRenderPhysicalPlanNestedObjectExpression(t *testing.T) { } func TestRenderPhysicalPlanObjectExpressionOmitsNullFields(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } @@ -273,10 +252,7 @@ func TestRenderPhysicalPlanObjectExpressionOmitsNullFields(t *testing.T) { } func TestPhysicalPlanValidateRejectsRecursiveObjectExpression(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } @@ -292,11 +268,7 @@ func TestPhysicalPlanValidateRejectsRecursiveObjectExpression(t *testing.T) { func TestRenderPhysicalPlanRejectsUnsupportedOrAmbiguousOperations(t *testing.T) { newPlan := func(t *testing.T) ir.PhysicalPlan { t.Helper() - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "project-1"}) if err != nil { t.Fatal(err) } @@ -365,11 +337,7 @@ func TestRenderPhysicalPlanRejectsUnsupportedOrAmbiguousOperations(t *testing.T) } func TestRenderPhysicalPlanRejectsMissingGenericScope(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "project-1"}) if err != nil { t.Fatal(err) } @@ -382,16 +350,12 @@ func TestRenderPhysicalPlanRejectsMissingGenericScope(t *testing.T) { } func TestRenderPhysicalPlanRejectsMisboundGenericEdgeTypeDiscriminator(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", - Children: []semantic.SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - }}, - }, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", + }}, + }}, semantic.ExecutionContext{Project: "project-1"}) if err != nil { t.Fatal(err) } @@ -408,11 +372,7 @@ func TestRenderPhysicalPlanRejectsMisboundGenericEdgeTypeDiscriminator(t *testin } func TestRenderPhysicalPlanKeepsCollectionAndProjectionValuesOutOfAQL(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, semantic.ExecutionContext{Project: "project-1"}) if err != nil { t.Fatal(err) } diff --git a/internal/dataframe/compiler/physical_required_match_test.go b/internal/dataframe/compiler/physical_required_match_test.go index 1870e2ba..cf55c09c 100644 --- a/internal/dataframe/compiler/physical_required_match_test.go +++ b/internal/dataframe/compiler/physical_required_match_test.go @@ -10,12 +10,9 @@ import ( ) func TestRenderPhysicalPlanRequiredInboundTraversalMatch(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ - Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", MatchMode: spec.TraversalMatchRequired, - }}}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ + Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", MatchMode: spec.TraversalMatchRequired, + }}}}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatal(err) } @@ -47,12 +44,9 @@ func TestRenderPhysicalPlanRequiredInboundTraversalMatch(t *testing.T) { } func TestRenderPhysicalPlanRequiredOutboundResearchStudyMatch(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "project-1", - Root: semantic.SemanticNode{Alias: "root", ResourceType: "ResearchSubject", Children: []semantic.SemanticNode{{ - Alias: "study", ResourceType: "ResearchStudy", EdgeLabel: "study", MatchMode: spec.TraversalMatchRequired, - }}}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "ResearchSubject", Children: []semantic.SemanticNode{{ + Alias: "study", ResourceType: "ResearchStudy", EdgeLabel: "study", MatchMode: spec.TraversalMatchRequired, + }}}}, semantic.ExecutionContext{Project: "project-1"}) if err != nil { t.Fatal(err) } @@ -73,13 +67,10 @@ func TestRenderPhysicalPlanRequiredOutboundResearchStudyMatch(t *testing.T) { } func TestBuildPhysicalPlanRequiredTraversalWithFilterUsesTypedPredicate(t *testing.T) { - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, Project: "project-1", - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ - Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", MatchMode: spec.TraversalMatchRequired, - Filters: []spec.TypedFilter{{FieldRef: "Condition.id", Selector: "id", FieldKind: spec.FilterString, Operator: spec.FilterExists}}, - }}}, - }) + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ + Alias: "condition", ResourceType: "Condition", EdgeLabel: "subject_Patient", MatchMode: spec.TraversalMatchRequired, + Filters: []spec.TypedFilter{{FieldRef: "Condition.id", Selector: "id", FieldKind: spec.FilterString, Operator: spec.FilterExists}}, + }}}}, semantic.ExecutionContext{Project: "project-1"}) if err != nil { t.Fatal(err) } diff --git a/internal/dataframe/compiler/physical_scope_constants.go b/internal/dataframe/compiler/physical_scope_constants.go deleted file mode 100644 index a7266b98..00000000 --- a/internal/dataframe/compiler/physical_scope_constants.go +++ /dev/null @@ -1,14 +0,0 @@ -package compiler - -const ( - datasetGenerationBindKey = "dataset_generation" - datasetGenerationField = "dataset_generation" - physicalScopeProjectBind = "project" - physicalScopeAllowedBind = "scope_allowed" - physicalScopeAuthPathsBind = "auth_resource_paths" - physicalScopeAuthPathsUnrestrictedBind = "auth_resource_paths_unrestricted" - physicalScopeAuthPathField = "auth_resource_path" - physicalScopeProjectField = "project" - physicalScopeDatasetGenerationBind = datasetGenerationBindKey - physicalScopeDatasetGenerationField = datasetGenerationField -) diff --git a/internal/dataframe/compiler/physical_scope_test.go b/internal/dataframe/compiler/physical_scope_test.go index 10065876..475dc3ff 100644 --- a/internal/dataframe/compiler/physical_scope_test.go +++ b/internal/dataframe/compiler/physical_scope_test.go @@ -52,7 +52,7 @@ func TestValidateGenericPhysicalPlanScopeRejectsMissingMisboundAndReorderedScope name: "misbound root project filter", mutate: func(t *testing.T, plan *ir.PhysicalPlan) { index := findProjectFilter(t, *plan, "root") - plan.Operations[index].Filter.Predicate.Right = &ir.PhysicalValue{BindKey: physicalScopeAllowedBind} + plan.Operations[index].Filter.Predicate.Right = &ir.PhysicalValue{BindKey: "scope_allowed"} }, want: "project scope filter at operation", }, @@ -86,7 +86,7 @@ func TestValidateGenericPhysicalPlanScopeRejectsMissingMisboundAndReorderedScope name: "misbound root auth allowed marker", mutate: func(t *testing.T, plan *ir.PhysicalPlan) { index := findTestAuthScopeEquality(t, *plan, "root_scope_allowed") - plan.Operations[index].Filter.Predicate.Right = &ir.PhysicalValue{BindKey: physicalScopeProjectBind} + plan.Operations[index].Filter.Predicate.Right = &ir.PhysicalValue{BindKey: "project"} }, want: "auth scope equality", }, @@ -97,7 +97,7 @@ func TestValidateGenericPhysicalPlanScopeRejectsMissingMisboundAndReorderedScope inputs := plan.Operations[index].DerivedLet.Inputs filtered := inputs[:0] for _, input := range inputs { - if input.Variable == "node_1" && physicalPathEquals(input.Path, []string{physicalScopeAuthPathField}) { + if input.Variable == "node_1" && physicalPathEquals(input.Path, []string{"auth_resource_path"}) { continue } filtered = append(filtered, input) @@ -130,20 +130,15 @@ func TestValidateGenericPhysicalPlanScopeRejectsMissingMisboundAndReorderedScope func genericScopePhysicalPlan(t *testing.T) ir.PhysicalPlan { t.Helper() - plan, err := buildGenericPhysicalPlan(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{ - Alias: "root", ResourceType: "Patient", + plan, err := buildGenericPhysicalPlanWithContext(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", Children: []semantic.SemanticNode{{ - Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", - Children: []semantic.SemanticNode{{ - Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", - }}, + Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", }}, - }, - }) + }}, + }}, semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}) if err != nil { t.Fatalf("BuildGenericPhysicalPlan() error = %v", err) } diff --git a/internal/dataframe/compiler/plan_test_helpers_test.go b/internal/dataframe/compiler/plan_test_helpers_test.go index 54fe48c9..bce6e7d8 100644 --- a/internal/dataframe/compiler/plan_test_helpers_test.go +++ b/internal/dataframe/compiler/plan_test_helpers_test.go @@ -3,9 +3,29 @@ package compiler import ( "github.com/calypr/loom/internal/dataframe/compiler/ir" "github.com/calypr/loom/internal/dataframe/compiler/lower" + "github.com/calypr/loom/internal/dataframe/expression" "github.com/calypr/loom/internal/dataframe/semantic" + "github.com/calypr/loom/internal/dataframe/spec" ) -func buildGenericPhysicalPlan(plan semantic.SemanticPlan) (ir.PhysicalPlan, error) { - return lower.BuildGenericPhysicalPlanWithPolicy(plan, ir.DefaultPhysicalOptimizationPolicy()) +func buildGenericPhysicalPlan(plan semantic.OutputPlan) (ir.PhysicalPlan, error) { + return lower.BuildGenericPhysicalPlanWithPolicy(plan, semantic.ExecutionContext{Project: "p"}, ir.DefaultPhysicalOptimizationPolicy()) +} + +func buildGenericPhysicalPlanWithContext(plan semantic.OutputPlan, context semantic.ExecutionContext) (ir.PhysicalPlan, error) { + return lower.BuildGenericPhysicalPlanWithPolicy(plan, context, ir.DefaultPhysicalOptimizationPolicy()) +} + +func testSemanticField(name string, selector spec.Selector, projection spec.ProjectionMode) semantic.SemanticField { + return semantic.SemanticField{ + Name: name, + Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: selector.CanonicalPath()})}, + Projection: projection, + } +} + +func testSemanticFieldWithFallback(name string, selector spec.Selector, fallback spec.Selector) semantic.SemanticField { + field := testSemanticField(name, selector, "") + field.Fallbacks = []semantic.SemanticExpression{{Expression: expression.Select(expression.SelectorRef{Path: fallback.CanonicalPath()})}} + return field } diff --git a/internal/dataframe/compiler/recipe_paging.go b/internal/dataframe/compiler/recipe_paging.go new file mode 100644 index 00000000..1a8d084a --- /dev/null +++ b/internal/dataframe/compiler/recipe_paging.go @@ -0,0 +1,153 @@ +package compiler + +import ( + "fmt" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/compiler/lower" + "github.com/calypr/loom/internal/dataframe/compiler/optimize" + "github.com/calypr/loom/internal/dataframe/compiler/render/aql" + "github.com/calypr/loom/internal/dataframe/recipe" +) + +const ( + RootPageAfterKeyBind = "loom_root_page_after_key" + RootPageSizeBind = genericPhysicalExecutionLimitBind + RootPageKeysBind = "loom_root_page_keys" +) + +// CompiledOutputPage contains immutable query templates for bounded root +// execution. RootKeys selects the next root page even when those roots emit no +// output rows. Rows executes the complete output for exactly that page. +type CompiledOutputPage struct { + RootKeysQuery string + RootKeysBindVars map[string]any + RowsQuery string + RowsBindVars map[string]any + RowsDiagnostics ir.CompilerPlanDiagnostics +} + +// CompileRecipeOutputPageWithPolicy builds typed key-discovery and selected- +// root templates. Page binds are compiler-owned and callers may only replace +// their values between executions. +func CompileRecipeOutputPageWithPolicy(output lower.CompiledRecipeOutput, bindings recipe.RuntimeBindings, pageSize int, policy ir.PhysicalOptimizationPolicy) (CompiledOutputPage, error) { + if pageSize < 1 { + return CompiledOutputPage{}, fmt.Errorf("root page size must be positive") + } + physical, err := optimizedOutputPlan(output, policy) + if err != nil { + return CompiledOutputPage{}, err + } + keysPlan, err := rootKeysPagePlan(physical, pageSize) + if err != nil { + return CompiledOutputPage{}, fmt.Errorf("build root-key page: %w", err) + } + rowsPlan, err := selectedRootsPlan(physical) + if err != nil { + return CompiledOutputPage{}, fmt.Errorf("build selected-root page: %w", err) + } + rowsPlan, err = withGenericPhysicalExecutionWindow(rowsPlan, 0) + if err != nil { + return CompiledOutputPage{}, fmt.Errorf("apply selected-root execution window: %w", err) + } + if bindings.IncludeAuthResourcePath { + if err := appendAuthResourcePathProjection(&rowsPlan); err != nil { + return CompiledOutputPage{}, err + } + } + keys, err := aql.RenderPhysicalPlan(keysPlan) + if err != nil { + return CompiledOutputPage{}, fmt.Errorf("render root-key page: %w", err) + } + rows, err := aql.RenderPhysicalPlan(rowsPlan) + if err != nil { + return CompiledOutputPage{}, fmt.Errorf("render selected-root page: %w", err) + } + return CompiledOutputPage{ + RootKeysQuery: keys.Query, RootKeysBindVars: keys.BindVars, + RowsQuery: rows.Query, RowsBindVars: rows.BindVars, RowsDiagnostics: physicalPlanDiagnostics(rowsPlan), + }, nil +} + +func optimizedOutputPlan(output lower.CompiledRecipeOutput, policy ir.PhysicalOptimizationPolicy) (ir.PhysicalPlan, error) { + if output.OptimizedPlan != nil { + return clonePhysicalPlan(*output.OptimizedPlan), nil + } + physical, err := optimize.OptimizePhysicalPlanWithPolicy(output.Plan, policy) + if err != nil { + return ir.PhysicalPlan{}, fmt.Errorf("optimize canonical recipe plan: %w", err) + } + return physical, nil +} + +func rootKeysPagePlan(plan ir.PhysicalPlan, pageSize int) (ir.PhysicalPlan, error) { + if err := ir.ValidateGenericPhysicalPlanScope(plan); err != nil { + return ir.PhysicalPlan{}, err + } + if len(plan.Operations) < 6 || plan.Operations[0].RootScan == nil { + return ir.PhysicalPlan{}, fmt.Errorf("generic physical plan requires a root scan and scope") + } + for _, key := range []string{RootPageAfterKeyBind, RootPageSizeBind} { + if _, exists := plan.BindVars[key]; exists { + return ir.PhysicalPlan{}, fmt.Errorf("root page bind %q is already defined", key) + } + } + out := clonePhysicalPlan(plan) + root := out.Operations[0].RootScan.Variable + insertAt := rootPageInsertionIndex(out.Operations) + out.BindVars[RootPageAfterKeyBind] = "" + out.BindVars[RootPageSizeBind] = pageSize + source := ir.PhysicalSource{SemanticNode: out.Source.SemanticNode, ResourceType: out.Source.ResourceType, SemanticField: "_key"} + left := ir.PhysicalValue{Variable: root, Path: []string{"_key"}} + right := ir.PhysicalValue{BindKey: RootPageAfterKeyBind} + out.Operations = append(append([]ir.PhysicalOperation(nil), out.Operations[:insertAt]...), + ir.PhysicalOperation{Kind: ir.PhysicalFilterOp, Source: source, Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "GT", Left: left, Right: &right}}}, + ir.PhysicalOperation{Kind: ir.PhysicalSortOp, Source: source, Sort: &ir.PhysicalSort{Value: left}}, + ir.PhysicalOperation{Kind: ir.PhysicalLimitOp, Source: source, Limit: &ir.PhysicalLimit{BindKey: RootPageSizeBind}}, + ir.PhysicalOperation{Kind: ir.PhysicalReturnOp, Source: source, Return: &ir.PhysicalReturn{Projections: []ir.PhysicalProjection{{Name: "_key", Value: left}}}}, + ) + if err := ir.ValidateGenericPhysicalPlanScope(out); err != nil { + return ir.PhysicalPlan{}, err + } + return out, nil +} + +func selectedRootsPlan(plan ir.PhysicalPlan) (ir.PhysicalPlan, error) { + if _, exists := plan.BindVars[RootPageKeysBind]; exists { + return ir.PhysicalPlan{}, fmt.Errorf("root page bind %q is already defined", RootPageKeysBind) + } + out := clonePhysicalPlan(plan) + root := out.Operations[0].RootScan.Variable + insertAt := rootPageInsertionIndex(out.Operations) + out.BindVars[RootPageKeysBind] = []string{} + left := ir.PhysicalValue{Variable: root, Path: []string{"_key"}} + right := ir.PhysicalValue{BindKey: RootPageKeysBind} + filter := ir.PhysicalOperation{ + Kind: ir.PhysicalFilterOp, + Source: ir.PhysicalSource{SemanticNode: out.Source.SemanticNode, ResourceType: out.Source.ResourceType, SemanticField: "_key"}, + Filter: &ir.PhysicalFilter{Predicate: ir.PhysicalPredicate{Operator: "IN", Left: left, Right: &right}}, + } + operations := make([]ir.PhysicalOperation, 0, len(out.Operations)+1) + operations = append(operations, out.Operations[:insertAt]...) + operations = append(operations, filter) + operations = append(operations, out.Operations[insertAt:]...) + out.Operations = operations + if err := ir.ValidateGenericPhysicalPlanScope(out); err != nil { + return ir.PhysicalPlan{}, err + } + return out, nil +} + +// rootPageInsertionIndex returns the boundary after scoped root predicates and +// before the first cardinality-changing or navigation operation. +func rootPageInsertionIndex(operations []ir.PhysicalOperation) int { + index := 5 // ROOT_SCAN plus the canonical four-operation root scope block. + for index < len(operations) { + operation := operations[index] + if operation.Kind != ir.PhysicalFilterOp { + break + } + index++ + } + return index +} diff --git a/internal/dataframe/compiler/recipe_paging_test.go b/internal/dataframe/compiler/recipe_paging_test.go new file mode 100644 index 00000000..4aeb15c8 --- /dev/null +++ b/internal/dataframe/compiler/recipe_paging_test.go @@ -0,0 +1,60 @@ +package compiler + +import ( + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/compiler/lower" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/semantic" + "github.com/calypr/loom/internal/dataframe/spec" +) + +func TestCompileRecipeOutputPageSelectsRootsBeforeExpansion(t *testing.T) { + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Fields: []semantic.SemanticField{{ + Name: "id", FieldRef: "Patient.id", + Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "id"})}, Projection: spec.ProjectionScalar, + }}, + }}) + if err != nil { + t.Fatal(err) + } + selector, err := spec.ParseSelector("extension[].url") + if err != nil { + t.Fatal(err) + } + unnest := ir.PhysicalOperation{Kind: ir.PhysicalUnnestOp, Unnest: &ir.PhysicalUnnest{ + InputVariable: "root", OutputVariable: "item", JoinMode: ir.PhysicalUnnestInner, + Expression: ir.PhysicalExpression{ + Kind: ir.PhysicalExtractExpression, Cardinality: ir.PhysicalArrayCardinality, NullBehavior: ir.PhysicalEmptyOnNull, + Extract: &ir.PhysicalExtract{Source: ir.PhysicalValue{Variable: "root", Path: []string{"payload"}}, ResourceType: "Patient", Selector: selector, ExecutionMode: ir.PhysicalSelectorConditionalArray}, + }, + }} + plan.Operations = append(plan.Operations, ir.PhysicalOperation{}) + copy(plan.Operations[6:], plan.Operations[5:]) + plan.Operations[5] = unnest + page, err := CompileRecipeOutputPageWithPolicy(lower.CompiledRecipeOutput{ + Name: "patients", RootResourceType: "Patient", Plan: plan, + }, recipe.RuntimeBindings{Project: "p"}, 25, ir.DefaultPhysicalOptimizationPolicy()) + if err != nil { + t.Fatal(err) + } + if got := page.RootKeysBindVars[RootPageSizeBind]; got != 25 { + t.Fatalf("root page size bind = %#v, want 25", got) + } + if strings.Contains(page.RootKeysQuery, "unnest") || strings.Contains(page.RootKeysQuery, "extension") { + t.Fatalf("root-key discovery crossed the expansion boundary:\n%s", page.RootKeysQuery) + } + keyFilter := strings.Index(page.RowsQuery, "root._key IN @"+RootPageKeysBind) + unnestIndex := strings.Index(page.RowsQuery, "FOR item IN") + if keyFilter < 0 || unnestIndex < 0 || keyFilter > unnestIndex { + t.Fatalf("selected-root filter was not rendered before UNNEST:\n%s", page.RowsQuery) + } + if !strings.Contains(page.RootKeysQuery, "root._key > @"+RootPageAfterKeyBind) { + t.Fatalf("root-key discovery is not a keyset query:\n%s", page.RootKeysQuery) + } +} diff --git a/internal/dataframe/compiler/render/aql/collections.go b/internal/dataframe/compiler/render/aql/collections.go index 4e5885ba..043540b2 100644 --- a/internal/dataframe/compiler/render/aql/collections.go +++ b/internal/dataframe/compiler/render/aql/collections.go @@ -199,7 +199,7 @@ func (r *physicalPlanRenderer) renderPivot(expression ir.PhysicalExpression) (st if pivot.ItemSource.CanonicalPath() == "" { return "", fmt.Errorf("pivot item source is required when item resource type is set") } - itemValues, sourceErr := r.renderSelectorArrayFromSource(item+".payload", pivot.ItemSource, false) + itemValues, sourceErr := r.renderSelectorArrayFromSource(item+".payload", pivot.ItemSource, false, false) if sourceErr != nil { return "", fmt.Errorf("pivot item source: %w", sourceErr) } @@ -286,7 +286,7 @@ func (r *physicalPlanRenderer) renderPivotSelector(resourceItem, item string, pr if itemScoped { source = item } - return r.renderSelectorArrayFromSource(source, selector, false) + return r.renderSelectorArrayFromSource(source, selector, false, false) } // renderAggregate emits reductions over either a correlated PhysicalSet or a @@ -368,6 +368,20 @@ func (r *physicalPlanRenderer) renderAggregate(expression ir.PhysicalExpression) case ir.PhysicalFirstAggregate: return "FIRST(" + flattened + ")", nil } + case ir.PhysicalContainsAllAggregate: + if aggregate.Value == nil { + return "", fmt.Errorf("aggregate operation %q requires a value expression", aggregate.Operation) + } + if aggregate.RequiredValuesBindKey == "" { + return "", fmt.Errorf("aggregate operation %q requires required values", aggregate.Operation) + } + values, err := r.renderAggregateValue(*aggregate.Value, items, perItem) + if err != nil { + return "", err + } + required := "@" + aggregate.RequiredValuesBindKey + item := r.newInternalVariable("aggregate_required_value") + return "LENGTH(" + required + ") == LENGTH(FOR " + item + " IN " + required + " FILTER POSITION(FLATTEN(" + values + "), " + item + ") RETURN 1)", nil } return "", fmt.Errorf("unsupported aggregate operation %q", aggregate.Operation) } diff --git a/internal/dataframe/compiler/render/aql/lookup_test.go b/internal/dataframe/compiler/render/aql/lookup_test.go deleted file mode 100644 index 6809985e..00000000 --- a/internal/dataframe/compiler/render/aql/lookup_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package aql_test - -import ( - "strings" - "testing" - - "github.com/calypr/loom/internal/dataframe/compiler/ir" - "github.com/calypr/loom/internal/dataframe/compiler/lower" - aql "github.com/calypr/loom/internal/dataframe/compiler/render/aql" - "github.com/calypr/loom/internal/dataframe/semantic" -) - -func TestRenderPhysicalLookupExpression(t *testing.T) { - value := func(path ...string) ir.PhysicalExpression { - return ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalScalarCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "dynamic_item", Path: path}} - } - lookup := ir.PhysicalExpression{ - Kind: ir.PhysicalLookupExpression, - Cardinality: ir.PhysicalScalarCardinality, - NullBehavior: ir.PhysicalPreserveNull, - Lookup: &ir.PhysicalLookup{ - Source: ir.PhysicalExpression{Kind: ir.PhysicalValueExpression, Cardinality: ir.PhysicalArrayCardinality, NullBehavior: ir.PhysicalPreserveNull, Value: &ir.PhysicalValue{Variable: "root", Path: []string{"items"}}}, - ItemVariable: "dynamic_item", - ItemKey: value("url"), - ItemValue: value("value"), - MatchBindKey: "dynamic_key", - }, - } - plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }, ir.DefaultPhysicalOptimizationPolicy()) - if err != nil { - t.Fatal(err) - } - plan.BindVars["dynamic_key"] = "x" - for index := range plan.Operations { - if plan.Operations[index].Kind == ir.PhysicalReturnOp { - plan.Operations[index].Return.Projections = []ir.PhysicalProjection{{Name: "lookup", Expression: &lookup}} - } - } - rendered, err := aql.RenderPhysicalPlan(plan) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(rendered.Query, "FIRST(FOR dynamic_item IN") || !strings.Contains(rendered.Query, "dynamic_item.url == @dynamic_key") || !strings.Contains(rendered.Query, "RETURN dynamic_item.value") { - t.Fatalf("unexpected lookup AQL: %s", rendered.Query) - } - if rendered.BindVars["dynamic_key"] != "x" { - t.Fatalf("lookup bind changed: %#v", rendered.BindVars) - } -} diff --git a/internal/dataframe/compiler/render/aql/navigation_layout.go b/internal/dataframe/compiler/render/aql/navigation_layout.go index 83530c5f..d95eb1d7 100644 --- a/internal/dataframe/compiler/render/aql/navigation_layout.go +++ b/internal/dataframe/compiler/render/aql/navigation_layout.go @@ -36,7 +36,7 @@ func buildNavigationRenderLayout(plan ir.PhysicalPlan) (physicalNavigationRender } index := 5 - for index < last && plan.Operations[index].Kind == ir.PhysicalFilterOp && plan.Operations[index].Filter.Expression != nil { + for index < last && plan.Operations[index].Kind == ir.PhysicalFilterOp { layout.rootPredicates = append(layout.rootPredicates, plan.Operations[index]) index++ } @@ -70,12 +70,12 @@ func buildNavigationRenderLayout(plan ir.PhysicalPlan) (physicalNavigationRender for index < last { operation := plan.Operations[index] if operation.Kind == ir.PhysicalExpressionLetOp { - layout.expressionLets = append(layout.expressionLets, operation) + layout.postWindow = append(layout.postWindow, physicalNavigationRenderItem{operation: operation}) index++ continue } if operation.Kind == ir.PhysicalSetOp { - layout.sets = append(layout.sets, *operation.Set) + layout.postWindow = append(layout.postWindow, physicalNavigationRenderItem{operation: operation}) index++ continue } @@ -97,7 +97,7 @@ func buildNavigationRenderLayout(plan ir.PhysicalPlan) (physicalNavigationRender if _, err := validateGenericNavigationScopeBlock(scope, traversal.TargetVariable, traversal.EdgeVariable, traversal.TargetVariable); err != nil { return physicalNavigationRenderLayout{}, fmt.Errorf("traversal at operation %d scope: %w", index, err) } - layout.traversals = append(layout.traversals, physicalNavigationTraversal{traversal: traversal, scope: scope}) + layout.postWindow = append(layout.postWindow, physicalNavigationRenderItem{operation: operation, traversalScope: scope}) index += 1 + traversalScopeLength } unnestVariables := map[string]struct{}{} @@ -107,7 +107,13 @@ func buildNavigationRenderLayout(plan ir.PhysicalPlan) (physicalNavigationRender unnestVariables[unnest.Ordinality] = struct{}{} } } - if err := validateNavigationReturnScope(layout.returnOp, layout.root.Variable, rootScopeVariable, unnestVariables); err != nil { + reductionVariables := map[string]struct{}{} + for _, operation := range plan.Operations { + if operation.Set != nil && operation.Set.Reduction != nil { + reductionVariables[operation.Set.Reduction.Variable] = struct{}{} + } + } + if err := validateNavigationReturnScope(layout.returnOp, layout.root.Variable, rootScopeVariable, unnestVariables, reductionVariables); err != nil { return physicalNavigationRenderLayout{}, err } return layout, nil @@ -253,7 +259,7 @@ func sameRenderPhysicalValue(left, right ir.PhysicalValue) bool { return true } -func validateNavigationReturnScope(returnOp ir.PhysicalReturn, rootVariable, rootScopeVariable string, unnestVariables map[string]struct{}) error { +func validateNavigationReturnScope(returnOp ir.PhysicalReturn, rootVariable, rootScopeVariable string, unnestVariables, reductionVariables map[string]struct{}) error { for _, projection := range returnOp.Projections { if projection.Expression != nil { continue @@ -265,6 +271,9 @@ func validateNavigationReturnScope(returnOp ir.PhysicalReturn, rootVariable, roo if _, ok := unnestVariables[projection.Value.Variable]; ok { continue } + if _, ok := reductionVariables[projection.Value.Variable]; ok { + continue + } return fmt.Errorf("RETURN projection %q references %q, but traversal variables are local to LET subqueries", projection.Name, projection.Value.Variable) } } diff --git a/internal/dataframe/compiler/render/aql/navigation_render.go b/internal/dataframe/compiler/render/aql/navigation_render.go index b7b1577c..edb419c3 100644 --- a/internal/dataframe/compiler/render/aql/navigation_render.go +++ b/internal/dataframe/compiler/render/aql/navigation_render.go @@ -199,6 +199,13 @@ func (r *physicalPlanRenderer) renderSet(set ir.PhysicalSet, index int) ([]strin lines[len(lines)-1] = " ))" } r.setVariables[set.Variable] = set.Variable + if set.Reduction != nil { + reduced, err := r.renderPhysicalSetReduction(*set.Reduction) + if err != nil { + return nil, err + } + lines = append(lines, reduced...) + } if set.Prepared != nil { prepared, err := r.renderPreparedSet(*set.Prepared) if err != nil { @@ -238,7 +245,7 @@ func (r *physicalPlanRenderer) renderPhysicalSetProjection(item string, projecti "resourceType: " + item + ".resourceType", } for _, field := range projection.Fields { - values, err := r.renderSelectorByMode(item+".payload", field.Selector, field.ExecutionMode) + values, err := r.renderSelectorByMode(item+".payload", field.Selector, field.ExecutionMode, field.Demand) if err != nil { return "", fmt.Errorf("projected field %q: %w", field.Name, err) } @@ -247,6 +254,29 @@ func (r *physicalPlanRenderer) renderPhysicalSetProjection(item string, projecti return "{ " + strings.Join(fields, ", ") + " }", nil } +func (r *physicalPlanRenderer) renderPhysicalSetReduction(reduction ir.PhysicalSetReduction) ([]string, error) { + if r.setVariables[reduction.SourceSetVariable] == "" { + return nil, fmt.Errorf("reduction source set %q has not been rendered", reduction.SourceSetVariable) + } + fields := make([]string, 0, len(reduction.Fields)) + for _, field := range reduction.Fields { + source := fmt.Sprintf("%s[*].%s", reduction.SourceSetVariable, field.SourceField) + var value string + switch field.Mode { + case ir.PhysicalSetReductionFirst: + value = "FIRST(FLATTEN(" + source + "))" + case ir.PhysicalSetReductionAll: + value = "FLATTEN(" + source + ")" + case ir.PhysicalSetReductionDistinct: + value = "SORTED_UNIQUE(FLATTEN(" + source + "))" + default: + return nil, fmt.Errorf("unsupported set reduction mode %q", field.Mode) + } + fields = append(fields, field.Name+": "+value) + } + return []string{fmt.Sprintf(" LET %s = { %s }", reduction.Variable, strings.Join(fields, ", "))}, nil +} + func (r *physicalPlanRenderer) renderPreparedSet(prepared ir.PhysicalPreparedSet) ([]string, error) { if r.setVariables[prepared.SourceSetVariable] == "" { return nil, fmt.Errorf("prepared source set %q has not been rendered", prepared.SourceSetVariable) @@ -266,7 +296,7 @@ func (r *physicalPlanRenderer) renderPreparedSet(prepared ir.PhysicalPreparedSet fmt.Sprintf("__loom_prepared_node: %s", item), } for _, field := range prepared.Fields { - values, err := r.renderSelectorArrayFromSource(item+".payload", field.Selector, false) + values, err := r.renderSelectorArrayFromSource(item+".payload", field.Selector, false, false) if err != nil { return nil, fmt.Errorf("prepared field %q: %w", field.Name, err) } @@ -333,6 +363,13 @@ func (r *physicalPlanRenderer) renderSharedSubset(set ir.PhysicalSet) ([]string, lines[len(lines)-1] = " ))" } r.setVariables[set.Variable] = set.Variable + if set.Reduction != nil { + reduced, err := r.renderPhysicalSetReduction(*set.Reduction) + if err != nil { + return nil, err + } + lines = append(lines, reduced...) + } if set.Prepared != nil { prepared, err := r.renderPreparedSet(*set.Prepared) if err != nil { @@ -362,6 +399,12 @@ func physicalPlanVariableNames(plan ir.PhysicalPlan) map[string]struct{} { variables[operation.ExpressionLet.Variable] = struct{}{} case ir.PhysicalSetOp: variables[operation.Set.Variable] = struct{}{} + if operation.Set.Reduction != nil { + variables[operation.Set.Reduction.Variable] = struct{}{} + } + if operation.Set.Prepared != nil { + variables[operation.Set.Prepared.Variable] = struct{}{} + } case ir.PhysicalUnnestOp: variables[operation.Unnest.InputVariable] = struct{}{} variables[operation.Unnest.OutputVariable] = struct{}{} diff --git a/internal/dataframe/compiler/render/aql/predicates.go b/internal/dataframe/compiler/render/aql/predicates.go index 41c7df8b..0dd7c22c 100644 --- a/internal/dataframe/compiler/render/aql/predicates.go +++ b/internal/dataframe/compiler/render/aql/predicates.go @@ -12,11 +12,8 @@ func (r *physicalPlanRenderer) renderPredicate(predicate ir.PhysicalPredicate) ( if predicate.LeftExpression != nil { return r.renderSelectorPredicate(predicate) } - if strings.ToUpper(strings.TrimSpace(predicate.Operator)) != "EQUALS" { - return "", fmt.Errorf("unsupported physical filter operator %q", predicate.Operator) - } if predicate.Right == nil { - return "", fmt.Errorf("EQUALS filter requires a right value") + return "", fmt.Errorf("physical filter operator %q requires a right value", predicate.Operator) } left, err := r.renderValue(predicate.Left) if err != nil { @@ -26,7 +23,16 @@ func (r *physicalPlanRenderer) renderPredicate(predicate ir.PhysicalPredicate) ( if err != nil { return "", err } - return left + " == " + right, nil + switch strings.ToUpper(strings.TrimSpace(predicate.Operator)) { + case "EQUALS": + return left + " == " + right, nil + case "IN": + return left + " IN " + right, nil + case "GT": + return left + " > " + right, nil + default: + return "", fmt.Errorf("unsupported direct physical filter operator %q", predicate.Operator) + } } func (r *physicalPlanRenderer) renderSelectorPredicate(predicate ir.PhysicalPredicate) (string, error) { diff --git a/internal/dataframe/compiler/render/aql/render.go b/internal/dataframe/compiler/render/aql/render.go index ff48fa25..6e05f982 100644 --- a/internal/dataframe/compiler/render/aql/render.go +++ b/internal/dataframe/compiler/render/aql/render.go @@ -87,24 +87,31 @@ func RenderPhysicalPlan(plan ir.PhysicalPlan) (RenderedPhysicalPlan, error) { } lines = append(lines, line...) } - for index, traversal := range layout.traversals { - line, err := renderer.renderTraversalSet(traversal, layout.root.Variable, index+1) - if err != nil { - return RenderedPhysicalPlan{}, fmt.Errorf("render traversal %d: %w", index+1, err) - } - lines = append(lines, line...) - } - for index, set := range layout.sets { - line, err := renderer.renderSet(set, index+1) - if err != nil { - return RenderedPhysicalPlan{}, fmt.Errorf("render child set %d: %w", index+1, err) - } - lines = append(lines, line...) - } - for index, operation := range layout.expressionLets { - line, err := renderer.renderExpressionLet(operation, " ") - if err != nil { - return RenderedPhysicalPlan{}, fmt.Errorf("render expression LET %d: %w", index, err) + traversalIndex, setIndex, expressionLetIndex := 0, 0, 0 + for _, item := range layout.postWindow { + var line []string + switch item.operation.Kind { + case ir.PhysicalTraversalOp: + traversalIndex++ + block := physicalNavigationTraversal{traversal: *item.operation.Traversal, scope: item.traversalScope} + line, err = renderer.renderTraversalSet(block, layout.root.Variable, traversalIndex) + if err != nil { + return RenderedPhysicalPlan{}, fmt.Errorf("render traversal %d: %w", traversalIndex, err) + } + case ir.PhysicalSetOp: + setIndex++ + line, err = renderer.renderSet(*item.operation.Set, setIndex) + if err != nil { + return RenderedPhysicalPlan{}, fmt.Errorf("render child set %d: %w", setIndex, err) + } + case ir.PhysicalExpressionLetOp: + line, err = renderer.renderExpressionLet(item.operation, " ") + if err != nil { + return RenderedPhysicalPlan{}, fmt.Errorf("render expression LET %d: %w", expressionLetIndex, err) + } + expressionLetIndex++ + default: + return RenderedPhysicalPlan{}, fmt.Errorf("render post-window operation %q: unsupported operation", item.operation.Kind) } lines = append(lines, line...) } @@ -200,20 +207,24 @@ func (r *physicalPlanRenderer) renderScopeOperation(operation ir.PhysicalOperati } // physicalNavigationRenderLayout is the intentionally narrow executable shape -// produced by BuildGenericPhysicalPlan. Traversals are retained as sets rather -// than emitted as top-level loops so the root scan remains the row grain. +// produced by BuildGenericPhysicalPlan. Post-window operations retain physical +// plan order because sets, traversals, and expression LETs may depend on values +// introduced by any preceding operation. type physicalNavigationRenderLayout struct { root ir.PhysicalRootScan rootScope []ir.PhysicalOperation rootPredicates []ir.PhysicalOperation rootWindow []ir.PhysicalOperation - traversals []physicalNavigationTraversal unnests []ir.PhysicalUnnest - sets []ir.PhysicalSet - expressionLets []ir.PhysicalOperation + postWindow []physicalNavigationRenderItem returnOp ir.PhysicalReturn } +type physicalNavigationRenderItem struct { + operation ir.PhysicalOperation + traversalScope []ir.PhysicalOperation +} + type physicalNavigationTraversal struct { traversal ir.PhysicalTraversal scope []ir.PhysicalOperation diff --git a/internal/dataframe/compiler/render/aql/selectors.go b/internal/dataframe/compiler/render/aql/selectors.go index 4611ce1c..89a922cb 100644 --- a/internal/dataframe/compiler/render/aql/selectors.go +++ b/internal/dataframe/compiler/render/aql/selectors.go @@ -51,7 +51,7 @@ func (r *physicalPlanRenderer) renderExtract(expression ir.PhysicalExpression) ( if setSource { break } - values, err := r.renderConditionalSelectorArray(source, extract.Selector) + values, err := r.renderConditionalSelectorArray(source, extract.Selector, expression.Cardinality != ir.PhysicalArrayCardinality) if err != nil { return "", err } @@ -66,7 +66,7 @@ func (r *physicalPlanRenderer) renderExtract(expression ir.PhysicalExpression) ( } arrays := make([]string, 0, 1+len(extract.Fallbacks)) for _, selector := range append([]spec.Selector{extract.Selector}, extract.Fallbacks...) { - array, err := r.renderSelectorArrayFromSource(source, selector, setSource) + array, err := r.renderSelectorArrayFromSource(source, selector, setSource, false) if err != nil { return "", err } @@ -88,17 +88,18 @@ func (r *physicalPlanRenderer) renderExtract(expression ir.PhysicalExpression) ( return "FIRST(" + values + ")", nil } -func (r *physicalPlanRenderer) renderSelectorByMode(source string, selector spec.Selector, mode ir.PhysicalSelectorExecutionMode) (string, error) { +func (r *physicalPlanRenderer) renderSelectorByMode(source string, selector spec.Selector, mode ir.PhysicalSelectorExecutionMode, demand ir.PhysicalSelectorValueDemand) (string, error) { if mode == ir.PhysicalSelectorDirectScalar && selectorHasNoArrays(selector) && selector.Filter == nil { - return "(FOR __loom_value IN [" + compileDirectExpr(source, selector.Steps) + "] FILTER __loom_value != null RETURN __loom_value)", nil + value := compileDirectExpr(source, selector.Steps) + return "(" + value + " == null ? [] : [" + value + "])", nil } if mode == ir.PhysicalSelectorConditionalArray && selectorHasIteratedArray(selector) && selector.Filter == nil { - return r.renderConditionalSelectorArray(source, selector) + return r.renderConditionalSelectorArray(source, selector, demand == ir.PhysicalSelectorFirstValue) } - return r.renderSelectorArrayFromSource(source, selector, false) + return r.renderSelectorArrayFromSource(source, selector, false, demand == ir.PhysicalSelectorFirstValue) } -func (r *physicalPlanRenderer) renderConditionalSelectorArray(source string, selector spec.Selector) (string, error) { +func (r *physicalPlanRenderer) renderConditionalSelectorArray(source string, selector spec.Selector, firstOnly bool) (string, error) { if len(selector.Steps) == 0 { return "", fmt.Errorf("selector is required") } @@ -117,11 +118,15 @@ func (r *physicalPlanRenderer) renderConditionalSelectorArray(source string, sel } current = next } - lines = append(lines, "LET __value = "+extractFinalExpr(current, last), "FILTER __value != null", "RETURN __value") + lines = append(lines, "LET __value = "+extractFinalExpr(current, last), "FILTER __value != null") + if firstOnly { + lines = append(lines, "LIMIT 1") + } + lines = append(lines, "RETURN __value") return "(\n " + strings.Join(lines, "\n ") + "\n )", nil } -func (r *physicalPlanRenderer) renderSelectorArrayFromSource(source string, selector spec.Selector, setSource bool) (string, error) { +func (r *physicalPlanRenderer) renderSelectorArrayFromSource(source string, selector spec.Selector, setSource, firstOnly bool) (string, error) { if len(selector.Steps) == 0 { return "", fmt.Errorf("selector is required") } @@ -147,7 +152,11 @@ func (r *physicalPlanRenderer) renderSelectorArrayFromSource(source string, sele r.bindVars[key] = selector.Filter.Needle lines = append(lines, fmt.Sprintf(" FILTER CONTAINS(%s.%s ? %s.%s : \"\", @%s)", current, selector.Filter.Field, current, selector.Filter.Field, key)) } - lines = append(lines, " LET __value = "+extractFinalExpr(current, last), " FILTER __value != null", " RETURN __value") + lines = append(lines, " LET __value = "+extractFinalExpr(current, last), " FILTER __value != null") + if firstOnly { + lines = append(lines, " LIMIT 1") + } + lines = append(lines, " RETURN __value") return "(\n " + strings.Join(lines, "\n ") + "\n )", nil } diff --git a/internal/dataframe/compiler/render/aql/unnest_test.go b/internal/dataframe/compiler/render/aql/unnest_test.go index aebccba5..7b5e0810 100644 --- a/internal/dataframe/compiler/render/aql/unnest_test.go +++ b/internal/dataframe/compiler/render/aql/unnest_test.go @@ -63,12 +63,11 @@ func TestRenderPhysicalPlanRejectsUnnestAfterRootWindow(t *testing.T) { func genericUnnestPlan(t *testing.T, mode ir.PhysicalUnnestJoinMode, ordinality string) ir.PhysicalPlan { t.Helper() - plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic.SemanticPlan{ - Version: 1, - Project: "project-1", - AuthResourcePaths: []string{"/programs/p1"}, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }, ir.DefaultPhysicalOptimizationPolicy()) + plan, err := lower.BuildGenericPhysicalPlanWithPolicy( + semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, + semantic.ExecutionContext{Project: "project-1", AuthResourcePaths: []string{"/programs/p1"}}, + ir.DefaultPhysicalOptimizationPolicy(), + ) if err != nil { t.Fatal(err) } diff --git a/internal/dataframe/compiler/render/aql/validation.go b/internal/dataframe/compiler/render/aql/validation.go index f74287c1..11aaad0f 100644 --- a/internal/dataframe/compiler/render/aql/validation.go +++ b/internal/dataframe/compiler/render/aql/validation.go @@ -141,11 +141,13 @@ func validateRenderableOperation(operation ir.PhysicalOperation, collectionKeys if operation.Filter.Expression != nil { return validateRenderablePredicateExpression(*operation.Filter.Expression, collectionKeys) } - if strings.ToUpper(strings.TrimSpace(operation.Filter.Predicate.Operator)) != "EQUALS" { + switch strings.ToUpper(strings.TrimSpace(operation.Filter.Predicate.Operator)) { + case "EQUALS", "IN", "GT": + default: return fmt.Errorf("unsupported physical filter operator %q", operation.Filter.Predicate.Operator) } if operation.Filter.Predicate.Right == nil { - return fmt.Errorf("EQUALS filter requires a right value") + return fmt.Errorf("physical filter operator %q requires a right value", operation.Filter.Predicate.Operator) } if err := checkValue(operation.Filter.Predicate.Left); err != nil { return err @@ -176,7 +178,7 @@ func validateRenderableOperation(operation ir.PhysicalOperation, collectionKeys case ir.PhysicalReturnOp: for _, projection := range operation.Return.Projections { if projection.Expression != nil { - if projection.Expression.Kind != ir.PhysicalValueExpression && projection.Expression.Kind != ir.PhysicalExtractExpression && projection.Expression.Kind != ir.PhysicalAggregateExpression && projection.Expression.Kind != ir.PhysicalPivotExpression && projection.Expression.Kind != ir.PhysicalSliceExpression && projection.Expression.Kind != ir.PhysicalLookupExpression && projection.Expression.Kind != ir.PhysicalObjectLookupExpression && projection.Expression.Kind != ir.PhysicalKeyedMapExpression && projection.Expression.Kind != ir.PhysicalObjectKeysExpression && projection.Expression.Kind != ir.PhysicalKeySetExpression && projection.Expression.Kind != ir.PhysicalObjectExpression && projection.Expression.Kind != ir.PhysicalCallExpression { + if projection.Expression.Kind != ir.PhysicalValueExpression && projection.Expression.Kind != ir.PhysicalExtractExpression && projection.Expression.Kind != ir.PhysicalAggregateExpression && projection.Expression.Kind != ir.PhysicalPivotExpression && projection.Expression.Kind != ir.PhysicalSliceExpression && projection.Expression.Kind != ir.PhysicalObjectLookupExpression && projection.Expression.Kind != ir.PhysicalKeyedMapExpression && projection.Expression.Kind != ir.PhysicalObjectKeysExpression && projection.Expression.Kind != ir.PhysicalKeySetExpression && projection.Expression.Kind != ir.PhysicalObjectExpression && projection.Expression.Kind != ir.PhysicalCallExpression { return fmt.Errorf("unsupported physical return expression kind %q", projection.Expression.Kind) } continue diff --git a/internal/dataframe/compiler/render/aql/values.go b/internal/dataframe/compiler/render/aql/values.go index 4748c381..0f1f090e 100644 --- a/internal/dataframe/compiler/render/aql/values.go +++ b/internal/dataframe/compiler/render/aql/values.go @@ -24,8 +24,6 @@ func (r *physicalPlanRenderer) renderExpression(expression ir.PhysicalExpression return r.renderPivot(expression) case ir.PhysicalSliceExpression: return r.renderSlice(expression) - case ir.PhysicalLookupExpression: - return r.renderLookup(expression) case ir.PhysicalObjectLookupExpression: if expression.ObjectLookup == nil { return "", fmt.Errorf("OBJECT_LOOKUP expression is missing payload") @@ -113,39 +111,6 @@ func (r *physicalPlanRenderer) renderKeyedMap(expression ir.PhysicalExpression) )`, item, sourceLoop, key, valueExpression, values), nil } -// renderLookup emits the sole canonical AQL lowering for a bounded dynamic -// key/value projection. AQL's FOR-over-null behavior supplies the empty -// source semantics; FIRST preserves the historical scalar-column contract -// when a key is absent or appears more than once. -func (r *physicalPlanRenderer) renderLookup(expression ir.PhysicalExpression) (string, error) { - lookup := expression.Lookup - if lookup == nil { - return "", fmt.Errorf("LOOKUP expression is missing payload") - } - source, err := r.renderExpression(lookup.Source) - if err != nil { - return "", fmt.Errorf("lookup source: %w", err) - } - key, err := r.renderExpression(lookup.ItemKey) - if err != nil { - return "", fmt.Errorf("lookup item key: %w", err) - } - value, err := r.renderExpression(lookup.ItemValue) - if err != nil { - return "", fmt.Errorf("lookup item value: %w", err) - } - if lookup.MatchBindKey == "" { - return "", fmt.Errorf("lookup match bind key is required") - } - if _, ok := r.bindVars[lookup.MatchBindKey]; !ok { - return "", fmt.Errorf("lookup match bind key %q is not defined", lookup.MatchBindKey) - } - // AQL FOR over a null array produces no rows, which is the canonical - // missing-key/null projection behavior. Keep the source expression single - // evaluated per lookup instead of duplicating it in a null ternary. - return fmt.Sprintf("FIRST(FOR %s IN %s FILTER %s == @%s RETURN %s)", lookup.ItemVariable, source, key, lookup.MatchBindKey, value), nil -} - func (r *physicalPlanRenderer) renderKeySet(expression ir.PhysicalExpression) (string, error) { keySet := expression.KeySet if keySet == nil { diff --git a/internal/dataframe/compiler/selection_semantics_test.go b/internal/dataframe/compiler/selection_semantics_test.go index 8941f6c9..08fb9f67 100644 --- a/internal/dataframe/compiler/selection_semantics_test.go +++ b/internal/dataframe/compiler/selection_semantics_test.go @@ -9,7 +9,7 @@ import ( func TestResolveSemanticFieldScalarAuto(t *testing.T) { selector, _ := spec.ParseSelector("gender") - got, err := semantic.ResolveSemanticField("Patient", "root", 0, semantic.SemanticField{Name: "gender", Selector: selector}) + got, err := semantic.ResolveSemanticField("Patient", "root", 0, testSemanticField("gender", selector, "")) if err != nil { t.Fatalf("ResolveSemanticField: %v", err) } @@ -20,7 +20,7 @@ func TestResolveSemanticFieldScalarAuto(t *testing.T) { func TestResolveSemanticFieldDetectsRepeatedAncestor(t *testing.T) { selector, _ := spec.ParseSelector("name[].family") - got, err := semantic.ResolveSemanticField("Patient", "root", 0, semantic.SemanticField{Name: "family", Selector: selector}) + got, err := semantic.ResolveSemanticField("Patient", "root", 0, testSemanticField("family", selector, "")) if err != nil { t.Fatalf("ResolveSemanticField: %v", err) } @@ -43,7 +43,8 @@ func TestResolveSemanticFieldValueModes(t *testing.T) { {"DISTINCT", spec.ProjectionDistinctArray}, } for _, test := range tests { - got, err := semantic.ResolveSemanticField("Patient", "root", 0, semantic.SemanticField{Name: "id", Selector: selector, ValueMode: test.mode}) + projection := map[string]spec.ProjectionMode{"FIRST": spec.ProjectionFirst, "ALL": spec.ProjectionArray, "DISTINCT": spec.ProjectionDistinctArray}[test.mode] + got, err := semantic.ResolveSemanticField("Patient", "root", 0, testSemanticField("id", selector, projection)) if err != nil { t.Errorf("mode %s: %v", test.mode, err) continue @@ -63,10 +64,10 @@ func TestResolveSemanticFieldRejectsInvalidSemantics(t *testing.T) { resourceType string field semantic.SemanticField }{ - {"unknown resource", "Imaginary", semantic.SemanticField{Name: "x", Selector: valid}}, - {"unknown path", "Patient", semantic.SemanticField{Name: "x", Selector: missing}}, - {"invalid value mode", "Patient", semantic.SemanticField{Name: "x", Selector: valid, ValueMode: "SCALAR"}}, - {"implicit array traversal", "Patient", semantic.SemanticField{Name: "x", Selector: implicitArray}}, + {"unknown resource", "Imaginary", testSemanticField("x", valid, "")}, + {"unknown path", "Patient", testSemanticField("x", missing, "")}, + {"invalid value mode", "Patient", testSemanticField("x", valid, "invalid")}, + {"implicit array traversal", "Patient", testSemanticField("x", implicitArray, "")}, {"empty selector", "Patient", semantic.SemanticField{Name: "x"}}, } for _, test := range tests { diff --git a/internal/dataframe/compiler/selector_first_limit_test.go b/internal/dataframe/compiler/selector_first_limit_test.go new file mode 100644 index 00000000..8698fa60 --- /dev/null +++ b/internal/dataframe/compiler/selector_first_limit_test.go @@ -0,0 +1,91 @@ +package compiler + +import ( + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/compiler/render/aql" + "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/semantic" + "github.com/calypr/loom/internal/dataframe/spec" +) + +func TestRenderFirstProjectionStopsSelectorAfterFirstValue(t *testing.T) { + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Fields: []semantic.SemanticField{{ + Name: "extension_url", FieldRef: "Patient.extension[].url", + Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "extension[].url"})}, + Projection: spec.ProjectionFirst, + }}, + }}) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(rendered.Query, "FILTER __value != null\n LIMIT 1\n RETURN __value") { + t.Fatalf("FIRST selector does not stop after its first value:\n%s", rendered.Query) + } +} + +func TestRenderArrayProjectionRetainsEverySelectorValue(t *testing.T) { + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Fields: []semantic.SemanticField{{ + Name: "extension_urls", FieldRef: "Patient.extension[].url", + Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "extension[].url"})}, + Projection: spec.ProjectionArray, + }}, + }}) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + if strings.Contains(rendered.Query, "LIMIT 1") { + t.Fatalf("array selector was truncated:\n%s", rendered.Query) + } +} + +func TestRenderChildSelectorLimitsOnlyWhenEveryConsumerUsesFirst(t *testing.T) { + build := func(projections ...spec.ProjectionMode) string { + t.Helper() + fields := make([]semantic.SemanticField, 0, len(projections)) + for index, projection := range projections { + fields = append(fields, semantic.SemanticField{ + Name: "title_" + string(rune('a'+index)), FieldRef: "DocumentReference.content[].attachment.title", + Expr: semantic.SemanticExpression{Expression: expression.Select(expression.SelectorRef{Path: "content[].attachment.title"})}, + Projection: projection, + }) + } + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{ + Alias: "root", ResourceType: "Patient", + Children: []semantic.SemanticNode{{ + Alias: "document", ResourceType: "DocumentReference", EdgeLabel: "subject_Patient", Fields: fields, + }}, + }}) + if err != nil { + t.Fatal(err) + } + rendered, err := aql.RenderPhysicalPlan(plan) + if err != nil { + t.Fatal(err) + } + return rendered.Query + } + + firstOnly := build(spec.ProjectionFirst) + if !strings.Contains(firstOnly, "FILTER __value != null\n LIMIT 1\n RETURN __value") { + t.Fatalf("first-only child selector does not stop early:\n%s", firstOnly) + } + + sharedWithArray := build(spec.ProjectionFirst, spec.ProjectionArray) + if strings.Contains(sharedWithArray, "LIMIT 1") { + t.Fatalf("shared all-values child selector was truncated:\n%s", sharedWithArray) + } +} diff --git a/internal/dataframe/compiler/semantic_validation_test.go b/internal/dataframe/compiler/semantic_validation_test.go index ffbe9b59..26ae290d 100644 --- a/internal/dataframe/compiler/semantic_validation_test.go +++ b/internal/dataframe/compiler/semantic_validation_test.go @@ -14,7 +14,7 @@ func TestValidateSemanticGraphAcceptsGeneratedAcyclicGraph(t *testing.T) { Alias: "file", ResourceType: "DocumentReference", EdgeLabel: "subject_Specimen", }}, }) - if err := semantic.ValidateSemanticGraph(plan); err != nil { + if err := semantic.ValidateSemanticGraph(plan.Root); err != nil { t.Fatal(err) } } @@ -28,7 +28,7 @@ func TestValidateSemanticGraphRejectsDuplicateAlias(t *testing.T) { } func TestValidateSemanticGraphRejectsUnknownRootAndTraversal(t *testing.T) { - unknownRoot := semantic.SemanticPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "InventedResource"}} + unknownRoot := semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "InventedResource"}} assertSemanticValidationError(t, unknownRoot, "root resource type \"InventedResource\"") unknownTraversal := semanticValidationPlan(semantic.SemanticNode{ @@ -38,30 +38,30 @@ func TestValidateSemanticGraphRejectsUnknownRootAndTraversal(t *testing.T) { } func TestValidateSemanticGraphAcceptsRepeatedSelfReference(t *testing.T) { - plan := semantic.SemanticPlan{Root: semantic.SemanticNode{ + plan := semantic.OutputPlan{Root: semantic.SemanticNode{ Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ Alias: "linked", ResourceType: "Patient", EdgeLabel: "link_other_Patient", Children: []semantic.SemanticNode{{Alias: "linked_again", ResourceType: "Patient", EdgeLabel: "link_other_Patient"}}, }}, }} - if err := semantic.ValidateSemanticGraph(plan); err != nil { + if err := semantic.ValidateSemanticGraph(plan.Root); err != nil { t.Fatalf("repeated self-reference should remain a valid finite query: %v", err) } } func TestValidateSemanticGraphAllowsOneHopSelfReference(t *testing.T) { - plan := semantic.SemanticPlan{Root: semantic.SemanticNode{ + plan := semantic.OutputPlan{Root: semantic.SemanticNode{ Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{Alias: "linked", ResourceType: "Patient", EdgeLabel: "link_other_Patient"}}, }} - if err := semantic.ValidateSemanticGraph(plan); err != nil { + if err := semantic.ValidateSemanticGraph(plan.Root); err != nil { t.Fatalf("one-hop self reference should remain a valid finite query: %v", err) } } func TestValidateSemanticGraphAcceptsMoreThanFourHops(t *testing.T) { - plan := semantic.SemanticPlan{Root: semantic.SemanticNode{ + plan := semantic.OutputPlan{Root: semantic.SemanticNode{ Alias: "root", ResourceType: "Patient", Children: []semantic.SemanticNode{{ Alias: "specimen", ResourceType: "Specimen", EdgeLabel: "subject_Patient", @@ -79,23 +79,23 @@ func TestValidateSemanticGraphAcceptsMoreThanFourHops(t *testing.T) { }}, }}, }} - if err := semantic.ValidateSemanticGraph(plan); err != nil { + if err := semantic.ValidateSemanticGraph(plan.Root); err != nil { t.Fatalf("finite route longer than four hops should remain valid: %v", err) } } func TestValidateSemanticGraphRejectsMalformedRoot(t *testing.T) { - assertSemanticValidationError(t, semantic.SemanticPlan{Root: semantic.SemanticNode{Alias: "patient", ResourceType: "Patient"}}, "root alias must be") - assertSemanticValidationError(t, semantic.SemanticPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", EdgeLabel: "subject"}}, "must not declare edge label") + assertSemanticValidationError(t, semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "patient", ResourceType: "Patient"}}, "root alias must be") + assertSemanticValidationError(t, semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", EdgeLabel: "subject"}}, "must not declare edge label") } -func semanticValidationPlan(children ...semantic.SemanticNode) semantic.SemanticPlan { - return semantic.SemanticPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: children}} +func semanticValidationPlan(children ...semantic.SemanticNode) semantic.OutputPlan { + return semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient", Children: children}} } -func assertSemanticValidationError(t *testing.T, plan semantic.SemanticPlan, contains string) { +func assertSemanticValidationError(t *testing.T, plan semantic.OutputPlan, contains string) { t.Helper() - err := semantic.ValidateSemanticGraph(plan) + err := semantic.ValidateSemanticGraph(plan.Root) if err == nil || !strings.Contains(err.Error(), contains) { t.Fatalf("error = %v, want substring %q", err, contains) } diff --git a/internal/dataframe/errors/errors.go b/internal/dataframe/errors/errors.go index 3a5ec898..3e1b67c7 100644 --- a/internal/dataframe/errors/errors.go +++ b/internal/dataframe/errors/errors.go @@ -59,11 +59,13 @@ const ( CodeOutputEncodingFailed ErrorCode = "OUTPUT_ENCODING_FAILED" CodeDynamicSchemaDrift ErrorCode = "DYNAMIC_SCHEMA_DRIFT" CodeRecipeContractViolation ErrorCode = "RECIPE_CONTRACT_VIOLATION" - CodeFederationIncompatible ErrorCode = "FEDERATION_INCOMPATIBLE" CodeInvalidSelector ErrorCode = "INVALID_SELECTOR" CodeReceiptStoreUnavailable ErrorCode = "RECEIPT_STORE_UNAVAILABLE" CodePreviewTimeout ErrorCode = "PREVIEW_TIMEOUT" CodePreviewResponseTooLarge ErrorCode = "PREVIEW_RESPONSE_TOO_LARGE" + CodeQueryMemoryLimitExceeded ErrorCode = "QUERY_MEMORY_LIMIT_EXCEEDED" + CodeQueryResourceLimitExceeded ErrorCode = "QUERY_RESOURCE_LIMIT_EXCEEDED" + CodeQueryBackendOutOfMemory ErrorCode = "QUERY_BACKEND_OUT_OF_MEMORY" ) // UserError is the semantic error contract shared by GraphQL, preview, and @@ -349,8 +351,6 @@ func defaultMessage(code ErrorCode) string { return "runtime dataframe fields could not be reconciled" case CodeRecipeContractViolation: return "the published output violates its recipe contract" - case CodeFederationIncompatible: - return "published sources cannot share a logical schema" case CodeInvalidSelector: return "a complete dataframe selector is required" case CodeReceiptStoreUnavailable: @@ -359,6 +359,12 @@ func defaultMessage(code ErrorCode) string { return "the preview took too long to complete" case CodePreviewResponseTooLarge: return "the preview response is too large" + case CodeQueryMemoryLimitExceeded: + return "the dataframe query exceeded the database's configured memory limit" + case CodeQueryResourceLimitExceeded: + return "the dataframe query exceeded a configured database resource limit" + case CodeQueryBackendOutOfMemory: + return "the dataframe database ran out of memory while executing the query" default: return "internal server error" } diff --git a/internal/dataframe/errors/errors_test.go b/internal/dataframe/errors/errors_test.go index 57837618..a58667a6 100644 --- a/internal/dataframe/errors/errors_test.go +++ b/internal/dataframe/errors/errors_test.go @@ -21,9 +21,9 @@ func TestErrorCodesAreUniqueAndStable(t *testing.T) { CodeGenerationLoadIncomplete, CodeGenerationActivationUnknown, CodeInvalidGenerationFile, CodeDuplicateGenerationFile, CodePublicationInProgress, CodePublicationConflict, CodePublicationLeaseLost, CodePublicationFailed, CodeOutputEncodingFailed, - CodeDynamicSchemaDrift, CodeRecipeContractViolation, CodeFederationIncompatible, + CodeDynamicSchemaDrift, CodeRecipeContractViolation, CodeInvalidSelector, CodeReceiptStoreUnavailable, CodePreviewTimeout, - CodePreviewResponseTooLarge, + CodePreviewResponseTooLarge, CodeQueryMemoryLimitExceeded, CodeQueryResourceLimitExceeded, CodeQueryBackendOutOfMemory, } seen := make(map[ErrorCode]struct{}, len(codes)) for _, code := range codes { diff --git a/internal/dataframe/runtime/auth_scope_test.go b/internal/dataframe/execution/auth_scope_test.go similarity index 74% rename from internal/dataframe/runtime/auth_scope_test.go rename to internal/dataframe/execution/auth_scope_test.go index 7b5f8bb9..10337787 100644 --- a/internal/dataframe/runtime/auth_scope_test.go +++ b/internal/dataframe/execution/auth_scope_test.go @@ -1,4 +1,4 @@ -package runtime +package execution import ( "testing" @@ -11,12 +11,11 @@ import ( ) func TestGenericPhysicalPlanRestrictedEmptyScopeBindsFalse(t *testing.T) { - plan, err := lower.BuildGenericPhysicalPlanWithPolicy(semantic.SemanticPlan{ - Version: 1, - Project: "P1", - AuthScopeMode: authscope.ReadScopeRestricted, - Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}, - }, ir.DefaultPhysicalOptimizationPolicy()) + plan, err := lower.BuildGenericPhysicalPlanWithPolicy( + semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}, + semantic.ExecutionContext{Project: "P1", AuthScopeMode: authscope.ReadScopeRestricted}, + ir.DefaultPhysicalOptimizationPolicy(), + ) if err != nil { t.Fatal(err) } diff --git a/internal/dataframe/recipe/engine/control.go b/internal/dataframe/execution/control.go similarity index 96% rename from internal/dataframe/recipe/engine/control.go rename to internal/dataframe/execution/control.go index 3059cea3..ccead323 100644 --- a/internal/dataframe/recipe/engine/control.go +++ b/internal/dataframe/execution/control.go @@ -1,4 +1,4 @@ -package engine +package execution import ( "context" @@ -10,7 +10,6 @@ import ( dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" "github.com/calypr/loom/internal/dataframe/recipe" recipeexec "github.com/calypr/loom/internal/dataframe/recipe/exec" - "github.com/calypr/loom/internal/dataframe/runtime" "github.com/calypr/loom/internal/dataframe/semantic" ) @@ -20,7 +19,7 @@ import ( // physical diagnostics through ExplainPhysical; raw IR and AQL stay private. type Control struct { Engine *Engine - ExplainConnection func(context.Context, runtime.CompiledQuery) (ExplainAssessment, error) + ExplainConnection func(context.Context, CompiledQuery) (ExplainAssessment, error) } func (c Control) Validate(ctx context.Context, name string, bindings recipe.RuntimeBindings) (Validation, error) { @@ -79,7 +78,7 @@ func (c Control) ExplainPhysical(ctx context.Context, name string, bindings reci return PhysicalExplanation{}, fmt.Errorf("output %q: %w", output.Name, err) } item := PhysicalOutputExplanation{ - Name: output.Name, PlanFingerprint: runtime.CompiledQueryFingerprint(compiled), + Name: output.Name, PlanFingerprint: CompiledQueryFingerprint(compiled), Columns: append([]string(nil), compiled.PublicColumns...), Diagnostics: compiled.PlanDiagnostics, } if live { diff --git a/internal/dataframe/recipe/engine/control_explain_types.go b/internal/dataframe/execution/control_explain_types.go similarity index 99% rename from internal/dataframe/recipe/engine/control_explain_types.go rename to internal/dataframe/execution/control_explain_types.go index d7cf078f..2ff04765 100644 --- a/internal/dataframe/recipe/engine/control_explain_types.go +++ b/internal/dataframe/execution/control_explain_types.go @@ -1,4 +1,4 @@ -package engine +package execution import ( "github.com/calypr/loom/internal/dataframe/compiler/ir" diff --git a/internal/dataframe/recipe/engine/control_types.go b/internal/dataframe/execution/control_types.go similarity index 97% rename from internal/dataframe/recipe/engine/control_types.go rename to internal/dataframe/execution/control_types.go index 4cc26392..bff98f0b 100644 --- a/internal/dataframe/recipe/engine/control_types.go +++ b/internal/dataframe/execution/control_types.go @@ -1,7 +1,7 @@ // Package control exposes the backend-neutral control-plane operations // needed by GraphQL or HTTP adapters. It deliberately contains no transport // types and no translation-specific output names. -package engine +package execution import ( "github.com/calypr/loom/internal/dataframe/recipe/exec" diff --git a/internal/dataframe/recipe/engine/engine.go b/internal/dataframe/execution/engine.go similarity index 90% rename from internal/dataframe/recipe/engine/engine.go rename to internal/dataframe/execution/engine.go index 81db9d9c..434f2892 100644 --- a/internal/dataframe/recipe/engine/engine.go +++ b/internal/dataframe/execution/engine.go @@ -1,8 +1,8 @@ -// Package engine is Loom's production recipe execution seam. It owns +// Package execution is Loom's production recipe execution seam. It owns // recipe resolution, scoped discovery, canonical compiler orchestration, and // streaming row execution; transport adapters do not interpret recipes or // construct AQL. -package engine +package execution import ( "context" @@ -26,15 +26,6 @@ import ( "github.com/calypr/loom/internal/dataframe/spec" ) -type Registry interface { - LoadRecipe(context.Context, string) (exec.Entry, error) -} - -type VersionedRegistry interface { - Registry - LoadRecipeVersion(context.Context, string, string) (exec.Entry, error) -} - type QueryRows func(context.Context, string, int, map[string]any, func(map[string]any) error) error const ( @@ -67,21 +58,25 @@ type PreviewSummary struct { } type Config struct { - Registry Registry + Registry exec.Reader Revisions recipe.RevisionStore ResolveBundle func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (recipe.Bundle, error) QueryRows QueryRows ScopeDigest func(recipe.RuntimeBindings) string BatchSize int + // RootPageRows bounds the number of root documents evaluated by one wide + // output query. Zero keeps the legacy single-query execution path. + RootPageRows int } type Engine struct { - registry Registry + registry exec.Reader revisions recipe.RevisionStore resolveBundle func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (recipe.Bundle, error) queryRows QueryRows scopeDigest func(recipe.RuntimeBindings) string batchSize int + rootPageRows int } // Resolved contains the immutable recipe after schema discovery, semantic @@ -125,6 +120,8 @@ type OutputStream struct { bindVars map[string]any stream QueryRows batchSize int + rootPageRows int + page *compiler.CompiledOutputPage } type DynamicColumnCheck struct { @@ -150,7 +147,10 @@ func New(cfg Config) (*Engine, error) { if batch <= 0 { batch = 1000 } - return &Engine{registry: cfg.Registry, revisions: cfg.Revisions, resolveBundle: cfg.ResolveBundle, queryRows: cfg.QueryRows, scopeDigest: cfg.ScopeDigest, batchSize: batch}, nil + if cfg.RootPageRows < 0 { + return nil, fmt.Errorf("recipe root page rows cannot be negative") + } + return &Engine{registry: cfg.Registry, revisions: cfg.Revisions, resolveBundle: cfg.ResolveBundle, queryRows: cfg.QueryRows, scopeDigest: cfg.ScopeDigest, batchSize: batch, rootPageRows: cfg.RootPageRows}, nil } func (e *Engine) Resolve(ctx context.Context, name string, bindings recipe.RuntimeBindings) (Resolved, error) { @@ -177,11 +177,7 @@ func (e *Engine) Resolve(ctx context.Context, name string, bindings recipe.Runti // ResolveVersion loads an exact immutable recipe version. New publication // workflows must use this method; Resolve is the deprecated default alias. func (e *Engine) ResolveVersion(ctx context.Context, name, translationVersion string, bindings recipe.RuntimeBindings) (Resolved, error) { - registry, ok := e.registry.(VersionedRegistry) - if !ok { - return Resolved{}, fmt.Errorf("recipe registry does not support exact versions") - } - entry, err := registry.LoadRecipeVersion(ctx, name, translationVersion) + entry, err := e.registry.LoadRecipeVersion(ctx, name, translationVersion) if err != nil { return Resolved{}, err } @@ -440,11 +436,20 @@ func (e *Engine) streamForOutput(resolved Resolved, name string, limit int) (Out if err != nil { return OutputStream{}, compiler.CompiledQuery{}, fmt.Errorf("output %q: %w", output.Name, err) } - return OutputStream{ + stream := OutputStream{ Name: output.Name, Columns: append([]string(nil), query.PublicColumns...), RowIdentity: query.RowIdentity.Clone(), DynamicChecks: dynamicChecks(output.DynamicColumns), query: query.Query, bindVars: query.BindVars, - stream: e.queryRows, batchSize: e.batchSize, - }, query, nil + stream: e.queryRows, batchSize: e.batchSize, rootPageRows: e.rootPageRows, + } + if e.rootPageRows > 0 { + page, pageErr := compiler.CompileRecipeOutputPageWithPolicy(output, resolved.Semantic.SemanticPlan.Bindings, e.rootPageRows, ir.DefaultPhysicalOptimizationPolicy()) + if pageErr != nil { + return OutputStream{}, compiler.CompiledQuery{}, fmt.Errorf("output %q paging: %w", output.Name, pageErr) + } + stream.page = &page + query.PlanDiagnostics = page.RowsDiagnostics + } + return stream, query, nil } return OutputStream{}, compiler.CompiledQuery{}, previewAdmissionError(dataframeerrors.CodeInvalidRequest, "requested preview output is not available", map[string]any{"output": name}) } @@ -541,7 +546,7 @@ func (e *Engine) PreviewOutput(ctx context.Context, resolved Resolved, request P count := 0 var visitorErr error queryStarted := time.Now() - queryErr := stream.stream(ctx, stream.query, stream.batchSize, stream.bindVars, func(row map[string]any) error { + queryErr := stream.streamRaw(ctx, func(row map[string]any) error { if err := contextError(ctx); err != nil { return err } @@ -558,6 +563,9 @@ func (e *Engine) PreviewOutput(ctx context.Context, resolved Resolved, request P return err } count++ + if count >= limit { + return errPreviewLimit + } return nil }) summary.RowCount = count @@ -646,7 +654,7 @@ func (s OutputStream) Stream(ctx context.Context, visit func(map[string]any) err return StreamResult{}, fmt.Errorf("row visitor is required") } count := 0 - err := s.stream(ctx, s.query, s.batchSize, s.bindVars, func(row map[string]any) error { + err := s.streamRaw(ctx, func(row map[string]any) error { resolved, err := materializePostQueryRowWithChecks(row, s.DynamicChecks) if err != nil { return err @@ -661,6 +669,51 @@ func (s OutputStream) Stream(ctx context.Context, visit func(map[string]any) err return StreamResult{Output: s.Name, Columns: append([]string(nil), s.Columns...), RowCount: count}, err } +func (s OutputStream) streamRaw(ctx context.Context, visit func(map[string]any) error) error { + if s.page == nil || s.rootPageRows == 0 { + return s.stream(ctx, s.query, s.batchSize, s.bindVars, visit) + } + after := "" + for { + keys := make([]string, 0, s.rootPageRows) + keyBinds := cloneBindVars(s.page.RootKeysBindVars) + keyBinds[compiler.RootPageAfterKeyBind] = after + if err := s.stream(ctx, s.page.RootKeysQuery, s.batchSize, keyBinds, func(row map[string]any) error { + key, ok := row["_key"].(string) + if !ok || key == "" { + return fmt.Errorf("root-key page returned an invalid _key") + } + if len(keys) != 0 && key <= keys[len(keys)-1] { + return fmt.Errorf("root-key page is not strictly ordered") + } + keys = append(keys, key) + return nil + }); err != nil { + return err + } + if len(keys) == 0 { + return nil + } + rowBinds := cloneBindVars(s.page.RowsBindVars) + rowBinds[compiler.RootPageKeysBind] = keys + if err := s.stream(ctx, s.page.RowsQuery, s.batchSize, rowBinds, visit); err != nil { + return err + } + after = keys[len(keys)-1] + if len(keys) < s.rootPageRows { + return nil + } + } +} + +func cloneBindVars(input map[string]any) map[string]any { + clone := make(map[string]any, len(input)) + for key, value := range input { + clone[key] = value + } + return clone +} + // publicStreamRow removes compiler-only projections after they have served // post-query validation and row-identity derivation. auth_resource_path is the // one hidden projection retained for publication because it preserves the diff --git a/internal/dataframe/recipe/engine/engine_test.go b/internal/dataframe/execution/engine_test.go similarity index 81% rename from internal/dataframe/recipe/engine/engine_test.go rename to internal/dataframe/execution/engine_test.go index 7ad49f68..e2438496 100644 --- a/internal/dataframe/recipe/engine/engine_test.go +++ b/internal/dataframe/execution/engine_test.go @@ -1,4 +1,4 @@ -package engine +package execution import ( "context" @@ -28,6 +28,10 @@ func (invalidRecipeRegistry) LoadRecipe(context.Context, string) (exec.Entry, er }}, nil } +func (r invalidRecipeRegistry) LoadRecipeVersion(ctx context.Context, name, _ string) (exec.Entry, error) { + return r.LoadRecipe(ctx, name) +} + func TestMaterializeMarksResolutionFailures(t *testing.T) { engine, err := New(Config{ Registry: invalidRecipeRegistry{}, @@ -155,7 +159,7 @@ func TestPreviewOutputEnforcesLimitAndCancellation(t *testing.T) { count++ return nil }) - if err != nil || count != 2 || summary.RowCount != 2 || rowsSeen != 3 { + if err != nil || count != 2 || summary.RowCount != 2 || rowsSeen != 2 { t.Fatalf("limit count=%d summary=%#v rowsSeen=%d err=%v", count, summary, rowsSeen, err) } @@ -188,6 +192,103 @@ func TestPreviewOutputEnforcesLimitAndCancellation(t *testing.T) { } } +func TestRootKeyPagingPreservesExpandedRowsAndAdvancesPastEmptyRoots(t *testing.T) { + type calls struct{ keys, rows int } + newPagedEngine := func(observed *calls) *Engine { + t.Helper() + queryRows := func(_ context.Context, _ string, _ int, binds map[string]any, visit func(map[string]any) error) error { + if afterValue, ok := binds[compiler.RootPageAfterKeyBind]; ok { + observed.keys++ + after, _ := afterValue.(string) + pageSize, _ := binds[compiler.RootPageSizeBind].(int) + emitted := 0 + for _, key := range []string{"a", "b", "c"} { + if key <= after || emitted == pageSize { + continue + } + if err := visit(map[string]any{"_key": key}); err != nil { + return err + } + emitted++ + } + return nil + } + keys, ok := binds[compiler.RootPageKeysBind].([]string) + if !ok { + return fmt.Errorf("unexpected unpaged query") + } + observed.rows++ + for _, key := range keys { + rowCount := 0 + switch key { + case "a": + rowCount = 30 + case "c": + rowCount = 1 + } + for index := 0; index < rowCount; index++ { + if err := visit(map[string]any{"_key": key, "id": fmt.Sprintf("%s-%02d", key, index)}); err != nil { + return err + } + } + } + return nil + } + engine, err := New(Config{Registry: invalidRecipeRegistry{}, QueryRows: queryRows, ScopeDigest: func(recipe.RuntimeBindings) string { return "scope" }, RootPageRows: 2}) + if err != nil { + t.Fatal(err) + } + return engine + } + + t.Run("complete stream", func(t *testing.T) { + observed := &calls{} + engine := newPagedEngine(observed) + resolved, err := engine.CompileResolvedBundle(context.Background(), testResolvedBundle([]string{}), recipe.RuntimeBindings{Project: "P1"}) + if err != nil { + t.Fatal(err) + } + streams, err := engine.Streams(context.Background(), resolved) + if err != nil { + t.Fatal(err) + } + var ids []string + result, err := streams[0].Stream(context.Background(), func(row map[string]any) error { + ids = append(ids, row["id"].(string)) + return nil + }) + if err != nil { + t.Fatal(err) + } + if result.RowCount != 31 || len(ids) != 31 || ids[0] != "a-00" || ids[30] != "c-00" { + t.Fatalf("paged rows result=%#v ids=%#v", result, ids) + } + if observed.keys != 2 || observed.rows != 2 { + t.Fatalf("query calls = %#v, want two key and two row pages", observed) + } + }) + + t.Run("preview output limit", func(t *testing.T) { + observed := &calls{} + engine := newPagedEngine(observed) + resolved, err := engine.CompileResolvedBundle(context.Background(), testResolvedBundle([]string{}), recipe.RuntimeBindings{Project: "P1"}) + if err != nil { + t.Fatal(err) + } + count := 0 + summary, err := engine.PreviewOutput(context.Background(), resolved, PreviewRequest{Output: "Patient", Limit: 25}, func(map[string]any) error { + count++ + return nil + }) + if err != nil || count != 25 || summary.RowCount != 25 { + t.Fatalf("preview count=%d summary=%#v err=%v", count, summary, err) + } + if observed.keys != 1 || observed.rows != 1 { + t.Fatalf("preview calls = %#v, want one bounded page", observed) + } + }) +} + func TestPreviewOutputNormalizesVisitorAndDynamicSchemaErrors(t *testing.T) { driftEngine := testEngine(func(_ context.Context, _ string, _ int, _ map[string]any, visit func(map[string]any) error) error { return visit(map[string]any{ diff --git a/internal/dataframe/runtime/execution_service.go b/internal/dataframe/execution/execution_service.go similarity index 94% rename from internal/dataframe/runtime/execution_service.go rename to internal/dataframe/execution/execution_service.go index ef1bd2c9..aea5a98d 100644 --- a/internal/dataframe/runtime/execution_service.go +++ b/internal/dataframe/execution/execution_service.go @@ -1,4 +1,4 @@ -package runtime +package execution import ( "context" @@ -41,9 +41,9 @@ func (s *Service) RunCompiled(ctx context.Context, compiled CompiledQuery) (*Res return result, nil } -func (s *Service) streamQuery(ctx context.Context, compiled CompiledQuery, visit func(map[string]any) error) (StreamResult, error) { +func (s *Service) streamQuery(ctx context.Context, compiled CompiledQuery, visit func(map[string]any) error) (streamResult, error) { if visit == nil { - return StreamResult{}, fmt.Errorf("row visitor is required") + return streamResult{}, fmt.Errorf("row visitor is required") } columns := materializedColumns(compiled.Columns, compiled.PivotFields) seenColumns := make(map[string]struct{}, len(columns)) @@ -56,7 +56,7 @@ func (s *Service) streamQuery(ctx context.Context, compiled CompiledQuery, visit var rowMaterialization time.Duration queryStarted := time.Now() if s.queryRows == nil { - return StreamResult{}, dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) + return streamResult{}, dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) } err := s.queryRows(ctx, compiled.Query, 1000, compiled.BindVars, func(row map[string]any) error { rowStarted := time.Now() @@ -87,7 +87,7 @@ func (s *Service) streamQuery(ctx context.Context, compiled CompiledQuery, visit } sort.Strings(newColumns) columns = append(columns, newColumns...) - result := StreamResult{ + result := streamResult{ Columns: columns, RowCount: rowCount, Diagnostics: QueryDiagnostics{ diff --git a/internal/dataframe/runtime/execution_test.go b/internal/dataframe/execution/execution_test.go similarity index 99% rename from internal/dataframe/runtime/execution_test.go rename to internal/dataframe/execution/execution_test.go index bdbca9e9..c55f7fdc 100644 --- a/internal/dataframe/runtime/execution_test.go +++ b/internal/dataframe/execution/execution_test.go @@ -1,4 +1,4 @@ -package runtime +package execution import ( "context" diff --git a/internal/dataframe/runtime/explain.go b/internal/dataframe/execution/explain.go similarity index 98% rename from internal/dataframe/runtime/explain.go rename to internal/dataframe/execution/explain.go index cf546081..3e3a469c 100644 --- a/internal/dataframe/runtime/explain.go +++ b/internal/dataframe/execution/explain.go @@ -1,4 +1,4 @@ -package runtime +package execution import ( "crypto/sha256" diff --git a/internal/dataframe/runtime/explain_test.go b/internal/dataframe/execution/explain_test.go similarity index 97% rename from internal/dataframe/runtime/explain_test.go rename to internal/dataframe/execution/explain_test.go index 147f2614..1cba8506 100644 --- a/internal/dataframe/runtime/explain_test.go +++ b/internal/dataframe/execution/explain_test.go @@ -1,4 +1,4 @@ -package runtime +package execution import "testing" diff --git a/internal/dataframe/runtime/helpers.go b/internal/dataframe/execution/helpers.go similarity index 95% rename from internal/dataframe/runtime/helpers.go rename to internal/dataframe/execution/helpers.go index 2f54e6d0..2c596f35 100644 --- a/internal/dataframe/runtime/helpers.go +++ b/internal/dataframe/execution/helpers.go @@ -1,4 +1,4 @@ -package runtime +package execution import ( "strings" diff --git a/internal/dataframe/runtime/helpers_test.go b/internal/dataframe/execution/helpers_test.go similarity index 93% rename from internal/dataframe/runtime/helpers_test.go rename to internal/dataframe/execution/helpers_test.go index 5f94cccc..5f0092cd 100644 --- a/internal/dataframe/runtime/helpers_test.go +++ b/internal/dataframe/execution/helpers_test.go @@ -1,4 +1,4 @@ -package runtime +package execution import "testing" diff --git a/internal/dataframe/recipe/engine/identity_test.go b/internal/dataframe/execution/identity_test.go similarity index 99% rename from internal/dataframe/recipe/engine/identity_test.go rename to internal/dataframe/execution/identity_test.go index d22c1260..a4219858 100644 --- a/internal/dataframe/recipe/engine/identity_test.go +++ b/internal/dataframe/execution/identity_test.go @@ -1,4 +1,4 @@ -package engine +package execution import ( "context" diff --git a/internal/dataframe/recipe/engine/named_uuid.go b/internal/dataframe/execution/named_uuid.go similarity index 93% rename from internal/dataframe/recipe/engine/named_uuid.go rename to internal/dataframe/execution/named_uuid.go index 6a02187e..f033b9db 100644 --- a/internal/dataframe/recipe/engine/named_uuid.go +++ b/internal/dataframe/execution/named_uuid.go @@ -1,6 +1,6 @@ -// Package engine contains the exact named-UUID contract shared by +// Package execution contains the exact named-UUID contract shared by // compiler-owned post-query execution and legacy differential tests. -package engine +package execution import ( "fmt" diff --git a/internal/dataframe/recipe/engine/named_uuid_test.go b/internal/dataframe/execution/named_uuid_test.go similarity index 98% rename from internal/dataframe/recipe/engine/named_uuid_test.go rename to internal/dataframe/execution/named_uuid_test.go index 58d92963..d1d4eead 100644 --- a/internal/dataframe/recipe/engine/named_uuid_test.go +++ b/internal/dataframe/execution/named_uuid_test.go @@ -1,4 +1,4 @@ -package engine +package execution import ( "testing" diff --git a/internal/dataframe/recipe/engine/postquery.go b/internal/dataframe/execution/postquery.go similarity index 99% rename from internal/dataframe/recipe/engine/postquery.go rename to internal/dataframe/execution/postquery.go index 7dad4419..716f2dc0 100644 --- a/internal/dataframe/recipe/engine/postquery.go +++ b/internal/dataframe/execution/postquery.go @@ -1,4 +1,4 @@ -package engine +package execution import ( "encoding/json" diff --git a/internal/dataframe/recipe/engine/postquery_test.go b/internal/dataframe/execution/postquery_test.go similarity index 99% rename from internal/dataframe/recipe/engine/postquery_test.go rename to internal/dataframe/execution/postquery_test.go index bf22abf6..2fa40f7e 100644 --- a/internal/dataframe/recipe/engine/postquery_test.go +++ b/internal/dataframe/execution/postquery_test.go @@ -1,4 +1,4 @@ -package engine +package execution import ( "errors" diff --git a/internal/dataframe/runtime/service.go b/internal/dataframe/execution/service.go similarity index 95% rename from internal/dataframe/runtime/service.go rename to internal/dataframe/execution/service.go index a2949304..d8d7c9cf 100644 --- a/internal/dataframe/runtime/service.go +++ b/internal/dataframe/execution/service.go @@ -1,4 +1,4 @@ -package runtime +package execution import ( "context" diff --git a/internal/dataframe/runtime/types.go b/internal/dataframe/execution/types.go similarity index 81% rename from internal/dataframe/runtime/types.go rename to internal/dataframe/execution/types.go index 4a33f8b4..550c118f 100644 --- a/internal/dataframe/runtime/types.go +++ b/internal/dataframe/execution/types.go @@ -1,4 +1,4 @@ -package runtime +package execution import ( "time" @@ -33,10 +33,10 @@ type QueryDiagnostics struct { Plan ir.CompilerPlanDiagnostics } -// StreamResult describes rows delivered to a streaming caller. Columns are -// finalized only after iteration because flattened pivots can add bounded, -// data-dependent output keys. -type StreamResult struct { +// streamResult is the internal result of generic compiled-query iteration. +// The public recipe-facing StreamResult remains in engine.go and carries the +// output name and publication-safe columns. +type streamResult struct { Columns []string RowCount int Diagnostics QueryDiagnostics diff --git a/internal/dataframe/expression/expression_checking.go b/internal/dataframe/expression/expression_checking.go index 6c588f95..20c4479f 100644 --- a/internal/dataframe/expression/expression_checking.go +++ b/internal/dataframe/expression/expression_checking.go @@ -128,18 +128,6 @@ func validateLiteral(l Literal) error { } return nil } - if l.Type.Cardinality == Many { - value := reflect.ValueOf(l.Value) - if value.Kind() != reflect.Array && value.Kind() != reflect.Slice { - return fmt.Errorf("many literal value %T must be an array or slice", l.Value) - } - for index := 0; index < value.Len(); index++ { - if !validScalarLiteral(l.Type.Kind, value.Index(index).Interface()) { - return fmt.Errorf("many literal element %d is incompatible with %s", index, l.Type.Kind) - } - } - return nil - } if !validScalarLiteral(l.Type.Kind, l.Value) { return fmt.Errorf("literal value %T is incompatible with %s", l.Value, l.Type.Kind) } diff --git a/internal/dataframe/expression/recipe_adapter.go b/internal/dataframe/expression/recipe_adapter.go index 10ab7fb4..5dfcd450 100644 --- a/internal/dataframe/expression/recipe_adapter.go +++ b/internal/dataframe/expression/recipe_adapter.go @@ -39,21 +39,23 @@ func FromRecipe(input recipe.Expression) (Expression, error) { return Expression{}, fmt.Errorf("recipe expression is empty") } call := Call{Name: input.Call} - for _, arg := range input.Args { - converted, err := FromRecipe(arg) + persistedCast := strings.EqualFold(input.Call, "cast") && len(input.Args) == 2 + if persistedCast { + target, err := parsePersistedCastTarget(input.Args[1]) if err != nil { return Expression{}, err } - call.Args = append(call.Args, converted) + call.Target = &target } - if strings.EqualFold(input.Call, "cast") && len(input.Args) == 2 && input.Args[1].Literal != nil { - var targetName string - if err := json.Unmarshal(input.Args[1].Literal, &targetName); err == nil { - target := typeForName(targetName) - if target.Kind != "" { - call.Target = &target - } + for index, arg := range input.Args { + if persistedCast && index == 1 { + continue + } + converted, err := FromRecipe(arg) + if err != nil { + return Expression{}, err } + call.Args = append(call.Args, converted) } return Expression{Kind: CallNode, Call: &call, NullBehavior: NullPropagate}, nil } @@ -106,6 +108,28 @@ func FromRecipeInContexts(input recipe.Expression, contexts map[string]struct{}) func isContextName(value string) bool { return value == "root" || value == "item" } +func parsePersistedCastTarget(input recipe.Expression) (Type, error) { + if input.Literal == nil { + return Type{}, fmt.Errorf("cast target must be a literal") + } + if !json.Valid(input.Literal) { + return Type{}, fmt.Errorf("cast target literal is invalid JSON") + } + var value any + if err := json.Unmarshal(input.Literal, &value); err != nil { + return Type{}, fmt.Errorf("cast target literal is invalid JSON: %w", err) + } + targetName, ok := value.(string) + if !ok { + return Type{}, fmt.Errorf("cast target must be a string, got %T", value) + } + target := typeForName(targetName) + if target.Kind == "" { + return Type{}, fmt.Errorf("unsupported cast target %q", targetName) + } + return target, nil +} + func recipeLiteral(raw json.RawMessage) (Literal, error) { dec := json.NewDecoder(strings.NewReader(string(raw))) dec.UseNumber() @@ -129,17 +153,32 @@ func recipeLiteral(raw json.RawMessage) (Literal, error) { case []any: kind := KindString if len(v) > 0 { - switch v[0].(type) { - case bool: - kind = KindBoolean - case json.Number: - kind = KindDecimal + var ok bool + kind, ok = recipeScalarKind(v[0]) + if !ok { + return Literal{}, fmt.Errorf("literal array element 0 has unsupported type %T", v[0]) + } + if kind == KindNull { + return Literal{}, fmt.Errorf("literal array element 0 cannot be null") + } + for index, item := range v[1:] { + itemKind, ok := recipeScalarKind(item) + if !ok { + return Literal{}, fmt.Errorf("literal array element %d has unsupported type %T", index+1, item) + } + if itemKind != kind { + return Literal{}, fmt.Errorf("literal array contains mixed kinds at index %d: expected %s, got %s", index+1, kind, itemKind) + } } } if kind == KindDecimal { converted := make([]any, len(v)) for i, item := range v { - number, err := strconv.ParseFloat(item.(json.Number).String(), 64) + numberValue, ok := item.(json.Number) + if !ok { + return Literal{}, fmt.Errorf("literal array element %d has type %T, expected number", i, item) + } + number, err := strconv.ParseFloat(numberValue.String(), 64) if err != nil { return Literal{}, err } @@ -153,6 +192,21 @@ func recipeLiteral(raw json.RawMessage) (Literal, error) { } } +func recipeScalarKind(value any) (ValueKind, bool) { + switch value.(type) { + case nil: + return KindNull, true + case bool: + return KindBoolean, true + case string: + return KindString, true + case json.Number: + return KindDecimal, true + default: + return "", false + } +} + func typeForName(name string) Type { switch strings.ToLower(strings.TrimSpace(name)) { case "string": diff --git a/internal/dataframe/expression/recipe_adapter_test.go b/internal/dataframe/expression/recipe_adapter_test.go index 9a3c87f0..666b8ac6 100644 --- a/internal/dataframe/expression/recipe_adapter_test.go +++ b/internal/dataframe/expression/recipe_adapter_test.go @@ -1,6 +1,8 @@ package expression import ( + "encoding/json" + "strings" "testing" "github.com/calypr/loom/internal/dataframe/recipe" @@ -24,3 +26,90 @@ func TestFromRecipeBuildsTypedAST(t *testing.T) { t.Fatalf("type = %s", checked.Type) } } + +func TestFromRecipeNormalizesPersistedCastTarget(t *testing.T) { + input := recipe.Expression{Call: "cast", Args: []recipe.Expression{ + {Literal: json.RawMessage(`"7"`)}, + {Literal: json.RawMessage(`"integer"`)}, + }} + expr, err := FromRecipe(input) + if err != nil { + t.Fatal(err) + } + if expr.Call == nil || len(expr.Call.Args) != 1 { + t.Fatalf("cast args = %#v, want one value argument", expr.Call) + } + if expr.Call.Target == nil || *expr.Call.Target != (Type{Kind: KindInteger, Cardinality: RequiredOne}) { + t.Fatalf("cast target = %#v, want integer/required_one", expr.Call.Target) + } + checked, err := expr.Check(TypeContext{}) + if err != nil { + t.Fatalf("check normalized cast: %v", err) + } + if checked.Type != (Type{Kind: KindInteger, Cardinality: RequiredOne}) { + t.Fatalf("cast type = %s, want integer/required_one", checked.Type) + } +} + +func TestFromRecipeRejectsInvalidPersistedCastTargets(t *testing.T) { + tests := []struct { + name string + target recipe.Expression + want string + }{ + { + name: "nonliteral", + target: recipe.Expression{Select: "root.target"}, + want: "cast target must be a literal", + }, + { + name: "malformed JSON", + target: recipe.Expression{Literal: json.RawMessage(`{"`)}, + want: "cast target literal is invalid JSON", + }, + { + name: "nonstring", + target: recipe.Expression{Literal: json.RawMessage(`123`)}, + want: "cast target must be a string", + }, + { + name: "unsupported", + target: recipe.Expression{Literal: json.RawMessage(`"timestamp"`)}, + want: "unsupported cast target", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := recipe.Expression{Call: "cast", Args: []recipe.Expression{ + {Literal: json.RawMessage(`"7"`)}, + test.target, + }} + expr, err := FromRecipe(input) + if err == nil { + t.Fatal("invalid cast target unexpectedly converted") + } + if expr != (Expression{}) { + t.Fatalf("invalid cast target returned AST: %#v", expr) + } + if !strings.Contains(err.Error(), test.want) { + t.Fatalf("error = %v, want %q", err, test.want) + } + }) + } +} + +func TestFromRecipeRejectsMixedLiteralArrayWithoutPanic(t *testing.T) { + defer func() { + if recovered := recover(); recovered != nil { + t.Fatalf("mixed literal array panicked: %v", recovered) + } + }() + + _, err := FromRecipe(recipe.Expression{Literal: json.RawMessage(`[1,true]`)}) + if err == nil { + t.Fatal("mixed literal array unexpectedly converted") + } + if !strings.Contains(err.Error(), "mixed") { + t.Fatalf("mixed literal array error = %v, want mixed-kind validation", err) + } +} diff --git a/internal/dataframe/publication/arango/bundle_registry.go b/internal/dataframe/publication/arango/bundle_registry.go index 91215d71..9456c3bb 100644 --- a/internal/dataframe/publication/arango/bundle_registry.go +++ b/internal/dataframe/publication/arango/bundle_registry.go @@ -8,6 +8,7 @@ import ( "time" "github.com/calypr/loom/internal/dataframe/publication" + arangostore "github.com/calypr/loom/internal/store/arango" ) const ( @@ -16,12 +17,6 @@ const ( BundleLeasesCollection = "loom_dataframe_bundle_leases" ) -// AQLExecutor is implemented by Loom's concrete Arango client. Keeping it -// optional preserves the small fake Client used by existing registry tests. -type AQLExecutor interface { - ExecuteAQL(context.Context, string, map[string]interface{}) error -} - func (r *Registry) SaveExecution(ctx context.Context, execution publication.BundleExecution) error { data, err := json.Marshal(execution) if err != nil { @@ -75,42 +70,6 @@ func (r *Registry) ListExecutions(ctx context.Context, state publication.BundleS return out, err } -// ListSelectorExecutions avoids the five full lifecycle scans otherwise -// required to assemble dataframe project status. Exact output matching stays -// in the caller so queued executions (which intentionally have no outputs yet) -// remain visible as BUILDING. -func (r *Registry) ListSelectorExecutions(ctx context.Context, selector publication.DataframeSelector, before time.Time) ([]publication.BundleExecution, error) { - out := []publication.BundleExecution{} - states := []publication.BundleState{ - publication.BundleQueued, publication.BundlePending, - publication.BundleRunning, publication.BundlePreflight, publication.BundleLoading, - publication.BundleValidating, - publication.BundlePublished, publication.BundleReady, - publication.BundleFailed, - } - err := r.client.QueryRows(ctx, `FOR doc IN @@collection -FILTER (doc.name == @recipe OR doc.Name == @recipe) - AND (doc.translationVersion == @translationVersion OR doc.TranslationVersion == @translationVersion) - AND doc.state IN @states AND doc.updatedAt < @before -SORT doc.updatedAt ASC RETURN doc`, r.batchSize, map[string]interface{}{ - "@collection": BundleExecutionsCollection, - "recipe": selector.Recipe, "translationVersion": selector.TranslationVersion, - "states": states, "before": before, - }, func(row map[string]any) error { - data, err := json.Marshal(row) - if err != nil { - return err - } - var execution publication.BundleExecution - if err := json.Unmarshal(data, &execution); err != nil { - return err - } - out = append(out, execution.CanonicalizeLegacy()) - return nil - }) - return out, err -} - func (r *Registry) FindExecutionBySelector(ctx context.Context, project, generation string, selector publication.DataframeSelector) (publication.BundleExecution, publication.BundleOutputRecord, error) { if err := selector.Validate(); err != nil { return publication.BundleExecution{}, publication.BundleOutputRecord{}, err @@ -136,29 +95,6 @@ SORT doc.publishedAt DESC, doc.readyAt DESC, doc.updatedAt DESC LIMIT 1 RETURN d return publication.BundleExecution{}, publication.BundleOutputRecord{}, publication.ErrBundleNotFound } -// DataframeSelectorForExecution exposes exact selector metadata to federation -// without making the reader understand the registry's persisted document. -func (r *Registry) DataframeSelectorForExecution(ctx context.Context, executionID, outputName string) (publication.DataframeSelector, error) { - execution, err := r.GetExecution(ctx, executionID) - if err != nil { - return publication.DataframeSelector{}, err - } - for _, output := range execution.Outputs { - if output.Name != outputName { - continue - } - if output.Selector.Valid() { - return output.Selector, nil - } - selector := execution.Selector(outputName) - if selector.Valid() { - return selector, nil - } - break - } - return publication.DataframeSelector{}, publication.ErrBundleNotFound -} - func (r *Registry) loadExecution(ctx context.Context, query string, vars map[string]interface{}) (publication.BundleExecution, error) { var found *publication.BundleExecution err := r.client.QueryRows(ctx, query, r.batchSize, vars, func(row map[string]any) error { @@ -250,15 +186,21 @@ func (r *Registry) PublishExecution(ctx context.Context, name, expected string, updated := false err = r.client.QueryRows(ctx, `LET pointer = DOCUMENT(@@pointers, @pointerKey) LET execution = DOCUMENT(@@executions, @executionKey) -FILTER execution != null AND (pointer == null OR pointer.executionId == @expected) +LET lease = DOCUMENT(@@leases, @leaseKey) +FILTER execution != null + AND lease != null + AND lease.ownerId == @owner + AND lease.expiresAt >= @now + AND (pointer == null OR pointer.executionId == @expected) UPDATE execution WITH @execution IN @@executions UPSERT {_key: @pointerKey} INSERT {_key: @pointerKey, name: @name, executionId: @executionKey, updatedAt: @updatedAt} UPDATE {executionId: @executionKey, updatedAt: @updatedAt} IN @@pointers RETURN {updated: true}`, r.batchSize, map[string]interface{}{ - "@pointers": BundlePointersCollection, "@executions": BundleExecutionsCollection, + "@pointers": BundlePointersCollection, "@executions": BundleExecutionsCollection, "@leases": BundleLeasesCollection, "pointerKey": pointerDocumentKey(name), "executionKey": execution.ID, + "leaseKey": execution.Key, "owner": execution.OwnerID, "now": time.Now().UTC(), "name": name, "expected": expected, "updatedAt": execution.UpdatedAt, "execution": executionDoc, }, func(map[string]any) error { updated = true; return nil }) if err != nil { @@ -292,7 +234,7 @@ RETURN {claimed: true}`, r.batchSize, map[string]interface{}{"@collection": Bund } func (r *Registry) ReleaseBundleLease(ctx context.Context, key, owner string) error { - executor, ok := r.client.(AQLExecutor) + executor, ok := r.client.(arangostore.AQLExecutor) if !ok { return nil } diff --git a/internal/dataframe/publication/arango/bundle_registry_test.go b/internal/dataframe/publication/arango/bundle_registry_test.go index 6029e132..05d2be85 100644 --- a/internal/dataframe/publication/arango/bundle_registry_test.go +++ b/internal/dataframe/publication/arango/bundle_registry_test.go @@ -1,10 +1,37 @@ package arango import ( + "context" + "encoding/json" "regexp" + "strings" "testing" + "time" + + "github.com/calypr/loom/internal/dataframe/publication" + arangostore "github.com/calypr/loom/internal/store/arango" ) +type publishCaptureClient struct { + testing *testing.T +} + +func (c publishCaptureClient) InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error { + return nil +} + +func (c publishCaptureClient) QueryRows(_ context.Context, query string, _ int, bindVars map[string]interface{}, visit arangostore.RowVisitor) error { + for _, fragment := range []string{"LET lease = DOCUMENT(@@leases, @leaseKey)", "lease.ownerId == @owner", "lease.expiresAt >= @now"} { + if !strings.Contains(query, fragment) { + c.testing.Fatalf("publication query lacks lease guard %q", fragment) + } + } + if bindVars["@leases"] != BundleLeasesCollection || bindVars["leaseKey"] != "bundle-key" || bindVars["owner"] != "publisher-a" { + c.testing.Fatalf("publication lease bindings = %#v", bindVars) + } + return visit(map[string]any{"updated": true}) +} + func TestPointerDocumentKeyAcceptsLogicalNamesWithNULSeparators(t *testing.T) { name := "HTAN_INT-BForePC\x00\x00aced-meta-default" first := pointerDocumentKey(name) @@ -16,3 +43,18 @@ func TestPointerDocumentKeyAcceptsLogicalNamesWithNULSeparators(t *testing.T) { t.Fatalf("pointer key is not an Arango-safe SHA256: %q", first) } } + +func TestPublishExecutionRequiresCurrentLeaseOwner(t *testing.T) { + registry, err := New(publishCaptureClient{testing: t}) + if err != nil { + t.Fatal(err) + } + now := time.Now().UTC() + execution := publication.BundleExecution{ + ID: "execution-a", Key: "bundle-key", OwnerID: "publisher-a", + State: publication.BundlePublished, UpdatedAt: now, + } + if err := registry.PublishExecution(context.Background(), "project\x00generation\x00recipe", "", execution); err != nil { + t.Fatal(err) + } +} diff --git a/internal/dataframe/publication/arango/registry.go b/internal/dataframe/publication/arango/registry.go index e88d09e0..73d0debb 100644 --- a/internal/dataframe/publication/arango/registry.go +++ b/internal/dataframe/publication/arango/registry.go @@ -1,17 +1,12 @@ package arango import ( - "context" - "encoding/json" "fmt" arangostore "github.com/calypr/loom/internal/store/arango" ) -type Client interface { - QueryRows(context.Context, string, int, map[string]interface{}, arangostore.RowVisitor) error - InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error -} +type Client = arangostore.RowBatchClient type Registry struct { client Client diff --git a/internal/dataframe/publication/bundle.go b/internal/dataframe/publication/bundle.go index f15e6f3e..45701a5b 100644 --- a/internal/dataframe/publication/bundle.go +++ b/internal/dataframe/publication/bundle.go @@ -10,28 +10,10 @@ import ( "time" "github.com/calypr/loom/internal/dataset" - "github.com/calypr/loom/internal/store/clickhouse" ) type DataframeSelector = dataset.DataframeSelector -type AtomicBundleTx interface { - CreateOutput(context.Context, string, []clickhouse.Column) error - InsertRows(context.Context, string, []clickhouse.Column, []map[string]any) error - Commit(context.Context) error - Abort(context.Context, error) error - Rollback(context.Context) error -} - -// AtomicBundleSchemaFinalizer is the schema-aware publication extension used -// by targets that can physically drop unpopulated discovered columns before -// verification and pointer activation. -type AtomicBundleSchemaFinalizer interface { - AtomicBundleTx - FinalizeSchema(context.Context, []OutputSchema) error - SetFinalSchemaDigest(string) error -} - // BundleState is the durable lifecycle of a multi-output publication. The // physical ClickHouse tables are never reader-visible until the logical // pointer and PUBLISHED execution are committed atomically. @@ -157,9 +139,6 @@ type BundleExecution struct { FailureRetryable bool `json:"failureRetryable,omitempty"` OwnerID string `json:"ownerId,omitempty"` LeaseExpiresAt *time.Time `json:"leaseExpiresAt,omitempty"` - Attempt int `json:"attempt,omitempty"` - MaxAttempts int `json:"maxAttempts,omitempty"` - NextAttemptAt *time.Time `json:"nextAttemptAt,omitempty"` FailurePhase string `json:"failurePhase,omitempty"` FailureOutput string `json:"failureOutput,omitempty"` FailureDetails string `json:"failureDetails,omitempty"` @@ -208,13 +187,6 @@ type BundleCatalog interface { ReleaseBundleLease(context.Context, string, string) error } -// SelectorExecutionCatalog is the optimized read seam used by federation -// status discovery. Implementations return all lifecycle attempts for one -// exact selector with a single backing-store query. -type SelectorExecutionCatalog interface { - ListSelectorExecutions(context.Context, DataframeSelector, time.Time) ([]BundleExecution, error) -} - // ExactExecutionCatalog is consumed by project release verification. It never // falls back to latest-by-output or a name-only recipe. type ExactExecutionCatalog interface { diff --git a/internal/dataframe/publication/clickhouse/bundle_store.go b/internal/dataframe/publication/clickhouse/bundle_store.go index bd45d6cb..c3c7c203 100644 --- a/internal/dataframe/publication/clickhouse/bundle_store.go +++ b/internal/dataframe/publication/clickhouse/bundle_store.go @@ -23,15 +23,6 @@ var ( ErrBundleCommitUncertain = errors.New("bundle publication commit outcome is uncertain") ) -type IdentityBundleStore interface { - BeginBundleFor(context.Context, publication.BundleIdentity) (publication.AtomicBundleTx, error) -} - -type ClaimedIdentityBundleStore interface { - IdentityBundleStore - BeginClaimedBundleFor(context.Context, string, string, publication.BundleIdentity) (publication.AtomicBundleTx, error) -} - // ClickHouseBundleStore publishes staged tables by advancing a durable logical // pointer in publication.BundleCatalog. ClickHouse itself has no cross-table transaction; // the pointer is therefore the visibility boundary. @@ -49,17 +40,6 @@ type BundleClickHouseStore interface { InsertRows(context.Context, string, []clickhouse.Column, []map[string]any) error VerifyOutput(context.Context, string, []clickhouse.Column, int64) error DropTable(context.Context, string) error -} - -type bundleColumnDropper interface { - DropColumns(context.Context, string, []string) error -} - -// BundleClickHouseColumnStore is the schema-aware extension implemented by -// production ClickHouse clients. The base store remains compatible with -// lightweight readers/fakes that never request column pruning. -type BundleClickHouseColumnStore interface { - BundleClickHouseStore DropColumns(context.Context, string, []string) error } @@ -70,7 +50,51 @@ func NewBundleStore(client BundleClickHouseStore, catalog publication.BundleCata return &ClickHouseBundleStore{clickHouse: client, catalog: catalog, prefix: "loom_bundle", leaseTTL: 2 * time.Minute, leaseRenewInterval: 30 * time.Second}, nil } -func (s *ClickHouseBundleStore) BeginBundleFor(ctx context.Context, identity publication.BundleIdentity) (publication.AtomicBundleTx, error) { +var _ publication.Target = (*ClickHouseBundleStore)(nil) +var _ publication.Transaction = (*clickHouseBundleTx)(nil) + +// SupportsObjectValues reports the native JSON support of this target. +func (s *ClickHouseBundleStore) SupportsObjectValues() bool { return true } + +func (s *ClickHouseBundleStore) Begin(ctx context.Context, identity publication.PublicationIdentity, schemas []publication.OutputSchema) (publication.Transaction, error) { + bundleIdentity := publication.BundleIdentity{ + Name: identity.Name, TranslationVersion: identity.TranslationVersion, OutputName: identity.OutputName, + Project: identity.Project, DatasetGeneration: identity.DatasetGeneration, RecipeDigest: identity.RecipeDigest, + SchemaDigest: identity.SchemaDigest, ScopeDigest: identity.ScopeDigest, EngineVersion: identity.EngineVersion, + AuthScopeMode: identity.AuthScopeMode, AuthResourcePaths: append([]string(nil), identity.AuthResourcePaths...), + } + tx, err := s.beginBundle(ctx, bundleIdentity) + if err != nil { + return nil, err + } + for _, schema := range schemas { + columns, err := toColumns(schema.Columns) + if err != nil { + cause := fmt.Errorf("output %q schema: %w", schema.Name, err) + cleanupCtx, cancel := boundedBundleCleanupContext(ctx) + abortErr := tx.Abort(cleanupCtx, cause) + cancel() + return nil, errors.Join(cause, abortErr) + } + if err := tx.CreateOutput(ctx, schema.Name, columns); err != nil { + cause := fmt.Errorf("output %q create: %w", schema.Name, err) + cleanupCtx, cancel := boundedBundleCleanupContext(ctx) + abortErr := tx.Abort(cleanupCtx, cause) + cancel() + return nil, errors.Join(cause, abortErr) + } + if err := tx.SetOutputMetadata(schema.Name, schema.Columns); err != nil { + cause := fmt.Errorf("output %q metadata: %w", schema.Name, err) + cleanupCtx, cancel := boundedBundleCleanupContext(ctx) + abortErr := tx.Abort(cleanupCtx, cause) + cancel() + return nil, errors.Join(cause, abortErr) + } + } + return tx, nil +} + +func (s *ClickHouseBundleStore) beginBundle(ctx context.Context, identity publication.BundleIdentity) (*clickHouseBundleTx, error) { // ponytail: one process-wide begin lock; catalog leases handle cross-process races, per-key locks if throughput matters. s.mu.Lock() defer s.mu.Unlock() @@ -84,9 +108,15 @@ func (s *ClickHouseBundleStore) BeginBundleFor(ctx context.Context, identity pub if existing, err := s.catalog.FindExecutionByKey(ctx, key); err == nil { switch existing.State { case publication.BundlePublished, publication.BundleReady: - return &clickHouseBundleTx{store: s, execution: existing, idempotent: true}, nil + return &clickHouseBundleTx{store: s, execution: existing, idempotent: true, columns: make(map[string][]clickhouse.Column)}, nil case publication.BundleQueued, publication.BundleRunning, publication.BundlePending, publication.BundlePreflight, publication.BundleLoading, publication.BundleValidating: - return nil, dataframeerrors.Wrap(fmt.Errorf("%w: %s", ErrBundleInFlight, existing.ID), dataframeerrors.CodePublicationInProgress, "", dataframeerrors.WithRetryable(true)) + return nil, dataframeerrors.Wrap( + fmt.Errorf("%w: %s", ErrBundleInFlight, existing.ID), + dataframeerrors.CodePublicationInProgress, + "", + dataframeerrors.WithDetails(map[string]any{"executionId": existing.ID}), + dataframeerrors.WithRetryable(true), + ) } } else if !errors.Is(err, publication.ErrBundleNotFound) { return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) @@ -105,7 +135,7 @@ func (s *ClickHouseBundleStore) BeginBundleFor(ctx context.Context, identity pub if !acquired { return nil, dataframeerrors.Wrap(fmt.Errorf("%w: lease for %s", ErrBundleInFlight, key), dataframeerrors.CodePublicationInProgress, "", dataframeerrors.WithRetryable(true)) } - execution := publication.BundleExecution{ID: id, Key: key, BundleIdentity: identity, State: publication.BundleQueued, CreatedAt: now, UpdatedAt: now, OwnerID: id, LeaseExpiresAt: &leaseUntil, Attempt: 1, MaxAttempts: 1} + execution := publication.BundleExecution{ID: id, Key: key, BundleIdentity: identity, State: publication.BundleQueued, CreatedAt: now, UpdatedAt: now, OwnerID: id, LeaseExpiresAt: &leaseUntil} if err := s.catalog.SaveExecution(ctx, execution); err != nil { if releaseErr := s.releaseLease(ctx, key, id); releaseErr != nil { return nil, errors.Join(err, releaseErr) @@ -124,27 +154,6 @@ func (s *ClickHouseBundleStore) BeginBundleFor(ctx context.Context, identity pub return tx, nil } -func (s *ClickHouseBundleStore) BeginClaimedBundleFor(ctx context.Context, executionID, ownerID string, identity publication.BundleIdentity) (publication.AtomicBundleTx, error) { - execution, err := s.catalog.GetExecution(ctx, executionID) - if err != nil { - return nil, err - } - execution = execution.CanonicalizeLegacy() - if execution.Name != identity.Name || execution.TranslationVersion != identity.TranslationVersion || execution.Project != identity.Project || execution.DatasetGeneration != identity.DatasetGeneration || execution.OwnerID != ownerID || execution.State != publication.BundleRunning { - return nil, dataframeerrors.NewError(dataframeerrors.CodePublicationLeaseLost, "", dataframeerrors.WithRetryable(true)) - } - // Resolution-time digests are intentionally filled after enqueue. The - // command key remains the pre-validation idempotency key. - execution.BundleIdentity = identity - expectedPointer, err := s.pointer(ctx, identity.PointerName()) - if err != nil { - return nil, err - } - tx := &clickHouseBundleTx{store: s, execution: execution, expectedPointer: expectedPointer} - tx.startLeaseRenewal(ctx) - return tx, nil -} - func (s *ClickHouseBundleStore) pointer(ctx context.Context, name string) (string, error) { p, err := s.catalog.GetPointer(ctx, name) if err != nil { @@ -167,6 +176,7 @@ type clickHouseBundleTx struct { store *ClickHouseBundleStore execution publication.BundleExecution expectedPointer string + columns map[string][]clickhouse.Column idempotent bool closed bool leaseLost bool @@ -188,6 +198,17 @@ func (t *clickHouseBundleTx) ExistingPublishedOutputs() []publication.PublishedO return result } +func (t *clickHouseBundleTx) WriteBatch(ctx context.Context, output string, rows []map[string]any) error { + if t.closed { + return fmt.Errorf("ClickHouse publication transaction is closed") + } + columns, ok := t.columns[output] + if !ok { + return fmt.Errorf("output %q was not declared", output) + } + return t.InsertRows(ctx, output, columns, rows) +} + const bundleCleanupTimeout = 10 * time.Second func boundedBundleCleanupContext(parent context.Context) (context.Context, context.CancelFunc) { @@ -297,6 +318,7 @@ func (t *clickHouseBundleTx) CreateOutput(ctx context.Context, name string, colu if len(columns) == 0 { return fmt.Errorf("bundle output %q has no columns", name) } + logicalColumns := append([]clickhouse.Column(nil), columns...) columns = withRowIdentityColumn(columns) for _, c := range columns { if c.Name == "__loom_row_id" { @@ -315,12 +337,17 @@ func (t *clickHouseBundleTx) CreateOutput(ctx context.Context, name string, colu converted[i] = publication.PhysicalColumn{Name: c.Name, ClickHouse: c.Type} } t.execution.Outputs = append(t.execution.Outputs, publication.BundleOutputRecord{Name: name, PhysicalTable: table, Selector: t.execution.Selector(name), Columns: converted, State: publication.BundleRunning}) + if t.columns == nil { + t.columns = make(map[string][]clickhouse.Column) + } + // Keep the caller-facing schema separate from the physical row identity + // column. InsertRows adds generated identities when it receives this view. + t.columns[name] = logicalColumns return t.save(ctx) } // SetOutputMetadata persists semantic schema alongside the physical table -// definition. The optional transaction method preserves compatibility with -// existing AtomicBundleTx implementations and test fakes. +// definition. func (t *clickHouseBundleTx) SetOutputMetadata(name string, columns []publication.LogicalColumn) error { if t.idempotent { return nil @@ -399,11 +426,7 @@ func (t *clickHouseBundleTx) FinalizeSchema(ctx context.Context, schemas []publi kept = append(kept, column) } if len(dropped) > 0 { - dropper, ok := t.store.clickHouse.(bundleColumnDropper) - if !ok { - return fmt.Errorf("ClickHouse store cannot drop staging columns for output %q", schema.Name) - } - if err := dropper.DropColumns(ctx, record.PhysicalTable, dropped); err != nil { + if err := t.store.clickHouse.DropColumns(ctx, record.PhysicalTable, dropped); err != nil { return err } } @@ -502,38 +525,38 @@ func cloneBundleRow(row map[string]any) map[string]any { return copy } -func (t *clickHouseBundleTx) Commit(ctx context.Context) error { +func (t *clickHouseBundleTx) Commit(ctx context.Context) ([]publication.PublishedOutput, error) { if t.idempotent { t.closed = true - return nil + return t.ExistingPublishedOutputs(), nil } if t.closed { - return fmt.Errorf("bundle transaction is closed") + return nil, fmt.Errorf("bundle transaction is closed") } if err := t.ensureLease(); err != nil { - return err + return nil, err } if len(t.execution.Outputs) == 0 { - return t.fail(ctx, fmt.Errorf("bundle has no outputs")) + return nil, t.fail(ctx, fmt.Errorf("bundle has no outputs")) } t.execution.State = publication.BundleValidating for i := range t.execution.Outputs { t.execution.Outputs[i].State = publication.BundleValidating } if err := t.save(ctx); err != nil { - return err + return nil, err } for i := range t.execution.Outputs { output := &t.execution.Outputs[i] if output.RowCount < 0 { - return t.failPhase(ctx, "VERIFY_OUTPUT", output.Name, fmt.Errorf("output %q has invalid row count", output.Name)) + return nil, t.failPhase(ctx, "VERIFY_OUTPUT", output.Name, fmt.Errorf("output %q has invalid row count", output.Name)) } columns := make([]clickhouse.Column, len(output.Columns)) for columnIndex, column := range output.Columns { columns[columnIndex] = clickhouse.Column{Name: column.Name, Type: column.ClickHouse} } if err := t.store.clickHouse.VerifyOutput(ctx, output.PhysicalTable, columns, output.RowCount); err != nil { - return t.failPhase(ctx, "VERIFY_OUTPUT", output.Name, dataframeerrors.Wrap(err, dataframeerrors.CodePublicationFailed, "", dataframeerrors.WithRetryable(true))) + return nil, t.failPhase(ctx, "VERIFY_OUTPUT", output.Name, dataframeerrors.Wrap(err, dataframeerrors.CodePublicationFailed, "", dataframeerrors.WithRetryable(true))) } verified := time.Now().UTC() output.VerifiedAt = &verified @@ -546,23 +569,31 @@ func (t *clickHouseBundleTx) Commit(ctx context.Context) error { if err := t.store.catalog.PublishExecution(ctx, t.execution.PointerName(), t.expectedPointer, t.execution); err != nil { if errors.Is(err, publication.ErrBundlePointerConflict) { err = dataframeerrors.Wrap(err, dataframeerrors.CodePublicationConflict, "", dataframeerrors.WithRetryable(true)) - return t.failPhase(ctx, "COMMIT_POINTER", "", fmt.Errorf("publish bundle pointer: %w", err)) + return nil, t.failPhase(ctx, "COMMIT_POINTER", "", fmt.Errorf("publish bundle pointer: %w", err)) } committed, conclusive, confirmErr := t.confirmCommit(ctx) if committed { t.closed = true - return t.stopLease() + return t.publishedOutputs(), t.stopLease() } if !conclusive { t.closed = true // Prevent the runner from deleting possibly published tables. uncertain := dataframeerrors.Wrap(errors.Join(ErrBundleCommitUncertain, err, confirmErr), dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - return publication.WithPhase(errors.Join(uncertain, t.stopLease()), "COMMIT_POINTER", "") + return nil, publication.WithPhase(errors.Join(uncertain, t.stopLease()), "COMMIT_POINTER", "") } err = dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - return t.failPhase(ctx, "COMMIT_POINTER", "", fmt.Errorf("publish bundle pointer: %w", err)) + return nil, t.failPhase(ctx, "COMMIT_POINTER", "", fmt.Errorf("publish bundle pointer: %w", err)) } t.closed = true - return t.stopLease() + return t.publishedOutputs(), t.stopLease() +} + +func (t *clickHouseBundleTx) publishedOutputs() []publication.PublishedOutput { + result := make([]publication.PublishedOutput, 0, len(t.execution.Outputs)) + for _, output := range t.execution.Outputs { + result = append(result, publication.PublishedOutput{Name: output.Name, PhysicalName: output.PhysicalTable, RowCount: output.RowCount, ByteCount: output.ByteCount}) + } + return result } // confirmCommit distinguishes a lost success response from a transaction that @@ -588,10 +619,6 @@ func (t *clickHouseBundleTx) confirmCommit(ctx context.Context) (committed, conc return false, false, fmt.Errorf("publication metadata is inconsistent: execution state %q, pointer %q", execution.State, pointer.ExecutionID) } -func (t *clickHouseBundleTx) Rollback(ctx context.Context) error { - return t.Abort(ctx, fmt.Errorf("bundle rolled back")) -} - func (t *clickHouseBundleTx) Abort(ctx context.Context, cause error) error { if t.idempotent || t.closed { return nil @@ -693,7 +720,53 @@ func validateBundleColumn(column publication.PhysicalColumn) error { func validBundleOutput(value string) bool { return bundleOutputRE.MatchString(value) } -// Reconcile removes abandoned staging tables and requeues bounded-retry work. +func toColumns(columns []publication.LogicalColumn) ([]clickhouse.Column, error) { + result := make([]clickhouse.Column, 0, len(columns)) + for _, column := range columns { + kind := strings.ToLower(strings.TrimSpace(column.Kind)) + columnType := "String" + switch kind { + case "boolean": + columnType = "Bool" + case "integer": + columnType = "Int64" + case "decimal": + columnType = "Float64" + case "date": + columnType = "Date" + case "date-time": + columnType = "DateTime64(3)" + case "uuid": + columnType = "UUID" + case "code": + columnType = "String" + case "object": + columnType = "JSON" + } + if column.Repeated { + columnType = "Array(" + columnType + ")" + } else if column.Nullable { + columnType = "Nullable(" + columnType + ")" + } + result = append(result, clickhouse.Column{Name: column.Name, Type: columnType}) + } + return result, nil +} + +func allOutputsQueryable(outputs []publication.BundleOutputRecord) bool { + if len(outputs) == 0 { + return false + } + for _, output := range outputs { + if !output.Queryable() { + return false + } + } + return true +} + +// Reconcile removes abandoned staging tables and repairs persisted publication +// state left behind by an interrupted commit. // It is safe to call repeatedly during startup; published pointers and queued // commands that have not started are never touched. func (s *ClickHouseBundleStore) Reconcile(ctx context.Context, olderThan time.Time) error { @@ -714,6 +787,23 @@ func (s *ClickHouseBundleStore) Reconcile(ctx context.Context, olderThan time.Ti } execution.OwnerID = reconcilerID cleanupCtx, cancel := boundedBundleCleanupContext(ctx) + pointer, pointerErr := s.catalog.GetPointer(cleanupCtx, execution.PointerName()) + if pointerErr != nil && !errors.Is(pointerErr, publication.ErrBundleNotFound) { + releaseErr := s.catalog.ReleaseBundleLease(cleanupCtx, execution.Key, reconcilerID) + cancel() + return errors.Join(pointerErr, releaseErr) + } + // A pointer is the visibility boundary. If this execution is already + // visible, its tables are live even when a stale lifecycle snapshot + // still reports a non-successful state; never clean those tables up. + if pointerErr == nil && pointer.ExecutionID == execution.ID { + releaseErr := s.catalog.ReleaseBundleLease(cleanupCtx, execution.Key, reconcilerID) + cancel() + if releaseErr != nil { + return releaseErr + } + continue + } var first error remaining := make([]publication.BundleOutputRecord, 0, len(execution.Outputs)) for _, output := range execution.Outputs { @@ -728,11 +818,6 @@ func (s *ClickHouseBundleStore) Reconcile(ctx context.Context, olderThan time.Ti execution.FailureRetryable = true execution.UpdatedAt = time.Now().UTC() execution.Outputs = remaining - if execution.MaxAttempts > 0 && execution.Attempt < execution.MaxAttempts { - execution.State = publication.BundleQueued - next := execution.UpdatedAt - execution.NextAttemptAt = &next - } execution.OwnerID = "" execution.LeaseExpiresAt = nil if err := s.catalog.SaveExecution(cleanupCtx, execution); err != nil { diff --git a/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go b/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go index 6aa086e9..8a3d71c6 100644 --- a/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go +++ b/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" "github.com/calypr/loom/internal/dataframe/publication" dfpublished "github.com/calypr/loom/internal/dataframe/published" "github.com/calypr/loom/internal/store/clickhouse" @@ -154,6 +155,7 @@ func (c *bundleCatalogFixture) ReleaseBundleLease(context.Context, string, strin type bundleClickHouseFixture struct { tables map[string][]map[string]any + lastColumns map[string][]clickhouse.Column failInsert bool insertCalls int maxRows int @@ -162,14 +164,15 @@ type bundleClickHouseFixture struct { } func newBundleClickHouseFixture() *bundleClickHouseFixture { - return &bundleClickHouseFixture{tables: map[string][]map[string]any{}} + return &bundleClickHouseFixture{tables: map[string][]map[string]any{}, lastColumns: map[string][]clickhouse.Column{}} } func (c *bundleClickHouseFixture) CreateTable(_ context.Context, name string, _ []clickhouse.Column) error { c.tables[name] = nil return nil } -func (c *bundleClickHouseFixture) InsertRows(_ context.Context, name string, _ []clickhouse.Column, rows []map[string]any) error { +func (c *bundleClickHouseFixture) InsertRows(_ context.Context, name string, columns []clickhouse.Column, rows []map[string]any) error { c.insertCalls++ + c.lastColumns[name] = append([]clickhouse.Column(nil), columns...) if len(rows) > c.maxRows { c.maxRows = len(rows) } @@ -187,6 +190,18 @@ func (c *bundleClickHouseFixture) DropTable(_ context.Context, name string) erro delete(c.tables, name) return nil } +func (c *bundleClickHouseFixture) DropColumns(_ context.Context, name string, columns []string) error { + rows, ok := c.tables[name] + if !ok { + return errors.New("table not found") + } + for _, row := range rows { + for _, column := range columns { + delete(row, column) + } + } + return nil +} func (c *bundleClickHouseFixture) VerifyOutput(_ context.Context, name string, columns []clickhouse.Column, expectedRows int64) error { if c.verifyErr != nil { return c.verifyErr @@ -209,11 +224,65 @@ func (c *bundleClickHouseFixture) QueryRows(_ context.Context, query string, _ [ return nil, errors.New("table not found") } +func TestClickHouseBundleStoreImplementsPublicationTargetDirectly(t *testing.T) { + catalog := newBundleCatalogFixture() + client := newBundleClickHouseFixture() + store, err := NewBundleStore(client, catalog) + if err != nil { + t.Fatal(err) + } + tx, err := store.Begin(context.Background(), publication.PublicationIdentity{Name: "Observation"}, []publication.OutputSchema{{ + Name: "Observation", + Columns: []publication.LogicalColumn{{Name: "id", Kind: "string"}}, + }}) + if err != nil { + t.Fatal(err) + } + if err := tx.WriteBatch(context.Background(), "Observation", []map[string]any{{"id": "1"}}); err != nil { + t.Fatal(err) + } + outputs, err := tx.Commit(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(outputs) != 1 || outputs[0].Name != "Observation" { + t.Fatalf("published outputs = %#v", outputs) + } + if len(client.lastColumns) != 1 { + t.Fatalf("insert columns = %#v", client.lastColumns) + } + for _, columns := range client.lastColumns { + if len(columns) != 2 || columns[0].Name != "__loom_row_id" || columns[1].Name != "id" { + t.Fatalf("insert columns = %#v", columns) + } + } +} + +func TestClickHouseBundleStoreReportsInProgressExecution(t *testing.T) { + catalog := newBundleCatalogFixture() + store, err := NewBundleStore(newBundleClickHouseFixture(), catalog) + if err != nil { + t.Fatal(err) + } + identity := publication.PublicationIdentity{Name: "Observation", Project: "project-a", DatasetGeneration: "generation-a"} + bundleIdentity := publication.BundleIdentity{Name: identity.Name, Project: identity.Project, DatasetGeneration: identity.DatasetGeneration, EngineVersion: "loom"} + catalog.executions["execution-a"] = publication.BundleExecution{ID: "execution-a", Key: bundleIdentity.Key(), BundleIdentity: bundleIdentity, State: publication.BundleRunning} + + _, err = store.Begin(context.Background(), identity, nil) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodePublicationInProgress) { + t.Fatalf("Begin() error = %v, want PUBLICATION_IN_PROGRESS", err) + } + if got := userErr.Details()["executionId"]; got != "execution-a" { + t.Fatalf("executionId = %v, want execution-a", got) + } +} + func TestClickHouseBundleStoreReconcilesStaleExecution(t *testing.T) { catalog := newBundleCatalogFixture() client := newBundleClickHouseFixture() store, _ := NewBundleStore(client, catalog) - tx, err := store.BeginBundleFor(context.Background(), publication.BundleIdentity{Name: "stale"}) + tx, err := store.beginBundle(context.Background(), publication.BundleIdentity{Name: "stale"}) if err != nil { t.Fatal(err) } @@ -237,6 +306,28 @@ func TestClickHouseBundleStoreReconcilesStaleExecution(t *testing.T) { } } +func TestClickHouseBundleStoreReconcileNeverDropsVisibleExecution(t *testing.T) { + catalog := newBundleCatalogFixture() + client := newBundleClickHouseFixture() + store, _ := NewBundleStore(client, catalog) + identity := publication.BundleIdentity{Project: "project-a", DatasetGeneration: "generation-1", Name: "Observation"} + execution := publication.BundleExecution{ + ID: "published-but-stale", Key: identity.Key(), BundleIdentity: identity, + State: publication.BundleRunning, UpdatedAt: time.Now().Add(-time.Hour), + Outputs: []publication.BundleOutputRecord{{Name: "Observation", PhysicalTable: "loom_visible_observation", State: publication.BundleRunning}}, + } + catalog.executions[execution.ID] = execution + catalog.pointers[identity.PointerName()] = publication.BundlePointer{Name: identity.PointerName(), ExecutionID: execution.ID} + client.tables[execution.Outputs[0].PhysicalTable] = []map[string]any{{"id": "1"}} + + if err := store.Reconcile(context.Background(), time.Now().Add(-time.Minute)); err != nil { + t.Fatal(err) + } + if _, ok := client.tables[execution.Outputs[0].PhysicalTable]; !ok { + t.Fatal("reconciliation dropped a table referenced by the visible pointer") + } +} + func TestClickHouseBundleStoreReconcileFinishesClaimedCleanupAfterCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) catalog := &leaseBundleCatalog{bundleCatalogFixture: newBundleCatalogFixture(), acquire: true, onAcquire: cancel, requireSaveContext: true} @@ -263,29 +354,26 @@ func TestPublishedOutputResolutionIsProjectAndGenerationScoped(t *testing.T) { {Name: "observation", TranslationVersion: "legacy", Project: "project-a", DatasetGeneration: "generation-1"}, {Name: "observation", TranslationVersion: "legacy", Project: "project-b", DatasetGeneration: "generation-1"}, } + verified := time.Now().UTC() for index, identity := range identities { execution := publication.BundleExecution{ ID: "execution-" + string(rune('a'+index)), BundleIdentity: identity, State: publication.BundleReady, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), - Outputs: []publication.BundleOutputRecord{{Name: "Observation", PhysicalTable: "loom_table_" + identity.Project, Columns: []publication.PhysicalColumn{{Name: "id", ClickHouse: "String"}}, State: publication.BundleReady}}, + Outputs: []publication.BundleOutputRecord{{Name: "Observation", PhysicalTable: "loom_table_" + identity.Project, Columns: []publication.PhysicalColumn{{Name: "id", ClickHouse: "String"}}, State: publication.BundleReady, VerifiedAt: &verified}}, } catalog.executions[execution.ID] = execution catalog.pointers[identity.PointerName()] = publication.BundlePointer{Name: identity.PointerName(), ExecutionID: execution.ID} } reader := &dfpublished.Reader{Catalog: catalog} selector := dfpublished.DataframeSelector{Recipe: "observation", TranslationVersion: "legacy", Output: "Observation"} - firstSources, err := reader.CurrentFederatedSources(context.Background(), []string{"project-a"}, selector) + first, err := reader.CurrentProjectDataset(context.Background(), "project-a", selector) if err != nil { t.Fatal(err) } - secondSources, err := reader.CurrentFederatedSources(context.Background(), []string{"project-b"}, selector) + second, err := reader.CurrentProjectDataset(context.Background(), "project-b", selector) if err != nil { t.Fatal(err) } - if len(firstSources) != 1 || len(secondSources) != 1 { - t.Fatalf("source counts = %d and %d, want 1 each", len(firstSources), len(secondSources)) - } - first, second := firstSources[0], secondSources[0] if first.PhysicalTable == second.PhysicalTable || first.Project == second.Project { t.Fatalf("project-scoped outputs collided: first=%#v second=%#v", first, second) } @@ -296,10 +384,10 @@ func TestClickHouseBundleStoreRejectsDuplicateInFlightExecution(t *testing.T) { client := newBundleClickHouseFixture() store, _ := NewBundleStore(client, catalog) identity := publication.BundleIdentity{Name: "in-flight"} - if _, err := store.BeginBundleFor(context.Background(), identity); err != nil { + if _, err := store.beginBundle(context.Background(), identity); err != nil { t.Fatal(err) } - if _, err := store.BeginBundleFor(context.Background(), identity); !errors.Is(err, ErrBundleInFlight) { + if _, err := store.beginBundle(context.Background(), identity); !errors.Is(err, ErrBundleInFlight) { t.Fatalf("duplicate execution error = %v", err) } } @@ -307,9 +395,9 @@ func TestClickHouseBundleStoreRejectsDuplicateInFlightExecution(t *testing.T) { func TestClickHouseBundleStoreDoesNotSwallowPointerFailure(t *testing.T) { catalog := &leaseBundleCatalog{bundleCatalogFixture: newBundleCatalogFixture(), acquire: true, pointerErr: errors.New("pointer lookup failed")} store, _ := NewBundleStore(newBundleClickHouseFixture(), catalog) - _, err := store.BeginBundleFor(context.Background(), publication.BundleIdentity{Name: "pointer-failure"}) + _, err := store.beginBundle(context.Background(), publication.BundleIdentity{Name: "pointer-failure"}) if err == nil || !errors.Is(err, catalog.pointerErr) { - t.Fatalf("BeginBundleFor() error = %v", err) + t.Fatalf("beginBundle() error = %v", err) } if catalog.acquireCalls != 0 || catalog.releaseCalls != 0 { t.Fatalf("lease calls = acquire %d release %d, want no lease on pointer failure", catalog.acquireCalls, catalog.releaseCalls) @@ -319,9 +407,9 @@ func TestClickHouseBundleStoreDoesNotSwallowPointerFailure(t *testing.T) { func TestClickHouseBundleStoreReleasesLeaseWhenInitialSaveFails(t *testing.T) { catalog := &leaseBundleCatalog{bundleCatalogFixture: newBundleCatalogFixture(), acquire: true, saveErr: errors.New("save failed")} store, _ := NewBundleStore(newBundleClickHouseFixture(), catalog) - _, err := store.BeginBundleFor(context.Background(), publication.BundleIdentity{Name: "save-failure"}) + _, err := store.beginBundle(context.Background(), publication.BundleIdentity{Name: "save-failure"}) if err == nil || !errors.Is(err, catalog.saveErr) { - t.Fatalf("BeginBundleFor() error = %v", err) + t.Fatalf("beginBundle() error = %v", err) } if catalog.releaseCalls != 1 { t.Fatalf("lease release calls = %d, want 1", catalog.releaseCalls) @@ -333,7 +421,7 @@ func TestClickHouseBundleTransactionFailsAfterLeaseLoss(t *testing.T) { client := newBundleClickHouseFixture() store, _ := NewBundleStore(client, catalog) store.leaseRenewInterval = time.Millisecond - tx, err := store.BeginBundleFor(context.Background(), publication.BundleIdentity{Name: "lease-loss"}) + tx, err := store.beginBundle(context.Background(), publication.BundleIdentity{Name: "lease-loss"}) if err != nil { t.Fatal(err) } @@ -344,14 +432,14 @@ func TestClickHouseBundleTransactionFailsAfterLeaseLoss(t *testing.T) { if err := tx.InsertRows(context.Background(), "one", []clickhouse.Column{{Name: "id", Type: "String"}}, []map[string]any{{"id": "1"}}); !errors.Is(err, ErrBundleLeaseLost) { t.Fatalf("InsertRows() error = %v, want lease loss", err) } - _ = tx.Rollback(context.Background()) + _ = tx.Abort(context.Background(), errors.New("lease lost")) } func TestClickHouseBundleTransactionContinuesAfterLeaseRenewal(t *testing.T) { catalog := &leaseBundleCatalog{bundleCatalogFixture: newBundleCatalogFixture(), acquire: true, renewResult: true, renewStarted: make(chan struct{})} store, _ := NewBundleStore(newBundleClickHouseFixture(), catalog) store.leaseRenewInterval = time.Millisecond - tx, err := store.BeginBundleFor(context.Background(), publication.BundleIdentity{Name: "lease-renewal"}) + tx, err := store.beginBundle(context.Background(), publication.BundleIdentity{Name: "lease-renewal"}) if err != nil { t.Fatal(err) } @@ -376,14 +464,14 @@ func TestClickHouseBundleTransactionContinuesAfterLeaseRenewal(t *testing.T) { case <-time.After(time.Second): t.Fatal("transaction deadlocked after successful lease renewal") } - _ = tx.Rollback(context.Background()) + _ = tx.Abort(context.Background(), errors.New("lease renewal failed")) } func TestClickHouseBundleLeaseRenewalStopsWithTransactionCleanup(t *testing.T) { catalog := &leaseBundleCatalog{bundleCatalogFixture: newBundleCatalogFixture(), acquire: true, renewBlock: true, renewStarted: make(chan struct{})} store, _ := NewBundleStore(newBundleClickHouseFixture(), catalog) store.leaseRenewInterval = time.Millisecond - tx, err := store.BeginBundleFor(context.Background(), publication.BundleIdentity{Name: "lease-cancel"}) + tx, err := store.beginBundle(context.Background(), publication.BundleIdentity{Name: "lease-cancel"}) if err != nil { t.Fatal(err) } @@ -393,7 +481,7 @@ func TestClickHouseBundleLeaseRenewalStopsWithTransactionCleanup(t *testing.T) { t.Fatal("lease renewal did not start") } done := make(chan error, 1) - go func() { done <- tx.Rollback(context.Background()) }() + go func() { done <- tx.Abort(context.Background(), errors.New("lease cancelled")) }() select { case <-done: case <-time.After(time.Second): @@ -409,7 +497,7 @@ func TestClickHouseBundleCandidateIsInvisibleUntilReadyPointerCAS(t *testing.T) t.Fatal(err) } identity := publication.BundleIdentity{Project: "project-a", DatasetGeneration: "g1", Name: "Observation"} - tx, err := store.BeginBundleFor(context.Background(), identity) + tx, err := store.beginBundle(context.Background(), identity) if err != nil { t.Fatal(err) } @@ -425,7 +513,7 @@ func TestClickHouseBundleCandidateIsInvisibleUntilReadyPointerCAS(t *testing.T) if _, err := catalog.GetPointer(context.Background(), identity.PointerName()); !errors.Is(err, publication.ErrBundleNotFound) { t.Fatalf("candidate became visible before CAS: %v", err) } - if err := tx.Commit(context.Background()); err != nil { + if _, err := tx.Commit(context.Background()); err != nil { t.Fatal(err) } pointer, err := catalog.GetPointer(context.Background(), identity.PointerName()) @@ -460,7 +548,7 @@ func TestClickHouseBundleFailedCandidatePreservesOldPointer(t *testing.T) { client.failInsert = true candidateIdentity := old.BundleIdentity candidateIdentity.RecipeDigest = "new-recipe" - tx, err := store.BeginBundleFor(context.Background(), candidateIdentity) + tx, err := store.beginBundle(context.Background(), candidateIdentity) if err != nil { t.Fatal(err) } diff --git a/internal/dataframe/publication/clickhouse/target.go b/internal/dataframe/publication/clickhouse/target.go deleted file mode 100644 index 15a73b2b..00000000 --- a/internal/dataframe/publication/clickhouse/target.go +++ /dev/null @@ -1,206 +0,0 @@ -// Package clickhouse adapts Loom's existing atomic ClickHouse bundle store to -// the backend-neutral publication runner. -package clickhouse - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/calypr/loom/internal/dataframe/publication" - store "github.com/calypr/loom/internal/store/clickhouse" -) - -type Target struct { - Store IdentityBundleStore - ExecutionID string - OwnerID string -} - -// SupportsObjectValues tells the generic publication runner that this target -// can persist logical object values as native ClickHouse JSON columns. -func (t *Target) SupportsObjectValues() bool { return true } - -func New(bundleStore IdentityBundleStore) (*Target, error) { - if bundleStore == nil { - return nil, fmt.Errorf("ClickHouse bundle store is required") - } - return &Target{Store: bundleStore}, nil -} - -func (t *Target) Begin(ctx context.Context, identity publication.PublicationIdentity, schemas []publication.OutputSchema) (publication.Transaction, error) { - bundleIdentity := publication.BundleIdentity{Name: identity.Name, TranslationVersion: identity.TranslationVersion, OutputName: identity.OutputName, Project: identity.Project, DatasetGeneration: identity.DatasetGeneration, RecipeDigest: identity.RecipeDigest, SchemaDigest: identity.SchemaDigest, ScopeDigest: identity.ScopeDigest, EngineVersion: identity.EngineVersion, AuthScopeMode: identity.AuthScopeMode, AuthResourcePaths: append([]string(nil), identity.AuthResourcePaths...)} - var tx publication.AtomicBundleTx - var err error - if t.ExecutionID != "" { - claimed, ok := t.Store.(ClaimedIdentityBundleStore) - if !ok { - return nil, fmt.Errorf("ClickHouse bundle store cannot resume durable executions") - } - tx, err = claimed.BeginClaimedBundleFor(ctx, t.ExecutionID, t.OwnerID, bundleIdentity) - } else { - tx, err = t.Store.BeginBundleFor(ctx, bundleIdentity) - } - if err != nil { - return nil, err - } - result := &transaction{tx: tx, columns: make(map[string][]store.Column, len(schemas))} - for _, schema := range schemas { - columns, err := toColumns(schema.Columns) - if err != nil { - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) - _ = tx.Abort(cleanupCtx, fmt.Errorf("output %q schema: %w", schema.Name, err)) - cancel() - return nil, fmt.Errorf("output %q schema: %w", schema.Name, err) - } - if err := tx.CreateOutput(ctx, schema.Name, columns); err != nil { - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) - _ = tx.Abort(cleanupCtx, fmt.Errorf("output %q create: %w", schema.Name, err)) - cancel() - return nil, fmt.Errorf("output %q create: %w", schema.Name, err) - } - if metadata, ok := tx.(interface { - SetOutputMetadata(string, []publication.LogicalColumn) error - }); ok { - if err := metadata.SetOutputMetadata(schema.Name, schema.Columns); err != nil { - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) - _ = tx.Abort(cleanupCtx, fmt.Errorf("output %q metadata: %w", schema.Name, err)) - cancel() - return nil, fmt.Errorf("output %q metadata: %w", schema.Name, err) - } - } - result.columns[schema.Name] = columns - } - return result, nil -} - -func NewClaimed(store ClaimedIdentityBundleStore, executionID, ownerID string) (*Target, error) { - if store == nil || strings.TrimSpace(executionID) == "" || strings.TrimSpace(ownerID) == "" { - return nil, fmt.Errorf("claimed ClickHouse target requires store, execution ID, and owner ID") - } - return &Target{Store: store, ExecutionID: executionID, OwnerID: ownerID}, nil -} - -type transaction struct { - tx publication.AtomicBundleTx - columns map[string][]store.Column - closed bool -} - -func (t *transaction) SetOutputMetadata(name string, columns []publication.LogicalColumn) error { - metadata, ok := t.tx.(interface { - SetOutputMetadata(string, []publication.LogicalColumn) error - }) - if !ok { - return nil - } - return metadata.SetOutputMetadata(name, columns) -} - -func (t *transaction) Idempotent() bool { - status, ok := t.tx.(interface{ Idempotent() bool }) - return ok && status.Idempotent() -} - -func (t *transaction) ExistingPublishedOutputs() []publication.PublishedOutput { - status, ok := t.tx.(interface { - ExistingPublishedOutputs() []publication.PublishedOutput - }) - if !ok { - return nil - } - return status.ExistingPublishedOutputs() -} - -func (t *transaction) WriteBatch(ctx context.Context, output string, rows []map[string]any) error { - if t.closed { - return fmt.Errorf("ClickHouse publication transaction is closed") - } - columns, ok := t.columns[output] - if !ok { - return fmt.Errorf("output %q was not declared", output) - } - return t.tx.InsertRows(ctx, output, columns, rows) -} - -func (t *transaction) FinalizeSchema(ctx context.Context, schemas []publication.OutputSchema) error { - if t.closed { - return fmt.Errorf("ClickHouse publication transaction is closed") - } - if finalizer, ok := t.tx.(interface { - FinalizeSchema(context.Context, []publication.OutputSchema) error - }); ok { - return finalizer.FinalizeSchema(ctx, schemas) - } - for _, schema := range schemas { - columns, ok := t.columns[schema.Name] - if !ok || len(columns) != len(schema.Columns) { - return fmt.Errorf("ClickHouse transaction cannot finalize output %q schema", schema.Name) - } - } - return nil -} - -func (t *transaction) SetFinalSchemaDigest(digest string) error { - if setter, ok := t.tx.(interface{ SetFinalSchemaDigest(string) error }); ok { - return setter.SetFinalSchemaDigest(digest) - } - return nil -} - -func (t *transaction) Commit(ctx context.Context) ([]publication.PublishedOutput, error) { - if t.closed { - return nil, fmt.Errorf("ClickHouse publication transaction is closed") - } - if err := t.tx.Commit(ctx); err != nil { - return nil, err - } - t.closed = true - return nil, nil -} - -func (t *transaction) Rollback(ctx context.Context) error { - return t.Abort(ctx, fmt.Errorf("publication rolled back")) -} - -func (t *transaction) Abort(ctx context.Context, cause error) error { - if t.closed { - return nil - } - t.closed = true - return t.tx.Abort(ctx, cause) -} - -func toColumns(columns []publication.LogicalColumn) ([]store.Column, error) { - result := make([]store.Column, 0, len(columns)) - for _, column := range columns { - kind := strings.ToLower(strings.TrimSpace(column.Kind)) - columnType := "String" - switch kind { - case "boolean": - columnType = "Bool" - case "integer": - columnType = "Int64" - case "decimal": - columnType = "Float64" - case "date": - columnType = "Date" - case "date-time": - columnType = "DateTime64(3)" - case "uuid": - columnType = "UUID" - case "code": - columnType = "String" - case "object": - columnType = "JSON" - } - if column.Repeated { - columnType = "Array(" + columnType + ")" - } else if column.Nullable { - columnType = "Nullable(" + columnType + ")" - } - result = append(result, store.Column{Name: column.Name, Type: columnType}) - } - return result, nil -} diff --git a/internal/dataframe/publication/clickhouse/target_test.go b/internal/dataframe/publication/clickhouse/target_test.go deleted file mode 100644 index dfa00b88..00000000 --- a/internal/dataframe/publication/clickhouse/target_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package clickhouse - -import ( - "testing" - - "github.com/calypr/loom/internal/dataframe/publication" -) - -func TestToColumnsPreservesNullableFlatTypes(t *testing.T) { - columns, err := toColumns([]publication.LogicalColumn{ - {Name: "created", Kind: "date-time", Nullable: true}, - {Name: "tags", Kind: "string", Repeated: true, Nullable: true}, - }) - if err != nil { - t.Fatal(err) - } - if columns[0].Type != "Nullable(DateTime64(3))" { - t.Fatalf("created type = %q", columns[0].Type) - } - if columns[1].Type != "Array(String)" { - t.Fatalf("tags type = %q", columns[1].Type) - } -} - -func TestToColumnsUsesNativeJSONForObjectValues(t *testing.T) { - columns, err := toColumns([]publication.LogicalColumn{ - {Name: "method", Kind: "object", Nullable: true}, - {Name: "methods", Kind: "object", Repeated: true}, - }) - if err != nil { - t.Fatal(err) - } - if columns[0].Type != "Nullable(JSON)" { - t.Fatalf("method type = %q", columns[0].Type) - } - if columns[1].Type != "Array(JSON)" { - t.Fatalf("methods type = %q", columns[1].Type) - } -} diff --git a/internal/dataframe/publication/clickhouse/worker.go b/internal/dataframe/publication/clickhouse/worker.go deleted file mode 100644 index 64a2fa59..00000000 --- a/internal/dataframe/publication/clickhouse/worker.go +++ /dev/null @@ -1,275 +0,0 @@ -package clickhouse - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/calypr/loom/internal/dataframe/publication" - "github.com/google/uuid" -) - -type ExecutionProcessor func(context.Context, publication.BundleExecution, publication.Target) error - -type WorkerConfig struct { - Lease time.Duration - MaxAttempts int - RetryDelay time.Duration - WorkerID string -} - -// Worker leases durable QUEUED commands and retries only typed retryable -// failures. The execution is saved before the processor performs recipe or -// schema validation. -type Worker struct { - store *ClickHouseBundleStore - processor ExecutionProcessor - maxAttempts int - retryDelay time.Duration - workerID string -} - -// Run continuously drains durable work until ctx is canceled. Individual -// execution failures are reported and do not stop unrelated commands. -func (w *Worker) Run(ctx context.Context, pollInterval time.Duration, report func(error)) error { - if pollInterval <= 0 { - pollInterval = time.Second - } - ticker := time.NewTicker(pollInterval) - defer ticker.Stop() - for { - if err := w.RunOnce(ctx); err != nil && report != nil { - report(err) - } - select { - case <-ctx.Done(): - return ctx.Err() - case <-ticker.C: - } - } -} - -func NewWorker(store *ClickHouseBundleStore, processor ExecutionProcessor, cfg WorkerConfig) (*Worker, error) { - if store == nil || processor == nil { - return nil, fmt.Errorf("publication worker requires store and processor") - } - if cfg.Lease > 0 { - store.leaseTTL = cfg.Lease - store.leaseRenewInterval = cfg.Lease / 3 - } - if cfg.MaxAttempts <= 0 { - cfg.MaxAttempts = 3 - } - if cfg.RetryDelay <= 0 { - cfg.RetryDelay = time.Second - } - if strings.TrimSpace(cfg.WorkerID) == "" { - cfg.WorkerID = "publication-worker-" + uuid.NewString() - } - return &Worker{store: store, processor: processor, maxAttempts: cfg.MaxAttempts, retryDelay: cfg.RetryDelay, workerID: cfg.WorkerID}, nil -} - -// Enqueue is idempotent by the complete BundleIdentity, which includes the -// exact recipe version and generation. It performs no recipe/schema checks. -func (w *Worker) Enqueue(ctx context.Context, identity publication.BundleIdentity) (publication.BundleExecution, error) { - if strings.TrimSpace(identity.Name) == "" || strings.TrimSpace(identity.TranslationVersion) == "" { - return publication.BundleExecution{}, fmt.Errorf("recipe name and translation version are required") - } - key := identity.Key() - if existing, err := w.store.catalog.FindExecutionByKey(ctx, key); err == nil { - return existing.CanonicalizeLegacy(), nil - } else if !errors.Is(err, publication.ErrBundleNotFound) { - return publication.BundleExecution{}, err - } - owner := "enqueue-" + uuid.NewString() - expires := time.Now().UTC().Add(w.store.leaseTTL) - claimed, err := w.store.catalog.AcquireBundleLease(ctx, key, owner, expires) - if err != nil { - return publication.BundleExecution{}, err - } - if !claimed { - return publication.BundleExecution{}, dataframeerrors.NewError(dataframeerrors.CodePublicationInProgress, "", dataframeerrors.WithRetryable(true)) - } - defer func() { _ = w.store.catalog.ReleaseBundleLease(context.WithoutCancel(ctx), key, owner) }() - if existing, err := w.store.catalog.FindExecutionByKey(ctx, key); err == nil { - return existing.CanonicalizeLegacy(), nil - } else if !errors.Is(err, publication.ErrBundleNotFound) { - return publication.BundleExecution{}, err - } - now := time.Now().UTC() - execution := publication.BundleExecution{ - ID: uuid.NewString(), Key: key, BundleIdentity: identity, State: publication.BundleQueued, - CreatedAt: now, UpdatedAt: now, MaxAttempts: w.maxAttempts, - } - if err := w.store.catalog.SaveExecution(ctx, execution); err != nil { - return publication.BundleExecution{}, err - } - return execution, nil -} - -// RunOnce recovers expired work and drains commands that are currently due. -func (w *Worker) RunOnce(ctx context.Context) error { - now := time.Now().UTC() - if err := w.recoverExpired(ctx, now); err != nil { - return err - } - queued, err := w.store.catalog.ListExecutions(ctx, publication.BundleQueued, now.Add(time.Nanosecond)) - if err != nil { - return err - } - var joined error - for _, execution := range queued { - if execution.NextAttemptAt != nil && execution.NextAttemptAt.After(now) { - continue - } - if err := w.runExecution(ctx, execution); err != nil { - joined = errors.Join(joined, err) - } - } - return joined -} - -func (w *Worker) runExecution(ctx context.Context, execution publication.BundleExecution) error { - owner := w.workerID + "-" + uuid.NewString() - expires := time.Now().UTC().Add(w.store.leaseTTL) - claimed, err := w.store.catalog.AcquireBundleLease(ctx, execution.Key, owner, expires) - if err != nil || !claimed { - return err - } - execution.OwnerID, execution.LeaseExpiresAt = owner, &expires - execution.State = publication.BundleRunning - execution.Attempt++ - execution.UpdatedAt = time.Now().UTC() - execution.NextAttemptAt = nil - if len(execution.Outputs) != 0 { - remaining, cleanupErr := w.dropOutputs(ctx, execution.Outputs) - execution.Outputs = remaining - if cleanupErr != nil { - if saveErr := w.store.catalog.SaveExecution(ctx, execution); saveErr != nil { - cleanupErr = errors.Join(cleanupErr, saveErr) - } - return w.recordFailure(ctx, execution.ID, owner, publication.WithPhase(cleanupErr, "CLEANUP", "")) - } - } - if err := w.store.catalog.SaveExecution(ctx, execution); err != nil { - _ = w.store.catalog.ReleaseBundleLease(context.WithoutCancel(ctx), execution.Key, owner) - return err - } - target, err := NewClaimed(w.store, execution.ID, owner) - if err == nil { - err = w.processor(ctx, execution, target) - } - if err == nil { - published, loadErr := w.store.catalog.GetExecution(ctx, execution.ID) - if loadErr != nil { - err = loadErr - } else if !published.State.Successful() || !allOutputsQueryable(published.Outputs) { - err = dataframeerrors.NewError(dataframeerrors.CodePublicationFailed, "", dataframeerrors.WithRetryable(true)) - } - } - if err != nil { - if errors.Is(err, ErrBundleCommitUncertain) { - releaseErr := w.store.catalog.ReleaseBundleLease(context.WithoutCancel(ctx), execution.Key, owner) - return errors.Join(err, releaseErr) - } - return w.recordFailure(ctx, execution.ID, owner, err) - } - return w.store.catalog.ReleaseBundleLease(context.WithoutCancel(ctx), execution.Key, owner) -} - -func allOutputsQueryable(outputs []publication.BundleOutputRecord) bool { - if len(outputs) == 0 { - return false - } - for _, output := range outputs { - if !output.Queryable() { - return false - } - } - return true -} - -func (w *Worker) recordFailure(ctx context.Context, executionID, owner string, cause error) error { - cleanupCtx, cancel := boundedBundleCleanupContext(ctx) - defer cancel() - execution, loadErr := w.store.catalog.GetExecution(cleanupCtx, executionID) - if loadErr != nil { - return errors.Join(cause, loadErr) - } - remaining, cleanupErr := w.dropOutputs(cleanupCtx, execution.Outputs) - if cleanupErr != nil { - cleanupErr = dataframeerrors.Wrap(cleanupErr, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - cause = publication.WithPhase(errors.Join(cleanupErr, cause), "CLEANUP", "") - } - normalized := dataframeerrors.Normalize(cause) - execution.State = publication.BundleFailed - execution.FailureCode = normalized.Code() - execution.FailureRetryable = normalized.Retryable() - execution.Error = cause.Error() - execution.FailureDetails = cause.Error() - var phase *publication.PhaseError - if errors.As(cause, &phase) { - execution.FailurePhase, execution.FailureOutput = phase.Phase, phase.Output - } - execution.Outputs = remaining - execution.OwnerID, execution.LeaseExpiresAt = "", nil - execution.UpdatedAt = time.Now().UTC() - if normalized.Retryable() && execution.Attempt < execution.MaxAttempts { - execution.State = publication.BundleQueued - next := execution.UpdatedAt.Add(w.retryDelay * time.Duration(execution.Attempt)) - execution.NextAttemptAt = &next - } - saveErr := w.store.catalog.SaveExecution(cleanupCtx, execution) - releaseErr := w.store.catalog.ReleaseBundleLease(cleanupCtx, execution.Key, owner) - return errors.Join(cause, saveErr, releaseErr) -} - -func (w *Worker) recoverExpired(ctx context.Context, now time.Time) error { - for _, state := range []publication.BundleState{publication.BundleRunning, publication.BundleValidating} { - executions, err := w.store.catalog.ListExecutions(ctx, state, now) - if err != nil { - return err - } - for _, execution := range executions { - if execution.LeaseExpiresAt != nil && execution.LeaseExpiresAt.After(now) { - continue - } - owner := w.workerID + "-recovery-" + uuid.NewString() - expires := now.Add(w.store.leaseTTL) - claimed, err := w.store.catalog.AcquireBundleLease(ctx, execution.Key, owner, expires) - if err != nil { - return err - } - if !claimed { - continue - } - execution.OwnerID = owner - if err := w.store.catalog.SaveExecution(ctx, execution); err != nil { - return err - } - recovery := dataframeerrors.NewError(dataframeerrors.CodePublicationLeaseLost, "", dataframeerrors.WithRetryable(true)) - if err := w.recordFailure(ctx, execution.ID, owner, publication.WithPhase(recovery, "RECOVER_LEASE", "")); err != nil && execution.Attempt >= execution.MaxAttempts { - return err - } - } - } - return nil -} - -func (w *Worker) dropOutputs(ctx context.Context, outputs []publication.BundleOutputRecord) ([]publication.BundleOutputRecord, error) { - remaining := make([]publication.BundleOutputRecord, 0, len(outputs)) - var joined error - for _, output := range outputs { - if strings.TrimSpace(output.PhysicalTable) == "" { - continue - } - if err := w.store.clickHouse.DropTable(ctx, output.PhysicalTable); err != nil { - remaining = append(remaining, output) - joined = errors.Join(joined, fmt.Errorf("drop staging table %q: %w", output.PhysicalTable, err)) - } - } - return remaining, joined -} diff --git a/internal/dataframe/publication/clickhouse/worker_test.go b/internal/dataframe/publication/clickhouse/worker_test.go deleted file mode 100644 index 7a88cf34..00000000 --- a/internal/dataframe/publication/clickhouse/worker_test.go +++ /dev/null @@ -1,272 +0,0 @@ -package clickhouse - -import ( - "context" - "errors" - "testing" - "time" - - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/calypr/loom/internal/dataframe/publication" -) - -func workerIdentity() publication.BundleIdentity { - return publication.BundleIdentity{Name: "documents", TranslationVersion: "v1", Project: "p1", DatasetGeneration: "g1", RecipeDigest: "digest"} -} - -func publishFixture(ctx context.Context, execution publication.BundleExecution, target publication.Target, names ...string) error { - streams := make([]publication.OutputStream, 0, len(names)) - for _, name := range names { - name := name - streams = append(streams, publication.OutputStream{ - Name: name, Columns: []publication.LogicalColumn{{Name: "id", Kind: "string"}}, - Stream: func(_ context.Context, visit func(map[string]any) error) error { - return visit(map[string]any{"id": name + "-1"}) - }, - }) - } - _, err := publication.Publish(ctx, target, publication.PublicationIdentity{ - Name: execution.Name, TranslationVersion: execution.TranslationVersion, Project: execution.Project, - DatasetGeneration: execution.DatasetGeneration, RecipeDigest: execution.RecipeDigest, - }, streams, publication.Limits{}) - return err -} - -func TestWorkerDuplicateRequestsAreIdempotent(t *testing.T) { - catalog := newBundleCatalogFixture() - store, _ := NewBundleStore(newBundleClickHouseFixture(), catalog) - worker, _ := NewWorker(store, func(context.Context, publication.BundleExecution, publication.Target) error { return nil }, WorkerConfig{}) - first, err := worker.Enqueue(context.Background(), workerIdentity()) - if err != nil { - t.Fatal(err) - } - second, err := worker.Enqueue(context.Background(), workerIdentity()) - if err != nil { - t.Fatal(err) - } - if first.ID != second.ID || len(catalog.executions) != 1 || first.State != publication.BundleQueued { - t.Fatalf("enqueue was not idempotent: first=%#v second=%#v", first, second) - } -} - -func TestWorkerIdentityIncludesRestrictedEmptyScope(t *testing.T) { - unrestricted := workerIdentity() - unrestricted.AuthScopeMode = "unrestricted" - restricted := unrestricted - restricted.AuthScopeMode = "restricted" - if unrestricted.Key() == restricted.Key() { - t.Fatal("restricted-empty and unrestricted commands shared an idempotency key") - } - catalog := newBundleCatalogFixture() - store, _ := NewBundleStore(newBundleClickHouseFixture(), catalog) - var observed string - worker, _ := NewWorker(store, func(_ context.Context, execution publication.BundleExecution, _ publication.Target) error { - observed = execution.AuthScopeMode - return dataframeerrors.NewError(dataframeerrors.CodeInvalidData, "") - }, WorkerConfig{MaxAttempts: 1}) - if _, err := worker.Enqueue(context.Background(), restricted); err != nil { - t.Fatal(err) - } - _ = worker.RunOnce(context.Background()) - if observed != "restricted" { - t.Fatalf("recovered auth scope mode = %q, want restricted", observed) - } -} - -func TestWorkerRetriesTypedRetryableFailure(t *testing.T) { - catalog := newBundleCatalogFixture() - client := newBundleClickHouseFixture() - store, _ := NewBundleStore(client, catalog) - calls := 0 - worker, _ := NewWorker(store, func(ctx context.Context, execution publication.BundleExecution, target publication.Target) error { - calls++ - if calls == 1 { - return publication.WithPhase(dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)), "RESOLVE", "") - } - return publishFixture(ctx, execution, target, "one", "two") - }, WorkerConfig{MaxAttempts: 3, RetryDelay: time.Nanosecond}) - execution, _ := worker.Enqueue(context.Background(), workerIdentity()) - if err := worker.RunOnce(context.Background()); err == nil { - t.Fatal("first retryable failure was not reported") - } - retrying := catalog.executions[execution.ID] - if retrying.State != publication.BundleQueued || retrying.Attempt != 1 || retrying.FailurePhase != "RESOLVE" { - t.Fatalf("retry state = %#v", retrying) - } - past := time.Now().Add(-time.Second) - retrying.NextAttemptAt = &past - catalog.executions[execution.ID] = retrying - if err := worker.RunOnce(context.Background()); err != nil { - t.Fatal(err) - } - published := catalog.executions[execution.ID] - if published.State != publication.BundlePublished || published.Attempt != 2 || !allOutputsQueryable(published.Outputs) { - t.Fatalf("recovered execution = %#v", published) - } - for _, output := range published.Outputs { - want := publication.DataframeSelector{Recipe: "documents", TranslationVersion: "v1", Output: output.Name} - if output.Selector != want { - t.Fatalf("output selector = %#v, want %#v", output.Selector, want) - } - } -} - -func TestWorkerDoesNotRetryPermanentValidationFailure(t *testing.T) { - catalog := newBundleCatalogFixture() - store, _ := NewBundleStore(newBundleClickHouseFixture(), catalog) - calls := 0 - worker, _ := NewWorker(store, func(context.Context, publication.BundleExecution, publication.Target) error { - calls++ - return publication.WithPhase(dataframeerrors.NewError(dataframeerrors.CodeInvalidData, ""), "VALIDATE_RECIPE", "") - }, WorkerConfig{MaxAttempts: 3}) - execution, _ := worker.Enqueue(context.Background(), workerIdentity()) - _ = worker.RunOnce(context.Background()) - _ = worker.RunOnce(context.Background()) - failed := catalog.executions[execution.ID] - if calls != 1 || failed.State != publication.BundleFailed || failed.FailureRetryable || failed.Attempt != 1 { - t.Fatalf("permanent failure retried: calls=%d execution=%#v", calls, failed) - } -} - -func TestWorkerRecoversExpiredLeaseAndCleansStaging(t *testing.T) { - catalog := newBundleCatalogFixture() - client := newBundleClickHouseFixture() - store, _ := NewBundleStore(client, catalog) - worker, _ := NewWorker(store, func(ctx context.Context, execution publication.BundleExecution, target publication.Target) error { - return publishFixture(ctx, execution, target, "one") - }, WorkerConfig{MaxAttempts: 3, RetryDelay: time.Nanosecond}) - identity := workerIdentity() - past := time.Now().Add(-time.Minute) - execution := publication.BundleExecution{ID: "interrupted", Key: identity.Key(), BundleIdentity: identity, State: publication.BundleRunning, Attempt: 1, MaxAttempts: 3, CreatedAt: past, UpdatedAt: past, LeaseExpiresAt: &past, Outputs: []publication.BundleOutputRecord{{Name: "one", PhysicalTable: "staging_interrupted", State: publication.BundleRunning}}} - catalog.executions[execution.ID] = execution - client.tables["staging_interrupted"] = []map[string]any{{"id": "partial"}} - if err := worker.RunOnce(context.Background()); err != nil { - t.Fatal(err) - } - recovered := catalog.executions[execution.ID] - past = time.Now().Add(-time.Second) - recovered.NextAttemptAt = &past - catalog.executions[execution.ID] = recovered - if err := worker.RunOnce(context.Background()); err != nil { - t.Fatal(err) - } - if _, exists := client.tables["staging_interrupted"]; exists { - t.Fatal("interrupted staging table was not cleaned") - } - if got := catalog.executions[execution.ID]; got.State != publication.BundlePublished || got.Attempt != 2 { - t.Fatalf("recovered execution = %#v", got) - } -} - -func TestWorkerRecoversExpiredValidatingLease(t *testing.T) { - catalog := newBundleCatalogFixture() - client := newBundleClickHouseFixture() - store, _ := NewBundleStore(client, catalog) - worker, _ := NewWorker(store, func(ctx context.Context, execution publication.BundleExecution, target publication.Target) error { - return publishFixture(ctx, execution, target, "one") - }, WorkerConfig{MaxAttempts: 3, RetryDelay: time.Nanosecond}) - identity := workerIdentity() - past := time.Now().Add(-time.Minute) - execution := publication.BundleExecution{ID: "validating", Key: identity.Key(), BundleIdentity: identity, State: publication.BundleValidating, Attempt: 1, MaxAttempts: 3, CreatedAt: past, UpdatedAt: past, LeaseExpiresAt: &past, Outputs: []publication.BundleOutputRecord{{Name: "one", PhysicalTable: "staging_validating", State: publication.BundleValidating}}} - catalog.executions[execution.ID] = execution - client.tables["staging_validating"] = []map[string]any{{"id": "partial"}} - if err := worker.RunOnce(context.Background()); err != nil { - t.Fatal(err) - } - recovered := catalog.executions[execution.ID] - if recovered.State != publication.BundleQueued || len(recovered.Outputs) != 0 { - t.Fatalf("validating recovery = %#v", recovered) - } -} - -func TestWorkerRetainsFailedCleanupUntilDropSucceeds(t *testing.T) { - catalog := newBundleCatalogFixture() - client := newBundleClickHouseFixture() - client.dropErr = errors.New("ClickHouse unavailable") - store, _ := NewBundleStore(client, catalog) - worker, _ := NewWorker(store, func(ctx context.Context, execution publication.BundleExecution, target publication.Target) error { - return publishFixture(ctx, execution, target, "one") - }, WorkerConfig{MaxAttempts: 3, RetryDelay: time.Nanosecond}) - identity := workerIdentity() - past := time.Now().Add(-time.Minute) - execution := publication.BundleExecution{ID: "cleanup", Key: identity.Key(), BundleIdentity: identity, State: publication.BundleRunning, Attempt: 1, MaxAttempts: 3, CreatedAt: past, UpdatedAt: past, LeaseExpiresAt: &past, Outputs: []publication.BundleOutputRecord{{Name: "one", PhysicalTable: "staging_cleanup", State: publication.BundleRunning}}} - catalog.executions[execution.ID] = execution - client.tables["staging_cleanup"] = []map[string]any{{"id": "partial"}} - _ = worker.RunOnce(context.Background()) - retained := catalog.executions[execution.ID] - if retained.State != publication.BundleQueued || len(retained.Outputs) != 1 { - t.Fatalf("failed cleanup was forgotten: %#v", retained) - } - client.dropErr = nil - due := time.Now().Add(-time.Second) - retained.NextAttemptAt = &due - catalog.executions[execution.ID] = retained - if err := worker.RunOnce(context.Background()); err != nil { - t.Fatal(err) - } - if _, exists := client.tables["staging_cleanup"]; exists { - t.Fatal("retained staging table was not cleaned on retry") - } - if got := catalog.executions[execution.ID]; got.State != publication.BundlePublished { - t.Fatalf("execution did not recover: %#v", got) - } -} - -func TestWorkerConfirmsCommitAfterLostResponse(t *testing.T) { - catalog := &leaseBundleCatalog{bundleCatalogFixture: newBundleCatalogFixture(), acquire: true, renewResult: true, publishCommitThenErr: errors.New("response lost")} - client := newBundleClickHouseFixture() - store, _ := NewBundleStore(client, catalog) - worker, _ := NewWorker(store, func(ctx context.Context, execution publication.BundleExecution, target publication.Target) error { - return publishFixture(ctx, execution, target, "one") - }, WorkerConfig{MaxAttempts: 1}) - execution, err := worker.Enqueue(context.Background(), workerIdentity()) - if err != nil { - t.Fatal(err) - } - if err := worker.RunOnce(context.Background()); err != nil { - t.Fatal(err) - } - published := catalog.executions[execution.ID] - if published.State != publication.BundlePublished || !allOutputsQueryable(published.Outputs) { - t.Fatalf("lost response was not confirmed: %#v", published) - } - if _, exists := client.tables[published.Outputs[0].PhysicalTable]; !exists { - t.Fatal("confirmed published table was deleted") - } - pointer, err := catalog.GetPointer(context.Background(), workerIdentity().PointerName()) - if err != nil || pointer.ExecutionID != execution.ID { - t.Fatalf("published pointer = %#v, %v", pointer, err) - } -} - -func TestVerificationAndPointerFailuresNeverPublish(t *testing.T) { - for _, test := range []struct { - name string - verifyErr error - publishErr error - }{ - {name: "verification", verifyErr: errors.New("verification failed")}, - {name: "pointer", publishErr: publication.ErrBundlePointerConflict}, - } { - t.Run(test.name, func(t *testing.T) { - catalog := &leaseBundleCatalog{bundleCatalogFixture: newBundleCatalogFixture(), acquire: true, renewResult: true, publishErr: test.publishErr} - client := newBundleClickHouseFixture() - client.verifyErr = test.verifyErr - store, _ := NewBundleStore(client, catalog) - worker, _ := NewWorker(store, func(ctx context.Context, execution publication.BundleExecution, target publication.Target) error { - return publishFixture(ctx, execution, target, "one", "two") - }, WorkerConfig{MaxAttempts: 1}) - execution, _ := worker.Enqueue(context.Background(), workerIdentity()) - if err := worker.RunOnce(context.Background()); err == nil { - t.Fatal("failure was not returned") - } - failed := catalog.executions[execution.ID] - if failed.State == publication.BundlePublished || failed.PublishedAt != nil { - t.Fatalf("failed execution reported published: %#v", failed) - } - if _, err := catalog.GetPointer(context.Background(), workerIdentity().PointerName()); !errors.Is(err, publication.ErrBundleNotFound) { - t.Fatalf("visibility pointer changed: %v", err) - } - }) - } -} diff --git a/internal/dataframe/publication/runner.go b/internal/dataframe/publication/runner.go index ec7ff7bf..88509c74 100644 --- a/internal/dataframe/publication/runner.go +++ b/internal/dataframe/publication/runner.go @@ -26,10 +26,7 @@ func Publish(ctx context.Context, target Target, identity PublicationIdentity, o if err != nil { return Result{}, err } - supportsObjects := false - if objectTarget, ok := target.(ObjectValueTarget); ok { - supportsObjects = objectTarget.SupportsObjectValues() - } + supportsObjects := target.SupportsObjectValues() schemas, err := validateOutputs(normalizedOutputs, supportsObjects) if err != nil { return Result{}, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidData, "") @@ -39,23 +36,13 @@ func Publish(ctx context.Context, target Target, identity PublicationIdentity, o if err != nil { return Result{}, err } - if noop, ok := tx.(interface { - Idempotent() bool - ExistingPublishedOutputs() []PublishedOutput - }); ok && noop.Idempotent() { - return Result{Outputs: noop.ExistingPublishedOutputs()}, nil + if tx.Idempotent() { + return Result{Outputs: tx.ExistingPublishedOutputs()}, nil } fail := func(cause error) (Result, error) { cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) defer cancel() - var abortErr error - if aborter, ok := tx.(interface { - Abort(context.Context, error) error - }); ok { - abortErr = aborter.Abort(cleanupCtx, cause) - } else { - abortErr = tx.Rollback(cleanupCtx) - } + abortErr := tx.Abort(cleanupCtx, cause) return Result{}, errors.Join(cause, abortErr) } stats := make(map[string]PublishedOutput, len(normalizedOutputs)) @@ -128,23 +115,11 @@ func Publish(ctx context.Context, target Target, identity PublicationIdentity, o retained = append(retained, schema) } finalDigest := FinalSchemaDigest(identity, retained) - if finalizer, ok := tx.(interface { - FinalizeSchema(context.Context, []OutputSchema) error - }); ok { - if err := finalizer.FinalizeSchema(ctx, retained); err != nil { - return fail(fmt.Errorf("publication schema finalization: %w", err)) - } - } else { - for index := range retained { - if len(retained[index].Columns) != len(normalizedOutputs[index].Columns) { - return fail(fmt.Errorf("publication target cannot finalize pruned schema for output %q", retained[index].Name)) - } - } + if err := tx.FinalizeSchema(ctx, retained); err != nil { + return fail(fmt.Errorf("publication schema finalization: %w", err)) } - if setter, ok := tx.(interface{ SetFinalSchemaDigest(string) error }); ok { - if err := setter.SetFinalSchemaDigest(finalDigest); err != nil { - return fail(fmt.Errorf("publication schema digest: %w", err)) - } + if err := tx.SetFinalSchemaDigest(finalDigest); err != nil { + return fail(fmt.Errorf("publication schema digest: %w", err)) } published, err := tx.Commit(ctx) if err != nil { @@ -236,6 +211,9 @@ func validateOutputs(outputs []OutputStream, supportsObjects bool) ([]OutputSche if strings.TrimSpace(column.Name) == "" || strings.TrimSpace(column.Kind) == "" { return nil, fmt.Errorf("output %q has an invalid column", name) } + if !supportedLogicalKind(column.Kind) { + return nil, fmt.Errorf("column %q has unsupported logical kind %q", column.Name, column.Kind) + } if strings.EqualFold(strings.TrimSpace(column.Kind), "object") && !supportsObjects { return nil, fmt.Errorf("output %q object-valued column %q is not supported by the publication target", name, column.Name) } @@ -307,6 +285,15 @@ func validateValue(column LogicalColumn, value any, supportsObjects bool) error return validateScalar(column, value, supportsObjects) } +func supportedLogicalKind(kind string) bool { + switch strings.ToLower(strings.TrimSpace(kind)) { + case "string", "code", "uuid", "date", "date-time", "datetime", "integer", "decimal", "boolean", "object": + return true + default: + return false + } +} + func validateScalar(column LogicalColumn, value any, supportsObjects bool) error { if value == nil { return nil diff --git a/internal/dataframe/publication/runner_test.go b/internal/dataframe/publication/runner_test.go index 8c984aa2..678e278b 100644 --- a/internal/dataframe/publication/runner_test.go +++ b/internal/dataframe/publication/runner_test.go @@ -4,7 +4,10 @@ import ( "context" "errors" "fmt" + "strings" "testing" + + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" ) type fakeTx struct { @@ -21,13 +24,19 @@ func (t *fakeTx) Commit(context.Context) ([]PublishedOutput, error) { t.committed = true return []PublishedOutput{{Name: "patients", PhysicalName: "staged_patients"}}, nil } -func (t *fakeTx) Rollback(context.Context) error { t.rolledBack = true; return nil } +func (t *fakeTx) Abort(context.Context, error) error { t.rolledBack = true; return nil } +func (t *fakeTx) FinalizeSchema(context.Context, []OutputSchema) error { return nil } +func (t *fakeTx) SetFinalSchemaDigest(string) error { return nil } +func (t *fakeTx) Idempotent() bool { return false } +func (t *fakeTx) ExistingPublishedOutputs() []PublishedOutput { return nil } type fakeTarget struct { tx *fakeTx schemas []OutputSchema } +func (t *fakeTarget) SupportsObjectValues() bool { return false } + type finalizingTx struct { fakeTx finalized []OutputSchema @@ -46,6 +55,8 @@ func (t *finalizingTx) SetFinalSchemaDigest(digest string) error { type finalizingTarget struct{ tx *finalizingTx } +func (t *finalizingTarget) SupportsObjectValues() bool { return false } + func (t *finalizingTarget) Begin(_ context.Context, _ PublicationIdentity, _ []OutputSchema) (Transaction, error) { t.tx = &finalizingTx{} return t.tx, nil @@ -159,6 +170,24 @@ func TestPublishAllowsObjectsOnlyForObjectCapableTargets(t *testing.T) { } } +func TestPublishRejectsUnknownLogicalKindBeforeEmptyStreamBegins(t *testing.T) { + target := &fakeTarget{} + _, err := Publish(context.Background(), target, PublicationIdentity{Name: "r"}, []OutputStream{{ + Name: "empty", + Columns: []LogicalColumn{{Name: "value", Kind: "mystery"}}, + Stream: func(_ context.Context, _ func(map[string]any) error) error { + return nil + }, + }}, Limits{}) + var typedErr *dataframeerrors.Error + if err == nil || !errors.As(err, &typedErr) || typedErr.Code() != string(dataframeerrors.CodeInvalidData) || typedErr.Unwrap() == nil || !strings.Contains(typedErr.Unwrap().Error(), `column "value" has unsupported logical kind "mystery"`) { + t.Fatalf("expected unknown logical kind error, got %v", err) + } + if target.tx != nil { + t.Fatal("target began a transaction for an invalid empty stream") + } +} + func TestPublishProjectIDIsTrustedAndDeduplicated(t *testing.T) { target := &fakeTarget{} _, err := Publish(context.Background(), target, PublicationIdentity{Name: "r", Project: "trusted"}, []OutputStream{{ diff --git a/internal/dataframe/publication/types.go b/internal/dataframe/publication/types.go index 76663d4d..5350c707 100644 --- a/internal/dataframe/publication/types.go +++ b/internal/dataframe/publication/types.go @@ -54,16 +54,15 @@ type PublicationIdentity struct { // OutputName scopes single-output publications. The field is empty for // legacy/full-bundle publications and is used by incremental Explorer // loads so each output can retain its own visibility pointer. - OutputName string - Project string - DatasetGeneration string - RecipeDigest string - SchemaDigest string - ScopeDigest string - EngineVersion string - TargetConfigDigest string - AuthScopeMode string - AuthResourcePaths []string + OutputName string + Project string + DatasetGeneration string + RecipeDigest string + SchemaDigest string + ScopeDigest string + EngineVersion string + AuthScopeMode string + AuthResourcePaths []string } type PublishedOutput struct { @@ -75,30 +74,17 @@ type PublishedOutput struct { type Target interface { Begin(context.Context, PublicationIdentity, []OutputSchema) (Transaction, error) -} - -// ObjectValueTarget is implemented by publication targets that can persist -// logical object columns without flattening or stringifying them. Targets -// that do not implement this capability continue to receive the generic flat -// publication contract, where object values are rejected before any rows are -// written. -type ObjectValueTarget interface { SupportsObjectValues() bool } type Transaction interface { WriteBatch(context.Context, string, []map[string]any) error - Commit(context.Context) ([]PublishedOutput, error) - Rollback(context.Context) error -} - -// SchemaFinalizer is implemented by publication targets that can remove -// unpopulated discovered columns from their private staging tables. The -// runner uses the optional interface so older non-ClickHouse test targets -// remain source-compatible while production publication requires it when a -// column must be dropped. -type SchemaFinalizer interface { FinalizeSchema(context.Context, []OutputSchema) error + SetFinalSchemaDigest(string) error + Commit(context.Context) ([]PublishedOutput, error) + Abort(context.Context, error) error + Idempotent() bool + ExistingPublishedOutputs() []PublishedOutput } // FinalSchemaDigest computes the versioned digest for the schema actually diff --git a/internal/dataframe/published/aggregate_clickhouse_integration_test.go b/internal/dataframe/published/aggregate_clickhouse_integration_test.go index 96734543..d0c196d3 100644 --- a/internal/dataframe/published/aggregate_clickhouse_integration_test.go +++ b/internal/dataframe/published/aggregate_clickhouse_integration_test.go @@ -34,9 +34,7 @@ func TestAggregateBatchClickHouseIntegration(t *testing.T) { t.Fatal(err) } tableA := "loom_aggregate_a_" + uuid.NewString()[:8] - tableB := "loom_aggregate_b_" + uuid.NewString()[:8] defer client.DropTable(ctx, tableA) - defer client.DropTable(ctx, tableB) physicalColumns := []storeclickhouse.Column{ {Name: "__loom_row_id", Type: "UInt64"}, {Name: "facet", Type: "Nullable(String)"}, @@ -44,10 +42,8 @@ func TestAggregateBatchClickHouseIntegration(t *testing.T) { {Name: "value", Type: "Nullable(Float64)"}, {Name: "auth_resource_path", Type: "String"}, } - for _, table := range []string{tableA, tableB} { - if err := client.CreateTable(ctx, table, physicalColumns); err != nil { - t.Fatal(err) - } + if err := client.CreateTable(ctx, tableA, physicalColumns); err != nil { + t.Fatal(err) } if err := client.InsertRows(ctx, tableA, physicalColumns, []map[string]any{ {"__loom_row_id": uint64(1), "facet": "a", "facet_other": "x", "value": 2.0, "auth_resource_path": "allowed"}, @@ -56,30 +52,14 @@ func TestAggregateBatchClickHouseIntegration(t *testing.T) { }); err != nil { t.Fatal(err) } - if err := client.InsertRows(ctx, tableB, physicalColumns, []map[string]any{ - {"__loom_row_id": uint64(1), "facet": "b", "facet_other": "y", "value": 5.0, "auth_resource_path": "anything"}, - }); err != nil { - t.Fatal(err) - } - sourceColumns := []Column{ + columns := []Column{ {Name: "__loom_row_id", ClickHouse: "UInt64"}, {Name: "facet", ClickHouse: "Nullable(String)"}, {Name: "facet_other", ClickHouse: "Nullable(String)"}, {Name: "value", ClickHouse: "Nullable(Float64)"}, {Name: authResourcePathColumn, ClickHouse: "String"}, } - dataset := FederatedDataset{ - Columns: []Column{ - {Name: "facet", ClickHouse: "Nullable(String)"}, - {Name: "facet_other", ClickHouse: "Nullable(String)"}, - {Name: "value", ClickHouse: "Nullable(Float64)"}, - {Name: projectIDColumn, ClickHouse: "String"}, - }, - Sources: []Materialization{ - {ID: "a:Patient", Project: "a", PhysicalTable: tableA, Columns: sourceColumns}, - {ID: "b:Patient", Project: "b", PhysicalTable: tableB, Columns: sourceColumns}, - }, - } + dataset := Materialization{ID: "a:Patient", Project: "a", PhysicalTable: tableA, Columns: columns, Selector: DataframeSelector{Recipe: "recipe", Output: "Patient"}} result, err := (&Reader{ClickHouse: client}).ExecuteAggregateBatch(ctx, dataset, AggregateBatchRequest{ Jobs: []AggregateJob{ {ID: 1, ResponseMode: AggregateResponseLegacy, GroupBy: []string{"facet"}, Operation: "COUNT"}, @@ -88,10 +68,7 @@ func TestAggregateBatchClickHouseIntegration(t *testing.T) { {ID: 4, ResponseMode: AggregateResponseTerms, Column: "facet", Size: 2}, {ID: 5, ResponseMode: AggregateResponseTerms, Column: "facet_other", Size: 1}, }, - AccessByProject: map[string]SourceAccess{ - "a": {ResourcePaths: []string{"allowed"}}, - "b": {Unrestricted: true}, - }, + Unrestricted: true, }) if err != nil { t.Fatal(err) diff --git a/internal/dataframe/published/aggregate_execute.go b/internal/dataframe/published/aggregate_execute.go index 5795f94a..6d43c74e 100644 --- a/internal/dataframe/published/aggregate_execute.go +++ b/internal/dataframe/published/aggregate_execute.go @@ -15,7 +15,7 @@ import ( dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" ) -func (r *Reader) ExecuteAggregateBatch(ctx context.Context, dataset FederatedDataset, req AggregateBatchRequest) (AggregateBatchResult, error) { +func (r *Reader) ExecuteAggregateBatch(ctx context.Context, dataset Materialization, req AggregateBatchRequest) (AggregateBatchResult, error) { result := AggregateBatchResult{LogicalJobs: len(req.Jobs)} if r == nil || r.ClickHouse == nil { return result, dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) @@ -42,7 +42,7 @@ func (r *Reader) ExecuteAggregateBatch(ctx context.Context, dataset FederatedDat if r.Logger != nil { r.Logger.Debug("dataframe aggregate plan built", "selector", dataset.Selector.Key(), - "source_count", len(dataset.Sources), + "source_count", 1, "logical_jobs", result.LogicalJobs, "valid_jobs", len(plan.jobs), "deduplicated_jobs", result.DeduplicatedJobs, @@ -54,7 +54,7 @@ func (r *Reader) ExecuteAggregateBatch(ctx context.Context, dataset FederatedDat if err := ctx.Err(); err != nil { return assembleAggregateBatchResult(req.Jobs, plan, result), err } - sql, err := buildAggregateSQL(dataset, req.AccessByProject, statement) + sql, err := buildAggregateSQL(dataset, req, statement) if err != nil { setStatementError(plan.results, statement, err) continue @@ -72,7 +72,7 @@ func (r *Reader) ExecuteAggregateBatch(ctx context.Context, dataset FederatedDat "statement_kind", string(statement.kind), "job_count", len(statement.jobs), "grouping_sets", sql.groupingSets, - "source_count", len(dataset.Sources), + "source_count", 1, "source_column_count", len(sql.sourceColumns), "query_id", shortQueryID(sql.query), "query_bytes", len(sql.query), @@ -82,12 +82,12 @@ func (r *Reader) ExecuteAggregateBatch(ctx context.Context, dataset FederatedDat rows, err := r.ClickHouse.QueryRowsArgs(ctx, sql.query, sql.columns, sql.args...) if err != nil { if r.Logger != nil { - r.Logger.Error("clickhouse federated aggregate failed", + r.Logger.Error("clickhouse aggregate failed", "selector", dataset.Selector.Key(), "statement_kind", string(statement.kind), "job_count", len(statement.jobs), "grouping_sets", sql.groupingSets, - "source_count", len(dataset.Sources), + "source_count", 1, "source_column_count", len(sql.sourceColumns), "query_id", shortQueryID(sql.query), "query_bytes", len(sql.query), diff --git a/internal/dataframe/published/aggregate_execute_test.go b/internal/dataframe/published/aggregate_execute_test.go index e11921f8..4d063bc7 100644 --- a/internal/dataframe/published/aggregate_execute_test.go +++ b/internal/dataframe/published/aggregate_execute_test.go @@ -27,7 +27,7 @@ func TestExecuteAggregateBatchRichShapesShareScans(t *testing.T) { {ID: 2, ResponseMode: AggregateResponseStats, Column: "facet_000"}, {ID: 3, ResponseMode: AggregateResponseMissing, Column: "facet_000"}, }, - AccessByProject: map[string]SourceAccess{"project": {Unrestricted: true}}, + Unrestricted: true, }) if err != nil { t.Fatal(err) @@ -58,8 +58,8 @@ func TestExecuteAggregateBatchUsesTypedMultiColumnOrdering(t *testing.T) { {"__loom_slot": int64(0), "__loom_group_json": "[null,\"a\"]", "__loom_metric": int64(1)}, }}} result, err := (&Reader{ClickHouse: fake}).ExecuteAggregateBatch(context.Background(), dataset, AggregateBatchRequest{ - Jobs: []AggregateJob{{ID: 1, ResponseMode: AggregateResponseLegacy, GroupBy: []string{"facet_000", "facet_001"}, Operation: "COUNT"}}, - AccessByProject: map[string]SourceAccess{"project": {Unrestricted: true}}, + Jobs: []AggregateJob{{ID: 1, ResponseMode: AggregateResponseLegacy, GroupBy: []string{"facet_000", "facet_001"}, Operation: "COUNT"}}, + Unrestricted: true, }) if err != nil { t.Fatal(err) diff --git a/internal/dataframe/published/aggregate_plan.go b/internal/dataframe/published/aggregate_plan.go index a2e22f32..8241b72a 100644 --- a/internal/dataframe/published/aggregate_plan.go +++ b/internal/dataframe/published/aggregate_plan.go @@ -48,7 +48,7 @@ func ExcludeSelfFilters(filters []Filter, column string) []Filter { return result } -func buildAggregatePlan(dataset FederatedDataset, req AggregateBatchRequest) aggregatePlan { +func buildAggregatePlan(dataset Materialization, req AggregateBatchRequest) aggregatePlan { plan := aggregatePlan{results: make(map[int]AggregateJobResult, len(req.Jobs))} allowed := make(map[string]struct{}, len(dataset.Columns)) for _, column := range dataset.Columns { @@ -87,12 +87,22 @@ func buildAggregatePlan(dataset FederatedDataset, req AggregateBatchRequest) agg jobs := groups[key] kind := statementKind(jobs[0].job.ResponseMode) if kind == statementLegacy || kind == statementTerms || kind == statementBucket { - for start := 0; start < len(jobs); start += maxGroupingSets { - end := start + maxGroupingSets - if end > len(jobs) { - end = len(jobs) + batch := make([]*plannedAggregateJob, 0, min(len(jobs), maxGroupingSets)) + termColumns := make(map[string]struct{}) + for _, job := range jobs { + _, repeatedTermColumn := termColumns[job.job.Column] + if len(batch) == maxGroupingSets || (kind == statementTerms && repeatedTermColumn) { + plan.statements = append(plan.statements, aggregateStatementPlan{kind: kind, jobs: batch, filters: batch[0].filters}) + batch = make([]*plannedAggregateJob, 0, min(len(jobs), maxGroupingSets)) + clear(termColumns) } - plan.statements = append(plan.statements, aggregateStatementPlan{kind: kind, jobs: jobs[start:end], filters: jobs[0].filters}) + batch = append(batch, job) + if kind == statementTerms { + termColumns[job.job.Column] = struct{}{} + } + } + if len(batch) > 0 { + plan.statements = append(plan.statements, aggregateStatementPlan{kind: kind, jobs: batch, filters: batch[0].filters}) } } else { plan.statements = append(plan.statements, aggregateStatementPlan{kind: kind, jobs: jobs, filters: jobs[0].filters}) @@ -140,7 +150,7 @@ func validateAggregateJob(input AggregateJob, allowed map[string]struct{}) (Aggr for i := range job.GroupBy { job.GroupBy[i] = strings.TrimSpace(job.GroupBy[i]) if _, ok := allowed[job.GroupBy[i]]; !ok || internalAggregateColumn(job.GroupBy[i]) { - return job, nil, "", "", aggregateInvalidRequest("group column %q is not in federated dataset schema", job.GroupBy[i]) + return job, nil, "", "", aggregateInvalidRequest("group column %q is not in the published dataset schema", job.GroupBy[i]) } } filters, filterKey, err := canonicalFilters(job.Filters, allowed) @@ -154,14 +164,14 @@ func validateAggregateJob(input AggregateJob, allowed map[string]struct{}) (Aggr job.Column = "" case "COUNT_DISTINCT", "SUM", "AVG", "MIN", "MAX": if _, ok := allowed[job.Column]; !ok || internalAggregateColumn(job.Column) { - return job, nil, "", "", aggregateInvalidRequest("metric column %q is not in federated dataset schema", job.Column) + return job, nil, "", "", aggregateInvalidRequest("metric column %q is not in the published dataset schema", job.Column) } default: return job, nil, "", "", aggregateInvalidRequest("unsupported aggregate operation %q", input.Operation) } case AggregateResponseTerms, AggregateResponseHistogram, AggregateResponseDateHistogram, AggregateResponseStats, AggregateResponseMissing: if _, ok := allowed[job.Column]; !ok || internalAggregateColumn(job.Column) { - return job, nil, "", "", aggregateInvalidRequest("aggregate column %q is not in federated dataset schema", job.Column) + return job, nil, "", "", aggregateInvalidRequest("aggregate column %q is not in the published dataset schema", job.Column) } if job.ResponseMode == AggregateResponseTerms || job.ResponseMode == AggregateResponseHistogram || job.ResponseMode == AggregateResponseDateHistogram { if job.Size <= 0 { @@ -205,7 +215,7 @@ func canonicalFilters(filters []Filter, allowed map[string]struct{}) ([]Filter, filter.Column = strings.TrimSpace(filter.Column) filter.Op = strings.ToUpper(strings.TrimSpace(filter.Op)) if _, ok := allowed[filter.Column]; !ok || internalAggregateColumn(filter.Column) { - return nil, "", dataframeerrors.NewError(dataframeerrors.CodeInvalidFilter, fmt.Sprintf("filter column %q is not in federated dataset schema", filter.Column)) + return nil, "", dataframeerrors.NewError(dataframeerrors.CodeInvalidFilter, fmt.Sprintf("filter column %q is not in the published dataset schema", filter.Column)) } switch filter.Op { case "EQ", "NEQ", "IN", "NOT_IN", "LT", "LTE", "GT", "GTE", "CONTAINS", "STARTS_WITH", "EXISTS", "IS_NULL", "ARRAY_CONTAINS", "ARRAY_OVERLAPS": @@ -260,7 +270,7 @@ func canonicalCollection(value any) any { return values } -func statementFamily(job AggregateJob, dataset FederatedDataset) string { +func statementFamily(job AggregateJob, dataset Materialization) string { switch job.ResponseMode { case AggregateResponseLegacy: return "legacy:" + job.Operation + ":" + job.Column diff --git a/internal/dataframe/published/aggregate_plan_test.go b/internal/dataframe/published/aggregate_plan_test.go index 1afb657f..5969eb05 100644 --- a/internal/dataframe/published/aggregate_plan_test.go +++ b/internal/dataframe/published/aggregate_plan_test.go @@ -43,18 +43,12 @@ func (f *aggregateFakeQueryer) QueryRowsArgsVisit(ctx context.Context, query str return nil } -func aggregateTestDataset(columnCount int) FederatedDataset { +func aggregateTestDataset(columnCount int) Materialization { columns := make([]Column, columnCount) for i := range columns { columns[i] = Column{Name: fmt.Sprintf("facet_%03d", i), ClickHouse: "Nullable(String)"} } - return FederatedDataset{ - Columns: columns, - Sources: []Materialization{{ - ID: "source:Patient", Project: "project", PhysicalTable: "physical_patient", - Columns: append([]Column(nil), columns...), - }}, - } + return Materialization{ID: "source:Patient", Project: "project", PhysicalTable: "physical_patient", Columns: append([]Column(nil), columns...), Selector: DataframeSelector{Recipe: "recipe", Output: "Patient"}} } func TestAggregatePlanCombines156CountFacets(t *testing.T) { @@ -69,7 +63,7 @@ func TestAggregatePlanCombines156CountFacets(t *testing.T) { ) fake := &aggregateFakeQueryer{} result, err := (&Reader{ClickHouse: fake}).ExecuteAggregateBatch(context.Background(), dataset, AggregateBatchRequest{ - Jobs: jobs, AccessByProject: map[string]SourceAccess{"project": {Unrestricted: true}}, + Jobs: jobs, Unrestricted: true, }) if err != nil { t.Fatal(err) @@ -109,7 +103,7 @@ func TestAggregatePlanRanksMultipleTermsByProjectedSlot(t *testing.T) { {ID: 1, ResponseMode: AggregateResponseTerms, Column: "facet_000", Size: 10}, {ID: 2, ResponseMode: AggregateResponseTerms, Column: "facet_001", Size: 10}, }, - AccessByProject: map[string]SourceAccess{"project": {Unrestricted: true}}, + Unrestricted: true, }) if err != nil { t.Fatal(err) @@ -136,6 +130,29 @@ func TestAggregatePlanRanksMultipleTermsByProjectedSlot(t *testing.T) { } } +func TestAggregatePlanSeparatesRepeatedTermsColumn(t *testing.T) { + dataset := aggregateTestDataset(1) + plan := buildAggregatePlan(dataset, AggregateBatchRequest{Jobs: []AggregateJob{ + {ID: 1, ResponseMode: AggregateResponseTerms, Column: "facet_000", Size: 50}, + {ID: 2, ResponseMode: AggregateResponseTerms, Column: "facet_000", Size: 12}, + }}) + + groupingStatements := make([]aggregateStatementPlan, 0, 2) + for _, statement := range plan.statements { + if statement.kind == statementTerms { + groupingStatements = append(groupingStatements, statement) + } + } + if len(groupingStatements) != 2 { + t.Fatalf("terms statements = %d, want 2", len(groupingStatements)) + } + for index, statement := range groupingStatements { + if len(statement.jobs) != 1 { + t.Fatalf("terms statement %d jobs = %d, want 1", index, len(statement.jobs)) + } + } +} + func TestAggregatePlanCanonicalizesFilterAndMemberOrder(t *testing.T) { dataset := aggregateTestDataset(2) first := []Filter{{Column: "facet_001", Op: "IN", Value: []any{"b", "a"}}, {Column: "facet_000", Op: "EQ", Value: "x"}} @@ -157,7 +174,7 @@ func TestAggregatePlanValidationIsJobLocal(t *testing.T) { {ID: 1, ResponseMode: AggregateResponseLegacy, GroupBy: []string{"missing"}, Operation: "COUNT"}, {ID: 2, ResponseMode: AggregateResponseLegacy, GroupBy: []string{"facet_000"}, Operation: "COUNT"}, }, - AccessByProject: map[string]SourceAccess{"project": {Unrestricted: true}}, + Unrestricted: true, }) if err != nil { t.Fatal(err) @@ -180,7 +197,7 @@ func TestAggregateDedupFansOutIndependentRows(t *testing.T) { {ID: 1, ResponseMode: AggregateResponseLegacy, GroupBy: []string{"facet_000"}, Operation: "COUNT"}, {ID: 2, ResponseMode: AggregateResponseLegacy, GroupBy: []string{"facet_000"}, Operation: "COUNT"}, }, - AccessByProject: map[string]SourceAccess{"project": {Unrestricted: true}}, + Unrestricted: true, }) if err != nil { t.Fatal(err) diff --git a/internal/dataframe/published/aggregate_sql.go b/internal/dataframe/published/aggregate_sql.go index b1950723..f458e266 100644 --- a/internal/dataframe/published/aggregate_sql.go +++ b/internal/dataframe/published/aggregate_sql.go @@ -30,22 +30,22 @@ type aggregateSQL struct { groupingSets int } -func buildAggregateSQL(dataset FederatedDataset, access map[string]SourceAccess, statement aggregateStatementPlan) (aggregateSQL, error) { +func buildAggregateSQL(dataset Materialization, req AggregateBatchRequest, statement aggregateStatementPlan) (aggregateSQL, error) { switch statement.kind { case statementLegacy: - return buildGroupingSetSQL(dataset, access, statement, false) + return buildGroupingSetSQL(dataset, req, statement, false) case statementTerms: - return buildGroupingSetSQL(dataset, access, statement, true) + return buildGroupingSetSQL(dataset, req, statement, true) case statementBucket: - return buildBucketSQL(dataset, access, statement) + return buildBucketSQL(dataset, req, statement) case statementScalar: - return buildScalarSQL(dataset, access, statement) + return buildScalarSQL(dataset, req, statement) default: return aggregateSQL{}, fmt.Errorf("unknown aggregate statement kind %q", statement.kind) } } -func aggregateUnion(dataset FederatedDataset, access map[string]SourceAccess, statement aggregateStatementPlan) (string, []any, []string, error) { +func aggregateSource(dataset Materialization, req AggregateBatchRequest, statement aggregateStatementPlan) (string, []any, []string, error) { set := make(map[string]struct{}) for _, job := range statement.jobs { for _, column := range job.job.GroupBy { @@ -63,15 +63,36 @@ func aggregateUnion(dataset FederatedDataset, access map[string]SourceAccess, st columns = append(columns, column) } sort.Strings(columns) - union, args, err := federatedNormalizedUnion(dataset, columns, access) - return union, args, columns, err + present := make(map[string]Column, len(dataset.Columns)) + for _, column := range dataset.Columns { + present[column.Name] = column + } + selects := make([]string, 0, len(columns)) + for _, column := range columns { + if _, ok := present[column]; !ok { + return "", nil, nil, invalidRequest() + } + selects = append(selects, fmt.Sprintf("`%s`", column)) + } + if len(selects) == 0 { + selects = []string{"*"} + } + query := fmt.Sprintf("SELECT %s FROM `%s`", strings.Join(selects, ", "), dataset.PhysicalTable) + args := make([]any, 0) + if !req.Unrestricted && hasColumn(dataset.Columns, authResourcePathColumn) { + query += " WHERE `auth_resource_path` IN ?" + args = append(args, req.AuthResourcePaths) + } else if !req.Unrestricted { + query += " WHERE 0" + } + return query, args, columns, nil } -func buildGroupingSetSQL(dataset FederatedDataset, access map[string]SourceAccess, statement aggregateStatementPlan, terms bool) (aggregateSQL, error) { +func buildGroupingSetSQL(dataset Materialization, req AggregateBatchRequest, statement aggregateStatementPlan, terms bool) (aggregateSQL, error) { if !terms { - return buildLegacyGroupingSetSQL(dataset, access, statement) + return buildLegacyGroupingSetSQL(dataset, req, statement) } - return buildTermsGroupingSetSQL(dataset, access, statement) + return buildTermsGroupingSetSQL(dataset, req, statement) } // buildLegacyGroupingSetSQL keeps the aggregate state narrow even when an @@ -82,8 +103,8 @@ func buildGroupingSetSQL(dataset FederatedDataset, access map[string]SourceAcces // one physical GROUPING SETS aggregation over the narrow pair. The source // union is still built once per statement, and every logical job remains // represented by its slot in the grouped key. -func buildLegacyGroupingSetSQL(dataset FederatedDataset, access map[string]SourceAccess, statement aggregateStatementPlan) (aggregateSQL, error) { - union, args, sourceColumns, err := aggregateUnion(dataset, access, statement) +func buildLegacyGroupingSetSQL(dataset Materialization, req AggregateBatchRequest, statement aggregateStatementPlan) (aggregateSQL, error) { + union, args, sourceColumns, err := aggregateSource(dataset, req, statement) if err != nil { return aggregateSQL{}, err } @@ -141,8 +162,8 @@ func buildLegacyGroupingSetSQL(dataset FederatedDataset, access map[string]Sourc }, nil } -func buildTermsGroupingSetSQL(dataset FederatedDataset, access map[string]SourceAccess, statement aggregateStatementPlan) (aggregateSQL, error) { - union, args, sourceColumns, err := aggregateUnion(dataset, access, statement) +func buildTermsGroupingSetSQL(dataset Materialization, req AggregateBatchRequest, statement aggregateStatementPlan) (aggregateSQL, error) { + union, args, sourceColumns, err := aggregateSource(dataset, req, statement) if err != nil { return aggregateSQL{}, err } @@ -308,8 +329,8 @@ func legacyMetricOnColumn(job AggregateJob, column string) string { } } -func buildBucketSQL(dataset FederatedDataset, access map[string]SourceAccess, statement aggregateStatementPlan) (aggregateSQL, error) { - union, unionArgs, sourceColumns, err := aggregateUnion(dataset, access, statement) +func buildBucketSQL(dataset Materialization, req AggregateBatchRequest, statement aggregateStatementPlan) (aggregateSQL, error) { + union, unionArgs, sourceColumns, err := aggregateSource(dataset, req, statement) if err != nil { return aggregateSQL{}, err } @@ -394,8 +415,8 @@ func buildBucketSQL(dataset FederatedDataset, access map[string]SourceAccess, st }, nil } -func buildScalarSQL(dataset FederatedDataset, access map[string]SourceAccess, statement aggregateStatementPlan) (aggregateSQL, error) { - union, args, sourceColumns, err := aggregateUnion(dataset, access, statement) +func buildScalarSQL(dataset Materialization, req AggregateBatchRequest, statement aggregateStatementPlan) (aggregateSQL, error) { + union, args, sourceColumns, err := aggregateSource(dataset, req, statement) if err != nil { return aggregateSQL{}, err } @@ -432,7 +453,7 @@ func buildScalarSQL(dataset FederatedDataset, access map[string]SourceAccess, st return aggregateSQL{query: query, columns: columns, args: args, sourceColumns: sourceColumns}, nil } -func aggregateAllowedColumns(dataset FederatedDataset) map[string]struct{} { +func aggregateAllowedColumns(dataset Materialization) map[string]struct{} { allowed := make(map[string]struct{}, len(dataset.Columns)) for _, column := range dataset.Columns { if !internalAggregateColumn(column.Name) { diff --git a/internal/dataframe/published/aggregate_types.go b/internal/dataframe/published/aggregate_types.go index 664d4ecf..183be532 100644 --- a/internal/dataframe/published/aggregate_types.go +++ b/internal/dataframe/published/aggregate_types.go @@ -13,9 +13,10 @@ type AggregationSpec struct { } type AggregationsRequest struct { - Filters []Filter - Specs []AggregationSpec - AccessByProject map[string]SourceAccess + Filters []Filter + Specs []AggregationSpec + AuthResourcePaths []string + Unrestricted bool } type AggregationResult struct { @@ -28,8 +29,8 @@ type AggregationResult struct { } type AggregationsResult struct { - Dataset FederatedDataset - Aggregations []AggregationResult + Materialization Materialization + Aggregations []AggregationResult } const ( @@ -72,8 +73,9 @@ type AggregateJobResult struct { } type AggregateBatchRequest struct { - Jobs []AggregateJob - AccessByProject map[string]SourceAccess + Jobs []AggregateJob + AuthResourcePaths []string + Unrestricted bool } type AggregateBatchResult struct { diff --git a/internal/dataframe/published/current.go b/internal/dataframe/published/current.go new file mode 100644 index 00000000..39a88df3 --- /dev/null +++ b/internal/dataframe/published/current.go @@ -0,0 +1,164 @@ +package published + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "time" + + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + bundlepublication "github.com/calypr/loom/internal/dataframe/publication" + publication "github.com/calypr/loom/internal/dataset" +) + +// CurrentProjectDataset returns the one pointer-visible, successful output for +// a project and exact selector. Project identity is explicit at this boundary; +// this reader never unions tables from different projects. +func (r *Reader) CurrentProjectDataset(ctx context.Context, project string, selector DataframeSelector) (Materialization, error) { + project = strings.TrimSpace(project) + if r == nil || r.Catalog == nil { + return Materialization{}, fmt.Errorf("bundle catalog dependency is required") + } + if project == "" || !selector.Valid() { + return Materialization{}, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + activeGeneration, err := r.activeGeneration(ctx, project) + if err != nil { + return Materialization{}, err + } + if exact, ok := r.Catalog.(bundlepublication.ExactExecutionCatalog); ok && activeGeneration != "" { + execution, output, err := exact.FindExecutionBySelector(ctx, project, activeGeneration, selector) + if err != nil { + if errors.Is(err, bundlepublication.ErrBundleNotFound) { + return Materialization{}, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + return Materialization{}, err + } + if value, ok, err := r.currentMaterialization(ctx, execution, output, selector); err != nil { + return Materialization{}, err + } else if ok { + return value, nil + } + return Materialization{}, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + executions, err := r.Catalog.ListExecutions(ctx, bundlepublication.BundlePublished, time.Now().UTC().Add(time.Second)) + if err != nil { + return Materialization{}, err + } + for _, execution := range executions { + if execution.Project != project || !execution.State.Successful() { + continue + } + if activeGeneration != "" && execution.DatasetGeneration != activeGeneration { + continue + } + for _, output := range execution.Outputs { + if output.Name != selector.Output { + continue + } + if value, ok, err := r.currentMaterialization(ctx, execution, output, selector); err != nil { + return Materialization{}, err + } else if ok { + return value, nil + } + } + } + return Materialization{}, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") +} + +func (r *Reader) currentMaterialization(ctx context.Context, execution bundlepublication.BundleExecution, output bundlepublication.BundleOutputRecord, selector DataframeSelector) (Materialization, bool, error) { + if !execution.State.Successful() || !output.Queryable() || outputSelector(execution, output.Name, output.Selector) != selector { + return Materialization{}, false, nil + } + pointer, err := r.Catalog.GetPointer(ctx, execution.PointerName()) + if err != nil { + return Materialization{}, false, fmt.Errorf("resolve dataframe pointer: %w", err) + } + if pointer.ExecutionID != execution.ID { + return Materialization{}, false, nil + } + result := publishedMaterialization(execution, output, selector.Output) + result.Selector = selector + return result, true, nil +} + +// CurrentProjectDatasets returns every current output for one project. Each +// returned materialization is backed by one active ClickHouse table. +func (r *Reader) CurrentProjectDatasets(ctx context.Context, project string) ([]Materialization, error) { + project = strings.TrimSpace(project) + if r == nil || r.Catalog == nil { + return nil, fmt.Errorf("bundle catalog dependency is required") + } + if project == "" { + return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + activeGeneration, err := r.activeGeneration(ctx, project) + if err != nil { + return nil, err + } + executions, err := r.Catalog.ListExecutions(ctx, bundlepublication.BundlePublished, time.Now().UTC().Add(time.Second)) + if err != nil { + return nil, err + } + result := make([]Materialization, 0) + for _, execution := range executions { + if execution.Project != project || !execution.State.Successful() { + continue + } + if activeGeneration != "" && execution.DatasetGeneration != activeGeneration { + continue + } + pointer, err := r.Catalog.GetPointer(ctx, execution.PointerName()) + if err != nil { + return nil, fmt.Errorf("resolve dataframe pointer: %w", err) + } + if pointer.ExecutionID != execution.ID { + continue + } + for _, output := range execution.Outputs { + if !output.Queryable() { + continue + } + selector := outputSelector(execution, output.Name, output.Selector) + if !selector.Valid() { + continue + } + value := publishedMaterialization(execution, output, output.Name) + value.Selector = selector + result = append(result, value) + } + } + if len(result) == 0 { + return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") + } + sort.Slice(result, func(i, j int) bool { + if result[i].Name != result[j].Name { + return result[i].Name < result[j].Name + } + return result[i].ID < result[j].ID + }) + return result, nil +} + +func (r *Reader) activeGeneration(ctx context.Context, project string) (string, error) { + if r.ActiveManifestResolver == nil { + return "", nil + } + manifest, err := publication.ResolveActive(ctx, r.ActiveManifestResolver, project) + if err != nil { + if errors.Is(err, publication.ErrNoActiveGeneration) { + return "", nil + } + return "", err + } + return manifest.Dataset.Generation, nil +} + +func outputSelector(execution bundlepublication.BundleExecution, output string, value DataframeSelector) DataframeSelector { + if value.Valid() { + return value + } + return execution.Selector(output) +} diff --git a/internal/dataframe/published/current_test.go b/internal/dataframe/published/current_test.go new file mode 100644 index 00000000..89c69d70 --- /dev/null +++ b/internal/dataframe/published/current_test.go @@ -0,0 +1,61 @@ +package published + +import ( + "context" + "testing" + "time" + + "github.com/calypr/loom/internal/dataframe/publication" + "github.com/calypr/loom/internal/dataset" +) + +type exactCurrentCatalog struct { + publication.BundleCatalog + execution publication.BundleExecution + output publication.BundleOutputRecord +} + +func (c exactCurrentCatalog) FindExecutionBySelector(context.Context, string, string, publication.DataframeSelector) (publication.BundleExecution, publication.BundleOutputRecord, error) { + return c.execution, c.output, nil +} + +func (c exactCurrentCatalog) GetPointer(context.Context, string) (publication.BundlePointer, error) { + return publication.BundlePointer{ExecutionID: c.execution.ID}, nil +} + +type activeManifestFixture struct{ manifest dataset.Manifest } + +func (f activeManifestFixture) ResolveActiveManifest(context.Context, string) (dataset.Manifest, error) { + return f.manifest, nil +} + +func TestCurrentProjectDatasetUsesExactCatalogLookup(t *testing.T) { + selector := DataframeSelector{Recipe: "recipe", TranslationVersion: "v1", Output: "Patient"} + verified := time.Now().UTC() + output := publication.BundleOutputRecord{ + Name: "Patient", Selector: selector, PhysicalTable: "patient_table", + State: publication.BundlePublished, VerifiedAt: &verified, + } + execution := publication.BundleExecution{ + ID: "execution", BundleIdentity: publication.BundleIdentity{ + Name: "recipe", TranslationVersion: "v1", Project: "P1", DatasetGeneration: "generation", + }, + State: publication.BundlePublished, Outputs: []publication.BundleOutputRecord{output}, + } + manifest := dataset.Manifest{ + Dataset: dataset.Ref{Project: "P1", Generation: "generation"}, State: dataset.StateStaged, + SchemaIdentity: dataset.SchemaSnapshot{SchemaSHA256: "0000000000000000000000000000000000000000000000000000000000000000", GeneratedResourceTypes: []string{"Patient"}}, + } + reader := Reader{ + Catalog: exactCurrentCatalog{execution: execution, output: output}, + ActiveManifestResolver: activeManifestFixture{manifest: manifest}, + } + + value, err := reader.CurrentProjectDataset(context.Background(), "P1", selector) + if err != nil { + t.Fatal(err) + } + if value.ID != "execution:Patient" || value.PhysicalTable != "patient_table" { + t.Fatalf("materialization = %#v", value) + } +} diff --git a/internal/dataframe/published/errors.go b/internal/dataframe/published/errors.go index ed3362e3..1e9ebcf8 100644 --- a/internal/dataframe/published/errors.go +++ b/internal/dataframe/published/errors.go @@ -22,3 +22,15 @@ func backendCallError(err error) error { } return dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) } + +func backendUnavailable() error { + return dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) +} + +func invalidRequest() error { + return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") +} + +func invalidCursor() error { + return dataframeerrors.NewError(dataframeerrors.CodeInvalidCursor, "") +} diff --git a/internal/dataframe/published/federation.go b/internal/dataframe/published/federation.go deleted file mode 100644 index 63231bfe..00000000 --- a/internal/dataframe/published/federation.go +++ /dev/null @@ -1,733 +0,0 @@ -package published - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "sort" - "strings" - "time" - - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - bundlepublication "github.com/calypr/loom/internal/dataframe/publication" - publication "github.com/calypr/loom/internal/dataset" - fhirschema "github.com/calypr/loom/internal/fhir/schema" -) - -// FederatedDataset is the logical reader view of one FHIR resource type across all -// authorized project publications. project_id is a public, stable row column; -// legacy outputs that predate it are filled from the source publication project. -type FederatedDataset struct { - Selector DataframeSelector - Name string - Revision string - Sources []Materialization - Columns []Column - RowCount int64 - RowCountComplete bool - Availability FederationAvailability - ExpectedProjects int - ProjectStatuses []ProjectStatus -} - -type FederationAvailability string - -const ( - FederationAvailable FederationAvailability = "AVAILABLE" - FederationDegraded FederationAvailability = "DEGRADED" - FederationUnavailable FederationAvailability = "UNAVAILABLE" -) - -type ProjectState string - -const ( - ProjectCurrent ProjectState = "CURRENT" - ProjectStale ProjectState = "STALE" - ProjectBuilding ProjectState = "BUILDING" - ProjectFailed ProjectState = "FAILED" - ProjectMissing ProjectState = "MISSING" - ProjectExcluded ProjectState = "EXCLUDED" -) - -type ProjectStatus struct { - ProjectID string - State ProjectState - Generation string - ExecutionID string - CreatedAt time.Time - UpdatedAt time.Time - ErrorCode string - Retryable bool -} - -// ExecutionSelectorResolver is the narrow integration contract implemented by -// version-aware publication catalogs. It lets federation consume exact -// identities without depending on publication persistence internals. -type ExecutionSelectorResolver interface { - DataframeSelectorForExecution(context.Context, string, string) (DataframeSelector, error) -} - -// ProjectStatusResolver is implemented by the project-release store. The -// caller always passes an authorization-filtered project list. -type ProjectStatusResolver interface { - DataframeProjectStatuses(context.Context, []string, DataframeSelector) ([]ProjectStatus, error) -} - -// ReleaseExecutionResolver selects the immutable execution recorded by each -// active project release. A present project with an empty execution ID means -// the active release intentionally does not expose that selector. -type ReleaseExecutionResolver interface { - ActiveReleaseExecutionIDs(context.Context, []string, DataframeSelector) (map[string]string, error) - ActiveReleaseSelectors(context.Context, []string) ([]DataframeSelector, map[string]bool, error) -} - -// FederationSnapshotResolver resolves active release execution IDs and their -// project statuses together. This lets interactive reads avoid independently -// rediscovering the same release and publication lifecycle metadata. -type FederationSnapshotResolver interface { - ResolveFederationSnapshot(context.Context, []string, DataframeSelector) (map[string]string, []ProjectStatus, bool, error) -} - -func (r *Reader) FederationProjectStatuses(ctx context.Context, projects []string, selector DataframeSelector) ([]ProjectStatus, error) { - if r != nil && r.ProjectStatusResolver != nil { - return r.ProjectStatusResolver.DataframeProjectStatuses(ctx, append([]string(nil), projects...), selector) - } - return nil, nil -} - -// CurrentFederatedSnapshot resolves sources and project status through the -// combined release fast path when available. The legacy discovery methods are -// retained as a fallback for alternate catalogs and tests. -func (r *Reader) CurrentFederatedSnapshot(ctx context.Context, projects []string, selector DataframeSelector) ([]Materialization, []ProjectStatus, error) { - projects = normalizedProjects(projects) - if r != nil && r.FederationSnapshotResolver != nil { - if r.Catalog == nil { - return nil, nil, fmt.Errorf("bundle catalog dependency is required") - } - executionIDs, statuses, complete, err := r.FederationSnapshotResolver.ResolveFederationSnapshot(ctx, projects, selector) - if err != nil { - return nil, nil, err - } - if !complete { - sources, err := r.CurrentFederatedSources(ctx, projects, selector) - if err != nil { - return nil, nil, err - } - statuses, err := r.FederationProjectStatuses(ctx, projects, selector) - return sources, statuses, err - } - sources := make([]Materialization, 0, len(executionIDs)) - for _, project := range projects { - executionID := strings.TrimSpace(executionIDs[project]) - if executionID == "" { - continue - } - execution, err := r.Catalog.GetExecution(ctx, executionID) - if err != nil { - return nil, nil, err - } - if execution.Project != project || !execution.State.Successful() { - continue - } - for _, output := range execution.Outputs { - if output.Name != selector.Output { - continue - } - executionSelector := output.Selector - if !executionSelector.Valid() { - executionSelector = execution.Selector(selector.Output) - } - if executionSelector != selector { - continue - } - materialization := publishedMaterialization(execution, output, selector.Output) - materialization.Selector = selector - sources = append(sources, materialization) - break - } - } - return sources, statuses, nil - } - sources, err := r.CurrentFederatedSources(ctx, projects, selector) - if err != nil { - return nil, nil, err - } - statuses, err := r.FederationProjectStatuses(ctx, projects, selector) - return sources, statuses, err -} - -// SourceAccess is the already-resolved visibility for one published project. -// Missing entries are intentionally treated as restricted with no paths. -type SourceAccess struct { - ResourcePaths []string - Unrestricted bool -} - -// FederatedPageRequest is the projectless reader request. The caller supplies -// the already-resolved effective scope; this type never accepts a project -// selector from the browser. -type FederatedPageRequest struct { - Columns []string - Filters []Filter - Sort *Sort - First int - After string - AccessByProject map[string]SourceAccess -} - -// FederatedPage is the projectless row response. Source metadata is retained -// only for internal cursor/revision handling. -type FederatedPage struct { - Dataset FederatedDataset - Columns []string - Rows []map[string]any - TotalCount int64 - HasNext bool - NextCursor string -} - -// FederatedStreamRequest describes a bounded-memory scan over the same -// authorized source union used by interactive row reads. -type FederatedStreamRequest struct { - Columns []string - Filters []Filter - Sort *Sort - AccessByProject map[string]SourceAccess -} - -func normalizedProjects(projects []string) []string { - seen := make(map[string]struct{}, len(projects)) - result := make([]string, 0, len(projects)) - for _, project := range projects { - project = strings.TrimSpace(project) - if project == "" { - continue - } - if _, ok := seen[project]; ok { - continue - } - seen[project] = struct{}{} - result = append(result, project) - } - sort.Strings(result) - return result -} - -func hasOutputResourceType(execution bundlepublication.BundleExecution, resourceType string) bool { - for _, output := range execution.Outputs { - if output.Name == resourceType { - return true - } - } - return false -} - -func reconcileFederatedColumns(sources []Materialization) ([]Column, error) { - if len(sources) == 0 { - return nil, fmt.Errorf("federation has no sources") - } - type parsedType struct { - base string - array, nullable bool - } - parse := func(value string) (parsedType, error) { - value = strings.TrimSpace(value) - result := parsedType{} - if strings.HasPrefix(value, "Nullable(") && strings.HasSuffix(value, ")") { - result.nullable = true - value = value[len("Nullable(") : len(value)-1] - } - if strings.HasPrefix(value, "Array(") && strings.HasSuffix(value, ")") { - result.array = true - value = value[len("Array(") : len(value)-1] - } - if value == "" || strings.HasPrefix(value, "Nullable(") || strings.HasPrefix(value, "Array(") { - return result, fmt.Errorf("invalid ClickHouse type %q", value) - } - result.base = value - return result, nil - } - all := map[string]parsedType{} - typeSources := map[string]map[string][]string{} - presenceCount := map[string]int{} - firstOrder := make([]string, 0) - firstIndex := map[string]int{} - for sourceIndex, source := range sources { - seen := map[string]struct{}{} - for _, column := range source.Columns { - if column.Name == "__loom_row_id" { - continue - } - if _, ok := seen[column.Name]; ok { - return nil, fmt.Errorf("duplicate column %q in source %q", column.Name, source.ID) - } - seen[column.Name] = struct{}{} - presenceCount[column.Name]++ - parsed, err := parse(column.ClickHouse) - if err != nil { - return nil, err - } - if sourceIndex == 0 { - firstIndex[column.Name] = len(firstOrder) - firstOrder = append(firstOrder, column.Name) - } - if previous, ok := all[column.Name]; ok { - if previous.base != parsed.base || previous.array != parsed.array { - projects := make([]string, 0) - types := make([]string, 0) - for typ, values := range typeSources[column.Name] { - types = append(types, typ) - projects = append(projects, values...) - } - types = append(types, column.ClickHouse) - projects = append(projects, source.Project) - sort.Strings(types) - sort.Strings(projects) - return nil, dataframeerrors.NewError(dataframeerrors.CodeFederationIncompatible, "", dataframeerrors.WithDetails(map[string]any{"column": column.Name, "projects": projects, "types": types})) - } - previous.nullable = previous.nullable || parsed.nullable - all[column.Name] = previous - } else { - all[column.Name] = parsed - } - if typeSources[column.Name] == nil { - typeSources[column.Name] = map[string][]string{} - } - typeSources[column.Name][column.ClickHouse] = append(typeSources[column.Name][column.ClickHouse], source.Project) - } - } - newNames := make([]string, 0) - // project_id is part of Loom's dataframe contract. Keep it available when - // reconciling legacy publications whose physical table predates the column. - all[projectIDColumn] = parsedType{base: "String"} - for name := range all { - if _, ok := firstIndex[name]; !ok { - newNames = append(newNames, name) - } - } - sort.Strings(newNames) - order := append(firstOrder, newNames...) - result := make([]Column, 0, len(order)) - for _, name := range order { - parsed := all[name] - if name != projectIDColumn && presenceCount[name] < len(sources) && !parsed.array { - parsed.nullable = true - } - typ := parsed.base - if parsed.array { - typ = "Array(" + typ + ")" - } - if parsed.nullable && !parsed.array { - typ = "Nullable(" + typ + ")" - } - result = append(result, Column{Name: name, ClickHouse: typ}) - } - return result, nil -} - -func nowPlusSecond() time.Time { - return time.Now().UTC().Add(time.Second) -} - -func validFederationClickHouseType(value string) bool { - value = strings.TrimSpace(value) - if strings.HasPrefix(value, "Nullable(") && strings.HasSuffix(value, ")") { - value = value[len("Nullable(") : len(value)-1] - } - if strings.HasPrefix(value, "Array(") && strings.HasSuffix(value, ")") { - value = value[len("Array(") : len(value)-1] - } - return value != "" && !strings.HasPrefix(value, "Nullable(") && !strings.HasPrefix(value, "Array(") -} - -// CurrentFederatedSources returns the exact READY pointer-backed publications -// without touching schema reconciliation. It is the authorization snapshot -// boundary used by GraphQL and export callers. -func (r *Reader) CurrentFederatedSources(ctx context.Context, projects []string, selector DataframeSelector) ([]Materialization, error) { - if r == nil || r.Catalog == nil { - return nil, fmt.Errorf("bundle catalog dependency is required") - } - projects = normalizedProjects(projects) - if len(projects) == 0 || !selector.Valid() { - return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } - executions, err := r.Catalog.ListExecutions(ctx, bundlepublication.BundlePublished, nowPlusSecond()) - if err != nil { - return nil, err - } - activeGenerations := map[string]string{} - releaseExecutions := map[string]string{} - if r.ReleaseExecutionResolver != nil { - releaseExecutions, err = r.ReleaseExecutionResolver.ActiveReleaseExecutionIDs(ctx, projects, selector) - if err != nil { - return nil, err - } - } - if r.ActiveManifestResolver != nil { - for _, project := range projects { - manifest, resolveErr := publication.ResolveActive(ctx, r.ActiveManifestResolver, project) - if errors.Is(resolveErr, publication.ErrNoActiveGeneration) { - continue - } - if resolveErr != nil { - return nil, resolveErr - } - activeGenerations[project] = manifest.Dataset.Generation - } - } - allowed := make(map[string]struct{}, len(projects)) - for _, project := range projects { - allowed[project] = struct{}{} - } - selected := map[string]bundlepublication.BundleExecution{} - for _, execution := range executions { - if !execution.State.Successful() { - continue - } - if _, ok := allowed[execution.Project]; !ok { - continue - } - releaseExecutionID, releaseControlled := releaseExecutions[execution.Project] - if releaseControlled { - if releaseExecutionID == "" || releaseExecutionID != execution.ID { - continue - } - } else if r.ActiveManifestResolver != nil { - generation, ok := activeGenerations[execution.Project] - if ok { - if execution.DatasetGeneration != generation { - continue - } - } else if execution.DatasetGeneration != "" { - continue - } - } - if !releaseControlled { - pointer, pointerErr := r.Catalog.GetPointer(ctx, execution.PointerName()) - if pointerErr != nil { - return nil, fmt.Errorf("resolve dataframe pointer: %w", pointerErr) - } - if pointer.ExecutionID != execution.ID { - continue - } - } - // Output absence is a normal selector miss, not a catalog failure. In - // particular, a newer recipe version may replace Patient with - // ResearchSubject while both executions remain in publication history. - // Do not ask version-aware catalogs to resolve metadata for an output the - // execution never published. - if !hasOutputResourceType(execution, selector.Output) { - continue - } - executionSelector, selectorErr := r.selectorForExecution(ctx, execution, selector.Output) - if selectorErr != nil { - // A stale active-release pointer must degrade that project, not make - // every other authorized project's valid publication unreadable. - // Status resolution still reports the missing project to callers. - if errors.Is(selectorErr, bundlepublication.ErrBundleNotFound) { - continue - } - return nil, selectorErr - } - if executionSelector != selector { - continue - } - if current, ok := selected[execution.Project]; ok && current.ID != execution.ID { - return nil, dataframeerrors.NewError(dataframeerrors.CodeFederationIncompatible, "", dataframeerrors.WithDetails(map[string]any{"project": execution.Project, "selector": selector.Key(), "executions": []string{current.ID, execution.ID}})) - } - selected[execution.Project] = execution - } - result := make([]Materialization, 0, len(selected)) - for _, project := range projects { - execution, ok := selected[project] - if !ok { - continue - } - for _, output := range execution.Outputs { - if output.Name == selector.Output { - materialization := publishedMaterialization(execution, output, selector.Output) - materialization.Selector = selector - result = append(result, materialization) - break - } - } - } - if len(result) == 0 { - return []Materialization{}, nil - } - sort.Slice(result, func(i, j int) bool { - if result[i].Project != result[j].Project { - return result[i].Project < result[j].Project - } - return result[i].ID < result[j].ID - }) - return result, nil -} - -func (r *Reader) selectorForExecution(ctx context.Context, execution bundlepublication.BundleExecution, output string) (DataframeSelector, error) { - if resolver, ok := r.Catalog.(ExecutionSelectorResolver); ok { - selector, err := resolver.DataframeSelectorForExecution(ctx, execution.ID, output) - if err != nil { - return DataframeSelector{}, err - } - if selector.Valid() { - return selector, nil - } - return DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "") - } - selector := DataframeSelector{Recipe: execution.Name, TranslationVersion: strings.TrimSpace(execution.TranslationVersion), Output: output} - if !selector.Valid() { - return DataframeSelector{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidSelector, "") - } - return selector, nil -} - -func (r *Reader) CurrentFederatedResourceTypes(ctx context.Context, projects []string) ([]string, error) { - selectors, err := r.CurrentFederatedSelectors(ctx, projects) - if err != nil { - return nil, err - } - resourceTypes := map[string]struct{}{} - for _, selector := range selectors { - if fhirschema.HasResource(selector.Output) { - resourceTypes[selector.Output] = struct{}{} - } - } - result := make([]string, 0, len(resourceTypes)) - for resourceType := range resourceTypes { - result = append(result, resourceType) - } - sort.Strings(result) - return result, nil -} - -// CurrentFederatedSelectors discovers pointer-backed datasets by their full -// identity. Output-name-only discovery is intentionally not used here. -func (r *Reader) CurrentFederatedSelectors(ctx context.Context, projects []string) ([]DataframeSelector, error) { - if r == nil || r.Catalog == nil { - return nil, fmt.Errorf("bundle catalog dependency is required") - } - projects = normalizedProjects(projects) - allowed := make(map[string]struct{}, len(projects)) - for _, project := range projects { - allowed[project] = struct{}{} - } - activeGenerations := map[string]string{} - controlledProjects := map[string]bool{} - selectors := map[string]DataframeSelector{} - if r.ReleaseExecutionResolver != nil { - releaseSelectors, controlled, releaseErr := r.ReleaseExecutionResolver.ActiveReleaseSelectors(ctx, projects) - if releaseErr != nil { - return nil, releaseErr - } - controlledProjects = controlled - for _, selector := range releaseSelectors { - selectors[selector.Key()] = selector - } - } - if r.ActiveManifestResolver != nil { - for _, project := range projects { - manifest, resolveErr := publication.ResolveActive(ctx, r.ActiveManifestResolver, project) - if errors.Is(resolveErr, publication.ErrNoActiveGeneration) { - continue - } - if resolveErr != nil { - return nil, resolveErr - } - activeGenerations[project] = manifest.Dataset.Generation - } - } - executions, err := r.Catalog.ListExecutions(ctx, bundlepublication.BundlePublished, nowPlusSecond()) - if err != nil { - return nil, err - } - for _, execution := range executions { - if _, ok := allowed[execution.Project]; !ok { - continue - } - if controlledProjects[execution.Project] { - continue - } - if r.ActiveManifestResolver != nil { - generation, ok := activeGenerations[execution.Project] - if (ok && execution.DatasetGeneration != generation) || (!ok && execution.DatasetGeneration != "") { - continue - } - } - pointer, pointerErr := r.Catalog.GetPointer(ctx, execution.PointerName()) - if pointerErr != nil { - return nil, pointerErr - } - if pointer.ExecutionID != execution.ID { - continue - } - for _, output := range execution.Outputs { - selector, selectorErr := r.selectorForExecution(ctx, execution, output.Name) - if selectorErr != nil { - return nil, selectorErr - } - if !selector.Valid() { - continue - } - selectors[selector.Key()] = selector - } - } - result := make([]DataframeSelector, 0, len(selectors)) - for _, selector := range selectors { - result = append(result, selector) - } - sort.Slice(result, func(i, j int) bool { return result[i].Key() < result[j].Key() }) - return result, nil -} - -// CurrentProjectDatasets returns the current output materializations for one -// project without merging them with other projects. -func (r *Reader) CurrentProjectDatasets(ctx context.Context, project string) ([]Materialization, error) { - project = strings.TrimSpace(project) - if project == "" { - return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } - selectors, err := r.CurrentFederatedSelectors(ctx, []string{project}) - if err != nil { - return nil, err - } - result := make([]Materialization, 0, len(selectors)) - for _, selector := range selectors { - sources, sourceErr := r.CurrentFederatedSources(ctx, []string{project}, selector) - if sourceErr != nil { - return nil, sourceErr - } - result = append(result, sources...) - } - sort.Slice(result, func(i, j int) bool { - if result[i].Name != result[j].Name { - return result[i].Name < result[j].Name - } - return result[i].ID < result[j].ID - }) - if len(result) == 0 { - return nil, dataframeerrors.NewError(dataframeerrors.CodeDatasetNotFound, "") - } - return result, nil -} - -func ReconcileFederatedDataset(selector DataframeSelector, expectedProjects []string, sources []Materialization) (FederatedDataset, error) { - if len(sources) == 0 { - statuses := make([]ProjectStatus, 0, len(expectedProjects)) - for _, project := range expectedProjects { - statuses = append(statuses, ProjectStatus{ProjectID: project, State: ProjectMissing}) - } - return FederatedDataset{Selector: selector, Name: selector.Output, Availability: FederationUnavailable, ExpectedProjects: len(expectedProjects), ProjectStatuses: statuses}, nil - } - sort.Slice(sources, func(i, j int) bool { - if sources[i].Project != sources[j].Project { - return sources[i].Project < sources[j].Project - } - return sources[i].ID < sources[j].ID - }) - validSources := make([]Materialization, 0, len(sources)) - excluded := map[string]string{} - for _, source := range sources { - seen := map[string]struct{}{} - valid := strings.TrimSpace(source.PhysicalTable) != "" - for _, column := range source.Columns { - if strings.TrimSpace(column.Name) == "" || !validFederationClickHouseType(column.ClickHouse) { - valid = false - break - } - if _, ok := seen[column.Name]; ok { - valid = false - break - } - seen[column.Name] = struct{}{} - } - if !valid { - excluded[source.Project] = string(dataframeerrors.CodeRecipeContractViolation) - continue - } - validSources = append(validSources, source) - } - if len(validSources) == 0 { - statuses := make([]ProjectStatus, 0, len(expectedProjects)) - for _, project := range expectedProjects { - statuses = append(statuses, ProjectStatus{ProjectID: project, State: ProjectExcluded, ErrorCode: excluded[project]}) - } - return FederatedDataset{Selector: selector, Name: selector.Output, Availability: FederationUnavailable, ExpectedProjects: len(expectedProjects), ProjectStatuses: statuses}, nil - } - sources = validSources - columns, err := reconcileFederatedColumns(sources) - if err != nil { - if _, ok := dataframeerrors.AsUserError(err); ok { - return FederatedDataset{}, err - } - return FederatedDataset{}, dataframeerrors.Wrap(err, dataframeerrors.CodeFederationIncompatible, "") - } - parts := make([]string, 0, len(sources)) - var rowCount int64 - for _, source := range sources { - rowCount += source.RowCount - parts = append(parts, source.ID, source.DatasetGeneration, source.PhysicalTable) - } - for _, column := range columns { - parts = append(parts, column.Name, column.ClickHouse) - } - sort.Strings(parts) - digest := sha256.Sum256([]byte(strings.Join(parts, "\x00"))) - statuses := make([]ProjectStatus, 0, len(expectedProjects)) - byProject := make(map[string]Materialization, len(sources)) - for _, source := range sources { - byProject[source.Project] = source - } - for _, project := range expectedProjects { - status := ProjectStatus{ProjectID: project, State: ProjectMissing} - if code, ok := excluded[project]; ok { - status.State, status.ErrorCode = ProjectExcluded, code - } - if source, ok := byProject[project]; ok { - status.State, status.Generation, status.ExecutionID, status.CreatedAt, status.UpdatedAt = ProjectCurrent, source.DatasetGeneration, source.ID, source.CreatedAt, source.UpdatedAt - if status.UpdatedAt.IsZero() && source.ReadyAt != nil { - status.UpdatedAt = *source.ReadyAt - } - } - statuses = append(statuses, status) - } - availability := FederationAvailable - if len(sources) < len(expectedProjects) { - availability = FederationDegraded - } - return FederatedDataset{Selector: selector, Name: selector.Output, Revision: hex.EncodeToString(digest[:]), Sources: append([]Materialization(nil), sources...), Columns: columns, RowCount: rowCount, Availability: availability, ExpectedProjects: len(expectedProjects), ProjectStatuses: statuses}, nil -} - -// PublishedProjects returns project identities that have a current READY -// generation. It is used only as a candidate set when the authenticator does -// not embed project claims; row/source authorization still happens per source. -func (r *Reader) PublishedProjects(ctx context.Context) ([]string, error) { - if r == nil || r.Catalog == nil { - return nil, fmt.Errorf("bundle catalog dependency is required") - } - executions, err := r.Catalog.ListExecutions(ctx, bundlepublication.BundlePublished, nowPlusSecond()) - if err != nil { - return nil, err - } - projects := make(map[string]struct{}) - for _, execution := range executions { - if !execution.State.Successful() || execution.Project == "" { - continue - } - pointer, pointerErr := r.Catalog.GetPointer(ctx, execution.PointerName()) - if pointerErr != nil { - return nil, fmt.Errorf("resolve dataframe pointer: %w", pointerErr) - } - if pointer.ExecutionID == execution.ID { - projects[execution.Project] = struct{}{} - } - } - result := make([]string, 0, len(projects)) - for project := range projects { - result = append(result, project) - } - sort.Strings(result) - return result, nil -} diff --git a/internal/dataframe/published/federation_query.go b/internal/dataframe/published/federation_query.go deleted file mode 100644 index dcdccd28..00000000 --- a/internal/dataframe/published/federation_query.go +++ /dev/null @@ -1,351 +0,0 @@ -package published - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "strings" - - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" -) - -func (r *Reader) PageFederatedDataset(ctx context.Context, dataset FederatedDataset, req FederatedPageRequest) (FederatedPage, error) { - if r == nil || r.ClickHouse == nil { - return FederatedPage{}, dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - } - columns, allowed, err := federatedColumns(dataset, req.Columns, req.Sort) - if err != nil { - return FederatedPage{}, err - } - first := req.First - if first <= 0 { - first = 100 - } - if r.MaxPage > 0 && first > r.MaxPage { - first = r.MaxPage - } - cursor, err := decodeCursor(req.After) - if err != nil { - return FederatedPage{}, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidCursor, "") - } - unionColumns := append([]string(nil), columns...) - if req.Sort != nil && !contains(unionColumns, req.Sort.Column) { - unionColumns = append(unionColumns, req.Sort.Column) - } - for _, filter := range req.Filters { - if !contains(unionColumns, filter.Column) { - unionColumns = append(unionColumns, filter.Column) - } - } - union, unionArgs, err := federatedNormalizedUnion(dataset, unionColumns, req.AccessByProject) - if err != nil { - return FederatedPage{}, err - } - where, whereArgs, err := buildWhere(req.Filters, allowed) - if err != nil { - return FederatedPage{}, err - } - queryColumns := append([]string(nil), unionColumns...) - queryColumns = append(queryColumns, "__loom_row_id", "__loom_global_row_id") - selectColumns := quotedColumns(queryColumns) + ", count() OVER() AS `__loom_total`" - readColumns := append(append([]string(nil), queryColumns...), "__loom_total") - counted := fmt.Sprintf("SELECT %s FROM (%s) AS __loom_federated", selectColumns, union) - queryArgs := append([]any(nil), unionArgs...) - queryArgs = append(queryArgs, whereArgs...) - if len(where) > 0 { - counted += " WHERE " + strings.Join(where, " AND ") - } - query := fmt.Sprintf("SELECT %s FROM (%s) AS __loom_counted", quotedColumns(readColumns), counted) - if cursor != nil { - cursorWhere, cursorArgs, err := federatedCursorPredicate(cursor, req.Sort) - if err != nil { - return FederatedPage{}, err - } - query += " WHERE " + cursorWhere - queryArgs = append(queryArgs, cursorArgs...) - } - if req.Sort != nil { - direction := "ASC" - if req.Sort.Desc { - direction = "DESC" - } - query += fmt.Sprintf(" ORDER BY `%s` %s, `__loom_global_row_id` ASC", req.Sort.Column, direction) - } else { - query += " ORDER BY `__loom_global_row_id` ASC" - } - query += fmt.Sprintf(" LIMIT %d", first+1) - rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, readColumns, queryArgs...) - if err != nil { - return FederatedPage{}, backendCallError(err) - } - var count int64 - if len(rows) > 0 { - count, err = numericCount(rows[0]["__loom_total"]) - if err != nil { - return FederatedPage{}, err - } - } else { - // A window has no carrier row for an empty page (including an after - // cursor beyond the end), so only that case needs the count fallback. - count, err = r.federatedCountDataset(ctx, dataset, allowed, req) - if err != nil { - return FederatedPage{}, err - } - } - hasNext := len(rows) > first - if hasNext { - rows = rows[:first] - } - next := "" - if hasNext && len(rows) > 0 { - last := rows[len(rows)-1] - rowID := fmt.Sprint(last["__loom_global_row_id"]) - var sortValue any - if req.Sort != nil { - sortValue = last[req.Sort.Column] - if sortValue == nil { - return FederatedPage{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidCursor, "") - } - } - next = encodeCursor(rowID, sortValue) - } - for _, row := range rows { - delete(row, "__loom_total") - delete(row, "__loom_row_id") - delete(row, "__loom_global_row_id") - for _, column := range unionColumns { - if !contains(columns, column) { - delete(row, column) - } - } - } - return FederatedPage{Dataset: dataset, Columns: columns, Rows: rows, TotalCount: count, HasNext: hasNext, NextCursor: next}, nil -} - -func (r *Reader) StreamFederatedDataset(ctx context.Context, dataset FederatedDataset, req FederatedStreamRequest, visit func(map[string]any) error) (FederatedDataset, error) { - if r == nil || r.ClickHouse == nil { - return FederatedDataset{}, dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - } - if visit == nil { - return FederatedDataset{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - columns, allowed, err := federatedColumns(dataset, req.Columns, req.Sort) - if err != nil { - return FederatedDataset{}, err - } - unionColumns := append([]string(nil), columns...) - if req.Sort != nil && !contains(unionColumns, req.Sort.Column) { - unionColumns = append(unionColumns, req.Sort.Column) - } - for _, filter := range req.Filters { - if !contains(unionColumns, filter.Column) { - unionColumns = append(unionColumns, filter.Column) - } - } - union, args, err := federatedNormalizedUnion(dataset, unionColumns, req.AccessByProject) - if err != nil { - return FederatedDataset{}, err - } - where, whereArgs, err := buildWhere(req.Filters, allowed) - if err != nil { - return FederatedDataset{}, err - } - args = append(args, whereArgs...) - queryColumns := append([]string(nil), unionColumns...) - queryColumns = append(queryColumns, "__loom_row_id", "__loom_global_row_id") - query := fmt.Sprintf("SELECT %s FROM (%s) AS __loom_federated", quotedColumns(queryColumns), union) - if len(where) > 0 { - query += " WHERE " + strings.Join(where, " AND ") - } - if req.Sort != nil { - direction := "ASC" - if req.Sort.Desc { - direction = "DESC" - } - query += fmt.Sprintf(" ORDER BY `%s` %s, `__loom_global_row_id` ASC", req.Sort.Column, direction) - } else { - query += " ORDER BY `__loom_global_row_id` ASC" - } - err = r.ClickHouse.QueryRowsArgsVisit(ctx, query, queryColumns, func(row map[string]any) error { - delete(row, "__loom_row_id") - delete(row, "__loom_global_row_id") - for _, column := range unionColumns { - if !contains(columns, column) { - delete(row, column) - } - } - return visit(row) - }, args...) - if err != nil { - return FederatedDataset{}, backendCallError(err) - } - return dataset, nil -} - -func (r *Reader) federatedCountDataset(ctx context.Context, dataset FederatedDataset, allowed map[string]struct{}, req FederatedPageRequest) (int64, error) { - union, args, err := federatedNormalizedUnion(dataset, datasetVisibleColumns(dataset), req.AccessByProject) - if err != nil { - return 0, err - } - where, whereArgs, err := buildWhere(req.Filters, allowed) - if err != nil { - return 0, err - } - args = append(args, whereArgs...) - query := fmt.Sprintf("SELECT count() AS `__loom_total` FROM (%s) AS __loom_federated", union) - if len(where) > 0 { - query += " WHERE " + strings.Join(where, " AND ") - } - rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, []string{"__loom_total"}, args...) - if err != nil { - return 0, backendCallError(err) - } - var total int64 - for _, row := range rows { - value, err := numericCount(row["__loom_total"]) - if err != nil { - return 0, err - } - total += value - } - return total, nil -} - -func datasetVisibleColumns(dataset FederatedDataset) []string { - result := make([]string, 0, len(dataset.Columns)) - for _, column := range dataset.Columns { - if column.Name != authResourcePathColumn && column.Name != "__loom_row_id" { - result = append(result, column.Name) - } - } - return result -} - -func federatedColumns(dataset FederatedDataset, requested []string, sort *Sort) ([]string, map[string]struct{}, error) { - for _, column := range requested { - if column == authResourcePathColumn { - return nil, nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - } - allowed := make(map[string]struct{}, len(dataset.Columns)) - columns := append([]string(nil), requested...) - for _, column := range dataset.Columns { - if column.Name == "__loom_row_id" { - continue - } - allowed[column.Name] = struct{}{} - if len(requested) == 0 { - if column.Name != authResourcePathColumn { - columns = append(columns, column.Name) - } - } - } - if err := validateReaderColumns(columns, allowed); err != nil { - return nil, nil, err - } - if sort != nil { - if sort.Column == authResourcePathColumn { - return nil, nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - if err := validateReaderColumns([]string{sort.Column}, allowed); err != nil { - return nil, nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidRequest, "") - } - } - return columns, allowed, nil -} - -// federatedNormalizedUnion creates the single typed source union used by rows, -// counts and aggregates. User predicates are deliberately applied by callers -// around this union; only per-source authorization remains inside branches. -func federatedNormalizedUnion(dataset FederatedDataset, columns []string, accessByProject map[string]SourceAccess) (string, []any, error) { - branches := make([]string, 0, len(dataset.Sources)) - args := make([]any, 0) - for _, source := range dataset.Sources { - present := make(map[string]Column, len(source.Columns)) - for _, column := range source.Columns { - present[column.Name] = column - } - selects := make([]string, 0, len(columns)+2) - for _, column := range columns { - target, ok := findColumn(dataset.Columns, column) - if !ok { - return "", nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - if column == projectIDColumn { - // The catalog project is authoritative, including for legacy tables - // without a physical project_id column. - selects = append(selects, fmt.Sprintf("CAST(? AS %s) AS `project_id`", target.ClickHouse)) - args = append(args, source.Project) - } else if sourceColumn, exists := present[column]; exists { - selects = append(selects, fmt.Sprintf("CAST(`%s` AS %s) AS `%s`", sourceColumn.Name, target.ClickHouse, column)) - } else if strings.HasPrefix(target.ClickHouse, "Array(") { - selects = append(selects, fmt.Sprintf("CAST([] AS %s) AS `%s`", target.ClickHouse, column)) - } else { - typ := target.ClickHouse - if !strings.HasPrefix(typ, "Nullable(") { - typ = "Nullable(" + typ + ")" - } - selects = append(selects, fmt.Sprintf("CAST(NULL AS %s) AS `%s`", typ, column)) - } - } - selects = append(selects, "toString(`__loom_row_id`) AS `__loom_row_id`", "concat(?, ':', toString(`__loom_row_id`)) AS `__loom_global_row_id`") - branch := "SELECT " + strings.Join(selects, ", ") + " FROM `" + source.PhysicalTable + "`" - access := accessByProject[source.Project] - if !access.Unrestricted { - if _, ok := present[authResourcePathColumn]; ok { - branch += " WHERE `auth_resource_path` IN ?" - args = append(args, source.ID, access.ResourcePaths) - } else { - branch += " WHERE 0" - args = append(args, source.ID) - } - } else { - args = append(args, source.ID) - } - branches = append(branches, branch) - } - return strings.Join(branches, " UNION ALL "), args, nil -} - -func validateReaderColumns(columns []string, allowed map[string]struct{}) error { - for _, column := range columns { - if column == "__loom_row_id" || column == "__loom_global_row_id" { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, fmt.Sprintf("column %q is internal to dataframe pagination", column)) - } - if _, ok := allowed[column]; !ok { - return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, fmt.Sprintf("column %q is not in federated dataset schema", column)) - } - } - return nil -} - -func quotedColumns(columns []string) string { - quoted := make([]string, len(columns)) - for index, column := range columns { - quoted[index] = fmt.Sprintf("`%s`", column) - } - return strings.Join(quoted, ", ") -} - -func shortQueryID(query string) string { - digest := sha256.Sum256([]byte(query)) - return hex.EncodeToString(digest[:8]) -} - -func federatedCursorPredicate(cursor *pageCursor, sort *Sort) (string, []any, error) { - if cursor == nil || cursor.RowID == "" { - return "", nil, fmt.Errorf("invalid federated cursor") - } - if sort == nil { - return "`__loom_global_row_id` > ?", []any{cursor.RowID}, nil - } - if cursor.SortValue == nil { - return "", nil, fmt.Errorf("federated keyset cursor is missing sort value") - } - op := ">" - if sort.Desc { - op = "<" - } - return fmt.Sprintf("(`%s` %s ? OR (`%s` = ? AND `__loom_global_row_id` > ?))", sort.Column, op, sort.Column), []any{cursor.SortValue, cursor.SortValue, cursor.RowID}, nil -} diff --git a/internal/dataframe/published/federation_test.go b/internal/dataframe/published/federation_test.go deleted file mode 100644 index 7b7aab9f..00000000 --- a/internal/dataframe/published/federation_test.go +++ /dev/null @@ -1,283 +0,0 @@ -package published - -import ( - "context" - "errors" - "strings" - "testing" - "time" - - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - bundlepublication "github.com/calypr/loom/internal/dataframe/publication" - publication "github.com/calypr/loom/internal/dataset" -) - -func TestReconcileFederatedDatasetAllowsSparseProjectColumns(t *testing.T) { - selector := DataframeSelector{Recipe: "documents", TranslationVersion: "v1", Output: "DocumentReference"} - dataset, err := ReconcileFederatedDataset(selector, []string{"a", "b"}, []Materialization{ - {ID: "a", Project: "a", PhysicalTable: "table_a", Columns: []Column{{Name: "id", ClickHouse: "String"}, {Name: "only_a", ClickHouse: "String"}}}, - {ID: "b", Project: "b", PhysicalTable: "table_b", Columns: []Column{{Name: "id", ClickHouse: "String"}}}, - }) - if err != nil { - t.Fatal(err) - } - column, ok := findColumn(dataset.Columns, "only_a") - if !ok || column.ClickHouse != "Nullable(String)" { - t.Fatalf("sparse column = %#v", column) - } - if dataset.Availability != FederationAvailable { - t.Fatalf("availability = %s", dataset.Availability) - } -} - -func TestReconcileFederatedDatasetReportsCardinalityCollision(t *testing.T) { - selector := DataframeSelector{Recipe: "documents", TranslationVersion: "v1", Output: "DocumentReference"} - _, err := ReconcileFederatedDataset(selector, []string{"a", "b"}, []Materialization{ - {ID: "a", Project: "a", PhysicalTable: "table_a", Columns: []Column{{Name: "code", ClickHouse: "String"}}}, - {ID: "b", Project: "b", PhysicalTable: "table_b", Columns: []Column{{Name: "code", ClickHouse: "Array(String)"}}}, - }) - var userErr dataframeerrors.UserError - if !errors.As(err, &userErr) || userErr.Code() != string(dataframeerrors.CodeFederationIncompatible) { - t.Fatalf("error = %v", err) - } - details := userErr.Details() - if details["column"] != "code" { - t.Fatalf("details = %#v", details) - } -} - -func TestReconcileExcludesMalformedSourceAndDegrades(t *testing.T) { - selector := DataframeSelector{Recipe: "documents", TranslationVersion: "v1", Output: "DocumentReference"} - dataset, err := ReconcileFederatedDataset(selector, []string{"a", "b"}, []Materialization{ - {ID: "a", Project: "a", PhysicalTable: "table_a", Columns: []Column{{Name: "id", ClickHouse: "String"}}}, - {ID: "b", Project: "b", PhysicalTable: "", Columns: []Column{{Name: "id", ClickHouse: "String"}}}, - }) - if err != nil { - t.Fatal(err) - } - if dataset.Availability != FederationDegraded || len(dataset.Sources) != 1 { - t.Fatalf("dataset = %#v", dataset) - } - if dataset.ProjectStatuses[1].State != ProjectExcluded { - t.Fatalf("statuses = %#v", dataset.ProjectStatuses) - } -} - -func TestExactSelectorDoesNotMixRecipesSharingOutput(t *testing.T) { - now := time.Now().UTC() - executions := []bundlepublication.BundleExecution{ - {ID: "recipe-a", BundleIdentity: bundlepublication.BundleIdentity{Name: "recipe-a", TranslationVersion: "v1", Project: "p"}, State: bundlepublication.BundleReady, UpdatedAt: now, Outputs: []bundlepublication.BundleOutputRecord{{Name: "DocumentReference", PhysicalTable: "a"}}}, - {ID: "recipe-b", BundleIdentity: bundlepublication.BundleIdentity{Name: "recipe-b", TranslationVersion: "v1", Project: "p"}, State: bundlepublication.BundleReady, UpdatedAt: now, Outputs: []bundlepublication.BundleOutputRecord{{Name: "DocumentReference", PhysicalTable: "b"}}}, - } - pointers := map[string]bundlepublication.BundlePointer{} - for _, execution := range executions { - pointers[execution.PointerName()] = bundlepublication.BundlePointer{ExecutionID: execution.ID} - } - reader := Reader{Catalog: &federationCatalog{executions: executions, pointers: pointers}} - sources, err := reader.CurrentFederatedSources(context.Background(), []string{"p"}, DataframeSelector{Recipe: "recipe-a", TranslationVersion: "v1", Output: "DocumentReference"}) - if err != nil { - t.Fatal(err) - } - if len(sources) != 1 || sources[0].PhysicalTable != "a" { - t.Fatalf("sources = %#v", sources) - } -} - -func TestRecipeVersionsRemainIndependentlyQueryable(t *testing.T) { - now := time.Now().UTC() - executions := []bundlepublication.BundleExecution{ - {ID: "v1", BundleIdentity: bundlepublication.BundleIdentity{Name: "documents", Project: "p", DatasetGeneration: "g1"}, State: bundlepublication.BundleReady, UpdatedAt: now, Outputs: []bundlepublication.BundleOutputRecord{{Name: "DocumentReference", PhysicalTable: "v1_table"}}}, - {ID: "v2", BundleIdentity: bundlepublication.BundleIdentity{Name: "documents", Project: "p", DatasetGeneration: "g2"}, State: bundlepublication.BundleReady, UpdatedAt: now, Outputs: []bundlepublication.BundleOutputRecord{{Name: "DocumentReference", PhysicalTable: "v2_table"}}}, - } - pointers := map[string]bundlepublication.BundlePointer{} - for _, execution := range executions { - pointers[execution.PointerName()] = bundlepublication.BundlePointer{ExecutionID: execution.ID} - } - catalog := &versionedFederationCatalog{federationCatalog: &federationCatalog{executions: executions, pointers: pointers}, selectors: map[string]DataframeSelector{ - "v1": {Recipe: "documents", TranslationVersion: "v1"}, "v2": {Recipe: "documents", TranslationVersion: "v2"}, - }} - reader := Reader{Catalog: catalog} - for _, version := range []string{"v1", "v2"} { - sources, err := reader.CurrentFederatedSources(context.Background(), []string{"p"}, DataframeSelector{Recipe: "documents", TranslationVersion: version, Output: "DocumentReference"}) - if err != nil || len(sources) != 1 || sources[0].PhysicalTable != version+"_table" { - t.Fatalf("version %s sources = %#v, %v", version, sources, err) - } - } -} - -func TestVersionedExecutionWithoutRequestedOutputIsSkipped(t *testing.T) { - now := time.Now().UTC() - execution := bundlepublication.BundleExecution{ - ID: "research-subject", BundleIdentity: bundlepublication.BundleIdentity{ - Name: "calypr-meta-default", TranslationVersion: "v2", Project: "p", DatasetGeneration: "g", - }, - State: bundlepublication.BundlePublished, UpdatedAt: now, - Outputs: []bundlepublication.BundleOutputRecord{{Name: "ResearchSubject", PhysicalTable: "research_subject"}}, - } - catalog := &missingOutputSelectorCatalog{federationCatalog: &federationCatalog{ - executions: []bundlepublication.BundleExecution{execution}, - pointers: map[string]bundlepublication.BundlePointer{execution.PointerName(): {ExecutionID: execution.ID}}, - }} - reader := Reader{Catalog: catalog} - - sources, err := reader.CurrentFederatedSources(context.Background(), []string{"p"}, DataframeSelector{ - Recipe: "calypr-meta-default", TranslationVersion: "v2", Output: "Patient", - }) - if err != nil { - t.Fatalf("CurrentFederatedSources() error = %v", err) - } - if len(sources) != 0 { - t.Fatalf("sources = %#v, want no Patient publication", sources) - } - if catalog.selectorCalls != 0 { - t.Fatalf("selector resolver called %d times for absent output", catalog.selectorCalls) - } -} - -func TestFederatedUnionSynthesizesProjectIDForLegacySource(t *testing.T) { - dataset := FederatedDataset{ - Columns: []Column{{Name: "id", ClickHouse: "String"}, {Name: projectIDColumn, ClickHouse: "String"}}, - Sources: []Materialization{{ID: "execution:Patient", Project: "HTAN_INT-BForePC", PhysicalTable: "loom_patient", Columns: []Column{{Name: "id", ClickHouse: "String"}}}}, - } - query, args, err := federatedNormalizedUnion(dataset, []string{projectIDColumn}, map[string]SourceAccess{"HTAN_INT-BForePC": {Unrestricted: true}}) - if err != nil { - t.Fatalf("federatedNormalizedUnion() error = %v", err) - } - if !strings.Contains(query, "CAST(? AS String) AS `project_id`") { - t.Fatalf("query does not synthesize project_id: %s", query) - } - if len(args) != 2 || args[0] != "HTAN_INT-BForePC" || args[1] != "execution:Patient" { - t.Fatalf("args = %#v, want project then source id", args) - } -} - -func TestFederatedColumnsRejectsUnknownTableColumnAsInvalidRequest(t *testing.T) { - _, _, err := federatedColumns(FederatedDataset{ - Columns: []Column{{Name: "patient_id", ClickHouse: "String"}}, - }, []string{"stale_column"}, nil) - userErr, ok := dataframeerrors.AsUserError(err) - if !ok { - t.Fatalf("error = %v, want typed user error", err) - } - if userErr.Code() != string(dataframeerrors.CodeInvalidRequest) { - t.Fatalf("error code = %q, want INVALID_REQUEST", userErr.Code()) - } - if userErr.Retryable() { - t.Fatal("unknown table column must not be retryable") - } -} - -type federationCatalog struct { - bundlepublication.BundleCatalog - executions []bundlepublication.BundleExecution - pointers map[string]bundlepublication.BundlePointer -} - -type versionedFederationCatalog struct { - *federationCatalog - selectors map[string]DataframeSelector -} - -type missingOutputSelectorCatalog struct { - *federationCatalog - selectorCalls int -} - -type staleExecutionSelectorCatalog struct { - *federationCatalog - selectors map[string]DataframeSelector - staleID string -} - -func (c *staleExecutionSelectorCatalog) DataframeSelectorForExecution(_ context.Context, executionID, output string) (DataframeSelector, error) { - if executionID == c.staleID { - return DataframeSelector{}, bundlepublication.ErrBundleNotFound - } - selector := c.selectors[executionID] - selector.Output = output - return selector, nil -} - -func (c *missingOutputSelectorCatalog) DataframeSelectorForExecution(context.Context, string, string) (DataframeSelector, error) { - c.selectorCalls++ - return DataframeSelector{}, bundlepublication.ErrBundleNotFound -} - -type releaseExecutionFixture map[string]string - -func (f releaseExecutionFixture) ActiveReleaseExecutionIDs(context.Context, []string, DataframeSelector) (map[string]string, error) { - result := make(map[string]string, len(f)) - for project, executionID := range f { - result[project] = executionID - } - return result, nil -} - -func (f releaseExecutionFixture) ActiveReleaseSelectors(context.Context, []string) ([]DataframeSelector, map[string]bool, error) { - controlled := make(map[string]bool, len(f)) - for project := range f { - controlled[project] = true - } - return nil, controlled, nil -} - -func (c *versionedFederationCatalog) DataframeSelectorForExecution(_ context.Context, executionID, output string) (DataframeSelector, error) { - selector := c.selectors[executionID] - selector.Output = output - return selector, nil -} - -func TestActiveReleaseExecutionOverridesMutablePublicationPointer(t *testing.T) { - now := time.Now().UTC() - selector := DataframeSelector{Recipe: "documents", TranslationVersion: "v1", Output: "DocumentReference"} - selected := bundlepublication.BundleExecution{ID: "release-selected", BundleIdentity: bundlepublication.BundleIdentity{Name: selector.Recipe, TranslationVersion: selector.TranslationVersion, Project: "p", DatasetGeneration: "old"}, State: bundlepublication.BundlePublished, UpdatedAt: now, Outputs: []bundlepublication.BundleOutputRecord{{Name: selector.Output, PhysicalTable: "stable", State: bundlepublication.BundlePublished}}} - newer := bundlepublication.BundleExecution{ID: "pointer-newer", BundleIdentity: bundlepublication.BundleIdentity{Name: selector.Recipe, TranslationVersion: selector.TranslationVersion, Project: "p", DatasetGeneration: "new"}, State: bundlepublication.BundlePublished, UpdatedAt: now.Add(time.Second), Outputs: []bundlepublication.BundleOutputRecord{{Name: selector.Output, PhysicalTable: "replacement", State: bundlepublication.BundlePublished}}} - catalog := &federationCatalog{executions: []bundlepublication.BundleExecution{selected, newer}, pointers: map[string]bundlepublication.BundlePointer{ - selected.PointerName(): {ExecutionID: newer.ID}, newer.PointerName(): {ExecutionID: newer.ID}, - }} - reader := Reader{Catalog: catalog, ActiveManifestResolver: noActiveFederationResolver{}, ReleaseExecutionResolver: releaseExecutionFixture{"p": selected.ID}} - sources, err := reader.CurrentFederatedSources(context.Background(), []string{"p"}, selector) - if err != nil { - t.Fatal(err) - } - if len(sources) != 1 || sources[0].ID != selected.ID+":"+selector.Output || sources[0].PhysicalTable != "stable" { - t.Fatalf("release-selected sources = %#v", sources) - } -} - -func TestStaleReleaseExecutionDoesNotPoisonValidFederatedSource(t *testing.T) { - now := time.Now().UTC() - selector := DataframeSelector{Recipe: "explorer_project_default", TranslationVersion: "repository-commit", Output: "Patient"} - valid := bundlepublication.BundleExecution{ID: "valid", BundleIdentity: bundlepublication.BundleIdentity{Name: selector.Recipe, TranslationVersion: selector.TranslationVersion, Project: "valid-project", DatasetGeneration: "g1"}, State: bundlepublication.BundlePublished, UpdatedAt: now, Outputs: []bundlepublication.BundleOutputRecord{{Name: selector.Output, PhysicalTable: "patient_valid", State: bundlepublication.BundlePublished}}} - stale := bundlepublication.BundleExecution{ID: "stale", BundleIdentity: bundlepublication.BundleIdentity{Name: selector.Recipe, TranslationVersion: selector.TranslationVersion, Project: "stale-project", DatasetGeneration: "g2"}, State: bundlepublication.BundlePublished, UpdatedAt: now, Outputs: []bundlepublication.BundleOutputRecord{{Name: selector.Output, PhysicalTable: "patient_stale", State: bundlepublication.BundlePublished}}} - catalog := &staleExecutionSelectorCatalog{federationCatalog: &federationCatalog{executions: []bundlepublication.BundleExecution{valid, stale}}, selectors: map[string]DataframeSelector{valid.ID: selector}, staleID: stale.ID} - reader := Reader{Catalog: catalog, ReleaseExecutionResolver: releaseExecutionFixture{"valid-project": valid.ID, "stale-project": stale.ID}} - - sources, err := reader.CurrentFederatedSources(context.Background(), []string{"valid-project", "stale-project"}, selector) - if err != nil { - t.Fatal(err) - } - if len(sources) != 1 || sources[0].Project != "valid-project" || sources[0].PhysicalTable != "patient_valid" { - t.Fatalf("sources = %#v", sources) - } -} - -func (c *federationCatalog) ListExecutions(_ context.Context, state bundlepublication.BundleState, before time.Time) ([]bundlepublication.BundleExecution, error) { - result := make([]bundlepublication.BundleExecution, 0, len(c.executions)) - for _, execution := range c.executions { - if execution.State.Canonical() == state.Canonical() && execution.UpdatedAt.Before(before) { - result = append(result, execution) - } - } - return result, nil -} - -func (c *federationCatalog) GetPointer(_ context.Context, name string) (bundlepublication.BundlePointer, error) { - return c.pointers[name], nil -} - -type noActiveFederationResolver struct{} - -func (noActiveFederationResolver) ResolveActiveManifest(context.Context, string) (publication.Manifest, error) { - return publication.Manifest{}, publication.ErrNoActiveGeneration -} diff --git a/internal/dataframe/published/page.go b/internal/dataframe/published/page.go new file mode 100644 index 00000000..86493bde --- /dev/null +++ b/internal/dataframe/published/page.go @@ -0,0 +1,291 @@ +package published + +import ( + "context" + "fmt" + "strings" +) + +// PageRequest describes a bounded keyset page over one published table. +// AuthResourcePaths is the already-resolved effective read scope. An +// unrestricted request deliberately omits the auth_resource_path predicate. +type PageRequest struct { + Columns []string + Filters []Filter + Sort *Sort + First int + After string + AuthResourcePaths []string + Unrestricted bool +} + +// StreamRequest describes a bounded-memory scan over one published table. +type StreamRequest struct { + Columns []string + Filters []Filter + Sort *Sort + AuthResourcePaths []string + Unrestricted bool +} + +// Page reads one active/queryable materialization. It never constructs a +// cross-project UNION and therefore carries only one table's cursor state. +func (r *Reader) Page(ctx context.Context, materialization Materialization, req PageRequest) (Page, error) { + if r == nil || r.ClickHouse == nil { + return Page{}, backendUnavailable() + } + columns, allowed, err := readerColumns(materialization.Columns, req.Columns, req.Sort) + if err != nil { + return Page{}, err + } + first := req.First + if first <= 0 { + first = 100 + } + if r.MaxPage > 0 && first > r.MaxPage { + first = r.MaxPage + } + cursor, err := decodeCursor(req.After) + if err != nil { + return Page{}, err + } + queryColumns := append([]string(nil), columns...) + if req.Sort != nil && !contains(queryColumns, req.Sort.Column) { + queryColumns = append(queryColumns, req.Sort.Column) + } + queryColumns = append(queryColumns, "__loom_row_id") + selectColumns := quotedColumns(queryColumns) + ", count() OVER() AS `__loom_total`" + readColumns := append(append([]string(nil), queryColumns...), "__loom_total") + query := fmt.Sprintf("SELECT %s FROM `%s`", selectColumns, materialization.PhysicalTable) + args := make([]any, 0) + where := make([]string, 0, len(req.Filters)+1) + if !req.Unrestricted { + if hasColumn(materialization.Columns, authResourcePathColumn) { + where = append(where, "`auth_resource_path` IN ?") + args = append(args, req.AuthResourcePaths) + } else { + where = append(where, "0") + } + } + filterWhere, filterArgs, err := buildWhere(req.Filters, allowed) + if err != nil { + return Page{}, err + } + where = append(where, filterWhere...) + args = append(args, filterArgs...) + if len(where) > 0 { + query += " WHERE " + strings.Join(where, " AND ") + } + if cursor != nil { + cursorWhere, cursorArgs, err := pageCursorPredicate(cursor, req.Sort) + if err != nil { + return Page{}, err + } + if len(where) > 0 { + query += " AND " + cursorWhere + } else { + query += " WHERE " + cursorWhere + } + args = append(args, cursorArgs...) + } + if req.Sort != nil { + direction := "ASC" + if req.Sort.Desc { + direction = "DESC" + } + query += fmt.Sprintf(" ORDER BY `%s` %s, `__loom_row_id` ASC", req.Sort.Column, direction) + } else { + query += " ORDER BY `__loom_row_id` ASC" + } + query += fmt.Sprintf(" LIMIT %d", first+1) + rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, readColumns, args...) + if err != nil { + return Page{}, backendCallError(err) + } + var total int64 + if len(rows) > 0 { + total, err = numericCount(rows[0]["__loom_total"]) + if err != nil { + return Page{}, err + } + } else { + total, err = r.count(ctx, materialization, req, allowed) + if err != nil { + return Page{}, err + } + } + hasNext := len(rows) > first + if hasNext { + rows = rows[:first] + } + next := "" + if hasNext && len(rows) > 0 { + last := rows[len(rows)-1] + var sortValue any + if req.Sort != nil { + sortValue = last[req.Sort.Column] + if sortValue == nil { + return Page{}, invalidCursor() + } + } + next = encodeCursor(fmt.Sprint(last["__loom_row_id"]), sortValue) + } + for _, row := range rows { + delete(row, "__loom_total") + delete(row, "__loom_row_id") + for _, column := range queryColumns { + if !contains(columns, column) { + delete(row, column) + } + } + } + return Page{Materialization: materialization, Columns: columns, Rows: rows, TotalCount: total, HasNext: hasNext, NextCursor: next}, nil +} + +// Stream scans one published table without retaining all rows in memory. +func (r *Reader) Stream(ctx context.Context, materialization Materialization, req StreamRequest, visit func(map[string]any) error) error { + if r == nil || r.ClickHouse == nil { + return backendUnavailable() + } + if visit == nil { + return invalidRequest() + } + columns, allowed, err := readerColumns(materialization.Columns, req.Columns, req.Sort) + if err != nil { + return err + } + queryColumns := append([]string(nil), columns...) + if req.Sort != nil && !contains(queryColumns, req.Sort.Column) { + queryColumns = append(queryColumns, req.Sort.Column) + } + queryColumns = append(queryColumns, "__loom_row_id") + query := fmt.Sprintf("SELECT %s FROM `%s`", quotedColumns(queryColumns), materialization.PhysicalTable) + args := make([]any, 0) + where := make([]string, 0, len(req.Filters)+1) + if !req.Unrestricted { + if hasColumn(materialization.Columns, authResourcePathColumn) { + where = append(where, "`auth_resource_path` IN ?") + args = append(args, req.AuthResourcePaths) + } else { + where = append(where, "0") + } + } + filterWhere, filterArgs, err := buildWhere(req.Filters, allowed) + if err != nil { + return err + } + where = append(where, filterWhere...) + args = append(args, filterArgs...) + if len(where) > 0 { + query += " WHERE " + strings.Join(where, " AND ") + } + if req.Sort != nil { + direction := "ASC" + if req.Sort.Desc { + direction = "DESC" + } + query += fmt.Sprintf(" ORDER BY `%s` %s, `__loom_row_id` ASC", req.Sort.Column, direction) + } else { + query += " ORDER BY `__loom_row_id` ASC" + } + if err := r.ClickHouse.QueryRowsArgsVisit(ctx, query, queryColumns, func(row map[string]any) error { + delete(row, "__loom_row_id") + for _, column := range queryColumns { + if !contains(columns, column) { + delete(row, column) + } + } + return visit(row) + }, args...); err != nil { + return backendCallError(err) + } + return nil +} + +func readerColumns(source []Column, requested []string, sortBy *Sort) ([]string, map[string]struct{}, error) { + for _, column := range requested { + if column == authResourcePathColumn { + return nil, nil, invalidRequest() + } + } + allowed := make(map[string]struct{}, len(source)) + columns := append([]string(nil), requested...) + for _, column := range source { + if column.Name == "__loom_row_id" || column.Name == authResourcePathColumn { + continue + } + allowed[column.Name] = struct{}{} + if len(requested) == 0 { + columns = append(columns, column.Name) + } + } + if err := validateReaderColumns(columns, allowed); err != nil { + return nil, nil, err + } + if sortBy != nil { + if sortBy.Column == authResourcePathColumn { + return nil, nil, invalidRequest() + } + if err := validateReaderColumns([]string{sortBy.Column}, allowed); err != nil { + return nil, nil, err + } + } + return columns, allowed, nil +} + +func (r *Reader) count(ctx context.Context, materialization Materialization, req PageRequest, allowed map[string]struct{}) (int64, error) { + query := fmt.Sprintf("SELECT count() AS `__loom_total` FROM `%s`", materialization.PhysicalTable) + args := make([]any, 0) + where := make([]string, 0, len(req.Filters)+1) + if !req.Unrestricted { + if hasColumn(materialization.Columns, authResourcePathColumn) { + where = append(where, "`auth_resource_path` IN ?") + args = append(args, req.AuthResourcePaths) + } else { + where = append(where, "0") + } + } + filterWhere, filterArgs, err := buildWhere(req.Filters, allowed) + if err != nil { + return 0, err + } + where = append(where, filterWhere...) + args = append(args, filterArgs...) + if len(where) > 0 { + query += " WHERE " + strings.Join(where, " AND ") + } + rows, err := r.ClickHouse.QueryRowsArgs(ctx, query, []string{"__loom_total"}, args...) + if err != nil { + return 0, backendCallError(err) + } + if len(rows) == 0 { + return 0, nil + } + return numericCount(rows[0]["__loom_total"]) +} + +func pageCursorPredicate(cursor *pageCursor, sortBy *Sort) (string, []any, error) { + if cursor == nil || cursor.RowID == "" { + return "", nil, invalidCursor() + } + if sortBy == nil { + return "`__loom_row_id` > ?", []any{cursor.RowID}, nil + } + if cursor.SortValue == nil { + return "", nil, invalidCursor() + } + op := ">" + if sortBy.Desc { + op = "<" + } + return fmt.Sprintf("(`%s` %s ? OR (`%s` = ? AND `__loom_row_id` > ?))", sortBy.Column, op, sortBy.Column), []any{cursor.SortValue, cursor.SortValue, cursor.RowID}, nil +} + +func hasColumn(columns []Column, name string) bool { + for _, column := range columns { + if column.Name == name { + return true + } + } + return false +} diff --git a/internal/dataframe/published/page_test.go b/internal/dataframe/published/page_test.go new file mode 100644 index 00000000..7c535913 --- /dev/null +++ b/internal/dataframe/published/page_test.go @@ -0,0 +1,42 @@ +package published + +import ( + "context" + "strings" + "testing" +) + +type pageQueryer struct{ queries []string } + +func (q *pageQueryer) QueryRowsArgs(_ context.Context, query string, _ []string, _ ...any) ([]map[string]any, error) { + q.queries = append(q.queries, query) + return []map[string]any{{"__loom_row_id": "2", "__loom_total": int64(2)}}, nil +} + +func (q *pageQueryer) QueryRowsArgsVisit(context.Context, string, []string, func(map[string]any) error, ...any) error { + return nil +} + +func TestPageCursorAddsWhereForUnrestrictedTable(t *testing.T) { + queryer := &pageQueryer{} + materialization := Materialization{ + PhysicalTable: "published_patient", + Columns: []Column{{Name: "name", ClickHouse: "String"}}, + } + _, err := (&Reader{ClickHouse: queryer}).Page(context.Background(), materialization, PageRequest{ + First: 1, After: encodeCursor("1", nil), Unrestricted: true, + }) + if err != nil { + t.Fatal(err) + } + if len(queryer.queries) != 1 { + t.Fatalf("queries = %d, want 1", len(queryer.queries)) + } + query := queryer.queries[0] + if !strings.Contains(query, " WHERE `__loom_row_id` > ?") { + t.Fatalf("cursor predicate missing WHERE: %s", query) + } + if strings.Contains(query, " AND `__loom_row_id`") { + t.Fatalf("cursor predicate incorrectly uses AND without WHERE: %s", query) + } +} diff --git a/internal/dataframe/published/query_helpers.go b/internal/dataframe/published/query_helpers.go new file mode 100644 index 00000000..47af30a8 --- /dev/null +++ b/internal/dataframe/published/query_helpers.go @@ -0,0 +1,11 @@ +package published + +import ( + "crypto/sha256" + "encoding/hex" +) + +func shortQueryID(query string) string { + digest := sha256.Sum256([]byte(query)) + return hex.EncodeToString(digest[:8]) +} diff --git a/internal/dataframe/published/read.go b/internal/dataframe/published/read.go index 024f6198..9115dfc3 100644 --- a/internal/dataframe/published/read.go +++ b/internal/dataframe/published/read.go @@ -20,14 +20,11 @@ type ClickHouseQueryer interface { } type Reader struct { - ClickHouse ClickHouseQueryer - Catalog bundlepublication.BundleCatalog - Logger *slog.Logger - MaxPage int - ActiveManifestResolver publication.ActiveResolver - ProjectStatusResolver ProjectStatusResolver - ReleaseExecutionResolver ReleaseExecutionResolver - FederationSnapshotResolver FederationSnapshotResolver + ClickHouse ClickHouseQueryer + Catalog bundlepublication.BundleCatalog + Logger *slog.Logger + MaxPage int + ActiveManifestResolver publication.ActiveResolver } type Filter struct { @@ -175,3 +172,23 @@ func contains(values []string, needle string) bool { } return false } + +func validateReaderColumns(columns []string, allowed map[string]struct{}) error { + for _, column := range columns { + if column == "__loom_row_id" || column == "__loom_total" { + return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + if _, ok := allowed[column]; !ok { + return dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + } + return nil +} + +func quotedColumns(columns []string) string { + quoted := make([]string, len(columns)) + for index, column := range columns { + quoted[index] = fmt.Sprintf("`%s`", column) + } + return strings.Join(quoted, ", ") +} diff --git a/internal/dataframe/published/types.go b/internal/dataframe/published/types.go index c0c3e7a7..74d73bd5 100644 --- a/internal/dataframe/published/types.go +++ b/internal/dataframe/published/types.go @@ -26,29 +26,25 @@ type Column struct { type DataframeSelector = dataset.DataframeSelector type Materialization struct { - ID string `json:"id"` - Name string `json:"name"` - Revision string `json:"revision,omitempty"` - Project string `json:"project"` - DatasetGeneration string `json:"datasetGeneration"` - State State `json:"state"` - ScopeUnrestricted bool `json:"scopeUnrestricted"` - AuthResourcePaths []string `json:"authResourcePaths,omitempty"` - Columns []Column `json:"columns"` - PhysicalTable string `json:"physicalTable"` - RowCount int64 `json:"rowCount"` - RowCountKnown bool `json:"-"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - ReadyAt *time.Time `json:"readyAt,omitempty"` - Error string `json:"error,omitempty"` - FailureCode string `json:"failureCode,omitempty"` - FailureRetryable bool `json:"failureRetryable,omitempty"` - Selector DataframeSelector `json:"selector"` - Availability FederationAvailability `json:"availability,omitempty"` - IncludedProjects int `json:"includedProjects,omitempty"` - ExpectedProjects int `json:"expectedProjects,omitempty"` - ProjectStatuses []ProjectStatus `json:"projectStatuses,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Revision string `json:"revision,omitempty"` + Project string `json:"project"` + DatasetGeneration string `json:"datasetGeneration"` + State State `json:"state"` + ScopeUnrestricted bool `json:"scopeUnrestricted"` + AuthResourcePaths []string `json:"authResourcePaths,omitempty"` + Columns []Column `json:"columns"` + PhysicalTable string `json:"physicalTable"` + RowCount int64 `json:"rowCount"` + RowCountKnown bool `json:"-"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ReadyAt *time.Time `json:"readyAt,omitempty"` + Error string `json:"error,omitempty"` + FailureCode string `json:"failureCode,omitempty"` + FailureRetryable bool `json:"failureRetryable,omitempty"` + Selector DataframeSelector `json:"selector"` } func publishedMaterialization(execution publication.BundleExecution, output publication.BundleOutputRecord, resourceType string) Materialization { diff --git a/internal/dataframe/recipe/document_types.go b/internal/dataframe/recipe/document_types.go index 682bbd27..1e4735b6 100644 --- a/internal/dataframe/recipe/document_types.go +++ b/internal/dataframe/recipe/document_types.go @@ -31,21 +31,64 @@ type Bundle struct { // Output describes one row-shaped result. Names are semantic names, not // storage identifiers. type Output struct { - Name string `json:"name"` - RootResourceType string `json:"rootResourceType"` - RowGrain string `json:"rowGrain"` - Fields []Field `json:"fields,omitempty"` - Filters []Filter `json:"filters,omitempty"` - Pivots []Pivot `json:"pivots,omitempty"` - Aggregates []Aggregate `json:"aggregates,omitempty"` - Slices []RepresentativeSlice `json:"slices,omitempty"` - Traversals []Traversal `json:"traversals,omitempty"` - Expand *Expansion `json:"expand,omitempty"` - Identity *Identity `json:"identity,omitempty"` - DynamicColumns []DynamicColumn `json:"dynamicColumns,omitempty"` - ExtensionColumns []ExtensionColumn `json:"extensionColumns,omitempty"` - CatalogProjections []CatalogProjection `json:"catalogProjections,omitempty"` - CollisionPolicy string `json:"collisionPolicy,omitempty"` + Name string `json:"name"` + RootResourceType string `json:"rootResourceType"` + RowGrain string `json:"rowGrain"` + RootColumnNaming RootColumnNaming `json:"rootColumnNaming,omitempty"` + TraversalColumnNaming TraversalColumnNaming `json:"traversalColumnNaming,omitempty"` + Fields []Field `json:"fields,omitempty"` + Filters []Filter `json:"filters,omitempty"` + Pivots []Pivot `json:"pivots,omitempty"` + Aggregates []Aggregate `json:"aggregates,omitempty"` + Slices []RepresentativeSlice `json:"slices,omitempty"` + Traversals []Traversal `json:"traversals,omitempty"` + Expand *Expansion `json:"expand,omitempty"` + Identity *Identity `json:"identity,omitempty"` + DynamicColumns []DynamicColumn `json:"dynamicColumns,omitempty"` + ExtensionColumns []ExtensionColumn `json:"extensionColumns,omitempty"` + CatalogProjections []CatalogProjection `json:"catalogProjections,omitempty"` + CollisionPolicy string `json:"collisionPolicy,omitempty"` +} + +type RootColumnNaming string + +const ( + RootColumnNamingPrefixed RootColumnNaming = "PREFIXED" + RootColumnNamingExact RootColumnNaming = "EXACT" +) + +func (n RootColumnNaming) Valid() bool { + return n == "" || n == RootColumnNamingPrefixed || n == RootColumnNamingExact +} + +func (n RootColumnNaming) Normalized() RootColumnNaming { + if n == "" { + return RootColumnNamingPrefixed + } + return n +} + +// TraversalColumnNaming controls how traversal aliases contribute to public +// output column names. PATH is the backwards-compatible recipe behavior and +// includes every ancestor alias. ALIAS treats each traversal alias as a +// globally scoped output namespace, which is useful for authoring systems that +// allocate stable, globally unique occurrence IDs. +type TraversalColumnNaming string + +const ( + TraversalColumnNamingPath TraversalColumnNaming = "PATH" + TraversalColumnNamingAlias TraversalColumnNaming = "ALIAS" +) + +func (n TraversalColumnNaming) Valid() bool { + return n == "" || n == TraversalColumnNamingPath || n == TraversalColumnNamingAlias +} + +func (n TraversalColumnNaming) Normalized() TraversalColumnNaming { + if n == "" { + return TraversalColumnNamingPath + } + return n } // Field projects one named semantic value into an output row. @@ -237,11 +280,12 @@ const ( AggregateDistinctValues AggregateOperation = "DISTINCT_VALUES" AggregateMin AggregateOperation = "MIN" AggregateMax AggregateOperation = "MAX" + AggregateContainsAll AggregateOperation = "CONTAINS_ALL" ) func (op AggregateOperation) Valid() bool { switch op { - case AggregateCount, AggregateCountDistinct, AggregateExists, AggregateDistinctValues, AggregateMin, AggregateMax: + case AggregateCount, AggregateCountDistinct, AggregateExists, AggregateDistinctValues, AggregateMin, AggregateMax, AggregateContainsAll: return true default: return false @@ -249,12 +293,14 @@ func (op AggregateOperation) Valid() bool { } type Aggregate struct { - Name string `json:"name"` - Operation AggregateOperation `json:"operation"` - FieldRef string `json:"fieldRef,omitempty"` - Expr *Expression `json:"expr,omitempty"` - Where *Filter `json:"where,omitempty"` - ValueMode ValueMode `json:"valueMode,omitempty"` + Name string `json:"name"` + OutputName string `json:"outputName,omitempty"` + Operation AggregateOperation `json:"operation"` + FieldRef string `json:"fieldRef,omitempty"` + Expr *Expression `json:"expr,omitempty"` + Where *Filter `json:"where,omitempty"` + ValueMode ValueMode `json:"valueMode,omitempty"` + RequiredValues []string `json:"requiredValues,omitempty"` } type RepresentativeSlice struct { diff --git a/internal/dataframe/recipe/document_validation.go b/internal/dataframe/recipe/document_validation.go index b546322f..4788c14a 100644 --- a/internal/dataframe/recipe/document_validation.go +++ b/internal/dataframe/recipe/document_validation.go @@ -45,6 +45,12 @@ func (b Bundle) Validate() error { if strings.TrimSpace(output.RowGrain) == "" { return validationError("required", path+".rowGrain", "rowGrain is required") } + if !output.TraversalColumnNaming.Valid() { + return validationError("invalid_traversal_column_naming", path+".traversalColumnNaming", "must be PATH or ALIAS") + } + if !output.RootColumnNaming.Valid() { + return validationError("invalid_root_column_naming", path+".rootColumnNaming", "must be PREFIXED or EXACT") + } if output.CollisionPolicy != "" && output.CollisionPolicy != "error" && output.CollisionPolicy != "overwrite" && output.CollisionPolicy != "coalesce" { return validationError("invalid_collision_policy", path+".collisionPolicy", "must be error, overwrite, or coalesce") } @@ -55,6 +61,11 @@ func (b Bundle) Validate() error { if err := validateTraversals(output.Traversals, path+".traversals", 0); err != nil { return err } + if output.TraversalColumnNaming.Normalized() == TraversalColumnNamingAlias { + if err := validateGloballyUniqueTraversalAliases(output.Traversals, path+".traversals", map[string]string{}); err != nil { + return err + } + } if output.Expand != nil { if err := validateRecipeName(output.Expand.As, path+".expand.as"); err != nil { return err @@ -86,6 +97,24 @@ func (b Bundle) Validate() error { return nil } +func validateGloballyUniqueTraversalAliases(items []Traversal, path string, seen map[string]string) error { + for index, traversal := range items { + itemPath := fmt.Sprintf("%s[%d]", path, index) + alias := strings.TrimSpace(traversal.Alias) + if alias == "" { + alias = strings.TrimSpace(traversal.Name) + } + if firstPath, exists := seen[alias]; exists { + return validationError("duplicate_global_traversal_alias", itemPath+".alias", fmt.Sprintf("alias %q is already used at %s", alias, firstPath)) + } + seen[alias] = itemPath + ".alias" + if err := validateGloballyUniqueTraversalAliases(traversal.Traversals, itemPath+".traversals", seen); err != nil { + return err + } + } + return nil +} + func validateTraversals(items []Traversal, path string, depth int) error { if depth > maxExpressionDepth { return validationError("max_depth", path, "traversal depth exceeds limit") diff --git a/internal/dataframe/recipe/exec/arango/registry.go b/internal/dataframe/recipe/exec/arango/registry.go index 25052a16..32f9f356 100644 --- a/internal/dataframe/recipe/exec/arango/registry.go +++ b/internal/dataframe/recipe/exec/arango/registry.go @@ -15,10 +15,7 @@ import ( const Collection = "loom_dataframe_recipes" -type Client interface { - QueryRows(context.Context, string, int, map[string]interface{}, arangostore.RowVisitor) error - InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error -} +type Client = arangostore.RowBatchClient type Registry struct { client Client @@ -136,4 +133,4 @@ func recipeDocumentKey(name, translationVersion string) string { return hex.EncodeToString(sum[:]) } -var _ exec.VersionedStore = (*Registry)(nil) +var _ exec.Store = (*Registry)(nil) diff --git a/internal/dataframe/recipe/exec/arango/registry_test.go b/internal/dataframe/recipe/exec/arango/registry_test.go new file mode 100644 index 00000000..814c08b4 --- /dev/null +++ b/internal/dataframe/recipe/exec/arango/registry_test.go @@ -0,0 +1,220 @@ +package arango + +import ( + "context" + "encoding/json" + "errors" + "sort" + "strings" + "testing" + + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/recipe/exec" + arangostore "github.com/calypr/loom/internal/store/arango" +) + +type legacyRegistryClient struct { + recipes []map[string]any + executions []map[string]any +} + +func (c *legacyRegistryClient) InsertBatchRaw(_ context.Context, collection string, data []json.RawMessage, _ bool, _ string) error { + if collection != Collection { + return errors.New("unexpected collection") + } + for _, raw := range data { + var doc map[string]any + if err := json.Unmarshal(raw, &doc); err != nil { + return err + } + key := stringValue(doc["_key"]) + for i := range c.recipes { + if stringValue(c.recipes[i]["_key"]) == key { + c.recipes[i] = doc + continue + } + } + if !containsRecipeKey(c.recipes, key) { + c.recipes = append(c.recipes, doc) + } + } + return nil +} + +func (c *legacyRegistryClient) QueryRows(_ context.Context, query string, _ int, vars map[string]interface{}, visit arangostore.RowVisitor) error { + if strings.Contains(query, "RETURN {replaced: true}") { + for _, execution := range c.executions { + if matchesRecipeIdentity(execution, vars) { + return nil + } + } + key := stringValue(vars["key"]) + document, ok := vars["document"].(map[string]any) + if !ok { + return errors.New("replacement document has unexpected type") + } + for i := range c.recipes { + if stringValue(c.recipes[i]["_key"]) == key { + c.recipes[i] = document + if err := visit(map[string]any{"replaced": true}); err != nil { + return err + } + return nil + } + } + return nil + } + collection := stringValue(vars["@collection"]) + if collection == "loom_dataframe_bundle_executions" { + for _, doc := range c.executions { + if !matchesRecipeIdentity(doc, vars) { + continue + } + if strings.Contains(query, "RETURN {used: true}") { + if err := visit(map[string]any{"used": true}); err != nil { + return err + } + continue + } + if err := visit(map[string]any{"replaced": true}); err != nil { + return err + } + } + return nil + } + + matching := make([]map[string]any, 0, len(c.recipes)) + for _, doc := range c.recipes { + if !matchesRecipeIdentity(doc, vars) { + continue + } + if version, ok := vars["translationVersion"]; ok && stringValue(doc["translationVersion"]) != stringValue(version) { + continue + } + matching = append(matching, doc) + } + sort.Slice(matching, func(i, j int) bool { + return stringValue(matching[i]["translationVersion"]) > stringValue(matching[j]["translationVersion"]) + }) + if len(matching) > 0 { + if err := visit(matching[0]); err != nil { + return err + } + } + return nil +} + +func containsRecipeKey(docs []map[string]any, key string) bool { + for _, doc := range docs { + if stringValue(doc["_key"]) == key { + return true + } + } + return false +} + +func matchesRecipeIdentity(doc map[string]any, vars map[string]interface{}) bool { + name := stringValue(vars["name"]) + version := stringValue(vars["translationVersion"]) + docName := stringValue(doc["name"]) + if docName == "" { + docName = stringValue(doc["Name"]) + } + docVersion := stringValue(doc["translationVersion"]) + if docVersion == "" { + docVersion = stringValue(doc["TranslationVersion"]) + } + return docName == name && (version == "" || docVersion == version) +} + +func legacyEntry(version, digest, rowGrain string) exec.Entry { + return exec.Entry{ + Bundle: recipe.Bundle{ + RecipeSchemaVersion: recipe.CurrentSchemaVersion, + Name: "files", + TranslationVersion: version, + Outputs: []recipe.Output{{ + Name: "DocumentReference", + RootResourceType: "DocumentReference", + RowGrain: rowGrain, + }}, + }, + Digest: digest, + } +} + +func TestRegistrySaveAndLoadRecipes(t *testing.T) { + client := &legacyRegistryClient{} + registry, err := New(client) + if err != nil { + t.Fatal(err) + } + first := legacyEntry("1", "digest-1", "document_reference") + latest := legacyEntry("2", "digest-2", "document_reference") + if err := registry.SaveRecipe(context.Background(), first); err != nil { + t.Fatal(err) + } + if err := registry.SaveRecipe(context.Background(), latest); err != nil { + t.Fatal(err) + } + + loaded, err := registry.LoadRecipe(context.Background(), "files") + if err != nil { + t.Fatal(err) + } + if loaded.Digest != latest.Digest { + t.Fatalf("LoadRecipe() digest = %q, want %q", loaded.Digest, latest.Digest) + } + loaded, err = registry.LoadRecipeVersion(context.Background(), "files", "1") + if err != nil { + t.Fatal(err) + } + if loaded.Digest != first.Digest { + t.Fatalf("LoadRecipeVersion() digest = %q, want %q", loaded.Digest, first.Digest) + } + if _, err := registry.LoadRecipeVersion(context.Background(), "files", "missing"); !errors.Is(err, exec.ErrRecipeNotFound) { + t.Fatalf("LoadRecipeVersion() error = %v, want ErrRecipeNotFound", err) + } +} + +func TestRegistryReplaceRecipeVersionAndUsage(t *testing.T) { + client := &legacyRegistryClient{} + registry, err := New(client) + if err != nil { + t.Fatal(err) + } + original := legacyEntry("1", "digest-1", "document_reference") + replacement := legacyEntry("1", "digest-2", "document_reference_changed") + if err := registry.SaveRecipe(context.Background(), original); err != nil { + t.Fatal(err) + } + used, err := registry.RecipeVersionUsed(context.Background(), "files", "1") + if err != nil { + t.Fatal(err) + } + if used { + t.Fatal("RecipeVersionUsed() = true before an execution exists") + } + if err := registry.ReplaceRecipeVersion(context.Background(), replacement); err != nil { + t.Fatal(err) + } + loaded, err := registry.LoadRecipeVersion(context.Background(), "files", "1") + if err != nil { + t.Fatal(err) + } + if loaded.Digest != replacement.Digest || loaded.Bundle.Outputs[0].RowGrain != replacement.Bundle.Outputs[0].RowGrain { + t.Fatalf("replacement was not loaded: %#v", loaded) + } + + client.executions = []map[string]any{{"Name": "files", "TranslationVersion": "1"}} + used, err = registry.RecipeVersionUsed(context.Background(), "files", "1") + if err != nil { + t.Fatal(err) + } + if !used { + t.Fatal("RecipeVersionUsed() = false after an execution exists") + } + if err := registry.ReplaceRecipeVersion(context.Background(), original); !errors.Is(err, exec.ErrRecipeVersionImmutable) { + t.Fatalf("ReplaceRecipeVersion() error = %v, want ErrRecipeVersionImmutable", err) + } +} diff --git a/internal/dataframe/recipe/exec/arango/revisions.go b/internal/dataframe/recipe/exec/arango/revisions.go index a07247fb..9108dcf8 100644 --- a/internal/dataframe/recipe/exec/arango/revisions.go +++ b/internal/dataframe/recipe/exec/arango/revisions.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "sort" "strings" @@ -51,14 +52,18 @@ func (r *RevisionRegistry) Register(ctx context.Context, project string, bundle if err != nil { return recipe.RecipeRevision{}, err } - if parent != "" { - current, currentErr := r.currentDigest(ctx, project, bundle.Name) - if currentErr != nil && currentErr != exec.ErrRecipeNotFound { - return recipe.RecipeRevision{}, currentErr - } - if current != "" && current != parent { + current, currentErr := r.currentDigest(ctx, project, bundle.Name) + switch { + case currentErr == nil: + if parent == "" || current != parent { return recipe.RecipeRevision{}, fmt.Errorf("recipe revision parent %q is not current (current %q)", parent, current) } + case errors.Is(currentErr, exec.ErrRecipeNotFound): + if parent != "" { + return recipe.RecipeRevision{}, fmt.Errorf("recipe revision parent %q provided for first revision", parent) + } + default: + return recipe.RecipeRevision{}, currentErr } value := recipe.RecipeRevision{Project: project, Name: bundle.Name, Digest: digest, Parent: parent, Bundle: bundle, CreatedAt: time.Now().UTC()} data, err := json.Marshal(value) diff --git a/internal/dataframe/recipe/exec/arango/revisions_test.go b/internal/dataframe/recipe/exec/arango/revisions_test.go index 1a2cf238..5f81f246 100644 --- a/internal/dataframe/recipe/exec/arango/revisions_test.go +++ b/internal/dataframe/recipe/exec/arango/revisions_test.go @@ -81,3 +81,50 @@ func TestRevisionRegistryRoundTrip(t *testing.T) { t.Fatalf("cross-project err=%v", err) } } + +func TestRevisionRegistryRejectsParentForFirstRevision(t *testing.T) { + client := &revisionClient{} + registry, err := NewRevisionRegistry(client) + if err != nil { + t.Fatal(err) + } + bundle := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "files", TranslationVersion: "1", Outputs: []recipe.Output{{Name: "DocumentReference", RootResourceType: "DocumentReference", RowGrain: "document_reference"}}} + + if _, err := registry.Register(context.Background(), "project-a", bundle, "missing"); err == nil { + t.Fatal("Register accepted a parent for the first revision") + } + if len(client.docs) != 0 { + t.Fatalf("Register persisted a dangling first revision: %#v", client.docs) + } +} + +func TestRevisionRegistryRequiresCurrentParentForLaterRevisions(t *testing.T) { + client := &revisionClient{} + registry, err := NewRevisionRegistry(client) + if err != nil { + t.Fatal(err) + } + bundle := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "files", TranslationVersion: "1", Outputs: []recipe.Output{{Name: "DocumentReference", RootResourceType: "DocumentReference", RowGrain: "document_reference"}}} + first, err := registry.Register(context.Background(), "project-a", bundle, "") + if err != nil { + t.Fatal(err) + } + + bundle.TranslationVersion = "2" + second, err := registry.Register(context.Background(), "project-a", bundle, first.Digest) + if err != nil { + t.Fatal(err) + } + if second.Parent != first.Digest { + t.Fatalf("second.Parent = %q, want %q", second.Parent, first.Digest) + } + + bundle.TranslationVersion = "3" + if _, err := registry.Register(context.Background(), "project-a", bundle, ""); err == nil { + t.Fatal("Register accepted an empty parent for a later revision") + } + bundle.TranslationVersion = "4" + if _, err := registry.Register(context.Background(), "project-a", bundle, first.Digest); err == nil { + t.Fatal("Register accepted a stale parent for a later revision") + } +} diff --git a/internal/dataframe/recipe/exec/persistent.go b/internal/dataframe/recipe/exec/persistent.go index ea18753e..fa412161 100644 --- a/internal/dataframe/recipe/exec/persistent.go +++ b/internal/dataframe/recipe/exec/persistent.go @@ -9,19 +9,18 @@ import ( "github.com/calypr/loom/internal/dataframe/recipe" ) -// Store is the persistence boundary for recipe registration. Implementations -// may use Arango, a file-backed registry, or another durable store; the -// registry itself owns validation, canonicalization, and idempotency rules. -type Store interface { - SaveRecipe(context.Context, Entry) error +// Reader is the exact recipe lookup contract used by execution. Name-only +// lookup intentionally means "current", while exact version lookup is always +// available without a runtime capability assertion. +type Reader interface { LoadRecipe(context.Context, string) (Entry, error) + LoadRecipeVersion(context.Context, string, string) (Entry, error) } -// VersionedStore is the durable contract used by new recipe workflows. The -// name-only Store methods remain above for the compatibility window only. -type VersionedStore interface { - Store - LoadRecipeVersion(context.Context, string, string) (Entry, error) +// Store is the persistence boundary for immutable versioned registration. +type Store interface { + Reader + SaveRecipe(context.Context, Entry) error ReplaceRecipeVersion(context.Context, Entry) error RecipeVersionUsed(context.Context, string, string) (bool, error) } @@ -47,23 +46,19 @@ func (r PersistentRegistry) RegisterVersion(ctx context.Context, bundle recipe.B if err != nil { return Entry{}, err } - store, ok := r.Store.(VersionedStore) - if !ok { - return Entry{}, fmt.Errorf("persistent recipe store does not support versioned recipes") - } - existing, err := store.LoadRecipeVersion(ctx, bundle.Name, bundle.TranslationVersion) + existing, err := r.Store.LoadRecipeVersion(ctx, bundle.Name, bundle.TranslationVersion) if err == nil { if existing.Digest == entry.Digest { return existing, nil } - used, err := store.RecipeVersionUsed(ctx, bundle.Name, bundle.TranslationVersion) + used, err := r.Store.RecipeVersionUsed(ctx, bundle.Name, bundle.TranslationVersion) if err != nil { return Entry{}, fmt.Errorf("inspect recipe version usage: %w", err) } if used { return Entry{}, fmt.Errorf("%w: %s@%s", ErrRecipeVersionImmutable, bundle.Name, bundle.TranslationVersion) } - if err := store.ReplaceRecipeVersion(ctx, entry); err != nil { + if err := r.Store.ReplaceRecipeVersion(ctx, entry); err != nil { return Entry{}, err } return entry, nil @@ -71,7 +66,7 @@ func (r PersistentRegistry) RegisterVersion(ctx context.Context, bundle recipe.B if !errors.Is(err, ErrRecipeNotFound) { return Entry{}, fmt.Errorf("load recipe %q: %w", bundle.Name, err) } - if err := store.SaveRecipe(ctx, entry); err != nil { + if err := r.Store.SaveRecipe(ctx, entry); err != nil { return Entry{}, err } return entry, nil diff --git a/internal/dataframe/recipe/fragments.go b/internal/dataframe/recipe/fragments.go index 506df628..1bff3bcb 100644 --- a/internal/dataframe/recipe/fragments.go +++ b/internal/dataframe/recipe/fragments.go @@ -95,81 +95,213 @@ func (l FragmentLibrary) ExpandBundle(bundle Bundle) (Bundle, error) { } func (l FragmentLibrary) expandOutput(output Output) (Output, error) { - for i, field := range output.Fields { - expr, err := l.ExpandExpression(field.Expr) - if err != nil { - return Output{}, fmt.Errorf("field %q: %w", field.Name, err) - } - output.Fields[i].Expr = expr + var err error + output.Fields, err = l.expandFields(output.Fields) + if err != nil { + return Output{}, err + } + output.Pivots, err = l.expandPivots(output.Pivots) + if err != nil { + return Output{}, err } - for i, traversal := range output.Traversals { - resolved, err := l.expandTraversal(traversal) + output.Aggregates, err = l.expandAggregates(output.Aggregates) + if err != nil { + return Output{}, err + } + output.Slices, err = l.expandSlices(output.Slices) + if err != nil { + return Output{}, err + } + output.Traversals = append([]Traversal(nil), output.Traversals...) + for i := range output.Traversals { + resolved, err := l.expandTraversal(output.Traversals[i]) if err != nil { return Output{}, err } output.Traversals[i] = resolved } if output.Expand != nil { - expr, err := l.ExpandExpression(output.Expand.From) + expansion := *output.Expand + expr, err := l.ExpandExpression(expansion.From) if err != nil { return Output{}, err } - output.Expand.From = expr + expansion.From = expr + output.Expand = &expansion } if output.Identity != nil { - expr, err := l.ExpandExpression(output.Identity.Expr) + identity := *output.Identity + expr, err := l.ExpandExpression(identity.Expr) if err != nil { return Output{}, err } - output.Identity.Expr = expr + identity.Expr = expr + output.Identity = &identity + } + output.DynamicColumns, err = l.expandDynamicColumns(output.DynamicColumns) + if err != nil { + return Output{}, err + } + output.ExtensionColumns, err = l.expandExtensionColumns(output.ExtensionColumns) + if err != nil { + return Output{}, err + } + return output, nil +} + +func (l FragmentLibrary) expandTraversal(traversal Traversal) (Traversal, error) { + var err error + traversal.Fields, err = l.expandFields(traversal.Fields) + if err != nil { + return Traversal{}, err } - for i, dynamic := range output.DynamicColumns { - expr, err := l.ExpandExpression(dynamic.Source) + traversal.Pivots, err = l.expandPivots(traversal.Pivots) + if err != nil { + return Traversal{}, err + } + traversal.Aggregates, err = l.expandAggregates(traversal.Aggregates) + if err != nil { + return Traversal{}, err + } + traversal.Slices, err = l.expandSlices(traversal.Slices) + if err != nil { + return Traversal{}, err + } + traversal.Traversals = append([]Traversal(nil), traversal.Traversals...) + for i := range traversal.Traversals { + resolved, err := l.expandTraversal(traversal.Traversals[i]) if err != nil { - return Output{}, err + return Traversal{}, err + } + traversal.Traversals[i] = resolved + } + if traversal.From != nil { + expr, err := l.ExpandExpression(*traversal.From) + if err != nil { + return Traversal{}, err + } + traversal.From = &expr + } + traversal.DynamicColumns, err = l.expandDynamicColumns(traversal.DynamicColumns) + if err != nil { + return Traversal{}, err + } + traversal.ExtensionColumns, err = l.expandExtensionColumns(traversal.ExtensionColumns) + if err != nil { + return Traversal{}, err + } + return traversal, nil +} + +func (l FragmentLibrary) expandFields(fields []Field) ([]Field, error) { + fields = append([]Field(nil), fields...) + for i := range fields { + expr, err := l.ExpandExpression(fields[i].Expr) + if err != nil { + return nil, fmt.Errorf("field %q: %w", fields[i].Name, err) + } + fields[i].Expr = expr + } + return fields, nil +} + +func (l FragmentLibrary) expandPivots(pivots []Pivot) ([]Pivot, error) { + pivots = append([]Pivot(nil), pivots...) + for i := range pivots { + pivot := &pivots[i] + pivot.ValueFallbacks = append([]Expression(nil), pivot.ValueFallbacks...) + columnExpr, err := l.ExpandExpression(pivot.ColumnExpr) + if err != nil { + return nil, fmt.Errorf("pivot %q column expression: %w", pivot.Name, err) + } + pivot.ColumnExpr = columnExpr + valueExpr, err := l.ExpandExpression(pivot.ValueExpr) + if err != nil { + return nil, fmt.Errorf("pivot %q value expression: %w", pivot.Name, err) } - output.DynamicColumns[i].Source = expr - if dynamic.Key != nil { - value, err := l.ExpandExpression(*dynamic.Key) + pivot.ValueExpr = valueExpr + for j := range pivot.ValueFallbacks { + fallback, err := l.ExpandExpression(pivot.ValueFallbacks[j]) if err != nil { - return Output{}, err + return nil, fmt.Errorf("pivot %q fallback: %w", pivot.Name, err) } - output.DynamicColumns[i].Key = &value + pivot.ValueFallbacks[j] = fallback } - if dynamic.Value != nil { - value, err := l.ExpandExpression(*dynamic.Value) + if !pivot.ItemSource.zero() { + itemSource, err := l.ExpandExpression(pivot.ItemSource) if err != nil { - return Output{}, err + return nil, fmt.Errorf("pivot %q item source: %w", pivot.Name, err) } - output.DynamicColumns[i].Value = &value + pivot.ItemSource = itemSource } } - return output, nil + return pivots, nil } -func (l FragmentLibrary) expandTraversal(traversal Traversal) (Traversal, error) { - for i, field := range traversal.Fields { - expr, err := l.ExpandExpression(field.Expr) +func (l FragmentLibrary) expandAggregates(aggregates []Aggregate) ([]Aggregate, error) { + aggregates = append([]Aggregate(nil), aggregates...) + for i := range aggregates { + if aggregates[i].Expr == nil { + continue + } + expr, err := l.ExpandExpression(*aggregates[i].Expr) if err != nil { - return Traversal{}, err + return nil, fmt.Errorf("aggregate %q: %w", aggregates[i].Name, err) } - traversal.Fields[i].Expr = expr + aggregates[i].Expr = &expr } - for i, child := range traversal.Traversals { - resolved, err := l.expandTraversal(child) + return aggregates, nil +} + +func (l FragmentLibrary) expandSlices(slices []RepresentativeSlice) ([]RepresentativeSlice, error) { + slices = append([]RepresentativeSlice(nil), slices...) + for i := range slices { + fields, err := l.expandFields(slices[i].Fields) if err != nil { - return Traversal{}, err + return nil, fmt.Errorf("slice %q: %w", slices[i].Name, err) } - traversal.Traversals[i] = resolved + slices[i].Fields = fields } - if traversal.From != nil { - expr, err := l.ExpandExpression(*traversal.From) + return slices, nil +} + +func (l FragmentLibrary) expandDynamicColumns(columns []DynamicColumn) ([]DynamicColumn, error) { + columns = append([]DynamicColumn(nil), columns...) + for i := range columns { + column := &columns[i] + source, err := l.ExpandExpression(column.Source) if err != nil { - return Traversal{}, err + return nil, fmt.Errorf("dynamic column %q source: %w", column.Name, err) + } + column.Source = source + if column.Key != nil { + key, err := l.ExpandExpression(*column.Key) + if err != nil { + return nil, fmt.Errorf("dynamic column %q key: %w", column.Name, err) + } + column.Key = &key + } + if column.Value != nil { + value, err := l.ExpandExpression(*column.Value) + if err != nil { + return nil, fmt.Errorf("dynamic column %q value: %w", column.Name, err) + } + column.Value = &value } - traversal.From = &expr } - return traversal, nil + return columns, nil +} + +func (l FragmentLibrary) expandExtensionColumns(columns []ExtensionColumn) ([]ExtensionColumn, error) { + columns = append([]ExtensionColumn(nil), columns...) + for i := range columns { + source, err := l.ExpandExpression(columns[i].Source) + if err != nil { + return nil, fmt.Errorf("extension column %q source: %w", columns[i].Name, err) + } + columns[i].Source = source + } + return columns, nil } func (l FragmentLibrary) expand(input Expression, stack []string, depth int) (Expression, error) { @@ -216,6 +348,7 @@ func (l FragmentLibrary) expand(input Expression, stack []string, depth int) (Ex } func substitute(input Expression, bindings map[string]Expression) Expression { + input.Args = append([]Expression(nil), input.Args...) if input.Select != "" { key := strings.TrimSpace(input.Select) for name, value := range bindings { diff --git a/internal/dataframe/recipe/fragments_test.go b/internal/dataframe/recipe/fragments_test.go index a160a11e..e682d497 100644 --- a/internal/dataframe/recipe/fragments_test.go +++ b/internal/dataframe/recipe/fragments_test.go @@ -1,6 +1,10 @@ package recipe -import "testing" +import ( + "bytes" + "encoding/json" + "testing" +) func TestFragmentExpansionIsHygienicAndDigestable(t *testing.T) { lib := FragmentLibrary{Fragments: map[string]Fragment{"reference_id": {Name: "reference_id", Version: "1", Params: []string{"value"}, Expr: Expression{Call: "path_segment", Args: []Expression{{Select: "$value"}}}}}} @@ -36,3 +40,217 @@ func TestBundleExpandFragmentsProducesStandaloneDigestInput(t *testing.T) { t.Fatalf("bundle was not standalone: %#v", expanded) } } + +func TestExpandBundleExpandsEveryExpressionNodeWithoutMutatingInputs(t *testing.T) { + fragment := Fragment{ + Name: "selector", + Version: "1", + Params: []string{"value"}, + Expr: Expression{Call: "path_segment", Args: []Expression{{Select: "$value"}}}, + } + lib := FragmentLibrary{Fragments: map[string]Fragment{"selector": fragment}} + + bundle := Bundle{ + RecipeSchemaVersion: 1, + Name: "all-expression-nodes", + TranslationVersion: "v1", + Fragments: &lib, + Outputs: []Output{{ + Name: "Patient", + RootResourceType: "Patient", + RowGrain: "patient", + Fields: []Field{{ + Name: "field", + Expr: fragmentCall("root.field"), + Fallbacks: []Expression{{Select: "root.fallback"}}, + }}, + Pivots: []Pivot{{ + Name: "pivot", + ColumnExpr: fragmentCall("root.column"), + ValueExpr: fragmentCall("root.value"), + ValueFallbacks: []Expression{fragmentCall("root.value_fallback")}, + ItemSource: fragmentCall("root.items"), + Columns: []string{"one"}, + }}, + Aggregates: []Aggregate{{ + Name: "aggregate", + Operation: AggregateMin, + Expr: expressionPointer(fragmentCall("root.aggregate")), + }}, + Slices: []RepresentativeSlice{{ + Name: "slice", + Limit: 1, + Fields: []Field{{ + Name: "slice_field", + Expr: fragmentCall("root.slice"), + }}, + }}, + Traversals: []Traversal{{ + Name: "subject", + ToResourceType: "Observation", + From: expressionPointer(fragmentCall("root.subject")), + Fields: []Field{{ + Name: "subject_field", + Expr: fragmentCall("subject.field"), + }}, + Pivots: []Pivot{{ + Name: "subject_pivot", + ColumnExpr: fragmentCall("subject.column"), + ValueExpr: fragmentCall("subject.value"), + Columns: []string{"one"}, + }}, + Aggregates: []Aggregate{{ + Name: "subject_aggregate", + Operation: AggregateMax, + Expr: expressionPointer(fragmentCall("subject.aggregate")), + }}, + Slices: []RepresentativeSlice{{ + Name: "subject_slice", + Limit: 1, + Fields: []Field{{ + Name: "subject_slice_field", + Expr: fragmentCall("subject.slice"), + }}, + }}, + Traversals: []Traversal{{ + Name: "detail", + ToResourceType: "Condition", + From: expressionPointer(fragmentCall("subject.detail")), + Fields: []Field{{ + Name: "detail_field", + Expr: fragmentCall("detail.field"), + }}, + }}, + DynamicColumns: []DynamicColumn{{ + Name: "subject_dynamic", + Source: fragmentCall("subject.dynamic_source"), + Key: expressionPointer(fragmentCall("subject.dynamic_key")), + Value: expressionPointer(fragmentCall("subject.dynamic_value")), + }}, + ExtensionColumns: []ExtensionColumn{{ + Name: "subject_extension", + Source: fragmentCall("subject.extension"), + MaxColumns: 1, + }}, + }}, + Expand: &Expansion{ + From: fragmentCall("root.expand"), + As: "item", + }, + Identity: &Identity{ + Name: "identity", + Expr: fragmentCall("root.identity"), + }, + DynamicColumns: []DynamicColumn{{ + Name: "dynamic", + Source: fragmentCall("root.dynamic_source"), + Key: expressionPointer(fragmentCall("root.dynamic_key")), + Value: expressionPointer(fragmentCall("root.dynamic_value")), + }}, + ExtensionColumns: []ExtensionColumn{{ + Name: "extension", + Source: fragmentCall("root.extension"), + MaxColumns: 1, + }}, + }}, + } + + before, err := json.Marshal(bundle) + if err != nil { + t.Fatal(err) + } + expanded, err := bundle.ExpandFragments() + if err != nil { + t.Fatal(err) + } + after, err := json.Marshal(bundle) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Fatalf("fragment expansion mutated its input\nbefore: %s\nafter: %s", before, after) + } + if expanded.Fragments != nil { + t.Fatal("expanded bundle still contains fragments") + } + repeated, err := bundle.ExpandFragments() + if err != nil { + t.Fatal(err) + } + firstJSON, err := json.Marshal(expanded) + if err != nil { + t.Fatal(err) + } + repeatedJSON, err := json.Marshal(repeated) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(firstJSON, repeatedJSON) { + t.Fatalf("repeated expansion changed the result\nfirst: %s\nagain: %s", firstJSON, repeatedJSON) + } + + output := expanded.Outputs[0] + assertExpandedExpression(t, output.Fields[0].Expr, "root.field") + assertExpandedExpression(t, output.Pivots[0].ColumnExpr, "root.column") + assertExpandedExpression(t, output.Pivots[0].ValueExpr, "root.value") + assertExpandedExpression(t, output.Pivots[0].ValueFallbacks[0], "root.value_fallback") + assertExpandedExpression(t, output.Pivots[0].ItemSource, "root.items") + assertExpandedExpression(t, *output.Aggregates[0].Expr, "root.aggregate") + assertExpandedExpression(t, output.Slices[0].Fields[0].Expr, "root.slice") + assertExpandedExpression(t, *output.Traversals[0].From, "root.subject") + assertExpandedExpression(t, output.Traversals[0].Fields[0].Expr, "subject.field") + assertExpandedExpression(t, output.Traversals[0].Pivots[0].ColumnExpr, "subject.column") + assertExpandedExpression(t, output.Traversals[0].Pivots[0].ValueExpr, "subject.value") + assertExpandedExpression(t, *output.Traversals[0].Aggregates[0].Expr, "subject.aggregate") + assertExpandedExpression(t, output.Traversals[0].Slices[0].Fields[0].Expr, "subject.slice") + assertExpandedExpression(t, *output.Traversals[0].Traversals[0].From, "subject.detail") + assertExpandedExpression(t, output.Traversals[0].Traversals[0].Fields[0].Expr, "detail.field") + assertExpandedExpression(t, output.Traversals[0].DynamicColumns[0].Source, "subject.dynamic_source") + assertExpandedExpression(t, *output.Traversals[0].DynamicColumns[0].Key, "subject.dynamic_key") + assertExpandedExpression(t, *output.Traversals[0].DynamicColumns[0].Value, "subject.dynamic_value") + assertExpandedExpression(t, output.Traversals[0].ExtensionColumns[0].Source, "subject.extension") + assertExpandedExpression(t, output.Expand.From, "root.expand") + assertExpandedExpression(t, output.Identity.Expr, "root.identity") + assertExpandedExpression(t, output.DynamicColumns[0].Source, "root.dynamic_source") + assertExpandedExpression(t, *output.DynamicColumns[0].Key, "root.dynamic_key") + assertExpandedExpression(t, *output.DynamicColumns[0].Value, "root.dynamic_value") + assertExpandedExpression(t, output.ExtensionColumns[0].Source, "root.extension") +} + +func TestFragmentExpansionDoesNotMutateFragmentBodies(t *testing.T) { + lib := FragmentLibrary{Fragments: map[string]Fragment{ + "selector": { + Name: "selector", + Version: "1", + Params: []string{"value"}, + Expr: Expression{Call: "path_segment", Args: []Expression{{Select: "$value"}}}, + }, + }} + + first, err := lib.ExpandExpression(fragmentCall("root.first")) + if err != nil { + t.Fatal(err) + } + second, err := lib.ExpandExpression(fragmentCall("root.second")) + if err != nil { + t.Fatal(err) + } + assertExpandedExpression(t, first, "root.first") + assertExpandedExpression(t, second, "root.second") + if got := lib.Fragments["selector"].Expr.Args[0].Select; got != "$value" { + t.Fatalf("fragment body was mutated to %q", got) + } +} + +func fragmentCall(selectPath string) Expression { + return Expression{Call: "fragment:selector", Args: []Expression{{Select: selectPath}}} +} + +func expressionPointer(expr Expression) *Expression { return &expr } + +func assertExpandedExpression(t *testing.T, expr Expression, selectPath string) { + t.Helper() + if expr.Call != "path_segment" || len(expr.Args) != 1 || expr.Args[0].Select != selectPath { + t.Fatalf("expected path_segment(%q), got %#v", selectPath, expr) + } +} diff --git a/internal/dataframe/recipe/types_test.go b/internal/dataframe/recipe/types_test.go index c036d3b1..8f6f7387 100644 --- a/internal/dataframe/recipe/types_test.go +++ b/internal/dataframe/recipe/types_test.go @@ -116,6 +116,34 @@ func TestValidationRejectsVersionNamesAndArity(t *testing.T) { } } +func TestTraversalColumnNamingValidation(t *testing.T) { + valid := `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[{"name":"x","rootResourceType":"Patient","rowGrain":"patient","traversalColumnNaming":"ALIAS","traversals":[{"name":"edge_a","alias":"occ_a","toResourceType":"Condition"},{"name":"edge_b","alias":"occ_b","toResourceType":"Condition"}]}]}` + if _, err := Parse([]byte(valid)); err != nil { + t.Fatal(err) + } + + invalidMode := strings.Replace(valid, `"ALIAS"`, `"LOCAL"`, 1) + if _, err := Parse([]byte(invalidMode)); err == nil || !strings.Contains(err.Error(), "invalid_traversal_column_naming") { + t.Fatalf("expected traversal naming validation error, got %v", err) + } + + duplicateAlias := `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[{"name":"x","rootResourceType":"Patient","rowGrain":"patient","traversalColumnNaming":"ALIAS","traversals":[{"name":"edge_a","alias":"occ_a","toResourceType":"Patient","traversals":[{"name":"edge_nested","alias":"occ_b","toResourceType":"Condition"}]},{"name":"edge_b","alias":"occ_b","toResourceType":"Condition"}]}]}` + if _, err := Parse([]byte(duplicateAlias)); err == nil || !strings.Contains(err.Error(), "duplicate_global_traversal_alias") { + t.Fatalf("expected global alias validation error, got %v", err) + } +} + +func TestRootColumnNamingValidation(t *testing.T) { + valid := `{"recipeSchemaVersion":1,"name":"x","translationVersion":"1","outputs":[{"name":"x","rootResourceType":"Patient","rowGrain":"patient","rootColumnNaming":"EXACT"}]}` + if _, err := Parse([]byte(valid)); err != nil { + t.Fatal(err) + } + invalid := strings.Replace(valid, `"EXACT"`, `"RENAMED"`, 1) + if _, err := Parse([]byte(invalid)); err == nil || !strings.Contains(err.Error(), "invalid_root_column_naming") { + t.Fatalf("expected root naming validation error, got %v", err) + } +} + func TestExplainContainsNoPhysicalDetails(t *testing.T) { b, err := Parse([]byte(validDocument)) if err != nil { diff --git a/internal/dataframe/recipe/validation_rich.go b/internal/dataframe/recipe/validation_rich.go index 525812e0..0192b696 100644 --- a/internal/dataframe/recipe/validation_rich.go +++ b/internal/dataframe/recipe/validation_rich.go @@ -224,13 +224,18 @@ func (a Aggregate) validateAt(path string, budget *int) error { if err := validateRecipeName(a.Name, path+".name"); err != nil { return err } + if a.OutputName != "" { + if err := validateRecipeName(a.OutputName, path+".outputName"); err != nil { + return err + } + } if !a.Operation.Valid() { return validationError("invalid_aggregate_operation", path+".operation", fmt.Sprintf("unsupported operation %q", a.Operation)) } if !a.ValueMode.Valid() { return validationError("invalid_value_mode", path+".valueMode", fmt.Sprintf("unsupported value mode %q", a.ValueMode)) } - requiresExpr := a.Operation == AggregateCountDistinct || a.Operation == AggregateDistinctValues || a.Operation == AggregateMin || a.Operation == AggregateMax + requiresExpr := a.Operation == AggregateCountDistinct || a.Operation == AggregateDistinctValues || a.Operation == AggregateMin || a.Operation == AggregateMax || a.Operation == AggregateContainsAll if requiresExpr && a.Expr == nil { return validationError("required", path+".expr", "operation requires expr") } @@ -247,6 +252,23 @@ func (a Aggregate) validateAt(path string, budget *int) error { return err } } + if a.Operation == AggregateContainsAll { + if len(a.RequiredValues) == 0 { + return validationError("required", path+".requiredValues", "CONTAINS_ALL requires at least one required value") + } + seen := map[string]bool{} + for index, value := range a.RequiredValues { + if strings.TrimSpace(value) == "" { + return validationError("required", fmt.Sprintf("%s.requiredValues[%d]", path, index), "required value must not be empty") + } + if seen[value] { + return validationError("duplicate_name", fmt.Sprintf("%s.requiredValues[%d]", path, index), "duplicate required value") + } + seen[value] = true + } + } else if len(a.RequiredValues) != 0 { + return validationError("invalid_required_values", path+".requiredValues", "requiredValues is only valid for CONTAINS_ALL") + } return nil } @@ -352,6 +374,16 @@ func validateNodeShape(fields []Field, filters []Filter, pivots []Pivot, aggrega if err := aggregate.validateAt(p, budget); err != nil { return err } + finalName := aggregate.Name + if aggregate.OutputName != "" { + finalName = aggregate.OutputName + } + if finalName != aggregate.Name { + if names[finalName] { + return validationError("duplicate_name", p+".outputName", "name conflicts with another node output") + } + names[finalName] = true + } } for i, slice := range slices { p := fmt.Sprintf("%s.slices[%d]", path, i) diff --git a/internal/dataframe/runtime/profile.go b/internal/dataframe/runtime/profile.go deleted file mode 100644 index 7ccdf5f6..00000000 --- a/internal/dataframe/runtime/profile.go +++ /dev/null @@ -1 +0,0 @@ -package runtime diff --git a/internal/dataframe/semantic/execution_context.go b/internal/dataframe/semantic/execution_context.go new file mode 100644 index 00000000..5c7247ec --- /dev/null +++ b/internal/dataframe/semantic/execution_context.go @@ -0,0 +1,14 @@ +package semantic + +import "github.com/calypr/loom/internal/authscope" + +// ExecutionContext carries request-scoped provenance into physical lowering. +// It is deliberately separate from OutputPlan so runtime authorization and +// dataset selection can never become part of a persisted recipe or semantic +// output digest. +type ExecutionContext struct { + Project string + DatasetGeneration string + AuthResourcePaths []string + AuthScopeMode authscope.ReadScopeMode +} diff --git a/internal/dataframe/semantic/recipe_plan_output.go b/internal/dataframe/semantic/recipe_plan_output.go index 144771f7..759054fb 100644 --- a/internal/dataframe/semantic/recipe_plan_output.go +++ b/internal/dataframe/semantic/recipe_plan_output.go @@ -61,10 +61,10 @@ func buildRecipeOutput(output recipe.Output, bindings recipe.RuntimeBindings) (O if err := unnest.Validate(); err != nil { return OutputPlan{}, fmt.Errorf("expand: %w", err) } - plan := OutputPlan{Name: output.Name, RootResourceType: output.RootResourceType, RowGrain: grain, Collision: output.CollisionPolicy, Unnest: unnest} + plan := OutputPlan{Name: output.Name, RootResourceType: output.RootResourceType, RowGrain: grain, RootColumnNaming: output.RootColumnNaming.Normalized(), TraversalColumnNaming: output.TraversalColumnNaming.Normalized(), Collision: output.CollisionPolicy, Unnest: unnest} return finishRecipeOutput(plan, output, scope) } - plan := OutputPlan{Name: output.Name, RootResourceType: output.RootResourceType, RowGrain: grain, Collision: output.CollisionPolicy} + plan := OutputPlan{Name: output.Name, RootResourceType: output.RootResourceType, RowGrain: grain, RootColumnNaming: output.RootColumnNaming.Normalized(), TraversalColumnNaming: output.TraversalColumnNaming.Normalized(), Collision: output.CollisionPolicy} return finishRecipeOutput(plan, output, scope) } @@ -85,12 +85,10 @@ func finishRecipeOutput(plan OutputPlan, output recipe.Output, scope scopeFrame) if plan.Collision == "" { plan.Collision = "error" } - plan.Fields = make([]SemanticProjection, 0, len(output.Fields)) plan.CatalogProjections = make([]string, 0, len(output.CatalogProjections)) for _, projection := range output.CatalogProjections { plan.CatalogProjections = append(plan.CatalogProjections, projection.Name) } - plan.DeclaredOrder = make([]string, 0, len(output.Fields)) plan.Root = SemanticNode{Alias: "root", ResourceType: output.RootResourceType, Fields: make([]SemanticField, 0, len(output.Fields))} rootFilters, err := LowerRecipeFilters(output.RootResourceType, output.Filters) if err != nil { @@ -106,9 +104,7 @@ func finishRecipeOutput(plan OutputPlan, output recipe.Output, scope scopeFrame) if err != nil { return OutputPlan{}, fmt.Errorf("field %q: %w", field.Name, err) } - plan.Fields = append(plan.Fields, normalized.projection) - plan.DeclaredOrder = append(plan.DeclaredOrder, field.Name) - plan.Root.Fields = append(plan.Root.Fields, normalized.field) + plan.Root.Fields = append(plan.Root.Fields, normalized) } for index, traversal := range output.Traversals { child, err := buildRecipeTraversal(traversal, scope, fmt.Sprintf("traversals[%d]", index)) @@ -190,7 +186,7 @@ func buildRecipeTraversal(input recipe.Traversal, parent scopeFrame, path string if err != nil { return SemanticNode{}, err } - node.Fields = append(node.Fields, normalized.field) + node.Fields = append(node.Fields, normalized) } for index, child := range input.Traversals { nested, err := buildRecipeTraversal(child, scope, fmt.Sprintf("%s.traversals[%d]", path, index)) diff --git a/internal/dataframe/semantic/recipe_plan_test.go b/internal/dataframe/semantic/recipe_plan_test.go index 6b1362bf..88d1de68 100644 --- a/internal/dataframe/semantic/recipe_plan_test.go +++ b/internal/dataframe/semantic/recipe_plan_test.go @@ -1,6 +1,7 @@ package semantic import ( + "reflect" "strings" "testing" @@ -8,6 +9,31 @@ import ( "github.com/calypr/loom/internal/dataframe/recipe" ) +func TestRecipePlanStoresRootProjectionOrderOnlyOnRootNode(t *testing.T) { + bundle := recipe.Bundle{ + RecipeSchemaVersion: 1, Name: "canonical-fields", TranslationVersion: "test", + Outputs: []recipe.Output{{Name: "Patient", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{ + {Name: "id", Expr: recipe.Expression{Select: "id"}}, + {Name: "gender", Expr: recipe.Expression{Select: "gender"}}, + }}}, + } + plan, err := BuildRecipePlan(bundle, recipe.RuntimeBindings{Project: "p"}) + if err != nil { + t.Fatal(err) + } + output := plan.Outputs[0] + if got := len(output.Root.Fields); got != 2 || output.Root.Fields[0].Name != "id" || output.Root.Fields[1].Name != "gender" { + t.Fatalf("root fields = %#v, want authored order", output.Root.Fields) + } + typ := reflect.TypeOf(output) + if _, ok := typ.FieldByName("Fields"); ok { + t.Fatal("OutputPlan retains duplicate root Fields storage") + } + if _, ok := typ.FieldByName("DeclaredOrder"); ok { + t.Fatal("OutputPlan retains duplicate declared order storage") + } +} + func TestSemanticUnnestRejectsNonRepeatedAndUnsafeBindings(t *testing.T) { base := SemanticUnnest{ Source: SemanticExpression{Type: expression.Type{Kind: expression.KindString, Cardinality: expression.OptionalOne}}, diff --git a/internal/dataframe/semantic/recipe_plan_types.go b/internal/dataframe/semantic/recipe_plan_types.go index 8fdbc72a..4cb28228 100644 --- a/internal/dataframe/semantic/recipe_plan_types.go +++ b/internal/dataframe/semantic/recipe_plan_types.go @@ -29,21 +29,21 @@ type RecipePlan struct { } type OutputPlan struct { - Name string - Root SemanticNode - RootResourceType string - RowGrain spec.RowGrain - Identity *SemanticExpression + Name string + Root SemanticNode + RootResourceType string + RowGrain spec.RowGrain + RootColumnNaming recipe.RootColumnNaming + TraversalColumnNaming recipe.TraversalColumnNaming + Identity *SemanticExpression // Unnest is the canonical semantic row-producing operation for an output. // Persisted recipe expand syntax is lowered into this operation; consumers // must not infer cardinality-changing behavior from a transport-facing // explanation field; the typed UNNEST operation is authoritative. Unnest *SemanticUnnest - Fields []SemanticProjection DynamicMaps []SemanticDynamicMap CatalogProjections []string Collision string - DeclaredOrder []string } // SemanticExpression keeps the checked typed AST together with the logical @@ -56,14 +56,6 @@ type SemanticExpression struct { Context string } -type SemanticProjection struct { - Name string - FieldRef string - ValueMode string - Expr SemanticExpression - Discovered bool -} - // UnnestJoinMode makes null/empty collection behavior explicit at the // semantic boundary. The renderer must not infer this from the AQL context. type UnnestJoinMode string @@ -183,7 +175,7 @@ func (p RecipePlan) Explain() RecipePlanExplanation { out := RecipePlanExplanation{Version: p.Version, RecipeDigest: p.RecipeDigest, TranslationVersion: p.TranslationVersion, Outputs: make([]OutputPlanExplanation, 0, len(p.Outputs))} for _, output := range p.Outputs { e := OutputPlanExplanation{Name: output.Name, Root: output.RootResourceType, RowGrain: output.RowGrain, DynamicMap: make([]string, 0, len(output.DynamicMaps)), CatalogProjections: append([]string(nil), output.CatalogProjections...)} - for _, field := range output.Fields { + for _, field := range output.Root.Fields { e.Fields = append(e.Fields, explainExpression(field.Expr)) } if output.Identity != nil { diff --git a/internal/dataframe/semantic/recipe_projection.go b/internal/dataframe/semantic/recipe_projection.go index 2ed5883d..1c0c83e7 100644 --- a/internal/dataframe/semantic/recipe_projection.go +++ b/internal/dataframe/semantic/recipe_projection.go @@ -16,66 +16,67 @@ import ( ) // buildRecipeProjection checks one recipe field in the supplied lexical scope -// and converts it to the semantic projection consumed by generic lowering. +// and converts it to the canonical semantic field consumed by generic +// lowering. // The function is intentionally kept separate from BuildRecipePlan so the // root and traversal builders can use exactly the same fallback/value-mode // rules, including slice fields. // -// Schema-version-one fallbacks are selector-only. Selector-only fields retain -// SemanticField.Selector/Fallbacks, which lets the physical lowerer preserve -// typed selector specialization and prepared fallback reuse. A non-selector -// field is still checked through the generic expression AST; its explicit -// value mode is represented by an AST call rather than renderer branching. - -type normalizedRecipeProjection struct { - projection SemanticProjection - field SemanticField -} - -func normalizeRecipeProjection(field recipe.Field, scope scopeFrame, path string) (normalizedRecipeProjection, error) { +// Schema-version-one fallbacks are selector-only. Their checked expression +// forms remain attached to the field; selector metadata is derived only by +// the physical lowerer when it needs schema-specialized extraction. +func normalizeRecipeProjection(field recipe.Field, scope scopeFrame, path string) (SemanticField, error) { if strings.TrimSpace(field.Name) == "" { - return normalizedRecipeProjection{}, fmt.Errorf("%s.name is required", path) + return SemanticField{}, fmt.Errorf("%s.name is required", path) } primary, err := scope.expression(field.Expr, path+".expr") if err != nil { - return normalizedRecipeProjection{}, err + return SemanticField{}, err } fallbacks := make([]SemanticExpression, 0, len(field.Fallbacks)) for index, input := range field.Fallbacks { fallback, err := scope.expression(input, fmt.Sprintf("%s.fallbacks[%d]", path, index)) if err != nil { - return normalizedRecipeProjection{}, err + return SemanticField{}, err } if fallback.Expression.Selector == nil { - return normalizedRecipeProjection{}, fmt.Errorf("%s.fallbacks[%d] must be a selector expression", path, index) + return SemanticField{}, fmt.Errorf("%s.fallbacks[%d] must be a selector expression", path, index) } fallbacks = append(fallbacks, fallback) } // A selector primary plus selector fallbacks is the optimized physical - // representation. Keep the checked expression as the projection source, - // while copying selectors into SemanticField below for generic lowering. + // representation. Keep only the checked expressions here; selectors are + // derived from them by ResolveSemanticField at the physical boundary. if primary.Expression.Selector != nil { - selector, err := recipeSelector(primary.Expression) - if err != nil { - return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + cardinality := spec.CardinalityOptionalOne + if primary.Type.Cardinality == expression.Many { + cardinality = spec.CardinalityMany } - fallbackSelectors := make([]spec.Selector, 0, len(fallbacks)) - for index, fallback := range fallbacks { - parsed, err := recipeSelector(fallback.Expression) - if err != nil { - return normalizedRecipeProjection{}, fmt.Errorf("%s.fallbacks[%d]: %w", path, index, err) + for _, fallback := range fallbacks { + fallbackSelector, selectorErr := recipeSelector(fallback.Expression) + if selectorErr != nil { + return SemanticField{}, fmt.Errorf("%s fallback: %w", path, selectorErr) } - fallbackSelectors = append(fallbackSelectors, parsed) + repeated, _, cardinalityErr := spec.SelectorCardinality(resourceTypeForScope(scope, fallback.Expression), fallbackSelector) + if cardinalityErr != nil { + return SemanticField{}, fmt.Errorf("%s fallback: %w", path, cardinalityErr) + } + if repeated { + cardinality = spec.CardinalityMany + } + } + projection, _, err := projectionForValueMode(string(field.ValueMode), cardinality) + if err != nil { + return SemanticField{}, fmt.Errorf("%s: %w", path, err) } fieldSemantic := SemanticField{ - Name: field.Name, FieldRef: field.FieldRef, Selector: selector, - Fallbacks: fallbackSelectors, ValueMode: string(field.ValueMode), - Expr: &primary.Expression, ExprType: primary.Type, SourcePath: primary.SourcePath, Discovered: field.Discovered, + Name: field.Name, FieldRef: field.FieldRef, Expr: primary, + Fallbacks: fallbacks, Projection: projection, Discovered: field.Discovered, } if err := validateRecipeProjectionTypes(primary.Type, fallbacks); err != nil { - return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + return SemanticField{}, fmt.Errorf("%s: %w", path, err) } selection, err := ResolveSemanticField(resourceTypeForScope(scope, primary.Expression), selectorContext(primary.Expression), 0, fieldSemantic) if err != nil { @@ -83,7 +84,7 @@ func normalizeRecipeProjection(field recipe.Field, scope scopeFrame, path string // malformed/unknown context is already rejected by scope.expression; // retain its useful diagnostic here rather than silently weakening the // cardinality contract. - return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + return SemanticField{}, fmt.Errorf("%s: %w", path, err) } projected := primary switch selection.Projection { @@ -97,24 +98,32 @@ func normalizeRecipeProjection(field recipe.Field, scope scopeFrame, path string projected.Type.Cardinality = expression.Many case spec.ProjectionScalar: default: - return normalizedRecipeProjection{}, fmt.Errorf("%s: unsupported selector projection %q", path, selection.Projection) + return SemanticField{}, fmt.Errorf("%s: unsupported selector projection %q", path, selection.Projection) } - fieldSemantic.ExprType = projected.Type - return normalizedRecipeProjection{projection: SemanticProjection{ - Name: field.Name, FieldRef: field.FieldRef, ValueMode: string(field.ValueMode), Expr: projected, Discovered: field.Discovered, - }, field: fieldSemantic}, nil + fieldSemantic.Expr = projected + fieldSemantic.Projection = selection.Projection + return fieldSemantic, nil } if len(fallbacks) != 0 { - return normalizedRecipeProjection{}, fmt.Errorf("%s.fallbacks require a selector primary expression", path) + return SemanticField{}, fmt.Errorf("%s.fallbacks require a selector primary expression", path) + } + projection, _, err := projectionForValueMode(string(field.ValueMode), semanticCardinality(primary.Type)) + if err != nil { + return SemanticField{}, fmt.Errorf("%s: %w", path, err) } projected, err := applyRecipeValueMode(scope, primary, field.ValueMode) if err != nil { - return normalizedRecipeProjection{}, fmt.Errorf("%s: %w", path, err) + return SemanticField{}, fmt.Errorf("%s: %w", path, err) + } + return SemanticField{Name: field.Name, FieldRef: field.FieldRef, Expr: projected, Projection: projection, Discovered: field.Discovered}, nil +} + +func semanticCardinality(input expression.Type) spec.Cardinality { + if input.Cardinality == expression.Many { + return spec.CardinalityMany } - return normalizedRecipeProjection{projection: SemanticProjection{Name: field.Name, FieldRef: field.FieldRef, ValueMode: string(field.ValueMode), Expr: projected}, field: SemanticField{ - Name: field.Name, FieldRef: field.FieldRef, ValueMode: string(field.ValueMode), Expr: &projected.Expression, ExprType: projected.Type, SourcePath: projected.SourcePath, Discovered: field.Discovered, - }}, nil + return spec.CardinalityOptionalOne } // recipeProjectionField returns the canonical selector-bearing SemanticField @@ -122,11 +131,7 @@ func normalizeRecipeProjection(field recipe.Field, scope scopeFrame, path string // Expr is the checked expression; physical expression lowering owns that // case, while selector lowering remains schema-backed. func recipeProjectionField(field recipe.Field, scope scopeFrame, path string) (SemanticField, error) { - result, err := normalizeRecipeProjection(field, scope, path) - if err != nil { - return SemanticField{}, err - } - return result.field, nil + return normalizeRecipeProjection(field, scope, path) } // recipeSelector converts a checked expression selector to the schema selector diff --git a/internal/dataframe/semantic/recipe_rich_shaping.go b/internal/dataframe/semantic/recipe_rich_shaping.go index 2df2577f..01730ce7 100644 --- a/internal/dataframe/semantic/recipe_rich_shaping.go +++ b/internal/dataframe/semantic/recipe_rich_shaping.go @@ -269,15 +269,18 @@ func lowerRecipeAggregates(resourceType, alias string, scope scopeFrame, aggrega return nil, fmt.Errorf("%s.valueMode %q is unsupported", path, input.ValueMode) } semanticAggregate := SemanticAggregate{ - Name: input.Name, - Operation: operation, - FieldRef: input.FieldRef, - ValueMode: string(input.ValueMode), + Name: input.Name, + OutputName: input.OutputName, + Operation: operation, + FieldRef: input.FieldRef, + ValueMode: string(input.ValueMode), + RequiredValues: append([]string(nil), input.RequiredValues...), } requiresSelector := operation == string(recipe.AggregateCountDistinct) || operation == string(recipe.AggregateDistinctValues) || operation == string(recipe.AggregateMin) || - operation == string(recipe.AggregateMax) + operation == string(recipe.AggregateMax) || + operation == string(recipe.AggregateContainsAll) if input.Expr != nil { if !requiresSelector { return nil, fmt.Errorf("%s.expr is not accepted for operation %s", path, operation) @@ -287,16 +290,34 @@ func lowerRecipeAggregates(resourceType, alias string, scope scopeFrame, aggrega return nil, err } semanticAggregate.Selector = &selector + metadata, ok := fhirschema.ResolveTerminalScalarMetadata(resourceType, selector.CanonicalPath()) + if !ok || metadata.Primitive == fhirschema.PrimitiveUnknown { + return nil, fmt.Errorf("%s.expr does not resolve to a supported scalar selector", path) + } + semanticAggregate.ValueKind = primitiveKind(metadata.Primitive) } else if requiresSelector { return nil, fmt.Errorf("%s.expr is required for operation %s", path, operation) } + switch operation { + case string(recipe.AggregateCount), string(recipe.AggregateCountDistinct): + semanticAggregate.ValueKind = expression.KindInteger + case string(recipe.AggregateMin), string(recipe.AggregateMax): + if semanticAggregate.ValueKind == "" { + semanticAggregate.ValueKind = expression.KindString + } + case string(recipe.AggregateExists): + semanticAggregate.ValueKind = expression.KindBoolean + case string(recipe.AggregateContainsAll): + semanticAggregate.ValueKind = expression.KindBoolean + } if input.Where != nil { - predicate, equals, err := lowerRecipePredicate(resourceType, alias, scope, input.Where, path+".where") + predicate, equals, kind, err := lowerRecipePredicate(resourceType, alias, scope, input.Where, path+".where") if err != nil { return nil, err } semanticAggregate.Predicate = predicate semanticAggregate.PredicateEquals = equals + semanticAggregate.PredicateKind = kind } out = append(out, semanticAggregate) } @@ -341,21 +362,22 @@ func lowerRecipeSlices(resourceType, alias string, scope scopeFrame, slices []re return nil, fmt.Errorf("%s.name %q is duplicated", fieldPath, field.Name) } seenFields[field.Name] = struct{}{} - if semanticField.Expr == nil || semanticField.Expr.Selector == nil { + if semanticField.Expr.Expression.Selector == nil { return nil, fmt.Errorf("%s.expr must be a selector for canonical slice lowering", fieldPath) } - if err := ensureSelectorOwnedByNode(semanticField.Expr.Selector.Context, alias, fieldPath+".expr"); err != nil { + if err := ensureSelectorOwnedByNode(semanticField.Expr.Expression.Selector.Context, alias, fieldPath+".expr"); err != nil { return nil, err } semanticSlice.Fields = append(semanticSlice.Fields, semanticField) } if input.Where != nil { - predicate, equals, err := lowerRecipePredicate(resourceType, alias, scope, input.Where, path+".where") + predicate, equals, kind, err := lowerRecipePredicate(resourceType, alias, scope, input.Where, path+".where") if err != nil { return nil, err } semanticSlice.Predicate = predicate semanticSlice.PredicateEquals = equals + semanticSlice.PredicateKind = kind } out = append(out, semanticSlice) } @@ -404,38 +426,51 @@ func recipeNodeSelector(resourceType, alias string, scope scopeFrame, input reci } // lowerRecipePredicate maps the only predicate shape currently representable -// by SemanticAggregate and SemanticSlice: selector existence or a string +// by SemanticAggregate and SemanticSlice: selector existence or string/code // equality. Richer boolean predicates remain ordinary node filters until the // canonical physical predicate IR grows an equivalent typed representation. -func lowerRecipePredicate(resourceType, alias string, scope scopeFrame, input *recipe.Filter, path string) (*spec.Selector, string, error) { +func lowerRecipePredicate(resourceType, alias string, scope scopeFrame, input *recipe.Filter, path string) (*spec.Selector, string, spec.FilterValueKind, error) { if input == nil { - return nil, "", nil + return nil, "", "", nil } filters, err := LowerRecipeFiltersForAlias(resourceType, alias, []recipe.Filter{*input}) if err != nil { - return nil, "", fmt.Errorf("%s: %w", path, err) + return nil, "", "", fmt.Errorf("%s: %w", path, err) } if len(filters) != 1 { - return nil, "", fmt.Errorf("%s must contain exactly one predicate", path) + return nil, "", "", fmt.Errorf("%s must contain exactly one predicate", path) } filter := filters[0] selector, err := spec.ParseSelector(filter.Selector) if err != nil { - return nil, "", fmt.Errorf("%s selector: %w", path, err) + return nil, "", "", fmt.Errorf("%s selector: %w", path, err) } if err := validateSemanticSelector(resourceType, selector); err != nil { - return nil, "", fmt.Errorf("%s selector: %w", path, err) + return nil, "", "", fmt.Errorf("%s selector: %w", path, err) } switch filter.Operator { case spec.FilterExists: - return &selector, "", nil + return &selector, "", filter.FieldKind, nil case spec.FilterEquals: - if len(filter.Values) != 1 || filter.Values[0].String == nil || filter.Values[0].Kind != spec.FilterString { - return nil, "", fmt.Errorf("%s equality predicate must use one STRING value", path) + if len(filter.Values) != 1 { + return nil, "", "", fmt.Errorf("%s equality predicate must use one value", path) + } + switch value := filter.Values[0]; value.Kind { + case spec.FilterString: + if value.String == nil { + return nil, "", "", fmt.Errorf("%s equality predicate has no STRING value", path) + } + return &selector, *value.String, value.Kind, nil + case spec.FilterCode: + if value.Code == nil || strings.TrimSpace(value.Code.Code) == "" { + return nil, "", "", fmt.Errorf("%s equality predicate has no CODE value", path) + } + return &selector, value.Code.Code, value.Kind, nil + default: + return nil, "", "", fmt.Errorf("%s equality predicate kind %s is not representable", path, value.Kind) } - return &selector, *filter.Values[0].String, nil default: - return nil, "", fmt.Errorf("%s operator %s is not representable by canonical aggregate/slice predicates", path, filter.Operator) + return nil, "", "", fmt.Errorf("%s operator %s is not representable by canonical aggregate/slice predicates", path, filter.Operator) } } diff --git a/internal/dataframe/semantic/recipe_rich_shaping_test.go b/internal/dataframe/semantic/recipe_rich_shaping_test.go index eba28ef7..21e64726 100644 --- a/internal/dataframe/semantic/recipe_rich_shaping_test.go +++ b/internal/dataframe/semantic/recipe_rich_shaping_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataframe/spec" ) func TestLowerRecipePivotsUsesGeneratedFamilyAndDeclaredColumns(t *testing.T) { @@ -95,6 +96,35 @@ func TestLowerRecipeAggregatesRejectsUnrepresentableWhereAndCountExpr(t *testing } } +func TestLowerRecipeAggregatesPreservesOutputNameAndRequiredValues(t *testing.T) { + scope := newRootScope("Specimen") + aggregates, err := lowerRecipeAggregates("Specimen", "specimen", scope, []recipe.Aggregate{{ + Name: "paired", OutputName: "has_pair", Operation: recipe.AggregateContainsAll, + Expr: recipeExpr("type.coding[].code"), RequiredValues: []string{"Tumor", "Normal"}, + }}) + if err != nil { + t.Fatal(err) + } + if len(aggregates) != 1 || aggregates[0].OutputName != "has_pair" || aggregates[0].ValueKind != "boolean" || len(aggregates[0].RequiredValues) != 2 { + t.Fatalf("aggregate = %#v", aggregates) + } +} + +func TestLowerRecipeAggregatesPreservesCodePredicateKind(t *testing.T) { + code := "Tumor" + scope := newRootScope("Specimen") + aggregates, err := lowerRecipeAggregates("Specimen", "specimen", scope, []recipe.Aggregate{{ + Name: "tumor_count", Operation: recipe.AggregateCount, + Where: &recipe.Filter{Select: "specimen.type.coding[].code", Operator: recipe.FilterEquals, Quantifier: recipe.QuantifierAny, Values: []recipe.FilterValue{{Kind: recipe.FilterCode, Code: &recipe.CodeValue{Code: code}}}}, + }}) + if err != nil { + t.Fatal(err) + } + if len(aggregates) != 1 || aggregates[0].PredicateKind != spec.FilterCode || aggregates[0].PredicateEquals != code { + t.Fatalf("aggregate = %#v", aggregates) + } +} + func TestLowerRecipeSlicesUsesProjectionFallbacksAndPredicate(t *testing.T) { scope := newRootScope("Patient") want := "female" @@ -113,7 +143,7 @@ func TestLowerRecipeSlicesUsesProjectionFallbacksAndPredicate(t *testing.T) { t.Fatalf("slice = %#v", slices) } field := slices[0].Fields[0] - if len(field.Fallbacks) != 1 || field.ValueMode != string(recipe.ValueModeFirst) { + if len(field.Fallbacks) != 1 || field.Projection != spec.ProjectionFirst { t.Fatalf("slice field = %#v", field) } } diff --git a/internal/dataframe/semantic/selection_semantics.go b/internal/dataframe/semantic/selection_semantics.go index f80c380a..a99cc563 100644 --- a/internal/dataframe/semantic/selection_semantics.go +++ b/internal/dataframe/semantic/selection_semantics.go @@ -9,6 +9,40 @@ import ( fhirschema "github.com/calypr/loom/internal/fhir/schema" ) +// PrimarySelector derives the schema selector used by the physical compiler +// from the checked primary expression. Semantic plans keep expressions as +// their single source of truth so diagnostics, type information, and +// lowering cannot observe different selector values. +func (field SemanticField) PrimarySelector() (spec.Selector, error) { + if field.Expr.Expression.Selector == nil { + return spec.Selector{}, fmt.Errorf("field %q expression is not a selector", field.Name) + } + selector, err := spec.ParseSelector(field.Expr.Expression.Selector.Path) + if err != nil { + return spec.Selector{}, fmt.Errorf("field %q selector: %w", field.Name, err) + } + return selector, nil +} + +// FallbackSelectors derives the ordered selector fallback chain from checked +// semantic expressions. Fallbacks are selector-only in the current recipe +// contract; rejecting anything else here keeps that constraint at the +// semantic/physical boundary instead of relying on a parallel selector list. +func (field SemanticField) FallbackSelectors() ([]spec.Selector, error) { + selectors := make([]spec.Selector, 0, len(field.Fallbacks)) + for index, fallback := range field.Fallbacks { + if fallback.Expression.Selector == nil { + return nil, fmt.Errorf("field %q fallback %d is not a selector", field.Name, index) + } + selector, err := spec.ParseSelector(fallback.Expression.Selector.Path) + if err != nil { + return nil, fmt.Errorf("field %q fallback %d selector: %w", field.Name, index, err) + } + selectors = append(selectors, selector) + } + return selectors, nil +} + // SelectionSemanticSpec is the compiler-ready meaning of one field selection. // It deliberately contains selectors and schema facts, not rendered AQL. type SelectionSemanticSpec struct { @@ -29,11 +63,19 @@ func ResolveSemanticField(resourceType, nodeAlias string, index int, field Seman if !fhirschema.HasResource(resourceType) { return SelectionSemanticSpec{}, fmt.Errorf("field %q: resource type %q is not in the active FHIR schema", field.Name, resourceType) } - repeated, paths, err := spec.SelectorCardinality(resourceType, field.Selector) + selector, err := field.PrimarySelector() + if err != nil { + return SelectionSemanticSpec{}, err + } + fallbacks, err := field.FallbackSelectors() + if err != nil { + return SelectionSemanticSpec{}, err + } + repeated, paths, err := spec.SelectorCardinality(resourceType, selector) if err != nil { return SelectionSemanticSpec{}, fmt.Errorf("field %q: %w", field.Name, err) } - for fallbackIndex, fallback := range field.Fallbacks { + for fallbackIndex, fallback := range fallbacks { fallbackRepeated, fallbackPaths, err := spec.SelectorCardinality(resourceType, fallback) if err != nil { return SelectionSemanticSpec{}, fmt.Errorf("field %q fallback %d: %w", field.Name, fallbackIndex, err) @@ -46,8 +88,17 @@ func ResolveSemanticField(resourceType, nodeAlias string, index int, field Seman if repeated { cardinality = spec.CardinalityMany } - projection, legacyAuto, err := projectionForValueMode(field.ValueMode, cardinality) - if err != nil { + projection := field.Projection + legacyAuto := false + if projection == "" { + // Direct semantic graph callers may omit Projection. Preserve the + // historical AUTO behavior for that transport-neutral input while + // recipe plans record the resolved projection explicitly. + projection, legacyAuto, err = projectionForValueMode("", cardinality) + if err != nil { + return SelectionSemanticSpec{}, fmt.Errorf("field %q: %w", field.Name, err) + } + } else if err := projection.Validate(); err != nil { return SelectionSemanticSpec{}, fmt.Errorf("field %q: %w", field.Name, err) } if err := spec.ValidateProjection(cardinality, projection); err != nil { @@ -60,7 +111,7 @@ func ResolveSemanticField(resourceType, nodeAlias string, index int, field Seman return SelectionSemanticSpec{ Alias: nodeAlias + "." + name, NodeAlias: nodeAlias, ResourceType: resourceType, FieldRef: field.FieldRef, - Selector: field.Selector, Fallbacks: append([]spec.Selector(nil), field.Fallbacks...), + Selector: selector, Fallbacks: fallbacks, Cardinality: cardinality, Projection: projection, LegacyAuto: legacyAuto, RepeatedPaths: paths, }, nil diff --git a/internal/dataframe/semantic/semantic_plan.go b/internal/dataframe/semantic/semantic_nodes.go similarity index 52% rename from internal/dataframe/semantic/semantic_plan.go rename to internal/dataframe/semantic/semantic_nodes.go index dba76395..d1c7352b 100644 --- a/internal/dataframe/semantic/semantic_plan.go +++ b/internal/dataframe/semantic/semantic_nodes.go @@ -1,25 +1,13 @@ package semantic -// SemanticPlan is the backend-independent graph consumed by the physical -// compiler. Recipe bundles are the only production input format; the recipe -// builder populates this graph after schema, scope, and expression validation. - import ( - "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/dataframe/expression" "github.com/calypr/loom/internal/dataframe/spec" ) -type SemanticPlan struct { - Version int - Project string - DatasetGeneration string - AuthResourcePaths []string - AuthScopeMode authscope.ReadScopeMode - RowIdentity *spec.RowIdentity - Root SemanticNode -} - +// SemanticNode is the canonical backend-independent graph node used by every +// dataframe frontend. Runtime request provenance belongs in ExecutionContext, +// while output-specific shaping belongs in OutputPlan. type SemanticNode struct { Alias string ResourceType string @@ -36,14 +24,15 @@ type SemanticNode struct { } type SemanticField struct { - Name string - FieldRef string - Selector spec.Selector - Fallbacks []spec.Selector - ValueMode string - Expr *expression.Expression - ExprType expression.Type - SourcePath string + Name string + FieldRef string + // Expr and Fallbacks are the checked semantic expressions that produced + // this field. Selectors are intentionally derived from these expressions + // at the physical boundary; keeping both representations here allowed the + // recipe planner and lowerer to silently diverge. + Expr SemanticExpression + Fallbacks []SemanticExpression + Projection spec.ProjectionMode Discovered bool } @@ -64,21 +53,24 @@ type SemanticPivot struct { type SemanticAggregate struct { Name string + OutputName string Operation string FieldRef string Selector *spec.Selector - PredicateField string Predicate *spec.Selector PredicateEquals string + PredicateKind spec.FilterValueKind ValueMode string + RequiredValues []string + ValueKind expression.ValueKind } type SemanticSlice struct { Name string Limit int - PredicateField string Predicate *spec.Selector PredicateEquals string + PredicateKind spec.FilterValueKind Fields []SemanticField } @@ -86,26 +78,3 @@ func validateSemanticSelector(resourceType string, selector spec.Selector) error _, _, err := spec.SelectorCardinality(resourceType, selector) return err } - -// Explain returns a compact logical summary. It intentionally does not expose -// physical AQL decisions. - -type SemanticPlanExplanation struct { - Version int - RootResourceType string - DatasetGeneration string - RowIdentity *spec.RowIdentity - Nodes []SemanticNodeExplanation -} - -type SemanticNodeExplanation struct { - Alias string - ParentAlias string - ResourceType string - EdgeLabel string - MatchMode spec.TraversalMatchMode - FieldCount int - PivotCount int - AggregateCount int - SliceCount int -} diff --git a/internal/dataframe/semantic/semantic_validation.go b/internal/dataframe/semantic/semantic_validation.go index e76ce2db..ad8eedf5 100644 --- a/internal/dataframe/semantic/semantic_validation.go +++ b/internal/dataframe/semantic/semantic_validation.go @@ -8,29 +8,29 @@ import ( ) // ValidateSemanticGraph checks the schema and structural safety of an already -// constructed semantic plan. It performs no observed-data or authorization -// checks and does not mutate the plan. -func ValidateSemanticGraph(plan SemanticPlan) error { - if strings.TrimSpace(plan.Root.ResourceType) == "" { +// constructed semantic graph. It performs no observed-data or authorization +// checks and does not mutate the graph. +func ValidateSemanticGraph(root SemanticNode) error { + if strings.TrimSpace(root.ResourceType) == "" { return fmt.Errorf("semantic graph root resource type is required") } - if plan.Root.Alias != "root" { - return fmt.Errorf("semantic graph root alias must be %q, got %q", "root", plan.Root.Alias) + if root.Alias != "root" { + return fmt.Errorf("semantic graph root alias must be %q, got %q", "root", root.Alias) } - if strings.TrimSpace(plan.Root.EdgeLabel) != "" { - return fmt.Errorf("semantic graph root must not declare edge label %q", plan.Root.EdgeLabel) + if strings.TrimSpace(root.EdgeLabel) != "" { + return fmt.Errorf("semantic graph root must not declare edge label %q", root.EdgeLabel) } - if plan.Root.MatchMode != "" { - return fmt.Errorf("semantic graph root must not declare traversal match mode %q", plan.Root.MatchMode) + if root.MatchMode != "" { + return fmt.Errorf("semantic graph root must not declare traversal match mode %q", root.MatchMode) } - if !fhirschema.HasResource(plan.Root.ResourceType) { - return fmt.Errorf("semantic graph root resource type %q is not represented by the active generated FHIR schema", plan.Root.ResourceType) + if !fhirschema.HasResource(root.ResourceType) { + return fmt.Errorf("semantic graph root resource type %q is not represented by the active generated FHIR schema", root.ResourceType) } state := semanticValidationState{ aliases: map[string]string{}, } - return state.validateChildren(plan.Root, []string{plan.Root.ResourceType}) + return state.validateChildren(root, []string{root.ResourceType}) } type semanticValidationState struct { diff --git a/internal/dataset/arango/manifest_lifecycle.go b/internal/dataset/arango/manifest_lifecycle.go index b828add5..c1cc2cf3 100644 --- a/internal/dataset/arango/manifest_lifecycle.go +++ b/internal/dataset/arango/manifest_lifecycle.go @@ -23,16 +23,12 @@ var ( ErrUnexpectedStoreResult = errors.New("unexpected generation store result") ) -type QueryRowsClient interface { - QueryRows(context.Context, string, int, map[string]interface{}, arangostore.RowVisitor) error -} - -var _ QueryRowsClient = (*arangostore.Client)(nil) +var _ arangostore.RowQueryer = (*arangostore.Client)(nil) var _ publication.ActiveResolver = (*Store)(nil) -type Store struct{ client QueryRowsClient } +type Store struct{ client arangostore.RowQueryer } -func New(client QueryRowsClient) (*Store, error) { +func New(client arangostore.RowQueryer) (*Store, error) { if client == nil { return nil, ErrNilQueryClient } diff --git a/internal/dataset/arango/release_store.go b/internal/dataset/arango/release_store.go new file mode 100644 index 00000000..d732ddfa --- /dev/null +++ b/internal/dataset/arango/release_store.go @@ -0,0 +1,178 @@ +package arango + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/calypr/loom/internal/dataset" + arangostore "github.com/calypr/loom/internal/store/arango" +) + +func (s *Store) SaveRelease(ctx context.Context, release dataset.ProjectRelease) (dataset.ProjectRelease, error) { + document, err := lifecycleDocument(release) + if err != nil { + return dataset.ProjectRelease{}, err + } + document["_key"] = releaseDocumentKey(release.ID) + document["recordType"] = releaseRecordType + binds := lifecycleCollectionBindVars() + binds["key"], binds["candidate"] = document["_key"], document + binds["active_release_key"] = activeReleaseDocumentKey(release.Project) + binds["active_release_placeholder"] = activeReleasePlaceholderDocument(release.Project) + var saved *dataset.ProjectRelease + err = s.client.QueryRows(ctx, saveReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { + decoded, err := decodeValue[dataset.ProjectRelease](row) + if err != nil { + return err + } + saved = &decoded + return nil + }) + if err != nil { + return dataset.ProjectRelease{}, err + } + if saved == nil { + return dataset.ProjectRelease{}, fmt.Errorf("release already exists with different content") + } + return *saved, nil +} + +func (s *Store) ReadActiveRelease(ctx context.Context, project string) (dataset.ActiveRelease, error) { + binds := lifecycleCollectionBindVars() + binds["key"] = activeReleaseDocumentKey(project) + var active *dataset.ActiveRelease + err := s.client.QueryRows(ctx, readActiveReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { + decoded, err := decodeValue[dataset.ActiveRelease](row) + if err != nil { + return err + } + active = &decoded + return nil + }) + if err != nil { + return dataset.ActiveRelease{}, fmt.Errorf("read active release: %w", err) + } + if active == nil { + return dataset.ActiveRelease{}, dataset.ErrNoActiveRelease + } + return *active, nil +} + +func (s *Store) CompareAndSwapActivateRelease(ctx context.Context, release dataset.ProjectRelease, expectedRevision int64) (dataset.ActiveRelease, error) { + return CompareAndSwapActivateRelease(ctx, s.client, release, expectedRevision) +} + +// CompareAndSwapActivateRelease commits the active Explorer revision and +// matching dataset release in one Arango transaction. +func CompareAndSwapActivateRelease(ctx context.Context, client arangostore.RowQueryer, release dataset.ProjectRelease, expectedRevision int64) (dataset.ActiveRelease, error) { + releaseDocument, err := lifecycleDocument(release) + if err != nil { + return dataset.ActiveRelease{}, err + } + releaseDocument["_key"] = releaseDocumentKey(release.ID) + releaseDocument["recordType"] = releaseRecordType + binds := lifecycleCollectionBindVars() + binds["project"] = release.Project + binds["release_key"] = releaseDocument["_key"] + binds["release_id"] = release.ID + binds["generation"] = release.Generation + binds["manifest_key"] = manifestDocumentKey(dataset.Ref{Project: release.Project, Generation: release.Generation}) + binds["active_key"] = activeDocumentKey(release.Project) + binds["active_release_key"] = activeReleaseDocumentKey(release.Project) + binds["expected_revision"] = expectedRevision + binds["next_revision"] = expectedRevision + 1 + binds["staged_state"] = string(dataset.StateStaged) + binds["ready_state"] = string(dataset.StateReady) + var active *dataset.ActiveRelease + err = client.QueryRows(ctx, activateReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { + decoded, err := decodeValue[dataset.ActiveRelease](row) + if err != nil { + return err + } + active = &decoded + return nil + }) + if err != nil { + return dataset.ActiveRelease{}, fmt.Errorf("activate release: %w", err) + } + if active == nil { + return dataset.ActiveRelease{}, dataset.ErrReleaseActivationConflict + } + return *active, nil +} + +func lifecycleDocument(value any) (map[string]any, error) { + encoded, err := json.Marshal(value) + if err != nil { + return nil, err + } + var document map[string]any + if err := json.Unmarshal(encoded, &document); err != nil { + return nil, err + } + return document, nil +} + +func decodeValue[T any](value any) (T, error) { + var decoded T + encoded, err := json.Marshal(value) + if err != nil { + return decoded, err + } + if err := json.Unmarshal(encoded, &decoded); err != nil { + return decoded, err + } + return decoded, nil +} + +func releaseDocumentKey(id string) string { return documentKey("release", id) } +func activeReleaseDocumentKey(project string) string { return documentKey("active_release", project) } + +const saveReleaseAQL = ` +LET existing = DOCUMENT(@@lifecycle_collection, @key) +LET activeRelease = DOCUMENT(@@lifecycle_collection, @active_release_key) +LET compatible = existing != null AND existing.recordType == "project_release" + AND UNSET(existing, "_key", "_id", "_rev", "recordType") == UNSET(@candidate, "_key", "recordType") +FILTER existing == null OR compatible +LET documents = APPEND(existing == null ? [@candidate] : [], activeRelease == null ? [@active_release_placeholder] : []) +LET inserted = (FOR document IN documents INSERT document INTO @@lifecycle_collection RETURN NEW) +LET saved = existing == null ? FIRST(FOR document IN inserted FILTER document.recordType == "project_release" RETURN document) : existing +RETURN UNSET(saved, "_key", "_id", "_rev", "recordType") +` + +const readActiveReleaseAQL = ` +LET pointer = DOCUMENT(@@lifecycle_collection, @key) +FILTER pointer != null AND pointer.recordType == "active_project_release" +LET release = DOCUMENT(@@lifecycle_collection, pointer.releaseKey) +FILTER release != null AND release.recordType == "project_release" +RETURN {release: UNSET(release, "_key", "_id", "_rev", "recordType"), revision: pointer.revision} +` + +const activateReleaseAQL = ` +LET manifest = DOCUMENT(@@lifecycle_collection, @manifest_key) +LET generationPointer = DOCUMENT(@@lifecycle_collection, @active_key) +LET releasePointer = DOCUMENT(@@lifecycle_collection, @active_release_key) +LET storedRelease = DOCUMENT(@@lifecycle_collection, @release_key) +LET currentRevision = releasePointer == null ? 0 : releasePointer.revision +LET alreadyActive = releasePointer != null AND releasePointer.releaseId == @release_id AND currentRevision == @expected_revision + 1 +LET selectedRevision = alreadyActive ? currentRevision : @next_revision +FILTER storedRelease != null AND storedRelease.recordType == "project_release" +FILTER storedRelease.project == @project AND storedRelease.id == @release_id AND storedRelease.generation == @generation +FILTER manifest != null AND manifest.recordType == "manifest" +FILTER manifest.dataset.project == @project AND manifest.dataset.generation == @generation +FILTER manifest.state IN [@staged_state, @ready_state] +FILTER generationPointer != null AND generationPointer.recordType == "active_generation" +FILTER releasePointer != null AND releasePointer.recordType == "active_project_release" +FILTER currentRevision == @expected_revision OR alreadyActive +LET updates = [ + {document: releasePointer, patch: {releaseKey: @release_key, releaseId: @release_id, revision: selectedRevision}}, + {document: generationPointer, patch: {dataset: manifest.dataset, manifestKey: manifest._key, releaseId: @release_id, releaseRevision: selectedRevision}} +] +FOR item IN updates + UPDATE item.document WITH item.patch IN @@lifecycle_collection OPTIONS {ignoreRevs: false, mergeObjects: false} + FILTER item.document.recordType == "active_project_release" + RETURN {release: UNSET(storedRelease, "_key", "_id", "_rev", "recordType"), revision: selectedRevision} +` + +var _ dataset.ReleaseRepository = (*Store)(nil) diff --git a/internal/dataset/arango/snapshot_release_store.go b/internal/dataset/arango/snapshot_release_store.go deleted file mode 100644 index 495c19eb..00000000 --- a/internal/dataset/arango/snapshot_release_store.go +++ /dev/null @@ -1,506 +0,0 @@ -package arango - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/calypr/loom/internal/dataset" -) - -func (s *Store) CreateOrResumeSnapshot(ctx context.Context, candidate dataset.SnapshotGeneration) (dataset.SnapshotGeneration, error) { - if err := candidate.Validate(); err != nil { - return dataset.SnapshotGeneration{}, err - } - document, err := lifecycleDocument(candidate) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - document["_key"] = snapshotDocumentKey(candidate.Dataset) - document["recordType"] = snapshotRecordType - document["project"] = candidate.Dataset.Project - binds := lifecycleCollectionBindVars() - binds["key"], binds["candidate"] = document["_key"], document - rows, err := s.snapshotRows(ctx, createOrResumeSnapshotAQL, binds) - if err != nil { - return dataset.SnapshotGeneration{}, fmt.Errorf("create or resume snapshot: %w", err) - } - if len(rows) == 0 { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotConflict - } - return rows[0], nil -} - -func (s *Store) ReadSnapshot(ctx context.Context, ref dataset.Ref) (dataset.SnapshotGeneration, error) { - if err := ref.Validate(); err != nil { - return dataset.SnapshotGeneration{}, err - } - binds := lifecycleCollectionBindVars() - binds["project"] = ref.Project - binds["key"], binds["generation"] = snapshotDocumentKey(ref), ref.Generation - rows, err := s.snapshotRows(ctx, readSnapshotAQL, binds) - if err != nil { - return dataset.SnapshotGeneration{}, fmt.Errorf("read snapshot: %w", err) - } - if len(rows) == 0 { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotNotFound - } - return rows[0], nil -} - -func (s *Store) RecordSnapshotUpload(ctx context.Context, ref dataset.Ref, upload dataset.ResourceUpload) (dataset.SnapshotGeneration, error) { - binds := lifecycleCollectionBindVars() - binds["project"] = ref.Project - binds["key"], binds["generation"], binds["upload"] = snapshotDocumentKey(ref), ref.Generation, upload - binds["loading_state"] = string(dataset.StateLoading) - rows, err := s.snapshotRows(ctx, recordSnapshotUploadAQL, binds) - if err != nil { - return dataset.SnapshotGeneration{}, fmt.Errorf("record snapshot upload: %w", err) - } - if len(rows) == 0 { - existing, readErr := s.ReadSnapshot(ctx, ref) - if readErr != nil { - return dataset.SnapshotGeneration{}, readErr - } - if prior, ok := existing.Upload(upload.ResourceType); ok && prior.SHA256 != upload.SHA256 { - return dataset.SnapshotGeneration{}, dataset.ErrChecksumConflict - } - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotConflict - } - return rows[0], nil -} - -func (s *Store) TransitionSnapshot(ctx context.Context, ref dataset.Ref, expected, next dataset.State, now time.Time) (dataset.SnapshotGeneration, error) { - binds := lifecycleCollectionBindVars() - binds["project"] = ref.Project - binds["key"], binds["generation"] = snapshotDocumentKey(ref), ref.Generation - binds["expected"], binds["next"], binds["updated_at"] = string(expected), string(next), now.UTC() - rows, err := s.snapshotRows(ctx, transitionSnapshotAQL, binds) - if err != nil { - return dataset.SnapshotGeneration{}, fmt.Errorf("transition snapshot: %w", err) - } - if len(rows) == 0 { - existing, readErr := s.ReadSnapshot(ctx, ref) - if readErr == nil && existing.State == next { - return existing, nil - } - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotConflict - } - return rows[0], nil -} - -func (s *Store) ListSnapshotProjects(ctx context.Context) ([]string, error) { - return s.projectRows(ctx, listSnapshotProjectsAQL, snapshotRecordType) -} - -func (s *Store) SaveRelease(ctx context.Context, release dataset.ProjectRelease) (dataset.ProjectRelease, error) { - document, err := lifecycleDocument(release) - if err != nil { - return dataset.ProjectRelease{}, err - } - document["_key"] = releaseDocumentKey(release.ID) - document["recordType"] = releaseRecordType - binds := lifecycleCollectionBindVars() - binds["key"], binds["candidate"] = document["_key"], document - binds["active_release_key"] = activeReleaseDocumentKey(release.Project) - binds["active_release_placeholder"] = activeReleasePlaceholderDocument(release.Project) - var saved *dataset.ProjectRelease - err = s.client.QueryRows(ctx, saveReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { - decoded, err := decodeValue[dataset.ProjectRelease](row) - if err != nil { - return err - } - saved = &decoded - return nil - }) - if err != nil { - return dataset.ProjectRelease{}, err - } - if saved == nil { - return dataset.ProjectRelease{}, dataset.ErrSnapshotConflict - } - return *saved, nil -} - -func (s *Store) ReadRelease(ctx context.Context, project, releaseID string) (dataset.ProjectRelease, error) { - binds := lifecycleCollectionBindVars() - binds["project"] = project - binds["key"], binds["release_id"] = releaseDocumentKey(releaseID), releaseID - var release *dataset.ProjectRelease - err := s.client.QueryRows(ctx, readReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { - decoded, err := decodeValue[dataset.ProjectRelease](row) - if err != nil { - return err - } - release = &decoded - return nil - }) - if err != nil { - return dataset.ProjectRelease{}, err - } - if release == nil { - return dataset.ProjectRelease{}, dataset.ErrReleaseNotFound - } - return *release, nil -} - -func (s *Store) ReadActiveRelease(ctx context.Context, project string) (dataset.ActiveRelease, error) { - binds := lifecycleCollectionBindVars() - binds["key"] = activeReleaseDocumentKey(project) - var active *dataset.ActiveRelease - err := s.client.QueryRows(ctx, readActiveReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { - decoded, err := decodeValue[dataset.ActiveRelease](row) - if err != nil { - return err - } - active = &decoded - return nil - }) - if err != nil { - return dataset.ActiveRelease{}, fmt.Errorf("read active release: %w", err) - } - if active == nil { - return dataset.ActiveRelease{}, dataset.ErrNoActiveRelease - } - return *active, nil -} - -func (s *Store) CompareAndSwapActivateRelease(ctx context.Context, release dataset.ProjectRelease, expectedRevision int64) (dataset.ActiveRelease, error) { - releaseDocument, err := lifecycleDocument(release) - if err != nil { - return dataset.ActiveRelease{}, err - } - releaseDocument["_key"] = releaseDocumentKey(release.ID) - releaseDocument["recordType"] = releaseRecordType - binds := lifecycleCollectionBindVars() - binds["project"] = release.Project - binds["release_key"] = releaseDocument["_key"] - binds["release_id"] = release.ID - binds["generation"] = release.Generation - binds["manifest_key"] = manifestDocumentKey(dataset.Ref{Project: release.Project, Generation: release.Generation}) - binds["active_key"] = activeDocumentKey(release.Project) - binds["active_release_key"] = activeReleaseDocumentKey(release.Project) - binds["expected_revision"] = expectedRevision - binds["next_revision"] = expectedRevision + 1 - binds["staged_state"] = string(dataset.StateStaged) - binds["ready_state"] = string(dataset.StateReady) - var active *dataset.ActiveRelease - err = s.client.QueryRows(ctx, activateReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { - decoded, err := decodeValue[dataset.ActiveRelease](row) - if err != nil { - return err - } - active = &decoded - return nil - }) - if err != nil { - return dataset.ActiveRelease{}, fmt.Errorf("activate release: %w", err) - } - if active == nil { - return dataset.ActiveRelease{}, dataset.ErrReleaseActivationConflict - } - return *active, nil -} - -func (s *Store) ListReleaseProjects(ctx context.Context) ([]string, error) { - return s.projectRows(ctx, listReleaseProjectsAQL, activeReleaseRecordType) -} - -func (s *Store) ListRetentionGenerations(ctx context.Context) ([]dataset.RetentionGeneration, error) { - results := make([]dataset.RetentionGeneration, 0) - binds := map[string]any{ - "@lifecycle_collection": LifecycleCollection, - "@executions_collection": "loom_dataframe_bundle_executions", - "snapshot_record_type": snapshotRecordType, - "active_record_type": activeRecordType, - "active_release_record_type": activeReleaseRecordType, - "in_flight_states": []string{"QUEUED", "RUNNING", "VALIDATING", "PENDING", "PREFLIGHT", "LOADING"}, - } - err := s.client.QueryRows(ctx, listRetentionGenerationsAQL, metadataBatchSize, binds, func(row map[string]any) error { - decoded, err := decodeValue[struct { - Dataset dataset.Ref `json:"dataset"` - State dataset.State `json:"state"` - UpdatedAt time.Time `json:"updatedAt"` - Active bool `json:"active"` - LastGood bool `json:"lastGood"` - InFlight bool `json:"inFlight"` - Recoverable bool `json:"recoverable"` - }](row) - if err != nil { - return err - } - results = append(results, dataset.RetentionGeneration{ - Dataset: decoded.Dataset, State: decoded.State, UpdatedAt: decoded.UpdatedAt, - Active: decoded.Active, LastGood: decoded.LastGood, InFlight: decoded.InFlight, Recoverable: decoded.Recoverable, - }) - return nil - }) - return results, err -} - -func (s *Store) DeleteGeneration(ctx context.Context, ref dataset.Ref) error { - snapshot, err := s.ReadSnapshot(ctx, ref) - if err != nil { - return err - } - if snapshot.State != dataset.StateFailed { - return fmt.Errorf("refuse to delete recoverable generation %s/%s in state %s", ref.Project, ref.Generation, snapshot.State) - } - targets := make([]string, 0) - if _, manifestErr := s.ReadManifest(ctx, ref); manifestErr == nil { - targets = append(targets, snapshot.ExpectedResourceTypes...) - targets = append(targets, "fhir_edge", "fhir_field_catalog", "fhir_relationship_catalog") - } else if !errors.Is(manifestErr, ErrManifestNotFound) { - return manifestErr - } - for _, target := range targets { - binds := guardedDeletionBindVars(ref) - binds["@target_collection"] = target - if err := s.client.QueryRows(ctx, guardedDeleteGenerationDataAQL, metadataBatchSize, binds, func(map[string]any) error { return nil }); err != nil { - return err - } - } - binds := guardedDeletionBindVars(ref) - binds["snapshot_key"] = snapshotDocumentKey(ref) - binds["manifest_key"] = manifestDocumentKey(ref) - if err := s.client.QueryRows(ctx, guardedDeleteGenerationMetadataAQL, metadataBatchSize, binds, func(map[string]any) error { return nil }); err != nil { - return err - } - return nil -} - -func guardedDeletionBindVars(ref dataset.Ref) map[string]any { - return map[string]any{ - "@lifecycle_collection": LifecycleCollection, - "@executions_collection": "loom_dataframe_bundle_executions", - "project": ref.Project, "generation": ref.Generation, - "active_key": activeDocumentKey(ref.Project), - "active_release_key": activeReleaseDocumentKey(ref.Project), - "failed_state": string(dataset.StateFailed), - "in_flight_states": []string{"QUEUED", "RUNNING", "VALIDATING", "PENDING", "PREFLIGHT", "LOADING"}, - } -} - -func (s *Store) snapshotRows(ctx context.Context, query string, binds map[string]any) ([]dataset.SnapshotGeneration, error) { - rows := make([]dataset.SnapshotGeneration, 0, 1) - err := s.client.QueryRows(ctx, query, metadataBatchSize, binds, func(row map[string]any) error { - decoded, err := decodeValue[dataset.SnapshotGeneration](row) - if err != nil { - return err - } - rows = append(rows, decoded) - return nil - }) - return rows, err -} - -func (s *Store) projectRows(ctx context.Context, query, recordType string) ([]string, error) { - projects := make([]string, 0) - err := s.client.QueryRows(ctx, query, metadataBatchSize, map[string]any{"@lifecycle_collection": LifecycleCollection, "record_type": recordType}, func(row map[string]any) error { - project, _ := row["project"].(string) - if project != "" { - projects = append(projects, project) - } - return nil - }) - return projects, err -} - -func lifecycleDocument(value any) (map[string]any, error) { - encoded, err := json.Marshal(value) - if err != nil { - return nil, err - } - var document map[string]any - if err := json.Unmarshal(encoded, &document); err != nil { - return nil, err - } - return document, nil -} - -func decodeValue[T any](value any) (T, error) { - var decoded T - encoded, err := json.Marshal(value) - if err != nil { - return decoded, err - } - if err := json.Unmarshal(encoded, &decoded); err != nil { - return decoded, err - } - return decoded, nil -} - -func snapshotDocumentKey(ref dataset.Ref) string { - return documentKey("snapshot", ref.Project, ref.Generation) -} -func releaseDocumentKey(id string) string { return documentKey("release", id) } -func activeReleaseDocumentKey(project string) string { return documentKey("active_release", project) } - -const createOrResumeSnapshotAQL = ` -LET existing = DOCUMENT(@@lifecycle_collection, @key) -LET compatible = existing != null - AND existing.recordType == "snapshot_generation" - AND existing.dataset == @candidate.dataset - AND existing.gitCommit == @candidate.gitCommit - AND existing.expectedResourceTypes == @candidate.expectedResourceTypes - AND existing.authResourcePath == @candidate.authResourcePath -FILTER existing == null OR compatible -LET stored = existing == null ? FIRST(INSERT @candidate INTO @@lifecycle_collection RETURN NEW) : existing -RETURN UNSET(stored, "_key", "_id", "_rev", "recordType", "project") -` - -const readSnapshotAQL = ` -FOR snapshot IN @@lifecycle_collection - FILTER snapshot._key == @key AND snapshot.recordType == "snapshot_generation" - FILTER snapshot.dataset.project == @project AND snapshot.dataset.generation == @generation - LIMIT 1 - RETURN UNSET(snapshot, "_key", "_id", "_rev", "recordType", "project") -` - -const recordSnapshotUploadAQL = ` -LET snapshot = DOCUMENT(@@lifecycle_collection, @key) -LET prior = snapshot == null ? null : FIRST(FOR upload IN snapshot.uploads FILTER upload.resourceType == @upload.resourceType RETURN upload) -LET idempotent = prior != null AND prior.sha256 == @upload.sha256 AND prior.size == @upload.size -FILTER snapshot != null AND snapshot.recordType == "snapshot_generation" -FILTER snapshot.dataset.project == @project AND snapshot.dataset.generation == @generation -FILTER @upload.resourceType IN snapshot.expectedResourceTypes -FILTER idempotent OR (snapshot.state == @loading_state AND prior == null) -LET stored = idempotent ? snapshot : FIRST(UPDATE snapshot WITH {uploads: APPEND(snapshot.uploads, @upload), updatedAt: @upload.uploadedAt} IN @@lifecycle_collection RETURN NEW) -RETURN UNSET(stored, "_key", "_id", "_rev", "recordType", "project") -` - -const transitionSnapshotAQL = ` -FOR snapshot IN @@lifecycle_collection - FILTER snapshot._key == @key AND snapshot.recordType == "snapshot_generation" - FILTER snapshot.dataset.project == @project AND snapshot.dataset.generation == @generation - FILTER snapshot.state == @expected - UPDATE snapshot WITH {state: @next, updatedAt: @updated_at, abortedAt: @next == "FAILED" ? @updated_at : null} IN @@lifecycle_collection - RETURN UNSET(NEW, "_key", "_id", "_rev", "recordType", "project") -` - -const listSnapshotProjectsAQL = `FOR doc IN @@lifecycle_collection FILTER doc.recordType == @record_type OR doc.recordType == "manifest" COLLECT project = doc.dataset.project SORT project RETURN {project}` -const listReleaseProjectsAQL = `FOR doc IN @@lifecycle_collection FILTER doc.recordType == @record_type OR doc.recordType == "project_release" COLLECT project = doc.project SORT project RETURN {project}` - -const saveReleaseAQL = ` -LET existing = DOCUMENT(@@lifecycle_collection, @key) -LET activeRelease = DOCUMENT(@@lifecycle_collection, @active_release_key) -LET compatible = existing != null AND existing.recordType == "project_release" - AND UNSET(existing, "_key", "_id", "_rev", "recordType") == UNSET(@candidate, "_key", "recordType") -FILTER existing == null OR compatible -LET documents = APPEND(existing == null ? [@candidate] : [], activeRelease == null ? [@active_release_placeholder] : []) -LET inserted = (FOR document IN documents INSERT document INTO @@lifecycle_collection RETURN NEW) -LET saved = existing == null ? FIRST(FOR document IN inserted FILTER document.recordType == "project_release" RETURN document) : existing -RETURN UNSET(saved, "_key", "_id", "_rev", "recordType") -` - -const readReleaseAQL = ` -FOR release IN @@lifecycle_collection - FILTER release._key == @key AND release.recordType == "project_release" - FILTER release.project == @project AND release.id == @release_id - LIMIT 1 - RETURN UNSET(release, "_key", "_id", "_rev", "recordType") -` - -const listRetentionGenerationsAQL = ` -FOR snapshot IN @@lifecycle_collection - FILTER snapshot.recordType == @snapshot_record_type - LET activeGeneration = FIRST(FOR pointer IN @@lifecycle_collection FILTER pointer.recordType == @active_record_type AND pointer.project == snapshot.dataset.project RETURN pointer.dataset.generation) - LET activeReleasePointer = FIRST(FOR pointer IN @@lifecycle_collection FILTER pointer.recordType == @active_release_record_type AND pointer.project == snapshot.dataset.project RETURN pointer) - LET activeRelease = activeReleasePointer == null ? null : DOCUMENT(@@lifecycle_collection, activeReleasePointer.releaseKey) - LET lastGood = activeRelease != null AND (activeRelease.generation == snapshot.dataset.generation OR snapshot.dataset.generation IN activeRelease.publications[*].generation) - LET inFlight = FIRST(FOR execution IN @@executions_collection - LET project = execution.project != null ? execution.project : execution.Project - LET generation = execution.datasetGeneration != null ? execution.datasetGeneration : execution.DatasetGeneration - FILTER project == snapshot.dataset.project AND generation == snapshot.dataset.generation - FILTER execution.state IN @in_flight_states LIMIT 1 RETURN true) - RETURN { - dataset: snapshot.dataset, state: snapshot.state, updatedAt: snapshot.updatedAt, - active: activeGeneration == snapshot.dataset.generation, - lastGood, inFlight: inFlight == true, - recoverable: snapshot.state IN ["LOADING", "STAGED", "READY"] - } -` - -const guardedDeleteGenerationDataAQL = ` -LET snapshot = FIRST(FOR doc IN @@lifecycle_collection FILTER doc.recordType == "snapshot_generation" AND doc.dataset.project == @project AND doc.dataset.generation == @generation RETURN doc) -LET active = DOCUMENT(@@lifecycle_collection, @active_key) -LET releasePointer = DOCUMENT(@@lifecycle_collection, @active_release_key) -LET release = releasePointer == null ? null : DOCUMENT(@@lifecycle_collection, releasePointer.releaseKey) -LET protectedByRelease = release != null AND (release.generation == @generation OR @generation IN release.publications[*].generation) -LET inFlight = FIRST(FOR execution IN @@executions_collection - LET project = execution.project != null ? execution.project : execution.Project - LET generation = execution.datasetGeneration != null ? execution.datasetGeneration : execution.DatasetGeneration - FILTER project == @project AND generation == @generation AND execution.state IN @in_flight_states LIMIT 1 RETURN true) -FILTER snapshot != null AND snapshot.state == @failed_state -FILTER active == null OR active.dataset.generation != @generation -FILTER !protectedByRelease AND inFlight == null -FOR document IN @@target_collection - FILTER document.project == @project AND document.dataset_generation == @generation - REMOVE document IN @@target_collection - COLLECT WITH COUNT INTO removed - RETURN {removed} -` - -const guardedDeleteGenerationMetadataAQL = ` -LET snapshot = DOCUMENT(@@lifecycle_collection, @snapshot_key) -LET manifest = DOCUMENT(@@lifecycle_collection, @manifest_key) -LET active = DOCUMENT(@@lifecycle_collection, @active_key) -LET releasePointer = DOCUMENT(@@lifecycle_collection, @active_release_key) -LET release = releasePointer == null ? null : DOCUMENT(@@lifecycle_collection, releasePointer.releaseKey) -LET protectedByRelease = release != null AND (release.generation == @generation OR @generation IN release.publications[*].generation) -LET inFlight = FIRST(FOR execution IN @@executions_collection - LET project = execution.project != null ? execution.project : execution.Project - LET generation = execution.datasetGeneration != null ? execution.datasetGeneration : execution.DatasetGeneration - FILTER project == @project AND generation == @generation AND execution.state IN @in_flight_states LIMIT 1 RETURN true) -FILTER snapshot != null AND snapshot.state == @failed_state -FILTER active == null OR active.dataset.generation != @generation -FILTER !protectedByRelease AND inFlight == null -LET documents = APPEND([snapshot], manifest != null AND manifest.state == "FAILED" ? [manifest] : []) -FOR document IN documents - REMOVE document IN @@lifecycle_collection OPTIONS {ignoreErrors: true} - COLLECT WITH COUNT INTO removed - RETURN {removed} -` - -const readActiveReleaseAQL = ` -LET pointer = DOCUMENT(@@lifecycle_collection, @key) -FILTER pointer != null AND pointer.recordType == "active_project_release" -LET release = DOCUMENT(@@lifecycle_collection, pointer.releaseKey) -FILTER release != null AND release.recordType == "project_release" -RETURN {release: UNSET(release, "_key", "_id", "_rev", "recordType"), revision: pointer.revision} -` - -// Both visibility pointers are updated in the same AQL transaction. A release -// therefore cannot expose new graph data without the matching dataframe set, -// or expose dataframe metadata while graph reads still use the prior generation. -const activateReleaseAQL = ` -LET manifest = DOCUMENT(@@lifecycle_collection, @manifest_key) -LET generationPointer = DOCUMENT(@@lifecycle_collection, @active_key) -LET releasePointer = DOCUMENT(@@lifecycle_collection, @active_release_key) -LET storedRelease = DOCUMENT(@@lifecycle_collection, @release_key) -LET currentRevision = releasePointer == null ? 0 : releasePointer.revision -LET alreadyActive = releasePointer != null AND releasePointer.releaseId == @release_id AND currentRevision == @expected_revision + 1 -LET selectedRevision = alreadyActive ? currentRevision : @next_revision -FILTER storedRelease != null AND storedRelease.recordType == "project_release" -FILTER storedRelease.project == @project AND storedRelease.id == @release_id AND storedRelease.generation == @generation -FILTER manifest != null AND manifest.recordType == "manifest" -FILTER manifest.dataset.project == @project AND manifest.dataset.generation == @generation -FILTER manifest.state IN [@staged_state, @ready_state] -FILTER generationPointer != null AND generationPointer.recordType == "active_generation" -FILTER releasePointer != null AND releasePointer.recordType == "active_project_release" -FILTER currentRevision == @expected_revision OR alreadyActive -LET updates = [ - {document: releasePointer, patch: {releaseKey: @release_key, releaseId: @release_id, revision: selectedRevision}}, - {document: generationPointer, patch: {dataset: manifest.dataset, manifestKey: manifest._key, releaseId: @release_id, releaseRevision: selectedRevision}} -] -FOR item IN updates - UPDATE item.document WITH item.patch IN @@lifecycle_collection OPTIONS {ignoreRevs: false, mergeObjects: false} - FILTER item.document.recordType == "active_project_release" - RETURN {release: UNSET(storedRelease, "_key", "_id", "_rev", "recordType"), revision: selectedRevision} -` - -var _ dataset.SnapshotRepository = (*Store)(nil) -var _ dataset.ReleaseRepository = (*Store)(nil) -var _ dataset.RetentionRepository = (*Store)(nil) diff --git a/internal/dataset/arango/store_test.go b/internal/dataset/arango/store_test.go index 057f36db..3c10e16c 100644 --- a/internal/dataset/arango/store_test.go +++ b/internal/dataset/arango/store_test.go @@ -150,26 +150,6 @@ func TestNewAndMissingActive(t *testing.T) { } } -func TestSnapshotPersistenceIsIdempotentAndChecksumQualified(t *testing.T) { - now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC) - snapshot, err := publication.NewSnapshotGeneration("project-a", "commit-a", "", []string{"Observation", "Patient"}, now) - if err != nil { - t.Fatal(err) - } - fake := &fakeQueryClient{responses: [][]map[string]any{{jsonObject(t, snapshot)}}} - store := mustStore(t, fake) - created, err := store.CreateOrResumeSnapshot(context.Background(), snapshot) - if err != nil || !reflect.DeepEqual(created, snapshot) { - t.Fatalf("CreateOrResumeSnapshot = %#v, %v", created, err) - } - call := fake.onlyCall(t) - for _, required := range []string{"existing.expectedResourceTypes == @candidate.expectedResourceTypes", "existing.authResourcePath == @candidate.authResourcePath", "existing.gitCommit == @candidate.gitCommit"} { - if !strings.Contains(call.query, required) { - t.Fatalf("create/resume query missing %q:\n%s", required, call.query) - } - } -} - func TestReleaseActivationAtomicallyUpdatesReleaseAndGenerationPointers(t *testing.T) { now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC) release := publication.ProjectRelease{ID: "release-a", Project: "project-a", GitCommit: "commit-a", Generation: "commit-a", CreatedAt: now} @@ -226,15 +206,6 @@ func TestReleaseActivationEmptyCASResultIsConflict(t *testing.T) { } } -func TestListRetentionGenerationsPassesOnlyDeclaredBindVariables(t *testing.T) { - fake := &fakeQueryClient{} - store := mustStore(t, fake) - if _, err := store.ListRetentionGenerations(context.Background()); err != nil { - t.Fatal(err) - } - assertOnlyDeclaredBindVariables(t, fake.onlyCall(t)) -} - func TestReadActiveReleasePassesOnlyDeclaredBindVariables(t *testing.T) { fake := &fakeQueryClient{} store := mustStore(t, fake) @@ -277,7 +248,7 @@ func fixtureManifest(t *testing.T, project, id string, state publication.State) return manifest } -func mustStore(t *testing.T, client QueryRowsClient) *Store { +func mustStore(t *testing.T, client arangostore.RowQueryer) *Store { t.Helper() store, err := New(client) if err != nil { diff --git a/internal/dataset/dataframe_selector.go b/internal/dataset/dataframe_selector.go index 9a36f347..2e780743 100644 --- a/internal/dataset/dataframe_selector.go +++ b/internal/dataset/dataframe_selector.go @@ -6,7 +6,7 @@ import ( ) // DataframeSelector is the exact, versioned identity of one recipe output. -// It is shared by publication, release activation, federation, GraphQL and ETL. +// It is shared by publication, release activation, GraphQL, and ETL. type DataframeSelector struct { Recipe string `json:"recipe"` TranslationVersion string `json:"translationVersion"` diff --git a/internal/dataset/inventory.go b/internal/dataset/inventory.go deleted file mode 100644 index 47d722e0..00000000 --- a/internal/dataset/inventory.go +++ /dev/null @@ -1,71 +0,0 @@ -package dataset - -import ( - "context" - "sort" -) - -type ExecutionProjectSource interface { - ListExecutionProjects(context.Context) ([]string, error) -} - -type ProjectInventory struct { - Snapshots SnapshotRepository - Releases ReleaseRepository - Executions ExecutionProjectSource -} - -func sortedSet(values map[string]struct{}) []string { - result := make([]string, 0, len(values)) - for value := range values { - result = append(result, value) - } - sort.Strings(result) - return result -} - -// ExpectedProjects returns only caller-authorized identities in auth mode. In -// no-auth development mode it exposes every project observed through snapshot -// sessions, releases, or publication attempts, including projects that never -// reached successful publication. -func (i ProjectInventory) ExpectedProjects(ctx context.Context, authorized []string, noAuth bool) ([]string, error) { - if !noAuth { - set := make(map[string]struct{}, len(authorized)) - for _, project := range authorized { - if project != "" { - set[project] = struct{}{} - } - } - return sortedSet(set), nil - } - set := make(map[string]struct{}) - collect := func(values []string, err error) error { - if err != nil { - return err - } - for _, value := range values { - if value != "" { - set[value] = struct{}{} - } - } - return nil - } - if i.Snapshots != nil { - if err := collect(i.Snapshots.ListSnapshotProjects(ctx)); err != nil { - return nil, err - } - } - if i.Releases != nil { - if err := collect(i.Releases.ListReleaseProjects(ctx)); err != nil { - return nil, err - } - } - if i.Executions != nil { - if err := collect(i.Executions.ListExecutionProjects(ctx)); err != nil { - return nil, err - } - } - result := sortedSet(set) - sort.Strings(result) - return result, nil -} diff --git a/internal/dataset/memory_store_test.go b/internal/dataset/memory_store_test.go index 170d9d70..b6c36db7 100644 --- a/internal/dataset/memory_store_test.go +++ b/internal/dataset/memory_store_test.go @@ -4,21 +4,28 @@ import ( "context" "fmt" "reflect" - "sort" "sync" - "time" ) -// MemoryLifecycleStore is a concurrency-correct test implementation. type MemoryLifecycleStore struct { mu sync.RWMutex - snapshots map[Ref]SnapshotGeneration + manifests map[Ref]Manifest releases map[string]ActiveRelease releaseRecords map[string]ProjectRelease } func NewMemoryLifecycleStore() *MemoryLifecycleStore { - return &MemoryLifecycleStore{snapshots: make(map[Ref]SnapshotGeneration), releases: make(map[string]ActiveRelease), releaseRecords: make(map[string]ProjectRelease)} + return &MemoryLifecycleStore{manifests: make(map[Ref]Manifest), releases: make(map[string]ActiveRelease), releaseRecords: make(map[string]ProjectRelease)} +} + +func (s *MemoryLifecycleStore) ReadManifest(_ context.Context, ref Ref) (Manifest, error) { + s.mu.RLock() + defer s.mu.RUnlock() + manifest, ok := s.manifests[ref] + if !ok { + return Manifest{}, ErrManifestNotFound + } + return manifest, nil } func (s *MemoryLifecycleStore) SaveRelease(_ context.Context, release ProjectRelease) (ProjectRelease, error) { @@ -27,7 +34,7 @@ func (s *MemoryLifecycleStore) SaveRelease(_ context.Context, release ProjectRel key := release.Project + "\x00" + release.ID if existing, ok := s.releaseRecords[key]; ok { if !reflect.DeepEqual(existing, release) { - return ProjectRelease{}, ErrSnapshotConflict + return ProjectRelease{}, fmt.Errorf("release already exists with different content") } return cloneRelease(existing), nil } @@ -35,111 +42,6 @@ func (s *MemoryLifecycleStore) SaveRelease(_ context.Context, release ProjectRel return cloneRelease(release), nil } -func (s *MemoryLifecycleStore) ReadRelease(_ context.Context, project, releaseID string) (ProjectRelease, error) { - s.mu.RLock() - defer s.mu.RUnlock() - release, ok := s.releaseRecords[project+"\x00"+releaseID] - if !ok { - return ProjectRelease{}, ErrReleaseNotFound - } - return cloneRelease(release), nil -} - -func (s *MemoryLifecycleStore) CreateOrResumeSnapshot(_ context.Context, candidate SnapshotGeneration) (SnapshotGeneration, error) { - if err := candidate.Validate(); err != nil { - return SnapshotGeneration{}, err - } - s.mu.Lock() - defer s.mu.Unlock() - if existing, ok := s.snapshots[candidate.Dataset]; ok { - if existing.GitCommit != candidate.GitCommit || existing.AuthResourcePath != candidate.AuthResourcePath || !reflect.DeepEqual(existing.ExpectedResourceTypes, candidate.ExpectedResourceTypes) { - return SnapshotGeneration{}, ErrSnapshotConflict - } - return cloneSnapshot(existing), nil - } - s.snapshots[candidate.Dataset] = cloneSnapshot(candidate) - return cloneSnapshot(candidate), nil -} - -func (s *MemoryLifecycleStore) ReadSnapshot(_ context.Context, ref Ref) (SnapshotGeneration, error) { - s.mu.RLock() - defer s.mu.RUnlock() - snapshot, ok := s.snapshots[ref] - if !ok { - return SnapshotGeneration{}, ErrSnapshotNotFound - } - return cloneSnapshot(snapshot), nil -} - -func (s *MemoryLifecycleStore) RecordSnapshotUpload(_ context.Context, ref Ref, upload ResourceUpload) (SnapshotGeneration, error) { - s.mu.Lock() - defer s.mu.Unlock() - snapshot, ok := s.snapshots[ref] - if !ok { - return SnapshotGeneration{}, ErrSnapshotNotFound - } - if snapshot.State != StateLoading { - if prior, found := snapshot.Upload(upload.ResourceType); found && prior.SHA256 == upload.SHA256 && prior.Size == upload.Size { - return cloneSnapshot(snapshot), nil - } - return SnapshotGeneration{}, ErrSnapshotFinalized - } - if !contains(snapshot.ExpectedResourceTypes, upload.ResourceType) { - return SnapshotGeneration{}, fmt.Errorf("%w: resource type %q was not declared", ErrSnapshotConflict, upload.ResourceType) - } - for i, prior := range snapshot.Uploads { - if prior.ResourceType != upload.ResourceType { - continue - } - if prior.SHA256 != upload.SHA256 || prior.Size != upload.Size { - return SnapshotGeneration{}, ErrChecksumConflict - } - snapshot.Uploads[i] = prior - return cloneSnapshot(snapshot), nil - } - snapshot.Uploads = append(snapshot.Uploads, upload) - sort.Slice(snapshot.Uploads, func(i, j int) bool { return snapshot.Uploads[i].ResourceType < snapshot.Uploads[j].ResourceType }) - snapshot.UpdatedAt = upload.UploadedAt - s.snapshots[ref] = snapshot - return cloneSnapshot(snapshot), nil -} - -func (s *MemoryLifecycleStore) TransitionSnapshot(_ context.Context, ref Ref, expected, next State, now time.Time) (SnapshotGeneration, error) { - s.mu.Lock() - defer s.mu.Unlock() - snapshot, ok := s.snapshots[ref] - if !ok { - return SnapshotGeneration{}, ErrSnapshotNotFound - } - if snapshot.State == next { - return cloneSnapshot(snapshot), nil - } - if snapshot.State != expected || expected != StateLoading || (next != StateStaged && next != StateFailed) { - return SnapshotGeneration{}, fmt.Errorf("%w: %s -> %s", ErrSnapshotConflict, snapshot.State, next) - } - if next == StateStaged && len(snapshot.MissingResourceTypes()) != 0 { - return SnapshotGeneration{}, ErrGenerationIncomplete - } - snapshot.State = next - snapshot.UpdatedAt = now.UTC() - if next == StateFailed { - aborted := now.UTC() - snapshot.AbortedAt = &aborted - } - s.snapshots[ref] = snapshot - return cloneSnapshot(snapshot), nil -} - -func (s *MemoryLifecycleStore) ListSnapshotProjects(context.Context) ([]string, error) { - s.mu.RLock() - defer s.mu.RUnlock() - set := make(map[string]struct{}) - for ref := range s.snapshots { - set[ref.Project] = struct{}{} - } - return sortedSet(set), nil -} - func (s *MemoryLifecycleStore) ReadActiveRelease(_ context.Context, project string) (ActiveRelease, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -168,26 +70,6 @@ func (s *MemoryLifecycleStore) CompareAndSwapActivateRelease(_ context.Context, return cloneActiveRelease(active), nil } -func (s *MemoryLifecycleStore) ListReleaseProjects(context.Context) ([]string, error) { - s.mu.RLock() - defer s.mu.RUnlock() - set := make(map[string]struct{}, len(s.releases)) - for project := range s.releases { - set[project] = struct{}{} - } - return sortedSet(set), nil -} - -func cloneSnapshot(value SnapshotGeneration) SnapshotGeneration { - value.ExpectedResourceTypes = append([]string(nil), value.ExpectedResourceTypes...) - value.Uploads = append([]ResourceUpload(nil), value.Uploads...) - if value.AbortedAt != nil { - copy := *value.AbortedAt - value.AbortedAt = © - } - return value -} - func cloneRelease(value ProjectRelease) ProjectRelease { value.Publications = append([]ReleasePublication(nil), value.Publications...) value.RequiredVerifications = append([]ContractVerification(nil), value.RequiredVerifications...) @@ -199,14 +81,5 @@ func cloneActiveRelease(value ActiveRelease) ActiveRelease { return value } -func contains(values []string, target string) bool { - for _, value := range values { - if value == target { - return true - } - } - return false -} - -var _ SnapshotRepository = (*MemoryLifecycleStore)(nil) +var _ ManifestReader = (*MemoryLifecycleStore)(nil) var _ ReleaseRepository = (*MemoryLifecycleStore)(nil) diff --git a/internal/dataset/release.go b/internal/dataset/release.go index 42d1249a..553e76f8 100644 --- a/internal/dataset/release.go +++ b/internal/dataset/release.go @@ -13,7 +13,6 @@ import ( var ( ErrNoActiveRelease = errors.New("no active project release") - ErrReleaseNotFound = errors.New("project release was not found") ErrReleaseRequirementsUnmet = errors.New("project release requirements are unmet") ErrReleaseActivationConflict = errors.New("project release activation compare-and-swap conflict") ) @@ -91,17 +90,11 @@ type ManifestReader interface { type ReleaseRepository interface { SaveRelease(context.Context, ProjectRelease) (ProjectRelease, error) - ReadRelease(context.Context, string, string) (ProjectRelease, error) ReadActiveRelease(context.Context, string) (ActiveRelease, error) CompareAndSwapActivateRelease(context.Context, ProjectRelease, int64) (ActiveRelease, error) - ListReleaseProjects(context.Context) ([]string, error) } type ReleaseService struct { - Snapshots SnapshotRepository - // Manifests provides compatibility for valid generations created before - // upload-tracking snapshot records were introduced. Release activation is - // already guarded by this same immutable staged manifest. Manifests ManifestReader Releases ReleaseRepository Verifier PublicationVerifier @@ -114,8 +107,8 @@ type ReleaseService struct { // release can use this as a preflight and avoid producing orphaned dataframe // publications for a generation that cannot be activated. func (s ReleaseService) ValidateGeneration(ctx context.Context, project, generation string) error { - if s.Snapshots == nil { - return fmt.Errorf("snapshot repository is required") + if s.Manifests == nil { + return fmt.Errorf("manifest reader is required") } ref, err := NewRef(strings.TrimSpace(project), strings.TrimSpace(generation)) if err != nil { @@ -125,22 +118,8 @@ func (s ReleaseService) ValidateGeneration(ctx context.Context, project, generat } func (s ReleaseService) validateGeneration(ctx context.Context, ref Ref) error { - snapshot, err := s.Snapshots.ReadSnapshot(ctx, ref) - if err == nil { - if snapshot.State != StateStaged { - return fmt.Errorf("%w: generation %s is %s", ErrReleaseRequirementsUnmet, ref.Generation, snapshot.State) - } - return nil - } - if !errors.Is(err, ErrSnapshotNotFound) || s.Manifests == nil { - return err - } - // Legacy loads can have the durable manifest and active graph pointer but no - // snapshot_generation upload-audit document. The manifest is the exact - // generation record checked again by the atomic activation transaction, so a - // staged legacy manifest is sufficient and does not need fabricated uploads. - manifest, manifestErr := s.Manifests.ReadManifest(ctx, ref) - if manifestErr != nil { + manifest, err := s.Manifests.ReadManifest(ctx, ref) + if err != nil { return err } if !manifest.IsStaged() { @@ -150,7 +129,7 @@ func (s ReleaseService) validateGeneration(ctx context.Context, ref Ref) error { } func (s ReleaseService) Create(ctx context.Context, request ActivationRequest) (ProjectRelease, error) { - if s.Snapshots == nil || s.Releases == nil || s.Verifier == nil { + if s.Manifests == nil || s.Releases == nil || s.Verifier == nil { return ProjectRelease{}, fmt.Errorf("release service dependencies are required") } ref, err := NewRef(strings.TrimSpace(request.Project), strings.TrimSpace(request.Generation)) @@ -248,17 +227,6 @@ func (s ReleaseService) Activate(ctx context.Context, request ActivationRequest) return s.Releases.CompareAndSwapActivateRelease(ctx, release, request.ExpectedRevision) } -func (s ReleaseService) ActivateExisting(ctx context.Context, project, releaseID string, expectedRevision int64) (ActiveRelease, error) { - if s.Releases == nil { - return ActiveRelease{}, fmt.Errorf("release repository is required") - } - release, err := s.Releases.ReadRelease(ctx, project, releaseID) - if err != nil { - return ActiveRelease{}, err - } - return s.Releases.CompareAndSwapActivateRelease(ctx, release, expectedRevision) -} - func (s ReleaseService) Active(ctx context.Context, project string) (ActiveRelease, error) { if s.Releases == nil { return ActiveRelease{}, fmt.Errorf("release repository is required") diff --git a/internal/dataset/release_test.go b/internal/dataset/release_test.go index 4b7ef995..b07c07fc 100644 --- a/internal/dataset/release_test.go +++ b/internal/dataset/release_test.go @@ -3,7 +3,6 @@ package dataset import ( "context" "errors" - "reflect" "sync" "testing" "time" @@ -34,12 +33,12 @@ func TestReleaseActivationRequiresPublishedQueryableGenerationAndPreservesPointe ctx := context.Background() store := NewMemoryLifecycleStore() now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC) - stageSnapshot(t, store, "project-a", "commit-a", now) + stageManifest(t, store, "project-a", "commit-a") selector := DataframeSelector{Recipe: "core", TranslationVersion: "v1", Output: "Patient"} verifier := &verificationFixture{results: map[string]PublicationVerification{ selector.Key(): {Selector: selector, ExecutionID: "execution-a", Generation: "commit-a", State: "FAILED", Queryable: false}, }, errors: map[string]error{}} - service := ReleaseService{Snapshots: store, Releases: store, Verifier: verifier, Required: []DataframeSelector{selector}, Now: func() time.Time { return now }} + service := ReleaseService{Manifests: store, Releases: store, Verifier: verifier, Required: []DataframeSelector{selector}, Now: func() time.Time { return now }} if _, err := service.Activate(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a", ExpectedRevision: 0}); !errors.Is(err, ErrReleaseRequirementsUnmet) { t.Fatalf("failed requirement = %v", err) } @@ -49,41 +48,36 @@ func TestReleaseActivationRequiresPublishedQueryableGenerationAndPreservesPointe verification := verifier.results[selector.Key()] verification.State, verification.Queryable, verification.VerifiedAt = "PUBLISHED", true, now verifier.results[selector.Key()] = verification - release, err := service.Create(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a"}) - if err != nil { + if _, err := service.Create(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a"}); err != nil { t.Fatal(err) } if _, err := store.ReadActiveRelease(ctx, "project-a"); !errors.Is(err, ErrNoActiveRelease) { t.Fatalf("release creation moved pointer: %v", err) } - active, err := service.ActivateExisting(ctx, "project-a", release.ID, 0) + active, err := service.Activate(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a", ExpectedRevision: 0}) if err != nil || active.Revision != 1 || active.Release.Generation != "commit-a" || len(active.Release.Publications) != 1 || active.Release.Publications[0].Stale { t.Fatalf("activation = %#v, %v", active, err) } } -func TestValidateGenerationRequiresExistingStagedSnapshot(t *testing.T) { +func TestValidateGenerationRequiresExistingStagedManifest(t *testing.T) { ctx := context.Background() store := NewMemoryLifecycleStore() - service := ReleaseService{Snapshots: store} + service := ReleaseService{Manifests: store} - if err := service.ValidateGeneration(ctx, "project-a", "missing"); !errors.Is(err, ErrSnapshotNotFound) { + if err := service.ValidateGeneration(ctx, "project-a", "missing"); !errors.Is(err, ErrManifestNotFound) { t.Fatalf("missing generation validation = %v", err) } - now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC) - snapshot, err := NewSnapshotGeneration("project-a", "loading", "", []string{"Patient"}, now) - if err != nil { - t.Fatal(err) - } - if _, err := store.CreateOrResumeSnapshot(ctx, snapshot); err != nil { - t.Fatal(err) - } + ref, _ := NewRef("project-a", "loading") + schema, _ := NewSchemaSnapshot("schema-a", "R4", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", []string{"Patient"}) + loading, _ := NewManifest(ref, schema) + store.manifests[ref] = loading if err := service.ValidateGeneration(ctx, "project-a", "loading"); !errors.Is(err, ErrReleaseRequirementsUnmet) { t.Fatalf("loading generation validation = %v", err) } - stageSnapshot(t, store, "project-a", "staged", now) + stageManifest(t, store, "project-a", "staged") if err := service.ValidateGeneration(ctx, "project-a", "staged"); err != nil { t.Fatalf("staged generation validation = %v", err) } @@ -112,7 +106,7 @@ func TestValidateGenerationAcceptsLegacyStagedManifestWithoutSnapshot(t *testing verifier := &verificationFixture{results: map[string]PublicationVerification{ selector.Key(): published(selector, "execution-a", ref.Generation, time.Now().UTC()), }, errors: map[string]error{}} - service := ReleaseService{Snapshots: store, Manifests: manifestReaderFixture{manifest: manifest}, Releases: store, Verifier: verifier, Required: []DataframeSelector{selector}} + service := ReleaseService{Manifests: manifestReaderFixture{manifest: manifest}, Releases: store, Verifier: verifier, Required: []DataframeSelector{selector}} if err := service.ValidateGeneration(ctx, ref.Project, ref.Generation); err != nil { t.Fatalf("legacy staged manifest validation = %v", err) } @@ -123,7 +117,6 @@ func TestValidateGenerationAcceptsLegacyStagedManifestWithoutSnapshot(t *testing func TestValidateGenerationRejectsLegacyLoadingManifestWithoutSnapshot(t *testing.T) { ctx := context.Background() - store := NewMemoryLifecycleStore() ref, err := NewRef("project-a", "legacy-loading") if err != nil { t.Fatal(err) @@ -136,7 +129,7 @@ func TestValidateGenerationRejectsLegacyLoadingManifestWithoutSnapshot(t *testin if err != nil { t.Fatal(err) } - service := ReleaseService{Snapshots: store, Manifests: manifestReaderFixture{manifest: manifest}} + service := ReleaseService{Manifests: manifestReaderFixture{manifest: manifest}} if err := service.ValidateGeneration(ctx, ref.Project, ref.Generation); !errors.Is(err, ErrReleaseRequirementsUnmet) { t.Fatalf("legacy loading manifest validation = %v", err) } @@ -149,9 +142,9 @@ func TestReleaseActivationCarriesOptionalPublicationAsStale(t *testing.T) { required := DataframeSelector{Recipe: "core", TranslationVersion: "v1", Output: "Patient"} optional := DataframeSelector{Recipe: "optional", TranslationVersion: "v1", Output: "Observation"} verifier := &verificationFixture{results: map[string]PublicationVerification{}, errors: map[string]error{}} - service := ReleaseService{Snapshots: store, Releases: store, Verifier: verifier, Required: []DataframeSelector{required}, Now: func() time.Time { return now }} + service := ReleaseService{Manifests: store, Releases: store, Verifier: verifier, Required: []DataframeSelector{required}, Now: func() time.Time { return now }} - stageSnapshot(t, store, "project-a", "commit-a", now) + stageManifest(t, store, "project-a", "commit-a") verifier.results[required.Key()] = published(required, "required-a", "commit-a", now) verifier.results[optional.Key()] = published(optional, "optional-a", "commit-a", now) first, err := service.Activate(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a", ExpectedRevision: 0, OptionalSelectors: []DataframeSelector{optional}}) @@ -159,7 +152,7 @@ func TestReleaseActivationCarriesOptionalPublicationAsStale(t *testing.T) { t.Fatalf("first activation = %#v, %v", first, err) } - stageSnapshot(t, store, "project-a", "commit-b", now.Add(time.Hour)) + stageManifest(t, store, "project-a", "commit-b") verifier.results[required.Key()] = published(required, "required-b", "commit-b", now.Add(time.Hour)) second, err := service.Activate(ctx, ActivationRequest{Project: "project-a", Generation: "commit-b", ExpectedRevision: 1}) if err != nil { @@ -178,14 +171,14 @@ func TestReleaseActivationAddsPublicationWithoutDroppingSameGenerationPublicatio ctx := context.Background() store := NewMemoryLifecycleStore() now := time.Date(2026, 8, 18, 12, 0, 0, 0, time.UTC) - stageSnapshot(t, store, "project-a", "commit-a", now) + stageManifest(t, store, "project-a", "commit-a") firstSelector := DataframeSelector{Recipe: "explorer-one", TranslationVersion: "v1", Output: "Patient"} secondSelector := DataframeSelector{Recipe: "explorer-two", TranslationVersion: "v1", Output: "Observation"} verifier := &verificationFixture{results: map[string]PublicationVerification{ firstSelector.Key(): published(firstSelector, "execution-a", "commit-a", now), secondSelector.Key(): published(secondSelector, "execution-b", "commit-a", now), }, errors: map[string]error{}} - service := ReleaseService{Snapshots: store, Releases: store, Verifier: verifier, Now: func() time.Time { return now }} + service := ReleaseService{Manifests: store, Releases: store, Verifier: verifier, Now: func() time.Time { return now }} first, err := service.Activate(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a", ExpectedRevision: 0, OptionalSelectors: []DataframeSelector{firstSelector}}) if err != nil { @@ -207,105 +200,26 @@ func TestReleaseActivationAddsPublicationWithoutDroppingSameGenerationPublicatio } } -func TestConcurrentReleaseActivationUsesCompareAndSwap(t *testing.T) { - ctx := context.Background() - store := NewMemoryLifecycleStore() - now := time.Now().UTC() - stageSnapshot(t, store, "project-a", "commit-a", now) - selector := DataframeSelector{Recipe: "core", TranslationVersion: "v1", Output: "Patient"} - optional := DataframeSelector{Recipe: "core", TranslationVersion: "v1", Output: "Observation"} - verifier := &verificationFixture{results: map[string]PublicationVerification{ - selector.Key(): published(selector, "execution-a", "commit-a", now), - optional.Key(): published(optional, "execution-b", "commit-a", now), - }, errors: map[string]error{}} - service := ReleaseService{Snapshots: store, Releases: store, Verifier: verifier, Required: []DataframeSelector{selector}, Now: func() time.Time { return now }} - first, err := service.Create(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a"}) +func stageManifest(t *testing.T, store *MemoryLifecycleStore, project, generation string) { + t.Helper() + ref, err := NewRef(project, generation) if err != nil { t.Fatal(err) } - second, err := service.Create(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a", OptionalSelectors: []DataframeSelector{optional}}) + schema, err := NewSchemaSnapshot("schema-a", "R4", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", []string{"Patient"}) if err != nil { t.Fatal(err) } - if first.ID == second.ID { - t.Fatal("distinct release contents produced the same ID") - } - - start := make(chan struct{}) - errorsSeen := make(chan error, 2) - for _, releaseID := range []string{first.ID, second.ID} { - go func(releaseID string) { - <-start - _, err := service.ActivateExisting(ctx, "project-a", releaseID, 0) - errorsSeen <- err - }(releaseID) - } - close(start) - results := []error{<-errorsSeen, <-errorsSeen} - var success, conflict int - for _, err := range results { - if err == nil { - success++ - } else if errors.Is(err, ErrReleaseActivationConflict) { - conflict++ - } - } - if success != 1 || conflict != 1 { - t.Fatalf("activation results = %#v", results) - } -} - -func TestActivationRetryOfSameReleaseIsIdempotent(t *testing.T) { - ctx := context.Background() - store := NewMemoryLifecycleStore() - now := time.Now().UTC() - stageSnapshot(t, store, "project-a", "commit-a", now) - selector := DataframeSelector{Recipe: "core", TranslationVersion: "v1", Output: "Patient"} - verifier := &verificationFixture{results: map[string]PublicationVerification{selector.Key(): published(selector, "execution-a", "commit-a", now)}, errors: map[string]error{}} - service := ReleaseService{Snapshots: store, Releases: store, Verifier: verifier, Required: []DataframeSelector{selector}, Now: func() time.Time { return now }} - release, err := service.Create(ctx, ActivationRequest{Project: "project-a", Generation: "commit-a"}) - if err != nil { - t.Fatal(err) + manifest, err := NewManifest(ref, schema) + if err == nil { + manifest, err = manifest.Transition(StateReady) } - first, err := service.ActivateExisting(ctx, "project-a", release.ID, 0) if err != nil { t.Fatal(err) } - retry, err := service.ActivateExisting(ctx, "project-a", release.ID, 0) - if err != nil || !reflect.DeepEqual(retry, first) { - t.Fatalf("activation retry = %#v, %v; want %#v", retry, err, first) - } -} - -func TestProjectInventoryFiltersAuthorizedAndIncludesObservedInNoAuth(t *testing.T) { - store := NewMemoryLifecycleStore() - now := time.Now().UTC() - snapshot, _ := NewSnapshotGeneration("observed", "commit", "", []string{"Patient"}, now) - _, _ = store.CreateOrResumeSnapshot(context.Background(), snapshot) - inventory := ProjectInventory{Snapshots: store, Releases: store, Executions: projectSource{"attempted"}} - got, err := inventory.ExpectedProjects(context.Background(), []string{"authorized"}, false) - if err != nil || !reflect.DeepEqual(got, []string{"authorized"}) { - t.Fatalf("authorized inventory = %v, %v", got, err) - } - got, err = inventory.ExpectedProjects(context.Background(), nil, true) - if err != nil || !reflect.DeepEqual(got, []string{"attempted", "observed"}) { - t.Fatalf("no-auth inventory = %v, %v", got, err) - } -} - -type projectSource []string - -func (p projectSource) ListExecutionProjects(context.Context) ([]string, error) { return p, nil } - -func stageSnapshot(t *testing.T, store *MemoryLifecycleStore, project, generation string, now time.Time) { - t.Helper() - snapshot, _ := NewSnapshotGeneration(project, generation, "", []string{"Patient"}, now) - _, _ = store.CreateOrResumeSnapshot(context.Background(), snapshot) - upload, _ := NewResourceUpload("Patient", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1, now) - _, _ = store.RecordSnapshotUpload(context.Background(), snapshot.Dataset, upload) - if _, err := store.TransitionSnapshot(context.Background(), snapshot.Dataset, StateLoading, StateStaged, now); err != nil { - t.Fatal(err) - } + store.mu.Lock() + store.manifests[ref] = manifest + store.mu.Unlock() } func published(selector DataframeSelector, execution, generation string, now time.Time) PublicationVerification { diff --git a/internal/dataset/retention.go b/internal/dataset/retention.go deleted file mode 100644 index 7ea64589..00000000 --- a/internal/dataset/retention.go +++ /dev/null @@ -1,65 +0,0 @@ -package dataset - -import ( - "context" - "fmt" - "time" -) - -type RetentionGeneration struct { - Dataset Ref - State State - UpdatedAt time.Time - Active bool - LastGood bool - InFlight bool - Recoverable bool -} - -type RetentionRepository interface { - ListRetentionGenerations(context.Context) ([]RetentionGeneration, error) - DeleteGeneration(context.Context, Ref) error -} - -type GenerationBlobCleaner interface { - DeleteGeneration(context.Context, Ref) error -} - -type RetentionService struct { - Repository RetentionRepository - Blobs GenerationBlobCleaner - Retention time.Duration - Now func() time.Time -} - -func (s RetentionService) RunOnce(ctx context.Context) ([]Ref, error) { - if s.Repository == nil || s.Retention <= 0 { - return nil, nil - } - now := time.Now().UTC() - if s.Now != nil { - now = s.Now().UTC() - } - candidates, err := s.Repository.ListRetentionGenerations(ctx) - if err != nil { - return nil, err - } - deleted := make([]Ref, 0) - for _, candidate := range candidates { - // Active, last-good, staged/recoverable, and in-flight data are always - // protected regardless of age. Only terminal failed generations expire. - if candidate.Active || candidate.LastGood || candidate.InFlight || candidate.Recoverable || candidate.State == StateLoading || candidate.State == StateStaged || candidate.State == StateReady || candidate.UpdatedAt.After(now.Add(-s.Retention)) { - continue - } - if err := s.Repository.DeleteGeneration(ctx, candidate.Dataset); err != nil { - return deleted, fmt.Errorf("delete generation metadata %s/%s: %w", candidate.Dataset.Project, candidate.Dataset.Generation, err) - } - if s.Blobs != nil { - if err := s.Blobs.DeleteGeneration(ctx, candidate.Dataset); err != nil { - return deleted, fmt.Errorf("delete generation blobs %s/%s: %w", candidate.Dataset.Project, candidate.Dataset.Generation, err) - } - } - deleted = append(deleted, candidate.Dataset) - } - return deleted, nil -} diff --git a/internal/dataset/retention_test.go b/internal/dataset/retention_test.go deleted file mode 100644 index ea9ec105..00000000 --- a/internal/dataset/retention_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package dataset - -import ( - "context" - "reflect" - "testing" - "time" -) - -type retentionFixture struct { - items []RetentionGeneration - deleted []Ref -} - -func (f *retentionFixture) ListRetentionGenerations(context.Context) ([]RetentionGeneration, error) { - return f.items, nil -} -func (f *retentionFixture) DeleteGeneration(_ context.Context, ref Ref) error { - f.deleted = append(f.deleted, ref) - return nil -} -func (f *retentionFixture) DeleteBlobs(_ context.Context, ref Ref) error { return nil } - -type blobCleanupFixture struct{ deleted []Ref } - -func (f *blobCleanupFixture) DeleteGeneration(_ context.Context, ref Ref) error { - f.deleted = append(f.deleted, ref) - return nil -} - -func TestRetentionProtectsActiveLastGoodStagedAndInFlightData(t *testing.T) { - now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC) - old := now.Add(-30 * 24 * time.Hour) - ref := func(generation string) Ref { return Ref{Project: "project-a", Generation: generation} } - repository := &retentionFixture{items: []RetentionGeneration{ - {Dataset: ref("active"), State: StateFailed, UpdatedAt: old, Active: true}, - {Dataset: ref("last-good"), State: StateFailed, UpdatedAt: old, LastGood: true}, - {Dataset: ref("staged"), State: StateStaged, UpdatedAt: old}, - {Dataset: ref("loading"), State: StateLoading, UpdatedAt: old}, - {Dataset: ref("in-flight"), State: StateFailed, UpdatedAt: old, InFlight: true}, - {Dataset: ref("recoverable"), State: StateFailed, UpdatedAt: old, Recoverable: true}, - {Dataset: ref("recent"), State: StateFailed, UpdatedAt: now.Add(-time.Hour)}, - {Dataset: ref("expired"), State: StateFailed, UpdatedAt: old}, - }} - blobs := &blobCleanupFixture{} - deleted, err := (RetentionService{Repository: repository, Blobs: blobs, Retention: 7 * 24 * time.Hour, Now: func() time.Time { return now }}).RunOnce(context.Background()) - if err != nil { - t.Fatal(err) - } - want := []Ref{ref("expired")} - if !reflect.DeepEqual(deleted, want) || !reflect.DeepEqual(repository.deleted, want) || !reflect.DeepEqual(blobs.deleted, want) { - t.Fatalf("deleted = %v metadata=%v blobs=%v", deleted, repository.deleted, blobs.deleted) - } -} diff --git a/internal/dataset/snapshot.go b/internal/dataset/snapshot.go deleted file mode 100644 index 258e0ac6..00000000 --- a/internal/dataset/snapshot.go +++ /dev/null @@ -1,157 +0,0 @@ -package dataset - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "sort" - "strings" - "time" -) - -var ( - ErrSnapshotNotFound = errors.New("snapshot generation was not found") - ErrSnapshotConflict = errors.New("snapshot generation metadata conflicts with immutable content") - ErrChecksumConflict = errors.New("snapshot resource checksum conflict") - ErrGenerationIncomplete = errors.New("snapshot generation is incomplete") - ErrSnapshotFinalized = errors.New("snapshot generation is already finalized") - ErrSnapshotAborted = errors.New("snapshot generation was aborted") -) - -// ResourceUpload is immutable checksum evidence for one resource type. -type ResourceUpload struct { - ResourceType string `json:"resourceType"` - SHA256 string `json:"sha256"` - Size int64 `json:"size"` - UploadedAt time.Time `json:"uploadedAt"` -} - -// SnapshotGeneration tracks resumable uploads independently of the graph -// manifest. Finalization stages the graph manifest; it never activates reads. -type SnapshotGeneration struct { - Dataset Ref `json:"dataset"` - GitCommit string `json:"gitCommit"` - State State `json:"state"` - ExpectedResourceTypes []string `json:"expectedResourceTypes"` - Uploads []ResourceUpload `json:"uploads,omitempty"` - AuthResourcePath string `json:"authResourcePath,omitempty"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - AbortedAt *time.Time `json:"abortedAt,omitempty"` -} - -func NewSnapshotGeneration(project, gitCommit, authResourcePath string, expected []string, now time.Time) (SnapshotGeneration, error) { - ref, err := NewRef(strings.TrimSpace(project), strings.TrimSpace(gitCommit)) - if err != nil { - return SnapshotGeneration{}, err - } - expected = normalizeResourceTypes(expected) - if len(expected) == 0 { - return SnapshotGeneration{}, fmt.Errorf("%w: expectedResourceTypes is required", ErrGenerationIncomplete) - } - if now.IsZero() { - now = time.Now().UTC() - } - return SnapshotGeneration{Dataset: ref, GitCommit: ref.Generation, State: StateLoading, ExpectedResourceTypes: expected, AuthResourcePath: strings.TrimSpace(authResourcePath), CreatedAt: now.UTC(), UpdatedAt: now.UTC()}, nil -} - -func normalizeResourceTypes(values []string) []string { - set := make(map[string]struct{}, len(values)) - for _, value := range values { - value = strings.TrimSpace(value) - if value != "" { - set[strings.Clone(value)] = struct{}{} - } - } - result := make([]string, 0, len(set)) - for value := range set { - result = append(result, value) - } - sort.Strings(result) - return result -} - -func (g SnapshotGeneration) Validate() error { - if err := g.Dataset.Validate(); err != nil { - return err - } - if g.GitCommit != g.Dataset.Generation { - return fmt.Errorf("gitCommit must equal dataset generation") - } - if g.State != StateLoading && g.State != StateStaged && g.State != StateFailed { - return fmt.Errorf("invalid snapshot state %q", g.State) - } - if len(g.ExpectedResourceTypes) == 0 || !sort.StringsAreSorted(g.ExpectedResourceTypes) { - return fmt.Errorf("expectedResourceTypes must be non-empty and sorted") - } - seen := make(map[string]struct{}, len(g.Uploads)) - for _, upload := range g.Uploads { - if _, ok := seen[upload.ResourceType]; ok { - return fmt.Errorf("duplicate upload for %q", upload.ResourceType) - } - seen[upload.ResourceType] = struct{}{} - if !validSHA256(upload.SHA256) || upload.Size < 0 { - return fmt.Errorf("invalid upload metadata for %q", upload.ResourceType) - } - } - return nil -} - -func (g SnapshotGeneration) MissingResourceTypes() []string { - uploaded := make(map[string]struct{}, len(g.Uploads)) - for _, upload := range g.Uploads { - uploaded[upload.ResourceType] = struct{}{} - } - missing := make([]string, 0) - for _, resourceType := range g.ExpectedResourceTypes { - if _, ok := uploaded[resourceType]; !ok { - missing = append(missing, resourceType) - } - } - return missing -} - -func (g SnapshotGeneration) Upload(resourceType string) (ResourceUpload, bool) { - for _, upload := range g.Uploads { - if upload.ResourceType == resourceType { - return upload, true - } - } - return ResourceUpload{}, false -} - -func NewResourceUpload(resourceType, checksum string, size int64, now time.Time) (ResourceUpload, error) { - resourceType = strings.TrimSpace(resourceType) - checksum = strings.ToLower(strings.TrimSpace(checksum)) - if resourceType == "" || strings.ContainsAny(resourceType, "/\\") { - return ResourceUpload{}, fmt.Errorf("invalid resource type") - } - if !validSHA256(checksum) { - return ResourceUpload{}, fmt.Errorf("checksum must be a lower-case SHA-256 digest") - } - if size < 0 { - return ResourceUpload{}, fmt.Errorf("size must be non-negative") - } - if now.IsZero() { - now = time.Now().UTC() - } - return ResourceUpload{ResourceType: resourceType, SHA256: checksum, Size: size, UploadedAt: now.UTC()}, nil -} - -func validSHA256(value string) bool { - if len(value) != sha256.Size*2 || strings.ToLower(value) != value { - return false - } - _, err := hex.DecodeString(value) - return err == nil -} - -type SnapshotRepository interface { - CreateOrResumeSnapshot(context.Context, SnapshotGeneration) (SnapshotGeneration, error) - ReadSnapshot(context.Context, Ref) (SnapshotGeneration, error) - RecordSnapshotUpload(context.Context, Ref, ResourceUpload) (SnapshotGeneration, error) - TransitionSnapshot(context.Context, Ref, State, State, time.Time) (SnapshotGeneration, error) - ListSnapshotProjects(context.Context) ([]string, error) -} diff --git a/internal/dataset/snapshot_test.go b/internal/dataset/snapshot_test.go deleted file mode 100644 index d180b321..00000000 --- a/internal/dataset/snapshot_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package dataset - -import ( - "context" - "errors" - "reflect" - "testing" - "time" -) - -func TestSnapshotRepositoryIsIdempotentAndRejectsConflictingContent(t *testing.T) { - ctx := context.Background() - store := NewMemoryLifecycleStore() - now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC) - candidate, err := NewSnapshotGeneration("project-a", "commit-a", "/programs/a", []string{"Patient", "Observation"}, now) - if err != nil { - t.Fatal(err) - } - first, err := store.CreateOrResumeSnapshot(ctx, candidate) - if err != nil { - t.Fatal(err) - } - second, err := store.CreateOrResumeSnapshot(ctx, candidate) - if err != nil || !reflect.DeepEqual(first, second) { - t.Fatalf("resume = %#v, %v", second, err) - } - conflict := candidate - conflict.ExpectedResourceTypes = []string{"Patient"} - if _, err := store.CreateOrResumeSnapshot(ctx, conflict); !errors.Is(err, ErrSnapshotConflict) { - t.Fatalf("metadata conflict = %v", err) - } - - upload, _ := NewResourceUpload("Patient", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 10, now.Add(time.Minute)) - if _, err := store.RecordSnapshotUpload(ctx, candidate.Dataset, upload); err != nil { - t.Fatal(err) - } - if _, err := store.RecordSnapshotUpload(ctx, candidate.Dataset, upload); err != nil { - t.Fatalf("identical upload retry = %v", err) - } - changed := upload - changed.SHA256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - if _, err := store.RecordSnapshotUpload(ctx, candidate.Dataset, changed); !errors.Is(err, ErrChecksumConflict) { - t.Fatalf("checksum conflict = %v", err) - } - if _, err := store.TransitionSnapshot(ctx, candidate.Dataset, StateLoading, StateStaged, now); !errors.Is(err, ErrGenerationIncomplete) { - t.Fatalf("incomplete finalize = %v", err) - } -} - -func TestSnapshotFinalizeAndAbortTransitionsAreIdempotent(t *testing.T) { - ctx := context.Background() - store := NewMemoryLifecycleStore() - now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC) - candidate, _ := NewSnapshotGeneration("project-a", "commit-a", "", []string{"Patient"}, now) - _, _ = store.CreateOrResumeSnapshot(ctx, candidate) - upload, _ := NewResourceUpload("Patient", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 10, now) - _, _ = store.RecordSnapshotUpload(ctx, candidate.Dataset, upload) - staged, err := store.TransitionSnapshot(ctx, candidate.Dataset, StateLoading, StateStaged, now) - if err != nil || staged.State != StateStaged { - t.Fatalf("stage = %#v, %v", staged, err) - } - stagedAgain, err := store.TransitionSnapshot(ctx, candidate.Dataset, StateLoading, StateStaged, now.Add(time.Hour)) - if err != nil || stagedAgain.UpdatedAt != staged.UpdatedAt { - t.Fatalf("idempotent stage = %#v, %v", stagedAgain, err) - } - - abortCandidate, _ := NewSnapshotGeneration("project-b", "commit-b", "", []string{"Patient"}, now) - _, _ = store.CreateOrResumeSnapshot(ctx, abortCandidate) - aborted, err := store.TransitionSnapshot(ctx, abortCandidate.Dataset, StateLoading, StateFailed, now) - if err != nil || aborted.State != StateFailed || aborted.AbortedAt == nil { - t.Fatalf("abort = %#v, %v", aborted, err) - } -} diff --git a/internal/explorer/arango/activation.go b/internal/explorer/arango/activation.go new file mode 100644 index 00000000..563af198 --- /dev/null +++ b/internal/explorer/arango/activation.go @@ -0,0 +1,224 @@ +package arango + +import ( + "context" + "fmt" + "time" + + publicationarango "github.com/calypr/loom/internal/dataframe/publication/arango" + "github.com/calypr/loom/internal/dataset" + datasetarango "github.com/calypr/loom/internal/dataset/arango" + "github.com/calypr/loom/internal/explorer" + store "github.com/calypr/loom/internal/store/arango" +) + +const activationReadAQL = `LET owner = DOCUMENT(@@explorers, @explorerKey) +LET candidate = DOCUMENT(@@revisions, @revisionKey) +FILTER owner != null AND candidate != null AND owner.project == @project AND owner.explorerId == @explorerId AND owner.managementMode == @management AND candidate.project == @project AND candidate.explorerId == @explorerId +FILTER candidate.status == "READY" OR (candidate.status == "ACTIVE" AND owner.activeRevisionId == @revisionKey) +LET prior = owner.activeRevisionId == null ? null : DOCUMENT(@@revisions, owner.activeRevisionId) +RETURN {owner: owner, candidate: candidate, prior: prior}` + +const compositeActivationReadAQL = `LET manifest = DOCUMENT(@@lifecycle, @manifestKey) +LET active = DOCUMENT(@@lifecycle, @activeKey) +LET owner = DOCUMENT(@@explorers, @explorerKey) +LET candidate = DOCUMENT(@@revisions, @revisionKey) +LET execution = candidate == null OR LENGTH(candidate.materializations) == 0 ? null : DOCUMENT(@@executions, candidate.materializations[0].materializationId) +FILTER manifest != null AND manifest.recordType == "manifest" AND manifest.dataset.project == @project AND manifest.dataset.generation == @generation AND manifest.state IN ["STAGED", "READY"] +FILTER active != null AND active.recordType == "active_generation" AND active.project == @project +FILTER owner != null AND owner.project == @project AND owner.explorerId == "default" AND owner.managementMode == "REPOSITORY" +FILTER candidate != null AND candidate.project == @project AND candidate.explorerId == "default" AND candidate.sourceGeneration == @generation +FILTER candidate.status == "READY" OR (candidate.status == "ACTIVE" AND owner.activeRevisionId == @revisionKey) +FILTER execution != null AND execution.project == @project AND execution.datasetGeneration == @generation AND execution.state == "PUBLISHED" +LET prior = owner.activeRevisionId == null ? null : DOCUMENT(@@revisions, owner.activeRevisionId) +RETURN {manifest: manifest, active: active, owner: owner, candidate: candidate, prior: prior}` + +const activationUpdateAQL = `FOR d IN @@c +FILTER d._key == @key +UPDATE d WITH @patch IN @@c +RETURN NEW` + +const ownerActivationUpdateAQL = `FOR d IN @@c +FILTER d._key == @key +REPLACE d WITH MERGE(UNSET(d, "activeConfig", "recipeDigest", "resolvedSchemaDigest", "sourceGeneration", "dataset", "publication", "emittedColumns", "materializations", "diagnostics"), {activeRevisionId: @revisionId}) IN @@c +RETURN NEW` + +type activationState struct { + owner map[string]any + candidate map[string]any + prior map[string]any +} + +// ActivateRepositoryGeneration is the repository-only composite visibility +// switch. It validates all pointers first, then performs each single-collection +// modification inside one Arango transaction so no partial activation is +// visible if any update fails. +func (s *Store) ActivateRepositoryGeneration(ctx context.Context, project, generation, revisionID string) error { + return s.client.WithTransaction(ctx, store.TransactionCollections{ + Read: []string{publicationarango.BundleExecutionsCollection}, + Write: []string{datasetarango.LifecycleCollection, ExplorersCollection, RevisionsCollection}, + }, func(txCtx context.Context, tx store.RowQueryer) error { + row, found, err := readActivationRow(txCtx, tx, compositeActivationReadAQL, map[string]any{ + "@lifecycle": datasetarango.LifecycleCollection, "@explorers": ExplorersCollection, + "@revisions": RevisionsCollection, "@executions": publicationarango.BundleExecutionsCollection, + "manifestKey": datasetarango.ManifestDocumentKey(dataset.Ref{Project: project, Generation: generation}), + "activeKey": datasetarango.ActiveDocumentKey(project), "explorerKey": explorerKey(project, "default"), + "revisionKey": revisionID, "project": project, "generation": generation, + }) + if err != nil { + return err + } + if !found { + return explorer.ErrDraftConflict + } + state, err := activationStateFromRow(row) + if err != nil { + return err + } + manifest, err := requiredActivationDocument(row, "manifest") + if err != nil { + return err + } + active, err := requiredActivationDocument(row, "active") + if err != nil { + return err + } + activeKey, err := activationDocumentKey(active) + if err != nil { + return err + } + manifestKey, err := activationDocumentKey(manifest) + if err != nil { + return err + } + dataset, ok := manifest["dataset"] + if !ok { + return fmt.Errorf("activation manifest has no dataset") + } + if err := updateActivationDocument(txCtx, tx, datasetarango.LifecycleCollection, activeKey, map[string]any{"dataset": dataset, "manifestKey": manifestKey}); err != nil { + return err + } + return activateRevisionAndOwner(txCtx, tx, state, time.Now().UTC()) + }) +} + +func readActivationRow(ctx context.Context, tx store.RowQueryer, query string, binds map[string]any) (map[string]any, bool, error) { + var row map[string]any + err := tx.QueryRows(ctx, query, 1, binds, func(value map[string]any) error { + if row != nil { + return fmt.Errorf("activation guard returned more than one row") + } + row = value + return nil + }) + return row, row != nil, err +} + +func activationStateFromRow(row map[string]any) (activationState, error) { + owner, err := requiredActivationDocument(row, "owner") + if err != nil { + return activationState{}, err + } + candidate, err := requiredActivationDocument(row, "candidate") + if err != nil { + return activationState{}, err + } + prior, err := optionalActivationDocument(row, "prior") + if err != nil { + return activationState{}, err + } + return activationState{owner: owner, candidate: candidate, prior: prior}, nil +} + +func requiredActivationDocument(row map[string]any, field string) (map[string]any, error) { + document, err := optionalActivationDocument(row, field) + if err != nil { + return nil, err + } + if document == nil { + return nil, fmt.Errorf("activation guard returned no %s document", field) + } + return document, nil +} + +func optionalActivationDocument(row map[string]any, field string) (map[string]any, error) { + value := row[field] + if value == nil { + return nil, nil + } + document, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("activation guard returned invalid %s document", field) + } + return document, nil +} + +func activationDocumentKey(document map[string]any) (string, error) { + key, ok := document["_key"].(string) + if !ok || key == "" { + return "", fmt.Errorf("activation document has no _key") + } + return key, nil +} + +func activateRevisionAndOwner(ctx context.Context, tx store.RowQueryer, state activationState, now time.Time) error { + candidateKey, err := activationDocumentKey(state.candidate) + if err != nil { + return err + } + if state.prior != nil { + priorKey, err := activationDocumentKey(state.prior) + if err != nil { + return err + } + if priorKey != candidateKey { + if err := updateActivationDocument(ctx, tx, RevisionsCollection, priorKey, map[string]any{"status": explorer.RevisionSuperseded}); err != nil { + return err + } + } + } + if err := updateActivationDocument(ctx, tx, RevisionsCollection, candidateKey, map[string]any{"status": explorer.RevisionActive, "activatedAt": now}); err != nil { + return err + } + ownerKey, err := activationDocumentKey(state.owner) + if err != nil { + return err + } + return updateActiveExplorerDocument(ctx, tx, ownerKey, candidateKey) +} + +func updateActiveExplorerDocument(ctx context.Context, tx store.RowQueryer, ownerKey, revisionID string) error { + var updated bool + err := tx.QueryRows(ctx, ownerActivationUpdateAQL, 1, map[string]any{"@c": ExplorersCollection, "key": ownerKey, "revisionId": revisionID}, func(map[string]any) error { + if updated { + return fmt.Errorf("activation owner update returned more than one document") + } + updated = true + return nil + }) + if err != nil { + return err + } + if !updated { + return explorer.ErrDraftConflict + } + return nil +} + +func updateActivationDocument(ctx context.Context, tx store.RowQueryer, collection, key string, patch map[string]any) error { + var updated bool + err := tx.QueryRows(ctx, activationUpdateAQL, 1, map[string]any{"@c": collection, "key": key, "patch": patch}, func(map[string]any) error { + if updated { + return fmt.Errorf("activation update returned more than one document") + } + updated = true + return nil + }) + if err != nil { + return err + } + if !updated { + return explorer.ErrDraftConflict + } + return nil +} diff --git a/internal/explorer/arango/capability_snapshot_store.go b/internal/explorer/arango/capability_snapshot_store.go index d41f1840..a12029a6 100644 --- a/internal/explorer/arango/capability_snapshot_store.go +++ b/internal/explorer/arango/capability_snapshot_store.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/calypr/loom/internal/explorer/capability" - "github.com/calypr/loom/internal/explorer/capabilitystore" store "github.com/calypr/loom/internal/store/arango" ) @@ -42,7 +41,7 @@ func NewCapabilitySnapshotStore(c client) (*CapabilitySnapshotStore, error) { } func (s *CapabilitySnapshotStore) Put(ctx context.Context, snapshot capability.Snapshot) (*capability.Snapshot, error) { - if err := capabilitystore.ValidateSnapshot(snapshot); err != nil { + if err := capability.ValidateSnapshot(snapshot); err != nil { return nil, err } doc, err := canonicalSnapshotDocument(snapshot, capabilitySnapshotKey(snapshot.Token)) @@ -59,10 +58,10 @@ func (s *CapabilitySnapshotStore) Put(ctx context.Context, snapshot capability.S return decodeErr } if value.Token != snapshot.Token { - return fmt.Errorf("%w: %s", capabilitystore.ErrTokenCollision, snapshot.Token) + return fmt.Errorf("%w: %s", capability.ErrTokenCollision, snapshot.Token) } if string(value.CanonicalPayload()) != string(snapshot.CanonicalPayload()) { - return fmt.Errorf("%w: %s", capabilitystore.ErrTokenCollision, snapshot.Token) + return fmt.Errorf("%w: %s", capability.ErrTokenCollision, snapshot.Token) } value = value.Clone() out = &value @@ -77,7 +76,7 @@ func (s *CapabilitySnapshotStore) Put(ctx context.Context, snapshot capability.S return nil, getErr } if string(existing.CanonicalPayload()) != string(snapshot.CanonicalPayload()) { - return nil, fmt.Errorf("%w: %s", capabilitystore.ErrTokenCollision, snapshot.Token) + return nil, fmt.Errorf("%w: %s", capability.ErrTokenCollision, snapshot.Token) } return existing, nil } @@ -93,7 +92,7 @@ func (s *CapabilitySnapshotStore) GetByToken(ctx context.Context, token string) }, func(row map[string]any) error { value, err := decode[capability.Snapshot](row) if err == nil { - err = capabilitystore.ValidateSnapshot(value) + err = capability.ValidateSnapshot(value) } if err == nil { value = value.Clone() @@ -105,7 +104,7 @@ func (s *CapabilitySnapshotStore) GetByToken(ctx context.Context, token string) return nil, err } if out == nil { - return nil, capabilitystore.ErrNotFound + return nil, capability.ErrNotFound } return out, nil } @@ -129,7 +128,7 @@ func (s *CapabilitySnapshotStore) GetByIdentity(ctx context.Context, identity ca }, func(row map[string]any) error { value, err := decode[capability.Snapshot](row) if err == nil { - err = capabilitystore.ValidateSnapshot(value) + err = capability.ValidateSnapshot(value) } if err == nil { value = value.Clone() @@ -141,7 +140,7 @@ func (s *CapabilitySnapshotStore) GetByIdentity(ctx context.Context, identity ca return nil, err } if out == nil { - return nil, capabilitystore.ErrNotFound + return nil, capability.ErrNotFound } return out, nil } @@ -193,4 +192,4 @@ LIMIT 1 RETURN d ` -var _ capabilitystore.Repository = (*CapabilitySnapshotStore)(nil) +var _ capability.Repository = (*CapabilitySnapshotStore)(nil) diff --git a/internal/explorer/arango/codec.go b/internal/explorer/arango/codec.go new file mode 100644 index 00000000..d4aa58ea --- /dev/null +++ b/internal/explorer/arango/codec.go @@ -0,0 +1,66 @@ +package arango + +import ( + "encoding/json" + "time" +) + +func decode[T any](value any) (T, error) { + var out T + raw, err := json.Marshal(normalizeUpdatedAt(value)) + if err != nil { + return out, err + } + err = json.Unmarshal(raw, &out) + return out, err +} + +// normalizeUpdatedAt keeps reads tolerant of the numeric Arango timestamp +// written by the short-lived last-write-wins regression. New writes use the +// normal JSON time string representation, but existing documents must remain +// readable so they can be repaired by a subsequent draft save. +func normalizeUpdatedAt(value any) any { + row, ok := value.(map[string]any) + if !ok { + return value + } + normalized := make(map[string]any, len(row)) + for key, item := range row { + normalized[key] = item + } + if timestamp, ok := numericTimestamp(row["updatedAt"]); ok { + normalized["updatedAt"] = timestamp + } + return normalized +} + +func numericTimestamp(value any) (string, bool) { + var millis int64 + switch number := value.(type) { + case float64: + millis = int64(number) + case float32: + millis = int64(number) + case int: + millis = int64(number) + case int64: + millis = number + case uint64: + millis = int64(number) + default: + return "", false + } + return time.UnixMilli(millis).UTC().Format(time.RFC3339Nano), true +} +func document(value any, k string) (map[string]any, error) { + raw, err := json.Marshal(value) + if err != nil { + return nil, err + } + var doc map[string]any + if err = json.Unmarshal(raw, &doc); err != nil { + return nil, err + } + doc["_key"] = k + return doc, nil +} diff --git a/internal/explorer/arango/collections.go b/internal/explorer/arango/collections.go index afc6ead1..9aa3f7c9 100644 --- a/internal/explorer/arango/collections.go +++ b/internal/explorer/arango/collections.go @@ -7,7 +7,10 @@ const ( ExplorersCollection = "loom_explorers" RevisionsCollection = "loom_explorer_revisions" CompilationReceiptsCollection = "loom_explorer_compilation_receipts" - RepositoryConfigsCollection = "loom_repository_explorer_configs" + // LegacyRepositoryConfigsCollection is read only. It remains in bootstrap + // for one compatibility window so startup can migrate old default-owner + // pointers into loom_explorers; no current workflow writes it. + LegacyRepositoryConfigsCollection = "loom_repository_explorer_configs" ) // CollectionSpecs is deliberately non-truncating: both draft history pointers @@ -15,13 +18,13 @@ const ( func CollectionSpecs() []store.CollectionSpec { return []store.CollectionSpec{ {Name: ExplorersCollection, Indexes: [][]string{{"project", "explorerId"}}}, - {Name: RevisionsCollection, Indexes: [][]string{{"project", "explorerId", "createdAt"}, {"project", "status"}, {"project", "explorerId", "sourceCommit", "definitionDigest", "sourceGeneration"}}}, + {Name: RevisionsCollection, Indexes: [][]string{{"project", "explorerId", "createdAt"}, {"project", "status"}}}, {Name: CompilationReceiptsCollection, Indexes: [][]string{ {"project", "explorerId", "intentDigest"}, {"project", "explorerId", "createdAt"}, {"project", "explorerId", "compilationKey", "receiptFormatVersion", "compilerContractVersion"}, }}, - {Name: RepositoryConfigsCollection, Indexes: [][]string{{"project"}}}, + {Name: LegacyRepositoryConfigsCollection, Indexes: [][]string{{"project"}}}, CapabilitySnapshotCollectionSpec(), } } diff --git a/internal/explorer/arango/drafts.go b/internal/explorer/arango/drafts.go new file mode 100644 index 00000000..e69059f0 --- /dev/null +++ b/internal/explorer/arango/drafts.go @@ -0,0 +1,30 @@ +package arango + +import ( + "context" + "strings" + "time" + + "github.com/calypr/loom/internal/explorer" +) + +func (s *Store) SaveDraft(ctx context.Context, e explorer.Explorer, expected int64, expectedDigest ...string) (*explorer.Explorer, error) { + e.UpdatedAt = time.Now().UTC() + raw, err := document(e, explorerKey(e.Project, e.ExplorerID)) + if err != nil { + return nil, err + } + digest := "" + if len(expectedDigest) > 0 { + digest = strings.TrimSpace(expectedDigest[0]) + } + var out *explorer.Explorer + err = s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key AND (d.draftVersion == @expected OR (!HAS(d, "draftVersion") AND @expected == 0)) AND (@expectedDigest == "" OR d.draftDigest == @expectedDigest) UPDATE d WITH MERGE(@doc, { draftVersion: NOT_NULL(d.draftVersion, 0) + 1 }) IN @@c RETURN NEW`, 1, map[string]any{"@c": ExplorersCollection, "key": explorerKey(e.Project, e.ExplorerID), "expected": expected, "expectedDigest": digest, "doc": raw}, func(row map[string]any) error { v, err := decode[explorer.Explorer](row); out = &v; return err }) + if err != nil { + return nil, err + } + if out == nil { + return nil, explorer.ErrDraftConflict + } + return out, nil +} diff --git a/internal/explorer/arango/explorers.go b/internal/explorer/arango/explorers.go new file mode 100644 index 00000000..ac88f10b --- /dev/null +++ b/internal/explorer/arango/explorers.go @@ -0,0 +1,45 @@ +package arango + +import ( + "context" + + "github.com/calypr/loom/internal/explorer" +) + +func (s *Store) List(ctx context.Context, project string) ([]explorer.Explorer, error) { + out := []explorer.Explorer{} + err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d.project == @project SORT d.explorerId RETURN d`, 1000, map[string]any{"@c": ExplorersCollection, "project": project}, func(row map[string]any) error { + v, err := decode[explorer.Explorer](row) + if err == nil { + out = append(out, v) + } + return err + }) + return out, err +} +func (s *Store) Get(ctx context.Context, project, id string) (*explorer.Explorer, error) { + var out *explorer.Explorer + err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key AND d.project == @project RETURN d`, 1, map[string]any{"@c": ExplorersCollection, "key": explorerKey(project, id), "project": project}, func(row map[string]any) error { v, err := decode[explorer.Explorer](row); out = &v; return err }) + if err != nil { + return nil, err + } + if out == nil { + return nil, explorer.ErrNotFound + } + return out, nil +} +func (s *Store) Create(ctx context.Context, e explorer.Explorer) (*explorer.Explorer, error) { + raw, err := document(e, explorerKey(e.Project, e.ExplorerID)) + if err != nil { + return nil, err + } + var out *explorer.Explorer + err = s.client.QueryRows(ctx, `INSERT @doc INTO @@c OPTIONS { overwriteMode: "ignore" } RETURN NEW`, 1, map[string]any{"@c": ExplorersCollection, "doc": raw}, func(row map[string]any) error { v, err := decode[explorer.Explorer](row); out = &v; return err }) + if err != nil { + return nil, err + } + if out == nil { + return nil, explorer.ErrDraftConflict + } + return out, nil +} diff --git a/internal/explorer/arango/publication.go b/internal/explorer/arango/publication.go new file mode 100644 index 00000000..24551f1b --- /dev/null +++ b/internal/explorer/arango/publication.go @@ -0,0 +1,77 @@ +package arango + +import ( + "context" + "time" + + "github.com/calypr/loom/internal/dataset" + datasetarango "github.com/calypr/loom/internal/dataset/arango" + "github.com/calypr/loom/internal/explorer" + store "github.com/calypr/loom/internal/store/arango" +) + +func (s *Store) PublishAuthoring(ctx context.Context, receipt explorer.CompilationReceipt, revision explorer.Revision, release dataset.ProjectRelease, expectedReleaseRevision int64) (*explorer.Revision, error) { + if err := validateReceipt(receipt); err != nil { + return nil, err + } + receiptDoc, err := document(receipt, receipt.ID) + if err != nil { + return nil, err + } + revisionDoc, err := document(revision, revision.ID) + if err != nil { + return nil, err + } + management := explorer.ManagementInteractive + if revision.ExplorerID == "default" { + management = explorer.ManagementRepository + } + var out *explorer.Revision + err = s.client.WithTransaction(ctx, store.TransactionCollections{Write: []string{datasetarango.LifecycleCollection, ExplorersCollection, RevisionsCollection, CompilationReceiptsCollection}}, func(txCtx context.Context, tx store.RowQueryer) error { + if err := tx.QueryRows(txCtx, `INSERT @doc INTO @@c OPTIONS { overwriteMode: "ignore" } RETURN NEW`, 1, map[string]any{"@c": CompilationReceiptsCollection, "doc": receiptDoc}, func(map[string]any) error { return nil }); err != nil { + return err + } + var stored *explorer.CompilationReceipt + if err := tx.QueryRows(txCtx, `FOR d IN @@c FILTER d._key == @key AND d.project == @project AND d.explorerId == @explorerId RETURN d`, 1, map[string]any{"@c": CompilationReceiptsCollection, "key": receipt.ID, "project": receipt.Project, "explorerId": receipt.ExplorerID}, func(row map[string]any) error { + value, err := decode[explorer.CompilationReceipt](row) + if err != nil { + return err + } + if err := validateReceipt(value); err != nil { + return err + } + stored = &value + return nil + }); err != nil { + return err + } + if stored == nil { + return explorer.ErrNotFound + } + if !sameReceipt(*stored, receipt) { + return explorer.ErrCorruptReceipt + } + if err := tx.QueryRows(txCtx, `UPSERT { _key: @key } INSERT @doc UPDATE {} IN @@c RETURN NEW`, 1, map[string]any{"@c": RevisionsCollection, "key": revision.ID, "doc": revisionDoc}, func(map[string]any) error { return nil }); err != nil { + return err + } + if _, err := datasetarango.CompareAndSwapActivateRelease(txCtx, tx, release, expectedReleaseRevision); err != nil { + return err + } + row, found, err := readActivationRow(txCtx, tx, activationReadAQL, map[string]any{"@explorers": ExplorersCollection, "@revisions": RevisionsCollection, "explorerKey": explorerKey(revision.Project, revision.ExplorerID), "revisionKey": revision.ID, "project": revision.Project, "explorerId": revision.ExplorerID, "management": management}) + if err != nil { + return err + } + if !found { + return explorer.ErrDraftConflict + } + state, err := activationStateFromRow(row) + if err != nil { + return err + } + if err := activateRevisionAndOwner(txCtx, tx, state, time.Now().UTC()); err != nil { + return err + } + return tx.QueryRows(txCtx, `FOR d IN @@c FILTER d._key == @key RETURN d`, 1, map[string]any{"@c": RevisionsCollection, "key": revision.ID}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) + }) + return out, err +} diff --git a/internal/explorer/arango/receipt_store_test.go b/internal/explorer/arango/receipt_store_test.go index e7501e47..39996cd9 100644 --- a/internal/explorer/arango/receipt_store_test.go +++ b/internal/explorer/arango/receipt_store_test.go @@ -48,6 +48,7 @@ func TestReceiptArangoStoreUsesImmutableTenantScopedQueries(t *testing.T) { AuthorizationScopeDigest: "scope", CapabilitySchemaDigest: "schema", SourceGeneration: "generation", RecipeDigest: "recipe", ResolvedSchemaDigest: "resolved-schema", Bundle: bundle, PublicOutputContract: json.RawMessage(`{"outputs":[{"outputId":"output","columns":[]}]}`), + OutputColumnProvenance: map[string]map[string]string{"output": {"__loom_row_id": "EXPLICIT"}}, } receipt.ResolvedRecipeDigest, _ = bundle.Digest() receipt.OutputContractDigest, _ = explorer.CompilationArtifactDigest(receipt.PublicOutputContract) diff --git a/internal/explorer/arango/receipts.go b/internal/explorer/arango/receipts.go new file mode 100644 index 00000000..f80e4aa5 --- /dev/null +++ b/internal/explorer/arango/receipts.go @@ -0,0 +1,80 @@ +package arango + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "strings" + + "github.com/calypr/loom/internal/explorer" +) + +func (s *Store) InsertCompilationReceipt(ctx context.Context, receipt explorer.CompilationReceipt) (*explorer.CompilationReceipt, error) { + if err := validateReceipt(receipt); err != nil { + return nil, err + } + doc, err := document(receipt, receipt.ID) + if err != nil { + return nil, err + } + // INSERT with overwriteMode=ignore makes the key immutable under retries + // and concurrent requests. The read below is authoritative and compares + // the content-addressed identity, detecting a corrupted key collision. + err = s.client.QueryRows(ctx, `INSERT @doc INTO @@c OPTIONS { overwriteMode: "ignore" } RETURN NEW`, 1, map[string]any{"@c": CompilationReceiptsCollection, "doc": doc}, func(map[string]any) error { return nil }) + if err != nil { + return nil, err + } + return s.GetCompilationReceiptForExplorer(ctx, receipt.Project, receipt.ExplorerID, receipt.ID) +} + +func (s *Store) GetCompilationReceiptForExplorer(ctx context.Context, project, explorerID, id string) (*explorer.CompilationReceipt, error) { + return s.readCompilationReceipt(ctx, `FOR d IN @@c FILTER d._key == @key AND d.project == @project AND d.explorerId == @explorerId RETURN d`, map[string]any{"@c": CompilationReceiptsCollection, "key": id, "project": project, "explorerId": explorerID}) +} + +func (s *Store) readCompilationReceipt(ctx context.Context, query string, binds map[string]any) (*explorer.CompilationReceipt, error) { + var out *explorer.CompilationReceipt + err := s.client.QueryRows(ctx, query, 1, binds, func(row map[string]any) error { + value, decodeErr := decode[explorer.CompilationReceipt](row) + if decodeErr != nil { + return decodeErr + } + if err := validateReceipt(value); err != nil { + return err + } + out = &value + return nil + }) + if err != nil { + return nil, err + } + if out == nil { + return nil, explorer.ErrNotFound + } + return out, nil +} + +func validateReceipt(receipt explorer.CompilationReceipt) error { + if strings.TrimSpace(receipt.ID) == "" { + return explorer.ErrCorruptReceipt + } + if receipt.ReceiptFormatVersion != 0 || receipt.CompilerContractVersion != "" || receipt.CompilationKey != "" { + if err := receipt.Validate(); err != nil { + return err + } + } + if strings.HasPrefix(receipt.ID, "receipt_") && len(receipt.ID) == len("receipt_")+sha256.Size*2 { + if _, err := hex.DecodeString(strings.TrimPrefix(receipt.ID, "receipt_")); err != nil { + return explorer.ErrCorruptReceipt + } + if err := receipt.ValidateID(); err != nil { + return explorer.ErrCorruptReceipt + } + } + return nil +} + +func sameReceipt(a, b explorer.CompilationReceipt) bool { + left, leftErr := explorer.ReceiptID(a) + right, rightErr := explorer.ReceiptID(b) + return leftErr == nil && rightErr == nil && left == right +} diff --git a/internal/explorer/arango/repository.go b/internal/explorer/arango/repository.go new file mode 100644 index 00000000..69246b87 --- /dev/null +++ b/internal/explorer/arango/repository.go @@ -0,0 +1,75 @@ +package arango + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/projectid" +) + +// legacyRepositoryConfig contains only the fields needed to recover the +// canonical default Explorer owner. Immutable generated state already lives in +// the referenced revision and is deliberately not decoded or copied here. +type legacyRepositoryConfig struct { + Project string `json:"project"` + Workspace json.RawMessage `json:"workspace"` + IntentDigest string `json:"intentDigest"` + ActiveRevisionID string `json:"activeRevisionId"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// MigrateLegacyRepositoryConfigs is an idempotent startup migration. It +// recovers only missing repository owners and never overwrites a current owner +// or deletes the legacy source document. +func (s *Store) MigrateLegacyRepositoryConfigs(ctx context.Context) error { + legacy := []legacyRepositoryConfig{} + err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d.activeRevisionId != null AND d.activeRevisionId != "" RETURN d`, 1000, map[string]any{"@c": LegacyRepositoryConfigsCollection}, func(row map[string]any) error { + value, decodeErr := decode[legacyRepositoryConfig](row) + if decodeErr == nil { + legacy = append(legacy, value) + } + return decodeErr + }) + if err != nil { + return fmt.Errorf("read legacy Explorer repository configs: %w", err) + } + for _, value := range legacy { + project := projectid.Legacy(value.Project) + if _, err := s.Get(ctx, project, "default"); err == nil { + continue + } else if !errors.Is(err, explorer.ErrNotFound) { + return err + } + revision, err := s.GetRevision(ctx, value.ActiveRevisionID) + if err != nil { + return fmt.Errorf("migrate legacy Explorer %q revision %q: %w", project, value.ActiveRevisionID, err) + } + draft := value.Workspace + if len(draft) == 0 { + draft = revision.AuthoringBundle + } + title := "Default" + if workspace, decodeErr := authoringv2.DecodeWorkspace(draft); decodeErr == nil && strings.TrimSpace(workspace.Explorer.Title) != "" { + title = workspace.Explorer.Title + } + digest := value.IntentDigest + if digest == "" { + digest = revision.IntentDigest + } + updatedAt := value.UpdatedAt + if updatedAt.IsZero() { + updatedAt = revision.CreatedAt + } + _, err = s.Create(ctx, explorer.Explorer{Project: project, ExplorerID: "default", Title: title, ManagementMode: explorer.ManagementRepository, DraftConfig: append([]byte(nil), draft...), DraftVersion: 1, DraftDigest: digest, ActiveRevisionID: revision.ID, UpdatedAt: updatedAt}) + if err != nil && !errors.Is(err, explorer.ErrDraftConflict) { + return fmt.Errorf("migrate legacy Explorer %q owner: %w", project, err) + } + } + return nil +} diff --git a/internal/explorer/arango/repository_test.go b/internal/explorer/arango/repository_test.go new file mode 100644 index 00000000..412b08bb --- /dev/null +++ b/internal/explorer/arango/repository_test.go @@ -0,0 +1,66 @@ +package arango + +import ( + "context" + "strings" + "testing" + "time" + + storepkg "github.com/calypr/loom/internal/store/arango" +) + +type migrationClient struct { + created map[string]any +} + +func (c *migrationClient) WithTransaction(ctx context.Context, _ storepkg.TransactionCollections, fn storepkg.TransactionFunc) error { + return fn(ctx, c) +} + +func (c *migrationClient) QueryRows(_ context.Context, query string, _ int, binds map[string]any, visit storepkg.RowVisitor) error { + switch { + case strings.Contains(query, "d.activeRevisionId != null"): + return visit(map[string]any{ + "project": "project-a", "activeRevisionId": "revision-a", "intentDigest": "sha256:intent", + "workspace": map[string]any{"apiVersion": "loom.calypr.org/explorer-authoring/v2", "kind": "ExplorerBuilderWorkspace", "explorer": map[string]any{"title": "Patients"}, "documents": []any{}, "tabs": []any{}}, + "updatedAt": time.Unix(10, 0).UTC(), + }) + case strings.Contains(query, "d._key == @key AND d.project"): + return nil + case strings.Contains(query, "FILTER d._key == @key RETURN d"): + return visit(map[string]any{ + "_key": "revision-a", "id": "revision-a", "project": "project-a", "explorerId": "default", + "authoringBundle": map[string]any{"apiVersion": "loom.calypr.org/explorer-authoring/v2", "kind": "ExplorerBuilderWorkspace", "explorer": map[string]any{"title": "Fallback"}, "documents": []any{}, "tabs": []any{}}, + "intentDigest": "sha256:intent", "status": "ACTIVE", "createdAt": time.Unix(5, 0).UTC(), + }) + case strings.Contains(query, "INSERT @doc INTO"): + c.created = binds["doc"].(map[string]any) + return visit(c.created) + default: + return nil + } +} + +func TestMigrateLegacyRepositoryConfigCreatesOnlyCanonicalOwner(t *testing.T) { + client := &migrationClient{} + store, err := New(client) + if err != nil { + t.Fatal(err) + } + if err := store.MigrateLegacyRepositoryConfigs(context.Background()); err != nil { + t.Fatal(err) + } + if client.created == nil { + t.Fatal("legacy owner was not migrated") + } + for key, want := range map[string]any{"project": "project-a", "explorerId": "default", "title": "Patients", "managementMode": "REPOSITORY", "activeRevisionId": "revision-a"} { + if got := client.created[key]; got != want { + t.Fatalf("%s=%v, want %v", key, got, want) + } + } + for _, duplicate := range []string{"activeConfig", "recipeDigest", "sourceGeneration", "materializations", "dataset", "publication", "diagnostics"} { + if _, exists := client.created[duplicate]; exists { + t.Fatalf("migrated owner retained generated field %q", duplicate) + } + } +} diff --git a/internal/explorer/arango/revisions.go b/internal/explorer/arango/revisions.go new file mode 100644 index 00000000..16b81ef1 --- /dev/null +++ b/internal/explorer/arango/revisions.go @@ -0,0 +1,56 @@ +package arango + +import ( + "context" + "fmt" + "time" + + "github.com/calypr/loom/internal/explorer" +) + +func (s *Store) InsertRevision(ctx context.Context, revision explorer.Revision) (*explorer.Revision, error) { + if revision.ID == "" { + return nil, fmt.Errorf("revision ID is required") + } + doc, err := document(revision, revision.ID) + if err != nil { + return nil, err + } + var out *explorer.Revision + err = s.client.QueryRows(ctx, `UPSERT { _key: @key } INSERT @doc UPDATE {} IN @@c RETURN NEW`, 1, map[string]any{"@c": RevisionsCollection, "key": revision.ID, "doc": doc}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) + if err != nil { + return nil, err + } + if out == nil { + return nil, explorer.ErrNotFound + } + return out, nil +} + +func (s *Store) GetRevision(ctx context.Context, id string) (*explorer.Revision, error) { + var out *explorer.Revision + err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key RETURN d`, 1, map[string]any{"@c": RevisionsCollection, "key": id}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) + if err != nil { + return nil, err + } + if out == nil { + return nil, explorer.ErrNotFound + } + return out, nil +} + +// FailRevision records the only non-activation revision transition performed +// outside the atomic activation workflows. +func (s *Store) FailRevision(ctx context.Context, id string, diagnostics []explorer.Diagnostic) (*explorer.Revision, error) { + now := time.Now().UTC() + patch := map[string]any{"status": explorer.RevisionFailed, "diagnostics": diagnostics, "failedAt": now} + var out *explorer.Revision + err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key UPDATE d WITH @patch IN @@c RETURN NEW`, 1, map[string]any{"@c": RevisionsCollection, "key": id, "patch": patch}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) + if err != nil { + return nil, err + } + if out == nil { + return nil, explorer.ErrNotFound + } + return out, nil +} diff --git a/internal/explorer/arango/store.go b/internal/explorer/arango/store.go index bb47b4a5..3969f148 100644 --- a/internal/explorer/arango/store.go +++ b/internal/explorer/arango/store.go @@ -4,22 +4,17 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" - "strings" - "time" - publicationarango "github.com/calypr/loom/internal/dataframe/publication/arango" - dataset "github.com/calypr/loom/internal/dataset" - datasetarango "github.com/calypr/loom/internal/dataset/arango" "github.com/calypr/loom/internal/explorer" store "github.com/calypr/loom/internal/store/arango" ) type client interface { - QueryRows(context.Context, string, int, map[string]any, store.RowVisitor) error + store.RowQueryer WithTransaction(context.Context, store.TransactionCollections, store.TransactionFunc) error } + type Store struct{ client client } func New(client client) (*Store, error) { @@ -28,558 +23,16 @@ func New(client client) (*Store, error) { } return &Store{client: client}, nil } + func key(parts ...string) string { h := sha256.New() - for _, p := range parts { + for _, part := range parts { _, _ = h.Write([]byte{0}) - _, _ = h.Write([]byte(p)) + _, _ = h.Write([]byte(part)) } return hex.EncodeToString(h.Sum(nil)) } -func explorerKey(project, id string) string { return "explorer_" + key(project, id) } -func repositoryConfigKey(project string) string { - return "repository_config_" + key(project, "default") -} -func (s *Store) GetRepositoryConfig(ctx context.Context, project string) (*explorer.RepositoryConfig, error) { - var out *explorer.RepositoryConfig - err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key RETURN d`, 1, map[string]any{"@c": RepositoryConfigsCollection, "key": repositoryConfigKey(project)}, func(row map[string]any) error { v, err := decode[explorer.RepositoryConfig](row); out = &v; return err }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrNotFound - } - if out.ExplorerID == "" { - out.ExplorerID, out.Management = "default", explorer.ManagementRepository - } - return out, nil -} -func (s *Store) SaveRepositoryConfig(ctx context.Context, value explorer.RepositoryConfig) (*explorer.RepositoryConfig, error) { - value.ExplorerID, value.Management = "default", explorer.ManagementRepository - value.UpdatedAt = time.Now().UTC() - doc, err := document(value, repositoryConfigKey(value.Project)) - if err != nil { - return nil, err - } - var out *explorer.RepositoryConfig - err = s.client.QueryRows(ctx, `UPSERT { _key: @key } INSERT @doc UPDATE @doc IN @@c RETURN NEW`, 1, map[string]any{"@c": RepositoryConfigsCollection, "key": repositoryConfigKey(value.Project), "doc": doc}, func(row map[string]any) error { v, err := decode[explorer.RepositoryConfig](row); out = &v; return err }) - if err != nil { - return nil, err - } - return out, nil -} -func (s *Store) List(ctx context.Context, project string) ([]explorer.Explorer, error) { - out := []explorer.Explorer{} - err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d.project == @project SORT d.explorerId RETURN d`, 1000, map[string]any{"@c": ExplorersCollection, "project": project}, func(row map[string]any) error { - v, err := decode[explorer.Explorer](row) - if err == nil { - out = append(out, v) - } - return err - }) - return out, err -} -func (s *Store) Get(ctx context.Context, project, id string) (*explorer.Explorer, error) { - var out *explorer.Explorer - err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key AND d.project == @project RETURN d`, 1, map[string]any{"@c": ExplorersCollection, "key": explorerKey(project, id), "project": project}, func(row map[string]any) error { v, err := decode[explorer.Explorer](row); out = &v; return err }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrNotFound - } - return out, nil -} -func (s *Store) CreateInteractive(ctx context.Context, e explorer.Explorer) (*explorer.Explorer, error) { - raw, err := document(e, explorerKey(e.Project, e.ExplorerID)) - if err != nil { - return nil, err - } - var out *explorer.Explorer - err = s.client.QueryRows(ctx, `INSERT @doc INTO @@c OPTIONS { overwriteMode: "ignore" } RETURN NEW`, 1, map[string]any{"@c": ExplorersCollection, "doc": raw}, func(row map[string]any) error { v, err := decode[explorer.Explorer](row); out = &v; return err }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrDraftConflict - } - return out, nil -} -func (s *Store) CreateRepository(ctx context.Context, e explorer.Explorer) (*explorer.Explorer, error) { - return s.CreateInteractive(ctx, e) -} -func (s *Store) SaveDraft(ctx context.Context, e explorer.Explorer, expected int64, expectedDigest ...string) (*explorer.Explorer, error) { - e.UpdatedAt = time.Now().UTC() - raw, err := document(e, explorerKey(e.Project, e.ExplorerID)) - if err != nil { - return nil, err - } - digest := "" - if len(expectedDigest) > 0 { - digest = strings.TrimSpace(expectedDigest[0]) - } - var out *explorer.Explorer - err = s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key AND (d.draftVersion == @expected OR (!HAS(d, "draftVersion") AND @expected == 0)) AND (@expectedDigest == "" OR d.draftDigest == @expectedDigest) UPDATE d WITH MERGE(@doc, { draftVersion: NOT_NULL(d.draftVersion, 0) + 1 }) IN @@c RETURN NEW`, 1, map[string]any{"@c": ExplorersCollection, "key": explorerKey(e.Project, e.ExplorerID), "expected": expected, "expectedDigest": digest, "doc": raw}, func(row map[string]any) error { v, err := decode[explorer.Explorer](row); out = &v; return err }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrDraftConflict - } - return out, nil -} - -func (s *Store) InsertCompilationReceipt(ctx context.Context, receipt explorer.CompilationReceipt) (*explorer.CompilationReceipt, error) { - if err := validateReceipt(receipt); err != nil { - return nil, err - } - doc, err := document(receipt, receipt.ID) - if err != nil { - return nil, err - } - // INSERT with overwriteMode=ignore makes the key immutable under retries - // and concurrent requests. The read below is authoritative and compares - // the content-addressed identity, detecting a corrupted key collision. - err = s.client.QueryRows(ctx, `INSERT @doc INTO @@c OPTIONS { overwriteMode: "ignore" } RETURN NEW`, 1, map[string]any{"@c": CompilationReceiptsCollection, "doc": doc}, func(map[string]any) error { return nil }) - if err != nil { - return nil, err - } - return s.GetCompilationReceiptForExplorer(ctx, receipt.Project, receipt.ExplorerID, receipt.ID) -} - -func (s *Store) GetCompilationReceiptForExplorer(ctx context.Context, project, explorerID, id string) (*explorer.CompilationReceipt, error) { - return s.readCompilationReceipt(ctx, `FOR d IN @@c FILTER d._key == @key AND d.project == @project AND d.explorerId == @explorerId RETURN d`, map[string]any{"@c": CompilationReceiptsCollection, "key": id, "project": project, "explorerId": explorerID}) -} - -func (s *Store) readCompilationReceipt(ctx context.Context, query string, binds map[string]any) (*explorer.CompilationReceipt, error) { - var out *explorer.CompilationReceipt - err := s.client.QueryRows(ctx, query, 1, binds, func(row map[string]any) error { - value, decodeErr := decode[explorer.CompilationReceipt](row) - if decodeErr != nil { - return decodeErr - } - if err := validateReceipt(value); err != nil { - return err - } - out = &value - return nil - }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrNotFound - } - return out, nil -} - -func validateReceipt(receipt explorer.CompilationReceipt) error { - if strings.TrimSpace(receipt.ID) == "" { - return explorer.ErrCorruptReceipt - } - if receipt.ReceiptFormatVersion != 0 || receipt.CompilerContractVersion != "" || receipt.CompilationKey != "" { - if err := receipt.Validate(); err != nil { - return err - } - } - if strings.HasPrefix(receipt.ID, "receipt_") && len(receipt.ID) == len("receipt_")+sha256.Size*2 { - if _, err := hex.DecodeString(strings.TrimPrefix(receipt.ID, "receipt_")); err != nil { - return explorer.ErrCorruptReceipt - } - if err := receipt.ValidateID(); err != nil { - return explorer.ErrCorruptReceipt - } - } - return nil -} - -func sameReceipt(a, b explorer.CompilationReceipt) bool { - left, leftErr := explorer.ReceiptID(a) - right, rightErr := explorer.ReceiptID(b) - return leftErr == nil && rightErr == nil && left == right -} - -func (s *Store) PublishAuthoring(ctx context.Context, receipt explorer.CompilationReceipt, revision explorer.Revision) (*explorer.Revision, error) { - if err := validateReceipt(receipt); err != nil { - return nil, err - } - receiptDoc, err := document(receipt, receipt.ID) - if err != nil { - return nil, err - } - revisionDoc, err := document(revision, revision.ID) - if err != nil { - return nil, err - } - management := explorer.ManagementInteractive - if revision.ExplorerID == "default" { - management = explorer.ManagementRepository - } - var out *explorer.Revision - err = s.client.WithTransaction(ctx, store.TransactionCollections{Write: []string{ExplorersCollection, RevisionsCollection, CompilationReceiptsCollection}}, func(txCtx context.Context, tx store.Transaction) error { - if err := tx.QueryRows(txCtx, `INSERT @doc INTO @@c OPTIONS { overwriteMode: "ignore" } RETURN NEW`, 1, map[string]any{"@c": CompilationReceiptsCollection, "doc": receiptDoc}, func(map[string]any) error { return nil }); err != nil { - return err - } - var stored *explorer.CompilationReceipt - if err := tx.QueryRows(txCtx, `FOR d IN @@c FILTER d._key == @key AND d.project == @project AND d.explorerId == @explorerId RETURN d`, 1, map[string]any{"@c": CompilationReceiptsCollection, "key": receipt.ID, "project": receipt.Project, "explorerId": receipt.ExplorerID}, func(row map[string]any) error { - value, err := decode[explorer.CompilationReceipt](row) - if err != nil { - return err - } - if err := validateReceipt(value); err != nil { - return err - } - stored = &value - return nil - }); err != nil { - return err - } - if stored == nil { - return explorer.ErrNotFound - } - if !sameReceipt(*stored, receipt) { - return explorer.ErrCorruptReceipt - } - if err := tx.QueryRows(txCtx, `UPSERT { _key: @key } INSERT @doc UPDATE {} IN @@c RETURN NEW`, 1, map[string]any{"@c": RevisionsCollection, "key": revision.ID, "doc": revisionDoc}, func(map[string]any) error { return nil }); err != nil { - return err - } - row, found, err := readActivationRow(txCtx, tx, activationReadAQL, map[string]any{"@explorers": ExplorersCollection, "@revisions": RevisionsCollection, "explorerKey": explorerKey(revision.Project, revision.ExplorerID), "revisionKey": revision.ID, "project": revision.Project, "explorerId": revision.ExplorerID, "management": management}) - if err != nil { - return err - } - if !found { - return explorer.ErrDraftConflict - } - state, err := activationStateFromRow(row) - if err != nil { - return err - } - if err := activateRevisionAndOwner(txCtx, tx, state, time.Now().UTC()); err != nil { - return err - } - return tx.QueryRows(txCtx, `FOR d IN @@c FILTER d._key == @key RETURN d`, 1, map[string]any{"@c": RevisionsCollection, "key": revision.ID}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) - }) - return out, err -} - -func (s *Store) InsertRevision(ctx context.Context, revision explorer.Revision) (*explorer.Revision, error) { - if revision.ID == "" { - return nil, fmt.Errorf("revision ID is required") - } - doc, err := document(revision, revision.ID) - if err != nil { - return nil, err - } - var out *explorer.Revision - err = s.client.QueryRows(ctx, `UPSERT { _key: @key } INSERT @doc UPDATE {} IN @@c RETURN NEW`, 1, map[string]any{"@c": RevisionsCollection, "key": revision.ID, "doc": doc}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrNotFound - } - return out, nil -} - -func (s *Store) GetRevision(ctx context.Context, id string) (*explorer.Revision, error) { - var out *explorer.Revision - err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key RETURN d`, 1, map[string]any{"@c": RevisionsCollection, "key": id}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrNotFound - } - return out, nil -} -// TransitionRevision updates lifecycle fields only. The AQL never accepts a -// replacement definition/recipe document, preserving revision immutability. -func (s *Store) TransitionRevision(ctx context.Context, id string, status explorer.RevisionStatus, diagnostics []explorer.Diagnostic) (*explorer.Revision, error) { - now := time.Now().UTC() - patch := map[string]any{"status": status, "diagnostics": diagnostics} - if status == explorer.RevisionReady { - patch["readyAt"] = now - } - if status == explorer.RevisionFailed { - patch["failedAt"] = now - } - var out *explorer.Revision - err := s.client.QueryRows(ctx, `FOR d IN @@c FILTER d._key == @key UPDATE d WITH @patch IN @@c RETURN NEW`, 1, map[string]any{"@c": RevisionsCollection, "key": id, "patch": patch}, func(row map[string]any) error { value, err := decode[explorer.Revision](row); out = &value; return err }) - if err != nil { - return nil, err - } - if out == nil { - return nil, explorer.ErrNotFound - } - return out, nil -} - -const activationReadAQL = `LET owner = DOCUMENT(@@explorers, @explorerKey) -LET candidate = DOCUMENT(@@revisions, @revisionKey) -FILTER owner != null AND candidate != null AND owner.project == @project AND owner.explorerId == @explorerId AND owner.managementMode == @management AND candidate.project == @project AND candidate.explorerId == @explorerId -FILTER candidate.status == "READY" OR (candidate.status == "ACTIVE" AND owner.activeRevisionId == @revisionKey) -LET prior = owner.activeRevisionId == null ? null : DOCUMENT(@@revisions, owner.activeRevisionId) -RETURN {owner: owner, candidate: candidate, prior: prior}` - -const compositeActivationReadAQL = `LET manifest = DOCUMENT(@@lifecycle, @manifestKey) -LET active = DOCUMENT(@@lifecycle, @activeKey) -LET owner = DOCUMENT(@@explorers, @explorerKey) -LET candidate = DOCUMENT(@@revisions, @revisionKey) -LET execution = candidate == null OR LENGTH(candidate.materializations) == 0 ? null : DOCUMENT(@@executions, candidate.materializations[0].materializationId) -FILTER manifest != null AND manifest.recordType == "manifest" AND manifest.dataset.project == @project AND manifest.dataset.generation == @generation AND manifest.state IN ["STAGED", "READY"] -FILTER active != null AND active.recordType == "active_generation" AND active.project == @project -FILTER owner != null AND owner.project == @project AND owner.explorerId == "default" AND owner.managementMode == "REPOSITORY" -FILTER candidate != null AND candidate.project == @project AND candidate.explorerId == "default" AND candidate.sourceGeneration == @generation -FILTER candidate.status == "READY" OR (candidate.status == "ACTIVE" AND owner.activeRevisionId == @revisionKey) -FILTER execution != null AND execution.project == @project AND execution.datasetGeneration == @generation AND execution.state == "PUBLISHED" -LET prior = owner.activeRevisionId == null ? null : DOCUMENT(@@revisions, owner.activeRevisionId) -RETURN {manifest: manifest, active: active, owner: owner, candidate: candidate, prior: prior}` - -const activationUpdateAQL = `FOR d IN @@c -FILTER d._key == @key -UPDATE d WITH @patch IN @@c -RETURN NEW` - -type activationState struct { - owner map[string]any - candidate map[string]any - prior map[string]any -} - -// ActivateRepositoryGeneration is the repository-only composite visibility -// switch. It validates all pointers first, then performs each single-collection -// modification inside one Arango transaction so no partial activation is -// visible if any update fails. -func (s *Store) ActivateRepositoryGeneration(ctx context.Context, project, generation, revisionID string) error { - return s.client.WithTransaction(ctx, store.TransactionCollections{ - Read: []string{publicationarango.BundleExecutionsCollection}, - Write: []string{datasetarango.LifecycleCollection, ExplorersCollection, RevisionsCollection}, - }, func(txCtx context.Context, tx store.Transaction) error { - row, found, err := readActivationRow(txCtx, tx, compositeActivationReadAQL, map[string]any{ - "@lifecycle": datasetarango.LifecycleCollection, "@explorers": ExplorersCollection, - "@revisions": RevisionsCollection, "@executions": publicationarango.BundleExecutionsCollection, - "manifestKey": datasetarango.ManifestDocumentKey(dataset.Ref{Project: project, Generation: generation}), - "activeKey": datasetarango.ActiveDocumentKey(project), "explorerKey": explorerKey(project, "default"), - "revisionKey": revisionID, "project": project, "generation": generation, - }) - if err != nil { - return err - } - if !found { - return explorer.ErrDraftConflict - } - state, err := activationStateFromRow(row) - if err != nil { - return err - } - manifest, err := requiredActivationDocument(row, "manifest") - if err != nil { - return err - } - active, err := requiredActivationDocument(row, "active") - if err != nil { - return err - } - activeKey, err := activationDocumentKey(active) - if err != nil { - return err - } - manifestKey, err := activationDocumentKey(manifest) - if err != nil { - return err - } - dataset, ok := manifest["dataset"] - if !ok { - return fmt.Errorf("activation manifest has no dataset") - } - if err := updateActivationDocument(txCtx, tx, datasetarango.LifecycleCollection, activeKey, map[string]any{"dataset": dataset, "manifestKey": manifestKey}); err != nil { - return err - } - return activateRevisionAndOwner(txCtx, tx, state, time.Now().UTC()) - }) -} - -func readActivationRow(ctx context.Context, tx store.Transaction, query string, binds map[string]any) (map[string]any, bool, error) { - var row map[string]any - err := tx.QueryRows(ctx, query, 1, binds, func(value map[string]any) error { - if row != nil { - return fmt.Errorf("activation guard returned more than one row") - } - row = value - return nil - }) - return row, row != nil, err -} - -func activationStateFromRow(row map[string]any) (activationState, error) { - owner, err := requiredActivationDocument(row, "owner") - if err != nil { - return activationState{}, err - } - candidate, err := requiredActivationDocument(row, "candidate") - if err != nil { - return activationState{}, err - } - prior, err := optionalActivationDocument(row, "prior") - if err != nil { - return activationState{}, err - } - return activationState{owner: owner, candidate: candidate, prior: prior}, nil -} - -func requiredActivationDocument(row map[string]any, field string) (map[string]any, error) { - document, err := optionalActivationDocument(row, field) - if err != nil { - return nil, err - } - if document == nil { - return nil, fmt.Errorf("activation guard returned no %s document", field) - } - return document, nil -} - -func optionalActivationDocument(row map[string]any, field string) (map[string]any, error) { - value := row[field] - if value == nil { - return nil, nil - } - document, ok := value.(map[string]any) - if !ok { - return nil, fmt.Errorf("activation guard returned invalid %s document", field) - } - return document, nil -} - -func activationDocumentKey(document map[string]any) (string, error) { - key, ok := document["_key"].(string) - if !ok || key == "" { - return "", fmt.Errorf("activation document has no _key") - } - return key, nil -} - -func activateRevisionAndOwner(ctx context.Context, tx store.Transaction, state activationState, now time.Time) error { - candidateKey, err := activationDocumentKey(state.candidate) - if err != nil { - return err - } - candidate, err := decode[explorer.Revision](state.candidate) - if err != nil { - return fmt.Errorf("decode activation candidate: %w", err) - } - if state.prior != nil { - priorKey, err := activationDocumentKey(state.prior) - if err != nil { - return err - } - if priorKey != candidateKey { - if err := updateActivationDocument(ctx, tx, RevisionsCollection, priorKey, map[string]any{"status": explorer.RevisionSuperseded}); err != nil { - return err - } - } - } - if err := updateActivationDocument(ctx, tx, RevisionsCollection, candidateKey, map[string]any{"status": explorer.RevisionActive, "activatedAt": now}); err != nil { - return err - } - ownerKey, err := activationDocumentKey(state.owner) - if err != nil { - return err - } - return updateActivationDocument(ctx, tx, ExplorersCollection, ownerKey, activeExplorerPatch(candidate, candidateKey, now)) -} - -func activeExplorerPatch(candidate explorer.Revision, revisionKey string, now time.Time) map[string]any { - publication := candidate.Publication - materializations, dataset := explorer.WithDataframeSelectors(candidate.Recipe, candidate.Materializations, candidate.Dataset) - publication.State = string(explorer.RevisionActive) - publication.RevisionID = revisionKey - publication.UpdatedAt = now - return map[string]any{ - "activeRevisionId": revisionKey, "activeConfig": candidate.Config, - "recipeDigest": candidate.RecipeDigest, "resolvedSchemaDigest": candidate.ResolvedSchemaDigest, - "sourceGeneration": candidate.SourceGeneration, "dataset": dataset, - "publication": publication, "emittedColumns": candidate.EmittedColumns, - "materializations": materializations, "diagnostics": candidate.Diagnostics, - } -} - -func updateActivationDocument(ctx context.Context, tx store.Transaction, collection, key string, patch map[string]any) error { - var updated bool - err := tx.QueryRows(ctx, activationUpdateAQL, 1, map[string]any{"@c": collection, "key": key, "patch": patch}, func(map[string]any) error { - if updated { - return fmt.Errorf("activation update returned more than one document") - } - updated = true - return nil - }) - if err != nil { - return err - } - if !updated { - return explorer.ErrDraftConflict - } - return nil -} +func explorerKey(project, id string) string { return "explorer_" + key(project, id) } var _ explorer.Store = (*Store)(nil) - -func decode[T any](value any) (T, error) { - var out T - raw, err := json.Marshal(normalizeUpdatedAt(value)) - if err != nil { - return out, err - } - err = json.Unmarshal(raw, &out) - return out, err -} - -// normalizeUpdatedAt keeps reads tolerant of the numeric Arango timestamp -// written by the short-lived last-write-wins regression. New writes use the -// normal JSON time string representation, but existing documents must remain -// readable so they can be repaired by a subsequent draft save. -func normalizeUpdatedAt(value any) any { - row, ok := value.(map[string]any) - if !ok { - return value - } - normalized := make(map[string]any, len(row)) - for key, item := range row { - normalized[key] = item - } - if timestamp, ok := numericTimestamp(row["updatedAt"]); ok { - normalized["updatedAt"] = timestamp - } - return normalized -} - -func numericTimestamp(value any) (string, bool) { - var millis int64 - switch number := value.(type) { - case float64: - millis = int64(number) - case float32: - millis = int64(number) - case int: - millis = int64(number) - case int64: - millis = number - case uint64: - millis = int64(number) - default: - return "", false - } - return time.UnixMilli(millis).UTC().Format(time.RFC3339Nano), true -} -func document(value any, k string) (map[string]any, error) { - raw, err := json.Marshal(value) - if err != nil { - return nil, err - } - var doc map[string]any - if err = json.Unmarshal(raw, &doc); err != nil { - return nil, err - } - doc["_key"] = k - return doc, nil -} diff --git a/internal/explorer/arango/store_test.go b/internal/explorer/arango/store_test.go index 9e61ecf0..8753d8a0 100644 --- a/internal/explorer/arango/store_test.go +++ b/internal/explorer/arango/store_test.go @@ -90,8 +90,11 @@ func TestActivateRepositoryGenerationUsesCompositeGuards(t *testing.T) { for _, call := range client.calls { allQueries += call.query } - if got := strings.Count(allQueries, "UPDATE d WITH"); got != 3 { - t.Fatalf("composite activation updates=%d, want 3:\n%s", got, allQueries) + if got := strings.Count(allQueries, "UPDATE d WITH"); got != 2 { + t.Fatalf("composite activation revision/dataset updates=%d, want 2:\n%s", got, allQueries) + } + if !strings.Contains(allQueries, "REPLACE d WITH MERGE(UNSET(d") { + t.Fatalf("owner activation does not remove legacy generated fields:\n%s", allQueries) } if got := client.calls[0].binds["generation"]; got != "generation-a" { t.Fatalf("generation bind=%v", got) diff --git a/internal/explorer/authoringv2/canonical.go b/internal/explorer/authoringv2/canonical.go index 2b833f3f..01a4634a 100644 --- a/internal/explorer/authoringv2/canonical.go +++ b/internal/explorer/authoringv2/canonical.go @@ -10,30 +10,6 @@ import ( "sort" ) -func (d Document) CanonicalJSON() ([]byte, error) { - if err := d.Validate(); err != nil { - return nil, err - } - n := d - n.RouteSteps = append([]RouteStep(nil), d.RouteSteps...) - n.Selections = append([]Selection(nil), d.Selections...) - sort.SliceStable(n.Selections, func(i, j int) bool { - left := n.Selections[i].CandidateID + "\x00" + n.Selections[i].OccurrenceID - right := n.Selections[j].CandidateID + "\x00" + n.Selections[j].OccurrenceID - return left < right - }) - if n.RouteSteps == nil { - n.RouteSteps = []RouteStep{} - } - if n.Selections == nil { - n.Selections = []Selection{} - } - if n.Presentation == nil { - n.Presentation = map[string]Presentation{} - } - return json.Marshal(n) -} - func (w Workspace) CanonicalJSON() ([]byte, error) { if err := w.Validate(); err != nil { return nil, err @@ -45,21 +21,6 @@ func (w Workspace) CanonicalJSON() ([]byte, error) { if n.Documents[i].Columns == nil { n.Documents[i].Columns = []Column{} } - n.Documents[i].Selections = append([]Selection(nil), n.Documents[i].Selections...) - sort.SliceStable(n.Documents[i].Selections, func(a, b int) bool { - left := n.Documents[i].Selections[a] - right := n.Documents[i].Selections[b] - return left.CandidateID+"\x00"+left.OccurrenceID+"\x00"+left.ProjectionMode < right.CandidateID+"\x00"+right.OccurrenceID+"\x00"+right.ProjectionMode - }) - if n.Documents[i].RouteSteps == nil { - n.Documents[i].RouteSteps = []RouteStep{} - } - if n.Documents[i].Selections == nil { - n.Documents[i].Selections = []Selection{} - } - if n.Documents[i].Presentation == nil { - n.Documents[i].Presentation = map[string]Presentation{} - } } n.Tabs = append([]Tab(nil), w.Tabs...) if n.Documents == nil { @@ -195,15 +156,6 @@ func (w Workspace) Digest() (string, error) { return "sha256:" + hex.EncodeToString(sum[:]), nil } -func (d Document) Digest() (string, error) { - raw, err := d.CanonicalJSON() - if err != nil { - return "", err - } - sum := sha256.Sum256(raw) - return "sha256:" + hex.EncodeToString(sum[:]), nil -} - func (c CatalogSnapshot) CanonicalJSON() ([]byte, error) { if err := c.Validate(); err != nil { return nil, err diff --git a/internal/explorer/authoringv2/commands.go b/internal/explorer/authoringv2/commands.go index 978882fc..13b05469 100644 --- a/internal/explorer/authoringv2/commands.go +++ b/internal/explorer/authoringv2/commands.go @@ -17,6 +17,7 @@ const ( CommandReorderTables = "REORDER_TABLES" CommandSetTableRoot = "SET_TABLE_ROOT" CommandAddRoute = "ADD_ROUTE" + CommandUpdateRouteEdge = "UPDATE_ROUTE_EDGE" CommandRemoveRoute = "REMOVE_ROUTE" CommandAddColumn = "ADD_COLUMN" CommandUpdateColumn = "UPDATE_COLUMN" @@ -142,6 +143,10 @@ func (c Command) validate() error { if !required(c.OutputID, c.ParentOccurrenceID, c.EdgeID) { return fmt.Errorf("ADD_ROUTE requires outputId, parentOccurrenceId, and edgeId") } + case CommandUpdateRouteEdge: + if !required(c.OutputID, c.OccurrenceID, c.EdgeID) || c.OccurrenceID == RootOccurrenceID { + return fmt.Errorf("UPDATE_ROUTE_EDGE requires a non-root occurrenceId and edgeId") + } case CommandRemoveRoute: if !required(c.OutputID, c.OccurrenceID) || c.OccurrenceID == RootOccurrenceID { return fmt.Errorf("REMOVE_ROUTE requires a non-root occurrenceId") @@ -290,12 +295,47 @@ func applyCommand(workspace *Workspace, catalog CatalogSnapshot, commandID strin if !fromOK || !toOK || from.ResourceType != parent.ResourceType { return result, fmt.Errorf("edge %q does not extend occurrence %q", edge.ID, command.ParentOccurrenceID) } + if routeUsesRelationship(&workspace.Documents[document].Route, from.ResourceType, to.ResourceType, edge.Label, "") { + return result, fmt.Errorf("edge %q is already used in this query", edge.ID) + } occurrenceID := commandGeneratedID("occ_", commandID, index, command.Type) if findRoute(&workspace.Documents[document].Route, occurrenceID) != nil { return CommandResult{Type: CommandResultRouteAdded, OutputID: command.OutputID, OccurrenceID: occurrenceID}, nil } parent.Children = append(parent.Children, RouteNode{OccurrenceID: occurrenceID, ResourceType: to.ResourceType, Relationship: edge.Label}) return CommandResult{Type: CommandResultRouteAdded, OutputID: command.OutputID, OccurrenceID: occurrenceID}, nil + case CommandUpdateRouteEdge: + document := documentIndex(workspace, command.OutputID) + if document < 0 { + return result, fmt.Errorf("output %q was not found", command.OutputID) + } + parent, occurrence := findRouteWithParent(&workspace.Documents[document].Route, command.OccurrenceID) + edge, ok := catalogEdge(catalog, command.EdgeID) + if parent == nil || occurrence == nil || !ok { + return result, fmt.Errorf("route occurrence, parent, or edge was not found") + } + from, fromOK := catalogNode(catalog, edge.FromNodeID) + to, toOK := catalogNode(catalog, edge.ToNodeID) + if !fromOK || !toOK || from.ResourceType != parent.ResourceType || to.ResourceType != occurrence.ResourceType { + return result, fmt.Errorf("edge %q cannot replace the relationship for occurrence %q", edge.ID, command.OccurrenceID) + } + currentEdges := []CatalogEdge{} + for _, candidate := range catalog.Edges { + candidateFrom, candidateFromOK := catalogNode(catalog, candidate.FromNodeID) + candidateTo, candidateToOK := catalogNode(catalog, candidate.ToNodeID) + if candidateFromOK && candidateToOK && candidateFrom.ResourceType == parent.ResourceType && candidateTo.ResourceType == occurrence.ResourceType && candidate.Label == occurrence.Relationship { + currentEdges = append(currentEdges, candidate) + } + } + if len(currentEdges) != 1 || currentEdges[0].FromNodeID != edge.FromNodeID || currentEdges[0].ToNodeID != edge.ToNodeID { + return result, fmt.Errorf("edge %q must preserve the catalog endpoints for occurrence %q", edge.ID, command.OccurrenceID) + } + if routeUsesRelationship(&workspace.Documents[document].Route, from.ResourceType, to.ResourceType, edge.Label, occurrence.OccurrenceID) { + return result, fmt.Errorf("edge %q is already used by another occurrence", edge.ID) + } + occurrence.Relationship = edge.Label + result.OccurrenceID = occurrence.OccurrenceID + return result, nil case CommandRemoveRoute: document := documentIndex(workspace, command.OutputID) if document < 0 { @@ -483,6 +523,35 @@ func findRoute(route *RouteNode, occurrenceID string) *RouteNode { return nil } +func findRouteWithParent(route *RouteNode, occurrenceID string) (*RouteNode, *RouteNode) { + for i := range route.Children { + if route.Children[i].OccurrenceID == occurrenceID { + return route, &route.Children[i] + } + if parent, found := findRouteWithParent(&route.Children[i], occurrenceID); found != nil { + return parent, found + } + } + return nil, nil +} + +func routeUsesRelationship(route *RouteNode, fromResourceType, toResourceType, relationship, exceptOccurrenceID string) bool { + if route.ResourceType == fromResourceType { + for i := range route.Children { + child := &route.Children[i] + if child.OccurrenceID != exceptOccurrenceID && child.ResourceType == toResourceType && child.Relationship == relationship { + return true + } + } + } + for i := range route.Children { + if routeUsesRelationship(&route.Children[i], fromResourceType, toResourceType, relationship, exceptOccurrenceID) { + return true + } + } + return false +} + func removeRoute(route *RouteNode, occurrenceID string, removed map[string]bool) bool { for i := range route.Children { if route.Children[i].OccurrenceID == occurrenceID { diff --git a/internal/explorer/authoringv2/commands_test.go b/internal/explorer/authoringv2/commands_test.go index 9fcdab0d..0e9ed695 100644 --- a/internal/explorer/authoringv2/commands_test.go +++ b/internal/explorer/authoringv2/commands_test.go @@ -64,6 +64,60 @@ func TestApplyCommandsOwnsNestedRouteAndColumnIdentities(t *testing.T) { } } +func TestApplyCommandsUpdatesRouteEdgeWithoutReplacingOccurrenceState(t *testing.T) { + catalog := commandCatalog() + catalog.Edges = append(catalog.Edges, CatalogEdge{ID: "patient-encounter-secondary", FromNodeID: "patient", ToNodeID: "encounter", Label: "researchEncounters"}) + workspace, create, err := ApplyCommands(emptyCommandWorkspace(), catalog, "create", []Command{{Type: CommandCreateTable, Title: "Patients", RootNodeID: "patient"}}) + if err != nil { + t.Fatal(err) + } + outputID := create[0].OutputID + workspace, route, err := ApplyCommands(workspace, catalog, "route", []Command{{Type: CommandAddRoute, OutputID: outputID, ParentOccurrenceID: RootOccurrenceID, EdgeID: "patient-encounter"}}) + if err != nil { + t.Fatal(err) + } + occurrenceID := route[0].OccurrenceID + workspace, _, err = ApplyCommands(workspace, catalog, "column", []Command{{Type: CommandAddColumn, OutputID: outputID, OccurrenceID: occurrenceID, CandidateID: "encounter-id"}}) + if err != nil { + t.Fatal(err) + } + workspace.Documents[0].Route.Children[0].Children = []RouteNode{{OccurrenceID: "nested", ResourceType: "Encounter", Relationship: "revisits"}} + + updated, _, err := ApplyCommands(workspace, catalog, "update-edge", []Command{{Type: CommandUpdateRouteEdge, OutputID: outputID, OccurrenceID: occurrenceID, EdgeID: "patient-encounter-secondary"}}) + if err != nil { + t.Fatal(err) + } + child := updated.Documents[0].Route.Children[0] + if child.OccurrenceID != occurrenceID || child.Relationship != "researchEncounters" || len(child.Children) != 1 || child.Children[0].OccurrenceID != "nested" { + t.Fatalf("updated route=%#v", child) + } + if len(updated.Documents[0].Columns) != 1 || updated.Documents[0].Columns[0].OccurrenceID != occurrenceID { + t.Fatalf("columns were not preserved: %#v", updated.Documents[0].Columns) + } +} + +func TestApplyCommandsRejectsReusingRelationshipFromAnotherOccurrence(t *testing.T) { + catalog := commandCatalog() + catalog.Edges = append(catalog.Edges, CatalogEdge{ID: "encounter-patient", FromNodeID: "encounter", ToNodeID: "patient", Label: "patient"}) + workspace, create, err := ApplyCommands(emptyCommandWorkspace(), catalog, "create", []Command{{Type: CommandCreateTable, Title: "Patients", RootNodeID: "patient"}}) + if err != nil { + t.Fatal(err) + } + outputID := create[0].OutputID + workspace, encounter, err := ApplyCommands(workspace, catalog, "encounter", []Command{{Type: CommandAddRoute, OutputID: outputID, ParentOccurrenceID: RootOccurrenceID, EdgeID: "patient-encounter"}}) + if err != nil { + t.Fatal(err) + } + workspace, patient, err := ApplyCommands(workspace, catalog, "patient", []Command{{Type: CommandAddRoute, OutputID: outputID, ParentOccurrenceID: encounter[0].OccurrenceID, EdgeID: "encounter-patient"}}) + if err != nil { + t.Fatal(err) + } + _, _, err = ApplyCommands(workspace, catalog, "repeat", []Command{{Type: CommandAddRoute, OutputID: outputID, ParentOccurrenceID: patient[0].OccurrenceID, EdgeID: "patient-encounter"}}) + if err == nil || !strings.Contains(err.Error(), "already used in this query") { + t.Fatalf("error=%v", err) + } +} + func TestApplyCommandsAddsFilterAndChartWithoutMakingColumnTableVisible(t *testing.T) { catalog := commandCatalog() catalog.Candidates[0].Filterable = true diff --git a/internal/explorer/authoringv2/semantic_types.go b/internal/explorer/authoringv2/semantic_types.go index 42ea285e..67880a35 100644 --- a/internal/explorer/authoringv2/semantic_types.go +++ b/internal/explorer/authoringv2/semantic_types.go @@ -27,10 +27,14 @@ type RouteNode struct { } type ColumnSource struct { - Kind string `json:"kind"` - FieldPath string `json:"fieldPath,omitempty"` - Match string `json:"match,omitempty"` - ProjectionMode string `json:"projectionMode,omitempty"` + Kind string `json:"kind"` + FieldPath string `json:"fieldPath,omitempty"` + Match string `json:"match,omitempty"` + ProjectionMode string `json:"projectionMode,omitempty"` + Operation string `json:"operation,omitempty"` + WherePath string `json:"wherePath,omitempty"` + WhereEquals string `json:"whereEquals,omitempty"` + RequiredValues []string `json:"requiredValues,omitempty"` } const ( @@ -40,6 +44,7 @@ const ( SourceCodingBySystem = "codingBySystem" SourceObservationComponentByCode = "observationComponentByCode" SourceProjectID = "projectId" + SourceAggregate = "aggregate" ) type Column struct { @@ -82,16 +87,7 @@ type FileActions struct { var physicalColumnPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) -func (d Document) semantic() bool { - return strings.TrimSpace(d.RootResourceType) != "" || strings.TrimSpace(d.Route.ResourceType) != "" || len(d.Columns) != 0 -} - -func (d Document) UsesSemanticColumns() bool { return d.semantic() } - func (d Document) semanticOccurrences() (map[string]RouteNode, error) { - if !d.semantic() { - return nil, nil - } if strings.TrimSpace(d.RootResourceType) == "" { return nil, fmt.Errorf("rootResourceType is required") } @@ -142,8 +138,45 @@ func (s ColumnSource) validate(path string) error { return fmt.Errorf("%s %s source requires match", path, s.Kind) } case SourceProjectID: - if strings.TrimSpace(s.FieldPath) != "" || strings.TrimSpace(s.Match) != "" { - return fmt.Errorf("%s projectId source has no parameters", path) + if strings.TrimSpace(s.FieldPath) != "" || strings.TrimSpace(s.Match) != "" || strings.TrimSpace(s.Operation) != "" || strings.TrimSpace(s.WherePath) != "" || strings.TrimSpace(s.WhereEquals) != "" || len(s.RequiredValues) != 0 || mode != "FIRST" { + return fmt.Errorf("%s projectId source only supports FIRST projectionMode and no other parameters", path) + } + case SourceAggregate: + if strings.TrimSpace(s.Match) != "" || strings.TrimSpace(s.ProjectionMode) != "" { + return fmt.Errorf("%s aggregate source forbids match and projectionMode", path) + } + op := strings.ToUpper(strings.TrimSpace(s.Operation)) + switch op { + case "COUNT", "COUNT_DISTINCT", "DISTINCT_VALUES", "MIN", "MAX", "EXISTS", "CONTAINS_ALL": + default: + return fmt.Errorf("%s aggregate source operation %q is unsupported", path, s.Operation) + } + requiresField := op == "COUNT_DISTINCT" || op == "DISTINCT_VALUES" || op == "MIN" || op == "MAX" || op == "CONTAINS_ALL" + if requiresField && strings.TrimSpace(s.FieldPath) == "" { + return fmt.Errorf("%s aggregate operation %s requires fieldPath", path, op) + } + if !requiresField && strings.TrimSpace(s.FieldPath) != "" { + return fmt.Errorf("%s aggregate operation %s forbids fieldPath", path, op) + } + if strings.TrimSpace(s.WhereEquals) != "" && strings.TrimSpace(s.WherePath) == "" { + return fmt.Errorf("%s.whereEquals requires wherePath", path) + } + if op == "CONTAINS_ALL" { + if len(s.RequiredValues) == 0 { + return fmt.Errorf("%s.requiredValues is required for CONTAINS_ALL", path) + } + seen := map[string]bool{} + for i, value := range s.RequiredValues { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("%s.requiredValues[%d] must be non-empty", path, i) + } + if seen[value] { + return fmt.Errorf("%s.requiredValues[%d] is duplicated", path, i) + } + seen[value] = true + } + } else if len(s.RequiredValues) != 0 { + return fmt.Errorf("%s.requiredValues is only valid for CONTAINS_ALL", path) } default: return fmt.Errorf("%s source kind %q is unsupported", path, s.Kind) diff --git a/internal/explorer/authoringv2/types.go b/internal/explorer/authoringv2/types.go index 07baa2e3..6dc6b4c3 100644 --- a/internal/explorer/authoringv2/types.go +++ b/internal/explorer/authoringv2/types.go @@ -3,11 +3,6 @@ // contracts: this package describes user intent and catalog facts only. package authoringv2 -import ( - "net/url" - "strings" -) - const ( APIVersion = "loom.calypr.org/explorer-authoring/v2" Kind = "ExplorerBuilderDocument" @@ -17,9 +12,8 @@ const ( RootOccurrenceID = "base" ) -// Document is the complete durable Builder intent. A route is a finite edge -// path from RootNodeID. Its root occurrence is always "base" and its tail is -// computed from the path; neither is a second authored node identity. +// Document is the complete durable Builder intent. Route occurrences form a +// semantic resource tree rooted at RootResourceType. type Document struct { APIVersion string `json:"-"` Kind string `json:"kind"` @@ -29,13 +23,6 @@ type Document struct { Columns []Column `json:"columns"` FixedFilters []FixedFilter `json:"fixedFilters,omitempty"` Actions []Action `json:"actions,omitempty"` - - // The opaque, linear authoring model is retained only for source-compatible - // internal tests while the hard-cut wire decoder rejects those fields. - RootNodeID string `json:"-"` - RouteSteps []RouteStep `json:"-"` - Selections []Selection `json:"-"` - Presentation map[string]Presentation `json:"-"` } type Output struct { @@ -44,17 +31,6 @@ type Output struct { RowLabel string `json:"rowLabel,omitempty"` } -type RouteStep struct { - EdgeID string `json:"edgeId"` - OccurrenceID string `json:"occurrenceId,omitempty"` -} - -type Selection struct { - CandidateID string `json:"candidateId"` - OccurrenceID string `json:"occurrenceId,omitempty"` - ProjectionMode string `json:"projectionMode"` -} - // Workspace is the atomic authoring unit. Documents are independent table // intents; tabs provide their ordered, visible runtime presentation. type Workspace struct { @@ -75,17 +51,6 @@ type Tab struct { Visible bool `json:"visible"` } -// Presentation contains display intent only. In particular, it has no -// selector, expression, physical collection, or generated-column fields. -type Presentation struct { - Label string `json:"label,omitempty"` - Visible *bool `json:"visible,omitempty"` - Order *int `json:"order,omitempty"` - Table *TablePresentation `json:"table,omitempty"` - Filter *FilterPresentation `json:"filter,omitempty"` - Chart *ChartPresentation `json:"chart,omitempty"` -} - type TablePresentation struct { Visible *bool `json:"visible,omitempty"` Order *int `json:"order,omitempty"` @@ -102,11 +67,6 @@ type ChartPresentation struct { Order *int `json:"order,omitempty"` } -func PresentationKey(candidateID, occurrenceID, projectionMode string) string { - encode := func(value string) string { return strings.ReplaceAll(url.QueryEscape(value), "+", "%20") } - return encode(candidateID) + "::" + encode(occurrenceID) + "::" + encode(projectionMode) -} - // CatalogSnapshot is an immutable, authorization-scoped projection. The // token identifies the exact snapshot used to validate a document. type CatalogSnapshot struct { @@ -183,22 +143,11 @@ type RoutePolicy struct { // BuilderState joins one workspace to the one catalog snapshot that proves it. type BuilderState struct { - APIVersion string `json:"apiVersion"` - Kind string `json:"kind"` - LifecycleState string `json:"lifecycleState"` - DraftVersion int64 `json:"draftVersion"` - DraftDigest string `json:"draftDigest"` - Workspace *Workspace `json:"workspace"` - // Document is retained only as a source-compatible internal migration aid. - Document *Document `json:"-"` - Catalog CatalogSnapshot `json:"catalog"` -} - -// RouteOccurrence is derived, never wire-authored. The first occurrence has -// ID "base". Unnamed route steps receive stable step-N IDs; TailOccurrence -// returns the final derived occurrence (base for an empty route). -type RouteOccurrence struct { - ID string - NodeID string - IncomingEdgeID string + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + LifecycleState string `json:"lifecycleState"` + DraftVersion int64 `json:"draftVersion"` + DraftDigest string `json:"draftDigest"` + Workspace *Workspace `json:"workspace"` + Catalog CatalogSnapshot `json:"catalog"` } diff --git a/internal/explorer/authoringv2/types_test.go b/internal/explorer/authoringv2/types_test.go index 1e00188a..b2be4159 100644 --- a/internal/explorer/authoringv2/types_test.go +++ b/internal/explorer/authoringv2/types_test.go @@ -26,80 +26,6 @@ func testCatalog() CatalogSnapshot { } } -func testDocument() Document { - return Document{ - APIVersion: APIVersion, Kind: Kind, Output: Output{ID: "out", Title: "Patients"}, RootNodeID: "patient", - RouteSteps: []RouteStep{{EdgeID: "patient-encounter", OccurrenceID: "first"}, {EdgeID: "encounter-self", OccurrenceID: "second"}, {EdgeID: "encounter-self", OccurrenceID: "third"}}, - Selections: []Selection{{CandidateID: "patient-id", OccurrenceID: "base", ProjectionMode: "SCALAR"}, {CandidateID: "encounter-id", OccurrenceID: "third", ProjectionMode: "SCALAR"}}, - Presentation: map[string]Presentation{PresentationKey("patient-id", "base", "SCALAR"): {Label: "Patient ID"}}, - } -} - -func TestBuilderStateSupportsRepeatedOccurrencesSelfLoopsAndLongFiniteRoutes(t *testing.T) { - catalog := testCatalog() - document := testDocument() - for i := 4; i < 12; i++ { - document.RouteSteps = append(document.RouteSteps, RouteStep{EdgeID: "encounter-self", OccurrenceID: "step-explicit-" + string(rune('a'+i))}) - } - state := BuilderState{APIVersion: APIVersion, Kind: StateKind, Document: &document, Catalog: catalog} - if err := state.Validate(); err != nil { - t.Fatal(err) - } - tail, err := document.TailOccurrence(catalog) - if err != nil || tail.ID != "step-explicit-l" { - t.Fatalf("tail=%#v err=%v", tail, err) - } -} - -func TestBuilderStateRejectsStaleAndDuplicateStructure(t *testing.T) { - catalog := testCatalog() - document := testDocument() - document.RouteSteps = append(document.RouteSteps, RouteStep{EdgeID: "missing"}) - state := BuilderState{APIVersion: APIVersion, Kind: StateKind, Document: &document, Catalog: catalog} - if err := state.Validate(); err == nil || !strings.Contains(err.Error(), "stale edge") { - t.Fatalf("stale edge error=%v", err) - } - document = testDocument() - document.Selections = append(document.Selections, document.Selections[0]) - if err := document.Validate(); err == nil || !strings.Contains(err.Error(), "duplicate selection") { - t.Fatalf("duplicate selection error=%v", err) - } - catalog.Edges = append(catalog.Edges, catalog.Edges[0]) - if err := catalog.Validate(); err == nil || !strings.Contains(err.Error(), "duplicate catalog edge") { - t.Fatalf("duplicate edge error=%v", err) - } -} - -func TestCanonicalizationAndDigestAreStableAcrossUnorderedCollections(t *testing.T) { - first := testDocument() - second := testDocument() - first.Selections = []Selection{first.Selections[1], first.Selections[0]} - second.Presentation = map[string]Presentation{PresentationKey("patient-id", "base", "SCALAR"): {Label: "Patient ID"}} - first.Presentation = map[string]Presentation{PresentationKey("patient-id", "base", "SCALAR"): {Label: "Patient ID"}} - a, err := first.CanonicalJSON() - if err != nil { - t.Fatal(err) - } - b, err := second.CanonicalJSON() - if err != nil { - t.Fatal(err) - } - if string(a) != string(b) { - t.Fatalf("canonical JSON differs:\n%s\n%s", a, b) - } - da, err := first.Digest() - if err != nil { - t.Fatal(err) - } - db, err := second.Digest() - if err != nil { - t.Fatal(err) - } - if da != db || !strings.HasPrefix(da, "sha256:") { - t.Fatalf("digests=%q,%q", da, db) - } -} - func TestEmptyBuilderStateAndBackendLeakage(t *testing.T) { state := BuilderState{APIVersion: APIVersion, Kind: StateKind, Catalog: testCatalog()} if err := state.Validate(); err != nil { diff --git a/internal/explorer/authoringv2/validation.go b/internal/explorer/authoringv2/validation.go index a9cbd589..145a62d4 100644 --- a/internal/explorer/authoringv2/validation.go +++ b/internal/explorer/authoringv2/validation.go @@ -5,51 +5,6 @@ import ( "strings" ) -func (d Document) Occurrences(c CatalogSnapshot) ([]RouteOccurrence, error) { - if err := d.Validate(); err != nil { - return nil, err - } - edges := c.edgeIndex() - current := d.RootNodeID - occ := []RouteOccurrence{{ID: RootOccurrenceID, NodeID: current}} - for i, step := range d.RouteSteps { - e := edges[step.EdgeID] - if e == nil { - return nil, fmt.Errorf("route step %d references unknown edge %q", i, step.EdgeID) - } - if e.FromNodeID != current { - return nil, fmt.Errorf("route step %d is disconnected: edge starts at %q, expected %q", i, e.FromNodeID, current) - } - id := step.OccurrenceID - if id == "" { - id = DerivedOccurrenceID(i) - } - occ = append(occ, RouteOccurrence{ID: id, NodeID: e.ToNodeID, IncomingEdgeID: e.ID}) - current = e.ToNodeID - } - return occ, nil -} - -func (d Document) TailOccurrence(c CatalogSnapshot) (RouteOccurrence, error) { - occ, err := d.Occurrences(c) - if err != nil { - return RouteOccurrence{}, err - } - return occ[len(occ)-1], nil -} - -func (d Document) TailOccurrenceID(c CatalogSnapshot) (string, error) { - tail, err := d.TailOccurrence(c) - if err != nil { - return "", err - } - return tail.ID, nil -} - -func DerivedOccurrenceID(stepIndex int) string { - return fmt.Sprintf("step-%d", stepIndex+1) -} - func (d Document) Validate() error { if d.Kind != Kind { return fmt.Errorf("unsupported V2 document protocol or kind") @@ -60,73 +15,14 @@ func (d Document) Validate() error { if !physicalColumnPattern.MatchString(d.Output.ID) { return fmt.Errorf("output id must contain only letters, digits, and underscores and may not start with a digit") } - if d.semantic() { - return d.validateSemantic() - } - if emptyID(d.RootNodeID) { - return fmt.Errorf("rootNodeId is required") - } - seenOccurrences := map[string]bool{RootOccurrenceID: true} - for i, step := range d.RouteSteps { - if emptyID(step.EdgeID) { - return fmt.Errorf("routeSteps[%d].edgeId is required", i) - } - occurrenceID := step.OccurrenceID - if occurrenceID == "" { - occurrenceID = DerivedOccurrenceID(i) - } - if step.OccurrenceID != "" { - if step.OccurrenceID == RootOccurrenceID { - return fmt.Errorf("routeSteps[%d] cannot author the derived base occurrence", i) - } - if emptyID(step.OccurrenceID) { - return fmt.Errorf("routeSteps[%d].occurrenceId is invalid", i) - } - } - if seenOccurrences[occurrenceID] { - return fmt.Errorf("duplicate route occurrence id %q", occurrenceID) - } - seenOccurrences[occurrenceID] = true - } - seenSelections := map[string]bool{} - for i, selection := range d.Selections { - if emptyID(selection.CandidateID) { - return fmt.Errorf("selections[%d].candidateId is required", i) - } - if strings.TrimSpace(selection.ProjectionMode) == "" { - return fmt.Errorf("selections[%d].projectionMode is required", i) - } - if selection.OccurrenceID == "base" && selection.OccurrenceID != "" { /* base is valid */ - } - if selection.OccurrenceID != "" && emptyID(selection.OccurrenceID) { - return fmt.Errorf("selections[%d].occurrenceId is invalid", i) - } - key := selection.CandidateID + "\x00" + selection.OccurrenceID + "\x00" + selection.ProjectionMode - if seenSelections[key] { - return fmt.Errorf("duplicate selection %q", selection.CandidateID) - } - seenSelections[key] = true - } - for key, p := range d.Presentation { - if emptyID(key) { - return fmt.Errorf("presentation keys must be non-empty IDs") - } - if p.Order != nil && *p.Order < 0 { - return fmt.Errorf("presentation[%q].order must not be negative", key) - } - } - return nil + return d.validateSemantic() } func (w Workspace) Validate() error { if w.APIVersion != APIVersion || w.Kind != WorkspaceKind { return fmt.Errorf("unsupported V2 workspace protocol or kind") } - semanticWorkspace := false - for _, document := range w.Documents { - semanticWorkspace = semanticWorkspace || document.semantic() - } - if semanticWorkspace && strings.TrimSpace(w.Explorer.Title) == "" { + if strings.TrimSpace(w.Explorer.Title) == "" { return fmt.Errorf("explorer.title is required") } outputs, tabs, tabOutputs, orders := map[string]bool{}, map[string]bool{}, map[string]bool{}, map[int]bool{} @@ -186,25 +82,13 @@ func (w Workspace) ValidateForPublication() error { if !visibleOutputs[document.Output.ID] { continue } - if document.semantic() { - visible := 0 - for _, column := range document.Columns { - if column.Table != nil && (column.Table.Visible == nil || *column.Table.Visible) { - visible++ - } - } - if visible == 0 { - return fmt.Errorf("NO_VISIBLE_COLUMNS: documents[%d]", i) - } - continue - } - hidden := 0 - for _, presentation := range document.Presentation { - if presentation.Visible != nil && !*presentation.Visible { - hidden++ + visible := 0 + for _, column := range document.Columns { + if column.Table != nil && (column.Table.Visible == nil || *column.Table.Visible) { + visible++ } } - if len(document.Selections) == 0 || hidden == len(document.Selections) { + if visible == 0 { return fmt.Errorf("NO_VISIBLE_COLUMNS: documents[%d]", i) } } @@ -321,161 +205,10 @@ func (s BuilderState) Validate() error { if s.LifecycleState == LifecycleReady && s.Workspace == nil { return fmt.Errorf("READY Builder state requires workspace") } - workspace := s.Workspace - if workspace == nil && s.Document != nil { - workspace = &Workspace{APIVersion: APIVersion, Kind: WorkspaceKind, Documents: []Document{*s.Document}, Tabs: []Tab{{ID: s.Document.Output.ID, Title: s.Document.Output.Title, OutputID: s.Document.Output.ID, Order: 0, Visible: true}}} - } - if workspace == nil { - return nil - } - if err := workspace.Validate(); err != nil { - return err - } - for i := range workspace.Documents { - document := workspace.Documents[i] - if document.semantic() { - continue - } - one := s - one.Workspace = nil - one.Document = &document - if err := one.validateDocument(); err != nil { - return fmt.Errorf("documents[%d]: %w", i, err) - } - } - return nil -} - -func (s BuilderState) validateDocument() error { - d := s.Document - if d == nil { + if s.Workspace == nil { return nil } - edges, candidates, nodes := s.Catalog.edgeIndex(), s.Catalog.candidateIndex(), s.Catalog.nodeIndex() - if nodes[d.RootNodeID].ID == "" { - return fmt.Errorf("document rootNodeId %q is stale", d.RootNodeID) - } - if !nodes[d.RootNodeID].RowRootEligible { - return fmt.Errorf("ROW_ROOT_NOT_ELIGIBLE: rootNodeId %q", d.RootNodeID) - } - seenEdges := map[string]bool{} - current := d.RootNodeID - for i, step := range d.RouteSteps { - e, ok := edges[step.EdgeID] - if !ok { - return fmt.Errorf("routeSteps[%d] references stale edge %q", i, step.EdgeID) - } - if e.FromNodeID != current { - return fmt.Errorf("routeSteps[%d] is disconnected: edge starts at %q, expected %q", i, e.FromNodeID, current) - } - if seenEdges[e.ID] && !s.Catalog.RoutePolicy.AllowRepeatedEdges { - return fmt.Errorf("repeated edge %q is not allowed by route policy", e.ID) - } - seenEdges[e.ID] = true - current = e.ToNodeID - } - if s.Catalog.RoutePolicy.MaxHops != nil && len(d.RouteSteps) > *s.Catalog.RoutePolicy.MaxHops { - return fmt.Errorf("route exceeds routePolicy.maxHops") - } - occ, err := d.Occurrences(s.Catalog) - if err != nil { - return err - } - occByID := map[string]RouteOccurrence{} - for _, item := range occ { - occByID[item.ID] = item - } - for i, selection := range d.Selections { - candidate, ok := candidates[selection.CandidateID] - if !ok { - return fmt.Errorf("selections[%d] references stale candidate %q", i, selection.CandidateID) - } - id := selection.OccurrenceID - if id == "" { - id = occ[len(occ)-1].ID - } - item, ok := occByID[id] - if !ok { - return fmt.Errorf("selections[%d] references stale occurrence %q", i, id) - } - if candidate.NodeID != item.NodeID { - return fmt.Errorf("selection %q belongs to node %q, not occurrence %q node %q", selection.CandidateID, candidate.NodeID, id, item.NodeID) - } - projectionAllowed := false - for _, mode := range candidate.ProjectionModes { - if selection.ProjectionMode == mode { - projectionAllowed = true - break - } - } - if !projectionAllowed { - return fmt.Errorf("selection %q projection mode %q is not advertised", selection.CandidateID, selection.ProjectionMode) - } - } - seenResolvedSelections := map[string]bool{} - for i, selection := range d.Selections { - occurrenceID := effectiveOccurrence(*d, s.Catalog, selection.OccurrenceID) - key := selection.CandidateID + "\x00" + occurrenceID + "\x00" + selection.ProjectionMode - if seenResolvedSelections[key] { - return fmt.Errorf("duplicate selection at selections[%d]", i) - } - seenResolvedSelections[key] = true - } - selectionKeys := map[string]bool{} - for _, selection := range d.Selections { - selectionKeys[PresentationKey(selection.CandidateID, effectiveOccurrence(*d, s.Catalog, selection.OccurrenceID), selection.ProjectionMode)] = true - } - for key, p := range d.Presentation { - if !selectionKeys[key] { - return fmt.Errorf("presentation %q does not resolve to a selection", key) - } - for _, selection := range d.Selections { - if PresentationKey(selection.CandidateID, effectiveOccurrence(*d, s.Catalog, selection.OccurrenceID), selection.ProjectionMode) != key { - continue - } - candidate := candidates[selection.CandidateID] - if p.Filter != nil && !candidate.Filterable { - return fmt.Errorf("UNSUPPORTED_FILTER: presentation %q", key) - } - if p.Chart != nil && !candidate.Chartable { - return fmt.Errorf("UNSUPPORTED_CHART: presentation %q", key) - } - } - } - return nil -} - -func effectiveOccurrence(d Document, c CatalogSnapshot, authored string) string { - if authored != "" { - return authored - } - occ, err := d.Occurrences(c) - if err != nil || len(occ) == 0 { - return authored - } - return occ[len(occ)-1].ID -} - -func (c CatalogSnapshot) nodeIndex() map[string]CatalogNode { - out := map[string]CatalogNode{} - for _, n := range c.Nodes { - out[n.ID] = n - } - return out -} -func (c CatalogSnapshot) edgeIndex() map[string]*CatalogEdge { - out := map[string]*CatalogEdge{} - for i := range c.Edges { - out[c.Edges[i].ID] = &c.Edges[i] - } - return out -} -func (c CatalogSnapshot) candidateIndex() map[string]CatalogCandidate { - out := map[string]CatalogCandidate{} - for _, n := range c.Candidates { - out[n.ID] = n - } - return out + return s.Workspace.Validate() } func emptyID(value string) bool { diff --git a/internal/explorer/authoringv2/workspace_test.go b/internal/explorer/authoringv2/workspace_test.go index f9578ba3..628b7daf 100644 --- a/internal/explorer/authoringv2/workspace_test.go +++ b/internal/explorer/authoringv2/workspace_test.go @@ -8,7 +8,8 @@ import ( ) func workspaceDocument(id string) Document { - return Document{Kind: Kind, Output: Output{ID: id, Title: id}, RootNodeID: "patient", RouteSteps: []RouteStep{}, Selections: []Selection{{CandidateID: "patient-id", OccurrenceID: RootOccurrenceID, ProjectionMode: "SCALAR"}}, Presentation: map[string]Presentation{}} + visible := true + return Document{Kind: Kind, Output: Output{ID: id, Title: id}, RootResourceType: "Patient", Route: RouteNode{OccurrenceID: RootOccurrenceID, ResourceType: "Patient"}, Columns: []Column{{Column: "patient_id", Label: "Patient ID", OccurrenceID: RootOccurrenceID, Source: ColumnSource{Kind: SourceField, FieldPath: "id", ProjectionMode: "VALUE"}, Table: &TablePresentation{Visible: &visible}}}} } func TestWorkspaceCanonicalizesDuplicateTableOrdersByStableColumnIdentity(t *testing.T) { @@ -52,7 +53,7 @@ func TestWorkspaceCanonicalizesDuplicateTableOrdersByStableColumnIdentity(t *tes } func fiveTableWorkspace() Workspace { - w := Workspace{APIVersion: APIVersion, Kind: WorkspaceKind, Documents: []Document{}, Tabs: []Tab{}} + w := Workspace{APIVersion: APIVersion, Kind: WorkspaceKind, Explorer: ExplorerMetadata{Title: "Five tables"}, Documents: []Document{}, Tabs: []Tab{}} for i := 0; i < 5; i++ { id := fmt.Sprintf("output_%d", i) w.Documents = append(w.Documents, workspaceDocument(id)) @@ -98,39 +99,11 @@ func TestWorkspaceRejectsDuplicateAndMissingMappings(t *testing.T) { } } -func TestDerivedOccurrencesStayOmittedAndAreUnique(t *testing.T) { - d := workspaceDocument("out") - d.RouteSteps = []RouteStep{{EdgeID: "patient-encounter"}, {EdgeID: "encounter-self"}} - occ, err := d.Occurrences(testCatalog()) - if err != nil { - t.Fatal(err) - } - if occ[1].ID != "step-1" || occ[2].ID != "step-2" || d.RouteSteps[0].OccurrenceID != "" { - t.Fatalf("occurrences=%#v document=%#v", occ, d) - } - d.RouteSteps[1].OccurrenceID = "step-1" - if err := d.Validate(); err == nil || !strings.Contains(err.Error(), "duplicate route occurrence") { - t.Fatalf("error=%v", err) - } -} - -func TestWorkspacePresentationCapabilitiesAndVisibleColumns(t *testing.T) { +func TestWorkspacePublicationRequiresVisibleColumns(t *testing.T) { w := fiveTableWorkspace() - catalog := testCatalog() - catalog.Candidates[0].Filterable = false - catalog.Candidates[0].Chartable = false - key := PresentationKey("patient-id", "base", "SCALAR") - w.Documents[0].Presentation[key] = Presentation{Filter: &FilterPresentation{Label: "ID"}} - state := BuilderState{APIVersion: APIVersion, Kind: StateKind, Workspace: &w, Catalog: catalog} - if err := state.Validate(); err == nil || !strings.Contains(err.Error(), "UNSUPPORTED_FILTER") { - t.Fatalf("error=%v", err) - } - w.Documents[0].Presentation[key] = Presentation{Chart: &ChartPresentation{Type: "bar"}} - if err := state.Validate(); err == nil || !strings.Contains(err.Error(), "UNSUPPORTED_CHART") { - t.Fatalf("error=%v", err) - } hidden := false - w.Documents[0].Presentation[key] = Presentation{Visible: &hidden} + w.Documents[0].Columns[0].Table.Visible = &hidden + state := BuilderState{APIVersion: APIVersion, Kind: StateKind, Workspace: &w, Catalog: testCatalog()} if err := state.Validate(); err != nil { t.Fatalf("mutable Builder state rejected an all-hidden table: %v", err) } @@ -139,10 +112,9 @@ func TestWorkspacePresentationCapabilitiesAndVisibleColumns(t *testing.T) { } } -func TestBuilderAllowsVisibleTableWithNoSelectionsUntilPublication(t *testing.T) { +func TestBuilderAllowsVisibleTableWithNoColumnsUntilPublication(t *testing.T) { w := fiveTableWorkspace() - w.Documents[0].Selections = []Selection{} - w.Documents[0].Presentation = map[string]Presentation{} + w.Documents[0].Columns = []Column{} state := BuilderState{APIVersion: APIVersion, Kind: StateKind, Workspace: &w, Catalog: testCatalog()} if err := state.Validate(); err != nil { t.Fatalf("transitional empty table rejected by Builder: %v", err) @@ -184,3 +156,59 @@ func TestDecodeWorkspaceRepairsPreviouslyOmittedEmptyColumns(t *testing.T) { t.Fatalf("decoded legacy draft did not restore required columns array: %s", encoded) } } + +func TestDecodeWorkspaceAcceptsOnlyFirstProjectionForPersistedProjectID(t *testing.T) { + for _, tt := range []struct { + name string + mode string + wantErr bool + }{ + {name: "omitted"}, + {name: "explicit first", mode: "FIRST"}, + {name: "value", mode: "VALUE", wantErr: true}, + {name: "all", mode: "ALL", wantErr: true}, + {name: "distinct", mode: "DISTINCT", wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + workspace := Workspace{ + APIVersion: APIVersion, + Kind: WorkspaceKind, + Explorer: ExplorerMetadata{Title: "Persisted"}, + Documents: []Document{workspaceDocument("patients")}, + Tabs: []Tab{{ID: "patients", Title: "Patients", OutputID: "patients", Visible: true}}, + } + workspace.Documents[0].Columns[0].Column = "project_id" + workspace.Documents[0].Columns[0].Source = ColumnSource{Kind: SourceProjectID, ProjectionMode: tt.mode} + raw, err := json.Marshal(workspace) + if err != nil { + t.Fatal(err) + } + _, err = DecodeWorkspace(raw) + if tt.wantErr && err == nil { + t.Fatalf("projectionMode %q was accepted", tt.mode) + } + if !tt.wantErr && err != nil { + t.Fatalf("projectionMode %q was rejected: %v", tt.mode, err) + } + }) + } +} + +func TestAggregateColumnSourceAcceptsClosedAggregateShape(t *testing.T) { + visible := true + document := workspaceDocument("patients") + document.Route.Children = []RouteNode{{OccurrenceID: "condition", ResourceType: "Condition", Relationship: "subject_Patient"}} + document.Columns = append(document.Columns, Column{ + Column: "condition_count", Label: "Condition count", OccurrenceID: "condition", + Source: ColumnSource{Kind: SourceAggregate, Operation: "COUNT", WherePath: "code.coding[].code", WhereEquals: "C50"}, + Table: &TablePresentation{Visible: &visible}, + }) + if err := document.Validate(); err != nil { + t.Fatal(err) + } + + document.Columns[1].Source.Operation = "NOT_AN_AGGREGATE" + if err := document.Validate(); err == nil { + t.Fatal("unsupported aggregate operation was accepted") + } +} diff --git a/internal/explorer/capability/builder.go b/internal/explorer/capability/builder.go index 256ca622..1ecb1fc3 100644 --- a/internal/explorer/capability/builder.go +++ b/internal/explorer/capability/builder.go @@ -10,9 +10,6 @@ import ( "strings" ) -// The observation interfaces intentionally speak only in evidence. They do -// not expose generated schema or database handles, so an observer can never -// turn an accidental profiler row into an executable capability on its own. type ResourceObservation struct { ResourceType string Populated bool @@ -42,22 +39,17 @@ type FieldObservation struct { SuggestionsTruncated bool } -type ResourceInventory interface { - ListResources(context.Context) ([]ResourceObservation, error) -} -type RelationshipEvidence interface { - ListRelationships(context.Context) ([]RelationshipObservation, error) -} -type FieldEvidence interface { - ListFields(context.Context) ([]FieldObservation, error) -} +// Evidence is the complete catalog evidence set used to build a capability +// snapshot. Availability is explicit because an empty result is valid evidence +// for some generations, while an unavailable source must fail closed. +type Evidence struct { + Resources []ResourceObservation + Relationships []RelationshipObservation + Fields []FieldObservation -// Observer is a convenience for adapters that naturally collect all three -// evidence classes together. Builder accepts the individual interfaces too. -type Observer interface { - ResourceInventory - RelationshipEvidence - FieldEvidence + ResourcesAvailable bool + RelationshipsAvailable bool + FieldsAvailable bool } type NodeProof struct { @@ -84,13 +76,13 @@ type CandidateProof struct { Reason string } -// CompilerProbe is the compiler's proof boundary. A Builder advertises an -// operation only after the corresponding probe succeeds. In particular, a +// CompilerCallbacks are the compiler's proof boundary. A Builder advertises an +// operation only after the corresponding callback succeeds. In particular, a // field is not trusted merely because the observation source reported it. -type CompilerProbe interface { - ProbeNode(context.Context, Node) (NodeProof, error) - ProbeEdge(context.Context, Edge) (EdgeProof, error) - ProbeCandidate(context.Context, Candidate) (CandidateProof, error) +type CompilerCallbacks struct { + Node func(context.Context, Node) (NodeProof, error) + Edge func(context.Context, Edge) (EdgeProof, error) + Candidate func(context.Context, Candidate) (CandidateProof, error) } type Builder struct { @@ -99,22 +91,18 @@ type Builder struct { Complete bool Truncated bool - Resources ResourceInventory - Relationships RelationshipEvidence - Fields FieldEvidence - Compiler CompilerProbe + Evidence Evidence + Compiler CompilerCallbacks } -type CapabilityBuilder = Builder - const DefaultSuggestionLimit = 32 -func NewBuilder(identity SnapshotIdentity, resources ResourceInventory, relationships RelationshipEvidence, fields FieldEvidence, compiler CompilerProbe) Builder { - return Builder{Identity: identity, Resources: resources, Relationships: relationships, Fields: fields, Compiler: compiler, Complete: true, Policy: Policy{Route: RoutePolicy{AllowsRepeatedEdges: true, AllowsSelfLoops: true}, Projection: ProjectionPolicy{SuggestionLimit: DefaultSuggestionLimit}}} +func NewBuilder(identity SnapshotIdentity, evidence Evidence, compiler CompilerCallbacks) Builder { + return Builder{Identity: identity, Evidence: evidence, Compiler: compiler, Complete: true, Policy: Policy{Route: RoutePolicy{AllowsRepeatedEdges: true, AllowsSelfLoops: true}, Projection: ProjectionPolicy{SuggestionLimit: DefaultSuggestionLimit}}} } -// Build is pure: all values returned by an adapter are copied before they are -// sorted or used, and no adapter or compiler state is modified. +// Build is pure: evidence is copied before it is sorted or used, and no +// caller-owned evidence or compiler state is modified. func (b Builder) Build(ctx context.Context) (Snapshot, error) { identity := b.Identity identity.Project = strings.TrimSpace(identity.Project) @@ -127,23 +115,16 @@ func (b Builder) Build(ctx context.Context) (Snapshot, error) { // READY graph. The caller may publish it after a later complete build. } diags := []Diagnostic{} - if b.Resources == nil || b.Relationships == nil || b.Fields == nil || b.Compiler == nil { - return b.failedWith(identity, diags, "MISSING_CAPABILITY_DEPENDENCY", "resource, relationship, field, and compiler interfaces are required"), fmt.Errorf("capability dependencies are incomplete") - } - resources, err := b.Resources.ListResources(ctx) - if err != nil { - return b.failedWith(identity, diags, "RESOURCE_INVENTORY_FAILED", err.Error()), err - } - relationships, err := b.Relationships.ListRelationships(ctx) - if err != nil { - return b.failedWith(identity, diags, "RELATIONSHIP_EVIDENCE_FAILED", err.Error()), err + if !b.Evidence.ResourcesAvailable || !b.Evidence.RelationshipsAvailable || !b.Evidence.FieldsAvailable || b.Compiler.Node == nil || b.Compiler.Edge == nil || b.Compiler.Candidate == nil { + return b.failedWith(identity, diags, "MISSING_CAPABILITY_DEPENDENCY", "resource, relationship, field, and compiler callbacks are required"), fmt.Errorf("capability dependencies are incomplete") } - // Field enrichment is intentionally mandatory and fetched even when no - // relationship evidence exists. An omitted enrichment source must fail - // closed rather than silently yielding a field-less usable catalog. - fields, err := b.Fields.ListFields(ctx) - if err != nil { - return b.failedWith(identity, diags, "FIELD_ENRICHMENT_FAILED", err.Error()), err + // Copy before sorting or truncating so callers retain ownership of their + // evidence and nested suggestion slices. + resources := append([]ResourceObservation(nil), b.Evidence.Resources...) + relationships := append([]RelationshipObservation(nil), b.Evidence.Relationships...) + fields := append([]FieldObservation(nil), b.Evidence.Fields...) + for i := range fields { + fields[i].SuggestedValues = append([]string(nil), fields[i].SuggestedValues...) } policy := b.Policy @@ -206,7 +187,7 @@ func (b Builder) Build(ctx context.Context) (Snapshot, error) { diags = append(diags, diag("NOT_POPULATED", "resource inventory did not observe any rows", t)) continue } - proof, e := b.Compiler.ProbeNode(ctx, n) + proof, e := b.Compiler.Node(ctx, n) if e != nil || !proof.Allowed || !proof.RowRootEligible || len(proof.SupportedOperations) == 0 { reason := proof.Reason if e != nil { @@ -248,7 +229,7 @@ func (b Builder) Build(ctx context.Context) (Snapshot, error) { diags = append(diags, diag("INVALID_RELATIONSHIP", "relationship endpoint or label is invalid or unproven", base)) continue } - proof, eProbe := b.Compiler.ProbeEdge(ctx, e) + proof, eProbe := b.Compiler.Edge(ctx, e) if eProbe != nil || !proof.Allowed { reason := proof.Reason if eProbe != nil { @@ -292,7 +273,7 @@ func (b Builder) Build(ctx context.Context) (Snapshot, error) { diags = append(diags, diag("INVALID_FIELD_PATH", "field evidence did not resolve to a usable node/path", base)) continue } - proof, eProbe := b.Compiler.ProbeCandidate(ctx, c) + proof, eProbe := b.Compiler.Candidate(ctx, c) if eProbe != nil || !proof.Allowed || len(proof.ProjectionModes) == 0 || len(proof.SupportedOperations) == 0 { reason := proof.Reason if eProbe != nil { @@ -318,7 +299,7 @@ func (b Builder) Build(ctx context.Context) (Snapshot, error) { if !b.Complete || b.Truncated { status = StatusBuilding } - if len(diags) > 0 && (b.Fields == nil) { + if len(diags) > 0 && !b.Evidence.FieldsAvailable { status = StatusFailed } s := NewSnapshot(identity, policy, status, b.Complete, b.Truncated, nodes, edges, candidates, diags) diff --git a/internal/explorer/capability/builder_test.go b/internal/explorer/capability/builder_test.go index 0ad579ac..3d3554e5 100644 --- a/internal/explorer/capability/builder_test.go +++ b/internal/explorer/capability/builder_test.go @@ -2,62 +2,16 @@ package capability import ( "context" - "fmt" "reflect" "testing" ) -type ProbeFuncs struct { - Node func(context.Context, Node) (NodeProof, error) - Edge func(context.Context, Edge) (EdgeProof, error) - Candidate func(context.Context, Candidate) (CandidateProof, error) -} - -func (p ProbeFuncs) ProbeNode(ctx context.Context, node Node) (NodeProof, error) { - if p.Node == nil { - return NodeProof{}, fmt.Errorf("node probe is not configured") - } - return p.Node(ctx, node) -} - -func (p ProbeFuncs) ProbeEdge(ctx context.Context, edge Edge) (EdgeProof, error) { - if p.Edge == nil { - return EdgeProof{}, fmt.Errorf("edge probe is not configured") - } - return p.Edge(ctx, edge) -} - -func (p ProbeFuncs) ProbeCandidate(ctx context.Context, candidate Candidate) (CandidateProof, error) { - if p.Candidate == nil { - return CandidateProof{}, fmt.Errorf("candidate probe is not configured") - } - return p.Candidate(ctx, candidate) -} - -type SliceObserver struct { - Resources []ResourceObservation - Relationships []RelationshipObservation - Fields []FieldObservation -} - -func (s SliceObserver) ListResources(context.Context) ([]ResourceObservation, error) { - return append([]ResourceObservation(nil), s.Resources...), nil -} - -func (s SliceObserver) ListRelationships(context.Context) ([]RelationshipObservation, error) { - return append([]RelationshipObservation(nil), s.Relationships...), nil -} - -func (s SliceObserver) ListFields(context.Context) ([]FieldObservation, error) { - return append([]FieldObservation(nil), s.Fields...), nil -} - func testIdentity() SnapshotIdentity { return SnapshotIdentity{Project: "p", Generation: "g", AuthorizationScopeDigest: "scope", SchemaDigest: "schema", ResourceInventoryDigest: "inventory", RelationshipDigest: "relationships", FieldDigest: "fields", ProtocolVersion: "protocol-1", CompilerVersion: "compiler-1", TraversalPolicyVersion: "route-1", ProjectionPolicyVersion: "projection-1"} } -func testProbe() ProbeFuncs { - return ProbeFuncs{ +func testProbe() CompilerCallbacks { + return CompilerCallbacks{ Node: func(_ context.Context, n Node) (NodeProof, error) { return NodeProof{Allowed: true, RowRootEligible: true, RowGrain: "RESOURCE", SupportedOperations: []Operation{OperationSelect, OperationFilter}}, nil }, @@ -68,8 +22,11 @@ func testProbe() ProbeFuncs { } } -func testBuilder(obs SliceObserver) Builder { - return NewBuilder(testIdentity(), obs, obs, obs, testProbe()) +func testBuilder(obs Evidence) Builder { + obs.ResourcesAvailable = true + obs.RelationshipsAvailable = true + obs.FieldsAvailable = true + return NewBuilder(testIdentity(), obs, testProbe()) } func TestSnapshotHashIsOrderIndependentAndIdentityComplete(t *testing.T) { @@ -98,7 +55,7 @@ func TestSnapshotHashIsOrderIndependentAndIdentityComplete(t *testing.T) { } func TestBuilderUsesProofAndCopiesObservation(t *testing.T) { - obs := SliceObserver{Resources: []ResourceObservation{{ResourceType: "Patient", Populated: true, DocumentCount: 4}, {ResourceType: "Observation", Populated: true}}, Relationships: []RelationshipObservation{{SourceResourceType: "Patient", TargetResourceType: "Patient", Label: "partOf", StorageDirection: "OUTBOUND"}, {SourceResourceType: "Patient", TargetResourceType: "Patient", Label: "partOf", StorageDirection: "OUTBOUND"}}, Fields: []FieldObservation{{ResourceType: "Patient", Path: "name", Label: "Name", LogicalType: "string", SuggestedValues: []string{"B", "A"}, Observed: true, Populated: true}}} + obs := Evidence{Resources: []ResourceObservation{{ResourceType: "Patient", Populated: true, DocumentCount: 4}, {ResourceType: "Observation", Populated: true}}, Relationships: []RelationshipObservation{{SourceResourceType: "Patient", TargetResourceType: "Patient", Label: "partOf", StorageDirection: "OUTBOUND"}, {SourceResourceType: "Patient", TargetResourceType: "Patient", Label: "partOf", StorageDirection: "OUTBOUND"}}, Fields: []FieldObservation{{ResourceType: "Patient", Path: "name", Label: "Name", LogicalType: "string", SuggestedValues: []string{"B", "A"}, Observed: true, Populated: true}}} s := testBuilder(obs).Build snapshot, err := s(context.Background()) if err != nil || !snapshot.Usable() { @@ -118,7 +75,7 @@ func TestBuilderUsesProofAndCopiesObservation(t *testing.T) { } func TestUnpopulatedInventoryResourcesStayOutOfUsableNodes(t *testing.T) { - obs := SliceObserver{Resources: []ResourceObservation{{ResourceType: "Patient", Populated: true, DocumentCount: 1}, {ResourceType: "Observation", DocumentCount: 0}}, Fields: []FieldObservation{}} + obs := Evidence{Resources: []ResourceObservation{{ResourceType: "Patient", Populated: true, DocumentCount: 1}, {ResourceType: "Observation", DocumentCount: 0}}, Fields: []FieldObservation{}} s, err := testBuilder(obs).Build(context.Background()) if err != nil { t.Fatal(err) @@ -132,14 +89,17 @@ func TestUnpopulatedInventoryResourcesStayOutOfUsableNodes(t *testing.T) { } func TestIDsAreBoundToSnapshotIdentity(t *testing.T) { - obs := SliceObserver{Resources: []ResourceObservation{{ResourceType: "Patient", Populated: true}}, Fields: []FieldObservation{{ResourceType: "Patient", Path: "name"}}} + obs := Evidence{Resources: []ResourceObservation{{ResourceType: "Patient", Populated: true}}, Fields: []FieldObservation{{ResourceType: "Patient", Path: "name"}}} a, err := testBuilder(obs).Build(context.Background()) if err != nil { t.Fatal(err) } id := testIdentity() id.Generation = "other-generation" - b := NewBuilder(id, obs, obs, obs, testProbe()) + obs.ResourcesAvailable = true + obs.RelationshipsAvailable = true + obs.FieldsAvailable = true + b := NewBuilder(id, obs, testProbe()) other, err := b.Build(context.Background()) if err != nil { t.Fatal(err) @@ -153,7 +113,7 @@ func TestIDsAreBoundToSnapshotIdentity(t *testing.T) { } func TestBuilderRejectsMalformedEvidenceAndCompilerFailures(t *testing.T) { - obs := SliceObserver{Resources: []ResourceObservation{{ResourceType: "Patient"}}, Relationships: []RelationshipObservation{{SourceResourceType: "Patient", TargetResourceType: "Bad/Type", Label: "x"}}, Fields: []FieldObservation{{ResourceType: "Patient", Path: "name..given"}}} + obs := Evidence{Resources: []ResourceObservation{{ResourceType: "Patient"}}, Relationships: []RelationshipObservation{{SourceResourceType: "Patient", TargetResourceType: "Bad/Type", Label: "x"}}, Fields: []FieldObservation{{ResourceType: "Patient", Path: "name..given"}}} // Build with a rejecting node leaves no usable root and retains audit data. b := testBuilder(obs) got, buildErr := b.Build(context.Background()) @@ -166,8 +126,10 @@ func TestBuilderRejectsMalformedEvidenceAndCompilerFailures(t *testing.T) { } func TestBuilderFieldEnrichmentIsFailClosed(t *testing.T) { - obs := SliceObserver{Resources: []ResourceObservation{{ResourceType: "Patient"}}, Relationships: nil, Fields: nil} - b := NewBuilder(testIdentity(), obs, obs, nil, testProbe()) + obs := Evidence{Resources: []ResourceObservation{{ResourceType: "Patient"}}, Relationships: nil, Fields: nil} + obs.ResourcesAvailable = true + obs.RelationshipsAvailable = true + b := NewBuilder(testIdentity(), obs, testProbe()) s, err := b.Build(context.Background()) if err == nil || s.Status != StatusFailed || s.Usable() { t.Fatalf("missing enrichment was not failed closed: %#v, %v", s, err) @@ -175,7 +137,7 @@ func TestBuilderFieldEnrichmentIsFailClosed(t *testing.T) { } func TestIncompleteAndTruncatedSnapshotsAreNotUsable(t *testing.T) { - obs := SliceObserver{} + obs := Evidence{} b := testBuilder(obs) b.Complete = false s, err := b.Build(context.Background()) diff --git a/internal/explorer/capability/domain.go b/internal/explorer/capability/domain.go index 2a373c36..8c422863 100644 --- a/internal/explorer/capability/domain.go +++ b/internal/explorer/capability/domain.go @@ -22,14 +22,6 @@ const ( StatusFailed Status = "FAILED" ) -// These aliases make the wire values convenient to use without making callers -// depend on the underlying spelling. -const ( - BUILDING Status = StatusBuilding - READY Status = StatusReady - FAILED Status = StatusFailed -) - type Operation string const ( @@ -49,13 +41,6 @@ const ( ProjectionDistinctArray ProjectionMode = "DISTINCT_ARRAY" ) -// Short aliases are useful to adapters that mirror the frontend vocabulary. -const ( - FIRST ProjectionMode = ProjectionFirst - ARRAY ProjectionMode = ProjectionArray - DISTINCT_ARRAY ProjectionMode = ProjectionDistinctArray -) - type FilterOperator string const ( @@ -200,10 +185,6 @@ type Snapshot struct { Token string `json:"token"` } -// CapabilitySnapshot is the descriptive name used at API seams; Snapshot is -// retained as the concise package-local spelling. -type CapabilitySnapshot = Snapshot - var ( ErrSnapshotUnavailable = errors.New("capability snapshot is unavailable") ErrStaleSnapshot = errors.New("capability snapshot token is stale") diff --git a/internal/explorer/capabilitystore/repository.go b/internal/explorer/capability/repository.go similarity index 52% rename from internal/explorer/capabilitystore/repository.go rename to internal/explorer/capability/repository.go index 22f37ec7..f2bcb5f0 100644 --- a/internal/explorer/capabilitystore/repository.go +++ b/internal/explorer/capability/repository.go @@ -1,6 +1,4 @@ -// Package capabilitystore contains persistence adapters for immutable -// Explorer capability snapshots. -package capabilitystore +package capability import ( "context" @@ -8,8 +6,6 @@ import ( "encoding/hex" "errors" "fmt" - - "github.com/calypr/loom/internal/explorer/capability" ) var ( @@ -22,20 +18,16 @@ var ( // changes an existing document. GetByIdentity returns a READY snapshot when // one exists; otherwise it returns the best available diagnostic snapshot. type Repository interface { - GetByToken(context.Context, string) (*capability.Snapshot, error) - GetByIdentity(context.Context, capability.SnapshotIdentity) (*capability.Snapshot, error) - Put(context.Context, capability.Snapshot) (*capability.Snapshot, error) + GetByToken(context.Context, string) (*Snapshot, error) + GetByIdentity(context.Context, SnapshotIdentity) (*Snapshot, error) + Put(context.Context, Snapshot) (*Snapshot, error) } -// Store is retained as a short name for callers that use stores throughout -// the rest of Explorer. -type Store = Repository - -func validate(snapshot capability.Snapshot) error { +func validateSnapshot(snapshot Snapshot) error { if snapshot.Token == "" { return fmt.Errorf("%w: token is required", ErrInvalidSnapshot) } - if snapshot.Status == capability.StatusReady && !snapshot.Usable() { + if snapshot.Status == StatusReady && !snapshot.Usable() { return fmt.Errorf("%w: READY snapshot must be complete and not truncated", ErrInvalidSnapshot) } want := snapshotToken(snapshot) @@ -45,11 +37,11 @@ func validate(snapshot capability.Snapshot) error { return nil } -// ValidateSnapshot applies the repository invariants without requiring a -// caller to select a particular adapter. -func ValidateSnapshot(snapshot capability.Snapshot) error { return validate(snapshot) } +// ValidateSnapshot applies repository invariants without requiring a caller +// to select a particular persistence adapter. +func ValidateSnapshot(snapshot Snapshot) error { return validateSnapshot(snapshot) } -func snapshotToken(snapshot capability.Snapshot) string { +func snapshotToken(snapshot Snapshot) string { sum := sha256.Sum256(snapshot.CanonicalPayload()) return "sha256:" + hex.EncodeToString(sum[:]) } diff --git a/internal/explorer/compilation.go b/internal/explorer/compilation.go index 89bdc907..43478b7a 100644 --- a/internal/explorer/compilation.go +++ b/internal/explorer/compilation.go @@ -1,18 +1,5 @@ package explorer -import ( - "github.com/calypr/loom/internal/dataframe/recipe" -) - -// Compilation is the transport-neutral result shared by the V2 compiler and -// publication lifecycle. It contains executable recipe data and derived -// physical columns; the lossless ExplorerConfigV2 packet is stored separately. -type Compilation struct { - Bundle recipe.Bundle - RecipeDigest string - EmittedColumns []EmittedColumn -} - type EmittedColumn struct { EmissionID string `json:"emissionId"` OutputID string `json:"outputId"` diff --git a/internal/explorer/compilation/compile.go b/internal/explorer/compilation/compile.go new file mode 100644 index 00000000..b7d90320 --- /dev/null +++ b/internal/explorer/compilation/compile.go @@ -0,0 +1,325 @@ +// Package compilation translates semantic Builder intent into the persistence- +// neutral recipe understood by the dataframe compiler. It deliberately has no +// HTTP, store, or server dependencies: a capability snapshot is its authority. +package compilation + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "regexp" + "strings" + + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/capability" + "github.com/calypr/loom/internal/projectid" +) + +const TranslationVersion = "authoring-v2-native-5" + +// Error is a structured translation failure. Stage, Code, Path, and Details +// remain transport-neutral so adapters never parse error strings. +type Error struct { + Stage string + Code string + Path string + Message string + Details map[string]any + Cause error +} + +func (e *Error) Error() string { + if e == nil { + return "compilation error" + } + if e.Path == "" { + return e.Code + ": " + e.Message + } + return e.Code + " at " + e.Path + ": " + e.Message +} + +func (e *Error) Unwrap() error { + if e == nil { + return nil + } + return e.Cause +} + +func fail(stage, code, path, message string, details map[string]any, cause error) error { + return &Error{Stage: stage, Code: code, Path: path, Message: message, Details: details, Cause: cause} +} + +// PresentationConfig is compiler-owned presentation data. The Builder may +// provide labels, visibility, and order, but never executable selectors. +type PresentationConfig struct { + OutputID string `json:"outputId"` + Title string `json:"title"` + Columns []PresentationColumn `json:"columns"` +} + +type PresentationColumn struct { + EmissionID string `json:"emissionId"` + PublicColumn string `json:"publicColumn"` + Label string `json:"label"` + Visible bool `json:"visible"` + Order int `json:"order"` + Pinned bool `json:"pinned"` + FilterLabel string `json:"filterLabel,omitempty"` + FilterOrder int `json:"filterOrder,omitempty"` + ChartType string `json:"chartType,omitempty"` + ChartTitle string `json:"chartTitle,omitempty"` + ChartOrder int `json:"chartOrder,omitempty"` +} + +// Result is one deterministic semantic document compilation. +type Result struct { + Bundle recipe.Bundle + RecipeDigest string + EmittedColumns []explorer.EmittedColumn + IdentityMappings []explorer.IdentityMapping + Presentation PresentationConfig + OutputContract explorer.PublicOutputContract +} + +// WorkspaceResult combines independently compiled table documents into one +// recipe and one public Explorer configuration. +type WorkspaceResult struct { + Bundle recipe.Bundle + RecipeDigest string + EmittedColumns []explorer.EmittedColumn + IdentityMappings []explorer.IdentityMapping + Presentations []PresentationConfig + OutputContracts []explorer.PublicOutputContract + Workspace authoringv2.Workspace +} + +func CompileWorkspace(ctx context.Context, project, explorerID string, workspace authoringv2.Workspace, snapshot capability.Snapshot) (WorkspaceResult, error) { + project = projectid.Canonical(project) + wire := catalogFromCapability(snapshot, explorerID) + if err := (authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, Workspace: &workspace, Catalog: wire}).Validate(); err != nil { + return WorkspaceResult{}, fail("intent", "INVALID_AUTHORING_INTENT", "$.workspace", err.Error(), nil, err) + } + workspace = workspace.NormalizePresentationOrders() + result := WorkspaceResult{ + Workspace: workspace, + Bundle: recipe.Bundle{ + RecipeSchemaVersion: recipe.CurrentSchemaVersion, + Name: "explorer_" + safeName(project) + "_" + safeName(explorerID), + TranslationVersion: TranslationVersion, + }, + EmittedColumns: []explorer.EmittedColumn{}, + IdentityMappings: []explorer.IdentityMapping{}, + Presentations: []PresentationConfig{}, + OutputContracts: []explorer.PublicOutputContract{}, + } + for i, document := range workspace.Documents { + compiled, err := Compile(ctx, project, explorerID, document, snapshot) + if err != nil { + return WorkspaceResult{}, fail("compile", "DOCUMENT_COMPILE_FAILED", fmt.Sprintf("$.workspace.documents[%d]", i), err.Error(), nil, err) + } + result.Bundle.Outputs = append(result.Bundle.Outputs, compiled.Bundle.Outputs...) + result.EmittedColumns = append(result.EmittedColumns, compiled.EmittedColumns...) + result.IdentityMappings = append(result.IdentityMappings, compiled.IdentityMappings...) + result.Presentations = append(result.Presentations, compiled.Presentation) + result.OutputContracts = append(result.OutputContracts, compiled.OutputContract) + } + if err := result.Bundle.Validate(); err != nil { + return WorkspaceResult{}, err + } + digest, err := result.Bundle.Digest() + if err != nil { + return WorkspaceResult{}, err + } + result.RecipeDigest = digest + return result, nil +} + +// Compile translates one semantic V2 document against the exact capability +// snapshot. It performs no discovery, profiling, or recipe execution. +func Compile(ctx context.Context, project, explorerID string, document authoringv2.Document, snapshot capability.Snapshot) (Result, error) { + if err := contextErr(ctx); err != nil { + return Result{}, err + } + project = projectid.Canonical(project) + explorerID = strings.TrimSpace(explorerID) + if project == "" { + return Result{}, fail("protocol", "PROJECT_REQUIRED", "$.project", "project is required", nil, nil) + } + if explorerID == "" { + return Result{}, fail("protocol", "EXPLORER_REQUIRED", "$.explorerId", "explorerId is required", nil, nil) + } + if err := snapshot.ValidateToken(snapshot.Token); err != nil { + return Result{}, fail("capability", "CAPABILITY_SNAPSHOT_UNUSABLE", "$.snapshot", "capability snapshot is unavailable or stale", map[string]any{"token": snapshot.Token}, err) + } + if projectid.Canonical(snapshot.Identity.Project) != project { + return Result{}, fail("capability", "PROJECT_MISMATCH", "$.project", "capability snapshot belongs to a different project", map[string]any{"snapshotProject": snapshot.Identity.Project, "project": project}, nil) + } + if err := document.Validate(); err != nil { + return Result{}, fail("intent", "INVALID_DOCUMENT", "$", err.Error(), nil, err) + } + if err := validateSnapshot(snapshot); err != nil { + return Result{}, err + } + return compileSemanticDocument(ctx, project, explorerID, document, snapshot) +} + +func catalogFromCapability(snapshot capability.Snapshot, explorerID string) authoringv2.CatalogSnapshot { + catalog := authoringv2.CatalogSnapshot{ + APIVersion: authoringv2.APIVersion, Kind: authoringv2.CatalogKind, + Project: snapshot.Identity.Project, ExplorerID: explorerID, + SourceGeneration: snapshot.Identity.Generation, AuthorizationScopeDigest: snapshot.Identity.AuthorizationScopeDigest, + ResolvedSchemaDigest: snapshot.Identity.SchemaDigest, SnapshotToken: snapshot.Token, Complete: snapshot.Complete, + RoutePolicy: authoringv2.RoutePolicy{Unbounded: snapshot.Policy.Route.MaxHops == 0, AllowRepeatedEdges: snapshot.Policy.Route.AllowsRepeatedEdges, AllowSelfLoops: snapshot.Policy.Route.AllowsSelfLoops}, + } + if snapshot.Policy.Route.MaxHops > 0 { + max := snapshot.Policy.Route.MaxHops + catalog.RoutePolicy.MaxHops = &max + } + for _, node := range snapshot.Nodes { + count := node.DocumentCount + catalog.Nodes = append(catalog.Nodes, authoringv2.CatalogNode{ID: node.ID, ResourceType: node.ResourceType, RowRootEligible: node.RowRootEligible, RowGrain: node.RowGrain, Populated: node.Populated, DocumentCount: &count}) + } + for _, edge := range snapshot.Edges { + catalog.Edges = append(catalog.Edges, authoringv2.CatalogEdge{ID: edge.ID, FromNodeID: edge.FromNodeID, ToNodeID: edge.ToNodeID, Label: edge.Label, Populated: edge.ObservedEdgeCount > 0}) + } + for _, candidate := range snapshot.Candidates { + modes := make([]string, len(candidate.ProjectionModes)) + for i, mode := range candidate.ProjectionModes { + modes[i] = wireProjectionMode(mode) + } + defaultMode := "" + if len(modes) > 0 { + defaultMode = modes[0] + } + catalog.Candidates = append(catalog.Candidates, authoringv2.CatalogCandidate{ + ID: candidate.ID, NodeID: candidate.NodeID, Label: candidate.Label, LogicalType: candidate.LogicalType, + Repeated: candidate.Cardinality != "scalar", Filterable: supportsOperation(candidate.SupportedOperations, capability.OperationFilter), Chartable: supportsOperation(candidate.SupportedOperations, capability.OperationChart), + ProjectionModes: modes, DefaultProjectionMode: defaultMode, Populated: candidate.Populated, + }) + } + return catalog +} + +func validateSnapshot(snapshot capability.Snapshot) error { + nodes := map[string]capability.Node{} + for i, node := range snapshot.Nodes { + if strings.TrimSpace(node.ID) == "" || strings.TrimSpace(node.ResourceType) == "" { + return fail("capability", "INVALID_NODE", fmt.Sprintf("$.snapshot.nodes[%d]", i), "node id and resourceType are required", nil, nil) + } + if _, duplicate := nodes[node.ID]; duplicate { + return fail("capability", "DUPLICATE_NODE", fmt.Sprintf("$.snapshot.nodes[%d]", i), "duplicate node id", map[string]any{"nodeId": node.ID}, nil) + } + nodes[node.ID] = node + } + edges := map[string]bool{} + for i, edge := range snapshot.Edges { + if edge.ID == "" || nodes[edge.FromNodeID].ID == "" || nodes[edge.ToNodeID].ID == "" || strings.TrimSpace(edge.Label) == "" { + return fail("capability", "INVALID_EDGE", fmt.Sprintf("$.snapshot.edges[%d]", i), "edge has stale endpoints or empty label", nil, nil) + } + if edges[edge.ID] { + return fail("capability", "DUPLICATE_EDGE", fmt.Sprintf("$.snapshot.edges[%d]", i), "duplicate edge id", map[string]any{"edgeId": edge.ID}, nil) + } + edges[edge.ID] = true + } + candidates := map[string]bool{} + for i, candidate := range snapshot.Candidates { + if candidate.ID == "" || nodes[candidate.NodeID].ID == "" || strings.TrimSpace(candidate.FieldPath) == "" { + return fail("capability", "INVALID_CANDIDATE", fmt.Sprintf("$.snapshot.candidates[%d]", i), "candidate has stale node or empty field path", nil, nil) + } + if candidates[candidate.ID] { + return fail("capability", "DUPLICATE_CANDIDATE", fmt.Sprintf("$.snapshot.candidates[%d]", i), "duplicate candidate id", map[string]any{"candidateId": candidate.ID}, nil) + } + candidates[candidate.ID] = true + } + return nil +} + +func supportsOperation(items []capability.Operation, want capability.Operation) bool { + for _, item := range items { + if item == want { + return true + } + } + return false +} + +func projectionValueMode(mode string) recipe.ValueMode { + switch strings.ToUpper(strings.TrimSpace(mode)) { + case string(capability.ProjectionFirst): + return recipe.ValueModeFirst + case "ALL", string(capability.ProjectionArray): + return recipe.ValueModeAll + case string(capability.ProjectionDistinctArray): + return recipe.ValueModeDistinct + default: + return recipe.ValueModeAuto + } +} + +func wireProjectionMode(mode capability.ProjectionMode) string { + switch mode { + case capability.ProjectionScalar: + return "VALUE" + case capability.ProjectionArray, capability.ProjectionDistinctArray: + return "ALL" + default: + return string(mode) + } +} + +var namePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + +func recipeName(label, id string) string { + if namePattern.MatchString(label) { + return label + } + return "edge_" + shortHash(id) +} + +func safeName(value string) string { + var builder strings.Builder + for _, char := range strings.ToLower(value) { + if char >= 'a' && char <= 'z' || char >= '0' && char <= '9' || char == '_' { + builder.WriteRune(char) + } else { + builder.WriteByte('_') + } + } + value = strings.Trim(builder.String(), "_") + if value == "" { + return "x" + } + if value[0] >= '0' && value[0] <= '9' { + return "x_" + value + } + return value +} + +func shortHash(value string) string { + sum := sha256.Sum256([]byte(value)) + return hex.EncodeToString(sum[:])[:16] +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value = strings.TrimSpace(value); value != "" { + return value + } + } + return "" +} + +func contextErr(ctx context.Context) error { + if ctx == nil { + return nil + } + select { + case <-ctx.Done(): + return fail("compile", "CONTEXT_CANCELED", "$", ctx.Err().Error(), nil, ctx.Err()) + default: + return nil + } +} diff --git a/internal/explorer/compilation/semantic_compile.go b/internal/explorer/compilation/semantic_compile.go index c1823bec..80c693c7 100644 --- a/internal/explorer/compilation/semantic_compile.go +++ b/internal/explorer/compilation/semantic_compile.go @@ -9,8 +9,10 @@ import ( "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataframe/spec" + "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/explorer/capability" + fhirschema "github.com/calypr/loom/internal/fhir/schema" ) type semanticOccurrence struct { @@ -20,9 +22,10 @@ type semanticOccurrence struct { } type semanticRecipeNode struct { - fields []recipe.Field - dynamics []recipe.DynamicColumn - pivots []recipe.Pivot + fields []recipe.Field + dynamics []recipe.DynamicColumn + pivots []recipe.Pivot + aggregates []recipe.Aggregate } func compileSemanticDocument(ctx context.Context, project, explorerID string, document authoringv2.Document, snapshot capability.Snapshot) (Result, error) { @@ -43,21 +46,25 @@ func compileSemanticDocument(ctx context.Context, project, explorerID string, do for id := range occurrences { nodes[id] = &semanticRecipeNode{} } - emitted := make([]EmittedColumn, 0, len(document.Columns)) - mappings := make([]IdentityMapping, 0, len(document.Columns)) + emitted := make([]explorer.EmittedColumn, 0, len(document.Columns)) + mappings := make([]explorer.IdentityMapping, 0, len(document.Columns)) presentation := PresentationConfig{OutputID: document.Output.ID, Title: document.Output.Title, Columns: make([]PresentationColumn, 0, len(document.Columns))} - contract := OutputContract{OutputID: document.Output.ID, Columns: make([]OutputColumn, 0, len(document.Columns))} + contract := explorer.PublicOutputContract{OutputID: document.Output.ID, Columns: make([]explorer.PublicOutputColumn, 0, len(document.Columns))} for index, column := range document.Columns { occurrence := occurrences[column.OccurrenceID] alias := semanticAlias(column.OccurrenceID) - leaf, err := semanticColumnLeaf(column.Column, column.OccurrenceID) - if err != nil { - return Result{}, fail("lower", "COLUMN_ROUTE_PREFIX_MISMATCH", fmt.Sprintf("$.columns[%d].column", index), err.Error(), map[string]any{"column": column.Column, "occurrenceId": column.OccurrenceID}, err) + leaf := column.Column + if column.Source.Kind != authoringv2.SourceAggregate { + var err error + leaf, err = semanticColumnLeaf(column.Column, column.OccurrenceID) + if err != nil { + return Result{}, fail("lower", "COLUMN_ROUTE_PREFIX_MISMATCH", fmt.Sprintf("$.columns[%d].column", index), err.Error(), map[string]any{"column": column.Column, "occurrenceId": column.OccurrenceID}, err) + } } logicalType := firstNonEmpty(column.LogicalType, "string") filterable, chartable := column.Filter != nil, column.Chart != nil - candidateID := "source_" + shortHash(column.OccurrenceID+"\x00"+column.Source.Kind+"\x00"+column.Source.FieldPath+"\x00"+column.Source.Match) + candidateID := "source_" + shortHash(column.OccurrenceID+"\x00"+column.Source.Kind+"\x00"+column.Source.FieldPath+"\x00"+column.Source.Match+"\x00"+column.Source.Operation+"\x00"+column.Source.WherePath+"\x00"+column.Source.WhereEquals+"\x00"+column.Column) projectionMode := firstNonEmpty(strings.ToUpper(column.Source.ProjectionMode), "FIRST") switch column.Source.Kind { @@ -81,6 +88,14 @@ func compileSemanticDocument(ctx context.Context, project, explorerID string, do return Result{}, fail("lower", "INVALID_TYPED_SOURCE", fmt.Sprintf("$.columns[%d].source", index), pivotErr.Error(), nil, pivotErr) } nodes[column.OccurrenceID].pivots = appendSemanticPivot(nodes[column.OccurrenceID].pivots, pivot) + case authoringv2.SourceAggregate: + aggregate, aggregateType, aggregateErr := semanticAggregate(column, alias, occurrence.graph.ResourceType) + if aggregateErr != nil { + return Result{}, fail("lower", "INVALID_TYPED_SOURCE", fmt.Sprintf("$.columns[%d].source", index), aggregateErr.Error(), nil, aggregateErr) + } + nodes[column.OccurrenceID].aggregates = append(nodes[column.OccurrenceID].aggregates, aggregate) + logicalType = aggregateType + projectionMode = "VALUE" default: dynamic, dynamicErr := semanticFixedLookup(column, alias, leaf, logicalType) if dynamicErr != nil { @@ -104,9 +119,9 @@ func compileSemanticDocument(ctx context.Context, project, explorerID string, do visible = false } emissionID := column.Column - emission := EmittedColumn{EmissionID: emissionID, OutputID: document.Output.ID, NodeID: occurrence.graph.ID, SelectionID: candidateID, CandidateID: candidateID, OccurrenceID: column.OccurrenceID, ProjectionMode: projectionMode, PublicColumn: column.Column, LogicalType: logicalType, Filterable: filterable, Chartable: chartable} + emission := explorer.EmittedColumn{EmissionID: emissionID, OutputID: document.Output.ID, NodeID: occurrence.graph.ID, SelectionID: candidateID, CandidateID: candidateID, OccurrenceID: column.OccurrenceID, ProjectionMode: projectionMode, PublicColumn: column.Column, Label: column.Label, LogicalType: logicalType, Filterable: filterable, Chartable: chartable} emitted = append(emitted, emission) - mappings = append(mappings, IdentityMapping{OutputID: document.Output.ID, CandidateID: candidateID, OccurrenceID: column.OccurrenceID, ProjectionMode: projectionMode, EmissionIDs: []string{emissionID}}) + mappings = append(mappings, explorer.IdentityMapping{OutputID: document.Output.ID, CandidateID: candidateID, OccurrenceID: column.OccurrenceID, ProjectionMode: projectionMode, EmissionIDs: []string{emissionID}}) presented := PresentationColumn{EmissionID: emissionID, PublicColumn: column.Column, Label: column.Label, Visible: visible, Order: orderValue, Pinned: pinned} if column.Filter != nil { presented.FilterLabel = firstNonEmpty(column.Filter.Label, column.Label) @@ -123,10 +138,10 @@ func compileSemanticDocument(ctx context.Context, project, explorerID string, do } } presentation.Columns = append(presentation.Columns, presented) - contract.Columns = append(contract.Columns, OutputColumn{Column: column.Column, Label: column.Label, LogicalType: logicalType, Filterable: filterable, Chartable: chartable}) + contract.Columns = append(contract.Columns, explorer.PublicOutputColumn{Column: column.Column, Label: column.Label, LogicalType: logicalType, Filterable: filterable, Chartable: chartable}) } - output := recipe.Output{Name: document.Output.ID, RootResourceType: root.graph.ResourceType, RowGrain: string(rowGrain), Fields: nodes[authoringv2.RootOccurrenceID].fields, Pivots: nodes[authoringv2.RootOccurrenceID].pivots, DynamicColumns: nodes[authoringv2.RootOccurrenceID].dynamics, CollisionPolicy: "error"} + output := recipe.Output{Name: document.Output.ID, RootResourceType: root.graph.ResourceType, RowGrain: string(rowGrain), RootColumnNaming: recipe.RootColumnNamingExact, TraversalColumnNaming: recipe.TraversalColumnNamingAlias, Fields: nodes[authoringv2.RootOccurrenceID].fields, Pivots: nodes[authoringv2.RootOccurrenceID].pivots, Aggregates: nodes[authoringv2.RootOccurrenceID].aggregates, DynamicColumns: nodes[authoringv2.RootOccurrenceID].dynamics, CollisionPolicy: "error"} output.Traversals = semanticTraversals(document.Route, occurrences, nodes) bundle := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "explorer_" + safeName(project) + "_" + safeName(explorerID), TranslationVersion: TranslationVersion, Outputs: []recipe.Output{output}} if err := bundle.Validate(); err != nil { @@ -214,7 +229,7 @@ func semanticTraversals(route authoringv2.RouteNode, occurrences map[string]sema for _, child := range children { occurrence := occurrences[child.OccurrenceID] node := nodes[child.OccurrenceID] - result = append(result, recipe.Traversal{Name: recipeName(occurrence.edge.Label, occurrence.edge.ID), Alias: semanticAlias(child.OccurrenceID), ToResourceType: occurrence.graph.ResourceType, MatchMode: recipe.MatchOptional, Fields: node.fields, Pivots: node.pivots, DynamicColumns: node.dynamics, Traversals: semanticTraversals(child, occurrences, nodes)}) + result = append(result, recipe.Traversal{Name: recipeName(occurrence.edge.Label, occurrence.edge.ID), Alias: semanticAlias(child.OccurrenceID), ToResourceType: occurrence.graph.ResourceType, MatchMode: recipe.MatchOptional, Fields: node.fields, Pivots: node.pivots, Aggregates: node.aggregates, DynamicColumns: node.dynamics, Traversals: semanticTraversals(child, occurrences, nodes)}) } return result } @@ -223,20 +238,19 @@ func semanticAlias(occurrenceID string) string { if occurrenceID == authoringv2.RootOccurrenceID { return "root" } - alias := safeName(occurrenceID) - if split := strings.LastIndex(alias, "__"); split >= 0 { - return alias[split+2:] - } - return alias + // Semantic Builder occurrence IDs are globally unique output namespaces. + // Keep the complete identifier so ALIAS traversal naming reproduces the + // authored public-column prefix even when an older client encoded route + // ancestry into the occurrence ID itself. + return safeName(occurrenceID) } func semanticColumnLeaf(column, occurrenceID string) (string, error) { if occurrenceID == authoringv2.RootOccurrenceID { return column, nil } - // Occurrence IDs encode the complete authored route path, while recipe - // traversal aliases are local to their parent. Strip the complete prefix - // here; physical lowering reconstructs it from the nested aliases. + // The traversal alias retains this complete globally unique occurrence ID; + // strip it here so physical lowering can add it exactly once. prefix := safeName(occurrenceID) + "__" if !strings.HasPrefix(column, prefix) || strings.TrimPrefix(column, prefix) == "" { return "", fmt.Errorf("column for occurrence %q must begin with %q", occurrenceID, prefix) @@ -277,6 +291,70 @@ func semanticFixedLookup(column authoringv2.Column, alias, leaf, logicalType str return recipe.DynamicColumn{Name: "fixed_" + shortHash(column.Column), ColumnPrefix: &empty, Source: recipe.Expression{Select: alias + "." + sourcePath}, Key: &key, Value: &value, Columns: []string{leaf}, MaxColumns: 1, ColumnTypes: map[string]string{leaf: logicalType}, ColumnSourceKeys: map[string]string{leaf: column.Source.Match}}, nil } +func semanticAggregate(column authoringv2.Column, alias, resourceType string) (recipe.Aggregate, string, error) { + op := strings.ToUpper(strings.TrimSpace(column.Source.Operation)) + operation := recipe.AggregateOperation(op) + aggregate := recipe.Aggregate{ + Name: "aggregate_" + shortHash(column.OccurrenceID+"\x00"+column.Column+"\x00"+op), OutputName: column.Column, + Operation: operation, FieldRef: column.Column, ValueMode: recipe.ValueModeAuto, + RequiredValues: append([]string(nil), column.Source.RequiredValues...), + } + if strings.TrimSpace(column.Source.FieldPath) != "" { + path := strings.Trim(strings.TrimSpace(column.Source.FieldPath), ".") + aggregate.Expr = &recipe.Expression{Select: alias + "." + path} + } + if wherePath := strings.Trim(strings.TrimSpace(column.Source.WherePath), "."); wherePath != "" { + where := &recipe.Filter{Select: alias + "." + wherePath, FieldRef: column.Column} + if strings.TrimSpace(column.Source.WhereEquals) == "" { + where.Operator = recipe.FilterExists + } else { + where.Operator = recipe.FilterEquals + where.Quantifier = recipe.QuantifierAny + value := column.Source.WhereEquals + metadata, ok := fhirschema.ResolveTerminalScalarMetadata(resourceType, wherePath) + if !ok || metadata.Primitive != fhirschema.PrimitiveString { + return recipe.Aggregate{}, "", fmt.Errorf("aggregate predicate selector %q must resolve to a string or code", column.Source.WherePath) + } + if wherePath == "code" || strings.HasSuffix(wherePath, ".code") { + where.Values = []recipe.FilterValue{{Kind: recipe.FilterCode, Code: &recipe.CodeValue{Code: value}}} + } else { + where.Values = []recipe.FilterValue{{Kind: recipe.FilterString, String: &value}} + } + } + aggregate.Where = where + } + + logicalType := "string" + switch operation { + case recipe.AggregateCount, recipe.AggregateCountDistinct: + logicalType = "integer" + case recipe.AggregateExists, recipe.AggregateContainsAll: + logicalType = "boolean" + case recipe.AggregateDistinctValues, recipe.AggregateMin, recipe.AggregateMax: + metadata, ok := fhirschema.ResolveTerminalScalarMetadata(resourceType, strings.Trim(strings.TrimSpace(column.Source.FieldPath), ".")) + if !ok || metadata.Primitive == fhirschema.PrimitiveUnknown { + return recipe.Aggregate{}, "", fmt.Errorf("aggregate selector %q is not represented by generated resource type %q", column.Source.FieldPath, resourceType) + } + switch metadata.Primitive { + case fhirschema.PrimitiveInteger: + logicalType = "integer" + case fhirschema.PrimitiveDecimal: + logicalType = "decimal" + case fhirschema.PrimitiveBoolean: + logicalType = "boolean" + case fhirschema.PrimitiveDate: + logicalType = "date" + case fhirschema.PrimitiveDateTime: + logicalType = "date_time" + default: + logicalType = "string" + } + default: + return recipe.Aggregate{}, "", fmt.Errorf("unsupported aggregate operation %q", column.Source.Operation) + } + return aggregate, logicalType, nil +} + func semanticObservationPivot(column authoringv2.Column, alias, leaf string) (recipe.Pivot, error) { separator := "__" + column.Source.Match if !strings.HasSuffix(leaf, separator) || strings.TrimSuffix(leaf, separator) == "" { diff --git a/internal/explorer/compilation/translate.go b/internal/explorer/compilation/translate.go deleted file mode 100644 index 70e616ca..00000000 --- a/internal/explorer/compilation/translate.go +++ /dev/null @@ -1,609 +0,0 @@ -// Package compilation translates the V2 Builder intent into the persistence- -// neutral recipe understood by the dataframe compiler. It deliberately has -// no HTTP, store, or server dependencies: a capability snapshot is the only -// authority consulted while compiling an intent. -package compilation - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "regexp" - "sort" - "strings" - - "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/spec" - "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/authoringv2" - "github.com/calypr/loom/internal/explorer/capability" -) - -const TranslationVersion = "authoring-v2-native-4" - -// Error is a structured translation failure. Stage, Code, Path, and Details -// are intentionally transport-neutral so an HTTP adapter can map it to its -// own diagnostic envelope without parsing an error string. -type Error struct { - Stage string - Code string - Path string - Message string - Details map[string]any - Cause error -} - -func (e *Error) Error() string { - if e == nil { - return "compilation error" - } - if e.Path == "" { - return e.Code + ": " + e.Message - } - return e.Code + " at " + e.Path + ": " + e.Message -} -func (e *Error) Unwrap() error { - if e == nil { - return nil - } - return e.Cause -} - -func fail(stage, code, path, message string, details map[string]any, cause error) error { - return &Error{Stage: stage, Code: code, Path: path, Message: message, Details: details, Cause: cause} -} - -// EmittedColumn is the immutable public output-column contract generated for -// one candidate occurrence. It intentionally mirrors the useful subset of -// explorer.EmittedColumn without importing the parent Explorer package. -type EmittedColumn struct { - EmissionID string `json:"emissionId"` - OutputID string `json:"outputId"` - NodeID string `json:"nodeId,omitempty"` - SelectionID string `json:"selectionId,omitempty"` - CandidateID string `json:"candidateId,omitempty"` - OccurrenceID string `json:"occurrenceId,omitempty"` - ProjectionMode string `json:"projectionMode"` - PublicColumn string `json:"publicColumn"` - LogicalType string `json:"logicalType"` - Filterable bool `json:"filterable"` - Chartable bool `json:"chartable"` -} - -type IdentityMapping struct { - OutputID string `json:"outputId"` - CandidateID string `json:"candidateId"` - OccurrenceID string `json:"occurrenceId"` - ProjectionMode string `json:"projectionMode"` - EmissionIDs []string `json:"emissionIds"` -} - -// PresentationConfig is compiler-owned presentation data. The Builder may -// provide labels, visibility, and order, but never executable selectors. -type PresentationConfig struct { - OutputID string `json:"outputId"` - Title string `json:"title"` - Columns []PresentationColumn `json:"columns"` -} -type PresentationColumn struct { - EmissionID string `json:"emissionId"` - PublicColumn string `json:"publicColumn"` - Label string `json:"label"` - Visible bool `json:"visible"` - Order int `json:"order"` - Pinned bool `json:"pinned"` - FilterLabel string `json:"filterLabel,omitempty"` - FilterOrder int `json:"filterOrder,omitempty"` - ChartType string `json:"chartType,omitempty"` - ChartTitle string `json:"chartTitle,omitempty"` - ChartOrder int `json:"chartOrder,omitempty"` -} - -type OutputColumn = explorer.PublicOutputColumn -type OutputContract = explorer.PublicOutputContract - -// Result is a complete, deterministic native compilation artifact. -type Result struct { - Bundle recipe.Bundle - RecipeDigest string - EmittedColumns []EmittedColumn - IdentityMappings []IdentityMapping - Presentation PresentationConfig - OutputContract OutputContract -} - -type WorkspaceResult struct { - Bundle recipe.Bundle - RecipeDigest string - EmittedColumns []EmittedColumn - IdentityMappings []IdentityMapping - Presentations []PresentationConfig - OutputContracts []OutputContract - Workspace authoringv2.Workspace -} - -func CompileWorkspace(ctx context.Context, project, explorerID string, workspace authoringv2.Workspace, snapshot capability.Snapshot) (WorkspaceResult, error) { - wire := catalogFromCapability(snapshot, explorerID) - if err := (authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, Workspace: &workspace, Catalog: wire}).Validate(); err != nil { - return WorkspaceResult{}, fail("intent", "INVALID_AUTHORING_INTENT", "$.workspace", err.Error(), nil, err) - } - workspace = workspace.NormalizePresentationOrders() - result := WorkspaceResult{Workspace: workspace, Bundle: recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "explorer_" + safeName(project) + "_" + safeName(explorerID), TranslationVersion: TranslationVersion}, EmittedColumns: []EmittedColumn{}, IdentityMappings: []IdentityMapping{}, Presentations: []PresentationConfig{}, OutputContracts: []OutputContract{}} - for i, document := range workspace.Documents { - compiled, err := Compile(ctx, project, explorerID, document, snapshot) - if err != nil { - return WorkspaceResult{}, fail("compile", "DOCUMENT_COMPILE_FAILED", fmt.Sprintf("$.workspace.documents[%d]", i), err.Error(), nil, err) - } - result.Bundle.Outputs = append(result.Bundle.Outputs, compiled.Bundle.Outputs...) - result.EmittedColumns = append(result.EmittedColumns, compiled.EmittedColumns...) - result.IdentityMappings = append(result.IdentityMappings, compiled.IdentityMappings...) - result.Presentations = append(result.Presentations, compiled.Presentation) - result.OutputContracts = append(result.OutputContracts, compiled.OutputContract) - } - if err := result.Bundle.Validate(); err != nil { - return WorkspaceResult{}, err - } - digest, err := result.Bundle.Digest() - if err != nil { - return WorkspaceResult{}, err - } - result.RecipeDigest = digest - return result, nil -} - -func catalogFromCapability(snapshot capability.Snapshot, explorerID string) authoringv2.CatalogSnapshot { - c := authoringv2.CatalogSnapshot{APIVersion: authoringv2.APIVersion, Kind: authoringv2.CatalogKind, Project: snapshot.Identity.Project, ExplorerID: explorerID, SourceGeneration: snapshot.Identity.Generation, AuthorizationScopeDigest: snapshot.Identity.AuthorizationScopeDigest, ResolvedSchemaDigest: snapshot.Identity.SchemaDigest, SnapshotToken: snapshot.Token, Complete: snapshot.Complete, RoutePolicy: authoringv2.RoutePolicy{Unbounded: snapshot.Policy.Route.MaxHops == 0, AllowRepeatedEdges: snapshot.Policy.Route.AllowsRepeatedEdges, AllowSelfLoops: snapshot.Policy.Route.AllowsSelfLoops}} - if snapshot.Policy.Route.MaxHops > 0 { - max := snapshot.Policy.Route.MaxHops - c.RoutePolicy.MaxHops = &max - } - for _, n := range snapshot.Nodes { - count := n.DocumentCount - c.Nodes = append(c.Nodes, authoringv2.CatalogNode{ID: n.ID, ResourceType: n.ResourceType, RowRootEligible: n.RowRootEligible, RowGrain: n.RowGrain, Populated: n.Populated, DocumentCount: &count}) - } - for _, e := range snapshot.Edges { - c.Edges = append(c.Edges, authoringv2.CatalogEdge{ID: e.ID, FromNodeID: e.FromNodeID, ToNodeID: e.ToNodeID, Label: e.Label, Populated: e.ObservedEdgeCount > 0}) - } - for _, candidate := range snapshot.Candidates { - modes := make([]string, len(candidate.ProjectionModes)) - for i, mode := range candidate.ProjectionModes { - modes[i] = wireProjectionMode(mode) - } - defaultMode := "" - if len(modes) > 0 { - defaultMode = modes[0] - } - c.Candidates = append(c.Candidates, authoringv2.CatalogCandidate{ID: candidate.ID, NodeID: candidate.NodeID, Label: candidate.Label, LogicalType: candidate.LogicalType, Repeated: candidate.Cardinality != "scalar", Filterable: supportsOperation(candidate.SupportedOperations, capability.OperationFilter), Chartable: supportsOperation(candidate.SupportedOperations, capability.OperationChart), ProjectionModes: modes, DefaultProjectionMode: defaultMode, Populated: candidate.Populated}) - } - return c -} - -// Compilation is a descriptive alias for callers that use the domain term -// rather than the translation verb. -type Compilation = Result - -// Compile translates one V2 document against the exact capability snapshot. -// It performs no catalog discovery, probing, profiling, or recipe execution. -func Compile(ctx context.Context, project, explorerID string, document authoringv2.Document, snapshot capability.Snapshot) (Result, error) { - if err := contextErr(ctx); err != nil { - return Result{}, err - } - project = strings.TrimSpace(project) - explorerID = strings.TrimSpace(explorerID) - if project == "" { - return Result{}, fail("protocol", "PROJECT_REQUIRED", "$.project", "project is required", nil, nil) - } - if explorerID == "" { - return Result{}, fail("protocol", "EXPLORER_REQUIRED", "$.explorerId", "explorerId is required", nil, nil) - } - if err := snapshot.ValidateToken(snapshot.Token); err != nil { - return Result{}, fail("capability", "CAPABILITY_SNAPSHOT_UNUSABLE", "$.snapshot", "capability snapshot is unavailable or stale", map[string]any{"token": snapshot.Token}, err) - } - if snapshot.Identity.Project != project { - return Result{}, fail("capability", "PROJECT_MISMATCH", "$.project", "capability snapshot belongs to a different project", map[string]any{"snapshotProject": snapshot.Identity.Project, "project": project}, nil) - } - if err := document.Validate(); err != nil { - return Result{}, fail("intent", "INVALID_DOCUMENT", "$", err.Error(), nil, err) - } - if err := validateSnapshot(snapshot); err != nil { - return Result{}, err - } - if document.UsesSemanticColumns() { - return compileSemanticDocument(ctx, project, explorerID, document, snapshot) - } - occurrences, routeEdges, err := resolveRoute(document, snapshot) - if err != nil { - return Result{}, err - } - if err := validateSelections(document, snapshot, occurrences); err != nil { - return Result{}, err - } - - root := occurrences[0] - rowGrain, ok := spec.InferRowGrain(root.ResourceType) - if !ok || !root.RowRootEligible { - return Result{}, fail("lower", "UNSUPPORTED_ROW_ROOT", "$.rootNodeId", "root node is not an eligible recipe row root", map[string]any{"nodeId": root.ID, "resourceType": root.ResourceType}, nil) - } - assignments := make([]assignment, 0, len(document.Selections)) - seenPairs := map[string]bool{} - for i, selection := range document.Selections { - occurrenceID := selection.OccurrenceID - if occurrenceID == "" { - occurrenceID = occurrences[len(occurrences)-1].ID - } - candidate, _ := snapshot.Candidate(selection.CandidateID) - key := selection.CandidateID + "\x00" + occurrenceID + "\x00" + strings.ToUpper(selection.ProjectionMode) - if seenPairs[key] { - return Result{}, fail("intent", "DUPLICATE_SELECTION", fmt.Sprintf("$.selections[%d]", i), "candidate occurrence is selected more than once", map[string]any{"candidateId": selection.CandidateID, "occurrenceId": occurrenceID}, nil) - } - seenPairs[key] = true - assignments = append(assignments, assignment{Selection: selection, Candidate: candidate, OccurrenceID: occurrenceID}) - } - // Route order is semantic; candidate order within an occurrence is stable - // by ID so receipt identity does not depend on browser array ordering. - sort.SliceStable(assignments, func(i, j int) bool { - ai, aj := occurrenceIndex(occurrences, assignments[i].OccurrenceID), occurrenceIndex(occurrences, assignments[j].OccurrenceID) - if ai != aj { - return ai < aj - } - return assignments[i].Candidate.ID < assignments[j].Candidate.ID - }) - - emitted := make([]EmittedColumn, 0, len(assignments)) - mappings := make([]IdentityMapping, 0, len(assignments)) - byPair := map[string]*IdentityMapping{} - fieldsByOccurrence := map[string][]recipe.Field{} - for _, a := range assignments { - fieldName := generatedID("c_", a.Candidate.ID+"\x00"+a.OccurrenceID+"\x00"+a.Selection.ProjectionMode) - public := fieldName - if occurrence := occurrenceIndex(occurrences, a.OccurrenceID); occurrence > 0 { - aliases := make([]string, occurrence) - for i := range aliases { - aliases[i] = routeAlias(i) - } - public = strings.Join(aliases, "__") + "__" + fieldName - } - emission := generatedID("em_", document.Output.ID+"\x00"+a.OccurrenceID+"\x00"+a.Candidate.ID+"\x00"+a.Selection.ProjectionMode) - column := EmittedColumn{EmissionID: emission, OutputID: document.Output.ID, NodeID: a.Candidate.NodeID, SelectionID: a.Candidate.ID, CandidateID: a.Candidate.ID, OccurrenceID: a.OccurrenceID, ProjectionMode: a.Selection.ProjectionMode, PublicColumn: public, LogicalType: a.Candidate.LogicalType, Filterable: supportsOperation(a.Candidate.SupportedOperations, capability.OperationFilter), Chartable: supportsOperation(a.Candidate.SupportedOperations, capability.OperationChart)} - emitted = append(emitted, column) - pair := a.Candidate.ID + "\x00" + a.OccurrenceID + "\x00" + a.Selection.ProjectionMode - if byPair[pair] == nil { - mappings = append(mappings, IdentityMapping{OutputID: document.Output.ID, CandidateID: a.Candidate.ID, OccurrenceID: a.OccurrenceID, ProjectionMode: a.Selection.ProjectionMode}) - byPair[pair] = &mappings[len(mappings)-1] - } - byPair[pair].EmissionIDs = append(byPair[pair].EmissionIDs, emission) - alias := "root" - if idx := occurrenceIndex(occurrences, a.OccurrenceID); idx > 0 { - alias = routeAlias(idx - 1) - } - fieldPath := strings.TrimPrefix(strings.TrimSpace(a.Candidate.FieldPath), "root.") - fieldsByOccurrence[a.OccurrenceID] = append(fieldsByOccurrence[a.OccurrenceID], recipe.Field{Name: fieldName, FieldRef: a.Candidate.FieldPath, Expr: recipe.Expression{Select: alias + "." + fieldPath}, ValueMode: projectionValueMode(a.Selection.ProjectionMode)}) - } - - output := recipe.Output{Name: document.Output.ID, RootResourceType: root.ResourceType, RowGrain: string(rowGrain), Fields: fieldsByOccurrence[authoringv2.RootOccurrenceID]} - var lowerRoute func(int) []recipe.Traversal - lowerRoute = func(i int) []recipe.Traversal { - if i >= len(routeEdges) { - return nil - } - item := recipe.Traversal{ - Name: recipeName(routeEdges[i].Label, routeEdges[i].ID), - Alias: routeAlias(i), - ToResourceType: occurrences[i+1].ResourceType, - MatchMode: recipe.MatchOptional, - Fields: fieldsByOccurrence[occurrences[i+1].ID], - Traversals: lowerRoute(i + 1), - } - return []recipe.Traversal{item} - } - output.Traversals = lowerRoute(0) - bundle := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "explorer_" + safeName(project) + "_" + safeName(explorerID), TranslationVersion: TranslationVersion, Outputs: []recipe.Output{output}} - if err := bundle.Validate(); err != nil { - return Result{}, fail("lower", "INVALID_RECIPE", "$.recipe", err.Error(), nil, err) - } - digest, err := bundle.Digest() - if err != nil { - return Result{}, fail("lower", "RECIPE_DIGEST_FAILED", "$.recipe", "recipe digest could not be calculated", nil, err) - } - presentation, err := makePresentation(document, emitted, snapshot) - if err != nil { - return Result{}, err - } - contract := OutputContract{OutputID: document.Output.ID} - for _, c := range emitted { - contract.Columns = append(contract.Columns, OutputColumn{Column: c.PublicColumn, Label: presentation.Columns[len(contract.Columns)].Label, LogicalType: c.LogicalType, Filterable: c.Filterable, Chartable: c.Chartable}) - } - return Result{Bundle: bundle, RecipeDigest: digest, EmittedColumns: emitted, IdentityMappings: mappings, Presentation: presentation, OutputContract: contract}, nil -} - -type occurrence struct { - ID, NodeID, ResourceType string - RowRootEligible bool -} -type assignment struct { - Selection authoringv2.Selection - Candidate capability.Candidate - OccurrenceID string -} - -func validateSnapshot(s capability.Snapshot) error { - nodes := map[string]capability.Node{} - for i, n := range s.Nodes { - if strings.TrimSpace(n.ID) == "" || strings.TrimSpace(n.ResourceType) == "" { - return fail("capability", "INVALID_NODE", fmt.Sprintf("$.snapshot.nodes[%d]", i), "node id and resourceType are required", nil, nil) - } - if _, ok := nodes[n.ID]; ok { - return fail("capability", "DUPLICATE_NODE", fmt.Sprintf("$.snapshot.nodes[%d]", i), "duplicate node id", map[string]any{"nodeId": n.ID}, nil) - } - nodes[n.ID] = n - } - edges := map[string]bool{} - for i, e := range s.Edges { - if e.ID == "" || nodes[e.FromNodeID].ID == "" || nodes[e.ToNodeID].ID == "" || strings.TrimSpace(e.Label) == "" { - return fail("capability", "INVALID_EDGE", fmt.Sprintf("$.snapshot.edges[%d]", i), "edge has stale endpoints or empty label", nil, nil) - } - if edges[e.ID] { - return fail("capability", "DUPLICATE_EDGE", fmt.Sprintf("$.snapshot.edges[%d]", i), "duplicate edge id", map[string]any{"edgeId": e.ID}, nil) - } - edges[e.ID] = true - } - candidates := map[string]bool{} - for i, c := range s.Candidates { - if c.ID == "" || nodes[c.NodeID].ID == "" || strings.TrimSpace(c.FieldPath) == "" { - return fail("capability", "INVALID_CANDIDATE", fmt.Sprintf("$.snapshot.candidates[%d]", i), "candidate has stale node or empty field path", nil, nil) - } - if candidates[c.ID] { - return fail("capability", "DUPLICATE_CANDIDATE", fmt.Sprintf("$.snapshot.candidates[%d]", i), "duplicate candidate id", map[string]any{"candidateId": c.ID}, nil) - } - candidates[c.ID] = true - } - return nil -} - -func resolveRoute(d authoringv2.Document, s capability.Snapshot) ([]occurrence, []capability.Edge, error) { - nodes := map[string]capability.Node{} - for _, n := range s.Nodes { - nodes[n.ID] = n - } - root, ok := nodes[d.RootNodeID] - if !ok { - return nil, nil, fail("route", "STALE_ROOT_NODE", "$.rootNodeId", "root node is not in the capability snapshot", map[string]any{"nodeId": d.RootNodeID}, nil) - } - if !root.RowRootEligible { - return nil, nil, fail("route", "ROOT_NOT_ELIGIBLE", "$.rootNodeId", "root node is not row-root eligible", nil, nil) - } - edges := map[string]capability.Edge{} - for _, e := range s.Edges { - edges[e.ID] = e - } - occ := []occurrence{{ID: authoringv2.RootOccurrenceID, NodeID: root.ID, ResourceType: root.ResourceType, RowRootEligible: root.RowRootEligible}} - path := []capability.Edge{} - seen := map[string]bool{} - seenOccurrences := map[string]bool{authoringv2.RootOccurrenceID: true} - current := root.ID - for i, step := range d.RouteSteps { - e, ok := edges[step.EdgeID] - if !ok { - return nil, nil, fail("route", "STALE_EDGE", fmt.Sprintf("$.routeSteps[%d].edgeId", i), "route edge is not in the capability snapshot", map[string]any{"edgeId": step.EdgeID}, nil) - } - if e.FromNodeID != current { - return nil, nil, fail("route", "DISCONNECTED_ROUTE", fmt.Sprintf("$.routeSteps[%d]", i), "route edge does not start at the current occurrence node", map[string]any{"expectedNodeId": current, "actualNodeId": e.FromNodeID}, nil) - } - if seen[e.ID] && !s.Policy.Route.AllowsRepeatedEdges { - return nil, nil, fail("route", "REPEATED_EDGE_NOT_ALLOWED", fmt.Sprintf("$.routeSteps[%d].edgeId", i), "route policy does not allow repeated edges", nil, nil) - } - if e.FromNodeID == e.ToNodeID && !s.Policy.Route.AllowsSelfLoops { - return nil, nil, fail("route", "SELF_LOOP_NOT_ALLOWED", fmt.Sprintf("$.routeSteps[%d].edgeId", i), "route policy does not allow self-loops", nil, nil) - } - seen[e.ID] = true - id := step.OccurrenceID - if id == "" { - id = authoringv2.DerivedOccurrenceID(i) - } - if seenOccurrences[id] { - return nil, nil, fail("route", "DUPLICATE_OCCURRENCE", fmt.Sprintf("$.routeSteps[%d].occurrenceId", i), "route occurrence id is not unique", map[string]any{"occurrenceId": id}, nil) - } - seenOccurrences[id] = true - n := nodes[e.ToNodeID] - occ = append(occ, occurrence{ID: id, NodeID: n.ID, ResourceType: n.ResourceType, RowRootEligible: n.RowRootEligible}) - path = append(path, e) - current = e.ToNodeID - } - if s.Policy.Route.MaxHops > 0 && len(path) > s.Policy.Route.MaxHops { - return nil, nil, fail("route", "ROUTE_TOO_LONG", "$.routeSteps", "route exceeds capability route policy", map[string]any{"maxHops": s.Policy.Route.MaxHops, "hops": len(path)}, nil) - } - return occ, path, nil -} - -func validateSelections(d authoringv2.Document, s capability.Snapshot, occ []occurrence) error { - byOcc := map[string]occurrence{} - for _, o := range occ { - byOcc[o.ID] = o - } - tail := occ[len(occ)-1].ID - for i, sel := range d.Selections { - c, ok := s.Candidate(sel.CandidateID) - if !ok { - return fail("intent", "STALE_CANDIDATE", fmt.Sprintf("$.selections[%d].candidateId", i), "candidate is not in the capability snapshot", map[string]any{"candidateId": sel.CandidateID}, nil) - } - id := sel.OccurrenceID - if id == "" { - id = tail - } - o, ok := byOcc[id] - if !ok { - return fail("intent", "STALE_OCCURRENCE", fmt.Sprintf("$.selections[%d].occurrenceId", i), "selection occurrence is not in the route", map[string]any{"occurrenceId": id}, nil) - } - if c.NodeID != o.NodeID { - return fail("intent", "SELECTION_NODE_MISMATCH", fmt.Sprintf("$.selections[%d]", i), "candidate does not belong to the selected route occurrence", map[string]any{"candidateNodeId": c.NodeID, "occurrenceNodeId": o.NodeID}, nil) - } - allowed := false - for _, m := range c.ProjectionModes { - if strings.EqualFold(wireProjectionMode(m), sel.ProjectionMode) || strings.EqualFold(string(m), sel.ProjectionMode) { - allowed = true - break - } - } - if !allowed { - return fail("intent", "UNSUPPORTED_PROJECTION_MODE", fmt.Sprintf("$.selections[%d].projectionMode", i), "projection mode is not advertised by the capability snapshot", map[string]any{"candidateId": c.ID, "projectionMode": sel.ProjectionMode, "supported": c.ProjectionModes}, nil) - } - } - return nil -} - -func occurrenceIndex(all []occurrence, id string) int { - for i, o := range all { - if o.ID == id { - return i - } - } - return -1 -} -func routeAlias(i int) string { return fmt.Sprintf("route_%d", i) } -func supportsOperation(items []capability.Operation, want capability.Operation) bool { - for _, v := range items { - if v == want { - return true - } - } - return false -} -func projectionValueMode(mode string) recipe.ValueMode { - switch strings.ToUpper(strings.TrimSpace(mode)) { - case string(capability.ProjectionFirst): - return recipe.ValueModeFirst - case "ALL", string(capability.ProjectionArray): - return recipe.ValueModeAll - case string(capability.ProjectionDistinctArray): - return recipe.ValueModeDistinct - default: - return recipe.ValueModeAuto - } -} - -func wireProjectionMode(mode capability.ProjectionMode) string { - switch mode { - case capability.ProjectionScalar: - return "VALUE" - case capability.ProjectionArray, capability.ProjectionDistinctArray: - return "ALL" - default: - return string(mode) - } -} - -var namePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) - -func recipeName(label, id string) string { - if namePattern.MatchString(label) { - return label - } - return "edge_" + shortHash(id) -} -func safeName(s string) string { - var b strings.Builder - for _, r := range strings.ToLower(s) { - if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' { - b.WriteRune(r) - } else { - b.WriteByte('_') - } - } - v := strings.Trim(b.String(), "_") - if v == "" { - return "x" - } - if v[0] >= '0' && v[0] <= '9' { - return "x_" + v - } - return v -} -func shortHash(s string) string { - sum := sha256.Sum256([]byte(s)) - return hex.EncodeToString(sum[:])[:16] -} -func generatedID(prefix, s string) string { return prefix + shortHash(s) } - -func firstNonEmpty(values ...string) string { - for _, value := range values { - if value = strings.TrimSpace(value); value != "" { - return value - } - } - return "" -} - -func contextErr(ctx context.Context) error { - if ctx == nil { - return nil - } - select { - case <-ctx.Done(): - return fail("compile", "CONTEXT_CANCELED", "$", ctx.Err().Error(), nil, ctx.Err()) - default: - return nil - } -} - -func makePresentation(d authoringv2.Document, emitted []EmittedColumn, snapshot capability.Snapshot) (PresentationConfig, error) { - candidates := make(map[string]capability.Candidate, len(snapshot.Candidates)) - for _, candidate := range snapshot.Candidates { - candidates[candidate.ID] = candidate - } - result := PresentationConfig{OutputID: d.Output.ID, Title: d.Output.Title, Columns: make([]PresentationColumn, 0, len(emitted))} - for i, c := range emitted { - p := authoringv2.Presentation{} - key := authoringv2.PresentationKey(c.CandidateID, c.OccurrenceID, c.ProjectionMode) - if raw, ok := d.Presentation[key]; ok { - p = raw - } - label := c.PublicColumn - if candidate, ok := candidates[c.CandidateID]; ok { - label = firstNonEmpty(candidate.Label, candidate.FieldPath, label) - } - if strings.TrimSpace(p.Label) != "" { - label = p.Label - } - visible := true - if p.Visible != nil { - visible = *p.Visible - } - order := i - if p.Order != nil { - order = *p.Order - } - column := PresentationColumn{EmissionID: c.EmissionID, PublicColumn: c.PublicColumn, Label: label, Visible: visible, Order: order} - if p.Table != nil { - column.Pinned = p.Table.Pinned - } - if p.Filter != nil { - column.FilterLabel = p.Filter.Label - column.FilterOrder = i - if p.Filter.Order != nil { - column.FilterOrder = *p.Filter.Order - } - } - if p.Chart != nil { - column.ChartType, column.ChartTitle = p.Chart.Type, p.Chart.Title - column.ChartOrder = i - if p.Chart.Order != nil { - column.ChartOrder = *p.Chart.Order - } - } - result.Columns = append(result.Columns, column) - } - for key := range d.Presentation { - found := false - for _, c := range emitted { - if key == authoringv2.PresentationKey(c.CandidateID, c.OccurrenceID, c.ProjectionMode) { - found = true - break - } - } - if !found { - return PresentationConfig{}, fail("presentation", "STALE_PRESENTATION", "$.presentation."+key, "presentation references an unselected emission", map[string]any{"key": key}, nil) - } - } - return result, nil -} diff --git a/internal/explorer/compilation/translate_test.go b/internal/explorer/compilation/translate_test.go index 22f58c0b..01fdb872 100644 --- a/internal/explorer/compilation/translate_test.go +++ b/internal/explorer/compilation/translate_test.go @@ -3,8 +3,6 @@ package compilation import ( "context" "errors" - "fmt" - "strings" "testing" "github.com/calypr/loom/internal/dataframe/recipe" @@ -13,7 +11,11 @@ import ( ) func fixtureSnapshot() capability.Snapshot { - identity := capability.SnapshotIdentity{Project: "project-a", Generation: "generation-a", AuthorizationScopeDigest: "scope-a", SchemaDigest: "schema-a", CompilerVersion: "compiler-a"} + return fixtureSnapshotForProject("project-a") +} + +func fixtureSnapshotForProject(project string) capability.Snapshot { + identity := capability.SnapshotIdentity{Project: project, Generation: "generation-a", AuthorizationScopeDigest: "scope-a", SchemaDigest: "schema-a", CompilerVersion: "compiler-a"} policy := capability.Policy{Route: capability.RoutePolicy{Version: "route-a", AllowsRepeatedEdges: true, AllowsSelfLoops: true}, Projection: capability.ProjectionPolicy{Version: "projection-a", Modes: []capability.ProjectionMode{capability.ProjectionScalar, capability.ProjectionFirst, capability.ProjectionArray, capability.ProjectionDistinctArray}}} nodes := []capability.Node{{ID: "n_patient", ResourceType: "Patient", RowRootEligible: true, RowGrain: "patient"}, {ID: "n_encounter", ResourceType: "Encounter", RowRootEligible: true, RowGrain: "resource"}} edges := []capability.Edge{{ID: "e_encounter", FromNodeID: "n_patient", ToNodeID: "n_encounter", Label: "encounters"}, {ID: "e_self", FromNodeID: "n_encounter", ToNodeID: "n_encounter", Label: "revisits"}} @@ -22,83 +24,37 @@ func fixtureSnapshot() capability.Snapshot { return capability.NewSnapshot(identity, policy, capability.StatusReady, true, false, nodes, edges, candidates, nil) } -func BenchmarkCompileRepresentativeBuilderState(b *testing.B) { - snapshot := fixtureSnapshot() - for candidateIndex := 0; candidateIndex < 5; candidateIndex++ { - snapshot.Candidates = append(snapshot.Candidates, capability.Candidate{ - ID: "c_encounter_" + fmt.Sprint(candidateIndex), NodeID: "n_encounter", - ResourceType: "Encounter", FieldPath: "subject.display" + fmt.Sprint(candidateIndex), - LogicalType: "string", ProjectionModes: []capability.ProjectionMode{capability.ProjectionFirst}, - SupportedOperations: []capability.Operation{capability.OperationSelect}, - }) - } - // NewSnapshot recomputes the token after extending the benchmark fixture. - snapshot = capability.NewSnapshot(snapshot.Identity, snapshot.Policy, capability.StatusReady, true, false, snapshot.Nodes, snapshot.Edges, snapshot.Candidates, nil) - document := authoringv2.Document{APIVersion: authoringv2.APIVersion, Kind: authoringv2.Kind, Output: authoringv2.Output{ID: "representative", Title: "Representative"}, RootNodeID: "n_patient", Presentation: map[string]authoringv2.Presentation{}} - for hop := 0; hop < 20; hop++ { - edgeID := "e_self" - if hop == 0 { - edgeID = "e_encounter" - } - occurrenceID := fmt.Sprintf("hop-%d", hop) - document.RouteSteps = append(document.RouteSteps, authoringv2.RouteStep{EdgeID: edgeID, OccurrenceID: occurrenceID}) - for candidateIndex := 0; candidateIndex < 5; candidateIndex++ { - document.Selections = append(document.Selections, authoringv2.Selection{CandidateID: "c_encounter_" + fmt.Sprint(candidateIndex), OccurrenceID: occurrenceID, ProjectionMode: "FIRST"}) - } - } - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := Compile(context.Background(), "project-a", "explorer-a", document, snapshot); err != nil { - b.Fatal(err) - } - } -} - -func fixtureDocument() authoringv2.Document { - return authoringv2.Document{APIVersion: authoringv2.APIVersion, Kind: authoringv2.Kind, Output: authoringv2.Output{ID: "patient_output", Title: "Patients"}, RootNodeID: "n_patient", RouteSteps: []authoringv2.RouteStep{{EdgeID: "e_encounter", OccurrenceID: "encounter"}, {EdgeID: "e_self", OccurrenceID: "encounter_again"}}, Selections: []authoringv2.Selection{{CandidateID: "c_patient_id", OccurrenceID: "base", ProjectionMode: "SCALAR"}, {CandidateID: "c_encounter_code", OccurrenceID: "encounter_again", ProjectionMode: "DISTINCT_ARRAY"}}, Presentation: map[string]authoringv2.Presentation{authoringv2.PresentationKey("c_patient_id", "base", "SCALAR"): {Label: "Patient ID"}}} -} +func TestCompileAcceptsEquivalentProjectIdentities(t *testing.T) { + visible := true + document := authoringv2.Document{ + Kind: authoringv2.Kind, + Output: authoringv2.Output{ID: "patient_output", Title: "Patients"}, + RootResourceType: "Patient", + Route: authoringv2.RouteNode{OccurrenceID: "base", ResourceType: "Patient"}, + Columns: []authoringv2.Column{{ + Column: "project_id", Label: "Project", LogicalType: "string", OccurrenceID: "base", + Source: authoringv2.ColumnSource{Kind: authoringv2.SourceProjectID}, + Table: &authoringv2.TablePresentation{Visible: &visible}, + }}, + } + snapshot := fixtureSnapshotForProject("HTAN_INT/BForePC") -func TestCompileNativeV2BuildsNestedRouteAndStableColumns(t *testing.T) { - snapshot := fixtureSnapshot() - result, err := Compile(context.Background(), "project-a", "explorer-a", fixtureDocument(), snapshot) - if err != nil { - t.Fatal(err) - } - if len(result.Bundle.Outputs) != 1 || len(result.Bundle.Outputs[0].Traversals) != 1 || len(result.Bundle.Outputs[0].Traversals[0].Traversals) != 1 { - t.Fatalf("route was not lowered as a nested chain: %#v", result.Bundle.Outputs[0].Traversals) - } - if got := result.Bundle.Outputs[0].Traversals[0].Traversals[0].Fields[0].ValueMode; got != "DISTINCT" { - t.Fatalf("projection value mode=%q", got) - } - if len(result.EmittedColumns) != 2 || result.EmittedColumns[0].EmissionID == result.EmittedColumns[1].EmissionID { - t.Fatalf("emissions=%#v", result.EmittedColumns) - } - nestedField := result.Bundle.Outputs[0].Traversals[0].Traversals[0].Fields[0].Name - if got, want := result.EmittedColumns[1].PublicColumn, "route_0__route_1__"+nestedField; got != want { - t.Fatalf("nested public column=%q, want engine-flattened name %q", got, want) - } - if result.Presentation.Columns[0].Label != "Patient ID" || !result.Presentation.Columns[0].Visible { - t.Fatalf("presentation=%#v", result.Presentation) - } - again, err := Compile(context.Background(), "project-a", "explorer-a", fixtureDocument(), snapshot) - if err != nil || again.RecipeDigest != result.RecipeDigest || again.EmittedColumns[0].EmissionID != result.EmittedColumns[0].EmissionID { - t.Fatalf("compile is not stable: first=%#v again=%#v err=%v", result, again, err) + for _, project := range []string{"HTAN_INT/BForePC", "HTAN_INT%2FBForePC", "HTAN_INT-BForePC"} { + t.Run(project, func(t *testing.T) { + result, err := Compile(context.Background(), project, "explorer-a", document, snapshot) + if err != nil { + t.Fatalf("Compile(%q) rejected an equivalent project identity: %v", project, err) + } + if got := string(result.Bundle.Outputs[0].Fields[0].Expr.Literal); got != `"HTAN_INT/BForePC"` { + t.Fatalf("project binding = %s, want canonical project identity", got) + } + }) } -} -func TestCompileNativeV2DefaultsPresentationToCandidateLabel(t *testing.T) { - document := fixtureDocument() - document.Presentation = map[string]authoringv2.Presentation{} - result, err := Compile(context.Background(), "project-a", "explorer-a", document, fixtureSnapshot()) - if err != nil { - t.Fatal(err) - } - if got, want := result.Presentation.Columns[0].Label, "Patient.id"; got != want { - t.Fatalf("default presentation label=%q, want candidate label %q", got, want) - } - if got, want := result.Presentation.Columns[1].Label, "Encounter.code.coding[].code"; got != want { - t.Fatalf("nested default presentation label=%q, want candidate label %q", got, want) + _, err := Compile(context.Background(), "HTAN_INT/OtherProject", "explorer-a", document, snapshot) + var compileErr *Error + if !errors.As(err, &compileErr) || compileErr.Code != "PROJECT_MISMATCH" { + t.Fatalf("different project error = %v, want PROJECT_MISMATCH", err) } } @@ -128,6 +84,12 @@ func TestCompileSemanticWorkspacePreservesAuthoredColumnsAndTypedSources(t *test if got := result.Bundle.Outputs[0].Traversals[0].Alias; got != "encounter" { t.Fatalf("traversal alias = %q", got) } + if got := result.Bundle.Outputs[0].TraversalColumnNaming; got != recipe.TraversalColumnNamingAlias { + t.Fatalf("traversal column naming = %q", got) + } + if got := result.Bundle.Outputs[0].RootColumnNaming; got != recipe.RootColumnNamingExact { + t.Fatalf("root column naming = %q", got) + } if got := string(result.Bundle.Outputs[0].Fields[1].Expr.Literal); got != `"project-a"` { t.Fatalf("project binding = %s", got) } @@ -175,33 +137,6 @@ func TestCompileSemanticWorkspacePreservesSiblingBranchesDeterministically(t *te } } -func TestCompileAllowsRepeatedSelfLoopAndLongFiniteRoutes(t *testing.T) { - snapshot := fixtureSnapshot() - document := fixtureDocument() - for i := 0; i < 12; i++ { - document.RouteSteps = append(document.RouteSteps, authoringv2.RouteStep{EdgeID: "e_self", OccurrenceID: "loop-" + string(rune('a'+i))}) - } - document.Selections[1].OccurrenceID = "loop-l" - if _, err := Compile(context.Background(), "project-a", "explorer-a", document, snapshot); err != nil { - t.Fatal(err) - } -} - -func TestCompileRejectsMismatchesWithStructuredErrors(t *testing.T) { - document := fixtureDocument() - _, err := Compile(context.Background(), "other-project", "explorer-a", document, fixtureSnapshot()) - var structured *Error - if !errors.As(err, &structured) || structured.Code != "PROJECT_MISMATCH" || structured.Stage != "capability" || structured.Path != "$.project" { - t.Fatalf("error=%#v", err) - } - document = fixtureDocument() - document.Selections[0].ProjectionMode = "ARRAY" - _, err = Compile(context.Background(), "project-a", "explorer-a", document, fixtureSnapshot()) - if !errors.As(err, &structured) || structured.Code != "UNSUPPORTED_PROJECTION_MODE" || !strings.Contains(structured.Path, "projectionMode") { - t.Fatalf("projection error=%#v", err) - } -} - func TestSemanticObservationPivotFreezesOnlyAuthoredComponentColumns(t *testing.T) { column := authoringv2.Column{ Column: "observation__observation_component_values__GENE_SYMBOL", @@ -225,8 +160,8 @@ func TestSemanticObservationPivotFreezesOnlyAuthoredComponentColumns(t *testing. } } -func TestSemanticNestedOccurrenceUsesLocalAliasAndFullPhysicalPrefix(t *testing.T) { - if got := semanticAlias("patient__condition"); got != "condition" { +func TestSemanticNestedOccurrenceUsesGloballyScopedAlias(t *testing.T) { + if got := semanticAlias("patient__condition"); got != "patient__condition" { t.Fatalf("nested alias = %q", got) } leaf, err := semanticColumnLeaf("patient__condition__code_coding_code", "patient__condition") @@ -234,3 +169,28 @@ func TestSemanticNestedOccurrenceUsesLocalAliasAndFullPhysicalPrefix(t *testing. t.Fatalf("nested physical leaf = %q, %v", leaf, err) } } + +func TestCompileSemanticAggregateUsesExactPublicName(t *testing.T) { + visible := true + document := authoringv2.Document{ + Kind: authoringv2.Kind, + Output: authoringv2.Output{ID: "patient_output", Title: "Patients"}, + RootResourceType: "Patient", + Route: authoringv2.RouteNode{OccurrenceID: "base", ResourceType: "Patient", Children: []authoringv2.RouteNode{{ + OccurrenceID: "encounter", ResourceType: "Encounter", Relationship: "encounters", + }}}, + Columns: []authoringv2.Column{ + {Column: "patient_id", Label: "Patient ID", OccurrenceID: "base", Source: authoringv2.ColumnSource{Kind: authoringv2.SourceField, FieldPath: "id", ProjectionMode: "VALUE"}, Table: &authoringv2.TablePresentation{Visible: &visible}}, + {Column: "encounter_count", Label: "Encounter count", OccurrenceID: "encounter", Source: authoringv2.ColumnSource{Kind: authoringv2.SourceAggregate, Operation: "COUNT"}, Table: &authoringv2.TablePresentation{Visible: &visible}}, + }, + } + + result, err := Compile(context.Background(), "project-a", "explorer-a", document, fixtureSnapshot()) + if err != nil { + t.Fatal(err) + } + aggregate := result.Bundle.Outputs[0].Traversals[0].Aggregates[0] + if aggregate.OutputName != "encounter_count" || aggregate.Operation != recipe.AggregateCount { + t.Fatalf("aggregate = %#v", aggregate) + } +} diff --git a/internal/explorer/compilation/workspace_test.go b/internal/explorer/compilation/workspace_test.go index 60a3fc2f..dc9820da 100644 --- a/internal/explorer/compilation/workspace_test.go +++ b/internal/explorer/compilation/workspace_test.go @@ -9,10 +9,11 @@ import ( ) func TestCompileWorkspaceProducesOneArtifactWithFiveOutputs(t *testing.T) { - workspace := authoringv2.Workspace{APIVersion: authoringv2.APIVersion, Kind: authoringv2.WorkspaceKind, Documents: []authoringv2.Document{}, Tabs: []authoringv2.Tab{}} + workspace := authoringv2.Workspace{APIVersion: authoringv2.APIVersion, Kind: authoringv2.WorkspaceKind, Explorer: authoringv2.ExplorerMetadata{Title: "Five outputs"}, Documents: []authoringv2.Document{}, Tabs: []authoringv2.Tab{}} + visible := true for i := 0; i < 5; i++ { id := fmt.Sprintf("output_%d", i) - workspace.Documents = append(workspace.Documents, authoringv2.Document{Kind: authoringv2.Kind, Output: authoringv2.Output{ID: id, Title: id}, RootNodeID: "n_patient", RouteSteps: []authoringv2.RouteStep{}, Selections: []authoringv2.Selection{{CandidateID: "c_patient_id", OccurrenceID: "base", ProjectionMode: "VALUE"}}, Presentation: map[string]authoringv2.Presentation{}}) + workspace.Documents = append(workspace.Documents, authoringv2.Document{Kind: authoringv2.Kind, Output: authoringv2.Output{ID: id, Title: id}, RootResourceType: "Patient", Route: authoringv2.RouteNode{OccurrenceID: authoringv2.RootOccurrenceID, ResourceType: "Patient"}, Columns: []authoringv2.Column{{Column: "patient_id", Label: "Patient ID", OccurrenceID: authoringv2.RootOccurrenceID, Source: authoringv2.ColumnSource{Kind: authoringv2.SourceField, FieldPath: "id", ProjectionMode: "VALUE"}, Table: &authoringv2.TablePresentation{Visible: &visible}}}}) workspace.Tabs = append(workspace.Tabs, authoringv2.Tab{ID: fmt.Sprintf("tab-%d", i), Title: id, OutputID: id, Order: i, Visible: true}) } result, err := CompileWorkspace(context.Background(), "project-a", "explorer-a", workspace, fixtureSnapshot()) @@ -32,3 +33,44 @@ func TestCompileWorkspaceProducesOneArtifactWithFiveOutputs(t *testing.T) { } } } + +func TestCompileWorkspaceCanonicalizesEquivalentProjectIdentities(t *testing.T) { + visible := true + workspace := authoringv2.Workspace{ + APIVersion: authoringv2.APIVersion, + Kind: authoringv2.WorkspaceKind, + Explorer: authoringv2.ExplorerMetadata{Title: "Patients"}, + Documents: []authoringv2.Document{{ + Kind: authoringv2.Kind, + Output: authoringv2.Output{ID: "patient_output", Title: "Patients"}, + RootResourceType: "Patient", + Route: authoringv2.RouteNode{OccurrenceID: "base", ResourceType: "Patient"}, + Columns: []authoringv2.Column{{ + Column: "project_id", Label: "Project", LogicalType: "string", OccurrenceID: "base", + Source: authoringv2.ColumnSource{Kind: authoringv2.SourceProjectID}, + Table: &authoringv2.TablePresentation{Visible: &visible}, + }}, + }}, + Tabs: []authoringv2.Tab{{ID: "patients", Title: "Patients", OutputID: "patient_output", Visible: true}}, + } + snapshot := fixtureSnapshotForProject("HTAN_INT/BForePC") + + canonical, err := CompileWorkspace(context.Background(), "HTAN_INT/BForePC", "default", workspace, snapshot) + if err != nil { + t.Fatal(err) + } + for _, project := range []string{"HTAN_INT%2FBForePC", "HTAN_INT-BForePC"} { + t.Run(project, func(t *testing.T) { + result, compileErr := CompileWorkspace(context.Background(), project, "default", workspace, snapshot) + if compileErr != nil { + t.Fatalf("CompileWorkspace(%q): %v", project, compileErr) + } + if result.Bundle.Name != canonical.Bundle.Name || result.RecipeDigest != canonical.RecipeDigest { + t.Fatalf("equivalent project changed bundle identity: name=%q digest=%q, want name=%q digest=%q", result.Bundle.Name, result.RecipeDigest, canonical.Bundle.Name, canonical.RecipeDigest) + } + if got := string(result.Bundle.Outputs[0].Fields[0].Expr.Literal); got != `"HTAN_INT/BForePC"` { + t.Fatalf("project binding = %s, want canonical project identity", got) + } + }) + } +} diff --git a/internal/explorer/compilation_receipt.go b/internal/explorer/compilation_receipt.go index 5a4bd233..1220ddff 100644 --- a/internal/explorer/compilation_receipt.go +++ b/internal/explorer/compilation_receipt.go @@ -20,10 +20,10 @@ import ( const ( // CompilationReceiptFormatVersion changes when the persisted receipt shape // or its execution invariants change incompatibly. - CompilationReceiptFormatVersion = 2 + CompilationReceiptFormatVersion = 3 // CompilationReceiptCompilerContractVersion changes when compilation // semantics change in a way that can alter a resolved receipt. - CompilationReceiptCompilerContractVersion = "loom.explorer.compiler/v7" + CompilationReceiptCompilerContractVersion = "loom.explorer.compiler/v10" // Short aliases make the current contract convenient for repositories and // callers that do not need to distinguish the receipt prefix. @@ -56,31 +56,35 @@ func CompilationArtifactDigest(raw json.RawMessage) (string, error) { // compatibility; Bundle is the resolved recipe used by execution. Neither // field contains a physical IR or rendered query. type CompilationReceipt struct { - ID string `json:"id"` - ReceiptFormatVersion int `json:"receiptFormatVersion"` - CompilerContractVersion string `json:"compilerContractVersion"` - Project string `json:"project"` - ExplorerID string `json:"explorerId"` - IntentDigest string `json:"intentDigest"` - SnapshotToken string `json:"snapshotToken"` - AuthorizationScopeDigest string `json:"authorizationScopeDigest,omitempty"` - CapabilitySchemaDigest string `json:"capabilitySchemaDigest,omitempty"` - SourceGeneration string `json:"sourceGeneration"` - CompilationKey string `json:"compilationKey,omitempty"` - RecipeDigest string `json:"recipeDigest"` - ResolvedRecipeDigest string `json:"resolvedRecipeDigest,omitempty"` - ResolvedSchemaDigest string `json:"resolvedSchemaDigest,omitempty"` - OutputContractDigest string `json:"outputContractDigest,omitempty"` - NormalizedBundle json.RawMessage `json:"normalizedBundle"` - Bundle recipe.Bundle `json:"compiledRecipe"` - CompiledConfig json.RawMessage `json:"compiledConfig,omitempty"` - PublicOutputContract json.RawMessage `json:"publicOutputContract,omitempty"` - IdentityMappings []IdentityMapping `json:"identityMappings"` - EmittedColumns []EmittedColumn `json:"emittedColumns"` - OutputFingerprints map[string]string `json:"outputFingerprints,omitempty"` - Warnings []CompilationWarning `json:"warnings,omitempty"` - RequestID string `json:"requestId,omitempty"` - CreatedAt time.Time `json:"createdAt"` + ID string `json:"id"` + ReceiptFormatVersion int `json:"receiptFormatVersion"` + CompilerContractVersion string `json:"compilerContractVersion"` + Project string `json:"project"` + ExplorerID string `json:"explorerId"` + IntentDigest string `json:"intentDigest"` + SnapshotToken string `json:"snapshotToken"` + AuthorizationScopeDigest string `json:"authorizationScopeDigest,omitempty"` + CapabilitySchemaDigest string `json:"capabilitySchemaDigest,omitempty"` + SourceGeneration string `json:"sourceGeneration"` + CompilationKey string `json:"compilationKey,omitempty"` + RecipeDigest string `json:"recipeDigest"` + ResolvedRecipeDigest string `json:"resolvedRecipeDigest,omitempty"` + ResolvedSchemaDigest string `json:"resolvedSchemaDigest,omitempty"` + OutputContractDigest string `json:"outputContractDigest,omitempty"` + NormalizedBundle json.RawMessage `json:"normalizedBundle"` + Bundle recipe.Bundle `json:"compiledRecipe"` + CompiledConfig json.RawMessage `json:"compiledConfig,omitempty"` + PublicOutputContract json.RawMessage `json:"publicOutputContract,omitempty"` + IdentityMappings []IdentityMapping `json:"identityMappings"` + EmittedColumns []EmittedColumn `json:"emittedColumns"` + OutputFingerprints map[string]string `json:"outputFingerprints,omitempty"` + // OutputColumnProvenance is the durable publication behavior for every + // compiler output column. Recipe Discovered flags are compiler-local and + // intentionally do not cross the authoring recipe JSON boundary. + OutputColumnProvenance map[string]map[string]string `json:"outputColumnProvenance,omitempty"` + Warnings []CompilationWarning `json:"warnings,omitempty"` + RequestID string `json:"requestId,omitempty"` + CreatedAt time.Time `json:"createdAt"` } type IdentityMapping struct { @@ -158,23 +162,24 @@ func ReceiptID(r CompilationReceipt) (string, error) { return "", fmt.Errorf("canonical public output contract: %w", err) } identity := struct { - CompilationKey string `json:"compilationKey"` - RecipeDigest string `json:"recipeDigest"` - ResolvedRecipeDigest string `json:"resolvedRecipeDigest,omitempty"` - ResolvedSchemaDigest string `json:"resolvedSchemaDigest,omitempty"` - OutputContractDigest string `json:"outputContractDigest,omitempty"` - Bundle recipe.Bundle `json:"compiledRecipe"` - CompiledConfig []byte `json:"compiledConfig,omitempty"` - PublicOutputContract []byte `json:"publicOutputContract,omitempty"` - Mappings []IdentityMapping `json:"identityMappings"` - Emissions []EmittedColumn `json:"emittedColumns"` - Fingerprints map[string]string `json:"outputFingerprints,omitempty"` - Warnings []CompilationWarning `json:"warnings,omitempty"` + CompilationKey string `json:"compilationKey"` + RecipeDigest string `json:"recipeDigest"` + ResolvedRecipeDigest string `json:"resolvedRecipeDigest,omitempty"` + ResolvedSchemaDigest string `json:"resolvedSchemaDigest,omitempty"` + OutputContractDigest string `json:"outputContractDigest,omitempty"` + Bundle recipe.Bundle `json:"compiledRecipe"` + CompiledConfig []byte `json:"compiledConfig,omitempty"` + PublicOutputContract []byte `json:"publicOutputContract,omitempty"` + Mappings []IdentityMapping `json:"identityMappings"` + Emissions []EmittedColumn `json:"emittedColumns"` + Fingerprints map[string]string `json:"outputFingerprints,omitempty"` + ColumnProvenance map[string]map[string]string `json:"outputColumnProvenance,omitempty"` + Warnings []CompilationWarning `json:"warnings,omitempty"` }{ key, r.RecipeDigest, r.ResolvedRecipeDigest, r.ResolvedSchemaDigest, r.OutputContractDigest, r.Bundle, compiledConfig, publicContract, r.IdentityMappings, r.EmittedColumns, - r.OutputFingerprints, r.Warnings, + r.OutputFingerprints, r.OutputColumnProvenance, r.Warnings, } return digestIdentity("receipt_", identity) } @@ -266,6 +271,17 @@ func (r CompilationReceipt) Validate() error { if r.OutputContractDigest != contractDigest { return fmt.Errorf("%w: receipt output contract digest mismatch: got %q want %q", ErrReceiptRecompileRequired, r.OutputContractDigest, contractDigest) } + if err := validateOutputColumnProvenance(r.OutputColumnProvenance); err != nil { + return err + } + if len(r.OutputColumnProvenance) != len(r.Bundle.Outputs) { + return fmt.Errorf("receipt output column provenance output set changed") + } + for _, output := range r.Bundle.Outputs { + if _, ok := r.OutputColumnProvenance[output.Name]; !ok { + return fmt.Errorf("receipt output column provenance is missing output %q", output.Name) + } + } } if r.ID != "" { if err := r.ValidateID(); err != nil { @@ -290,6 +306,23 @@ func (r CompilationReceipt) Validate() error { return nil } +func validateOutputColumnProvenance(values map[string]map[string]string) error { + if len(values) == 0 { + return fmt.Errorf("receipt output column provenance is required") + } + for output, columns := range values { + if strings.TrimSpace(output) == "" || len(columns) == 0 { + return fmt.Errorf("receipt output column provenance contains an empty output") + } + for column, provenance := range columns { + if strings.TrimSpace(column) == "" || (provenance != "EXPLICIT" && provenance != "DISCOVERED") { + return fmt.Errorf("receipt output column provenance is invalid for %q/%q", output, column) + } + } + } + return nil +} + // ValidateID verifies the stored ID against the receipt's content identity. func (r CompilationReceipt) ValidateID() error { if strings.TrimSpace(r.ID) == "" { diff --git a/internal/explorer/compilation_receipt_test.go b/internal/explorer/compilation_receipt_test.go index 6ba24464..d3e47a2f 100644 --- a/internal/explorer/compilation_receipt_test.go +++ b/internal/explorer/compilation_receipt_test.go @@ -26,6 +26,7 @@ func testReceipt() CompilationReceipt { Bundle: recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "receipt-test", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "out", RootResourceType: "Patient", RowGrain: "patient"}}}, CompiledConfig: json.RawMessage(`{"views":[]}`), PublicOutputContract: json.RawMessage(`{"outputs":[{"outputId":"out","columns":[]}]}`), + OutputColumnProvenance: map[string]map[string]string{"out": {"__loom_row_id": "EXPLICIT"}}, Warnings: []CompilationWarning{{Code: "EMPTY_OUTPUT", Message: "output has no selected fields"}}, } r.ResolvedRecipeDigest, _ = r.Bundle.Digest() @@ -52,6 +53,31 @@ func TestCompilationReceiptIdentityExcludesMutableMetadata(t *testing.T) { } } +func TestCompilationReceiptIdentityIncludesDurableColumnProvenance(t *testing.T) { + base := testReceipt() + first, err := ReceiptID(base) + if err != nil { + t.Fatal(err) + } + base.OutputColumnProvenance["out"]["__loom_row_id"] = "DISCOVERED" + second, err := ReceiptID(base) + if err != nil { + t.Fatal(err) + } + if first == second { + t.Fatal("durable publication provenance did not change receipt identity") + } +} + +func TestCompilationReceiptRejectsV2V7Contract(t *testing.T) { + receipt := testReceipt() + receipt.ReceiptFormatVersion = 2 + receipt.CompilerContractVersion = "loom.explorer.compiler/v7" + if err := receipt.Validate(); err == nil { + t.Fatal("accepted obsolete v2/v7 receipt") + } +} + func TestCompilationKeyChangesWithScopeAndCompilerContract(t *testing.T) { base := testReceipt() first, err := CompilationKey(base) diff --git a/internal/explorer/lifecycle/authoring.go b/internal/explorer/lifecycle/authoring.go new file mode 100644 index 00000000..8f7514bb --- /dev/null +++ b/internal/explorer/lifecycle/authoring.go @@ -0,0 +1,96 @@ +package lifecycle + +import ( + "context" + "errors" + "strings" + + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/capability" + explorercompilation "github.com/calypr/loom/internal/explorer/compilation" +) + +func (s *Service) ApplyCommands(ctx context.Context, project, explorerID string, request authoringv2.ApplyCommandsRequest, actor string) (*authoringv2.ApplyCommandsResponse, error) { + if err := request.Validate(); err != nil { + return nil, malformed("commands", err.Error(), err) + } + if s.config.Capability.Token == nil || s.config.Capability.Catalog == nil { + return nil, unavailable("commands", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured", nil) + } + snapshot, err := s.config.Capability.Token(ctx, project, request.SnapshotToken) + if err != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { + return nil, conflict("commands", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale or unavailable", nil, err) + } + response, err := s.store.ApplyWorkspaceCommands(ctx, project, explorerID, s.config.Capability.Catalog(snapshot, explorerID), request, actor) + switch { + case errors.Is(err, explorer.ErrDraftConflict): + return nil, conflict("commands", "DRAFT_CONFLICT", "the Explorer draft changed; reload before editing", nil, err) + case errors.Is(err, explorer.ErrAuthoringCommandConflict): + return nil, conflict("commands", "COMMAND_ID_CONFLICT", "commandId was already used for different intent", nil, err) + case err != nil: + return nil, unprocessable("commands", "INVALID_AUTHORING_COMMAND", err.Error(), err) + default: + return response, nil + } +} + +func (s *Service) compile(ctx context.Context, request compileRequest) (*explorer.CompilationReceipt, error) { + if (s.config.Capability.Token == nil && s.config.Capability.ForCompilation == nil) || s.config.CompileReceipt == nil { + return nil, unavailable("compile", "CAPABILITY_UNAVAILABLE", "Explorer V2 compiler is not configured", nil) + } + authorized, snapshot, err := s.resolveCompilationCapability(ctx, request.Project, request.SnapshotToken) + if err != nil { + return nil, conflict("capability", "STALE_CATALOG_SNAPSHOT", "the capability snapshot is stale or unavailable", map[string]any{"snapshotToken": request.SnapshotToken}, err) + } + workspace := request.Workspace.NormalizePresentationOrders() + if err := (authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, Workspace: &workspace, Catalog: s.catalog(snapshot, request.ExplorerID)}).Validate(); err != nil { + return nil, unprocessable("intent", workspaceValidationCode(err), err.Error(), err) + } + receipt, err := s.config.CompileReceipt(ctx, CompileReceiptRequest{Project: request.Project, ExplorerID: request.ExplorerID, Workspace: workspace, SnapshotToken: snapshot.Token, RequestID: request.RequestID, Authorized: authorized}) + if err != nil { + var compileErr *explorercompilation.Error + if errors.As(err, &compileErr) { + return nil, &Error{Class: ClassUnprocessable, Stage: compileErr.Stage, Code: compilationErrorCode(compileErr.Code), Message: compileErr.Message, Details: compileErr.Details, Cause: err} + } + return nil, err + } + if receipt == nil || strings.TrimSpace(receipt.ID) == "" { + return nil, unavailable("compile", "COMPILATION_RECEIPT_STORE_FAILED", "compiled authoring receipt was not persisted", nil) + } + return receipt, nil +} + +func (s *Service) Reconcile(ctx context.Context, request ReconcileRequest) (*explorer.CompilationReceipt, error) { + if strings.TrimSpace(request.SnapshotToken) == "" || request.DraftVersion < 0 { + return nil, malformed("reconcile", "snapshotToken, draftVersion, and draftDigest are required", nil) + } + owner, err := s.store.Get(ctx, request.Project, request.ExplorerID) + if err != nil { + return nil, err + } + if owner.DraftVersion != request.DraftVersion || owner.DraftDigest != request.DraftDigest { + return nil, conflict("reconcile", "DRAFT_CONFLICT", "the Explorer draft changed; reload before reconciling", nil, explorer.ErrDraftConflict) + } + workspace, err := authoringv2.DecodeWorkspace(owner.DraftConfig) + if err != nil { + return nil, conflict("reconcile", "AUTHORING_STATE_MISSING", "the saved Explorer draft cannot be reconciled", nil, err) + } + return s.compile(ctx, compileRequest{Project: request.Project, ExplorerID: request.ExplorerID, Workspace: workspace, SnapshotToken: request.SnapshotToken}) +} + +func (s *Service) catalog(snapshot capability.Snapshot, explorerID string) authoringv2.CatalogSnapshot { + if s.config.Capability.Catalog == nil { + return authoringv2.CatalogSnapshot{} + } + return s.config.Capability.Catalog(snapshot, explorerID) +} + +func (s *Service) resolveCompilationCapability(ctx context.Context, project, token string) (AuthorizedCapability, capability.Snapshot, error) { + if s.config.Capability.ForCompilation != nil { + authorized, err := s.config.Capability.ForCompilation(ctx, project, token) + return authorized, authorized.Snapshot, err + } + snapshot, err := s.config.Capability.Token(ctx, project, token) + return AuthorizedCapability{Snapshot: snapshot}, snapshot, err +} diff --git a/internal/explorer/lifecycle/errors.go b/internal/explorer/lifecycle/errors.go new file mode 100644 index 00000000..8e7b2a8f --- /dev/null +++ b/internal/explorer/lifecycle/errors.go @@ -0,0 +1,94 @@ +package lifecycle + +import ( + "errors" + + "github.com/calypr/loom/internal/explorer" +) + +// ErrorClass lets transports map application failures to their own response +// contracts without making HTTP status codes part of the application layer. +type ErrorClass string + +const ( + ClassMalformed ErrorClass = "malformed" + ClassForbidden ErrorClass = "forbidden" + ClassNotFound ErrorClass = "not_found" + ClassConflict ErrorClass = "conflict" + ClassUnprocessable ErrorClass = "unprocessable" + ClassUnavailable ErrorClass = "unavailable" + ClassInternal ErrorClass = "internal" +) + +// Error is a typed application failure. Details are safe, structured +// diagnostics; Cause remains available to server-side logging and is never +// serialized by lifecycle. +type Error struct { + Class ErrorClass + Stage string + Code string + Path string + Message string + Details map[string]any + Cause error +} + +func (e *Error) Error() string { + if e == nil { + return "Explorer lifecycle error" + } + if e.Code == "" { + return e.Message + } + return e.Code + ": " + e.Message +} + +func (e *Error) Unwrap() error { + if e == nil { + return nil + } + return e.Cause +} + +func failure(class ErrorClass, stage, code, message string, cause error) error { + return &Error{Class: class, Stage: stage, Code: code, Message: message, Cause: cause} +} + +func failureDetails(class ErrorClass, stage, code, message string, details map[string]any, cause error) error { + return &Error{Class: class, Stage: stage, Code: code, Message: message, Details: details, Cause: cause} +} + +func malformed(stage, message string, cause error) error { + return failure(ClassMalformed, stage, "MALFORMED_REQUEST", message, cause) +} + +func unavailable(stage, code, message string, cause error) error { + return failure(ClassUnavailable, stage, code, message, cause) +} + +func conflict(stage, code, message string, details map[string]any, cause error) error { + return failureDetails(ClassConflict, stage, code, message, details, cause) +} + +func forbidden(stage, message string, cause error) error { + return failure(ClassForbidden, stage, "FORBIDDEN", message, cause) +} + +func notFound(stage, code, message string, cause error) error { + return failure(ClassNotFound, stage, code, message, cause) +} + +func unprocessable(stage, code, message string, cause error) error { + return failure(ClassUnprocessable, stage, code, message, cause) +} + +func internal(stage, code, message string, cause error) error { + return failure(ClassInternal, stage, code, message, cause) +} + +func wrapReceiptLookup(stage string, err error) error { + if errors.Is(err, explorer.ErrNotFound) { + return notFound(stage, "COMPILE_RECEIPT_NOT_FOUND", "compilation receipt was not found", err) + } + return unavailable(stage, "RECEIPT_STORE_UNAVAILABLE", "the compilation receipt store is unavailable", err) +} diff --git a/internal/explorer/lifecycle/policy.go b/internal/explorer/lifecycle/policy.go new file mode 100644 index 00000000..210f2be1 --- /dev/null +++ b/internal/explorer/lifecycle/policy.go @@ -0,0 +1,251 @@ +package lifecycle + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "sort" + "strings" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataset" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/capability" + "github.com/calypr/loom/internal/projectid" +) + +func selectorsForBundle(bundle recipe.Bundle) []dataset.DataframeSelector { + selectors := make([]dataset.DataframeSelector, 0, len(bundle.Outputs)) + for _, output := range bundle.Outputs { + selectors = append(selectors, dataset.DataframeSelector{Recipe: bundle.Name, TranslationVersion: bundle.TranslationVersion, Output: output.Name}) + } + return selectors +} + +func verifyQueryableOutputs(bundle recipe.Bundle, execution Execution) error { + states := make(map[string]string, len(execution.Outputs)) + for _, output := range execution.Outputs { + states[output.Name] = strings.ToUpper(output.State) + } + for _, output := range bundle.Outputs { + state := states[output.Name] + if state != "PUBLISHED" && state != "READY" && state != "ACTIVE" { + return fmt.Errorf("output %q is not queryable (state %q)", output.Name, state) + } + } + return nil +} + +func materializations(bundle recipe.Bundle, execution Execution) []explorer.Materialization { + out := make([]explorer.Materialization, 0, len(execution.Outputs)) + for _, output := range execution.Outputs { + selector := dataset.DataframeSelector{Recipe: bundle.Name, TranslationVersion: bundle.TranslationVersion, Output: output.Name} + out = append(out, explorer.Materialization{OutputID: output.Name, Output: output.Name, MaterializationID: execution.ID, Selector: &selector, Columns: output.Columns}) + } + return out +} + +func datasetMetadataFromExecution(bundle recipe.Bundle, generation, schemaDigest string, execution Execution) explorer.DatasetMetadata { + outputs := make([]explorer.DatasetOutput, 0, len(execution.Outputs)) + for _, output := range execution.Outputs { + state := strings.ToUpper(output.State) + selector := dataset.DataframeSelector{Recipe: bundle.Name, TranslationVersion: bundle.TranslationVersion, Output: output.Name} + outputs = append(outputs, explorer.DatasetOutput{Name: output.Name, State: state, Queryable: state == "PUBLISHED" || state == "READY" || state == "ACTIVE", Selector: &selector, Columns: output.Columns}) + } + return explorer.DatasetMetadata{Generation: generation, SchemaDigest: schemaDigest, Outputs: outputs} +} + +func receiptHasOutput(bundle recipe.Bundle, id string) bool { + for _, output := range bundle.Outputs { + if output.Name == id { + return true + } + } + return false +} + +func emittedColumnsForOutput(receipt *explorer.CompilationReceipt, outputID string) []explorer.EmittedColumn { + columns := make([]explorer.EmittedColumn, 0) + if receipt == nil { + return columns + } + for _, column := range receipt.EmittedColumns { + if column.OutputID == outputID { + columns = append(columns, column) + } + } + return columns +} + +func (s *Service) lookupReceipt(ctx context.Context, project, explorerID, receiptID string) (*explorer.CompilationReceipt, error) { + var ( + receipt *explorer.CompilationReceipt + err error + ) + if s.config.ReceiptLookup != nil { + receipt, err = s.config.ReceiptLookup(ctx, project, explorerID, receiptID) + } else { + receipt, err = s.store.CompilationReceiptForExplorer(ctx, project, explorerID, receiptID) + } + if err != nil { + return nil, wrapReceiptLookup("receipt", err) + } + return receipt, nil +} + +func (s *Service) validateReceiptRoute(receipt *explorer.CompilationReceipt, project, explorerID string) error { + if receipt == nil || projectid.Canonical(receipt.Project) != projectid.Canonical(project) || receipt.ExplorerID != explorerID { + return notFound("receipt", "COMPILE_RECEIPT_NOT_FOUND", "compilation receipt was not found", explorer.ErrNotFound) + } + if strings.TrimSpace(receipt.ID) == "" || strings.TrimSpace(receipt.RecipeDigest) == "" || len(receipt.Bundle.Outputs) == 0 { + return conflict("receipt", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt is from an unsupported or incomplete compiler contract", nil, nil) + } + native := s.config.CompileReceipt != nil || s.config.PreviewReceipt != nil || s.config.MaterializeReceipt != nil + if native && (receipt.ReceiptFormatVersion != explorer.CurrentReceiptFormatVersion || receipt.CompilerContractVersion != explorer.CurrentCompilerContractVersion) { + return conflict("receipt", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt is from an unsupported compiler contract", nil, nil) + } + if native { + if err := receipt.Validate(); err != nil { + return conflict("receipt", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt failed integrity validation and must be recompiled", nil, err) + } + } + return nil +} + +func validateReceiptCapability(receipt *explorer.CompilationReceipt, snapshot capability.Snapshot) error { + if receipt == nil { + return fmt.Errorf("compilation receipt is required") + } + if snapshot.Identity.Project != receipt.Project || snapshot.Identity.Generation != receipt.SourceGeneration { + return fmt.Errorf("receipt capability project or generation changed") + } + if snapshot.Identity.AuthorizationScopeDigest != receipt.AuthorizationScopeDigest { + return fmt.Errorf("receipt authorization scope changed") + } + if snapshot.Identity.SchemaDigest != receipt.CapabilitySchemaDigest { + return fmt.Errorf("receipt capability schema changed") + } + return nil +} + +func validateAuthorizedReceiptExecution(receipt *explorer.CompilationReceipt, authorized AuthorizedCapability) error { + if receipt == nil { + return fmt.Errorf("receipt is required") + } + snapshot := authorized.Snapshot + if strings.TrimSpace(receipt.SnapshotToken) == "" || snapshot.Token != receipt.SnapshotToken { + return fmt.Errorf("capability token does not exactly match receipt token") + } + if err := snapshot.ValidateToken(receipt.SnapshotToken); err != nil { + return fmt.Errorf("capability token is invalid: %v", err) + } + if snapshot.Identity.Project != receipt.Project { + return fmt.Errorf("capability project changed") + } + if snapshot.Identity.Generation != receipt.SourceGeneration { + return fmt.Errorf("capability generation changed") + } + if snapshot.Identity.SchemaDigest != receipt.CapabilitySchemaDigest { + return fmt.Errorf("capability schema changed") + } + if snapshot.Identity.AuthorizationScopeDigest != receipt.AuthorizationScopeDigest { + return fmt.Errorf("capability scope digest changed") + } + return validateAuthorizedReadScope(authorized.Scope, receipt.AuthorizationScopeDigest) +} + +func validateAuthorizedReadScope(scope authscope.ReadScope, expectedDigest string) error { + switch scope.Mode { + case authscope.ReadScopeUnrestricted: + if len(scope.AuthResourcePaths) != 0 { + return fmt.Errorf("unrestricted scope must not carry resource paths") + } + case authscope.ReadScopeRestricted: + // Empty restricted scopes intentionally remain deny-all. + default: + return fmt.Errorf("authorization scope mode is missing or invalid") + } + paths := append([]string(nil), scope.AuthResourcePaths...) + sort.Strings(paths) + sum := sha256.Sum256([]byte(string(scope.Mode) + "\x00" + strings.Join(paths, "\x00"))) + if hex.EncodeToString(sum[:]) != expectedDigest { + return fmt.Errorf("authorized scope does not match receipt scope digest") + } + return nil +} + +func validateReceiptOutputContract(receipt *explorer.CompilationReceipt, outputID string) error { + if receipt == nil { + return fmt.Errorf("receipt is required") + } + outputID = strings.TrimSpace(outputID) + if outputID == "" { + return fmt.Errorf("output is required") + } + contract, err := explorer.DecodePublicOutputContracts(receipt.PublicOutputContract) + if err != nil { + return fmt.Errorf("public output contract is invalid: %v", err) + } + if _, ok := contract.Output(outputID); !ok { + return fmt.Errorf("requested output %q is not in the receipt public contract", outputID) + } + if !receiptHasOutput(receipt.Bundle, outputID) { + return fmt.Errorf("requested output %q is not in the receipt recipe", outputID) + } + return nil +} + +func applyAuthorizedScope(bindings *recipe.RuntimeBindings, authorized AuthorizedCapability, includeAuthResourcePath bool) { + if bindings == nil { + return + } + bindings.AuthResourcePaths = append([]string(nil), authorized.Scope.AuthResourcePaths...) + bindings.AuthScopeMode = authorized.Scope.Mode + bindings.IncludeAuthResourcePath = includeAuthResourcePath +} + +func workspaceValidationCode(err error) string { + message := err.Error() + for _, code := range []string{"DUPLICATE_OUTPUT_ID", "DUPLICATE_TAB_ID", "INVALID_TAB_OUTPUT_MAPPING", "INVALID_TAB_ORDER", "ROW_ROOT_NOT_ELIGIBLE", "UNSUPPORTED_FILTER", "UNSUPPORTED_CHART", "NO_VISIBLE_COLUMNS"} { + if strings.Contains(message, code) { + return code + } + } + switch { + case strings.Contains(message, "rootNodeId"): + return "INVALID_ROOT_NODE" + case strings.Contains(message, "route") || strings.Contains(message, "edge"): + return "INVALID_ROUTE" + case strings.Contains(message, "occurrence"): + return "INVALID_OCCURRENCE" + case strings.Contains(message, "projection mode"): + return "INVALID_PROJECTION_MODE" + case strings.Contains(message, "duplicate selection"): + return "DUPLICATE_SELECTION" + default: + return "INVALID_AUTHORING_INTENT" + } +} + +func compilationErrorCode(code string) string { + switch code { + case "STALE_ROOT_NODE": + return "INVALID_ROOT_NODE" + case "ROOT_NOT_ELIGIBLE", "UNSUPPORTED_ROW_ROOT": + return "ROW_ROOT_NOT_ELIGIBLE" + case "STALE_EDGE": + return "STALE_EDGE_ID" + case "DISCONNECTED_ROUTE", "REPEATED_EDGE_NOT_ALLOWED", "SELF_LOOP_NOT_ALLOWED", "ROUTE_TOO_LONG": + return "INVALID_ROUTE" + case "STALE_CANDIDATE": + return "STALE_CANDIDATE_ID" + case "STALE_OCCURRENCE", "DUPLICATE_OCCURRENCE": + return "INVALID_OCCURRENCE" + case "UNSUPPORTED_PROJECTION_MODE": + return "INVALID_PROJECTION_MODE" + default: + return code + } +} diff --git a/internal/explorer/lifecycle/preview.go b/internal/explorer/lifecycle/preview.go new file mode 100644 index 00000000..6e22510e --- /dev/null +++ b/internal/explorer/lifecycle/preview.go @@ -0,0 +1,61 @@ +package lifecycle + +import ( + "context" + "strings" + + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/projectid" +) + +func (s *Service) Preview(ctx context.Context, request PreviewRequest) (PreviewResult, error) { + if strings.TrimSpace(request.ReceiptID) == "" || strings.TrimSpace(request.OutputID) == "" { + return PreviewResult{}, malformed("preview", "receiptId and outputId are required", nil) + } + if request.Limit == 0 { + request.Limit = dataframeexecution.DefaultPreviewLimit + } + if request.Limit > dataframeexecution.MaxPreviewLimit { + return PreviewResult{}, unprocessable("preview", "INVALID_PREVIEW_LIMIT", "limit must be between 1 and 1000", nil) + } + if s.config.Capability.ForExecution == nil { + return PreviewResult{}, unavailable("preview", "PREVIEW_UNAVAILABLE", "authorized receipt execution is not configured", nil) + } + if s.config.PreviewReceipt == nil { + return PreviewResult{}, unavailable("preview", "PREVIEW_UNAVAILABLE", "Explorer preview is not configured", nil) + } + receipt, err := s.lookupReceipt(ctx, request.Project, request.ExplorerID, request.ReceiptID) + if err != nil { + return PreviewResult{}, err + } + if err := s.validateReceiptRoute(receipt, request.Project, request.ExplorerID); err != nil { + return PreviewResult{}, err + } + authorized, err := s.config.Capability.ForExecution(ctx, receipt.Project, receipt.SnapshotToken) + if err != nil || authorized.Snapshot.ValidateToken(receipt.SnapshotToken) != nil || strings.TrimSpace(authorized.Snapshot.Identity.Generation) != strings.TrimSpace(receipt.SourceGeneration) { + return PreviewResult{}, conflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil, err) + } + if err := validateAuthorizedReceiptExecution(receipt, authorized); err != nil { + return PreviewResult{}, conflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil, err) + } + if !receiptHasOutput(receipt.Bundle, request.OutputID) || validateReceiptOutputContract(receipt, request.OutputID) != nil { + return PreviewResult{}, unprocessable("preview", "UNKNOWN_AUTHORING_OUTPUT", "outputId is not in the receipt", nil) + } + bindings := recipe.RuntimeBindings{Project: projectid.Legacy(receipt.Project), DatasetGeneration: receipt.SourceGeneration, PreviewLimit: request.Limit, OutputNames: []string{request.OutputID}} + applyAuthorizedScope(&bindings, authorized, false) + columns := emittedColumnsForOutput(receipt, request.OutputID) + result := PreviewResult{Receipt: receipt, Columns: columns} + if request.SinkFactory == nil { + return PreviewResult{}, unavailable("preview", "PREVIEW_UNAVAILABLE", "native preview sink is not configured", nil) + } + sink, err := request.SinkFactory(receipt, columns) + if err != nil { + return PreviewResult{}, err + } + result.Summary, err = s.config.PreviewReceipt(ctx, receipt, bindings, sink) + if err != nil { + return PreviewResult{}, err + } + return result, nil +} diff --git a/internal/explorer/lifecycle/publish.go b/internal/explorer/lifecycle/publish.go new file mode 100644 index 00000000..c2abf4a5 --- /dev/null +++ b/internal/explorer/lifecycle/publish.go @@ -0,0 +1,121 @@ +package lifecycle + +import ( + "context" + "strings" + + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/capability" + "github.com/calypr/loom/internal/projectid" +) + +func (s *Service) Publish(ctx context.Context, request PublishRequest) (PublishResult, error) { + if strings.TrimSpace(request.ReceiptID) == "" { + return PublishResult{}, malformed("publish", "receiptId is required", nil) + } + if s.config.MaterializeReceipt == nil || s.config.PrepareRelease == nil || s.config.ValidateReleaseGeneration == nil || (s.config.Capability.Token == nil && s.config.Capability.ForExecution == nil) { + return PublishResult{}, unavailable("publish", "PUBLICATION_UNAVAILABLE", "Explorer publication is not configured", nil) + } + receipt, err := s.lookupReceipt(ctx, request.Project, request.ExplorerID, request.ReceiptID) + if err != nil { + return PublishResult{}, err + } + if err := s.validateReceiptRoute(receipt, request.Project, request.ExplorerID); err != nil { + return PublishResult{}, err + } + authorized, snapshot, err := s.resolveExecutionCapability(ctx, receipt.Project, receipt.SnapshotToken) + if err != nil || strings.TrimSpace(snapshot.Identity.Generation) != strings.TrimSpace(receipt.SourceGeneration) { + return PublishResult{}, conflict("publish", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil, err) + } + if s.config.CompileReceipt != nil { + if err := validateReceiptCapability(receipt, snapshot); err != nil { + return PublishResult{}, conflict("publish", "RECEIPT_STALE", err.Error(), nil, err) + } + } + if len(receipt.EmittedColumns) == 0 || len(receipt.CompiledConfig) == 0 { + return PublishResult{}, unprocessable("publish", "NO_SELECTED_COLUMNS", "select at least one output column before publishing", nil) + } + if s.config.CompileReceipt != nil { + workspace, decodeErr := authoringv2.DecodeWorkspace(receipt.NormalizedBundle) + if decodeErr != nil { + return PublishResult{}, conflict("publish", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt contains invalid authoring intent", nil, decodeErr) + } + if err := workspace.ValidateForPublication(); err != nil { + return PublishResult{}, unprocessable("publish", "NO_SELECTED_COLUMNS", "select at least one visible output column for every visible table before publishing", err) + } + } + if err := s.config.ValidateReleaseGeneration(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration); err != nil { + return PublishResult{}, conflict("publish", "RECEIPT_STALE", "the receipt generation is no longer active", map[string]any{"generation": receipt.SourceGeneration}, err) + } + bindings := recipe.RuntimeBindings{Project: projectid.Legacy(receipt.Project), DatasetGeneration: receipt.SourceGeneration} + applyAuthorizedScope(&bindings, authorized, true) + execution, err := s.config.MaterializeReceipt(ctx, receipt, bindings) + if err != nil { + if resourceErr := materializationResourceError("materialize", err); resourceErr != nil { + return PublishResult{}, resourceErr + } + if userErr, ok := dataframeerrors.AsUserError(err); ok { + switch userErr.Code() { + case string(dataframeerrors.CodePublicationInProgress): + details := userErr.Details() + if details == nil { + details = make(map[string]any) + } + details["retryable"] = userErr.Retryable() + return PublishResult{}, conflict("materialize", userErr.Code(), "Explorer publication is already in progress; retry after it completes", details, err) + } + } + return PublishResult{}, unavailable("materialize", "MATERIALIZATION_FAILED", "Explorer materialization failed; the active revision was retained", err) + } + if err := verifyQueryableOutputs(receipt.Bundle, execution); err != nil { + return PublishResult{}, unavailable("materialize", "MATERIALIZATION_FAILED", "materialization did not produce queryable outputs", err) + } + release, expectedReleaseRevision, err := s.config.PrepareRelease(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration, selectorsForBundle(receipt.Bundle)) + if err != nil { + return PublishResult{}, unavailable("activation", "MATERIALIZATION_ACTIVATION_FAILED", "dataset release preparation failed; the prior Explorer revision was retained", err) + } + now := s.now() + revisionID := "authoring_" + strings.TrimPrefix(receipt.ID, "receipt_") + revisionValue := explorer.Revision{ID: revisionID, Project: receipt.Project, ExplorerID: receipt.ExplorerID, Config: receipt.CompiledConfig, AuthoringBundle: receipt.NormalizedBundle, IntentDigest: receipt.IntentDigest, CompilationReceiptID: receipt.ID, PublicOutputContract: receipt.PublicOutputContract, Recipe: receipt.Bundle, RecipeDigest: receipt.RecipeDigest, ResolvedSchemaDigest: receipt.ResolvedSchemaDigest, SourceGeneration: receipt.SourceGeneration, Materializations: materializations(receipt.Bundle, execution), EmittedColumns: receipt.EmittedColumns, Dataset: datasetMetadataFromExecution(receipt.Bundle, receipt.SourceGeneration, receipt.ResolvedSchemaDigest, execution), Publication: explorer.PublicationMetadata{State: string(explorer.RevisionReady), Generation: receipt.SourceGeneration, ExecutionID: execution.ID, UpdatedAt: now}, Status: explorer.RevisionReady, CreatedBy: request.Actor, CreatedAt: now, ReadyAt: &now} + revision, err := s.store.PublishAuthoring(ctx, *receipt, revisionValue, release, expectedReleaseRevision) + if err != nil { + return PublishResult{}, err + } + if s.config.PersistPublishedWorkspace != nil { + if err := s.config.PersistPublishedWorkspace(ctx, receipt.Project, receipt.ExplorerID, receipt.NormalizedBundle); err != nil { + return PublishResult{}, unavailable("local_writeback", "LOCAL_WORKSPACE_WRITEBACK_FAILED", "Explorer published, but the local CONFIG workspace could not be updated", err) + } + } + return PublishResult{Receipt: receipt, Revision: revision, Execution: execution}, nil +} + +func materializationResourceError(stage string, err error) error { + userErr, ok := dataframeerrors.AsUserError(err) + if !ok { + return nil + } + var message string + switch userErr.Code() { + case string(dataframeerrors.CodeQueryMemoryLimitExceeded): + message = "Explorer materialization exceeded the ArangoDB query memory limit. Reduce the Explorer query complexity or ask an operator to increase ArangoDB's query-memory-limit, then publish again. The active revision was retained." + case string(dataframeerrors.CodeQueryResourceLimitExceeded): + message = "ArangoDB rejected Explorer materialization because the query exceeded a configured resource limit. Check the ArangoDB server log for the exact limit, then adjust the query or server configuration before publishing again. The active revision was retained." + case string(dataframeerrors.CodeQueryBackendOutOfMemory): + message = "ArangoDB ran out of memory while materializing the Explorer. Reduce the Explorer query complexity or ask an operator to increase the memory available to ArangoDB, then publish again. The active revision was retained." + default: + return nil + } + return failureDetails(ClassUnavailable, stage, userErr.Code(), message, userErr.Details(), err) +} + +func (s *Service) resolveExecutionCapability(ctx context.Context, project, token string) (AuthorizedCapability, capability.Snapshot, error) { + if s.config.Capability.ForExecution != nil { + authorized, err := s.config.Capability.ForExecution(ctx, project, token) + return authorized, authorized.Snapshot, err + } + snapshot, err := s.config.Capability.Token(ctx, project, token) + return AuthorizedCapability{Snapshot: snapshot}, snapshot, err +} diff --git a/internal/explorer/lifecycle/query.go b/internal/explorer/lifecycle/query.go new file mode 100644 index 00000000..7ab7b8ef --- /dev/null +++ b/internal/explorer/lifecycle/query.go @@ -0,0 +1,136 @@ +package lifecycle + +import ( + "context" + "errors" + "strings" + + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/capability" + "github.com/calypr/loom/internal/projectid" +) + +func (s *Service) List(ctx context.Context, project string) (ListResult, error) { + project = projectid.Canonical(project) + values, err := s.store.List(ctx, project) + if err != nil { + return ListResult{}, internal("list", "EXPLORER_READ_FAILED", err.Error(), err) + } + result := ListResult{Project: project, Summaries: make([]explorer.ExplorerSummaryV1, 0, len(values))} + for _, value := range values { + result.Summaries = append(result.Summaries, explorer.ExplorerSummaryV1{Project: project, ExplorerID: value.ExplorerID, Title: value.Title, Management: value.ManagementMode, ActiveRevisionID: value.ActiveRevisionID, UpdatedAt: value.UpdatedAt}) + } + return result, nil +} + +func (s *Service) Get(ctx context.Context, project, id string) (explorer.ExplorerStateV1, error) { + state, err := s.store.LoadExplorerState(ctx, projectid.Canonical(project), strings.TrimSpace(id)) + if errors.Is(err, explorer.ErrNotFound) { + return explorer.ExplorerStateV1{}, notFound("get", "EXPLORER_NOT_FOUND", "Explorer not found", err) + } + if err != nil { + return explorer.ExplorerStateV1{}, internal("get", "EXPLORER_READ_FAILED", err.Error(), err) + } + return state, nil +} + +func (s *Service) Create(ctx context.Context, request CreateRequest) (explorer.ExplorerSummaryV1, error) { + project := projectid.Canonical(request.Project) + name := strings.TrimSpace(request.Name) + if name == "" { + return explorer.ExplorerSummaryV1{}, malformed("create", "name is required", nil) + } + id := explorer.StableExplorerID(name) + if id == "default" { + return explorer.ExplorerSummaryV1{}, conflict("create", "EXPLORER_EXISTS", "the repository default already exists", nil, nil) + } + title := strings.TrimSpace(request.Title) + if title == "" { + title = name + } + value, err := s.store.CreateInteractiveFrom(ctx, project, id, title, strings.TrimSpace(request.SourceExplorerID), request.Actor) + if errors.Is(err, explorer.ErrDraftConflict) { + return explorer.ExplorerSummaryV1{}, conflict("create", "EXPLORER_EXISTS", "an Explorer with this name already exists", nil, err) + } + if err != nil { + return explorer.ExplorerSummaryV1{}, unprocessable("create", "INVALID_EXPLORER", err.Error(), err) + } + return explorer.ExplorerSummaryV1{Project: project, ExplorerID: value.ExplorerID, Title: value.Title, Management: value.ManagementMode, UpdatedAt: value.UpdatedAt}, nil +} + +func (s *Service) Capability(ctx context.Context, project, explorerID, generation string) (capability.Snapshot, authoringv2.CatalogSnapshot, error) { + if s.config.Capability.Current == nil || s.config.Capability.Catalog == nil { + return capability.Snapshot{}, authoringv2.CatalogSnapshot{}, unavailable("capability", "CAPABILITY_UNAVAILABLE", "Explorer capability compiler is not configured", nil) + } + snapshot, err := s.config.Capability.Current(ctx, project, explorerID, generation) + if err != nil || !snapshot.Usable() { + if err == nil { + err = capability.ErrSnapshotUnavailable + } + return capability.Snapshot{}, authoringv2.CatalogSnapshot{}, unavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error(), err) + } + return snapshot, s.config.Capability.Catalog(snapshot, explorerID), nil +} + +func (s *Service) Suggestions(ctx context.Context, request SuggestionsRequest) (SuggestionsResult, error) { + if strings.TrimSpace(request.SnapshotToken) == "" || strings.TrimSpace(request.NodeID) == "" { + return SuggestionsResult{}, malformed("suggestions", "snapshotToken and nodeId are required", nil) + } + if s.config.Capability.Token == nil || s.config.Capability.Catalog == nil { + return SuggestionsResult{}, unavailable("suggestions", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured", nil) + } + snapshot, err := s.config.Capability.Token(ctx, request.Project, request.SnapshotToken) + if err != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { + return SuggestionsResult{}, conflict("suggestions", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale", nil, err) + } + catalog := s.config.Capability.Catalog(snapshot, request.ExplorerID) + query := strings.ToLower(strings.TrimSpace(request.Query)) + result := SuggestionsResult{SnapshotToken: request.SnapshotToken, NodeID: request.NodeID, Candidates: make([]authoringv2.CatalogCandidate, 0)} + for _, candidate := range catalog.Candidates { + if candidate.NodeID == request.NodeID && (query == "" || strings.Contains(strings.ToLower(candidate.Label), query) || strings.Contains(strings.ToLower(candidate.ID), query)) { + result.Candidates = append(result.Candidates, candidate) + } + } + return result, nil +} + +func (s *Service) Builder(ctx context.Context, request BuilderRequest) (authoringv2.BuilderState, error) { + _, catalog, err := s.Capability(ctx, request.Project, request.ExplorerID, "") + if err != nil { + return authoringv2.BuilderState{}, err + } + owner, err := s.store.Get(ctx, request.Project, request.ExplorerID) + if err != nil { + if errors.Is(err, explorer.ErrNotFound) { + return authoringv2.BuilderState{}, notFound("builder", "EXPLORER_NOT_FOUND", "Explorer not found", err) + } + return authoringv2.BuilderState{}, err + } + state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, LifecycleState: authoringv2.LifecycleNew, DraftVersion: owner.DraftVersion, DraftDigest: owner.DraftDigest, Catalog: catalog} + var activeWorkspace *authoringv2.Workspace + if owner.ActiveRevisionID != "" { + active, activeErr := s.store.ActiveRevision(ctx, request.Project, request.ExplorerID) + if activeErr != nil { + return authoringv2.BuilderState{}, conflict("builder", "AUTHORING_STATE_MISSING", "the active Explorer revision cannot be loaded as V2 authoring state", map[string]any{"revisionId": owner.ActiveRevisionID}, activeErr) + } + workspace, decodeErr := authoringv2.DecodeWorkspace(active.AuthoringBundle) + if decodeErr != nil { + return authoringv2.BuilderState{}, conflict("builder", "AUTHORING_STATE_MISSING", "the active Explorer revision has no valid V2 workspace", map[string]any{"revisionId": active.ID}, decodeErr) + } + activeWorkspace = &workspace + } + if len(owner.DraftConfig) != 0 { + workspace, decodeErr := authoringv2.DecodeWorkspace(owner.DraftConfig) + if decodeErr != nil { + return authoringv2.BuilderState{}, conflict("builder", "DRAFT_STATE_INVALID", "the saved Explorer draft is not a valid V2 workspace", map[string]any{"draftVersion": owner.DraftVersion}, decodeErr) + } + state.Workspace, state.LifecycleState = &workspace, authoringv2.LifecycleReady + } else if activeWorkspace != nil { + state.Workspace, state.LifecycleState = activeWorkspace, authoringv2.LifecycleReady + } + if err := state.Validate(); err != nil { + return authoringv2.BuilderState{}, unavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error(), err) + } + return state, nil +} diff --git a/internal/explorer/lifecycle/repository_publish.go b/internal/explorer/lifecycle/repository_publish.go new file mode 100644 index 00000000..22633f24 --- /dev/null +++ b/internal/explorer/lifecycle/repository_publish.go @@ -0,0 +1,83 @@ +package lifecycle + +import ( + "context" + "errors" + "fmt" + + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/projectid" +) + +func (s *Service) PublishRepository(ctx context.Context, request RepositoryPublishRequest) (RepositoryPublishResult, error) { + if err := request.Workspace.ValidateForPublication(); err != nil { + return RepositoryPublishResult{}, unprocessable("repository_publish", "INVALID_WORKSPACE", err.Error(), err) + } + if s.config.Capability.Current == nil || s.config.Capability.ForCompilation == nil || s.config.CompileReceipt == nil { + return RepositoryPublishResult{}, unavailable("repository_publish", "PUBLICATION_UNAVAILABLE", "repository V2 compilation is not configured", nil) + } + snapshot, err := s.config.Capability.Current(ctx, request.Project, "default", request.Generation) + if err != nil || !snapshot.Usable() || snapshot.Identity.Generation != request.Generation { + return RepositoryPublishResult{}, conflict("repository_publish", "RECEIPT_STALE", fmt.Sprintf("resolve repository V2 capability: %v", err), nil, err) + } + authorized, err := s.config.Capability.ForCompilation(ctx, request.Project, snapshot.Token) + if err != nil { + return RepositoryPublishResult{}, forbidden("repository_publish", fmt.Sprintf("authorize repository V2 compilation: %v", err), err) + } + receipt, err := s.config.CompileReceipt(ctx, CompileReceiptRequest{Project: request.Project, ExplorerID: "default", Workspace: request.Workspace, SnapshotToken: snapshot.Token, Authorized: authorized}) + if err != nil || receipt == nil { + if err == nil { + err = errors.New("compiler returned no receipt") + } + return RepositoryPublishResult{}, unprocessable("repository_publish", "COMPILATION_FAILED", fmt.Sprintf("compile repository V2 workspace: %v", err), err) + } + if receipt.SourceGeneration != request.Generation { + return RepositoryPublishResult{}, conflict("repository_publish", "RECEIPT_STALE", "compiled receipt generation does not match deployment generation", nil, nil) + } + if s.config.Capability.ForExecution != nil { + authorized, err = s.config.Capability.ForExecution(ctx, request.Project, receipt.SnapshotToken) + if err != nil { + return RepositoryPublishResult{}, forbidden("repository_publish", fmt.Sprintf("authorize repository V2 materialization: %v", err), err) + } + } + bindings := recipe.RuntimeBindings{Project: projectid.Legacy(request.Project), DatasetGeneration: request.Generation} + applyAuthorizedScope(&bindings, authorized, true) + var execution Execution + if s.config.MaterializeReceipt == nil { + err = errors.New("repository V2 materialization is not configured") + } else { + execution, err = s.config.MaterializeReceipt(ctx, receipt, bindings) + } + if err != nil { + if resourceErr := materializationResourceError("repository_publish", err); resourceErr != nil { + return RepositoryPublishResult{}, resourceErr + } + return RepositoryPublishResult{}, unprocessable("repository_publish", "MATERIALIZATION_FAILED", fmt.Sprintf("materialize repository V2 workspace: %v", err), err) + } + if err := verifyQueryableOutputs(receipt.Bundle, execution); err != nil { + return RepositoryPublishResult{}, unprocessable("repository_publish", "MATERIALIZATION_FAILED", err.Error(), err) + } + materialized := materializations(receipt.Bundle, execution) + datasetMetadata := datasetMetadataFromExecution(receipt.Bundle, request.Generation, receipt.ResolvedSchemaDigest, execution) + publication := explorer.PublicationMetadata{State: string(explorer.RevisionReady), Generation: request.Generation, ExecutionID: execution.ID, UpdatedAt: s.now()} + owner, revision, err := s.store.UpsertRepositoryV2(ctx, *receipt, request.Commit, request.Actor, materialized, datasetMetadata, publication) + if err != nil { + return RepositoryPublishResult{}, internal("repository_publish", "PERSISTENCE_FAILED", fmt.Sprintf("persist Explorer lifecycle V2: %v", err), err) + } + if owner.ManagementMode != explorer.ManagementRepository { + return RepositoryPublishResult{}, conflict("repository_publish", "INVALID_MANAGEMENT_MODE", "default Explorer has invalid management mode", nil, nil) + } + if s.config.ActivateRelease == nil { + return RepositoryPublishResult{}, unavailable("repository_publish", "PUBLICATION_UNAVAILABLE", "release activation is not configured", nil) + } + if err := s.config.ActivateRelease(ctx, projectid.Legacy(request.Project), request.Generation, selectorsForBundle(receipt.Bundle)); err != nil { + _, _ = s.store.FailRevision(ctx, revision.ID, []explorer.Diagnostic{{Severity: "ERROR", Code: "RELEASE_ACTIVATION_FAILED", Message: err.Error(), Retryable: true}}) + return RepositoryPublishResult{}, conflict("repository_publish", "RELEASE_ACTIVATION_FAILED", fmt.Sprintf("activate published ExplorerConfigV2: %v", err), nil, err) + } + if err := s.store.ActivateRepositoryGeneration(ctx, request.Project, request.Generation, revision.ID); err != nil { + return RepositoryPublishResult{}, conflict("repository_publish", "RELEASE_ACTIVATION_FAILED", fmt.Sprintf("activate ExplorerConfigV2: %v", err), nil, err) + } + publication.State, publication.RevisionID, publication.UpdatedAt = string(explorer.RevisionActive), revision.ID, s.now() + return RepositoryPublishResult{Receipt: receipt, Owner: owner, Revision: revision, Execution: execution, Materializations: materialized, Dataset: datasetMetadata, Publication: publication}, nil +} diff --git a/internal/explorer/lifecycle/service.go b/internal/explorer/lifecycle/service.go new file mode 100644 index 00000000..fe424786 --- /dev/null +++ b/internal/explorer/lifecycle/service.go @@ -0,0 +1,28 @@ +package lifecycle + +import ( + "fmt" + "time" + + "github.com/calypr/loom/internal/explorer" +) + +// Service is the Explorer application layer. It coordinates persistence and +// deployment adapters while leaving all wire representation to transports. +type Service struct { + store *explorer.Service + config Config + now func() time.Time +} + +func New(store *explorer.Service, config Config) (*Service, error) { + if store == nil { + return nil, fmt.Errorf("Explorer lifecycle store is required") + } + now := config.Now + if now == nil { + now = func() time.Time { return time.Now().UTC() } + } + config.Now = now + return &Service{store: store, config: config, now: now}, nil +} diff --git a/internal/explorer/lifecycle/service_test.go b/internal/explorer/lifecycle/service_test.go new file mode 100644 index 00000000..7daad49a --- /dev/null +++ b/internal/explorer/lifecycle/service_test.go @@ -0,0 +1,459 @@ +package lifecycle + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/calypr/loom/internal/authscope" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataset" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/capability" +) + +type fakeStore struct { + explorer.Store + listValues []explorer.Explorer + state explorer.ExplorerStateV1 + created *explorer.Explorer + applyErr error + receipt *explorer.CompilationReceipt + publishErr error + published bool + release dataset.ProjectRelease + revision int64 + order *[]string +} + +func (f *fakeStore) List(context.Context, string) ([]explorer.Explorer, error) { + return append([]explorer.Explorer(nil), f.listValues...), nil +} + +func (f *fakeStore) Create(_ context.Context, value explorer.Explorer) (*explorer.Explorer, error) { + f.created = &value + return &value, nil +} + +func (f *fakeStore) Get(context.Context, string, string) (*explorer.Explorer, error) { + if f.created != nil { + return f.created, nil + } + if f.state.ExplorerID != "" { + return &explorer.Explorer{Project: f.state.Project, ExplorerID: f.state.ExplorerID, Title: f.state.Title, ManagementMode: f.state.Management}, nil + } + return &explorer.Explorer{Project: "project-a", ExplorerID: "patients", Title: "Patients"}, nil +} + +func (f *fakeStore) SaveDraft(_ context.Context, value explorer.Explorer, _ int64, _ ...string) (*explorer.Explorer, error) { + if f.applyErr != nil { + return nil, f.applyErr + } + value.DraftVersion++ + f.created = &value + return &value, nil +} + +func (f *fakeStore) GetCompilationReceiptForExplorer(context.Context, string, string, string) (*explorer.CompilationReceipt, error) { + if f.receipt == nil { + return nil, explorer.ErrNotFound + } + return f.receipt, nil +} + +func (f *fakeStore) GetRevision(context.Context, string) (*explorer.Revision, error) { + return nil, explorer.ErrNotFound +} + +func (f *fakeStore) PublishAuthoring(_ context.Context, _ explorer.CompilationReceipt, revision explorer.Revision, release dataset.ProjectRelease, expectedRevision int64) (*explorer.Revision, error) { + if f.order != nil { + *f.order = append(*f.order, "persist") + } + if f.publishErr != nil { + return nil, f.publishErr + } + f.release = release + f.revision = expectedRevision + f.published = true + return &revision, nil +} + +func (f *fakeStore) FailRevision(context.Context, string, []explorer.Diagnostic) (*explorer.Revision, error) { + return nil, nil +} + +func (f *fakeStore) ActivateRepositoryGeneration(context.Context, string, string, string) error { + if f.order != nil { + *f.order = append(*f.order, "activate-generation") + } + return nil +} + +func newTestService(t *testing.T, store *fakeStore, config Config) *Service { + t.Helper() + domain, err := explorer.NewService(store) + if err != nil { + t.Fatal(err) + } + service, err := New(domain, config) + if err != nil { + t.Fatal(err) + } + return service +} + +func readySnapshot(project, generation, token string, scope authscope.ReadScope) capability.Snapshot { + paths := append([]string(nil), scope.AuthResourcePaths...) + sort.Strings(paths) + sum := sha256.Sum256([]byte(string(scope.Mode) + "\x00" + strings.Join(paths, "\x00"))) + return capability.Snapshot{Identity: capability.SnapshotIdentity{Project: project, Generation: generation, AuthorizationScopeDigest: hex.EncodeToString(sum[:]), SchemaDigest: "schema"}, Status: capability.StatusReady, Complete: true, Token: token} +} + +func testConfig(snapshot capability.Snapshot) Config { + return Config{Capability: CapabilityResolver{ + Token: func(context.Context, string, string) (capability.Snapshot, error) { return snapshot, nil }, + ForExecution: func(context.Context, string, string) (AuthorizedCapability, error) { + return AuthorizedCapability{Snapshot: snapshot, Scope: authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}}, nil + }, + Catalog: func(capability.Snapshot, string) authoringv2.CatalogSnapshot { + return authoringv2.CatalogSnapshot{APIVersion: authoringv2.APIVersion, Kind: authoringv2.CatalogKind, Project: snapshot.Identity.Project, ExplorerID: "patients", SourceGeneration: snapshot.Identity.Generation, AuthorizationScopeDigest: snapshot.Identity.AuthorizationScopeDigest, SnapshotToken: snapshot.Token, Complete: true, RoutePolicy: authoringv2.RoutePolicy{Unbounded: true}, Nodes: []authoringv2.CatalogNode{{ID: "node", ResourceType: "Patient", RowRootEligible: true}}} + }, + }} +} + +func nativeReceipt(snapshot capability.Snapshot) *explorer.CompilationReceipt { + bundle := recipe.Bundle{ + RecipeSchemaVersion: recipe.CurrentSchemaVersion, + Name: "native-test", + TranslationVersion: "test", + Outputs: []recipe.Output{{Name: "patients", RootResourceType: "Patient", RowGrain: "patient"}}, + } + emitted := []explorer.EmittedColumn{{OutputID: "patients", EmissionID: "em_patient", PublicColumn: "patient_id", Label: "Patient ID", LogicalType: "string", Filterable: true, Chartable: true}} + contract := json.RawMessage(`{"outputs":[{"outputId":"patients","columns":[{"column":"patient_id","label":"Patient ID","logicalType":"string","filterable":true,"chartable":true}]}]}`) + bundleDigest, _ := bundle.Digest() + receipt := &explorer.CompilationReceipt{ + ReceiptFormatVersion: explorer.CurrentReceiptFormatVersion, + CompilerContractVersion: explorer.CurrentCompilerContractVersion, + Project: snapshot.Identity.Project, + ExplorerID: "patients", + IntentDigest: "intent", + SnapshotToken: snapshot.Token, + AuthorizationScopeDigest: snapshot.Identity.AuthorizationScopeDigest, + CapabilitySchemaDigest: snapshot.Identity.SchemaDigest, + SourceGeneration: snapshot.Identity.Generation, + RecipeDigest: bundleDigest, + ResolvedRecipeDigest: bundleDigest, + ResolvedSchemaDigest: "resolved-schema", + CompiledConfig: json.RawMessage(`{}`), + PublicOutputContract: contract, + Bundle: bundle, + EmittedColumns: emitted, + OutputColumnProvenance: map[string]map[string]string{"patients": {"patient_id": "EXPLICIT"}}, + } + receipt.OutputContractDigest, _ = explorer.CompilationArtifactDigest(contract) + receipt.CompilationKey, _ = explorer.CompilationKey(*receipt) + receipt.ID, _ = explorer.ReceiptID(*receipt) + return receipt +} + +func TestListGetCreateUseApplicationStore(t *testing.T) { + store := &fakeStore{listValues: []explorer.Explorer{{ExplorerID: "patients", Title: "Patients", ManagementMode: explorer.ManagementInteractive}}} + service := newTestService(t, store, Config{}) + list, err := service.List(context.Background(), "project-a") + if err != nil || len(list.Summaries) != 1 || list.Summaries[0].ExplorerID != "patients" { + t.Fatalf("list = %#v, err=%v", list, err) + } + store.state = explorer.ExplorerStateV1{Project: "project-a", ExplorerID: "patients"} + state, err := service.Get(context.Background(), "project-a", "patients") + if err != nil || state.ExplorerID != "patients" { + t.Fatalf("get = %#v, err=%v", state, err) + } + created, err := service.Create(context.Background(), CreateRequest{Project: "project-a", Name: "Sales", Actor: "alice"}) + if err != nil || created.ExplorerID != "sales" || store.created == nil { + t.Fatalf("create = %#v, err=%v", created, err) + } +} + +func TestApplyCommandsMapsCASFailures(t *testing.T) { + snapshot := readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) + request := authoringv2.ApplyCommandsRequest{CommandID: "command-a", SnapshotToken: "token", Commands: []authoringv2.Command{{Type: authoringv2.CommandCreateTable, Title: "Patients", RootNodeID: "node"}}} + for _, test := range []struct { + name string + err error + code string + }{ + {name: "draft", err: explorer.ErrDraftConflict, code: "DRAFT_CONFLICT"}, + {name: "command", err: explorer.ErrAuthoringCommandConflict, code: "COMMAND_ID_CONFLICT"}, + } { + t.Run(test.name, func(t *testing.T) { + store := &fakeStore{applyErr: test.err} + service := newTestService(t, store, testConfig(snapshot)) + _, err := service.ApplyCommands(context.Background(), "project-a", "patients", request, "alice") + var value *Error + if !errors.As(err, &value) || value.Class != ClassConflict || value.Code != test.code { + t.Fatalf("err=%v, want conflict %s", err, test.code) + } + }) + } +} + +func TestPreviewRejectsStaleGenerationAndScope(t *testing.T) { + receipt := nativeReceipt(readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted})) + for _, test := range []struct { + name string + snapshot capability.Snapshot + wantCode string + }{ + {name: "generation", snapshot: readySnapshot("project-a", "generation-b", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}), wantCode: "RECEIPT_STALE"}, + {name: "token", snapshot: readySnapshot("project-a", "generation-a", "other-token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}), wantCode: "RECEIPT_STALE"}, + } { + t.Run(test.name, func(t *testing.T) { + store := &fakeStore{receipt: receipt} + config := testConfig(test.snapshot) + config.PreviewReceipt = func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { + return dataframeexecution.PreviewSummary{}, nil + } + service := newTestService(t, store, config) + _, err := service.Preview(context.Background(), PreviewRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, OutputID: "patients", Limit: 10}) + var value *Error + if !errors.As(err, &value) || value.Code != test.wantCode { + t.Fatalf("err=%v, want %s", err, test.wantCode) + } + }) + } +} + +func TestValidateReceiptRouteAcceptsEquivalentProjectIdentities(t *testing.T) { + receipt := nativeReceipt(readySnapshot("HTAN_INT/BForePC", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted})) + for _, project := range []string{"HTAN_INT/BForePC", "HTAN_INT%2FBForePC", "HTAN_INT-BForePC"} { + t.Run(project, func(t *testing.T) { + if err := (&Service{}).validateReceiptRoute(receipt, project, "patients"); err != nil { + t.Fatalf("equivalent project identity %q was rejected: %v", project, err) + } + }) + } + + err := (&Service{}).validateReceiptRoute(receipt, "HTAN_INT/OtherProject", "patients") + var value *Error + if !errors.As(err, &value) || value.Code != "COMPILE_RECEIPT_NOT_FOUND" { + t.Fatalf("different project error = %v, want COMPILE_RECEIPT_NOT_FOUND", err) + } +} + +func TestAuthorizedReceiptExecutionPreservesRestrictedEmptyScope(t *testing.T) { + scope := authscope.ReadScope{Mode: authscope.ReadScopeRestricted} + snapshot := readySnapshot("project-a", "generation-a", "token", scope) + receipt := &explorer.CompilationReceipt{Project: "project-a", SnapshotToken: "token", SourceGeneration: "generation-a", CapabilitySchemaDigest: "schema", AuthorizationScopeDigest: snapshot.Identity.AuthorizationScopeDigest} + if err := validateAuthorizedReceiptExecution(receipt, AuthorizedCapability{Snapshot: snapshot, Scope: scope}); err != nil { + t.Fatal(err) + } + if err := validateAuthorizedReceiptExecution(receipt, AuthorizedCapability{Snapshot: snapshot, Scope: authscope.ReadScope{}}); err == nil { + t.Fatal("empty scope mode widened restricted receipt") + } +} + +func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { + snapshot := readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) + receipt := nativeReceipt(snapshot) + order := []string{} + store := &fakeStore{receipt: receipt, order: &order} + config := testConfig(snapshot) + config.ValidateReleaseGeneration = func(context.Context, string, string) error { order = append(order, "validate-generation"); return nil } + config.PrepareRelease = func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { + order = append(order, "prepare-release") + return dataset.ProjectRelease{ID: "release-a", Project: "project-a", Generation: "generation-a", GitCommit: "generation-a"}, 7, nil + } + config.MaterializeReceipt = func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings) (Execution, error) { + order = append(order, "materialize") + return Execution{ID: "execution-a", Outputs: []ExecutionOutput{{Name: "patients", State: "READY"}}}, nil + } + config.PersistPublishedWorkspace = func(_ context.Context, project, explorerID string, workspace []byte) error { + order = append(order, "writeback") + if project != "project-a" || explorerID != "patients" || len(workspace) != 0 { + t.Fatalf("writeback = %s/%s %s", project, explorerID, workspace) + } + return nil + } + service := newTestService(t, store, config) + result, err := service.Publish(context.Background(), PublishRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, Actor: "alice"}) + if err != nil || result.Revision == nil || !store.published { + t.Fatalf("publish = %#v, err=%v", result, err) + } + if got, want := fmt.Sprint(order), "[validate-generation materialize prepare-release persist writeback]"; got != want { + t.Fatalf("ordering=%s, want %s", got, want) + } + if store.release.ID != "release-a" || store.revision != 7 { + t.Fatalf("atomic commit input = release %q at revision %d", store.release.ID, store.revision) + } + + store.publishErr = errors.New("revision write failed") + _, err = service.Publish(context.Background(), PublishRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, Actor: "alice"}) + if err == nil || !strings.Contains(err.Error(), "revision write failed") { + t.Fatalf("publish failure=%v", err) + } + + store.publishErr = nil + store.published = false + config.PersistPublishedWorkspace = func(context.Context, string, string, []byte) error { + return errors.New("disk full") + } + service = newTestService(t, store, config) + _, err = service.Publish(context.Background(), PublishRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, Actor: "alice"}) + var writebackFailure *Error + if !errors.As(err, &writebackFailure) || writebackFailure.Code != "LOCAL_WORKSPACE_WRITEBACK_FAILED" || !store.published { + t.Fatalf("writeback failure published=%v, err=%v", store.published, err) + } + + store.published = false + config.PersistPublishedWorkspace = nil + config.PrepareRelease = func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { + return dataset.ProjectRelease{}, 0, errors.New("release preparation failed") + } + service = newTestService(t, store, config) + _, err = service.Publish(context.Background(), PublishRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, Actor: "alice"}) + if err == nil || store.published { + t.Fatalf("preparation failure published=%v, err=%v", store.published, err) + } +} + +func TestPublishPreservesPublicationInProgress(t *testing.T) { + snapshot := readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) + receipt := nativeReceipt(snapshot) + store := &fakeStore{receipt: receipt} + config := testConfig(snapshot) + config.ValidateReleaseGeneration = func(context.Context, string, string) error { return nil } + config.PrepareRelease = func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { + return dataset.ProjectRelease{}, 0, nil + } + config.MaterializeReceipt = func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings) (Execution, error) { + return Execution{}, dataframeerrors.NewError( + dataframeerrors.CodePublicationInProgress, + "an identical publication is already in progress", + dataframeerrors.WithDetails(map[string]any{"executionId": "execution-a"}), + dataframeerrors.WithRetryable(true), + ) + } + service := newTestService(t, store, config) + + _, err := service.Publish(context.Background(), PublishRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, Actor: "alice"}) + var lifecycleErr *Error + if !errors.As(err, &lifecycleErr) { + t.Fatalf("Publish() error = %v, want lifecycle error", err) + } + if lifecycleErr.Class != ClassConflict || lifecycleErr.Code != "PUBLICATION_IN_PROGRESS" { + t.Fatalf("Publish() error = %#v, want conflict PUBLICATION_IN_PROGRESS", lifecycleErr) + } + if got := lifecycleErr.Details["executionId"]; got != "execution-a" { + t.Fatalf("executionId = %v, want execution-a", got) + } + if got := lifecycleErr.Details["retryable"]; got != true { + t.Fatalf("retryable = %v, want true", got) + } +} + +func TestPublishKeepsMaterializationFailuresUnavailable(t *testing.T) { + snapshot := readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) + receipt := nativeReceipt(snapshot) + config := testConfig(snapshot) + config.ValidateReleaseGeneration = func(context.Context, string, string) error { return nil } + config.PrepareRelease = func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { + return dataset.ProjectRelease{}, 0, nil + } + config.MaterializeReceipt = func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings) (Execution, error) { + return Execution{}, errors.New("ClickHouse insert failed") + } + service := newTestService(t, &fakeStore{receipt: receipt}, config) + + _, err := service.Publish(context.Background(), PublishRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, Actor: "alice"}) + var lifecycleErr *Error + if !errors.As(err, &lifecycleErr) { + t.Fatalf("Publish() error = %v, want lifecycle error", err) + } + if lifecycleErr.Class != ClassUnavailable || lifecycleErr.Code != "MATERIALIZATION_FAILED" { + t.Fatalf("Publish() error = %#v, want unavailable MATERIALIZATION_FAILED", lifecycleErr) + } +} + +func TestPublishReportsQueryMemoryLimit(t *testing.T) { + snapshot := readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) + receipt := nativeReceipt(snapshot) + config := testConfig(snapshot) + config.ValidateReleaseGeneration = func(context.Context, string, string) error { return nil } + config.PrepareRelease = func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { + return dataset.ProjectRelease{}, 0, nil + } + config.MaterializeReceipt = func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings) (Execution, error) { + return Execution{}, dataframeerrors.NewError( + dataframeerrors.CodeQueryMemoryLimitExceeded, + "", + dataframeerrors.WithDetails(map[string]any{"backend": "arangodb"}), + ) + } + service := newTestService(t, &fakeStore{receipt: receipt}, config) + + _, err := service.Publish(context.Background(), PublishRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, Actor: "alice"}) + var lifecycleErr *Error + if !errors.As(err, &lifecycleErr) { + t.Fatalf("Publish() error = %v, want lifecycle error", err) + } + if lifecycleErr.Class != ClassUnavailable || lifecycleErr.Code != "QUERY_MEMORY_LIMIT_EXCEEDED" { + t.Fatalf("Publish() error = %#v, want unavailable QUERY_MEMORY_LIMIT_EXCEEDED", lifecycleErr) + } + if !strings.Contains(lifecycleErr.Message, "ArangoDB query memory limit") || !strings.Contains(lifecycleErr.Message, "active revision was retained") { + t.Fatalf("Publish() message = %q, want cause and retained-revision guidance", lifecycleErr.Message) + } + if got := lifecycleErr.Details["backend"]; got != "arangodb" { + t.Fatalf("backend = %v, want arangodb", got) + } +} + +func TestMaterializationResourceErrorDistinguishesDatabaseOutOfMemory(t *testing.T) { + cause := dataframeerrors.NewError( + dataframeerrors.CodeQueryBackendOutOfMemory, + "", + dataframeerrors.WithDetails(map[string]any{"backend": "arangodb"}), + ) + + err := materializationResourceError("repository_publish", cause) + var lifecycleErr *Error + if !errors.As(err, &lifecycleErr) { + t.Fatalf("materializationMemoryError() = %v, want lifecycle error", err) + } + if lifecycleErr.Class != ClassUnavailable || lifecycleErr.Code != "QUERY_BACKEND_OUT_OF_MEMORY" { + t.Fatalf("error = %#v, want unavailable QUERY_BACKEND_OUT_OF_MEMORY", lifecycleErr) + } + if !strings.Contains(lifecycleErr.Message, "ran out of memory") || strings.Contains(lifecycleErr.Message, "query-memory-limit") { + t.Fatalf("message = %q, want database OOM guidance without query-limit advice", lifecycleErr.Message) + } +} + +func TestMaterializationResourceErrorReportsOtherResourceLimitWithoutMemoryAdvice(t *testing.T) { + cause := dataframeerrors.NewError(dataframeerrors.CodeQueryResourceLimitExceeded, "") + + err := materializationResourceError("materialize", cause) + var lifecycleErr *Error + if !errors.As(err, &lifecycleErr) || lifecycleErr.Code != "QUERY_RESOURCE_LIMIT_EXCEEDED" { + t.Fatalf("materializationResourceError() = %#v, want QUERY_RESOURCE_LIMIT_EXCEEDED", lifecycleErr) + } + if !strings.Contains(lifecycleErr.Message, "configured resource limit") || strings.Contains(lifecycleErr.Message, "query-memory-limit") { + t.Fatalf("message = %q, want generic resource-limit guidance", lifecycleErr.Message) + } +} + +func TestServiceUsesConfiguredClock(t *testing.T) { + clock := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC) + service := newTestService(t, &fakeStore{}, Config{Now: func() time.Time { return clock }}) + if service.now() != clock { + t.Fatalf("clock=%v", service.now()) + } +} diff --git a/internal/explorer/lifecycle/types.go b/internal/explorer/lifecycle/types.go new file mode 100644 index 00000000..49fd1c2b --- /dev/null +++ b/internal/explorer/lifecycle/types.go @@ -0,0 +1,193 @@ +// Package lifecycle contains the transport-neutral Explorer application +// workflows. It owns orchestration and policy while transport packages remain +// responsible for request decoding, authorization context extraction, and +// response encoding. +package lifecycle + +import ( + "context" + "time" + + "github.com/calypr/loom/internal/authscope" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" + "github.com/calypr/loom/internal/dataframe/publication" + "github.com/calypr/loom/internal/dataframe/recipe" + "github.com/calypr/loom/internal/dataset" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/capability" +) + +// CapabilityResolver resolves the current or a retained immutable capability +// snapshot. The callbacks deliberately return domain values and do not expose +// HTTP, Fiber, GraphQL, or generated API types. +type CapabilityResolver struct { + Current func(context.Context, string, string, string) (capability.Snapshot, error) + Token func(context.Context, string, string) (capability.Snapshot, error) + ForCompilation func(context.Context, string, string) (AuthorizedCapability, error) + ForExecution func(context.Context, string, string) (AuthorizedCapability, error) + Catalog func(capability.Snapshot, string) authoringv2.CatalogSnapshot + ValidateReadScope func(authscope.ReadScope, string) error +} + +// AuthorizedCapability is the exact snapshot and authorization scope frozen +// into a compilation receipt or used for receipt-backed execution. +type AuthorizedCapability struct { + Snapshot capability.Snapshot + Scope authscope.ReadScope +} + +func (a AuthorizedCapability) Clone() AuthorizedCapability { + return AuthorizedCapability{Snapshot: a.Snapshot.Clone(), Scope: a.Scope.Clone()} +} + +type CompileReceiptRequest struct { + Project string + ExplorerID string + Workspace authoringv2.Workspace + SnapshotToken string + RequestID string + Authorized AuthorizedCapability +} + +type ReceiptCompiler func(context.Context, CompileReceiptRequest) (*explorer.CompilationReceipt, error) +type ReceiptReader func(context.Context, string, string, string) (*explorer.CompilationReceipt, error) +type ReceiptPreviewer func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (dataframeexecution.PreviewSummary, error) + +// Execution is the small logical publication result needed by Explorer. It +// intentionally avoids the GraphQL resolver's execution type. +type Execution struct { + ID string + Name string + RecipeDigest string + ResolvedSchemaDigest string + SourceGeneration string + State string + Outputs []ExecutionOutput +} + +type ExecutionOutput struct { + Name string + State string + RowCount *int + Columns []publication.PhysicalColumn +} + +type ReceiptMaterializer func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings) (Execution, error) +type GenerationValidator func(context.Context, string, string) error +type ReleaseActivator func(context.Context, string, string, []dataset.DataframeSelector) error +type ReleasePreparer func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) + +// Config contains deployment adapters. Lifecycle policy calls these narrow +// callbacks, but never imports the transport packages that construct them. +type Config struct { + Capability CapabilityResolver + + CompileReceipt ReceiptCompiler + PreviewReceipt ReceiptPreviewer + MaterializeReceipt ReceiptMaterializer + ReceiptLookup ReceiptReader + + ValidateReleaseGeneration GenerationValidator + ActivateRelease ReleaseActivator + PrepareRelease ReleasePreparer + PersistPublishedWorkspace func(context.Context, string, string, []byte) error + Now func() time.Time +} + +type ListResult struct { + Project string + Summaries []explorer.ExplorerSummaryV1 +} + +type CreateRequest struct { + Project string + Name string + Title string + SourceExplorerID string + Actor string +} + +type SuggestionsRequest struct { + Project string + ExplorerID string + SnapshotToken string + NodeID string + Query string +} + +type SuggestionsResult struct { + SnapshotToken string + NodeID string + Candidates []authoringv2.CatalogCandidate +} + +type BuilderRequest struct { + Project string + ExplorerID string +} + +type compileRequest struct { + Project string + ExplorerID string + Workspace authoringv2.Workspace + SnapshotToken string + RequestID string +} + +type ReconcileRequest struct { + Project string + ExplorerID string + SnapshotToken string + DraftVersion int64 + DraftDigest string +} + +type PreviewRequest struct { + Project string + ExplorerID string + ReceiptID string + OutputID string + Limit int + // SinkFactory is supplied by the transport adapter. It receives the + // validated receipt and emitted columns and returns a row sink for native + // streaming previews. Lifecycle never sees the response encoder itself. + SinkFactory func(*explorer.CompilationReceipt, []explorer.EmittedColumn) (func(map[string]any) error, error) +} + +type PreviewResult struct { + Receipt *explorer.CompilationReceipt + Columns []explorer.EmittedColumn + Summary dataframeexecution.PreviewSummary +} + +type PublishRequest struct { + Project string + ExplorerID string + ReceiptID string + Actor string +} + +type PublishResult struct { + Receipt *explorer.CompilationReceipt + Revision *explorer.Revision + Execution Execution +} + +type RepositoryPublishRequest struct { + Project string + Generation string + Workspace authoringv2.Workspace + Commit string + Actor string +} + +type RepositoryPublishResult struct { + Receipt *explorer.CompilationReceipt + Owner *explorer.Explorer + Revision *explorer.Revision + Execution Execution + Materializations []explorer.Materialization + Dataset explorer.DatasetMetadata + Publication explorer.PublicationMetadata +} diff --git a/internal/explorer/output_contract_test.go b/internal/explorer/output_contract_test.go index 8fa54072..5bea4919 100644 --- a/internal/explorer/output_contract_test.go +++ b/internal/explorer/output_contract_test.go @@ -111,6 +111,7 @@ func TestCompilationReceiptValidateAcceptsCurrentMultiOutputContract(t *testing. } r := testReceipt() r.Bundle = bundle + r.OutputColumnProvenance = map[string]map[string]string{"patients": {"patient_id": "EXPLICIT"}, "specimens": {"specimen_id": "EXPLICIT"}} r.EmittedColumns = emitted r.PublicOutputContract = raw r.ResolvedRecipeDigest, err = bundle.Digest() diff --git a/internal/explorer/service.go b/internal/explorer/service.go index 34a4f040..818fdb19 100644 --- a/internal/explorer/service.go +++ b/internal/explorer/service.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/projectid" ) @@ -31,13 +32,6 @@ func NewService(store Store) (*Service, error) { func (s *Service) List(ctx context.Context, project string) ([]Explorer, error) { return s.store.List(ctx, projectid.Legacy(project)) } -func (s *Service) RepositoryConfig(ctx context.Context, project string) (*RepositoryConfig, error) { - return s.store.GetRepositoryConfig(ctx, projectid.Legacy(project)) -} -func (s *Service) SaveRepositoryConfig(ctx context.Context, value RepositoryConfig) (*RepositoryConfig, error) { - value.Project = projectid.Legacy(value.Project) - return s.store.SaveRepositoryConfig(ctx, value) -} func (s *Service) Get(ctx context.Context, project, id string) (*Explorer, error) { return s.store.Get(ctx, projectid.Legacy(project), id) } @@ -51,25 +45,6 @@ func (s *Service) LoadExplorerState(ctx context.Context, project, id string) (Ex project = projectid.Canonical(project) id = strings.TrimSpace(id) owner, err := s.Get(ctx, project, id) - if errors.Is(err, ErrNotFound) && id == "default" { - // Repository bootstrap can briefly expose its repository record before - // the Explorer identity is created. Preserve the read contract for that - // window without manufacturing editable authoring state. - repository, repositoryErr := s.RepositoryConfig(ctx, project) - if repositoryErr != nil { - return ExplorerStateV1{}, err - } - owner = &Explorer{ - Project: projectid.Legacy(project), - ExplorerID: "default", - Title: configV2Title(repository.Config), - ManagementMode: ManagementRepository, - ActiveRevisionID: repository.ActiveRevisionID, - Publication: repository.Publication, - UpdatedAt: repository.UpdatedAt, - } - err = nil - } if err != nil { return ExplorerStateV1{}, err } @@ -82,7 +57,7 @@ func (s *Service) LoadExplorerState(ctx context.Context, project, id string) (Ex markNotPublished(&state) return state, nil } - revision, err := s.Revision(ctx, owner.ActiveRevisionID) + revision, err := s.store.GetRevision(ctx, owner.ActiveRevisionID) if err != nil { return ExplorerStateV1{}, fmt.Errorf("load active Explorer revision %q: %w", owner.ActiveRevisionID, err) } @@ -94,12 +69,14 @@ func (s *Service) LoadExplorerState(ctx context.Context, project, id string) (Ex state.Generated.ResolvedSchemaDigest = revision.ResolvedSchemaDigest state.Generated.SourceGeneration = revision.SourceGeneration state.Generated.EmittedColumns = append([]EmittedColumn(nil), revision.EmittedColumns...) - state.Generated.Materializations, state.Generated.Dataset = WithDataframeSelectors(revision.Recipe, revision.Materializations, revision.Dataset) + state.Generated.Materializations = append([]Materialization(nil), revision.Materializations...) + state.Generated.Dataset = revision.Dataset + state.Generated.Dataset.Outputs = append([]DatasetOutput(nil), revision.Dataset.Outputs...) state.Generated.Publication = revision.Publication state.Generated.Publication.State = firstNonEmptyString(revision.Publication.State, string(revision.Status), ExplorerRuntimeV1NotPublished) state.Generated.Publication.RevisionID = revision.ID state.Generated.Diagnostics = append([]Diagnostic(nil), revision.Diagnostics...) - state.Runtime = s.BuildViewerProjection(revision) + state.Runtime = BuildViewerProjection(revision) if state.Runtime == nil || len(state.Runtime.Outputs) == 0 { state.Generated.Publication.State = ExplorerRuntimeV1NotPublished if state.Runtime != nil { @@ -110,14 +87,6 @@ func (s *Service) LoadExplorerState(ctx context.Context, project, id string) (Ex return state, nil } -// BuildViewerProjection derives the renderer-facing state from one immutable -// revision. It is deliberately tolerant of revisions that contain an -// executable recipe but no authored presentation: the default table is -// generated from the query and publication metadata. -func (s *Service) BuildViewerProjection(revision *Revision) *ExplorerRuntimeV1 { - return buildViewerProjection(revision) -} - func newExplorerStateResponse(owner *Explorer) ExplorerStateV1 { project := projectid.Canonical(owner.Project) state := ExplorerStateV1{ @@ -132,16 +101,7 @@ func newExplorerStateResponse(owner *Explorer) ExplorerStateV1 { Version: owner.DraftVersion, Digest: owner.DraftDigest, }, - Generated: ExplorerStateV1Generated{ - RecipeDigest: owner.RecipeDigest, - ResolvedSchemaDigest: owner.ResolvedSchemaDigest, - SourceGeneration: owner.SourceGeneration, - EmittedColumns: append([]EmittedColumn(nil), owner.EmittedColumns...), - Materializations: append([]Materialization(nil), owner.Materializations...), - Dataset: owner.Dataset, - Publication: owner.Publication, - Diagnostics: append([]Diagnostic(nil), owner.Diagnostics...), - }, + Generated: ExplorerStateV1Generated{}, ActiveURL: explorerURL(project, owner.ExplorerID), UpdatedBy: owner.UpdatedBy, UpdatedAt: owner.UpdatedAt, @@ -154,26 +114,18 @@ func markNotPublished(state *ExplorerStateV1) { state.Runtime = nil } -func configV2Title(raw json.RawMessage) string { - var config ConfigV2 - if json.Unmarshal(raw, &config) != nil || strings.TrimSpace(config.Explorer.Title) == "" { - return "Default" - } - return config.Explorer.Title -} - func explorerURL(project, id string) string { return "/api/v1/projects/" + url.PathEscape(project) + "/explorers/" + url.PathEscape(id) } -// CreateEmptyInteractive creates only the Explorer identity. Unpublished +// createEmptyInteractive creates only the Explorer identity. Unpublished // Explorers have no persisted authoring content and hydrate as an empty valid // Builder model. -func (s *Service) CreateEmptyInteractive(ctx context.Context, project, id, title, actor string) (*Explorer, error) { +func (s *Service) createEmptyInteractive(ctx context.Context, project, id, title, actor string) (*Explorer, error) { if id == "" || id == "default" { return nil, fmt.Errorf("invalid interactive Explorer identity") } - return s.store.CreateInteractive(ctx, Explorer{Project: projectid.Legacy(project), ExplorerID: id, Title: title, ManagementMode: ManagementInteractive, UpdatedBy: actor, UpdatedAt: s.now()}) + return s.store.Create(ctx, Explorer{Project: projectid.Legacy(project), ExplorerID: id, Title: title, ManagementMode: ManagementInteractive, UpdatedBy: actor, UpdatedAt: s.now()}) } // CreateInteractiveFrom creates a new Explorer identity and, when requested, @@ -181,7 +133,7 @@ func (s *Service) CreateEmptyInteractive(ctx context.Context, project, id, title // reconstruct or re-identify that workspace. func (s *Service) CreateInteractiveFrom(ctx context.Context, project, id, title, sourceExplorerID, actor string) (*Explorer, error) { if strings.TrimSpace(sourceExplorerID) == "" { - return s.CreateEmptyInteractive(ctx, project, id, title, actor) + return s.createEmptyInteractive(ctx, project, id, title, actor) } if id == "" || id == "default" { return nil, fmt.Errorf("invalid interactive Explorer identity") @@ -199,7 +151,7 @@ func (s *Service) CreateInteractiveFrom(ctx context.Context, project, id, title, raw = revision.AuthoringBundle } if len(raw) == 0 { - return s.CreateEmptyInteractive(ctx, project, id, title, actor) + return s.createEmptyInteractive(ctx, project, id, title, actor) } workspace, err := authoringv2.DecodeWorkspace(raw) if err != nil { @@ -214,32 +166,7 @@ func (s *Service) CreateInteractiveFrom(ctx context.Context, project, id, title, if err != nil { return nil, err } - return s.store.CreateInteractive(ctx, Explorer{Project: projectid.Legacy(project), ExplorerID: id, Title: title, ManagementMode: ManagementInteractive, DraftConfig: canonical, DraftVersion: 1, DraftDigest: digest, UpdatedBy: actor, UpdatedAt: s.now()}) -} - -// SaveWorkspaceDraft persists canonical V2 authoring intent with optimistic -// concurrency. It never compiles, materializes, or changes the active pointer. -func (s *Service) SaveWorkspaceDraft(ctx context.Context, project, id string, workspace authoringv2.Workspace, expectedVersion int64, expectedDigest, actor string) (*Explorer, error) { - canonical, err := workspace.CanonicalJSON() - if err != nil { - return nil, err - } - digest, err := workspace.Digest() - if err != nil { - return nil, err - } - owner, err := s.Get(ctx, project, id) - if err != nil { - return nil, err - } - owner.DraftConfig = canonical - owner.DraftDigest = digest - owner.UpdatedBy = actor - owner.UpdatedAt = s.now() - if len(workspace.Tabs) > 0 && strings.TrimSpace(workspace.Tabs[0].Title) != "" { - owner.Title = workspace.Tabs[0].Title - } - return s.store.SaveDraft(ctx, *owner, expectedVersion, expectedDigest) + return s.store.Create(ctx, Explorer{Project: projectid.Legacy(project), ExplorerID: id, Title: title, ManagementMode: ManagementInteractive, DraftConfig: canonical, DraftVersion: 1, DraftDigest: digest, UpdatedBy: actor, UpdatedAt: s.now()}) } // ApplyWorkspaceCommands is the authoritative Builder mutation boundary. It @@ -322,9 +249,6 @@ func (s *Service) ApplyWorkspaceCommands(ctx context.Context, project, id string } return &authoringv2.ApplyCommandsResponse{CommandID: request.CommandID, Workspace: workspace, DraftVersion: stored.DraftVersion, DraftDigest: stored.DraftDigest, Results: results, Diagnostics: []any{}}, nil } -func (s *Service) Revision(ctx context.Context, id string) (*Revision, error) { - return s.store.GetRevision(ctx, id) -} func (s *Service) ActiveRevision(ctx context.Context, project, id string) (*Revision, error) { e, err := s.store.Get(ctx, projectid.Legacy(project), id) if err != nil { @@ -336,7 +260,7 @@ func (s *Service) ActiveRevision(ctx context.Context, project, id string) (*Revi return s.store.GetRevision(ctx, e.ActiveRevisionID) } func (s *Service) FailRevision(ctx context.Context, id string, diagnostics []Diagnostic) (*Revision, error) { - return s.store.TransitionRevision(ctx, id, RevisionFailed, diagnostics) + return s.store.FailRevision(ctx, id, diagnostics) } // CompilationReceiptForExplorer performs the tenant-scoped lookup used by @@ -350,15 +274,15 @@ func (s *Service) StoreCompilationReceipt(ctx context.Context, receipt Compilati return s.store.InsertCompilationReceipt(ctx, receipt) } -// PublishAuthoring atomically stores the server receipt and immutable revision -// and switches the Explorer's active pointer. -func (s *Service) PublishAuthoring(ctx context.Context, receipt CompilationReceipt, revision Revision) (*Revision, error) { +// PublishAuthoring atomically stores the receipt and immutable revision, then +// switches both the dataset release and Explorer active pointers. +func (s *Service) PublishAuthoring(ctx context.Context, receipt CompilationReceipt, revision Revision, release dataset.ProjectRelease, expectedReleaseRevision int64) (*Revision, error) { if receipt.ID == "" || revision.ID == "" || revision.CompilationReceiptID != receipt.ID { return nil, fmt.Errorf("authoring publication identity is incomplete") } receipt.Project = projectid.Canonical(receipt.Project) revision.Project = projectid.Legacy(revision.Project) - return s.store.PublishAuthoring(ctx, receipt, revision) + return s.store.PublishAuthoring(ctx, receipt, revision, release, expectedReleaseRevision) } // UpsertRepositoryV2 records the repository-owned default and its immutable @@ -389,9 +313,9 @@ func (s *Service) UpsertRepositoryV2(ctx context.Context, receipt CompilationRec } owner, err := s.store.Get(ctx, storageProject, "default") if errors.Is(err, ErrNotFound) { - owner, err = s.store.CreateRepository(ctx, Explorer{Project: storageProject, ExplorerID: "default", Title: title, ManagementMode: ManagementRepository, DraftConfig: workspace, DraftVersion: 1, DraftDigest: receipt.IntentDigest, SourceGeneration: receipt.SourceGeneration, Dataset: dataset, Publication: publication, UpdatedBy: actor, UpdatedAt: s.now()}) + owner, err = s.store.Create(ctx, Explorer{Project: storageProject, ExplorerID: "default", Title: title, ManagementMode: ManagementRepository, DraftConfig: workspace, DraftVersion: 1, DraftDigest: receipt.IntentDigest, UpdatedBy: actor, UpdatedAt: s.now()}) } else if err == nil { - owner.Title, owner.DraftConfig, owner.DraftDigest, owner.SourceGeneration, owner.Dataset, owner.Publication, owner.UpdatedBy = title, workspace, receipt.IntentDigest, receipt.SourceGeneration, dataset, publication, actor + owner.Title, owner.DraftConfig, owner.DraftDigest, owner.UpdatedBy = title, workspace, receipt.IntentDigest, actor owner, err = s.store.SaveDraft(ctx, *owner, owner.DraftVersion) } if err != nil { @@ -399,7 +323,7 @@ func (s *Service) UpsertRepositoryV2(ctx context.Context, receipt CompilationRec } revisionID := RepositoryRevisionID(project, sourceCommit, receipt.IntentDigest, receipt.SourceGeneration, receipt.ID) now := s.now() - revision, err := s.store.InsertRevision(ctx, Revision{ID: revisionID, Project: storageProject, ExplorerID: "default", Config: compiledConfig, ConfigDigest: receipt.IntentDigest, AuthoringBundle: workspace, IntentDigest: receipt.IntentDigest, CompilationReceiptID: receipt.ID, PublicOutputContract: append(json.RawMessage(nil), receipt.PublicOutputContract...), Recipe: receipt.Bundle, RecipeDigest: receipt.RecipeDigest, ResolvedSchemaDigest: receipt.ResolvedSchemaDigest, SourceGeneration: receipt.SourceGeneration, SourceCommit: sourceCommit, Dataset: dataset, Publication: publication, Materializations: append([]Materialization(nil), materializations...), EmittedColumns: append([]EmittedColumn(nil), receipt.EmittedColumns...), Status: RevisionReady, CreatedBy: actor, CreatedAt: now, ReadyAt: &now}) + revision, err := s.store.InsertRevision(ctx, Revision{ID: revisionID, Project: storageProject, ExplorerID: "default", Config: compiledConfig, AuthoringBundle: workspace, IntentDigest: receipt.IntentDigest, CompilationReceiptID: receipt.ID, PublicOutputContract: append(json.RawMessage(nil), receipt.PublicOutputContract...), Recipe: receipt.Bundle, RecipeDigest: receipt.RecipeDigest, ResolvedSchemaDigest: receipt.ResolvedSchemaDigest, SourceGeneration: receipt.SourceGeneration, Dataset: dataset, Publication: publication, Materializations: append([]Materialization(nil), materializations...), EmittedColumns: append([]EmittedColumn(nil), receipt.EmittedColumns...), Status: RevisionReady, CreatedBy: actor, CreatedAt: now, ReadyAt: &now}) if err != nil { return nil, nil, err } diff --git a/internal/explorer/store.go b/internal/explorer/store.go index 220f42a0..b0217df0 100644 --- a/internal/explorer/store.go +++ b/internal/explorer/store.go @@ -3,6 +3,8 @@ package explorer import ( "context" "errors" + + "github.com/calypr/loom/internal/dataset" ) var ( @@ -18,24 +20,21 @@ var ( ErrCorruptReceipt = errors.New("corrupt compilation receipt") ) -// Store deliberately exposes only lifecycle mutations for revisions. An +// Store deliberately exposes only lifecycle mutations for revisions. An // adapter must never replace definition, recipe, digests, source generation, // or frozen materialization mappings after InsertRevision succeeds. type Store interface { - GetRepositoryConfig(context.Context, string) (*RepositoryConfig, error) - SaveRepositoryConfig(context.Context, RepositoryConfig) (*RepositoryConfig, error) List(context.Context, string) ([]Explorer, error) Get(context.Context, string, string) (*Explorer, error) - CreateInteractive(context.Context, Explorer) (*Explorer, error) - CreateRepository(context.Context, Explorer) (*Explorer, error) + Create(context.Context, Explorer) (*Explorer, error) // SaveDraft is an optimistic compare-and-swap. Adapters must reject a // mismatched expected version/digest and increment the persisted version. SaveDraft(context.Context, Explorer, int64, ...string) (*Explorer, error) InsertCompilationReceipt(context.Context, CompilationReceipt) (*CompilationReceipt, error) GetCompilationReceiptForExplorer(context.Context, string, string, string) (*CompilationReceipt, error) - PublishAuthoring(context.Context, CompilationReceipt, Revision) (*Revision, error) + PublishAuthoring(context.Context, CompilationReceipt, Revision, dataset.ProjectRelease, int64) (*Revision, error) InsertRevision(context.Context, Revision) (*Revision, error) GetRevision(context.Context, string) (*Revision, error) - TransitionRevision(context.Context, string, RevisionStatus, []Diagnostic) (*Revision, error) + FailRevision(context.Context, string, []Diagnostic) (*Revision, error) ActivateRepositoryGeneration(context.Context, string, string, string) error } diff --git a/internal/explorer/types.go b/internal/explorer/types.go index 0e797321..443a7b9f 100644 --- a/internal/explorer/types.go +++ b/internal/explorer/types.go @@ -9,32 +9,6 @@ import ( "github.com/calypr/loom/internal/dataset" ) -// RepositoryConfig is the V2 repository-owned Explorer contract frozen at -// successful deployment. It is intentionally separate from the retired V1 -// authoring aggregate: the repository packet carries the executable baseline; -// live schema/readiness and publication metadata are stored alongside it. -type RepositoryConfig struct { - Project string `json:"project"` - ExplorerID string `json:"explorerId"` - Management ManagementMode `json:"management"` - Config json.RawMessage `json:"config"` - Workspace json.RawMessage `json:"workspace"` - ConfigDigest string `json:"configDigest,omitempty"` - IntentDigest string `json:"intentDigest,omitempty"` - CompilationReceiptID string `json:"compilationReceiptId,omitempty"` - PublicOutputContract json.RawMessage `json:"publicOutputContract,omitempty"` - SourceGeneration string `json:"sourceGeneration"` - SourceCommit string `json:"sourceCommit"` - ExecutionID string `json:"executionId"` - ActiveRevisionID string `json:"activeRevisionId,omitempty"` - DraftVersion int64 `json:"-"` - Materializations []Materialization `json:"materializations"` - Dataset DatasetMetadata `json:"dataset"` - Publication PublicationMetadata `json:"publication"` - Diagnostics []Diagnostic `json:"diagnostics,omitempty"` - UpdatedAt time.Time `json:"updatedAt"` -} - type ManagementMode string const ( @@ -45,12 +19,10 @@ const ( type RevisionStatus string const ( - RevisionPending RevisionStatus = "PENDING" - RevisionMaterializing RevisionStatus = "MATERIALIZING" - RevisionReady RevisionStatus = "READY" - RevisionActive RevisionStatus = "ACTIVE" - RevisionSuperseded RevisionStatus = "SUPERSEDED" - RevisionFailed RevisionStatus = "FAILED" + RevisionReady RevisionStatus = "READY" + RevisionActive RevisionStatus = "ACTIVE" + RevisionSuperseded RevisionStatus = "SUPERSEDED" + RevisionFailed RevisionStatus = "FAILED" ) type Diagnostic struct { @@ -98,24 +70,15 @@ type Explorer struct { ManagementMode ManagementMode `json:"managementMode"` // DraftConfig is the lossless, canonical ExplorerConfigV2 packet used by // the Builder. - DraftConfig json.RawMessage `json:"draftConfig,omitempty"` - DraftVersion int64 `json:"draftVersion"` - DraftDigest string `json:"draftDigest,omitempty"` - LastAuthoringCommandID string `json:"lastAuthoringCommandId,omitempty"` - LastAuthoringCommandDigest string `json:"lastAuthoringCommandDigest,omitempty"` - LastAuthoringCommandResults json.RawMessage `json:"lastAuthoringCommandResults,omitempty"` - ActiveRevisionID string `json:"activeRevisionId,omitempty"` - ActiveConfig json.RawMessage `json:"activeConfig,omitempty"` - RecipeDigest string `json:"recipeDigest,omitempty"` - ResolvedSchemaDigest string `json:"resolvedSchemaDigest,omitempty"` - SourceGeneration string `json:"sourceGeneration,omitempty"` - EmittedColumns []EmittedColumn `json:"emittedColumns,omitempty"` - Materializations []Materialization `json:"materializations,omitempty"` - Dataset DatasetMetadata `json:"dataset,omitempty"` - Publication PublicationMetadata `json:"publication,omitempty"` - Diagnostics []Diagnostic `json:"diagnostics,omitempty"` - UpdatedBy string `json:"updatedBy,omitempty"` - UpdatedAt time.Time `json:"updatedAt"` + DraftConfig json.RawMessage `json:"draftConfig,omitempty"` + DraftVersion int64 `json:"draftVersion"` + DraftDigest string `json:"draftDigest,omitempty"` + LastAuthoringCommandID string `json:"lastAuthoringCommandId,omitempty"` + LastAuthoringCommandDigest string `json:"lastAuthoringCommandDigest,omitempty"` + LastAuthoringCommandResults json.RawMessage `json:"lastAuthoringCommandResults,omitempty"` + ActiveRevisionID string `json:"activeRevisionId,omitempty"` + UpdatedBy string `json:"updatedBy,omitempty"` + UpdatedAt time.Time `json:"updatedAt"` } type Materialization struct { OutputID string `json:"outputId"` @@ -126,22 +89,12 @@ type Materialization struct { Columns []publication.PhysicalColumn `json:"columns"` } -// WithDataframeSelectors clones canonical publication metadata while -// preserving the selectors that identify the active dataset release. Missing -// selectors are invalid and are not synthesized. -func WithDataframeSelectors(_ recipe.Bundle, materializations []Materialization, metadata DatasetMetadata) ([]Materialization, DatasetMetadata) { - materializations = append([]Materialization(nil), materializations...) - metadata.Outputs = append([]DatasetOutput(nil), metadata.Outputs...) - return materializations, metadata -} - type Revision struct { ID string `json:"id"` Project string `json:"project"` ExplorerID string `json:"explorerId"` // Config is the exact immutable ExplorerConfigV2 packet for this revision. Config json.RawMessage `json:"config,omitempty"` - ConfigDigest string `json:"configDigest,omitempty"` AuthoringBundle json.RawMessage `json:"authoringBundle,omitempty"` IntentDigest string `json:"intentDigest,omitempty"` CompilationReceiptID string `json:"compilationReceiptId,omitempty"` @@ -150,41 +103,15 @@ type Revision struct { RecipeDigest string `json:"recipeDigest"` ResolvedSchemaDigest string `json:"resolvedSchemaDigest"` SourceGeneration string `json:"sourceGeneration"` - SourceCommit string `json:"sourceCommit,omitempty"` Materializations []Materialization `json:"materializations"` EmittedColumns []EmittedColumn `json:"emittedColumns"` Dataset DatasetMetadata `json:"dataset,omitempty"` Publication PublicationMetadata `json:"publication,omitempty"` Diagnostics []Diagnostic `json:"diagnostics"` - // Migration records the lossless source packet used to create this - // revision. The authoring bundle is the executable Builder intent, while - // these raw packets provide an auditable rollback path for migrations from - // frontend/repository configuration formats (including fields Loom does not - // yet interpret). - Migration *MigrationMetadata `json:"migration,omitempty"` - Status RevisionStatus `json:"status"` - CreatedBy string `json:"createdBy,omitempty"` - CreatedAt time.Time `json:"createdAt"` - ReadyAt *time.Time `json:"readyAt,omitempty"` - ActivatedAt *time.Time `json:"activatedAt,omitempty"` - FailedAt *time.Time `json:"failedAt,omitempty"` -} - -// MigrationMetadata is intentionally attached to the immutable revision. A -// migration must never discard the source document merely because its known -// fields were translated into authoring-v1. Raw JSON is retained so unknown -// or frontend-specific fields remain available for rollback and future -// translators without leaking them into the executable recipe contract. -type MigrationMetadata struct { - Kind string `json:"kind"` - Source string `json:"source"` - SourceProject string `json:"sourceProject"` - SourceExplorerID string `json:"sourceExplorerId"` - OriginalConfig json.RawMessage `json:"originalConfig,omitempty"` - OriginalMapping json.RawMessage `json:"originalMapping,omitempty"` - OriginalConfigDigest string `json:"originalConfigDigest,omitempty"` - OriginalMappingDigest string `json:"originalMappingDigest,omitempty"` - Actor string `json:"actor,omitempty"` - RequestID string `json:"requestId,omitempty"` - MigratedAt time.Time `json:"migratedAt"` + Status RevisionStatus `json:"status"` + CreatedBy string `json:"createdBy,omitempty"` + CreatedAt time.Time `json:"createdAt"` + ReadyAt *time.Time `json:"readyAt,omitempty"` + ActivatedAt *time.Time `json:"activatedAt,omitempty"` + FailedAt *time.Time `json:"failedAt,omitempty"` } diff --git a/internal/explorer/viewer_projection.go b/internal/explorer/viewer_projection.go index 036a5ca7..3a7c21aa 100644 --- a/internal/explorer/viewer_projection.go +++ b/internal/explorer/viewer_projection.go @@ -14,7 +14,9 @@ import ( // publication state to the renderer-facing contract. It intentionally does // not require an authored ConfigView: a valid recipe is enough to produce a // default table that a client can extend with presentation choices. -func buildViewerProjection(revision *Revision) *ExplorerRuntimeV1 { +// BuildViewerProjection is the pure immutable-revision to Viewer wire +// projection. It performs no persistence lookup and has no service state. +func BuildViewerProjection(revision *Revision) *ExplorerRuntimeV1 { if revision == nil { return nil } diff --git a/internal/fhir/schema/compiler_semantics.go b/internal/fhir/schema/compiler_semantics.go index d1239b6b..92fef1d5 100644 --- a/internal/fhir/schema/compiler_semantics.go +++ b/internal/fhir/schema/compiler_semantics.go @@ -99,9 +99,6 @@ func HasResource(resourceType string) bool { return ok } -// ResourceExists is the explicit predicate form of HasResource. -func ResourceExists(resourceType string) bool { return HasResource(resourceType) } - // DefinitionExists reports whether name is represented by the generated FHIR // schema. Unlike HasResource, it also accepts backbone and choice definitions // that are valid selector sources but are not graph collections. This is a diff --git a/internal/ingest/partition.go b/internal/ingest/partition.go deleted file mode 100644 index db14ada1..00000000 --- a/internal/ingest/partition.go +++ /dev/null @@ -1,77 +0,0 @@ -package ingest - -import ( - "bufio" - "bytes" - "fmt" - "io" - "os" - "path/filepath" - "regexp" - - "github.com/bytedance/sonic" -) - -var resourceTypeName = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) - -// PartitionNDJSON streams mixed FHIR resources into the per-resource files -// consumed by Loom's existing loader. It only reads resourceType here; full -// decoding and validation remain owned by the loader. -func PartitionNDJSON(reader io.Reader, dir string) (int, error) { - if reader == nil { - return 0, fmt.Errorf("NDJSON reader is required") - } - if err := os.MkdirAll(dir, 0o700); err != nil { - return 0, err - } - - files := map[string]*os.File{} - defer func() { - for _, file := range files { - _ = file.Close() - } - }() - - scanner := bufio.NewScanner(reader) - scanner.Buffer(make([]byte, 1024*1024), 64*1024*1024) - rows := 0 - for scanner.Scan() { - line := scanner.Bytes() - if len(bytes.TrimSpace(line)) == 0 { - continue - } - node, err := sonic.Get(line, "resourceType") - if err != nil { - return rows, fmt.Errorf("row %d: invalid JSON: %w", rows+1, err) - } - resourceType, err := node.String() - if err != nil || !resourceTypeName.MatchString(resourceType) { - return rows, fmt.Errorf("row %d: missing or invalid resourceType", rows+1) - } - file := files[resourceType] - if file == nil { - file, err = os.Create(filepath.Join(dir, resourceType+".ndjson")) - if err != nil { - return rows, err - } - files[resourceType] = file - } - if _, err := file.Write(append(line, '\n')); err != nil { - return rows, err - } - rows++ - } - if err := scanner.Err(); err != nil { - return rows, err - } - if rows == 0 { - return 0, fmt.Errorf("NDJSON body contains no resources") - } - for resourceType, file := range files { - if err := file.Close(); err != nil { - return rows, fmt.Errorf("close %s stream: %w", resourceType, err) - } - delete(files, resourceType) - } - return rows, nil -} diff --git a/internal/ingest/partition_test.go b/internal/ingest/partition_test.go deleted file mode 100644 index 875b23bd..00000000 --- a/internal/ingest/partition_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package ingest - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestPartitionNDJSONGroupsMixedFHIRResources(t *testing.T) { - dir := t.TempDir() - rows, err := PartitionNDJSON(strings.NewReader( - `{"resourceType":"Patient","id":"p1"}`+"\n"+ - `{"resourceType":"Specimen","id":"s1"}`+"\n"+ - `{"resourceType":"Patient","id":"p2"}`+"\n", - ), dir) - if err != nil { - t.Fatal(err) - } - if rows != 3 { - t.Fatalf("rows = %d, want 3", rows) - } - patient, err := os.ReadFile(filepath.Join(dir, "Patient.ndjson")) - if err != nil { - t.Fatal(err) - } - if strings.Count(string(patient), "\n") != 2 { - t.Fatalf("Patient rows = %q", patient) - } -} - -func TestPartitionNDJSONRejectsMissingResourceType(t *testing.T) { - _, err := PartitionNDJSON(strings.NewReader(`{"id":"p1"}`+"\n"), t.TempDir()) - if err == nil { - t.Fatal("expected missing resourceType error") - } -} diff --git a/internal/ingest/single_resource_load.go b/internal/ingest/single_resource_load.go deleted file mode 100644 index 0e613c5f..00000000 --- a/internal/ingest/single_resource_load.go +++ /dev/null @@ -1,58 +0,0 @@ -package ingest - -import ( - "context" - "io" - "os" - "path/filepath" - "strings" -) - -// LoadSingleResourceReader stages one resource stream into the normal resource -// loader so callers get the same validation, writes, and catalog updates as a -// directory load without exposing temporary-file details. -func LoadSingleResourceReader(ctx context.Context, opts LoadOptions, resourceType string, reader io.Reader, compressed bool) (LoadSummary, error) { - if opts.Dataset != nil { - return LoadSummary{}, ErrGenerationSingleResourceUnsupported - } - dir, err := os.MkdirTemp("", "arango-fhir-single-resource-") - if err != nil { - return LoadSummary{}, err - } - defer os.RemoveAll(dir) - - name := resourceType + ".ndjson" - if compressed { - name += ".gz" - } - target := filepath.Join(dir, name) - f, err := os.Create(target) - if err != nil { - return LoadSummary{}, err - } - if _, err := io.Copy(f, reader); err != nil { - _ = f.Close() - return LoadSummary{}, err - } - if err := f.Close(); err != nil { - return LoadSummary{}, err - } - - singleOpts := opts - singleOpts.MetaDir = dir - return Load(ctx, singleOpts) -} - -// LoadSingleResourceFile loads one resource file through the primary resource -// loader. Gzip input remains supported for callers that already have it. -func LoadSingleResourceFile(ctx context.Context, opts LoadOptions, resourceType, path string) (LoadSummary, error) { - if opts.Dataset != nil { - return LoadSummary{}, ErrGenerationSingleResourceUnsupported - } - file, err := os.Open(path) - if err != nil { - return LoadSummary{}, err - } - defer file.Close() - return LoadSingleResourceReader(ctx, opts, resourceType, file, strings.HasSuffix(path, ".gz")) -} diff --git a/internal/repositoryseed/preflight.go b/internal/repositoryseed/preflight.go new file mode 100644 index 00000000..ff398793 --- /dev/null +++ b/internal/repositoryseed/preflight.go @@ -0,0 +1,281 @@ +// Package repositoryseed validates a repository checkout before the local +// launcher is allowed to mutate Docker or database state. +package repositoryseed + +import ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/projectid" +) + +type Options struct { + Repository string + Config string + Project string +} + +type File struct { + Path string `json:"path"` + ResourceType string `json:"resourceType,omitempty"` + Resources int `json:"resources"` + Bytes int64 `json:"bytes"` + SHA256 string `json:"sha256"` +} + +type Manifest struct { + Repository string `json:"repository"` + MetaDir string `json:"metaDir"` + ConfigPath string `json:"configPath"` + ConfigDir string `json:"configDir"` + ConfigName string `json:"configName"` + Project string `json:"project"` + Generation string `json:"generation"` + MetaDigest string `json:"metaDigest"` + ConfigDigest string `json:"configDigest"` + SourceCommit string `json:"sourceCommit"` + Files []File `json:"files"` + Workspace []byte `json:"-"` +} + +type UnmaterializedError struct{ Paths []string } + +func (e *UnmaterializedError) Error() string { + var b strings.Builder + b.WriteString("repository data is not materialized; Loom did not start Docker\npointer files:\n") + for _, path := range e.Paths { + fmt.Fprintf(&b, " %s\n", path) + } + b.WriteString("materialize these files using this repository's normal Git LFS/DRS workflow, then rerun Loom; the Loom launcher does not download repository objects") + return b.String() +} + +func Prepare(options Options) (Manifest, error) { + root, err := filepath.Abs(first(options.Repository, ".")) + if err != nil { + return Manifest{}, fmt.Errorf("resolve repository: %w", err) + } + info, err := os.Stat(root) + if err != nil || !info.IsDir() { + return Manifest{}, fmt.Errorf("repository %q is not a directory", root) + } + metaDir := filepath.Join(root, "META") + metaPaths, err := filepath.Glob(filepath.Join(metaDir, "*.ndjson")) + if err != nil { + return Manifest{}, fmt.Errorf("discover META: %w", err) + } + sort.Strings(metaPaths) + if len(metaPaths) == 0 { + return Manifest{}, fmt.Errorf("repository %q contains no META/*.ndjson files", root) + } + configPath, err := resolveConfig(root, options.Config) + if err != nil { + return Manifest{}, err + } + selected := append(append([]string(nil), metaPaths...), configPath) + pointers := make([]string, 0) + for _, path := range selected { + if err := requireContainedRegularFile(root, path); err != nil { + return Manifest{}, err + } + pointer, readErr := isPointer(path) + if readErr != nil { + return Manifest{}, readErr + } + if pointer { + relative, _ := filepath.Rel(root, path) + pointers = append(pointers, filepath.ToSlash(relative)) + } + } + if len(pointers) != 0 { + return Manifest{}, &UnmaterializedError{Paths: pointers} + } + + metaHash := sha256.New() + files := make([]File, 0, len(metaPaths)) + totalResources := 0 + for _, path := range metaPaths { + file, validateErr := validateNDJSON(root, path) + if validateErr != nil { + return Manifest{}, validateErr + } + files = append(files, file) + totalResources += file.Resources + _, _ = io.WriteString(metaHash, file.Path+"\x00"+file.SHA256+"\x00") + } + if totalResources == 0 { + return Manifest{}, errors.New("META contains no FHIR resources") + } + workspace, err := os.ReadFile(configPath) + if err != nil { + return Manifest{}, fmt.Errorf("read CONFIG: %w", err) + } + decodedWorkspace, err := authoringv2.DecodeWorkspace(workspace) + if err != nil { + return Manifest{}, fmt.Errorf("CONFIG %s is not a valid native Loom V2 workspace: %w", filepath.Base(configPath), err) + } + if err := decodedWorkspace.ValidateForPublication(); err != nil { + return Manifest{}, fmt.Errorf("CONFIG %s cannot be published by Loom: %w", filepath.Base(configPath), err) + } + canonicalWorkspace, err := decodedWorkspace.CanonicalJSON() + if err != nil { + return Manifest{}, fmt.Errorf("canonicalize CONFIG %s: %w", filepath.Base(configPath), err) + } + configDigest, err := decodedWorkspace.Digest() + if err != nil { + return Manifest{}, fmt.Errorf("digest CONFIG %s: %w", filepath.Base(configPath), err) + } + metaDigest := hex.EncodeToString(metaHash.Sum(nil)) + project := projectid.Canonical(options.Project) + if project == "" { + project = projectFromFilename(configPath) + } + if project == "" { + return Manifest{}, errors.New("project could not be inferred from CONFIG filename; pass --project PROGRAM/PROJECT") + } + return Manifest{ + Repository: root, MetaDir: metaDir, ConfigPath: configPath, + ConfigDir: filepath.Dir(configPath), ConfigName: filepath.Base(configPath), + Project: project, Generation: "repo-" + metaDigest[:24], MetaDigest: metaDigest, + ConfigDigest: configDigest, SourceCommit: "workspace-" + strings.TrimPrefix(configDigest, "sha256:")[:24], + Files: files, Workspace: canonicalWorkspace, + }, nil +} + +func requireContainedRegularFile(root, path string) error { + resolvedRoot, err := filepath.EvalSymlinks(root) + if err != nil { + return fmt.Errorf("resolve repository %q: %w", root, err) + } + resolvedPath, err := filepath.EvalSymlinks(path) + if err != nil { + return fmt.Errorf("resolve repository input %q: %w", path, err) + } + relative, err := filepath.Rel(resolvedRoot, resolvedPath) + if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(os.PathSeparator)) { + return fmt.Errorf("repository input %q resolves outside the repository", path) + } + info, err := os.Stat(resolvedPath) + if err != nil || !info.Mode().IsRegular() { + return fmt.Errorf("repository input %q is not a regular file", path) + } + return nil +} + +func resolveConfig(root, configured string) (string, error) { + if strings.TrimSpace(configured) != "" { + path := configured + if !filepath.IsAbs(path) { + path = filepath.Join(root, path) + } + path = filepath.Clean(path) + configRoot := filepath.Join(root, "CONFIG") + string(os.PathSeparator) + if !strings.HasPrefix(path+string(os.PathSeparator), configRoot) { + return "", errors.New("--config must resolve inside the repository CONFIG directory") + } + if info, err := os.Stat(path); err != nil || !info.Mode().IsRegular() { + return "", fmt.Errorf("CONFIG file %q is not a regular file", path) + } + return path, nil + } + paths, err := filepath.Glob(filepath.Join(root, "CONFIG", "*.json")) + if err != nil { + return "", err + } + sort.Strings(paths) + if len(paths) == 0 { + return "", errors.New("repository contains no CONFIG/*.json file") + } + if len(paths) != 1 { + return "", fmt.Errorf("repository contains %d CONFIG/*.json files; select one with --config", len(paths)) + } + return paths[0], nil +} + +func isPointer(path string) (bool, error) { + file, err := os.Open(path) + if err != nil { + return false, fmt.Errorf("read %s: %w", path, err) + } + defer file.Close() + buffer := make([]byte, 512) + n, err := file.Read(buffer) + if err != nil && !errors.Is(err, io.EOF) { + return false, fmt.Errorf("read %s: %w", path, err) + } + value := string(buffer[:n]) + return strings.HasPrefix(value, "version https://git-lfs.github.com/spec/v1\n") && strings.Contains(value, "\noid sha256:"), nil +} + +func validateNDJSON(root, path string) (File, error) { + input, err := os.Open(path) + if err != nil { + return File{}, err + } + defer input.Close() + hash := sha256.New() + scanner := bufio.NewScanner(io.TeeReader(input, hash)) + scanner.Buffer(make([]byte, 64*1024), 64<<20) + resourceType := "" + rows := 0 + for scanner.Scan() { + line := bytes.TrimSpace(scanner.Bytes()) + if len(line) == 0 { + continue + } + var resource struct { + ResourceType string `json:"resourceType"` + ID string `json:"id"` + } + if err := json.Unmarshal(line, &resource); err != nil { + return File{}, fmt.Errorf("invalid NDJSON %s line %d: %w", filepath.Base(path), rows+1, err) + } + if strings.TrimSpace(resource.ResourceType) == "" || strings.TrimSpace(resource.ID) == "" { + return File{}, fmt.Errorf("invalid FHIR resource in %s line %d: resourceType and id are required", filepath.Base(path), rows+1) + } + if resourceType == "" { + resourceType = resource.ResourceType + } else if resource.ResourceType != resourceType { + return File{}, fmt.Errorf("META file %s mixes resource types %s and %s", filepath.Base(path), resourceType, resource.ResourceType) + } + rows++ + } + if err := scanner.Err(); err != nil { + return File{}, fmt.Errorf("read NDJSON %s: %w", filepath.Base(path), err) + } + if resourceType == "" { + resourceType = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + } + info, err := os.Stat(path) + if err != nil { + return File{}, err + } + relative, _ := filepath.Rel(root, path) + return File{Path: filepath.ToSlash(relative), ResourceType: resourceType, Resources: rows, Bytes: info.Size(), SHA256: hex.EncodeToString(hash.Sum(nil))}, nil +} + +func projectFromFilename(path string) string { + name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + name = strings.TrimSuffix(name, ".workspace") + return projectid.Canonical(name) +} + +func first(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + return "" +} diff --git a/internal/repositoryseed/preflight_test.go b/internal/repositoryseed/preflight_test.go new file mode 100644 index 00000000..1cb69446 --- /dev/null +++ b/internal/repositoryseed/preflight_test.go @@ -0,0 +1,105 @@ +package repositoryseed + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +const workspace = `{"apiVersion":"loom.calypr.org/explorer-authoring/v2","kind":"ExplorerBuilderWorkspace","explorer":{"title":"Patients"},"documents":[{"kind":"ExplorerBuilderDocument","output":{"id":"patients","title":"Patients"},"rootResourceType":"Patient","route":{"occurrenceId":"base","resourceType":"Patient"},"columns":[{"column":"patient_id","label":"Patient ID","logicalType":"string","occurrenceId":"base","source":{"kind":"field","fieldPath":"id","projectionMode":"VALUE"},"table":{"visible":true,"order":0}}]}],"tabs":[{"id":"patients","title":"Patients","outputId":"patients","order":0,"visible":true}]}` + +func TestPrepareReportsEveryPointerBeforeParsing(t *testing.T) { + root := t.TempDir() + mustWrite(t, root, "META/Patient.ndjson", pointer("a")) + mustWrite(t, root, "META/Specimen.ndjson", pointer("b")) + mustWrite(t, root, "CONFIG/HTAN_INT-BForePC.json", pointer("c")) + _, err := Prepare(Options{Repository: root}) + var unresolved *UnmaterializedError + if !errors.As(err, &unresolved) { + t.Fatalf("Prepare error = %v, want UnmaterializedError", err) + } + want := []string{"META/Patient.ndjson", "META/Specimen.ndjson", "CONFIG/HTAN_INT-BForePC.json"} + if !reflect.DeepEqual(unresolved.Paths, want) { + t.Fatalf("pointer paths = %#v, want %#v", unresolved.Paths, want) + } + if !strings.Contains(err.Error(), "does not download repository objects") { + t.Fatalf("error omitted materialization instruction: %v", err) + } +} + +func TestPrepareValidatesAndSeparatesDataIdentity(t *testing.T) { + root := t.TempDir() + mustWrite(t, root, "META/Patient.ndjson", `{"resourceType":"Patient","id":"p1"}`+"\n") + mustWrite(t, root, "CONFIG/HTAN_INT-BForePC.json", workspace) + first, err := Prepare(Options{Repository: root}) + if err != nil { + t.Fatal(err) + } + if first.Project != "HTAN_INT/BForePC" { + t.Fatalf("project = %q", first.Project) + } + if !strings.HasPrefix(first.Generation, "repo-") || len(first.Generation) != len("repo-")+24 { + t.Fatalf("generation = %q", first.Generation) + } + mustWrite(t, root, "CONFIG/HTAN_INT-BForePC.json", " \n"+workspace+"\n") + formatted, err := Prepare(Options{Repository: root}) + if err != nil { + t.Fatal(err) + } + if formatted.ConfigDigest != first.ConfigDigest || formatted.SourceCommit != first.SourceCommit { + t.Fatalf("formatting changed workspace identity: %#v != %#v", formatted, first) + } + mustWrite(t, root, "CONFIG/HTAN_INT-BForePC.json", strings.Replace(workspace, "Patients", "People", 1)) + second, err := Prepare(Options{Repository: root}) + if err != nil { + t.Fatal(err) + } + if second.Generation != first.Generation { + t.Fatalf("CONFIG change changed data generation: %s != %s", second.Generation, first.Generation) + } + if second.ConfigDigest == first.ConfigDigest { + t.Fatal("CONFIG change did not change config digest") + } +} + +func TestPrepareRejectsLegacyAndMalformedInputs(t *testing.T) { + root := t.TempDir() + mustWrite(t, root, "META/Patient.ndjson", `{"resourceType":"Patient"}`+"\n") + mustWrite(t, root, "CONFIG/HTAN_INT-BForePC.json", workspace) + if _, err := Prepare(Options{Repository: root}); err == nil || !strings.Contains(err.Error(), "resourceType and id") { + t.Fatalf("malformed META error = %v", err) + } + mustWrite(t, root, "META/Patient.ndjson", `{"resourceType":"Patient","id":"p1"}`+"\n") + mustWrite(t, root, "CONFIG/HTAN_INT-BForePC.json", `{"explorerConfig":[]}`) + if _, err := Prepare(Options{Repository: root}); err == nil || !strings.Contains(err.Error(), "not a valid native Loom V2 workspace") { + t.Fatalf("legacy CONFIG error = %v", err) + } +} + +func TestPrepareRequiresConfigSelectionWhenAmbiguous(t *testing.T) { + root := t.TempDir() + mustWrite(t, root, "META/Patient.ndjson", `{"resourceType":"Patient","id":"p1"}`+"\n") + mustWrite(t, root, "CONFIG/a.json", workspace) + mustWrite(t, root, "CONFIG/b.json", workspace) + if _, err := Prepare(Options{Repository: root}); err == nil || !strings.Contains(err.Error(), "select one with --config") { + t.Fatalf("ambiguous CONFIG error = %v", err) + } +} + +func mustWrite(t *testing.T, root, name, value string) { + t.Helper() + path := filepath.Join(root, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(value), 0o644); err != nil { + t.Fatal(err) + } +} + +func pointer(id string) string { + return "version https://git-lfs.github.com/spec/v1\noid sha256:" + strings.Repeat(id, 64) + "\nsize 123\n" +} diff --git a/internal/server/config_test.go b/internal/server/config_test.go index ab4dce57..7d125b37 100644 --- a/internal/server/config_test.go +++ b/internal/server/config_test.go @@ -10,15 +10,29 @@ import ( ) func TestParseServerOptionsWithoutConfigUsesFlags(t *testing.T) { - options, err := parseServerOptions([]string{"--listen", ":19091", "--no-auth", "--dataframer-recipe", "recipe.json"}, flag.ContinueOnError) + options, err := parseServerOptions([]string{"--listen", ":19091", "--no-auth", "--dataframer-recipe", "recipe.json", "--recipe-query-page-rows", "7"}, flag.ContinueOnError) if err != nil { t.Fatalf("parseServerOptions() error = %v", err) } - if options.Server.Listen != ":19091" || !options.Server.AllowUnauthenticated || !options.Auth.AllowUnauthenticated { + if options.Server.Listen != ":19091" || !options.Server.AllowUnauthenticated || !options.Auth.AllowUnauthenticated || options.Server.RecipeQueryPageRows != 7 { t.Fatalf("resolved options = %#v", options) } } +func TestLocalWorkspaceWritebackRequiresNoAuth(t *testing.T) { + path := filepath.Join(t.TempDir(), "workspace.json") + if _, err := parseServerOptions([]string{"--dataframer-recipe", "recipe.json", "--local-workspace-writeback", path, "--local-workspace-project", "project-a"}, flag.ContinueOnError); err == nil || !strings.Contains(err.Error(), "requires unauthenticated") { + t.Fatalf("authenticated writeback error = %v", err) + } + options, err := parseServerOptions([]string{"--no-auth", "--dataframer-recipe", "recipe.json", "--local-workspace-writeback", path, "--local-workspace-project", "project-a"}, flag.ContinueOnError) + if err != nil { + t.Fatal(err) + } + if options.Server.LocalWorkspaceWriteback != path { + t.Fatalf("writeback path = %q", options.Server.LocalWorkspaceWriteback) + } +} + func TestParseServerOptionsConfigRetainsSettingsButNoAuthOverridesAuth(t *testing.T) { path := filepath.Join(t.TempDir(), "config.yaml") contents := "server:\n listen: :19092\n allow_unauthenticated: false\n clickhouse:\n enabled: false\nauth:\n mode: basic\n basic:\n username: loom\n password: secret\n" @@ -44,6 +58,23 @@ func TestConfigDefaultsToBasicAndRequiresCredentials(t *testing.T) { } } +func TestRecipeQueryPageRowsCanBeConfiguredOrDisabled(t *testing.T) { + cfg := DefaultConfig() + if cfg.Server.RecipeQueryPageRows != 25 { + t.Fatalf("default recipe query page rows = %d, want 25", cfg.Server.RecipeQueryPageRows) + } + cfg.Server.RecipeQueryPageRows = 0 + cfg.Auth.AllowUnauthenticated = true + cfg.Server.ClickHouse.Enabled = false + if err := cfg.Validate(); err != nil { + t.Fatalf("disabled recipe query paging: %v", err) + } + cfg.Server.RecipeQueryPageRows = -1 + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "cannot be negative") { + t.Fatalf("negative recipe query page rows error = %v", err) + } +} + func TestLoadConfigStrictlyDecodesAndAppliesAuthSettings(t *testing.T) { path := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(path, []byte("server:\n listen: :18080\n dataframer:\n recipe: /etc/loom/dataframer.json\nauth:\n mode: calypr\n calypr:\n request_timeout: 7s\n cache_ttl: 45s\n"), 0o600); err != nil { @@ -121,17 +152,13 @@ func TestLoadConfigRejectsRemovedPublicationBackends(t *testing.T) { } } -func TestRequiredDataframeSelectorsAndSnapshotRetentionFromEnvironment(t *testing.T) { +func TestRequiredDataframeSelectorsFromEnvironment(t *testing.T) { t.Setenv("LOOM_REQUIRED_DATAFRAME_SELECTORS", `[{"recipe":"core","translationVersion":"v1","output":"Patient"}]`) - t.Setenv("LOOM_SNAPSHOT_RETENTION", "48h") - t.Setenv("LOOM_SNAPSHOT_DIRECTORY", t.TempDir()) - t.Setenv("LOOM_PUBLICATION_WORKER_LEASE", "90s") - t.Setenv("LOOM_PUBLICATION_MAX_ATTEMPTS", "5") cfg, err := LoadConfig("") if err != nil { t.Fatal(err) } - if len(cfg.Server.RequiredDataframeSelectors) != 1 || cfg.Server.RequiredDataframeSelectors[0].Output != "Patient" || cfg.Server.SnapshotRetention != 48*time.Hour || cfg.Server.PublicationWorkerLease != 90*time.Second || cfg.Server.PublicationMaxAttempts != 5 { + if len(cfg.Server.RequiredDataframeSelectors) != 1 || cfg.Server.RequiredDataframeSelectors[0].Output != "Patient" { t.Fatalf("environment config = %#v", cfg.Server) } if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "dataframer.recipe") { diff --git a/internal/server/dataframe_query_error_test.go b/internal/server/dataframe_query_error_test.go new file mode 100644 index 00000000..bb4ec71f --- /dev/null +++ b/internal/server/dataframe_query_error_test.go @@ -0,0 +1,70 @@ +package server + +import ( + "errors" + "testing" + + "github.com/arangodb/go-driver/v2/arangodb/shared" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" +) + +func TestClassifyDataframeQueryErrorPreservesArangoMemoryLimit(t *testing.T) { + driverErr := shared.ArangoError{ + HasError: true, + Code: 500, + ErrorNum: shared.ErrResourceLimit, + ErrorMessage: "AQL: query would use more memory than allowed", + } + + err := classifyDataframeQueryError(driverErr) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok { + t.Fatalf("classifyDataframeQueryError() = %v, want dataframe user error", err) + } + if userErr.Code() != string(dataframeerrors.CodeQueryMemoryLimitExceeded) { + t.Fatalf("code = %q, want %q", userErr.Code(), dataframeerrors.CodeQueryMemoryLimitExceeded) + } + if got := userErr.Details()["backend"]; got != "arangodb" { + t.Fatalf("backend = %v, want arangodb", got) + } + if !errors.Is(err, driverErr) { + t.Fatal("classified error did not preserve the driver cause") + } +} + +func TestClassifyDataframeQueryErrorPreservesArangoOutOfMemory(t *testing.T) { + driverErr := shared.ArangoError{ + HasError: true, + Code: 500, + ErrorNum: shared.ErrOutOfMemory, + ErrorMessage: "out of memory", + } + + err := classifyDataframeQueryError(driverErr) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodeQueryBackendOutOfMemory) { + t.Fatalf("classifyDataframeQueryError() = %#v, want %s", userErr, dataframeerrors.CodeQueryBackendOutOfMemory) + } +} + +func TestClassifyDataframeQueryErrorPreservesOtherArangoResourceLimit(t *testing.T) { + driverErr := shared.ArangoError{ + HasError: true, + Code: 500, + ErrorNum: shared.ErrResourceLimit, + ErrorMessage: "resource limit exceeded", + } + + err := classifyDataframeQueryError(driverErr) + userErr, ok := dataframeerrors.AsUserError(err) + if !ok || userErr.Code() != string(dataframeerrors.CodeQueryResourceLimitExceeded) { + t.Fatalf("classifyDataframeQueryError() = %#v, want %s", userErr, dataframeerrors.CodeQueryResourceLimitExceeded) + } +} + +func TestClassifyDataframeQueryErrorLeavesOtherFailuresUntouched(t *testing.T) { + want := errors.New("visitor failed") + if got := classifyDataframeQueryError(want); !errors.Is(got, want) || got != want { + t.Fatalf("classifyDataframeQueryError() = %v, want original error", got) + } +} diff --git a/internal/server/explain_wiring.go b/internal/server/explain_wiring.go index 59c5f59e..0c7a4926 100644 --- a/internal/server/explain_wiring.go +++ b/internal/server/explain_wiring.go @@ -3,15 +3,14 @@ package server import ( "context" - "github.com/calypr/loom/internal/dataframe/recipe/engine" - "github.com/calypr/loom/internal/dataframe/runtime" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" arangostore "github.com/calypr/loom/internal/store/arango" ) -func explainCompiledQuery(ctx context.Context, client *arangostore.Client, compiled runtime.CompiledQuery) (engine.ExplainAssessment, error) { +func explainCompiledQuery(ctx context.Context, client *arangostore.Client, compiled dataframeexecution.CompiledQuery) (dataframeexecution.ExplainAssessment, error) { result, err := client.Explain(ctx, arangostore.ExplainRequest{Query: compiled.Query, BindVars: compiled.BindVars}) if err != nil { - return engine.ExplainAssessment{}, err + return dataframeexecution.ExplainAssessment{}, err } - return engine.AssessmentFromArango(arangostore.AssessExplainResult(result)), nil + return dataframeexecution.AssessmentFromArango(arangostore.AssessExplainResult(result)), nil } diff --git a/internal/server/explorer_authoring_error_logging_test.go b/internal/server/explorer_authoring_error_logging_test.go deleted file mode 100644 index 6654a0fe..00000000 --- a/internal/server/explorer_authoring_error_logging_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package server - -import ( - "bytes" - "encoding/json" - "fmt" - "log/slog" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gofiber/fiber/v3" -) - -func TestAuthoringHTTPErrorLogsGenericCauseWithoutExposingIt(t *testing.T) { - var logs bytes.Buffer - previousLogger := slog.Default() - slog.SetDefault(slog.New(slog.NewJSONHandler(&logs, nil))) - t.Cleanup(func() { slog.SetDefault(previousLogger) }) - - app := fiber.New() - app.Get("/api/v1/projects/:project/explorers/:explorerId", func(c fiber.Ctx) error { - c.Locals("request_id", "builder-log-test") - cause := fmt.Errorf("compile receipt: %w", fmt.Errorf("semantic lowering exploded")) - return authoringHTTPError(c, cause) - }) - - response, err := app.Test(httptest.NewRequest(http.MethodGet, "/api/v1/projects/HTAN_INT%252FBForePC/explorers/default", nil)) - if err != nil { - t.Fatal(err) - } - defer response.Body.Close() - if response.StatusCode != http.StatusInternalServerError { - t.Fatalf("status = %d, want %d", response.StatusCode, http.StatusInternalServerError) - } - var body map[string]any - if err := json.NewDecoder(response.Body).Decode(&body); err != nil { - t.Fatal(err) - } - if body["code"] != "INTERNAL_ERROR" || body["message"] != "internal server error" { - t.Fatalf("response = %#v", body) - } - if encoded, err := json.Marshal(body); err != nil { - t.Fatal(err) - } else if strings.Contains(string(encoded), "semantic lowering exploded") { - t.Fatalf("response exposed internal cause: %s", encoded) - } - - var entry map[string]any - if err := json.Unmarshal(logs.Bytes(), &entry); err != nil { - t.Fatalf("decode log entry: %v\n%s", err, logs.String()) - } - for key, want := range map[string]any{ - "msg": "Explorer authoring request failed", - "request_id": "builder-log-test", - "method": http.MethodGet, - "path": "/api/v1/projects/HTAN_INT%252FBForePC/explorers/default", - "status": float64(http.StatusInternalServerError), - "project": "HTAN_INT/BForePC", - "explorer_id": "default", - "stage": "internal", - "code": "INTERNAL_ERROR", - "cause": "compile receipt: semantic lowering exploded", - "cause_type": "*fmt.wrapError", - } { - if got := entry[key]; got != want { - t.Errorf("log[%q] = %#v, want %#v; log=%s", key, got, want, logs.String()) - } - } -} diff --git a/internal/server/explorer_authoring_http.go b/internal/server/explorer_authoring_http.go index 3bb6fca0..0f2e97bc 100644 --- a/internal/server/explorer_authoring_http.go +++ b/internal/server/explorer_authoring_http.go @@ -1,65 +1,11 @@ package server -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "log/slog" - "strings" - - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/explorer" - "github.com/gofiber/fiber/v3" -) - -func decodeAuthoringStrict(raw []byte, value any) error { - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(value); err != nil { - return err - } - var trailing any - if err := decoder.Decode(&trailing); err != io.EOF { - if err == nil { - return fmt.Errorf("multiple JSON values") - } - return err - } - return nil -} - -func authoringRead(c fiber.Ctx, authorizeRead explorerConfigReadAuthorizer) error { - if err := authorizeRead(c.Context(), principalFromFiber(c), explorerProjectParam(c)); err != nil { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 403, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err}) - } - return nil -} - -func authoringWrite(c fiber.Ctx, authorizer authscope.Authorizer) error { - project := explorerProjectParam(c) - path := authscope.NormalizeAuthResourcePath(strings.TrimSpace(c.Query("auth_resource_path"))) - if err := authorizer.AuthorizeWrite(c.Context(), principalFromFiber(c), project, path); err != nil { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 403, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err}) - } - return nil -} - -func requestIDFromFiber(c fiber.Ctx) string { - value, _ := c.Locals("request_id").(string) - return value -} +import "github.com/calypr/loom/internal/explorer" func malformedRouteError(stage string, err error) error { return &explorer.AuthoringError{Status: 400, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: stage, Code: "MALFORMED_AUTHORING_REQUEST", JSONPath: "$", Message: err.Error()}, Cause: err} } -func authoringSemanticRoute(stage, path, code, message string, details map[string]any) error { - return &explorer.AuthoringError{Status: 422, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: stage, Code: code, JSONPath: path, Message: message, Details: details}} -} - func explorerConflict(stage, code, message string, details map[string]any) error { return &explorer.AuthoringError{Status: 409, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: stage, Code: code, Message: message, Details: details}} } @@ -67,102 +13,3 @@ func explorerConflict(stage, code, message string, details map[string]any) error func explorerUnavailable(stage, code, message string) error { return &explorer.AuthoringError{Status: 503, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: stage, Code: code, Message: message}} } - -func receiptHasOutput(bundle recipe.Bundle, id string) bool { - for _, output := range bundle.Outputs { - if output.Name == id { - return true - } - } - return false -} - -func authoringHTTPError(c fiber.Ctx, err error) error { - requestID := requestIDFromFiber(c) - var value *explorer.AuthoringError - if errors.As(err, &value) { - value.Diagnostic.RequestID = requestID - cause := value.Cause - if cause == nil { - cause = err - } - logAuthoringRequestFailure(c, requestID, value.Status, value.Diagnostic.Stage, value.Diagnostic.Code, value.Diagnostic.JSONPath, value.Diagnostic.Message, value.Diagnostic.Details, cause) - details := value.Diagnostic.Details - if details == nil { - details = map[string]any{} - } - diagnostic := fiber.Map{"severity": strings.ToLower(value.Diagnostic.Severity), "stage": value.Diagnostic.Stage, "code": value.Diagnostic.Code, "path": value.Diagnostic.JSONPath, "message": value.Diagnostic.Message, "requestId": requestID} - return c.Status(value.Status).JSON(fiber.Map{"code": value.Diagnostic.Code, "message": value.Diagnostic.Message, "diagnostics": []fiber.Map{diagnostic}, "requestId": requestID, "details": details}) - } - if errors.Is(err, explorer.ErrNotFound) { - return c.Status(404).JSON(fiber.Map{"code": "NOT_FOUND", "message": "Explorer resource not found", "diagnostics": []any{}, "requestId": requestID, "details": fiber.Map{}}) - } - logAuthoringRequestFailure(c, requestID, 500, "internal", "INTERNAL_ERROR", "", "internal server error", nil, err) - return c.Status(500).JSON(fiber.Map{"code": "INTERNAL_ERROR", "message": "internal server error", "diagnostics": []any{}, "requestId": requestID, "details": fiber.Map{}}) -} - -func logAuthoringRequestFailure(c fiber.Ctx, requestID string, status int, stage, code, jsonPath, message string, details map[string]any, cause error) { - attrs := []any{ - "request_id", requestID, - "method", c.Method(), - "path", c.Path(), - "status", status, - "project", explorerProjectParam(c), - "explorer_id", strings.TrimSpace(c.Params("explorerId")), - "stage", stage, - "code", code, - "json_path", jsonPath, - "message", message, - "details", details, - } - if cause != nil { - attrs = append(attrs, "cause", cause, "cause_type", fmt.Sprintf("%T", cause)) - } - slog.Error("Explorer authoring request failed", attrs...) -} - -func decodeStrict(raw []byte, value any) error { - decoder := json.NewDecoder(bytes.NewReader(raw)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(value); err != nil { - return err - } - var trailing any - if err := decoder.Decode(&trailing); err == nil { - return fmt.Errorf("multiple JSON values") - } - return nil -} - -func principalFromFiber(c fiber.Ctx) *authscope.Principal { - principal, _ := c.Locals("principal").(*authscope.Principal) - return principal -} - -func subjectFromFiber(c fiber.Ctx) string { - if principal := principalFromFiber(c); principal != nil { - return principal.Subject - } - return "" -} - -func authorizeExplorerWrite(c fiber.Ctx, authorizer authscope.Authorizer, project string) error { - path := authscope.NormalizeAuthResourcePath(strings.TrimSpace(c.Query("auth_resource_path"))) - if err := authorizer.AuthorizeWrite(c.Context(), principalFromFiber(c), project, path); err != nil { - return explorerV2Error(c, 403, "FORBIDDEN", "forbidden") - } - return nil -} - -func explorerV2Error(c fiber.Ctx, status int, code, message string) error { - return c.Status(status).JSON(fiber.Map{"error": fiber.Map{"code": code, "message": message}}) -} - -func firstNonEmpty(values ...string) string { - for _, value := range values { - if strings.TrimSpace(value) != "" { - return value - } - } - return "Explorer" -} diff --git a/internal/server/explorer_authoring_v2_routes.go b/internal/server/explorer_authoring_v2_routes.go deleted file mode 100644 index f74a354d..00000000 --- a/internal/server/explorer_authoring_v2_routes.go +++ /dev/null @@ -1,692 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "strings" - "time" - - explorerv2api "github.com/calypr/loom/generated/loomapi" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" - "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/authoringv2" - "github.com/calypr/loom/internal/explorer/capability" - explorercompilation "github.com/calypr/loom/internal/explorer/compilation" - "github.com/calypr/loom/internal/projectid" - "github.com/gofiber/fiber/v3" -) - -type explorerAuthoringHandlers struct { - getCapability fiber.Handler - searchSuggestions fiber.Handler - getCandidateSuggestions fiber.Handler - getBuilder fiber.Handler - saveDraft fiber.Handler - exportWorkspace fiber.Handler - applyCommands fiber.Handler - compileBuilder fiber.Handler - compile fiber.Handler - reconcile fiber.Handler - preview fiber.Handler - publish fiber.Handler -} - -// newExplorerAuthoringHandlers is the hard-cut traversal Builder contract. -// Generated OpenAPI code owns all method/path registration. -func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, capabilities ExplorerV2LifecycleConfig) *explorerAuthoringHandlers { - handlers := &explorerAuthoringHandlers{} - if authorizer == nil || authorizeRead == nil || explorers == nil { - return handlers - } - - readCapability := func(c fiber.Ctx) (capability.Snapshot, authoringv2.CatalogSnapshot, error) { - if capabilities.Capability == nil { - return capability.Snapshot{}, authoringv2.CatalogSnapshot{}, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", "Explorer capability compiler is not configured") - } - project, id := explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")) - snapshot, err := capabilities.Capability(c.Context(), project, id, "") - if err != nil || !snapshot.Usable() { - if err == nil { - err = capability.ErrSnapshotUnavailable - } - return capability.Snapshot{}, authoringv2.CatalogSnapshot{}, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error()) - } - return snapshot, authoringV2Catalog(snapshot, id), nil - } - - handlers.getCapability = func(c fiber.Ctx) error { - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - return c.JSON(explorerv2api.AuthoringCapability{ - ApiVersion: explorerv2api.LoomCalyprOrgexplorerAuthoringv2, - Kind: explorerv2api.ExplorerAuthoringCapabilities, - Operations: []explorerv2api.AuthoringCapabilityOperations{explorerv2api.Builder, explorerv2api.Compile, explorerv2api.Draft, explorerv2api.Export, explorerv2api.Suggestions, explorerv2api.Preview, explorerv2api.Publish, explorerv2api.Commands, explorerv2api.Reconcile}, - PreviewLimits: []int{10, 25, 50, 100}, - Features: explorerv2api.AuthoringFeatures{EmissionFilters: true, EmissionCharts: true}, - }) - } - - handlers.searchSuggestions = func(c fiber.Ctx) error { - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - var request explorerv2api.CandidateSearchRequest - if err := decodeAuthoringStrict(c.Body(), &request); err != nil || strings.TrimSpace(request.SnapshotToken) == "" || strings.TrimSpace(request.NodeId) == "" { - if err == nil { - err = fmt.Errorf("snapshotToken and nodeId are required") - } - return authoringHTTPError(c, malformedRouteError("suggestions", err)) - } - if capabilities.CapabilityToken == nil { - return authoringHTTPError(c, explorerUnavailable("suggestions", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured")) - } - snapshot, err := capabilities.CapabilityToken(c.Context(), explorerProjectParam(c), request.SnapshotToken) - if err != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { - return authoringHTTPError(c, explorerConflict("suggestions", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale", nil)) - } - query := "" - if request.Query != nil { - query = strings.ToLower(strings.TrimSpace(*request.Query)) - } - candidates := []authoringv2.CatalogCandidate{} - wire := authoringV2Catalog(snapshot, strings.TrimSpace(c.Params("explorerId"))) - for _, candidate := range wire.Candidates { - if candidate.NodeID == request.NodeId && (query == "" || strings.Contains(strings.ToLower(candidate.Label), query) || strings.Contains(strings.ToLower(candidate.ID), query)) { - candidates = append(candidates, candidate) - } - } - return c.JSON(fiber.Map{"apiVersion": authoringv2.APIVersion, "kind": "ExplorerBuilderCandidateSuggestions", "snapshotToken": request.SnapshotToken, "nodeId": request.NodeId, "candidates": candidates, "diagnostics": []any{}}) - } - - // Suggested values stay out of the main capability document. They are - // bounded during capability construction and fetched only when a user opens - // a field control, while retaining the exact snapshot-token semantics used - // by compilation. - handlers.getCandidateSuggestions = func(c fiber.Ctx) error { - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - if capabilities.CapabilityToken == nil { - return authoringHTTPError(c, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured")) - } - project := explorerProjectParam(c) - token, candidateID := strings.TrimSpace(c.Params("snapshotToken")), strings.TrimSpace(c.Params("candidateId")) - if token == "" || candidateID == "" { - return authoringHTTPError(c, malformedRouteError("capability", fmt.Errorf("snapshotToken and candidateId are required"))) - } - snapshot, err := capabilities.CapabilityToken(c.Context(), project, token) - if err != nil { - return authoringHTTPError(c, explorerConflict("capability", "STALE_CATALOG_SNAPSHOT", "the capability snapshot is stale or unavailable", map[string]any{"snapshotToken": token})) - } - for _, candidate := range snapshot.Candidates { - if candidate.ID != candidateID { - continue - } - return c.JSON(explorerv2api.CandidateSuggestions{ - ApiVersion: explorerv2api.LoomCalyprOrgexplorerAuthoringv2, - Kind: explorerv2api.ExplorerCandidateSuggestions, - SnapshotToken: token, CandidateId: candidateID, - Values: append([]string(nil), candidate.SuggestedValues...), - Complete: candidate.SuggestionsComplete, Truncated: candidate.SuggestionsTruncated, - }) - } - return authoringHTTPError(c, &explorer.AuthoringError{Status: http.StatusNotFound, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "capability", Code: "CANDIDATE_NOT_FOUND", Message: "candidate is not present in the capability snapshot"}}) - } - - handlers.getBuilder = func(c fiber.Ctx) error { - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - _, wire, err := readCapability(c) - if err != nil { - return authoringHTTPError(c, err) - } - project, id := explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")) - owner, err := explorers.Get(c.Context(), project, id) - if err != nil { - return authoringHTTPError(c, err) - } - state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, LifecycleState: authoringv2.LifecycleNew, DraftVersion: owner.DraftVersion, DraftDigest: owner.DraftDigest, Catalog: wire} - var activeWorkspace *authoringv2.Workspace - if owner.ActiveRevisionID != "" { - active, activeErr := explorers.ActiveRevision(c.Context(), project, id) - if activeErr != nil { - return authoringHTTPError(c, explorerConflict("builder", "AUTHORING_STATE_MISSING", "the active Explorer revision cannot be loaded as V2 authoring state", map[string]any{"revisionId": owner.ActiveRevisionID})) - } - workspace, decodeErr := authoringv2.DecodeWorkspace(active.AuthoringBundle) - if decodeErr != nil { - return authoringHTTPError(c, explorerConflict("builder", "AUTHORING_STATE_MISSING", "the active Explorer revision has no valid V2 workspace", map[string]any{"revisionId": active.ID})) - } - activeWorkspace = &workspace - } - if len(owner.DraftConfig) != 0 { - workspace, decodeErr := authoringv2.DecodeWorkspace(owner.DraftConfig) - if decodeErr != nil { - return authoringHTTPError(c, explorerConflict("builder", "DRAFT_STATE_INVALID", "the saved Explorer draft is not a valid V2 workspace", map[string]any{"draftVersion": owner.DraftVersion})) - } - state.Workspace = &workspace - state.LifecycleState = authoringv2.LifecycleReady - } else { - state.Workspace = activeWorkspace - if activeWorkspace != nil { - state.LifecycleState = authoringv2.LifecycleReady - } - } - if err := state.Validate(); err != nil { - return authoringHTTPError(c, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error())) - } - return c.JSON(state) - } - - handlers.saveDraft = func(c fiber.Ctx) error { - if err := authoringWrite(c, authorizer); err != nil { - return err - } - var request struct { - Workspace authoringv2.Workspace `json:"workspace"` - SnapshotToken string `json:"snapshotToken"` - ExpectedDraftVersion *int64 `json:"expectedDraftVersion"` - ExpectedDraftDigest string `json:"expectedDraftDigest,omitempty"` - } - if err := decodeAuthoringStrict(c.Body(), &request); err != nil || request.ExpectedDraftVersion == nil || strings.TrimSpace(request.SnapshotToken) == "" { - if err == nil { - err = fmt.Errorf("workspace, snapshotToken, and expectedDraftVersion are required") - } - return authoringHTTPError(c, malformedRouteError("draft", err)) - } - if capabilities.CapabilityToken == nil { - return authoringHTTPError(c, explorerUnavailable("draft", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured")) - } - project, id := explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")) - snapshot, err := capabilities.CapabilityToken(c.Context(), project, request.SnapshotToken) - if err != nil { - return authoringHTTPError(c, explorerConflict("draft", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale or unavailable", nil)) - } - state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, LifecycleState: authoringv2.LifecycleReady, Workspace: &request.Workspace, Catalog: authoringV2Catalog(snapshot, id)} - if err := state.Validate(); err != nil { - return authoringHTTPError(c, authoringSemanticRoute("draft", "$", workspaceValidationCode(err), err.Error(), nil)) - } - stored, err := explorers.SaveWorkspaceDraft(c.Context(), project, id, request.Workspace, *request.ExpectedDraftVersion, request.ExpectedDraftDigest, subjectFromFiber(c)) - if errors.Is(err, explorer.ErrDraftConflict) { - return authoringHTTPError(c, explorerConflict("draft", "DRAFT_CONFLICT", "the Explorer draft changed; reload before saving", nil)) - } - if err != nil { - return authoringHTTPError(c, err) - } - return c.JSON(fiber.Map{"workspace": json.RawMessage(stored.DraftConfig), "draftVersion": stored.DraftVersion, "draftDigest": stored.DraftDigest}) - } - - handlers.exportWorkspace = func(c fiber.Ctx) error { - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - project, id := explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")) - owner, err := explorers.Get(c.Context(), project, id) - if err != nil { - return authoringHTTPError(c, err) - } - raw := owner.DraftConfig - if len(raw) == 0 && owner.ActiveRevisionID != "" { - active, activeErr := explorers.ActiveRevision(c.Context(), project, id) - if activeErr != nil { - return authoringHTTPError(c, explorerConflict("export", "AUTHORING_STATE_MISSING", "the active Explorer revision cannot be exported", nil)) - } - raw = active.AuthoringBundle - } - workspace, err := authoringv2.DecodeWorkspace(raw) - if err != nil { - return authoringHTTPError(c, explorerConflict("export", "AUTHORING_STATE_MISSING", "no canonical V2 workspace is available for export", nil)) - } - canonical, err := workspace.CanonicalJSON() - if err != nil { - return authoringHTTPError(c, err) - } - c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - c.Set(fiber.HeaderContentDisposition, `attachment; filename="`+id+`.explorer-workspace.json"`) - return c.Send(canonical) - } - - handlers.applyCommands = func(c fiber.Ctx) error { - if err := authoringWrite(c, authorizer); err != nil { - return err - } - var request authoringv2.ApplyCommandsRequest - if err := decodeAuthoringStrict(c.Body(), &request); err != nil { - return authoringHTTPError(c, malformedRouteError("commands", err)) - } - if err := request.Validate(); err != nil { - return authoringHTTPError(c, malformedRouteError("commands", err)) - } - if capabilities.CapabilityToken == nil { - return authoringHTTPError(c, explorerUnavailable("commands", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured")) - } - project, id := explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")) - snapshot, err := capabilities.CapabilityToken(c.Context(), project, request.SnapshotToken) - if err != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { - return authoringHTTPError(c, explorerConflict("commands", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale or unavailable", nil)) - } - response, err := explorers.ApplyWorkspaceCommands(c.Context(), project, id, authoringV2Catalog(snapshot, id), request, subjectFromFiber(c)) - switch { - case errors.Is(err, explorer.ErrDraftConflict): - return authoringHTTPError(c, explorerConflict("commands", "DRAFT_CONFLICT", "the Explorer draft changed; reload before editing", nil)) - case errors.Is(err, explorer.ErrAuthoringCommandConflict): - return authoringHTTPError(c, explorerConflict("commands", "COMMAND_ID_CONFLICT", "commandId was already used for different intent", nil)) - case err != nil: - return authoringHTTPError(c, authoringSemanticRoute("commands", "$.commands", "INVALID_AUTHORING_COMMAND", err.Error(), nil)) - default: - return c.JSON(response) - } - } - - compileWorkspace := func(c fiber.Ctx, workspace authoringv2.Workspace, snapshotToken string) error { - if (capabilities.CapabilityToken == nil && capabilities.AuthorizedCapabilityCompile == nil) || capabilities.CompileReceipt == nil { - return authoringHTTPError(c, explorerUnavailable("compile", "CAPABILITY_UNAVAILABLE", "Explorer V2 compiler is not configured")) - } - project, id := explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")) - var err error - var snapshot capability.Snapshot - var authorized AuthorizedCapability - if capabilities.AuthorizedCapabilityCompile != nil { - authorized, err = capabilities.AuthorizedCapabilityCompile(c.Context(), project, snapshotToken) - snapshot = authorized.Snapshot - } else { - snapshot, err = capabilities.CapabilityToken(c.Context(), project, snapshotToken) - } - if err != nil { - return authoringHTTPError(c, explorerConflict("capability", "STALE_CATALOG_SNAPSHOT", "the capability snapshot is stale or unavailable", map[string]any{"snapshotToken": snapshotToken})) - } - wire := authoringV2Catalog(snapshot, id) - state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, Workspace: &workspace, Catalog: wire} - if err := state.Validate(); err != nil { - return authoringHTTPError(c, authoringSemanticRoute("intent", "$", workspaceValidationCode(err), err.Error(), nil)) - } - stored, err := capabilities.CompileReceipt(c.Context(), ExplorerV2ReceiptCompileRequest{Project: project, ExplorerID: id, Workspace: workspace, SnapshotToken: snapshot.Token, RequestID: requestIDFromFiber(c), Authorized: authorized}) - if err != nil { - var compileErr *explorercompilation.Error - if errors.As(err, &compileErr) { - return authoringHTTPError(c, authoringSemanticRoute(compileErr.Stage, compileErr.Path, compilationErrorCode(compileErr.Code), compileErr.Message, compileErr.Details)) - } - return authoringHTTPError(c, err) - } - if stored == nil || strings.TrimSpace(stored.ID) == "" { - return authoringHTTPError(c, explorerUnavailable("compile", "COMPILATION_RECEIPT_STORE_FAILED", "compiled authoring receipt was not persisted")) - } - return c.JSON(v2ReceiptResponse(stored, workspace)) - } - compileHandler := func(c fiber.Ctx) error { - if err := authoringWrite(c, authorizer); err != nil { - return err - } - var request explorerv2api.CompileRequest - if err := decodeAuthoringStrict(c.Body(), &request); err != nil { - return authoringHTTPError(c, malformedRouteError("intent", err)) - } - return compileWorkspace(c, request.Workspace, request.SnapshotToken) - } - handlers.compileBuilder = compileHandler - handlers.compile = compileHandler - handlers.reconcile = func(c fiber.Ctx) error { - if err := authoringWrite(c, authorizer); err != nil { - return err - } - var request struct { - SnapshotToken string `json:"snapshotToken"` - DraftVersion int64 `json:"draftVersion"` - DraftDigest string `json:"draftDigest"` - } - if err := decodeAuthoringStrict(c.Body(), &request); err != nil || strings.TrimSpace(request.SnapshotToken) == "" || request.DraftVersion < 0 { - if err == nil { - err = fmt.Errorf("snapshotToken, draftVersion, and draftDigest are required") - } - return authoringHTTPError(c, malformedRouteError("reconcile", err)) - } - owner, err := explorers.Get(c.Context(), explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId"))) - if err != nil { - return authoringHTTPError(c, err) - } - if owner.DraftVersion != request.DraftVersion || owner.DraftDigest != request.DraftDigest { - return authoringHTTPError(c, explorerConflict("reconcile", "DRAFT_CONFLICT", "the Explorer draft changed; reload before reconciling", nil)) - } - workspace, err := authoringv2.DecodeWorkspace(owner.DraftConfig) - if err != nil { - return authoringHTTPError(c, explorerConflict("reconcile", "AUTHORING_STATE_MISSING", "the saved Explorer draft cannot be reconciled", nil)) - } - return compileWorkspace(c, workspace, request.SnapshotToken) - } - - handlers.preview = func(c fiber.Ctx) error { - started := time.Now() - previewCtx, cancel := context.WithTimeout(c.Context(), explorerPreviewTimeout) - defer cancel() - responseBytes, responseRows := 0, 0 - logReceiptID, logOutputID := "", "" - lookupDuration, authorizationDuration := time.Duration(0), time.Duration(0) - previewSummary := engine.PreviewSummary{} - outcome := "failure" - defer func() { - if capabilities.Logger != nil { - capabilities.Logger.Info("Explorer receipt preview", "operation", "receipt_preview", "project", explorerProjectParam(c), "explorer_id", strings.TrimSpace(c.Params("explorerId")), "receipt_id", logReceiptID, "output_id", logOutputID, "outcome", outcome, "duration_ms", time.Since(started).Milliseconds(), "lookup_ms", lookupDuration.Milliseconds(), "authorization_ms", authorizationDuration.Milliseconds(), "lowering_ms", previewSummary.LoweringDuration.Milliseconds(), "query_ms", previewSummary.QueryDuration.Milliseconds(), "plan_mode", previewSummary.PlanMode, "plan_profile", previewSummary.PlanProfile, "plan_fingerprint", previewSummary.PlanFingerprint, "traversal_count", previewSummary.TraversalCount, "rows", responseRows, "bytes", responseBytes) - } - }() - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - var request explorerv2api.PreviewRequest - if err := decodeAuthoringStrict(c.Body(), &request); err != nil || strings.TrimSpace(request.ReceiptId) == "" || strings.TrimSpace(request.OutputId) == "" { - if err == nil { - err = fmt.Errorf("receiptId and outputId are required") - } - return authoringHTTPError(c, malformedRouteError("preview", err)) - } - logReceiptID, logOutputID = request.ReceiptId, request.OutputId - limit := engine.DefaultPreviewLimit - if request.Limit != nil { - limit = *request.Limit - } - if limit < 1 || limit > engine.MaxPreviewLimit { - return authoringHTTPError(c, authoringSemanticRoute("preview", "$.limit", "INVALID_PREVIEW_LIMIT", "limit must be between 1 and 1000", nil)) - } - nativeReceiptPreview := capabilities.PreviewReceipt != nil - if capabilities.AuthorizedCapabilityExecution == nil { - return authoringHTTPError(c, explorerUnavailable("preview", "PREVIEW_UNAVAILABLE", "authorized receipt execution is not configured")) - } - if capabilities.Preview == nil && capabilities.PreviewReceipt == nil { - return authoringHTTPError(c, explorerUnavailable("preview", "PREVIEW_UNAVAILABLE", "Explorer preview is not configured")) - } - lookupStarted := time.Now() - receipt, err := lookupV2Receipt(previewCtx, explorers, capabilities, explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")), request.ReceiptId) - lookupDuration = time.Since(lookupStarted) - if err != nil { - return authoringHTTPError(c, receiptRouteError("preview", err)) - } - if err := validateV2ReceiptRoute(c, receipt, capabilities); err != nil { - return authoringHTTPError(c, err) - } - authorizationStarted := time.Now() - authorized, err := capabilities.AuthorizedCapabilityExecution(previewCtx, receipt.Project, receipt.SnapshotToken) - authorizationDuration = time.Since(authorizationStarted) - snapshot := authorized.Snapshot - if err != nil || snapshot.ValidateToken(receipt.SnapshotToken) != nil || strings.TrimSpace(snapshot.Identity.Generation) != strings.TrimSpace(receipt.SourceGeneration) { - return authoringHTTPError(c, explorerConflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil)) - } - if capabilities.PreviewReceipt != nil { - if err := validateAuthorizedReceiptExecution(receipt, authorized); err != nil { - return authoringHTTPError(c, explorerConflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil)) - } - } - if nativeReceiptPreview { - if !receiptHasOutput(receipt.Bundle, request.OutputId) { - return authoringHTTPError(c, authoringSemanticRoute("preview", "$.outputId", "UNKNOWN_AUTHORING_OUTPUT", "outputId is not in the receipt", nil)) - } - if err := validateReceiptOutputContract(receipt, request.OutputId); err != nil { - return authoringHTTPError(c, authoringSemanticRoute("preview", "$.outputId", "UNKNOWN_AUTHORING_OUTPUT", "outputId is not in the receipt", nil)) - } - } else if !receiptHasOutput(receipt.Bundle, request.OutputId) { - return authoringHTTPError(c, authoringSemanticRoute("preview", "$.outputId", "UNKNOWN_AUTHORING_OUTPUT", "outputId is not in the receipt", nil)) - } - bindings := recipe.RuntimeBindings{Project: projectid.Legacy(receipt.Project), DatasetGeneration: receipt.SourceGeneration, PreviewLimit: limit, OutputNames: []string{request.OutputId}} - applyAuthorizedScope(&bindings, authorized, false) - columns := make([]explorer.EmittedColumn, 0) - for _, column := range receipt.EmittedColumns { - if column.OutputID == request.OutputId { - columns = append(columns, column) - } - } - var encoded []byte - if capabilities.PreviewReceipt != nil { - encoder, encoderErr := newPreviewResponseEncoder(receipt, request.OutputId, columns, maxExplorerPreviewResponseBytes) - if encoderErr != nil { - return previewRouteFailure(c, encoderErr) - } - var streamErr error - previewSummary, streamErr = capabilities.PreviewReceipt(previewCtx, receipt, bindings, encoder.Visit) - err = streamErr - responseRows = previewSummary.RowCount - if err == nil { - encoded, err = encoder.Finish() - } - } else { - var rows map[string][]map[string]any - rows, err = capabilities.Preview(previewCtx, receipt.Bundle, bindings) - if err == nil { - responseRows = len(rows[request.OutputId]) - encoded, err = encodeExplorerPreviewResponse(receipt, request.OutputId, columns, rows[request.OutputId], maxExplorerPreviewResponseBytes) - } - } - if err != nil { - return previewRouteFailure(c, err) - } - responseBytes = len(encoded) - outcome = "success" - c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) - return c.Send(encoded) - } - - handlers.publish = func(c fiber.Ctx) error { - if err := authoringWrite(c, authorizer); err != nil { - return err - } - var request explorerv2api.PublishRequest - if err := decodeAuthoringStrict(c.Body(), &request); err != nil || strings.TrimSpace(request.ReceiptId) == "" { - if err == nil { - err = fmt.Errorf("receiptId is required") - } - return authoringHTTPError(c, malformedRouteError("publish", err)) - } - if (capabilities.Materialize == nil && capabilities.MaterializeReceipt == nil) || capabilities.ActivateRelease == nil || capabilities.ValidateReleaseGeneration == nil || (capabilities.CapabilityToken == nil && capabilities.AuthorizedCapabilityExecution == nil) { - return authoringHTTPError(c, explorerUnavailable("publish", "PUBLICATION_UNAVAILABLE", "Explorer publication is not configured")) - } - receipt, err := lookupV2Receipt(c.Context(), explorers, capabilities, explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")), request.ReceiptId) - if err != nil { - return authoringHTTPError(c, receiptRouteError("publish", err)) - } - if err := validateV2ReceiptRoute(c, receipt, capabilities); err != nil { - return authoringHTTPError(c, err) - } - var snapshot capability.Snapshot - var authorized AuthorizedCapability - if capabilities.AuthorizedCapabilityExecution != nil { - authorized, err = capabilities.AuthorizedCapabilityExecution(c.Context(), receipt.Project, receipt.SnapshotToken) - snapshot = authorized.Snapshot - } else { - snapshot, err = capabilities.CapabilityToken(c.Context(), receipt.Project, receipt.SnapshotToken) - } - if err != nil || strings.TrimSpace(snapshot.Identity.Generation) != strings.TrimSpace(receipt.SourceGeneration) { - return authoringHTTPError(c, explorerConflict("publish", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil)) - } - if capabilities.CompileReceipt != nil { - if err := validateReceiptCapability(receipt, snapshot); err != nil { - return authoringHTTPError(c, explorerConflict("publish", "RECEIPT_STALE", err.Error(), nil)) - } - } - if len(receipt.EmittedColumns) == 0 || len(receipt.CompiledConfig) == 0 { - return authoringHTTPError(c, authoringSemanticRoute("publish", "$.receiptId", "NO_SELECTED_COLUMNS", "select at least one output column before publishing", nil)) - } - if capabilities.CompileReceipt != nil { - workspace, decodeErr := authoringv2.DecodeWorkspace(receipt.NormalizedBundle) - if decodeErr != nil { - return authoringHTTPError(c, explorerConflict("publish", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt contains invalid authoring intent", nil)) - } - if err := workspace.ValidateForPublication(); err != nil { - return authoringHTTPError(c, authoringSemanticRoute("publish", "$.receiptId", "NO_SELECTED_COLUMNS", "select at least one visible output column for every visible table before publishing", nil)) - } - } - if err := capabilities.ValidateReleaseGeneration(c.Context(), projectid.Legacy(receipt.Project), receipt.SourceGeneration); err != nil { - return authoringHTTPError(c, explorerConflict("publish", "RECEIPT_STALE", "the receipt generation is no longer active", map[string]any{"generation": receipt.SourceGeneration})) - } - bindings := recipe.RuntimeBindings{Project: projectid.Legacy(receipt.Project), DatasetGeneration: receipt.SourceGeneration} - applyAuthorizedScope(&bindings, authorized, true) - var execution graphresolver.RecipeExecution - if capabilities.MaterializeReceipt != nil { - execution, err = capabilities.MaterializeReceipt(c.Context(), receipt, bindings) - } else { - execution, err = capabilities.Materialize(c.Context(), receipt.Bundle, bindings) - } - if err != nil { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 503, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "materialize", Code: "MATERIALIZATION_FAILED", Message: "Explorer materialization failed; the active revision was retained"}, Cause: err}) - } - if err := verifyQueryableOutputs(receipt.Bundle, execution); err != nil { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 503, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "materialize", Code: "MATERIALIZATION_FAILED", Message: "materialization did not produce queryable outputs"}, Cause: err}) - } - if err := capabilities.ActivateRelease(c.Context(), projectid.Legacy(receipt.Project), receipt.SourceGeneration, selectorsForBundle(receipt.Bundle)); err != nil { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 503, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "activation", Code: "MATERIALIZATION_ACTIVATION_FAILED", Message: "dataset release activation failed; the prior Explorer revision was retained"}, Cause: err}) - } - now := time.Now().UTC() - revisionID := "authoring_" + strings.TrimPrefix(receipt.ID, "receipt_") - revision, err := explorers.PublishAuthoring(c.Context(), *receipt, explorer.Revision{ID: revisionID, Project: receipt.Project, ExplorerID: receipt.ExplorerID, Config: receipt.CompiledConfig, ConfigDigest: receipt.IntentDigest, AuthoringBundle: receipt.NormalizedBundle, IntentDigest: receipt.IntentDigest, CompilationReceiptID: receipt.ID, PublicOutputContract: receipt.PublicOutputContract, Recipe: receipt.Bundle, RecipeDigest: receipt.RecipeDigest, ResolvedSchemaDigest: receipt.ResolvedSchemaDigest, SourceGeneration: receipt.SourceGeneration, Materializations: explorerMaterializations(receipt.Bundle, execution), EmittedColumns: receipt.EmittedColumns, Dataset: datasetMetadataFromExecution(receipt.Bundle, receipt.SourceGeneration, receipt.ResolvedSchemaDigest, execution), Publication: explorer.PublicationMetadata{State: string(explorer.RevisionReady), Generation: receipt.SourceGeneration, ExecutionID: execution.ID, UpdatedAt: now}, Status: explorer.RevisionReady, CreatedBy: subjectFromFiber(c), CreatedAt: now, ReadyAt: &now}) - if err != nil { - return authoringHTTPError(c, err) - } - outputs := make([]explorerv2api.PublicationOutput, 0, len(revision.Materializations)) - for _, materialization := range revision.Materializations { - outputs = append(outputs, explorerv2api.PublicationOutput{OutputId: firstNonEmpty(materialization.OutputID, materialization.Output), State: "READY", MaterializationId: materialization.MaterializationID}) - } - return c.JSON(explorerv2api.PublishResponse{ - ApiVersion: explorerv2api.LoomCalyprOrgexplorerAuthoringv2, - Kind: explorerv2api.ExplorerBuilderPublication, - ReceiptId: receipt.ID, - RevisionId: revision.ID, - State: string(revision.Status), - Outputs: outputs, - Diagnostics: []explorerv2api.Diagnostic{}, - }) - } - return handlers -} - -func validateV2ReceiptRoute(c fiber.Ctx, receipt *explorer.CompilationReceipt, capabilities ExplorerV2LifecycleConfig) error { - if receipt == nil || receipt.Project != explorerProjectParam(c) || receipt.ExplorerID != strings.TrimSpace(c.Params("explorerId")) { - return &explorer.AuthoringError{Status: http.StatusNotFound, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "receipt", Code: "COMPILE_RECEIPT_NOT_FOUND", Message: "compilation receipt was not found"}} - } - if strings.TrimSpace(receipt.ID) == "" || strings.TrimSpace(receipt.RecipeDigest) == "" || len(receipt.Bundle.Outputs) == 0 { - return explorerConflict("receipt", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt is from an unsupported or incomplete compiler contract", nil) - } - nativeReceiptContract := capabilities.CompileReceipt != nil || capabilities.PreviewReceipt != nil || capabilities.MaterializeReceipt != nil - if nativeReceiptContract && (receipt.ReceiptFormatVersion != explorer.CurrentReceiptFormatVersion || receipt.CompilerContractVersion != explorer.CurrentCompilerContractVersion) { - return explorerConflict("receipt", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt is from an unsupported compiler contract", nil) - } - if nativeReceiptContract { - if err := receipt.Validate(); err != nil { - return explorerConflict("receipt", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt failed integrity validation and must be recompiled", nil) - } - } - return nil -} - -func lookupV2Receipt(ctx context.Context, explorers *explorer.Service, capabilities ExplorerV2LifecycleConfig, project, explorerID, receiptID string) (*explorer.CompilationReceipt, error) { - if capabilities.ReceiptLookup != nil { - return capabilities.ReceiptLookup(ctx, project, explorerID, receiptID) - } - if explorers == nil { - return nil, explorer.ErrNotFound - } - return explorers.CompilationReceiptForExplorer(ctx, project, explorerID, receiptID) -} - -func receiptRouteError(stage string, err error) error { - if errors.Is(err, explorer.ErrNotFound) { - return &explorer.AuthoringError{Status: http.StatusNotFound, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: stage, Code: "COMPILE_RECEIPT_NOT_FOUND", Message: "compilation receipt was not found"}} - } - if errors.Is(err, explorer.ErrReceiptRecompileRequired) { - return explorerConflict(stage, "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt is from an unsupported compiler contract", nil) - } - return explorerUnavailable(stage, "RECEIPT_STORE_UNAVAILABLE", "the compilation receipt store is unavailable") -} - -func workspaceValidationCode(err error) string { - message := err.Error() - for _, code := range []string{"DUPLICATE_OUTPUT_ID", "DUPLICATE_TAB_ID", "INVALID_TAB_OUTPUT_MAPPING", "INVALID_TAB_ORDER", "ROW_ROOT_NOT_ELIGIBLE", "UNSUPPORTED_FILTER", "UNSUPPORTED_CHART", "NO_VISIBLE_COLUMNS"} { - if strings.Contains(message, code) { - return code - } - } - switch { - case strings.Contains(message, "rootNodeId"): - return "INVALID_ROOT_NODE" - case strings.Contains(message, "route") || strings.Contains(message, "edge"): - return "INVALID_ROUTE" - case strings.Contains(message, "occurrence"): - return "INVALID_OCCURRENCE" - case strings.Contains(message, "projection mode"): - return "INVALID_PROJECTION_MODE" - case strings.Contains(message, "duplicate selection"): - return "DUPLICATE_SELECTION" - default: - return "INVALID_AUTHORING_INTENT" - } -} - -func compilationErrorCode(code string) string { - switch code { - case "STALE_ROOT_NODE": - return "INVALID_ROOT_NODE" - case "ROOT_NOT_ELIGIBLE", "UNSUPPORTED_ROW_ROOT": - return "ROW_ROOT_NOT_ELIGIBLE" - case "STALE_EDGE": - return "STALE_EDGE_ID" - case "DISCONNECTED_ROUTE", "REPEATED_EDGE_NOT_ALLOWED", "SELF_LOOP_NOT_ALLOWED", "ROUTE_TOO_LONG": - return "INVALID_ROUTE" - case "STALE_CANDIDATE": - return "STALE_CANDIDATE_ID" - case "STALE_OCCURRENCE", "DUPLICATE_OCCURRENCE": - return "INVALID_OCCURRENCE" - case "UNSUPPORTED_PROJECTION_MODE": - return "INVALID_PROJECTION_MODE" - default: - return code - } -} - -func applyAuthorizedScope(bindings *recipe.RuntimeBindings, authorized AuthorizedCapability, includeAuthResourcePath bool) { - if bindings == nil { - return - } - bindings.AuthResourcePaths = append([]string(nil), authorized.Scope.AuthResourcePaths...) - bindings.AuthScopeMode = authorized.Scope.Mode - bindings.IncludeAuthResourcePath = includeAuthResourcePath -} - -func v2ReceiptResponse(receipt *explorer.CompilationReceipt, workspace authoringv2.Workspace) explorerv2api.CompileResponse { - if receipt != nil && len(receipt.NormalizedBundle) != 0 { - if normalized, err := authoringv2.DecodeWorkspace(receipt.NormalizedBundle); err == nil { - workspace = normalized - } - } - outputs := make([]explorerv2api.ReceiptOutput, 0, len(workspace.Documents)) - for _, document := range workspace.Documents { - rowGrain := "" - for _, output := range receipt.Bundle.Outputs { - if output.Name == document.Output.ID { - rowGrain = output.RowGrain - break - } - } - columns := []explorerv2api.ContractColumn{} - for _, column := range receipt.EmittedColumns { - if column.OutputID != document.Output.ID { - continue - } - label := column.Label - if label == "" { - label = column.PublicColumn - } - columns = append(columns, explorerv2api.ContractColumn{Column: column.PublicColumn, Label: label, LogicalType: column.LogicalType, Filterable: column.Filterable, Chartable: column.Chartable}) - } - outputs = append(outputs, explorerv2api.ReceiptOutput{OutputId: document.Output.ID, Title: document.Output.Title, RowGrain: rowGrain, Columns: columns}) - } - return explorerv2api.CompileResponse{ - ApiVersion: explorerv2api.LoomCalyprOrgexplorerAuthoringv2, - Kind: explorerv2api.ExplorerBuilderReceipt, - ReceiptId: receipt.ID, SnapshotToken: receipt.SnapshotToken, - Generation: receipt.SourceGeneration, IntentDigest: receipt.IntentDigest, - CompilerVersion: explorer.CurrentCompilerContractVersion + "+" + explorercompilation.TranslationVersion, Builder: workspace, - Outputs: outputs, Diagnostics: []explorerv2api.Diagnostic{}, - } -} diff --git a/internal/server/explorer_builder_lifecycle.go b/internal/server/explorer_builder_lifecycle.go deleted file mode 100644 index dd083b9d..00000000 --- a/internal/server/explorer_builder_lifecycle.go +++ /dev/null @@ -1,88 +0,0 @@ -package server - -import ( - "errors" - "net/http" - "strings" - - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/explorer" - "github.com/gofiber/fiber/v3" -) - -type explorerLifecycleHandlers struct { - list fiber.Handler - get fiber.Handler - create fiber.Handler -} - -// newExplorerLifecycleHandlers exposes only collection summaries, creation, -// and the active runtime projection. Generated OpenAPI routing owns paths. -func newExplorerLifecycleHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, capabilities ExplorerV2LifecycleConfig) *explorerLifecycleHandlers { - handlers := &explorerLifecycleHandlers{} - if authorizer == nil || authorizeRead == nil || explorers == nil { - return handlers - } - handlers.list = func(c fiber.Ctx) error { - project := explorerProjectParam(c) - if err := authorizeRead(c.Context(), principalFromFiber(c), project); err != nil { - return explorerV2Error(c, http.StatusForbidden, "FORBIDDEN", "forbidden") - } - values, err := explorers.List(c.Context(), project) - if err != nil { - return explorerV2Error(c, 500, "EXPLORER_READ_FAILED", err.Error()) - } - summaries := make([]explorer.ExplorerSummaryV1, 0, len(values)) - for _, value := range values { - summaries = append(summaries, explorer.ExplorerSummaryV1{Project: project, ExplorerID: value.ExplorerID, Title: value.Title, Management: value.ManagementMode, ActiveRevisionID: value.ActiveRevisionID, UpdatedAt: value.UpdatedAt}) - } - return c.JSON(summaries) - } - - handlers.get = func(c fiber.Ctx) error { - project, id := explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")) - if err := authorizeRead(c.Context(), principalFromFiber(c), project); err != nil { - return explorerV2Error(c, http.StatusForbidden, "FORBIDDEN", "forbidden") - } - state, err := explorers.LoadExplorerState(c.Context(), project, id) - if errors.Is(err, explorer.ErrNotFound) { - return explorerV2Error(c, 404, "EXPLORER_NOT_FOUND", "Explorer not found") - } - if err != nil { - return explorerV2Error(c, 500, "EXPLORER_READ_FAILED", err.Error()) - } - return c.JSON(state) - } - - handlers.create = func(c fiber.Ctx) error { - project := explorerProjectParam(c) - if err := authorizeExplorerWrite(c, authorizer, project); err != nil { - return err - } - var request struct { - Name string `json:"name"` - Title string `json:"title,omitempty"` - SourceExplorerID string `json:"sourceExplorerId,omitempty"` - } - if err := decodeStrict(c.Body(), &request); err != nil || strings.TrimSpace(request.Name) == "" { - return explorerV2Error(c, 400, "MALFORMED_REQUEST", "name is required") - } - id := explorer.StableExplorerID(request.Name) - if id == "default" { - return explorerV2Error(c, 409, "EXPLORER_EXISTS", "the repository default already exists") - } - title := strings.TrimSpace(request.Title) - if title == "" { - title = strings.TrimSpace(request.Name) - } - value, err := explorers.CreateInteractiveFrom(c.Context(), project, id, title, strings.TrimSpace(request.SourceExplorerID), subjectFromFiber(c)) - if errors.Is(err, explorer.ErrDraftConflict) { - return explorerV2Error(c, 409, "EXPLORER_EXISTS", "an Explorer with this name already exists") - } - if err != nil { - return explorerV2Error(c, 422, "INVALID_EXPLORER", err.Error()) - } - return c.Status(http.StatusCreated).JSON(explorer.ExplorerSummaryV1{Project: project, ExplorerID: value.ExplorerID, Title: value.Title, Management: value.ManagementMode, UpdatedAt: value.UpdatedAt}) - } - return handlers -} diff --git a/internal/server/explorer_capability.go b/internal/server/explorer_capability.go index 09174383..49cb3548 100644 --- a/internal/server/explorer_capability.go +++ b/internal/server/explorer_capability.go @@ -17,7 +17,7 @@ import ( "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/explorer/capability" - "github.com/calypr/loom/internal/explorer/capabilitystore" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/calypr/loom/internal/projectid" "golang.org/x/sync/singleflight" ) @@ -44,53 +44,11 @@ type explorerCapabilityResolver struct { evidence catalog.CapabilityEvidenceReader scopes *authscope.ScopeResolver manifests dataset.ActiveResolver - snapshots capabilitystore.Repository + snapshots capability.Repository builds singleflight.Group } -type ExplorerCapabilityReader func(context.Context, string, string, string) (capability.Snapshot, error) -type ExplorerCapabilityTokenReader func(context.Context, string, string) (capability.Snapshot, error) - -// AuthorizedCapability is the capability contract handed to compilation and -// execution. The scope is part of the contract: callers must not derive it -// again from a token or accidentally turn a restricted-empty scope into an -// unrestricted one. Both values are copied before they cross this boundary. -type AuthorizedCapability struct { - Snapshot capability.Snapshot - Scope authscope.ReadScope -} - -// Clone returns an independent authorized capability. Snapshot.Clone also -// copies nested diagnostic values through the capability package's immutable -// accessor, while ReadScope.Clone protects the authorization path slice. -func (a AuthorizedCapability) Clone() AuthorizedCapability { - return AuthorizedCapability{Snapshot: a.Snapshot.Clone(), Scope: a.Scope.Clone()} -} - -// ExplorerAuthorizedCapabilityCompilationReader accepts an opaque snapshot -// token for a new compile. Implementations must require that token to belong -// to the currently active dataset generation. -type ExplorerAuthorizedCapabilityCompilationReader func(context.Context, string, string) (AuthorizedCapability, error) - -// ExplorerAuthorizedCapabilityExecutionReader loads an exact retained token -// for preview/materialization. Implementations may accept inactive generations -// while retained, but must re-authorize the caller against the token's exact -// generation and scope digest. -type ExplorerAuthorizedCapabilityExecutionReader func(context.Context, string, string) (AuthorizedCapability, error) - -// ExplorerCapabilityCompilationResolver documents the compile authorization -// seam for callers that want a resolver object rather than a function field. -type ExplorerCapabilityCompilationResolver interface { - ResolveForCompilation(context.Context, string, string) (AuthorizedCapability, error) -} - -// ExplorerCapabilityExecutionResolver documents the execution authorization -// seam for callers that want a resolver object rather than a function field. -type ExplorerCapabilityExecutionResolver interface { - ResolveForExecution(context.Context, string, string) (AuthorizedCapability, error) -} - -func newExplorerCapabilityResolver(evidence catalog.CapabilityEvidenceReader, scopes *authscope.ScopeResolver, manifests dataset.ActiveResolver, snapshots capabilitystore.Repository) (*explorerCapabilityResolver, error) { +func newExplorerCapabilityResolver(evidence catalog.CapabilityEvidenceReader, scopes *authscope.ScopeResolver, manifests dataset.ActiveResolver, snapshots capability.Repository) (*explorerCapabilityResolver, error) { if evidence == nil || manifests == nil || snapshots == nil { return nil, fmt.Errorf("Explorer capability evidence, manifest resolver, and snapshot repository are required") } @@ -150,14 +108,16 @@ func (r *explorerCapabilityResolver) Resolve(ctx context.Context, project, reque value, err, _ := r.builds.Do(key, func() (any, error) { if existing, getErr := r.snapshots.GetByIdentity(ctx, identity); getErr == nil && existing.Usable() { return existing.Clone(), nil - } else if getErr != nil && !errors.Is(getErr, capabilitystore.ErrNotFound) { + } else if getErr != nil && !errors.Is(getErr, capability.ErrNotFound) { return nil, getErr } - observer := capabilityObserverFromEvidence(evidence) - builder := capability.NewBuilder(identity, observer, observer, observer, explorerCapabilityCompiler{scope: compilerprobe.Scope{ + compiler := explorerCapabilityCompiler{scope: compilerprobe.Scope{ Project: storageProject, DatasetGeneration: generation, AuthResourcePaths: append([]string(nil), scope.AuthResourcePaths...), AuthScopeMode: scope.Mode, - }}) + }} + builder := capability.NewBuilder(identity, capabilityEvidenceFromCatalog(evidence), capability.CompilerCallbacks{ + Node: compiler.ProbeNode, Edge: compiler.ProbeEdge, Candidate: compiler.ProbeCandidate, + }) builder.Policy = capability.Policy{ Route: capability.RoutePolicy{Version: explorerTraversalPolicyVersion, MaxHops: 0, AllowsRepeatedEdges: true, AllowsSelfLoops: true}, Projection: capability.ProjectionPolicy{Version: explorerProjectionPolicyVersion, SuggestionLimit: capability.DefaultSuggestionLimit}, @@ -199,49 +159,49 @@ func (r *explorerCapabilityResolver) ResolveToken(ctx context.Context, project, // preview and other execution paths may use an inactive generation while its // immutable capability remains available. The caller's current effective // scope must still match the scope digest captured by the snapshot. -func (r *explorerCapabilityResolver) ResolveForExecution(ctx context.Context, project, token string) (AuthorizedCapability, error) { +func (r *explorerCapabilityResolver) ResolveForExecution(ctx context.Context, project, token string) (lifecycle.AuthorizedCapability, error) { project = projectid.Canonical(project) token = strings.TrimSpace(token) snapshot, err := r.snapshots.GetByToken(ctx, token) if err != nil { - return AuthorizedCapability{}, err + return lifecycle.AuthorizedCapability{}, err } if snapshot.Identity.Project != project { - return AuthorizedCapability{}, capability.ErrStaleSnapshot + return lifecycle.AuthorizedCapability{}, capability.ErrStaleSnapshot } principal, _ := authscope.PrincipalFromContext(ctx) if err := authscope.AuthorizeProject(principal, project, false); err != nil { - return AuthorizedCapability{}, err + return lifecycle.AuthorizedCapability{}, err } scope, err := r.resolveScope(ctx, principal, projectid.Legacy(project), snapshot.Identity.Generation) if err != nil { - return AuthorizedCapability{}, err + return lifecycle.AuthorizedCapability{}, err } if explorerScopeDigest(scope) != snapshot.Identity.AuthorizationScopeDigest { - return AuthorizedCapability{}, capability.ErrStaleSnapshot + return lifecycle.AuthorizedCapability{}, capability.ErrStaleSnapshot } if err := snapshot.ValidateToken(token); err != nil { - return AuthorizedCapability{}, err + return lifecycle.AuthorizedCapability{}, err } - return AuthorizedCapability{Snapshot: snapshot.Clone(), Scope: scope.Clone()}, nil + return lifecycle.AuthorizedCapability{Snapshot: snapshot.Clone(), Scope: scope.Clone()}, nil } // ResolveForCompilation loads an exact token for a new compile. Unlike // execution, compilation is only valid against the currently active immutable // dataset generation; a retained inactive token is rejected even when its // scope is otherwise still authorized. -func (r *explorerCapabilityResolver) ResolveForCompilation(ctx context.Context, project, token string) (AuthorizedCapability, error) { +func (r *explorerCapabilityResolver) ResolveForCompilation(ctx context.Context, project, token string) (lifecycle.AuthorizedCapability, error) { project = projectid.Canonical(project) authorized, err := r.ResolveForExecution(ctx, project, token) if err != nil { - return AuthorizedCapability{}, err + return lifecycle.AuthorizedCapability{}, err } manifest, err := dataset.ResolveActive(ctx, r.manifests, projectid.Legacy(project)) if err != nil { - return AuthorizedCapability{}, fmt.Errorf("resolve current capability generation: %w", err) + return lifecycle.AuthorizedCapability{}, fmt.Errorf("resolve current capability generation: %w", err) } if authorized.Snapshot.Identity.Generation != manifest.Dataset.Generation { - return AuthorizedCapability{}, capability.ErrStaleSnapshot + return lifecycle.AuthorizedCapability{}, capability.ErrStaleSnapshot } return authorized.Clone(), nil } @@ -293,30 +253,14 @@ func capabilityIdentityKey(identity capability.SnapshotIdentity) (string, error) return hex.EncodeToString(sum[:]), nil } -type capabilityEvidenceObserver struct { - resources []capability.ResourceObservation - relationships []capability.RelationshipObservation - fields []capability.FieldObservation -} - -func (o capabilityEvidenceObserver) ListResources(context.Context) ([]capability.ResourceObservation, error) { - return append([]capability.ResourceObservation(nil), o.resources...), nil -} -func (o capabilityEvidenceObserver) ListRelationships(context.Context) ([]capability.RelationshipObservation, error) { - return append([]capability.RelationshipObservation(nil), o.relationships...), nil -} -func (o capabilityEvidenceObserver) ListFields(context.Context) ([]capability.FieldObservation, error) { - out := append([]capability.FieldObservation(nil), o.fields...) - for i := range out { - out[i].SuggestedValues = append([]string(nil), out[i].SuggestedValues...) +func capabilityEvidenceFromCatalog(value catalog.CapabilityEvidence) capability.Evidence { + out := capability.Evidence{ + ResourcesAvailable: value.ResourceInventory.Available, + RelationshipsAvailable: value.Relationships.Available, + FieldsAvailable: value.FieldEnrichment.Available, } - return out, nil -} - -func capabilityObserverFromEvidence(value catalog.CapabilityEvidence) capabilityEvidenceObserver { - out := capabilityEvidenceObserver{} for _, item := range value.ResourceInventory.Values { - out.resources = append(out.resources, capability.ResourceObservation{ResourceType: item.ResourceType, Populated: item.DocumentCount > 0, DocumentCount: item.DocumentCount}) + out.Resources = append(out.Resources, capability.ResourceObservation{ResourceType: item.ResourceType, Populated: item.DocumentCount > 0, DocumentCount: item.DocumentCount}) } relationships := map[string]capability.RelationshipObservation{} for _, item := range value.Relationships.Values { @@ -343,10 +287,10 @@ func capabilityObserverFromEvidence(value catalog.CapabilityEvidence) capability } } for _, item := range relationships { - out.relationships = append(out.relationships, item) + out.Relationships = append(out.Relationships, item) } - sort.Slice(out.relationships, func(i, j int) bool { - a, b := out.relationships[i], out.relationships[j] + sort.Slice(out.Relationships, func(i, j int) bool { + a, b := out.Relationships[i], out.Relationships[j] return strings.Join([]string{a.SourceResourceType, a.Label, a.TargetResourceType, a.StorageDirection}, "\x00") < strings.Join([]string{b.SourceResourceType, b.Label, b.TargetResourceType, b.StorageDirection}, "\x00") }) type fieldAggregate struct { @@ -376,10 +320,10 @@ func capabilityObserverFromEvidence(value catalog.CapabilityEvidence) capability aggregate.observation.SuggestedValues = append(aggregate.observation.SuggestedValues, suggestion) } sort.Strings(aggregate.observation.SuggestedValues) - out.fields = append(out.fields, aggregate.observation) + out.Fields = append(out.Fields, aggregate.observation) } - sort.Slice(out.fields, func(i, j int) bool { - return out.fields[i].ResourceType+"\x00"+out.fields[i].Path < out.fields[j].ResourceType+"\x00"+out.fields[j].Path + sort.Slice(out.Fields, func(i, j int) bool { + return out.Fields[i].ResourceType+"\x00"+out.Fields[i].Path < out.Fields[j].ResourceType+"\x00"+out.Fields[j].Path }) return out } diff --git a/internal/server/explorer_capability_test.go b/internal/server/explorer_capability_test.go index 97bce33c..bb105660 100644 --- a/internal/server/explorer_capability_test.go +++ b/internal/server/explorer_capability_test.go @@ -248,8 +248,8 @@ func TestExplorerCapabilityResolverFailsClosedOnIncompleteFieldEnrichment(t *tes } } -func TestCapabilityObserverPublishesBothStoredRelationshipDirections(t *testing.T) { - observer := capabilityObserverFromEvidence(catalog.CapabilityEvidence{ +func TestCapabilityEvidencePublishesBothStoredRelationshipDirections(t *testing.T) { + evidence := capabilityEvidenceFromCatalog(catalog.CapabilityEvidence{ Relationships: catalog.RelationshipObservationResult{Values: []catalog.RelationshipObservation{{ Label: "subject_Patient", EdgeCount: 7, StorageFromType: "Specimen", StorageToType: "Patient", StorageDirection: "OUTBOUND", @@ -257,7 +257,7 @@ func TestCapabilityObserverPublishesBothStoredRelationshipDirections(t *testing. }}}, }) got := map[string]capability.RelationshipObservation{} - for _, relationship := range observer.relationships { + for _, relationship := range evidence.Relationships { got[relationship.SourceResourceType+"->"+relationship.TargetResourceType] = relationship } if outbound := got["Specimen->Patient"]; outbound.StorageDirection != "OUTBOUND" || outbound.ObservedEdgeCount != 7 { diff --git a/internal/server/explorer_config_route.go b/internal/server/explorer_config_route.go index 421f410a..806d3b0f 100644 --- a/internal/server/explorer_config_route.go +++ b/internal/server/explorer_config_route.go @@ -2,181 +2,32 @@ package server import ( "context" - "fmt" - "net/http" - "strings" - "time" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/authoringv2" - "github.com/calypr/loom/internal/projectid" - "github.com/gofiber/fiber/v3" + "github.com/calypr/loom/internal/explorer/lifecycle" ) type explorerConfigReadAuthorizer func(context.Context, *authscope.Principal, string) error +// explorerHTTPHandlers holds the dependencies used by the generated OpenAPI +// Explorer adapter. Fiber routing is registered once, from the generated +// server, rather than through a parallel handwritten handler tree. type explorerHTTPHandlers struct { - publishRepositoryConfig fiber.Handler - getRepositoryConfig fiber.Handler - lifecycle *explorerLifecycleHandlers - authoring *explorerAuthoringHandlers + authorizer authscope.Authorizer + authorizeRead explorerConfigReadAuthorizer + application *lifecycle.Service } -// newExplorerHTTPHandlers builds the live Explorer transport implementations; -// generated OpenAPI code owns all method/path registration. -func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, materialize graphresolver.ExplorerBundleMaterializer, lifecycle ...ExplorerV2LifecycleConfig) *explorerHTTPHandlers { - handlers := &explorerHTTPHandlers{} - if authorizer == nil || authorizeRead == nil || explorers == nil { - return handlers +func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, configs ...lifecycle.Config) *explorerHTTPHandlers { + config := lifecycle.Config{} + if len(configs) > 0 { + config = configs[0] } - handlers.publishRepositoryConfig = func(c fiber.Ctx) error { - project, generation := explorerProjectParam(c), strings.TrimSpace(c.Params("generation")) - if project == "" || generation == "" { - return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "project and generation are required"}) - } - // The dataset key (for example HTAN_INT-BForePC) is not an Arborist - // resource path. Reuse the scope supplied during the deferred upload. - authResourcePath := authscope.NormalizeAuthResourcePath(strings.TrimSpace(c.Query("auth_resource_path"))) - principal, _ := c.Locals("principal").(*authscope.Principal) - if err := authorizer.AuthorizeWrite(c.Context(), principal, project, authResourcePath); err != nil { - return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "forbidden"}) - } - workspace, err := authoringv2.DecodeWorkspace(c.Body()) - if err != nil { - return c.Status(http.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()}) - } - if err := workspace.ValidateForPublication(); err != nil { - return c.Status(http.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()}) - } - commit := strings.TrimSpace(c.Get("X-Loom-Source-Commit")) - if commit == "" { - return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "X-Loom-Source-Commit is required"}) - } - if len(lifecycle) == 0 || lifecycle[0].Capability == nil || lifecycle[0].AuthorizedCapabilityCompile == nil || lifecycle[0].CompileReceipt == nil { - return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "repository V2 compilation is not configured"}) - } - config := lifecycle[0] - snapshot, err := config.Capability(c.Context(), project, "default", generation) - if err != nil || !snapshot.Usable() || snapshot.Identity.Generation != generation { - if err == nil { - err = fmt.Errorf("capability generation %q does not match deployment generation %q", snapshot.Identity.Generation, generation) - } - return c.Status(http.StatusConflict).JSON(fiber.Map{"error": fmt.Sprintf("resolve repository V2 capability: %v", err)}) - } - authorized, err := config.AuthorizedCapabilityCompile(c.Context(), project, snapshot.Token) - if err != nil { - return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": fmt.Sprintf("authorize repository V2 compilation: %v", err)}) - } - receipt, err := config.CompileReceipt(c.Context(), ExplorerV2ReceiptCompileRequest{Project: project, ExplorerID: "default", Workspace: workspace, SnapshotToken: snapshot.Token, RequestID: requestIDFromFiber(c), Authorized: authorized}) - if err != nil || receipt == nil { - if err == nil { - err = fmt.Errorf("compiler returned no receipt") - } - return c.Status(http.StatusUnprocessableEntity).JSON(fiber.Map{"error": fmt.Sprintf("compile repository V2 workspace: %v", err)}) - } - if receipt.SourceGeneration != generation { - return c.Status(http.StatusConflict).JSON(fiber.Map{"error": "compiled receipt generation does not match deployment generation"}) - } - if config.AuthorizedCapabilityExecution != nil { - authorized, err = config.AuthorizedCapabilityExecution(c.Context(), project, receipt.SnapshotToken) - if err != nil { - return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": fmt.Sprintf("authorize repository V2 materialization: %v", err)}) - } - } - bindings := recipe.RuntimeBindings{Project: projectid.Legacy(project), DatasetGeneration: generation} - applyAuthorizedScope(&bindings, authorized, true) - var execution graphresolver.RecipeExecution - if config.MaterializeReceipt != nil { - execution, err = config.MaterializeReceipt(c.Context(), receipt, bindings) - } else if materialize != nil { - execution, err = materialize(c.Context(), receipt.Bundle, bindings) - } else { - err = fmt.Errorf("repository V2 materialization is not configured") - } - if err != nil { - return c.Status(http.StatusUnprocessableEntity).JSON(fiber.Map{"error": fmt.Sprintf("materialize repository V2 workspace: %v", err)}) - } - if err := verifyQueryableOutputs(receipt.Bundle, execution); err != nil { - return c.Status(http.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error(), "executionId": execution.ID}) - } - materializations := explorerMaterializations(receipt.Bundle, execution) - dataset := datasetMetadataFromExecution(receipt.Bundle, generation, receipt.ResolvedSchemaDigest, execution) - publication := explorer.PublicationMetadata{State: string(explorer.RevisionReady), Generation: generation, ExecutionID: execution.ID, UpdatedAt: time.Now().UTC()} - owner, revision, err := explorers.UpsertRepositoryV2(c.Context(), *receipt, commit, subjectFromFiber(c), materializations, dataset, publication) - if err != nil { - return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": fmt.Sprintf("persist Explorer lifecycle V2: %v", err)}) - } - if owner.ManagementMode != explorer.ManagementRepository { - return c.Status(http.StatusConflict).JSON(fiber.Map{"error": "default Explorer has invalid management mode"}) - } - if config.ActivateRelease == nil { - return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "release activation is not configured", "executionId": execution.ID}) - } - if err := config.ActivateRelease(c.Context(), projectid.Legacy(project), generation, selectorsForBundle(receipt.Bundle)); err != nil { - _, _ = explorers.FailRevision(c.Context(), revision.ID, []explorer.Diagnostic{{Severity: "ERROR", Code: "RELEASE_ACTIVATION_FAILED", Message: err.Error(), Retryable: true}}) - return c.Status(http.StatusConflict).JSON(fiber.Map{"error": fmt.Sprintf("activate published ExplorerConfigV2: %v", err), "executionId": execution.ID}) - } - if err := explorers.ActivateRepositoryGeneration(c.Context(), project, generation, revision.ID); err != nil { - return c.Status(http.StatusConflict).JSON(fiber.Map{"error": fmt.Sprintf("activate ExplorerConfigV2: %v", err), "executionId": execution.ID}) - } - publication.State = string(explorer.RevisionActive) - publication.RevisionID = revision.ID - publication.UpdatedAt = time.Now().UTC() - if _, err := explorers.SaveRepositoryConfig(c.Context(), explorer.RepositoryConfig{Project: project, Config: append([]byte(nil), receipt.CompiledConfig...), Workspace: append([]byte(nil), receipt.NormalizedBundle...), ConfigDigest: receipt.IntentDigest, IntentDigest: receipt.IntentDigest, CompilationReceiptID: receipt.ID, PublicOutputContract: append([]byte(nil), receipt.PublicOutputContract...), ActiveRevisionID: revision.ID, DraftVersion: owner.DraftVersion, SourceGeneration: generation, SourceCommit: commit, ExecutionID: execution.ID, Materializations: materializations, Dataset: dataset, Publication: publication}); err != nil { - return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": fmt.Sprintf("persist ExplorerConfigV2: %v", err)}) - } - return c.Status(http.StatusOK).JSON(fiber.Map{"project": project, "generation": generation, "explorerId": "default", "receiptId": receipt.ID, "revisionId": revision.ID, "executionId": execution.ID, "recipe": receipt.Bundle.Name, "translationVersion": receipt.Bundle.TranslationVersion, "activated": true}) + application, _ := lifecycle.New(explorers, config) + return &explorerHTTPHandlers{ + authorizer: authorizer, + authorizeRead: authorizeRead, + application: application, } - handlers.getRepositoryConfig = func(c fiber.Ctx) error { - project := explorerProjectParam(c) - principal, _ := c.Locals("principal").(*authscope.Principal) - if err := authorizeRead(c.Context(), principal, project); err != nil { - return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "forbidden"}) - } - value, err := explorers.RepositoryConfig(c.Context(), project) - if err == explorer.ErrNotFound { - return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "repository ExplorerConfigV2 not found"}) - } - if err != nil { - return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) - } - return c.JSON(value) - } - if len(lifecycle) == 0 { - lifecycle = []ExplorerV2LifecycleConfig{{Materialize: materialize}} - } - handlers.lifecycle = newExplorerLifecycleHandlers(authorizer, authorizeRead, explorers, lifecycle[0]) - handlers.authoring = newExplorerAuthoringHandlers(authorizer, authorizeRead, explorers, lifecycle[0]) - return handlers -} - -func explorerMaterializations(bundle recipe.Bundle, execution graphresolver.RecipeExecution) []explorer.Materialization { - out := make([]explorer.Materialization, 0, len(execution.Outputs)) - for _, output := range execution.Outputs { - out = append(out, explorer.Materialization{OutputID: output.Name, Output: output.Name, MaterializationID: execution.ID, Selector: explorerOutputSelector(bundle, output.Name), Columns: output.Columns}) - } - return out -} - -func datasetMetadataFromExecution(bundle recipe.Bundle, generation, schemaDigest string, execution graphresolver.RecipeExecution) explorer.DatasetMetadata { - outputs := make([]explorer.DatasetOutput, 0, len(execution.Outputs)) - for _, output := range execution.Outputs { - state := strings.ToUpper(output.State) - outputs = append(outputs, explorer.DatasetOutput{ - Name: output.Name, State: state, - Queryable: state == "PUBLISHED" || state == "READY" || state == "ACTIVE", - Selector: explorerOutputSelector(bundle, output.Name), - Columns: output.Columns, - }) - } - return explorer.DatasetMetadata{Generation: generation, SchemaDigest: schemaDigest, Outputs: outputs} -} - -func explorerOutputSelector(bundle recipe.Bundle, output string) *dataset.DataframeSelector { - selector := dataset.DataframeSelector{Recipe: bundle.Name, TranslationVersion: bundle.TranslationVersion, Output: output} - return &selector } diff --git a/internal/server/explorer_config_route_test.go b/internal/server/explorer_config_route_test.go index 09f4790f..eb96b5f0 100644 --- a/internal/server/explorer_config_route_test.go +++ b/internal/server/explorer_config_route_test.go @@ -2,20 +2,19 @@ package server import ( "context" - "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/dataframe/publication" "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/capability" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/gofiber/fiber/v3" ) @@ -26,11 +25,11 @@ func TestRepositoryDeploymentPersistsExecutableDataframeSelectors(t *testing.T) t.Fatal(err) } snapshot := testAuthoringV2CapabilitySnapshot() - materialize := func(_ context.Context, bundle recipe.Bundle, _ recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) { + materialize := func(_ context.Context, bundle recipe.Bundle, _ recipe.RuntimeBindings) (lifecycle.Execution, error) { if bundle.Name != "native-test" { t.Fatalf("materialized identity = %s/%s", bundle.Name, bundle.TranslationVersion) } - return graphresolver.RecipeExecution{ID: "execution-a", SourceGeneration: "generation-a", State: "PUBLISHED", Outputs: []graphresolver.RecipeExecutionOutput{{Name: "patients", State: "PUBLISHED", Columns: []publication.PhysicalColumn{{Name: "c_patient", LogicalType: "string", ClickHouse: "String"}}}}}, nil + return lifecycle.Execution{ID: "execution-a", SourceGeneration: "generation-a", State: "PUBLISHED", Outputs: []lifecycle.ExecutionOutput{{Name: "patients", State: "PUBLISHED", Columns: []publication.PhysicalColumn{{Name: "c_patient", LogicalType: "string", ClickHouse: "String"}}}}}, nil } activationCount := 0 activateRelease := func(_ context.Context, project, generation string, selectors []dataset.DataframeSelector) error { @@ -41,18 +40,24 @@ func TestRepositoryDeploymentPersistsExecutableDataframeSelectors(t *testing.T) return nil } app := fiber.New() - config := ExplorerV2LifecycleConfig{ - Capability: func(context.Context, string, string, string) (capability.Snapshot, error) { return snapshot, nil }, - CapabilityToken: func(context.Context, string, string) (capability.Snapshot, error) { return snapshot, nil }, - AuthorizedCapabilityCompile: func(context.Context, string, string) (AuthorizedCapability, error) { - return AuthorizedCapability{Snapshot: snapshot, Scope: authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}}, nil + config := lifecycle.Config{ + Capability: lifecycle.CapabilityResolver{ + Current: func(context.Context, string, string, string) (capability.Snapshot, error) { return snapshot, nil }, + Token: func(context.Context, string, string) (capability.Snapshot, error) { return snapshot, nil }, + ForCompilation: func(context.Context, string, string) (lifecycle.AuthorizedCapability, error) { + return lifecycle.AuthorizedCapability{Snapshot: snapshot, Scope: authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}}, nil + }, + Catalog: authoringV2Catalog, }, - CompileReceipt: func(ctx context.Context, request ExplorerV2ReceiptCompileRequest) (*explorer.CompilationReceipt, error) { + CompileReceipt: func(ctx context.Context, request lifecycle.CompileReceiptRequest) (*explorer.CompilationReceipt, error) { return persistTestNativeReceipt(ctx, t, service, request, snapshot) }, + MaterializeReceipt: func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { + return materialize(ctx, receipt.Bundle, bindings) + }, ActivateRelease: activateRelease, } - registerTestExplorerRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, materialize, config) + registerGeneratedExplorerTestRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, config) request := requestJSONWithHeaders(t, app, http.MethodPost, "/api/v1/projects/project-a/generations/generation-a/explorer-config", string(baselineExplorerWorkspaceV2()), map[string]string{"X-Loom-Source-Commit": "commit-a"}) if request.StatusCode != http.StatusOK { @@ -65,23 +70,19 @@ func TestRepositoryDeploymentPersistsExecutableDataframeSelectors(t *testing.T) if activationCount != 2 { t.Fatalf("release activation count = %d, want 2", activationCount) } - state, err := service.Get(context.Background(), "project-a", "default") + active, err := service.ActiveRevision(context.Background(), "project-a", "default") if err != nil { t.Fatal(err) } - if len(state.Dataset.Outputs) != 1 || state.Dataset.Outputs[0].Selector == nil { - t.Fatalf("dataset selector missing: %#v", state.Dataset.Outputs) + if len(active.Dataset.Outputs) != 1 || active.Dataset.Outputs[0].Selector == nil { + t.Fatalf("dataset selector missing: %#v", active.Dataset.Outputs) } - selector := state.Dataset.Outputs[0].Selector + selector := active.Dataset.Outputs[0].Selector if selector.Recipe != "native-test" || selector.Output != "patients" { t.Fatalf("dataset selector = %#v", selector) } - if len(state.Materializations) != 1 || state.Materializations[0].Selector == nil || *state.Materializations[0].Selector != *selector { - t.Fatalf("materialization selector = %#v", state.Materializations) - } - active, err := service.ActiveRevision(context.Background(), "project-a", "default") - if err != nil { - t.Fatal(err) + if len(active.Materializations) != 1 || active.Materializations[0].Selector == nil || *active.Materializations[0].Selector != *selector { + t.Fatalf("materialization selector = %#v", active.Materializations) } if len(active.AuthoringBundle) == 0 || active.CompilationReceiptID == "" || len(active.PublicOutputContract) == 0 { t.Fatalf("active repository revision lost V2 artifacts: %#v", active) @@ -90,19 +91,6 @@ func TestRepositoryDeploymentPersistsExecutableDataframeSelectors(t *testing.T) if builder.StatusCode != http.StatusOK || !strings.Contains(builder.Body, `"workspace"`) { t.Fatalf("builder reload status=%d body=%s", builder.StatusCode, builder.Body) } - draftBody := `{"workspace":` + string(baselineExplorerWorkspaceV2()) + `,"snapshotToken":"` + snapshot.Token + `","expectedDraftVersion":` + fmt.Sprint(state.DraftVersion) + `,"expectedDraftDigest":"` + state.DraftDigest + `"}` - draft := requestJSON(t, app, http.MethodPut, "/api/v1/projects/project-a/explorers/default/authoring/v2/draft", draftBody) - if draft.StatusCode != http.StatusOK || !strings.Contains(draft.Body, `"draftDigest":"sha256:`) { - t.Fatalf("save draft status=%d body=%s", draft.StatusCode, draft.Body) - } - stale := requestJSON(t, app, http.MethodPut, "/api/v1/projects/project-a/explorers/default/authoring/v2/draft", draftBody) - if stale.StatusCode != http.StatusConflict || !strings.Contains(stale.Body, `"code":"DRAFT_CONFLICT"`) { - t.Fatalf("stale draft status=%d body=%s", stale.StatusCode, stale.Body) - } - exported := requestJSON(t, app, http.MethodGet, "/api/v1/projects/project-a/explorers/default/authoring/v2/export", "") - if exported.StatusCode != http.StatusOK || exported.Body != string(baselineExplorerWorkspaceV2()) { - t.Fatalf("export status=%d body=%s", exported.StatusCode, exported.Body) - } viewer := requestJSON(t, app, http.MethodGet, "/api/v1/projects/project-a/explorers/default", "") if viewer.StatusCode != http.StatusOK || !strings.Contains(viewer.Body, `"label":"Patient ID"`) { t.Fatalf("viewer state status=%d body=%s", viewer.StatusCode, viewer.Body) diff --git a/internal/server/explorer_lifecycle_helpers.go b/internal/server/explorer_lifecycle_helpers.go index a52bf47b..ed57e221 100644 --- a/internal/server/explorer_lifecycle_helpers.go +++ b/internal/server/explorer_lifecycle_helpers.go @@ -5,51 +5,78 @@ import ( "encoding/hex" "encoding/json" "fmt" - "strings" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" - "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" - "github.com/calypr/loom/internal/dataset" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/compiler/lower" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" ) -func selectorsForBundle(bundle recipe.Bundle) []dataset.DataframeSelector { - selectors := make([]dataset.DataframeSelector, 0, len(bundle.Outputs)) - for _, output := range bundle.Outputs { - selectors = append(selectors, dataset.DataframeSelector{Recipe: bundle.Name, TranslationVersion: bundle.TranslationVersion, Output: output.Name}) - } - return selectors -} - -func verifyQueryableOutputs(bundle recipe.Bundle, execution graphresolver.RecipeExecution) error { - states := map[string]string{} - for _, output := range execution.Outputs { - states[output.Name] = strings.ToUpper(output.State) - } - for _, output := range bundle.Outputs { - state := states[output.Name] - if state != "PUBLISHED" && state != "READY" && state != "ACTIVE" { - return fmt.Errorf("output %q is not queryable (state %q)", output.Name, state) - } - } - return nil -} - -func resolvedOutputFingerprints(resolved engine.Resolved) map[string]string { +func resolvedOutputArtifacts(resolved dataframeexecution.Resolved) (map[string]string, map[string]map[string]string, error) { result := make(map[string]string, len(resolved.Compiled.Outputs)) + provenance := make(map[string]map[string]string, len(resolved.Compiled.Outputs)) for _, output := range resolved.Compiled.Outputs { + columns := make([]struct { + Name, SemanticPath, Kind, Cardinality string + Nullable, Internal, Identity bool + }, 0, len(output.OutputSchema)) + provenance[output.Name] = make(map[string]string, len(output.OutputSchema)) + for _, column := range output.OutputSchema { + columns = append(columns, struct { + Name, SemanticPath, Kind, Cardinality string + Nullable, Internal, Identity bool + }{column.Name, column.SemanticPath, column.Kind, column.Cardinality, column.Nullable, column.Internal, column.Identity}) + value := "EXPLICIT" + if column.Discovered { + value = "DISCOVERED" + } + provenance[output.Name][column.Name] = value + } + dynamic := make([]struct { + Name, SemanticPath, DynamicName, SourceKey, ValueType string + AllowUnknownKeys bool + }, 0, len(output.DynamicColumns)) + for _, column := range output.DynamicColumns { + dynamic = append(dynamic, struct { + Name, SemanticPath, DynamicName, SourceKey, ValueType string + AllowUnknownKeys bool + }{column.Name, column.SemanticPath, column.DynamicName, column.SourceKey, column.ValueType, column.AllowUnknownKeys}) + } payload, err := json.Marshal(struct { - Version int `json:"version"` - Name string `json:"name"` - Scope string `json:"scope"` - Generation string `json:"generation"` - Output any `json:"output"` - }{Version: 1, Name: output.Name, Scope: resolved.Semantic.ScopeDigest, Generation: resolved.Compiled.SourceGeneration, Output: output}) + Version, CompilerVersion int + Name, RootResourceType, RowGrain string + Scope, Generation string + Columns []string + OutputSchema any + RowIdentity any + DynamicColumns any + Plan ir.PhysicalPlan + }{2, resolved.Compiled.Version, output.Name, output.RootResourceType, string(output.RowGrain), resolved.Semantic.ScopeDigest, resolved.Compiled.SourceGeneration, append([]string(nil), output.Columns...), columns, output.RowIdentity, dynamic, ir.CanonicalExecutionPhysicalPlan(output.Plan)}) if err != nil { - continue + return nil, nil, fmt.Errorf("fingerprint output %q: %w", output.Name, err) } sum := sha256.Sum256(payload) result[output.Name] = hex.EncodeToString(sum[:]) } - return result + return result, provenance, nil +} + +func applyReceiptColumnProvenance(output *lower.CompiledRecipeOutput, values map[string]string) error { + if output == nil || len(values) != len(output.OutputSchema) { + return fmt.Errorf("output column provenance set changed") + } + for index := range output.OutputSchema { + value, ok := values[output.OutputSchema[index].Name] + if !ok || (value != "EXPLICIT" && value != "DISCOVERED") { + return fmt.Errorf("output column provenance missing for %q", output.OutputSchema[index].Name) + } + output.OutputSchema[index].Discovered = value == "DISCOVERED" + } + for index := range output.DynamicColumns { + value, ok := values[output.DynamicColumns[index].Name] + if !ok { + return fmt.Errorf("dynamic column provenance missing for %q", output.DynamicColumns[index].Name) + } + output.DynamicColumns[index].Discovered = value == "DISCOVERED" + } + return nil } diff --git a/internal/server/explorer_lifecycle_types.go b/internal/server/explorer_lifecycle_types.go deleted file mode 100644 index 8be75728..00000000 --- a/internal/server/explorer_lifecycle_types.go +++ /dev/null @@ -1,48 +0,0 @@ -package server - -import ( - "context" - "log/slog" - - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" - "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" - "github.com/calypr/loom/internal/dataset" - "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/authoringv2" -) - -type ExplorerV2Previewer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (map[string][]map[string]any, error) -type ExplorerV2Materializer = graphresolver.ExplorerBundleMaterializer - -type ExplorerV2ReceiptCompileRequest struct { - Project string - ExplorerID string - Workspace authoringv2.Workspace - SnapshotToken string - RequestID string - Authorized AuthorizedCapability -} - -type ExplorerV2ReceiptCompiler func(context.Context, ExplorerV2ReceiptCompileRequest) (*explorer.CompilationReceipt, error) -type ExplorerV2ReceiptReader func(context.Context, string, string, string) (*explorer.CompilationReceipt, error) -type ExplorerV2ReceiptPreviewer func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (engine.PreviewSummary, error) -type ExplorerV2ReceiptMaterializer func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) -type ExplorerV2GenerationValidator func(context.Context, string, string) error -type ExplorerV2ReleaseActivator func(context.Context, string, string, []dataset.DataframeSelector) error - -type ExplorerV2LifecycleConfig struct { - CompileReceipt ExplorerV2ReceiptCompiler - Capability ExplorerCapabilityReader - CapabilityToken ExplorerCapabilityTokenReader - AuthorizedCapabilityCompile ExplorerAuthorizedCapabilityCompilationReader - AuthorizedCapabilityExecution ExplorerAuthorizedCapabilityExecutionReader - Preview ExplorerV2Previewer - PreviewReceipt ExplorerV2ReceiptPreviewer - Materialize ExplorerV2Materializer - MaterializeReceipt ExplorerV2ReceiptMaterializer - ReceiptLookup ExplorerV2ReceiptReader - Logger *slog.Logger - ValidateReleaseGeneration ExplorerV2GenerationValidator - ActivateRelease ExplorerV2ReleaseActivator -} diff --git a/internal/server/explorer_lifecycle_wire.go b/internal/server/explorer_lifecycle_wire.go new file mode 100644 index 00000000..90448032 --- /dev/null +++ b/internal/server/explorer_lifecycle_wire.go @@ -0,0 +1,18 @@ +package server + +import ( + "context" + + httpapi "github.com/calypr/loom/internal/api/http" + "github.com/calypr/loom/internal/authscope" +) + +func requestIDFromContext(ctx context.Context) string { return httpapi.RequestIDFromContext(ctx) } + +func subjectFromContext(ctx context.Context) string { + principal, _ := authscope.PrincipalFromContext(ctx) + if principal == nil { + return "" + } + return principal.Subject +} diff --git a/internal/server/explorer_preview_response.go b/internal/server/explorer_preview_response.go index 3a0b3eb2..e8bcce6e 100644 --- a/internal/server/explorer_preview_response.go +++ b/internal/server/explorer_preview_response.go @@ -12,7 +12,6 @@ import ( explorerv2api "github.com/calypr/loom/generated/loomapi" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" "github.com/calypr/loom/internal/explorer" - "github.com/gofiber/fiber/v3" ) const ( @@ -22,46 +21,67 @@ const ( var ErrPreviewResponseTooLarge = errors.New("PREVIEW_RESPONSE_TOO_LARGE") -type receiptPreviewResolutionError struct{ Err error } +type receiptPreviewResolutionError struct { + ReceiptID string + Err error +} func (e *receiptPreviewResolutionError) Error() string { return e.Err.Error() } func (e *receiptPreviewResolutionError) Unwrap() error { return e.Err } -func previewRouteFailure(c fiber.Ctx, err error) error { +func classifyReceiptPreviewResolutionError(receiptID string, err error) error { + var mismatch *receiptContractMismatch + if errors.As(err, &mismatch) || errors.Is(err, explorer.ErrReceiptRecompileRequired) { + return &receiptPreviewResolutionError{ReceiptID: receiptID, Err: err} + } + return err +} + +// previewRouteError maps execution failures to the stable authoring error +// contract without requiring a Fiber context. The generated OpenAPI adapter +// uses this form directly. +func previewRouteError(err error) error { if errors.Is(err, ErrPreviewResponseTooLarge) { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 413, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "RESPONSE_TOO_LARGE", Message: "preview response exceeds the maximum size"}, Cause: err}) + return &explorer.AuthoringError{Status: 413, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "RESPONSE_TOO_LARGE", Message: "preview response exceeds the maximum size"}, Cause: err} } if errors.Is(err, context.DeadlineExceeded) { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 504, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_TIMEOUT", Message: "preview exceeded its execution deadline"}, Cause: err}) + return &explorer.AuthoringError{Status: 504, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_TIMEOUT", Message: "preview exceeded its execution deadline"}, Cause: err} } if errors.Is(err, context.Canceled) { - return authoringHTTPError(c, &explorer.AuthoringError{Status: 499, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "CLIENT_CANCELED", Message: "preview request was canceled"}, Cause: err}) + return &explorer.AuthoringError{Status: 499, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "CLIENT_CANCELED", Message: "preview request was canceled"}, Cause: err} } if userErr, ok := dataframeerrors.AsUserError(err); ok { switch userErr.Code() { case string(dataframeerrors.CodeClientCanceled): - return authoringHTTPError(c, &explorer.AuthoringError{Status: 499, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "CLIENT_CANCELED", Message: "preview request was canceled"}, Cause: err}) + return &explorer.AuthoringError{Status: 499, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "CLIENT_CANCELED", Message: "preview request was canceled"}, Cause: err} case string(dataframeerrors.CodePreviewTimeout): - return authoringHTTPError(c, &explorer.AuthoringError{Status: 504, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_TIMEOUT", Message: "preview exceeded its execution deadline"}, Cause: err}) + return &explorer.AuthoringError{Status: 504, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_TIMEOUT", Message: "preview exceeded its execution deadline"}, Cause: err} case string(dataframeerrors.CodePreviewResponseTooLarge): - return authoringHTTPError(c, &explorer.AuthoringError{Status: 413, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_RESPONSE_TOO_LARGE", Message: dataframeerrors.PublicMessage(err)}, Cause: err}) + return &explorer.AuthoringError{Status: 413, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_RESPONSE_TOO_LARGE", Message: dataframeerrors.PublicMessage(err)}, Cause: err} case string(dataframeerrors.CodePlanTooExpensive): - return authoringHTTPError(c, &explorer.AuthoringError{Status: 429, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: userErr.Code(), Message: dataframeerrors.PublicMessage(err)}, Cause: err}) + return &explorer.AuthoringError{Status: 429, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: userErr.Code(), Message: dataframeerrors.PublicMessage(err)}, Cause: err} case string(dataframeerrors.CodeBackendUnavailable), string(dataframeerrors.CodeReceiptStoreUnavailable): - return authoringHTTPError(c, &explorer.AuthoringError{Status: 503, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: userErr.Code(), Message: dataframeerrors.PublicMessage(err)}, Cause: err}) + return &explorer.AuthoringError{Status: 503, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: userErr.Code(), Message: dataframeerrors.PublicMessage(err)}, Cause: err} + case string(dataframeerrors.CodeQueryMemoryLimitExceeded), string(dataframeerrors.CodeQueryResourceLimitExceeded), string(dataframeerrors.CodeQueryBackendOutOfMemory): + return &explorer.AuthoringError{Status: 503, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: userErr.Code(), Message: dataframeerrors.PublicMessage(err), Details: userErr.Details()}, Cause: err} case string(dataframeerrors.CodeRecipeContractViolation), string(dataframeerrors.CodeDynamicSchemaDrift): - return authoringHTTPError(c, receiptPreviewConflict(err)) + return receiptPreviewConflict(err) default: - return authoringHTTPError(c, &explorer.AuthoringError{Status: 500, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_FAILED", Message: "receipt preview failed"}, Cause: err}) + return &explorer.AuthoringError{Status: 500, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_FAILED", Message: "receipt preview failed"}, Cause: err} } } if errors.As(err, new(*receiptPreviewResolutionError)) { - return authoringHTTPError(c, receiptPreviewConflict(err)) + return receiptPreviewConflict(err) } - return authoringHTTPError(c, &explorer.AuthoringError{Status: 500, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_FAILED", Message: "receipt preview failed"}, Cause: err}) + return &explorer.AuthoringError{Status: 500, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "preview", Code: "PREVIEW_FAILED", Message: "receipt preview failed"}, Cause: err} } func receiptPreviewConflict(cause error) error { + receiptID := "" + var resolution *receiptPreviewResolutionError + if errors.As(cause, &resolution) { + receiptID = resolution.ReceiptID + } return &explorer.AuthoringError{ Status: http.StatusConflict, Diagnostic: explorer.AuthoringDiagnostic{ @@ -69,6 +89,7 @@ func receiptPreviewConflict(cause error) error { Stage: "preview", Code: "RECEIPT_RECOMPILE_REQUIRED", Message: "receipt deterministic lowering no longer matches the stored artifact", + Details: receiptMismatchDetails(receiptID, cause), }, Cause: cause, } @@ -104,83 +125,6 @@ func (e *previewResponseTooLargeError) Error() string { func (e *previewResponseTooLargeError) Unwrap() error { return ErrPreviewResponseTooLarge } -// encodeExplorerPreviewResponse constructs the complete response before it is -// handed to Fiber. A capped writer makes overflow recoverable without ever -// sending a truncated JSON document to the client. -func encodeExplorerPreviewResponse(receipt *explorer.CompilationReceipt, outputID string, columns []explorer.EmittedColumn, rows []map[string]any, limit int) ([]byte, error) { - if limit <= 0 { - limit = maxExplorerPreviewResponseBytes - } - var out cappedPreviewBuffer - out.limit = limit - write := func(raw []byte) error { - if _, err := out.Write(raw); err != nil { - return err - } - return nil - } - writeString := func(value string) error { - raw, err := json.Marshal(value) - if err != nil { - return err - } - return write(raw) - } - if err := write([]byte(`{"apiVersion":"loom.calypr.org/explorer-authoring/v2","kind":"ExplorerBuilderPreview","receiptId":`)); err != nil { - return nil, err - } - if err := writeString(receipt.ID); err != nil { - return nil, err - } - if err := write([]byte(`,"outputId":`)); err != nil { - return nil, err - } - if err := writeString(outputID); err != nil { - return nil, err - } - encodedColumns, err := json.Marshal(v2EmissionColumns(columns)) - if err != nil { - return nil, err - } - if err := write([]byte(`,"columns":`)); err != nil { - return nil, err - } - if err := write(encodedColumns); err != nil { - return nil, err - } - if err := write([]byte(`,"rows":[`)); err != nil { - return nil, err - } - for index, row := range rows { - if index > 0 { - if err := write([]byte(",")); err != nil { - return nil, err - } - } - encodedRow, err := json.Marshal(row) - if err != nil { - return nil, err - } - if err := write(encodedRow); err != nil { - return nil, err - } - } - if err := write([]byte(`],"rowCount":`)); err != nil { - return nil, err - } - count, err := json.Marshal(len(rows)) - if err != nil { - return nil, err - } - if err := write(count); err != nil { - return nil, err - } - if err := write([]byte(`,"diagnostics":[]}`)); err != nil { - return nil, err - } - return out.Bytes(), nil -} - type previewResponseEncoder struct { out cappedPreviewBuffer rowCount int diff --git a/internal/server/explorer_preview_response_test.go b/internal/server/explorer_preview_response_test.go index 3e6cb85f..000faeb5 100644 --- a/internal/server/explorer_preview_response_test.go +++ b/internal/server/explorer_preview_response_test.go @@ -10,7 +10,6 @@ import ( dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" "github.com/calypr/loom/internal/explorer" - "github.com/gofiber/fiber/v3" ) func TestPreviewResponseEncoderProducesAtomicContract(t *testing.T) { @@ -47,16 +46,12 @@ func TestPreviewResponseEncoderRejectsOverflowWithoutResult(t *testing.T) { if err != nil { t.Fatal(err) } - err = encoder.Visit(map[string]any{"value": strings.Repeat("x", 2048)}) - if !errors.Is(err, ErrPreviewResponseTooLarge) { + if err := encoder.Visit(map[string]any{"value": strings.Repeat("x", 2048)}); !errors.Is(err, ErrPreviewResponseTooLarge) { t.Fatalf("Visit error = %v, want %v", err, ErrPreviewResponseTooLarge) } - if _, err := encodeExplorerPreviewResponse(&explorer.CompilationReceipt{ID: "receipt"}, "output", nil, []map[string]any{{"value": strings.Repeat("x", 2048)}}, 1024); !errors.Is(err, ErrPreviewResponseTooLarge) { - t.Fatalf("legacy encode error = %v, want %v", err, ErrPreviewResponseTooLarge) - } } -func TestPreviewRouteFailurePreservesStableClassifications(t *testing.T) { +func TestPreviewErrorPreservesStableClassifications(t *testing.T) { tests := []struct { name string err error @@ -68,20 +63,44 @@ func TestPreviewRouteFailurePreservesStableClassifications(t *testing.T) { {"oversized", &previewResponseTooLargeError{Limit: 32}, http.StatusRequestEntityTooLarge, "RESPONSE_TOO_LARGE"}, {"plan", dataframeerrors.NewError(dataframeerrors.CodePlanTooExpensive, "private"), http.StatusTooManyRequests, "PLAN_TOO_EXPENSIVE"}, {"backend", dataframeerrors.NewError(dataframeerrors.CodeBackendUnavailable, "private", dataframeerrors.WithRetryable(true)), http.StatusServiceUnavailable, "BACKEND_UNAVAILABLE"}, - {"receipt", &receiptPreviewResolutionError{Err: errors.New("private")}, http.StatusConflict, "RECEIPT_RECOMPILE_REQUIRED"}, + {"memory-limit", dataframeerrors.NewError(dataframeerrors.CodeQueryMemoryLimitExceeded, "private"), http.StatusServiceUnavailable, "QUERY_MEMORY_LIMIT_EXCEEDED"}, + {"resource-limit", dataframeerrors.NewError(dataframeerrors.CodeQueryResourceLimitExceeded, "private"), http.StatusServiceUnavailable, "QUERY_RESOURCE_LIMIT_EXCEEDED"}, + {"out-of-memory", dataframeerrors.NewError(dataframeerrors.CodeQueryBackendOutOfMemory, "private"), http.StatusServiceUnavailable, "QUERY_BACKEND_OUT_OF_MEMORY"}, + {"receipt", &receiptPreviewResolutionError{ReceiptID: "receipt-1", Err: contractMismatch("output_execution", "patients", "private-expected", "private-actual")}, http.StatusConflict, "RECEIPT_RECOMPILE_REQUIRED"}, {"unknown", errors.New("private"), http.StatusInternalServerError, "PREVIEW_FAILED"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - app := fiber.New() - app.Get("/", func(c fiber.Ctx) error { return previewRouteFailure(c, test.err) }) - response := requestJSON(t, app, http.MethodGet, "/", "") - if response.StatusCode != test.status || !strings.Contains(response.Body, `"code":"`+test.code+`"`) { - t.Fatalf("status=%d body=%s, want status=%d code=%s", response.StatusCode, response.Body, test.status, test.code) - } - if strings.Contains(response.Body, "private") { - t.Fatalf("private cause leaked: %s", response.Body) + var got *explorer.AuthoringError + if !errors.As(previewRouteError(test.err), &got) || got.Status != test.status || got.Diagnostic.Code != test.code { + t.Fatalf("error=%#v, want status=%d code=%s", got, test.status, test.code) } }) } } + +func TestClassifyReceiptPreviewResolutionErrorOnlyMarksContractFailuresForRecompile(t *testing.T) { + mismatch := classifyReceiptPreviewResolutionError("receipt-1", contractMismatch("output_execution", "patients", "expected", "actual")) + var resolution *receiptPreviewResolutionError + if !errors.As(mismatch, &resolution) || resolution.ReceiptID != "receipt-1" { + t.Fatalf("mismatch classification = %#v", mismatch) + } + var authoring *explorer.AuthoringError + if !errors.As(previewRouteError(mismatch), &authoring) || authoring.Status != http.StatusConflict { + t.Fatalf("mismatch route error = %#v", authoring) + } + if got := authoring.Diagnostic.Details["receiptId"]; got != "receipt-1" { + t.Fatalf("receiptId detail = %#v", got) + } + if got := authoring.Diagnostic.Details["outputId"]; got != "patients" { + t.Fatalf("outputId detail = %#v", got) + } + + compileFailure := classifyReceiptPreviewResolutionError("receipt-1", errors.New("compiler failed")) + if errors.As(compileFailure, &resolution) { + t.Fatalf("ordinary compiler failure was classified as a receipt mismatch: %v", compileFailure) + } + if !errors.As(previewRouteError(compileFailure), &authoring) || authoring.Status != http.StatusInternalServerError || authoring.Diagnostic.Code != "PREVIEW_FAILED" { + t.Fatalf("compiler route error = %#v", authoring) + } +} diff --git a/internal/server/explorer_preview_security.go b/internal/server/explorer_preview_security.go index 9284aa9c..a4f7ac85 100644 --- a/internal/server/explorer_preview_security.go +++ b/internal/server/explorer_preview_security.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataframe/recipe/engine" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/explorer" ) @@ -16,40 +16,6 @@ import ( // existing receipt-input conflict diagnostic. var ErrReceiptExecutionContract = errors.New("receipt execution contract mismatch") -// validateAuthorizedReceiptExecution proves that the capability and scope -// supplied for execution are exactly the inputs frozen in the receipt. It is -// intentionally stricter than comparing only the snapshot token: the scope -// mode is authoritative, so a restricted-empty scope can never become the -// legacy empty-slice/unrestricted interpretation. -func validateAuthorizedReceiptExecution(receipt *explorer.CompilationReceipt, authorized AuthorizedCapability) error { - if receipt == nil { - return receiptExecutionContractError("receipt is required") - } - snapshot := authorized.Snapshot - if strings.TrimSpace(receipt.SnapshotToken) == "" || snapshot.Token != receipt.SnapshotToken { - return receiptExecutionContractError("capability token does not exactly match receipt token") - } - if err := snapshot.ValidateToken(receipt.SnapshotToken); err != nil { - return receiptExecutionContractError("capability token is invalid: %v", err) - } - if snapshot.Identity.Project != receipt.Project { - return receiptExecutionContractError("capability project changed") - } - if snapshot.Identity.Generation != receipt.SourceGeneration { - return receiptExecutionContractError("capability generation changed") - } - if snapshot.Identity.SchemaDigest != receipt.CapabilitySchemaDigest { - return receiptExecutionContractError("capability schema changed") - } - if snapshot.Identity.AuthorizationScopeDigest != receipt.AuthorizationScopeDigest { - return receiptExecutionContractError("capability scope digest changed") - } - if err := validateAuthorizedReadScope(authorized.Scope, receipt.AuthorizationScopeDigest); err != nil { - return err - } - return nil -} - // validateAuthorizedReadScope verifies both the digest and the explicit mode. // In particular, restricted + zero paths is a valid deny-all result, while an // empty mode is rejected because downstream legacy code may interpret it as @@ -71,36 +37,11 @@ func validateAuthorizedReadScope(scope authscope.ReadScope, expectedDigest strin return nil } -// validateReceiptOutputContract proves that a requested output is present in -// the persisted public contract, rather than merely in the executable recipe. -func validateReceiptOutputContract(receipt *explorer.CompilationReceipt, outputID string) error { - if receipt == nil { - return receiptExecutionContractError("receipt is required") - } - outputID = strings.TrimSpace(outputID) - if outputID == "" { - return receiptExecutionContractError("output is required") - } - contract, err := explorer.DecodePublicOutputContracts(receipt.PublicOutputContract) - if err != nil { - return receiptExecutionContractError("public output contract is invalid: %v", err) - } - if _, ok := contract.Output(outputID); !ok { - return receiptExecutionContractError("requested output %q is not in the receipt public contract", outputID) - } - for _, output := range receipt.Bundle.Outputs { - if output.Name == outputID { - return nil - } - } - return receiptExecutionContractError("requested output %q is not in the receipt recipe", outputID) -} - // validateReceiptEnginePublicColumns proves that the engine and receipt expose // exactly the same public columns. Column order is deliberately not compared: // the compiler owns execution order while the receipt's output contract owns // presentation order. Internal identity/provenance projections are excluded. -func validateReceiptEnginePublicColumns(receipt *explorer.CompilationReceipt, resolved engine.Resolved) error { +func validateReceiptEnginePublicColumns(receipt *explorer.CompilationReceipt, resolved dataframeexecution.Resolved) error { if receipt == nil { return receiptExecutionContractError("receipt is required") } diff --git a/internal/server/explorer_preview_security_test.go b/internal/server/explorer_preview_security_test.go index 059e5c71..3fc867d0 100644 --- a/internal/server/explorer_preview_security_test.go +++ b/internal/server/explorer_preview_security_test.go @@ -8,53 +8,12 @@ import ( "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/dataframe/compiler/lower" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/capability" ) -func TestValidateAuthorizedReceiptExecutionAcceptsExactRetainedGeneration(t *testing.T) { - scope := authscope.ReadScope{Mode: authscope.ReadScopeRestricted, AuthResourcePaths: []string{"path-a"}} - snapshot := testAuthorizedCapabilitySnapshot(t, "generation-old", scope) - receipt := testSecurityReceipt(t, snapshot, "patients", []string{"id"}) - if err := validateAuthorizedReceiptExecution(receipt, AuthorizedCapability{Snapshot: snapshot, Scope: scope}); err != nil { - t.Fatal(err) - } -} - -func TestValidateAuthorizedReceiptExecutionRejectsChangedTokenGenerationSchemaOrScope(t *testing.T) { - scope := authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted} - snapshot := testAuthorizedCapabilitySnapshot(t, "generation-a", scope) - receipt := testSecurityReceipt(t, snapshot, "patients", []string{"id"}) - cases := map[string]func(*explorer.CompilationReceipt, *capability.Snapshot, *authscope.ReadScope){ - "token": func(r *explorer.CompilationReceipt, _ *capability.Snapshot, _ *authscope.ReadScope) { - r.SnapshotToken = "different-token" - }, - "generation": func(_ *explorer.CompilationReceipt, s *capability.Snapshot, _ *authscope.ReadScope) { - s.Identity.Generation = "generation-b" - }, - "schema": func(_ *explorer.CompilationReceipt, s *capability.Snapshot, _ *authscope.ReadScope) { - s.Identity.SchemaDigest = "different-schema" - }, - "scope": func(_ *explorer.CompilationReceipt, _ *capability.Snapshot, s *authscope.ReadScope) { - s.Mode = authscope.ReadScopeRestricted - }, - } - for name, mutate := range cases { - t.Run(name, func(t *testing.T) { - candidateReceipt := *receipt - candidateSnapshot := snapshot.Clone() - candidateScope := scope.Clone() - mutate(&candidateReceipt, &candidateSnapshot, &candidateScope) - err := validateAuthorizedReceiptExecution(&candidateReceipt, AuthorizedCapability{Snapshot: candidateSnapshot, Scope: candidateScope}) - if !errors.Is(err, ErrReceiptExecutionContract) { - t.Fatalf("error=%v, want ErrReceiptExecutionContract", err) - } - }) - } -} - func TestValidateAuthorizedReadScopePreservesRestrictedEmpty(t *testing.T) { expected := explorerScopeDigest(authscope.ReadScope{Mode: authscope.ReadScopeRestricted}) if err := validateAuthorizedReadScope(authscope.ReadScope{Mode: authscope.ReadScopeRestricted}, expected); err != nil { @@ -68,39 +27,10 @@ func TestValidateAuthorizedReadScopePreservesRestrictedEmpty(t *testing.T) { } } -func TestValidateReceiptOutputContract(t *testing.T) { - snapshot := testAuthorizedCapabilitySnapshot(t, "generation-a", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) - receipt := testSecurityReceipt(t, snapshot, "patients", []string{"id"}) - if err := validateReceiptOutputContract(receipt, "patients"); err != nil { - t.Fatal(err) - } - contracts, err := explorer.DecodePublicOutputContracts(receipt.PublicOutputContract) - if err != nil { - t.Fatal(err) - } - contracts.Outputs = append(contracts.Outputs, explorer.PublicOutputContract{OutputID: "specimens", Columns: []explorer.PublicOutputColumn{}}) - receipt.PublicOutputContract, err = json.Marshal(contracts) - if err != nil { - t.Fatal(err) - } - receipt.Bundle.Outputs = append(receipt.Bundle.Outputs, recipe.Output{Name: "specimens", RootResourceType: "Specimen", RowGrain: "resource"}) - if err := validateReceiptOutputContract(receipt, "specimens"); err != nil { - t.Fatal(err) - } - if err := validateReceiptOutputContract(receipt, "observations"); !errors.Is(err, ErrReceiptExecutionContract) { - t.Fatalf("unknown output error=%v, want contract mismatch", err) - } - malformed := *receipt - malformed.PublicOutputContract = json.RawMessage(`{"outputs":[{"outputId":"observations","columns":[]}]}`) - if err := validateReceiptOutputContract(&malformed, "patients"); !errors.Is(err, ErrReceiptExecutionContract) { - t.Fatalf("forged output contract error=%v, want contract mismatch", err) - } -} - func TestValidateReceiptEnginePublicColumnsUsesExactPublicColumnSet(t *testing.T) { snapshot := testAuthorizedCapabilitySnapshot(t, "generation-a", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) receipt := testSecurityReceipt(t, snapshot, "patients", []string{"id", "name"}) - resolved := engine.Resolved{Compiled: lower.CompiledRecipe{Outputs: []lower.CompiledRecipeOutput{{Name: "patients", OutputSchema: []lower.CompiledOutputColumn{{Name: "id"}, {Name: "__loom_row_id", Internal: true}, {Name: "name"}}}}}} + resolved := dataframeexecution.Resolved{Compiled: lower.CompiledRecipe{Outputs: []lower.CompiledRecipeOutput{{Name: "patients", OutputSchema: []lower.CompiledOutputColumn{{Name: "id"}, {Name: "__loom_row_id", Internal: true}, {Name: "name"}}}}}} if err := validateReceiptEnginePublicColumns(receipt, resolved); err != nil { t.Fatal(err) } diff --git a/internal/server/explorer_publication.go b/internal/server/explorer_publication.go index 464d7872..facc57f3 100644 --- a/internal/server/explorer_publication.go +++ b/internal/server/explorer_publication.go @@ -5,29 +5,29 @@ import ( "fmt" "log/slog" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/dataframe/publication" materializationarango "github.com/calypr/loom/internal/dataframe/publication/arango" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/lifecycle" ) // explorerReceiptMaterializer keeps publication bound to the immutable // compilation receipt. The receipt is the only artifact crossing the HTTP to -// execution boundary; its resolved recipe is lowered in memory by the engine. -func explorerReceiptMaterializer(recipeEngine *engine.Engine, target publication.Target, registry *materializationarango.Registry, degradation error, logger *slog.Logger, batchRows, batchBytes int) ExplorerV2ReceiptMaterializer { - return func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) { +// execution boundary; its resolved recipe is lowered in memory by the execution package. +func explorerReceiptMaterializer(recipeEngine *dataframeexecution.Engine, target publication.Target, registry *materializationarango.Registry, degradation error, logger *slog.Logger, batchRows, batchBytes int) lifecycle.ReceiptMaterializer { + return func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { if receipt == nil { - return graphresolver.RecipeExecution{}, fmt.Errorf("compilation receipt is required") + return lifecycle.Execution{}, fmt.Errorf("compilation receipt is required") } if target == nil { - return graphresolver.RecipeExecution{}, fmt.Errorf("Explorer publication is unavailable: %w", degradation) + return lifecycle.Execution{}, fmt.Errorf("Explorer publication is unavailable: %w", degradation) } bindings.IncludeAuthResourcePath = true var identity publication.BundleIdentity - _, err := recipeEngine.MaterializeResolvedBundle(ctx, receipt.Bundle, bindings, func(run context.Context, full engine.Resolved) error { - if validationErr := validateReceiptResolution(receipt, full); validationErr != nil { + _, err := recipeEngine.MaterializeResolvedBundle(ctx, receipt.Bundle, bindings, func(run context.Context, full dataframeexecution.Resolved) error { + if validationErr := validateReceiptResolution(receipt, &full); validationErr != nil { return validationErr } var publishErr error @@ -35,49 +35,20 @@ func explorerReceiptMaterializer(recipeEngine *engine.Engine, target publication return publishErr }) if err != nil { - return graphresolver.RecipeExecution{}, err + return lifecycle.Execution{}, err } published, err := registry.FindExecutionByKey(ctx, identity.Key()) if err != nil { - return graphresolver.RecipeExecution{}, fmt.Errorf("load Explorer publication: %w", err) + return lifecycle.Execution{}, fmt.Errorf("load Explorer publication: %w", err) } - outputs := make([]graphresolver.RecipeExecutionOutput, 0, len(published.Outputs)) + outputs := make([]lifecycle.ExecutionOutput, 0, len(published.Outputs)) for _, output := range published.Outputs { count := int(output.RowCount) - outputs = append(outputs, graphresolver.RecipeExecutionOutput{Name: output.Name, State: string(output.State.Canonical()), RowCount: &count, Columns: append([]publication.PhysicalColumn(nil), output.Columns...)}) + outputs = append(outputs, lifecycle.ExecutionOutput{Name: output.Name, State: string(output.State.Canonical()), RowCount: &count, Columns: append([]publication.PhysicalColumn(nil), output.Columns...)}) } if logger != nil { logger.Info("Explorer receipt materialization complete", "project", bindings.Project, "execution", published.ID) } - return graphresolver.RecipeExecution{ID: published.ID, Name: receipt.Bundle.Name, RecipeDigest: published.RecipeDigest, ResolvedSchemaDigest: published.SchemaDigest, SourceGeneration: published.DatasetGeneration, State: string(published.State.Canonical()), Outputs: outputs}, nil - } -} - -func explorerBundleMaterializer(recipeEngine *engine.Engine, target publication.Target, registry *materializationarango.Registry, degradation error, logger *slog.Logger, batchRows, batchBytes int) graphresolver.ExplorerBundleMaterializer { - return func(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) { - if target == nil { - return graphresolver.RecipeExecution{}, fmt.Errorf("Explorer publication is unavailable: %w", degradation) - } - bindings.IncludeAuthResourcePath = true - var identity publication.BundleIdentity - _, err := recipeEngine.MaterializeBundle(ctx, bundle, bindings, func(run context.Context, full engine.Resolved) error { - var publishErr error - identity, publishErr = publishResolvedRecipe(run, recipeEngine, target, bundle.Name, bindings, full, batchRows, batchBytes) - return publishErr - }) - if err != nil { - return graphresolver.RecipeExecution{}, err - } - published, err := registry.FindExecutionByKey(ctx, identity.Key()) - if err != nil { - return graphresolver.RecipeExecution{}, fmt.Errorf("load Explorer publication: %w", err) - } - outputs := make([]graphresolver.RecipeExecutionOutput, 0, len(published.Outputs)) - for _, output := range published.Outputs { - count := int(output.RowCount) - outputs = append(outputs, graphresolver.RecipeExecutionOutput{Name: output.Name, State: string(output.State.Canonical()), RowCount: &count, Columns: append([]publication.PhysicalColumn(nil), output.Columns...)}) - } - logger.Info("Explorer materialization complete", "project", bindings.Project, "execution", published.ID) - return graphresolver.RecipeExecution{ID: published.ID, Name: bundle.Name, RecipeDigest: published.RecipeDigest, ResolvedSchemaDigest: published.SchemaDigest, SourceGeneration: published.DatasetGeneration, State: string(published.State.Canonical()), Outputs: outputs}, nil + return lifecycle.Execution{ID: published.ID, Name: receipt.Bundle.Name, RecipeDigest: published.RecipeDigest, ResolvedSchemaDigest: published.SchemaDigest, SourceGeneration: published.DatasetGeneration, State: string(published.State.Canonical()), Outputs: outputs}, nil } } diff --git a/internal/server/explorer_receipt_contract.go b/internal/server/explorer_receipt_contract.go index c7685485..92c3caa5 100644 --- a/internal/server/explorer_receipt_contract.go +++ b/internal/server/explorer_receipt_contract.go @@ -3,19 +3,144 @@ package server import ( "context" "encoding/json" + "errors" "fmt" + "log/slog" "sort" "strings" + "time" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/explorer/capability" explorercompilation "github.com/calypr/loom/internal/explorer/compilation" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/calypr/loom/internal/projectid" ) +func compileExplorerReceipt(ctx context.Context, request lifecycle.CompileReceiptRequest, capabilityResolver *explorerCapabilityResolver, recipeEngine *dataframeexecution.Engine, explorerService *explorer.Service, logger *slog.Logger) (*explorer.CompilationReceipt, error) { + started := time.Now() + authorized := request.Authorized.Clone() + if strings.TrimSpace(authorized.Snapshot.Token) == "" { + var err error + authorized, err = capabilityResolver.ResolveForCompilation(ctx, request.Project, request.SnapshotToken) + if err != nil { + return nil, err + } + } else if authorized.Snapshot.Identity.Project != projectid.Canonical(request.Project) || authorized.Snapshot.ValidateToken(request.SnapshotToken) != nil { + return nil, capability.ErrStaleSnapshot + } + snapshot := authorized.Snapshot + if err := validateAuthorizedReadScope(authorized.Scope, snapshot.Identity.AuthorizationScopeDigest); err != nil { + return nil, capability.ErrStaleSnapshot + } + workspace := request.Workspace.NormalizePresentationOrders() + intentDigest, err := workspace.Digest() + if err != nil { + return nil, err + } + normalized, err := workspace.CanonicalJSON() + if err != nil { + return nil, err + } + translated, err := explorercompilation.CompileWorkspace(ctx, request.Project, request.ExplorerID, workspace, snapshot) + if err != nil { + return nil, err + } + bindings := recipe.RuntimeBindings{Project: projectid.Legacy(request.Project), DatasetGeneration: snapshot.Identity.Generation, AuthResourcePaths: append([]string(nil), authorized.Scope.AuthResourcePaths...), AuthScopeMode: authorized.Scope.Mode} + resolved, err := recipeEngine.CompileResolvedBundle(ctx, translated.Bundle, bindings) + if err != nil { + return nil, err + } + contract, err := json.Marshal(explorer.PublicOutputContracts{Outputs: translated.OutputContracts}) + if err != nil { + return nil, err + } + contractDigest, err := explorer.CompilationArtifactDigest(contract) + if err != nil { + return nil, err + } + resolvedRecipeDigest, err := resolved.Bundle.Digest() + if err != nil { + return nil, err + } + compiledConfig, err := compiledExplorerWorkspaceConfigV2(request.Project, request.ExplorerID, translated) + if err != nil { + return nil, err + } + fingerprints, columnProvenance, err := resolvedOutputArtifacts(resolved) + if err != nil { + return nil, fmt.Errorf("build receipt execution contract: %w", err) + } + receipt := explorer.CompilationReceipt{ReceiptFormatVersion: explorer.CurrentReceiptFormatVersion, CompilerContractVersion: explorer.CurrentCompilerContractVersion, Project: projectid.Canonical(request.Project), ExplorerID: request.ExplorerID, IntentDigest: intentDigest, SnapshotToken: request.SnapshotToken, AuthorizationScopeDigest: snapshot.Identity.AuthorizationScopeDigest, CapabilitySchemaDigest: snapshot.Identity.SchemaDigest, SourceGeneration: snapshot.Identity.Generation, RecipeDigest: resolved.StoredRecipeDigest, ResolvedRecipeDigest: resolvedRecipeDigest, ResolvedSchemaDigest: resolved.ResolvedSchemaDigest, OutputContractDigest: contractDigest, NormalizedBundle: normalized, Bundle: resolved.Bundle, CompiledConfig: compiledConfig, PublicOutputContract: contract, IdentityMappings: translated.IdentityMappings, EmittedColumns: translated.EmittedColumns, OutputFingerprints: fingerprints, OutputColumnProvenance: columnProvenance, RequestID: request.RequestID, CreatedAt: time.Now().UTC()} + receipt.CompilationKey, err = explorer.CompilationKey(receipt) + if err != nil { + return nil, err + } + receipt.ID, err = explorer.ReceiptID(receipt) + if err != nil { + return nil, err + } + stored, err := explorerService.StoreCompilationReceipt(ctx, receipt) + if err != nil { + return nil, err + } + if _, err := compileValidatedReceiptResolution(ctx, recipeEngine, stored, bindings); err != nil { + return nil, receiptCompilationConflict(stored.ID, err) + } + receiptBytes := 0 + if raw, marshalErr := json.Marshal(stored); marshalErr == nil { + receiptBytes = len(raw) + } + if logger != nil { + logger.Info("Explorer receipt compiled", "project", receipt.Project, "explorer_id", receipt.ExplorerID, "receipt_id", receipt.ID, "duration_ms", time.Since(started).Milliseconds(), "receipt_bytes", receiptBytes, "output_count", len(receipt.Bundle.Outputs), "column_count", len(receipt.EmittedColumns)) + } + return stored, nil +} + +type receiptContractMismatch struct { + Component string + OutputID string + Expected string + Actual string +} + +func (e *receiptContractMismatch) Error() string { + if e.OutputID != "" { + return fmt.Sprintf("receipt %s mismatch for output %q: expected %q, got %q", e.Component, e.OutputID, e.Expected, e.Actual) + } + return fmt.Sprintf("receipt %s mismatch: expected %q, got %q", e.Component, e.Expected, e.Actual) +} + +func contractMismatch(component, output, expected, actual string) error { + return &receiptContractMismatch{Component: component, OutputID: output, Expected: expected, Actual: actual} +} + +func receiptMismatchDetails(receiptID string, err error) map[string]any { + details := map[string]any{"component": "output_execution"} + if strings.TrimSpace(receiptID) != "" { + details["receiptId"] = receiptID + } + var mismatch *receiptContractMismatch + if errors.As(err, &mismatch) { + details["component"] = mismatch.Component + if mismatch.OutputID != "" { + details["outputId"] = mismatch.OutputID + } + } + return details +} + +func receiptCompilationConflict(receiptID string, cause error) error { + value := explorerConflict("compile", "COMPILATION_CONTRACT_MISMATCH", "the compiler could not reproduce the stored receipt execution contract", receiptMismatchDetails(receiptID, cause)) + if authoring, ok := value.(*explorer.AuthoringError); ok { + authoring.Cause = cause + } + return value +} + func compiledExplorerWorkspaceConfigV2(project, explorerID string, compiled explorercompilation.WorkspaceResult) ([]byte, error) { if len(compiled.EmittedColumns) == 0 { return nil, nil @@ -151,47 +276,53 @@ func firstNonEmptyWorkspaceTitle(values ...string) string { return "Explorer" } -func validateReceiptCapability(receipt *explorer.CompilationReceipt, snapshot capability.Snapshot) error { - if receipt == nil { - return fmt.Errorf("compilation receipt is required") - } - if snapshot.Identity.Project != receipt.Project || snapshot.Identity.Generation != receipt.SourceGeneration { - return fmt.Errorf("receipt capability project or generation changed") - } - if snapshot.Identity.AuthorizationScopeDigest != receipt.AuthorizationScopeDigest { - return fmt.Errorf("receipt authorization scope changed") - } - if snapshot.Identity.SchemaDigest != receipt.CapabilitySchemaDigest { - return fmt.Errorf("receipt capability schema changed") - } - return nil -} - // validateReceiptResolution proves that deterministic runtime lowering still // describes the exact resolved semantic artifact frozen in the receipt. It // intentionally compares no AQL or physical IR because those are // request-scoped implementation details. -func validateReceiptResolution(receipt *explorer.CompilationReceipt, resolved engine.Resolved) error { +func validateReceiptResolution(receipt *explorer.CompilationReceipt, resolved *dataframeexecution.Resolved) error { if receipt == nil { return fmt.Errorf("compilation receipt is required") } + if resolved == nil { + return fmt.Errorf("resolved compilation is required") + } digest, err := resolved.Bundle.Digest() if err != nil { return err } - if digest != receipt.ResolvedRecipeDigest || resolved.StoredRecipeDigest != receipt.RecipeDigest { - return fmt.Errorf("receipt recipe digest does not match deterministic lowering") + if digest != receipt.ResolvedRecipeDigest { + return contractMismatch("recipe", "", receipt.ResolvedRecipeDigest, digest) + } + if resolved.StoredRecipeDigest != receipt.RecipeDigest { + return contractMismatch("recipe", "", receipt.RecipeDigest, resolved.StoredRecipeDigest) } if resolved.ResolvedSchemaDigest != receipt.ResolvedSchemaDigest { - return fmt.Errorf("receipt resolved schema digest does not match deterministic lowering") + return contractMismatch("schema", "", receipt.ResolvedSchemaDigest, resolved.ResolvedSchemaDigest) + } + want, _, err := resolvedOutputArtifacts(*resolved) + if err != nil { + return err } - want := resolvedOutputFingerprints(resolved) if len(want) != len(receipt.OutputFingerprints) { - return fmt.Errorf("receipt output fingerprint set changed") + return contractMismatch("output_set", "", fmt.Sprint(len(receipt.OutputFingerprints)), fmt.Sprint(len(want))) } for output, fingerprint := range want { if strings.TrimSpace(receipt.OutputFingerprints[output]) != fingerprint { - return fmt.Errorf("receipt output %q fingerprint changed", output) + return contractMismatch("output_execution", output, receipt.OutputFingerprints[output], fingerprint) + } + } + if len(receipt.OutputColumnProvenance) != len(resolved.Compiled.Outputs) { + return contractMismatch("provenance", "", fmt.Sprint(len(resolved.Compiled.Outputs)), fmt.Sprint(len(receipt.OutputColumnProvenance))) + } + for index := range resolved.Compiled.Outputs { + output := &resolved.Compiled.Outputs[index] + values, ok := receipt.OutputColumnProvenance[output.Name] + if !ok { + return contractMismatch("provenance", output.Name, "present", "missing") + } + if err := applyReceiptColumnProvenance(output, values); err != nil { + return contractMismatch("provenance", output.Name, "complete", err.Error()) } } return nil @@ -201,18 +332,18 @@ func validateReceiptResolution(receipt *explorer.CompilationReceipt, resolved en // even when the caller intends to execute only one output. OutputNames is an // execution selector: allowing it to narrow compilation here would compare a // partial fingerprint and column set with the receipt's full contract. -func compileValidatedReceiptResolution(ctx context.Context, recipeEngine *engine.Engine, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (engine.Resolved, error) { +func compileValidatedReceiptResolution(ctx context.Context, recipeEngine *dataframeexecution.Engine, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (dataframeexecution.Resolved, error) { validationBindings := bindings validationBindings.OutputNames = nil resolved, err := recipeEngine.CompileResolvedBundle(ctx, receipt.Bundle, validationBindings) if err != nil { - return engine.Resolved{}, err + return dataframeexecution.Resolved{}, err } - if err := validateReceiptResolution(receipt, resolved); err != nil { - return engine.Resolved{}, err + if err := validateReceiptResolution(receipt, &resolved); err != nil { + return dataframeexecution.Resolved{}, err } if err := validateReceiptEnginePublicColumns(receipt, resolved); err != nil { - return engine.Resolved{}, err + return dataframeexecution.Resolved{}, contractMismatch("public_columns", "", "receipt public columns", err.Error()) } return resolved, nil } diff --git a/internal/server/explorer_receipt_contract_test.go b/internal/server/explorer_receipt_contract_test.go index 33c9ef5f..28d23a9d 100644 --- a/internal/server/explorer_receipt_contract_test.go +++ b/internal/server/explorer_receipt_contract_test.go @@ -11,17 +11,19 @@ import ( "time" "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/explorer/capability" explorercompilation "github.com/calypr/loom/internal/explorer/compilation" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/gofiber/fiber/v3" ) func TestCompileValidatedReceiptResolutionChecksAllOutputsForScopedPreview(t *testing.T) { - recipeEngine, err := engine.New(engine.Config{ + recipeEngine, err := dataframeexecution.New(dataframeexecution.Config{ Registry: compilerTestRegistry{}, QueryRows: func(context.Context, string, int, map[string]any, func(map[string]any) error) error { return nil @@ -51,12 +53,17 @@ func TestCompileValidatedReceiptResolutionChecksAllOutputsForScopedPreview(t *te if err != nil { t.Fatal(err) } + fingerprints, provenance, err := resolvedOutputArtifacts(compiled) + if err != nil { + t.Fatal(err) + } receipt := &explorer.CompilationReceipt{ - Bundle: bundle, - RecipeDigest: compiled.StoredRecipeDigest, - ResolvedRecipeDigest: resolvedDigest, - ResolvedSchemaDigest: compiled.ResolvedSchemaDigest, - OutputFingerprints: resolvedOutputFingerprints(compiled), + Bundle: bundle, + RecipeDigest: compiled.StoredRecipeDigest, + ResolvedRecipeDigest: resolvedDigest, + ResolvedSchemaDigest: compiled.ResolvedSchemaDigest, + OutputFingerprints: fingerprints, + OutputColumnProvenance: provenance, EmittedColumns: []explorer.EmittedColumn{ // Presentation order is intentionally different from compiler field // order. It must not invalidate an otherwise identical receipt. @@ -79,6 +86,90 @@ func TestCompileValidatedReceiptResolutionChecksAllOutputsForScopedPreview(t *te } } +func TestCompileValidatedReceiptResolutionSurvivesCompilerMetadataJSONRoundTrip(t *testing.T) { + recipeEngine, err := dataframeexecution.New(dataframeexecution.Config{Registry: compilerTestRegistry{}, QueryRows: func(context.Context, string, int, map[string]any, func(map[string]any) error) error { return nil }}) + if err != nil { + t.Fatal(err) + } + bundle := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "round-trip", TranslationVersion: explorercompilation.TranslationVersion, Outputs: []recipe.Output{{Name: "patients", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{{Name: "patient_id", Expr: recipe.Expression{Select: "root.id"}, Discovered: true}}}}} + bindings := recipe.RuntimeBindings{Project: "project-a", DatasetGeneration: "generation-a"} + compiled, err := recipeEngine.CompileResolvedBundle(context.Background(), bundle, bindings) + if err != nil { + t.Fatal(err) + } + fingerprints, provenance, err := resolvedOutputArtifacts(compiled) + if err != nil { + t.Fatal(err) + } + digest, err := bundle.Digest() + if err != nil { + t.Fatal(err) + } + receipt := explorer.CompilationReceipt{Bundle: bundle, RecipeDigest: compiled.StoredRecipeDigest, ResolvedRecipeDigest: digest, ResolvedSchemaDigest: compiled.ResolvedSchemaDigest, OutputFingerprints: fingerprints, OutputColumnProvenance: provenance, EmittedColumns: []explorer.EmittedColumn{{OutputID: "patients", PublicColumn: "patient_id"}}} + raw, err := json.Marshal(receipt) + if err != nil { + t.Fatal(err) + } + var stored explorer.CompilationReceipt + if err := json.Unmarshal(raw, &stored); err != nil { + t.Fatal(err) + } + if stored.Bundle.Outputs[0].Fields[0].Discovered { + t.Fatal("compiler-local provenance unexpectedly survived recipe JSON") + } + resolved, err := compileValidatedReceiptResolution(context.Background(), recipeEngine, &stored, bindings) + if err != nil { + t.Fatal(err) + } + restored := false + for _, column := range resolved.Compiled.Outputs[0].OutputSchema { + if column.Name == "patient_id" { + restored = column.Discovered + } + } + if !restored { + t.Fatal("durable receipt provenance was not restored") + } +} + +func TestResolvedOutputFingerprintExcludesOptimizerAndTransientProvenance(t *testing.T) { + recipeEngine, err := dataframeexecution.New(dataframeexecution.Config{Registry: compilerTestRegistry{}, QueryRows: func(context.Context, string, int, map[string]any, func(map[string]any) error) error { return nil }}) + if err != nil { + t.Fatal(err) + } + bundle := recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Name: "fingerprint", TranslationVersion: "test", Outputs: []recipe.Output{{Name: "patients", RootResourceType: "Patient", RowGrain: "patient", Fields: []recipe.Field{{Name: "patient_id", Expr: recipe.Expression{Select: "root.id"}}}}}} + resolved, err := recipeEngine.CompileResolvedBundle(context.Background(), bundle, recipe.RuntimeBindings{Project: "project-a", DatasetGeneration: "generation-a"}) + if err != nil { + t.Fatal(err) + } + beforeArtifacts, _, err := resolvedOutputArtifacts(resolved) + if err != nil { + t.Fatal(err) + } + before := beforeArtifacts["patients"] + resolved.Compiled.Outputs[0].OutputSchema[0].Discovered = !resolved.Compiled.Outputs[0].OutputSchema[0].Discovered + resolved.Compiled.Outputs[0].OptimizedPlan.OptimizationPolicy.Decisions = append(resolved.Compiled.Outputs[0].OptimizedPlan.OptimizationPolicy.Decisions, ir.PhysicalOptimizationDecision{Reason: "diagnostic-only"}) + afterArtifacts, _, err := resolvedOutputArtifacts(resolved) + if err != nil { + t.Fatal(err) + } + after := afterArtifacts["patients"] + if before != after { + t.Fatalf("transient compiler metadata changed canonical fingerprint: %q != %q", before, after) + } + for key := range resolved.Compiled.Outputs[0].Plan.BindVars { + resolved.Compiled.Outputs[0].Plan.BindVars[key] = "semantically-different" + break + } + changedArtifacts, _, err := resolvedOutputArtifacts(resolved) + if err != nil { + t.Fatal(err) + } + if changed := changedArtifacts["patients"]; changed == before { + t.Fatal("execution bind change did not change canonical fingerprint") + } +} + func TestCompiledExplorerWorkspaceConfigUsesIndependentStablePresentationOrders(t *testing.T) { compiled := explorercompilation.WorkspaceResult{ Workspace: authoringv2.Workspace{ @@ -88,7 +179,7 @@ func TestCompiledExplorerWorkspaceConfigUsesIndependentStablePresentationOrders( Tabs: []authoringv2.Tab{{ID: "tab", Title: "Out", OutputID: "out", Order: 0, Visible: true}}, }, Bundle: recipe.Bundle{RecipeSchemaVersion: recipe.CurrentSchemaVersion, Outputs: []recipe.Output{{Name: "out"}}}, - EmittedColumns: []explorercompilation.EmittedColumn{ + EmittedColumns: []explorer.EmittedColumn{ {EmissionID: "column_b", OutputID: "out", PublicColumn: "column_b"}, {EmissionID: "column_a", OutputID: "out", PublicColumn: "column_a"}, }, @@ -128,68 +219,39 @@ func TestNativeV2RouteUsesAuthorizedPersistedReceipt(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := service.CreateEmptyInteractive(context.Background(), "project-a", "custom", "Custom", "test"); err != nil { + if _, err := service.CreateInteractiveFrom(context.Background(), "project-a", "custom", "Custom", "", "test"); err != nil { t.Fatal(err) } - compileCalls := 0 previewCalls := 0 - config := ExplorerV2LifecycleConfig{ - Capability: func(context.Context, string, string, string) (capability.Snapshot, error) { return snapshot, nil }, - CapabilityToken: func(context.Context, string, string) (capability.Snapshot, error) { return snapshot, nil }, - AuthorizedCapabilityCompile: func(_ context.Context, _, token string) (AuthorizedCapability, error) { - if err := snapshot.ValidateToken(token); err != nil { - return AuthorizedCapability{}, err - } - return AuthorizedCapability{Snapshot: snapshot, Scope: scope}, nil - }, - AuthorizedCapabilityExecution: func(context.Context, string, string) (AuthorizedCapability, error) { - return AuthorizedCapability{Snapshot: snapshot, Scope: executionScope}, nil + config := lifecycle.Config{ + Capability: lifecycle.CapabilityResolver{ + ForExecution: func(context.Context, string, string) (lifecycle.AuthorizedCapability, error) { + return lifecycle.AuthorizedCapability{Snapshot: snapshot, Scope: executionScope}, nil + }, }, ReceiptLookup: service.CompilationReceiptForExplorer, - CompileReceipt: func(ctx context.Context, request ExplorerV2ReceiptCompileRequest) (*explorer.CompilationReceipt, error) { - compileCalls++ - if request.Authorized.Snapshot.Token != snapshot.Token || !request.Authorized.Scope.Unrestricted() { - t.Fatalf("compile lost authorized capability: %#v", request.Authorized) - } - return persistTestNativeReceipt(ctx, t, service, request, snapshot) - }, - PreviewReceipt: func(_ context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (engine.PreviewSummary, error) { + PreviewReceipt: func(_ context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { previewCalls++ if receipt == nil || bindings.AuthScopeMode != authscope.ReadScopeUnrestricted || bindings.IncludeAuthResourcePath { t.Fatalf("preview bindings widened or requested publication metadata: receipt=%#v bindings=%#v", receipt, bindings) } if err := visit(map[string]any{"c_patient": "patient-1"}); err != nil { - return engine.PreviewSummary{}, err + return dataframeexecution.PreviewSummary{}, err } - return engine.PreviewSummary{Output: "patients", Columns: []string{"c_patient"}, RowCount: 1, PlanMode: "physical", PlanProfile: "generic_fhir_graph_recipe", PlanFingerprint: "test", TraversalCount: 0}, nil + return dataframeexecution.PreviewSummary{Output: "patients", Columns: []string{"c_patient"}, RowCount: 1, PlanMode: "physical", PlanProfile: "generic_fhir_graph_recipe", PlanFingerprint: "test", TraversalCount: 0}, nil }, } - app := fiber.New() - registerTestExplorerAuthoringRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, config) - body := `{"workspace":` + string(baselineExplorerWorkspaceV2()) + `,"snapshotToken":"` + snapshot.Token + `"}` - compiled := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/compile", body) - if compiled.StatusCode != http.StatusOK { - t.Fatalf("compile status=%d body=%s", compiled.StatusCode, compiled.Body) - } - ownerAfterCompile, err := service.Get(context.Background(), "project-a", "custom") + workspace, err := authoringv2.DecodeWorkspace(baselineExplorerWorkspaceV2()) if err != nil { t.Fatal(err) } - if ownerAfterCompile.ActiveRevisionID != "" || len(ownerAfterCompile.DraftConfig) != 0 { - t.Fatalf("compile mutated active or draft state: %#v", ownerAfterCompile) - } - var result struct { - ReceiptID string `json:"receiptId"` - CompilerVersion string `json:"compilerVersion"` - } - if err := json.Unmarshal([]byte(compiled.Body), &result); err != nil { + receipt, err := persistTestNativeReceipt(context.Background(), t, service, lifecycle.CompileReceiptRequest{Project: "project-a", ExplorerID: "custom", Workspace: workspace, SnapshotToken: snapshot.Token, Authorized: lifecycle.AuthorizedCapability{Snapshot: snapshot, Scope: scope}}, snapshot) + if err != nil { t.Fatal(err) } - wantCompilerVersion := explorer.CurrentCompilerContractVersion + "+" + explorercompilation.TranslationVersion - if result.CompilerVersion != wantCompilerVersion { - t.Fatalf("compilerVersion=%q, want %q", result.CompilerVersion, wantCompilerVersion) - } - preview := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/preview", `{"receiptId":"`+result.ReceiptID+`","outputId":"patients","limit":5}`) + app := fiber.New() + registerGeneratedExplorerTestRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, config) + preview := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/preview", `{"receiptId":"`+receipt.ID+`","outputId":"patients","limit":5}`) if preview.StatusCode != http.StatusOK { t.Fatalf("preview status=%d body=%s", preview.StatusCode, preview.Body) } @@ -200,10 +262,10 @@ func TestNativeV2RouteUsesAuthorizedPersistedReceipt(t *testing.T) { if ownerAfterPreview.ActiveRevisionID != "" || len(ownerAfterPreview.DraftConfig) != 0 { t.Fatalf("preview mutated active or draft state: %#v", ownerAfterPreview) } - if compileCalls != 1 || previewCalls != 1 { - t.Fatalf("compile calls=%d preview calls=%d", compileCalls, previewCalls) + if previewCalls != 1 { + t.Fatalf("preview calls=%d", previewCalls) } - unknown := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/preview", `{"receiptId":"`+result.ReceiptID+`","outputId":"missing","limit":5}`) + unknown := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/preview", `{"receiptId":"`+receipt.ID+`","outputId":"missing","limit":5}`) if unknown.StatusCode != http.StatusUnprocessableEntity || !strings.Contains(unknown.Body, `"code":"UNKNOWN_AUTHORING_OUTPUT"`) || previewCalls != 1 { t.Fatalf("unknown output status=%d calls=%d body=%s", unknown.StatusCode, previewCalls, unknown.Body) } @@ -211,13 +273,13 @@ func TestNativeV2RouteUsesAuthorizedPersistedReceipt(t *testing.T) { "/api/v1/projects/project-b/explorers/custom/authoring/v2/preview", "/api/v1/projects/project-a/explorers/other/authoring/v2/preview", } { - foreign := requestJSON(t, app, http.MethodPost, path, `{"receiptId":"`+result.ReceiptID+`","outputId":"patients","limit":5}`) + foreign := requestJSON(t, app, http.MethodPost, path, `{"receiptId":"`+receipt.ID+`","outputId":"patients","limit":5}`) if foreign.StatusCode != http.StatusNotFound || !strings.Contains(foreign.Body, `"code":"COMPILE_RECEIPT_NOT_FOUND"`) || previewCalls != 1 { t.Fatalf("foreign receipt path=%s status=%d calls=%d body=%s", path, foreign.StatusCode, previewCalls, foreign.Body) } } executionScope = authscope.ReadScope{Mode: authscope.ReadScopeRestricted} - widened := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/preview", `{"receiptId":"`+result.ReceiptID+`","outputId":"patients","limit":5}`) + widened := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/preview", `{"receiptId":"`+receipt.ID+`","outputId":"patients","limit":5}`) if widened.StatusCode != http.StatusConflict || !strings.Contains(widened.Body, `"code":"RECEIPT_STALE"`) || previewCalls != 1 { t.Fatalf("scope mismatch status=%d calls=%d body=%s", widened.StatusCode, previewCalls, widened.Body) } @@ -229,21 +291,24 @@ func TestBuilderCommandsCreateBackendOwnedDraftAndReconcileIt(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := service.CreateEmptyInteractive(context.Background(), "project-a", "custom", "Custom", "test"); err != nil { + if _, err := service.CreateInteractiveFrom(context.Background(), "project-a", "custom", "Custom", "", "test"); err != nil { t.Fatal(err) } - config := ExplorerV2LifecycleConfig{ - Capability: func(context.Context, string, string, string) (capability.Snapshot, error) { return snapshot, nil }, - CapabilityToken: func(context.Context, string, string) (capability.Snapshot, error) { return snapshot, nil }, - AuthorizedCapabilityCompile: func(context.Context, string, string) (AuthorizedCapability, error) { - return AuthorizedCapability{Snapshot: snapshot, Scope: authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}}, nil + config := lifecycle.Config{ + Capability: lifecycle.CapabilityResolver{ + Current: func(context.Context, string, string, string) (capability.Snapshot, error) { return snapshot, nil }, + Token: func(context.Context, string, string) (capability.Snapshot, error) { return snapshot, nil }, + ForCompilation: func(context.Context, string, string) (lifecycle.AuthorizedCapability, error) { + return lifecycle.AuthorizedCapability{Snapshot: snapshot, Scope: authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}}, nil + }, + Catalog: authoringV2Catalog, }, - CompileReceipt: func(ctx context.Context, request ExplorerV2ReceiptCompileRequest) (*explorer.CompilationReceipt, error) { + CompileReceipt: func(ctx context.Context, request lifecycle.CompileReceiptRequest) (*explorer.CompilationReceipt, error) { return persistTestNativeReceipt(ctx, t, service, request, snapshot) }, } app := fiber.New() - registerTestExplorerAuthoringRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, config) + registerGeneratedExplorerTestRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, config) commandBody := `{"commandId":"browser-command-1","snapshotToken":"` + snapshot.Token + `","expectedDraftVersion":0,"commands":[{"type":"CREATE_TABLE","title":"Patients","rootNodeId":"n_patient"}]}` created := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/commands", commandBody) @@ -277,22 +342,28 @@ func TestBuilderCommandsCreateBackendOwnedDraftAndReconcileIt(t *testing.T) { } func TestCreateExplorerFromCurrentClonesWorkspaceOnServer(t *testing.T) { - service, err := explorer.NewService(newTestExplorerStore()) + workspace, err := authoringv2.DecodeWorkspace(baselineExplorerWorkspaceV2()) if err != nil { t.Fatal(err) } - if _, err := service.CreateEmptyInteractive(context.Background(), "project-a", "source", "Source", "test"); err != nil { + canonical, err := workspace.CanonicalJSON() + if err != nil { t.Fatal(err) } - workspace, err := authoringv2.DecodeWorkspace(baselineExplorerWorkspaceV2()) + digest, err := workspace.Digest() if err != nil { t.Fatal(err) } - if _, err := service.SaveWorkspaceDraft(context.Background(), "project-a", "source", workspace, 0, "", "test"); err != nil { + store := newTestExplorerStore() + if _, err := store.Create(context.Background(), explorer.Explorer{Project: "project-a", ExplorerID: "source", Title: "Source", ManagementMode: explorer.ManagementInteractive, DraftConfig: canonical, DraftVersion: 1, DraftDigest: digest}); err != nil { + t.Fatal(err) + } + service, err := explorer.NewService(store) + if err != nil { t.Fatal(err) } app := fiber.New() - registerTestExplorerLifecycleRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, ExplorerV2LifecycleConfig{}) + registerGeneratedExplorerTestRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, lifecycle.Config{}) created := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers", `{"name":"Cloned Explorer","title":"Cloned Explorer","sourceExplorerId":"source"}`) if created.StatusCode != http.StatusCreated { t.Fatalf("clone status=%d body=%s", created.StatusCode, created.Body) @@ -310,7 +381,7 @@ func TestCreateExplorerFromCurrentClonesWorkspaceOnServer(t *testing.T) { } } -func persistTestNativeReceipt(ctx context.Context, t *testing.T, service *explorer.Service, request ExplorerV2ReceiptCompileRequest, snapshot capability.Snapshot) (*explorer.CompilationReceipt, error) { +func persistTestNativeReceipt(ctx context.Context, t *testing.T, service *explorer.Service, request lifecycle.CompileReceiptRequest, snapshot capability.Snapshot) (*explorer.CompilationReceipt, error) { t.Helper() workspace := request.Workspace normalized, err := workspace.CanonicalJSON() @@ -339,7 +410,7 @@ func persistTestNativeReceipt(ctx context.Context, t *testing.T, service *explor ResolvedSchemaDigest: "resolved-schema", OutputContractDigest: contractDigest, NormalizedBundle: normalized, Bundle: bundle, CompiledConfig: json.RawMessage(`{}`), PublicOutputContract: contract, EmittedColumns: []explorer.EmittedColumn{{EmissionID: "em_patient", OutputID: "patients", CandidateID: "c_patient_id", OccurrenceID: authoringv2.RootOccurrenceID, ProjectionMode: "FIRST", PublicColumn: "c_patient", Label: "Patient ID", LogicalType: "string", Filterable: true, Chartable: true}}, - OutputFingerprints: map[string]string{"patients": "fingerprint"}, CreatedAt: time.Now().UTC(), + OutputFingerprints: map[string]string{"patients": "fingerprint"}, OutputColumnProvenance: map[string]map[string]string{"patients": {"c_patient": "EXPLICIT"}}, CreatedAt: time.Now().UTC(), } receipt.CompilationKey, err = explorer.CompilationKey(receipt) if err != nil { diff --git a/internal/server/explorer_route_test_helpers_test.go b/internal/server/explorer_route_test_helpers_test.go deleted file mode 100644 index 9430eca9..00000000 --- a/internal/server/explorer_route_test_helpers_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import ( - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/explorer" - "github.com/gofiber/fiber/v3" -) - -func registerTestExplorerLifecycleRoutes(router fiber.Router, authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, service *explorer.Service, config ExplorerV2LifecycleConfig) { - handlers := newExplorerLifecycleHandlers(authorizer, authorizeRead, service, config) - router.Get("/api/v1/projects/:project/explorers", handlers.list) - router.Post("/api/v1/projects/:project/explorers", handlers.create) - router.Get("/api/v1/projects/:project/explorers/:explorerId", handlers.get) -} - -func registerTestExplorerAuthoringRoutes(router fiber.Router, authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, service *explorer.Service, config ExplorerV2LifecycleConfig) { - h := newExplorerAuthoringHandlers(authorizer, authorizeRead, service, config) - base := "/api/v1/projects/:project/explorers/:explorerId/authoring/v2" - router.Get(base+"/capability", h.getCapability) - router.Get(base+"/capabilities/:snapshotToken/candidates/:candidateId/suggestions", h.getCandidateSuggestions) - router.Post(base+"/suggestions", h.searchSuggestions) - router.Get(base+"/builder", h.getBuilder) - router.Post(base+"/builder", h.compileBuilder) - router.Post(base+"/compile", h.compile) - router.Post(base+"/commands", h.applyCommands) - router.Post(base+"/reconcile", h.reconcile) - router.Put(base+"/draft", h.saveDraft) - router.Get(base+"/export", h.exportWorkspace) - router.Post(base+"/preview", h.preview) - router.Post(base+"/publish", h.publish) -} - -func registerTestExplorerRoutes(router fiber.Router, authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, service *explorer.Service, materialize graphresolver.ExplorerBundleMaterializer, config ExplorerV2LifecycleConfig) { - h := newExplorerHTTPHandlers(authorizer, authorizeRead, service, materialize, config) - router.Post("/api/v1/projects/:project/generations/:generation/explorer-config", h.publishRepositoryConfig) - router.Get("/api/v1/projects/:project/explorer-config", h.getRepositoryConfig) - registerTestExplorerLifecycleRoutes(router, authorizer, authorizeRead, service, config) - registerTestExplorerAuthoringRoutes(router, authorizer, authorizeRead, service, config) -} diff --git a/internal/server/explorer_state_route_test.go b/internal/server/explorer_state_route_test.go index d3018def..0c660fce 100644 --- a/internal/server/explorer_state_route_test.go +++ b/internal/server/explorer_state_route_test.go @@ -11,6 +11,7 @@ import ( "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/gofiber/fiber/v3" ) @@ -20,7 +21,7 @@ func TestExplorerStateRouteBuildsDefaultProjectionFromRecipe(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := service.CreateEmptyInteractive(context.Background(), "project-a", "patients", "Patients", "test"); err != nil { + if _, err := service.CreateInteractiveFrom(context.Background(), "project-a", "patients", "Patients", "", "test"); err != nil { t.Fatal(err) } selector := dataset.DataframeSelector{Recipe: "patients-query", TranslationVersion: "v1", Output: "patients"} @@ -47,7 +48,7 @@ func TestExplorerStateRouteBuildsDefaultProjectionFromRecipe(t *testing.T) { store.mu.Unlock() app := fiber.New() - registerTestExplorerLifecycleRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, ExplorerV2LifecycleConfig{}) + registerGeneratedExplorerTestRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, lifecycle.Config{}) response := requestJSON(t, app, http.MethodGet, "/api/v1/projects/project-a/explorers/patients", "") if response.StatusCode != http.StatusOK { t.Fatalf("status=%d body=%s", response.StatusCode, response.Body) @@ -75,10 +76,6 @@ func TestExplorerStateRouteBuildsDefaultProjectionFromRecipe(t *testing.T) { } func TestViewerProjectionDoesNotInventColumnsWithoutPublishedSchema(t *testing.T) { - service, err := explorer.NewService(newTestExplorerStore()) - if err != nil { - t.Fatal(err) - } selector := dataset.DataframeSelector{Recipe: "patients-query", TranslationVersion: "v1", Output: "patients"} revision := explorer.Revision{ Recipe: recipe.Bundle{ @@ -93,7 +90,7 @@ func TestViewerProjectionDoesNotInventColumnsWithoutPublishedSchema(t *testing.T }}, } - runtime := service.BuildViewerProjection(&revision) + runtime := explorer.BuildViewerProjection(&revision) if runtime == nil || len(runtime.Outputs) != 1 { t.Fatalf("runtime = %#v", runtime) } @@ -104,10 +101,6 @@ func TestViewerProjectionDoesNotInventColumnsWithoutPublishedSchema(t *testing.T } func TestViewerProjectionRejectsAliasedPhysicalColumns(t *testing.T) { - service, err := explorer.NewService(newTestExplorerStore()) - if err != nil { - t.Fatal(err) - } selector := dataset.DataframeSelector{Recipe: "patients-query", TranslationVersion: "v1", Output: "patients"} revision := explorer.Revision{ Recipe: recipe.Bundle{ @@ -128,7 +121,7 @@ func TestViewerProjectionRejectsAliasedPhysicalColumns(t *testing.T) { }}, } - runtime := service.BuildViewerProjection(&revision) + runtime := explorer.BuildViewerProjection(&revision) if runtime == nil || len(runtime.Outputs) != 1 { t.Fatalf("runtime = %#v", runtime) } @@ -142,12 +135,12 @@ func TestExplorerStateRouteReturnsExplicitUnpublishedProjection(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := service.CreateEmptyInteractive(context.Background(), "project-a", "empty", "Empty", "test"); err != nil { + if _, err := service.CreateInteractiveFrom(context.Background(), "project-a", "empty", "Empty", "", "test"); err != nil { t.Fatal(err) } app := fiber.New() - registerTestExplorerLifecycleRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, ExplorerV2LifecycleConfig{}) + registerGeneratedExplorerTestRoutes(app, authscope.AllowAllAuthorizer{}, func(context.Context, *authscope.Principal, string) error { return nil }, service, lifecycle.Config{}) response := requestJSON(t, app, http.MethodGet, "/api/v1/projects/project-a/explorers/empty", "") if response.StatusCode != http.StatusOK { t.Fatalf("status=%d body=%s", response.StatusCode, response.Body) diff --git a/internal/server/federation_status.go b/internal/server/federation_status.go deleted file mode 100644 index dfd54b61..00000000 --- a/internal/server/federation_status.go +++ /dev/null @@ -1,179 +0,0 @@ -package server - -import ( - "context" - "errors" - "time" - - "github.com/calypr/loom/internal/dataframe/publication" - "github.com/calypr/loom/internal/dataframe/published" - "github.com/calypr/loom/internal/dataset" -) - -// releaseProjectStatusResolver joins the active release with durable attempts. -// Its caller supplies an authorization-filtered project list, and the GraphQL -// service filters the result against that list again before returning it. -type releaseProjectStatusResolver struct { - releases dataset.ReleaseRepository - executions publication.BundleCatalog -} - -func (r releaseProjectStatusResolver) DataframeProjectStatuses(ctx context.Context, projects []string, selector dataset.DataframeSelector) ([]published.ProjectStatus, error) { - _, statuses, _, err := r.ResolveFederationSnapshot(ctx, projects, selector) - return statuses, err -} - -func (r releaseProjectStatusResolver) ResolveFederationSnapshot(ctx context.Context, projects []string, selector dataset.DataframeSelector) (map[string]string, []published.ProjectStatus, bool, error) { - latest, err := r.latestAttempts(ctx, selector) - if err != nil { - return nil, nil, false, err - } - executionIDs := make(map[string]string, len(projects)) - result := make([]published.ProjectStatus, 0, len(projects)) - complete := true - for _, project := range projects { - status := published.ProjectStatus{ProjectID: project, State: published.ProjectMissing} - active, activeErr := r.releases.ReadActiveRelease(ctx, project) - if activeErr != nil && !errors.Is(activeErr, dataset.ErrNoActiveRelease) { - return nil, nil, false, activeErr - } - if errors.Is(activeErr, dataset.ErrNoActiveRelease) { - complete = false - } - if activeErr == nil { - for _, binding := range active.Release.Publications { - if binding.Selector != selector { - continue - } - status.State = published.ProjectCurrent - if binding.Stale { - status.State = published.ProjectStale - } - status.Generation, status.ExecutionID = binding.Generation, binding.ExecutionID - executionIDs[project] = binding.ExecutionID - status.UpdatedAt = binding.VerifiedAt - break - } - } - if execution, ok := latest[project]; ok { - isReplacement := execution.ID != status.ExecutionID && (status.UpdatedAt.IsZero() || execution.UpdatedAt.After(status.UpdatedAt)) - if status.State == published.ProjectMissing || isReplacement { - canonical := execution.State.Canonical() - if canonical != publication.BundleFailed && canonical != publication.BundleQueued && canonical != publication.BundleRunning && canonical != publication.BundleValidating { - result = append(result, status) - continue - } - status.Generation, status.ExecutionID = execution.DatasetGeneration, execution.ID - status.CreatedAt, status.UpdatedAt = execution.CreatedAt, execution.UpdatedAt - status.ErrorCode, status.Retryable = execution.FailureCode, execution.FailureRetryable - if canonical == publication.BundleFailed { - status.State = published.ProjectFailed - } else { - status.State = published.ProjectBuilding - } - } - } - result = append(result, status) - } - return executionIDs, result, complete, nil -} - -func (r releaseProjectStatusResolver) ActiveReleaseExecutionIDs(ctx context.Context, projects []string, selector dataset.DataframeSelector) (map[string]string, error) { - result := make(map[string]string) - for _, project := range projects { - active, err := r.releases.ReadActiveRelease(ctx, project) - if errors.Is(err, dataset.ErrNoActiveRelease) { - continue - } - if err != nil { - return nil, err - } - result[project] = "" - for _, binding := range active.Release.Publications { - if binding.Selector == selector { - result[project] = binding.ExecutionID - break - } - } - } - return result, nil -} - -func (r releaseProjectStatusResolver) ActiveReleaseSelectors(ctx context.Context, projects []string) ([]published.DataframeSelector, map[string]bool, error) { - selectors := make(map[string]published.DataframeSelector) - controlled := make(map[string]bool) - for _, project := range projects { - active, err := r.releases.ReadActiveRelease(ctx, project) - if errors.Is(err, dataset.ErrNoActiveRelease) { - continue - } - if err != nil { - return nil, nil, err - } - controlled[project] = true - for _, binding := range active.Release.Publications { - selectors[binding.Selector.Key()] = binding.Selector - } - } - result := make([]published.DataframeSelector, 0, len(selectors)) - for _, selector := range selectors { - result = append(result, selector) - } - return result, controlled, nil -} - -func (r releaseProjectStatusResolver) latestAttempts(ctx context.Context, selector dataset.DataframeSelector) (map[string]publication.BundleExecution, error) { - latest := make(map[string]publication.BundleExecution) - if r.executions == nil { - return latest, nil - } - if catalog, ok := r.executions.(publication.SelectorExecutionCatalog); ok { - executions, err := catalog.ListSelectorExecutions(ctx, selector, time.Now().UTC().Add(time.Second)) - if err != nil { - return nil, err - } - for _, execution := range executions { - if execution.Project == "" || !executionContainsSelector(execution, selector) { - continue - } - if prior, exists := latest[execution.Project]; !exists || execution.UpdatedAt.After(prior.UpdatedAt) { - latest[execution.Project] = execution - } - } - return latest, nil - } - for _, state := range []publication.BundleState{publication.BundleQueued, publication.BundleRunning, publication.BundleValidating, publication.BundlePublished, publication.BundleFailed} { - executions, err := r.executions.ListExecutions(ctx, state, time.Now().UTC().Add(time.Second)) - if err != nil { - return nil, err - } - for _, execution := range executions { - if execution.Project == "" || !executionContainsSelector(execution, selector) { - continue - } - if prior, ok := latest[execution.Project]; !ok || execution.UpdatedAt.After(prior.UpdatedAt) { - latest[execution.Project] = execution - } - } - } - return latest, nil -} - -func executionContainsSelector(execution publication.BundleExecution, selector dataset.DataframeSelector) bool { - for _, output := range execution.Outputs { - candidate := output.Selector - if !candidate.Valid() { - candidate = execution.Selector(output.Name) - } - if candidate == selector { - return true - } - } - // A queued command intentionally has no outputs until validation. Its exact - // recipe/version identity still makes the requested output BUILDING. - return len(execution.Outputs) == 0 && execution.Name == selector.Recipe && execution.TranslationVersion == selector.TranslationVersion -} - -var _ published.ProjectStatusResolver = releaseProjectStatusResolver{} -var _ published.ReleaseExecutionResolver = releaseProjectStatusResolver{} -var _ published.FederationSnapshotResolver = releaseProjectStatusResolver{} diff --git a/internal/server/federation_status_test.go b/internal/server/federation_status_test.go deleted file mode 100644 index 846abc80..00000000 --- a/internal/server/federation_status_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package server - -import ( - "context" - "testing" - "time" - - "github.com/calypr/loom/internal/dataframe/publication" - "github.com/calypr/loom/internal/dataframe/published" - "github.com/calypr/loom/internal/dataset" -) - -type statusExecutionCatalog struct { - publication.BundleCatalog - executions []publication.BundleExecution -} - -type statusReleaseStore struct { - records map[string]dataset.ProjectRelease - active map[string]dataset.ActiveRelease -} - -func newStatusReleaseStore() *statusReleaseStore { - return &statusReleaseStore{records: make(map[string]dataset.ProjectRelease), active: make(map[string]dataset.ActiveRelease)} -} - -func (s *statusReleaseStore) SaveRelease(_ context.Context, release dataset.ProjectRelease) (dataset.ProjectRelease, error) { - s.records[release.Project+"\x00"+release.ID] = release - return release, nil -} - -func (s *statusReleaseStore) ReadRelease(_ context.Context, project, releaseID string) (dataset.ProjectRelease, error) { - release, ok := s.records[project+"\x00"+releaseID] - if !ok { - return dataset.ProjectRelease{}, dataset.ErrReleaseNotFound - } - return release, nil -} - -func (s *statusReleaseStore) ReadActiveRelease(_ context.Context, project string) (dataset.ActiveRelease, error) { - release, ok := s.active[project] - if !ok { - return dataset.ActiveRelease{}, dataset.ErrNoActiveRelease - } - return release, nil -} - -func (s *statusReleaseStore) CompareAndSwapActivateRelease(_ context.Context, release dataset.ProjectRelease, expected int64) (dataset.ActiveRelease, error) { - current := s.active[release.Project] - if current.Revision != expected { - return dataset.ActiveRelease{}, dataset.ErrReleaseActivationConflict - } - active := dataset.ActiveRelease{Release: release, Revision: expected + 1} - s.active[release.Project] = active - return active, nil -} - -func (s *statusReleaseStore) ListReleaseProjects(context.Context) ([]string, error) { - projects := make([]string, 0, len(s.active)) - for project := range s.active { - projects = append(projects, project) - } - return projects, nil -} - -func (c statusExecutionCatalog) ListExecutions(_ context.Context, state publication.BundleState, before time.Time) ([]publication.BundleExecution, error) { - result := make([]publication.BundleExecution, 0) - for _, execution := range c.executions { - if execution.State.Canonical() == state.Canonical() && execution.UpdatedAt.Before(before) { - result = append(result, execution) - } - } - return result, nil -} - -func TestReleaseProjectStatusResolverDistinguishesReleaseAndAttemptStates(t *testing.T) { - ctx := context.Background() - now := time.Now().UTC() - selector := dataset.DataframeSelector{Recipe: "core", TranslationVersion: "v1", Output: "Patient"} - store := newStatusReleaseStore() - release := dataset.ProjectRelease{ID: "release-1", Project: "current", Generation: "g1", GitCommit: "g1", Publications: []dataset.ReleasePublication{{Selector: selector, ExecutionID: "published-1", Generation: "g1", VerifiedAt: now}}} - if _, err := store.SaveRelease(ctx, release); err != nil { - t.Fatal(err) - } - if _, err := store.CompareAndSwapActivateRelease(ctx, release, 0); err != nil { - t.Fatal(err) - } - catalog := statusExecutionCatalog{executions: []publication.BundleExecution{ - {ID: "replacement-failed", BundleIdentity: publication.BundleIdentity{Name: "core", TranslationVersion: "v1", Project: "current", DatasetGeneration: "g2"}, State: publication.BundleFailed, CreatedAt: now.Add(time.Second), UpdatedAt: now.Add(time.Second), FailureCode: "PUBLICATION_FAILED", Outputs: []publication.BundleOutputRecord{{Name: "Patient", Selector: selector}}}, - {ID: "queued-1", BundleIdentity: publication.BundleIdentity{Name: "core", TranslationVersion: "v1", Project: "building", DatasetGeneration: "g2"}, State: publication.BundleQueued, CreatedAt: now, UpdatedAt: now}, - {ID: "failed-1", BundleIdentity: publication.BundleIdentity{Name: "core", TranslationVersion: "v1", Project: "failed", DatasetGeneration: "g3"}, State: publication.BundleFailed, CreatedAt: now, UpdatedAt: now, FailureCode: "PUBLICATION_FAILED", Outputs: []publication.BundleOutputRecord{{Name: "Patient", Selector: selector}}}, - }} - resolver := releaseProjectStatusResolver{releases: store, executions: catalog} - statuses, err := resolver.DataframeProjectStatuses(ctx, []string{"current", "building", "failed", "missing"}, selector) - if err != nil { - t.Fatal(err) - } - want := []published.ProjectState{published.ProjectFailed, published.ProjectBuilding, published.ProjectFailed, published.ProjectMissing} - for index := range want { - if statuses[index].State != want[index] { - t.Fatalf("statuses[%d] = %#v, want %s", index, statuses[index], want[index]) - } - } - ids, err := resolver.ActiveReleaseExecutionIDs(ctx, []string{"current", "missing"}, selector) - if err != nil || ids["current"] != "published-1" { - t.Fatalf("active release IDs = %#v, %v", ids, err) - } - selectors, controlled, err := resolver.ActiveReleaseSelectors(ctx, []string{"current", "missing"}) - if err != nil || len(selectors) != 1 || selectors[0] != selector || !controlled["current"] || controlled["missing"] { - t.Fatalf("active release selectors = %#v controlled=%#v err=%v", selectors, controlled, err) - } -} diff --git a/internal/server/http_test_helpers_test.go b/internal/server/http_test_helpers_test.go index 8523ef25..1eadd680 100644 --- a/internal/server/http_test_helpers_test.go +++ b/internal/server/http_test_helpers_test.go @@ -14,7 +14,6 @@ import ( "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/explorer/capability" - "github.com/calypr/loom/internal/explorer/capabilitystore" "github.com/gofiber/fiber/v3" ) @@ -40,7 +39,7 @@ func (s *testCapabilityStore) Put(_ context.Context, snapshot capability.Snapsho func (s *testCapabilityStore) GetByToken(_ context.Context, token string) (*capability.Snapshot, error) { stored, ok := s.byToken[token] if !ok { - return nil, capabilitystore.ErrNotFound + return nil, capability.ErrNotFound } result := stored.Clone() return &result, nil @@ -49,7 +48,7 @@ func (s *testCapabilityStore) GetByToken(_ context.Context, token string) (*capa func (s *testCapabilityStore) GetByIdentity(_ context.Context, identity capability.SnapshotIdentity) (*capability.Snapshot, error) { token, ok := s.byIdentity[identity] if !ok { - return nil, capabilitystore.ErrNotFound + return nil, capability.ErrNotFound } return s.GetByToken(context.Background(), token) } @@ -58,6 +57,10 @@ func (compilerTestRegistry) LoadRecipe(context.Context, string) (exec.Entry, err return exec.Entry{}, nil } +func (r compilerTestRegistry) LoadRecipeVersion(ctx context.Context, name, _ string) (exec.Entry, error) { + return r.LoadRecipe(ctx, name) +} + type testHTTPResponse struct { StatusCode int Body string @@ -84,36 +87,17 @@ func requestJSON(t *testing.T, app *fiber.App, method, path, body string) testHT type testExplorerStore struct { explorer.Store mu sync.Mutex - configs map[string]explorer.RepositoryConfig explorers map[string]explorer.Explorer receipts map[string]explorer.CompilationReceipt revisions map[string]explorer.Revision } func newTestExplorerStore() *testExplorerStore { - return &testExplorerStore{configs: map[string]explorer.RepositoryConfig{}, explorers: map[string]explorer.Explorer{}, receipts: map[string]explorer.CompilationReceipt{}, revisions: map[string]explorer.Revision{}} + return &testExplorerStore{explorers: map[string]explorer.Explorer{}, receipts: map[string]explorer.CompilationReceipt{}, revisions: map[string]explorer.Revision{}} } func testExplorerKey(project, id string) string { return project + "\x00" + id } -func (s *testExplorerStore) GetRepositoryConfig(_ context.Context, project string) (*explorer.RepositoryConfig, error) { - s.mu.Lock() - defer s.mu.Unlock() - value, ok := s.configs[project] - if !ok { - return nil, explorer.ErrNotFound - } - return &value, nil -} - -func (s *testExplorerStore) SaveRepositoryConfig(_ context.Context, value explorer.RepositoryConfig) (*explorer.RepositoryConfig, error) { - s.mu.Lock() - defer s.mu.Unlock() - value.ExplorerID, value.Management = "default", explorer.ManagementRepository - s.configs[value.Project] = value - return &value, nil -} - func (s *testExplorerStore) List(_ context.Context, project string) ([]explorer.Explorer, error) { s.mu.Lock() defer s.mu.Unlock() @@ -136,11 +120,7 @@ func (s *testExplorerStore) Get(_ context.Context, project, id string) (*explore return &value, nil } -func (s *testExplorerStore) CreateInteractive(_ context.Context, value explorer.Explorer) (*explorer.Explorer, error) { - return s.create(value) -} - -func (s *testExplorerStore) CreateRepository(_ context.Context, value explorer.Explorer) (*explorer.Explorer, error) { +func (s *testExplorerStore) Create(_ context.Context, value explorer.Explorer) (*explorer.Explorer, error) { return s.create(value) } @@ -219,14 +199,14 @@ func (s *testExplorerStore) GetRevision(_ context.Context, id string) (*explorer return &value, nil } -func (s *testExplorerStore) TransitionRevision(_ context.Context, id string, status explorer.RevisionStatus, diagnostics []explorer.Diagnostic) (*explorer.Revision, error) { +func (s *testExplorerStore) FailRevision(_ context.Context, id string, diagnostics []explorer.Diagnostic) (*explorer.Revision, error) { s.mu.Lock() defer s.mu.Unlock() value, ok := s.revisions[id] if !ok { return nil, explorer.ErrNotFound } - value.Status, value.Diagnostics = status, append([]explorer.Diagnostic(nil), diagnostics...) + value.Status, value.Diagnostics = explorer.RevisionFailed, append([]explorer.Diagnostic(nil), diagnostics...) s.revisions[id] = value return &value, nil } @@ -247,9 +227,7 @@ func (s *testExplorerStore) activateLocked(project, explorerID, revisionID strin if !ok { return explorer.ErrNotFound } - owner.ActiveRevisionID, owner.ActiveConfig = revision.ID, append([]byte(nil), revision.Config...) - owner.RecipeDigest, owner.ResolvedSchemaDigest, owner.SourceGeneration = revision.RecipeDigest, revision.ResolvedSchemaDigest, revision.SourceGeneration - owner.Materializations, owner.Dataset, owner.Publication = revision.Materializations, revision.Dataset, revision.Publication + owner.ActiveRevisionID = revision.ID owner.UpdatedAt = time.Now().UTC() s.explorers[key] = owner revision.Status = explorer.RevisionActive diff --git a/internal/server/local_workspace_writeback.go b/internal/server/local_workspace_writeback.go new file mode 100644 index 00000000..44456377 --- /dev/null +++ b/internal/server/local_workspace_writeback.go @@ -0,0 +1,73 @@ +package server + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/calypr/loom/internal/projectid" +) + +func localWorkspaceWriter(path, configuredProject string) (func(context.Context, string, string, []byte) error, error) { + path = strings.TrimSpace(path) + if path == "" { + return nil, nil + } + configuredProject = projectid.Canonical(configuredProject) + if configuredProject == "" { + return nil, fmt.Errorf("local workspace writeback project is required") + } + if !filepath.IsAbs(path) || !strings.EqualFold(filepath.Ext(path), ".json") { + return nil, fmt.Errorf("local workspace writeback must be an absolute .json file") + } + directory := filepath.Dir(path) + info, err := os.Stat(directory) + if err != nil || !info.IsDir() { + return nil, fmt.Errorf("local workspace writeback directory %q is unavailable", directory) + } + return func(ctx context.Context, project, _ string, workspace []byte) error { + if err := ctx.Err(); err != nil { + return err + } + if projectid.Canonical(project) != configuredProject { + return fmt.Errorf("project %q cannot update the local workspace for %q", project, configuredProject) + } + if len(workspace) == 0 { + return fmt.Errorf("published workspace is empty") + } + temporary, err := os.CreateTemp(directory, ".loom-workspace-*.json") + if err != nil { + return fmt.Errorf("create temporary workspace: %w", err) + } + temporaryPath := temporary.Name() + cleanup := func() { _ = os.Remove(temporaryPath) } + defer cleanup() + if err := temporary.Chmod(0o644); err != nil { + _ = temporary.Close() + return err + } + if _, err := temporary.Write(workspace); err != nil { + _ = temporary.Close() + return fmt.Errorf("write temporary workspace: %w", err) + } + if len(workspace) == 0 || workspace[len(workspace)-1] != '\n' { + if _, err := temporary.Write([]byte{'\n'}); err != nil { + _ = temporary.Close() + return err + } + } + if err := temporary.Sync(); err != nil { + _ = temporary.Close() + return fmt.Errorf("sync temporary workspace: %w", err) + } + if err := temporary.Close(); err != nil { + return err + } + if err := os.Rename(temporaryPath, path); err != nil { + return fmt.Errorf("replace local workspace %q: %w", path, err) + } + return nil + }, nil +} diff --git a/internal/server/local_workspace_writeback_test.go b/internal/server/local_workspace_writeback_test.go new file mode 100644 index 00000000..0006f8e0 --- /dev/null +++ b/internal/server/local_workspace_writeback_test.go @@ -0,0 +1,56 @@ +package server + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +func TestLocalWorkspaceWriterIsOptInAndAtomic(t *testing.T) { + if writer, err := localWorkspaceWriter("", ""); err != nil || writer != nil { + t.Fatalf("disabled writer should be nil; error = %v", err) + } + directory := t.TempDir() + path := filepath.Join(directory, "workspace.json") + if err := os.WriteFile(path, []byte("old\n"), 0o644); err != nil { + t.Fatal(err) + } + writer, err := localWorkspaceWriter(path, "HTAN_INT/BForePC") + if err != nil { + t.Fatal(err) + } + if err := writer(context.Background(), "HTAN_INT-BForePC", "default", []byte(`{"kind":"workspace"}`)); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(got) != "{\"kind\":\"workspace\"}\n" { + t.Fatalf("workspace = %q", got) + } + matches, err := filepath.Glob(filepath.Join(directory, ".loom-workspace-*.json")) + if err != nil || len(matches) != 0 { + t.Fatalf("temporary files = %#v, %v", matches, err) + } +} + +func TestLocalWorkspaceWriterRequiresExactAbsoluteJSONPath(t *testing.T) { + for _, path := range []string{"relative.json", filepath.Join(t.TempDir(), "workspace.txt"), filepath.Join(t.TempDir(), "missing", "workspace.json")} { + if _, err := localWorkspaceWriter(path, "project-a"); err == nil { + t.Fatalf("localWorkspaceWriter(%q) unexpectedly succeeded", path) + } + } +} + +func TestLocalWorkspaceWriterRejectsAnotherProject(t *testing.T) { + path := filepath.Join(t.TempDir(), "workspace.json") + writer, err := localWorkspaceWriter(path, "HTAN_INT/BForePC") + if err != nil { + t.Fatal(err) + } + if err := writer(context.Background(), "HTAN_INT/Other", "default", []byte(`{}`)); err == nil { + t.Fatal("another project updated the configured workspace") + } +} diff --git a/internal/server/openapi_data_routes.go b/internal/server/openapi_data_routes.go index 36255b6d..bf6aca70 100644 --- a/internal/server/openapi_data_routes.go +++ b/internal/server/openapi_data_routes.go @@ -2,528 +2,202 @@ package server import ( "context" + "encoding/json" "net/http" loomapi "github.com/calypr/loom/generated/loomapi" + httpapi "github.com/calypr/loom/internal/api/http" + "github.com/calypr/loom/internal/authscope" ) func (r *HTTPRoutes) GetHealth(ctx context.Context, _ loomapi.GetHealthRequestObject) (loomapi.GetHealthResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.server.HandleHealth) + body, status := r.server.Health(ctx) + value, err := rawJSON(body) if err != nil { return nil, err } if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetHealth200JSONResponse](body) - return response, decodeErr + return loomapi.GetHealth200JSONResponse(value), nil } if status == http.StatusServiceUnavailable { - response, decodeErr := decodeResponse[loomapi.GetHealth503JSONResponse](body) - return response, decodeErr + return loomapi.GetHealth503JSONResponse(value), nil } return nil, unexpectedResponseStatus("getHealth", status) } func (r *HTTPRoutes) GetLiveness(ctx context.Context, _ loomapi.GetLivenessRequestObject) (loomapi.GetLivenessResponseObject, error) { - _, body, err := runFiberHandler(ctx, r.server.HandleLiveness) + body, status := r.server.Liveness(ctx) + value, err := rawJSON(body) if err != nil { return nil, err } - response, decodeErr := decodeResponse[loomapi.GetLiveness200JSONResponse](body) - return response, decodeErr + if status != http.StatusOK { + return nil, unexpectedResponseStatus("getLiveness", status) + } + return loomapi.GetLiveness200JSONResponse(value), nil } func (r *HTTPRoutes) GetReadiness(ctx context.Context, _ loomapi.GetReadinessRequestObject) (loomapi.GetReadinessResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.server.HandleReadiness) + body, status := r.server.Readiness(ctx) + value, err := rawJSON(body) if err != nil { return nil, err } if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetReadiness200JSONResponse](body) - return response, decodeErr + return loomapi.GetReadiness200JSONResponse(value), nil + } + if status == http.StatusServiceUnavailable { + return loomapi.GetReadiness503JSONResponse(value), nil } - response, decodeErr := decodeResponse[loomapi.GetReadiness503JSONResponse](body) - return response, decodeErr + return nil, unexpectedResponseStatus("getReadiness", status) } -func (r *HTTPRoutes) UploadProjectResource(ctx context.Context, _ loomapi.UploadProjectResourceRequestObject) (loomapi.UploadProjectResourceResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleResource) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.UploadProjectResource200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr +func (r *HTTPRoutes) GetDatasetGenerationStatus(ctx context.Context, request loomapi.GetDatasetGenerationStatusRequestObject) (loomapi.GetDatasetGenerationStatusResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.GetDatasetGenerationStatus(ctx, request.Project, request.Generation, optionalString(request.Params.AuthResourcePath), principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.GetDatasetGenerationStatus200JSONResponse(value), nil } + status, body := mapServiceError(err, ctx) switch status { case http.StatusBadRequest: - return loomapi.UploadProjectResource400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil + return loomapi.GetDatasetGenerationStatus400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(body)}, nil case http.StatusUnauthorized: - return loomapi.UploadProjectResource401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil + return loomapi.GetDatasetGenerationStatus401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(body)}, nil case http.StatusForbidden: - return loomapi.UploadProjectResource403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case http.StatusUnsupportedMediaType: - return loomapi.UploadProjectResource415JSONResponse{ServiceUnsupportedMediaTypeJSONResponse: loomapi.ServiceUnsupportedMediaTypeJSONResponse(value)}, nil - case http.StatusUnprocessableEntity: - return loomapi.UploadProjectResource422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil + return loomapi.GetDatasetGenerationStatus403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(body)}, nil + case http.StatusNotFound: + return loomapi.GetDatasetGenerationStatus404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(body)}, nil case http.StatusInternalServerError: - return loomapi.UploadProjectResource500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil + return loomapi.GetDatasetGenerationStatus500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(body)}, nil case http.StatusServiceUnavailable: - return loomapi.UploadProjectResource503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil + return loomapi.GetDatasetGenerationStatus503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(body)}, nil default: - return nil, unexpectedResponseStatus("uploadProjectResource", status) + return nil, unexpectedResponseStatus("getDatasetGenerationStatus", status) } } -func (r *HTTPRoutes) CreateDatasetGeneration(ctx context.Context, _ loomapi.CreateDatasetGenerationRequestObject) (loomapi.CreateDatasetGenerationResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleCreateGeneration) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.CreateDatasetGeneration200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr +func (r *HTTPRoutes) CreateDatasetGeneration(ctx context.Context, request loomapi.CreateDatasetGenerationRequestObject) (loomapi.CreateDatasetGenerationResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.CreateDatasetGeneration(ctx, request.Project, request.Generation, request.Body, principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.CreateDatasetGeneration200JSONResponse(value), nil } + status, body := mapServiceError(err, ctx) switch status { case http.StatusBadRequest: - return loomapi.CreateDatasetGeneration400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(body)}, nil case http.StatusUnauthorized: - return loomapi.CreateDatasetGeneration401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(body)}, nil case http.StatusForbidden: - return loomapi.CreateDatasetGeneration403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(body)}, nil case http.StatusConflict: - return loomapi.CreateDatasetGeneration409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(body)}, nil case http.StatusUnsupportedMediaType: - return loomapi.CreateDatasetGeneration415JSONResponse{ServiceUnsupportedMediaTypeJSONResponse: loomapi.ServiceUnsupportedMediaTypeJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration415JSONResponse{ServiceUnsupportedMediaTypeJSONResponse: loomapi.ServiceUnsupportedMediaTypeJSONResponse(body)}, nil case http.StatusUnprocessableEntity: - return loomapi.CreateDatasetGeneration422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(body)}, nil case http.StatusInternalServerError: - return loomapi.CreateDatasetGeneration500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(body)}, nil case http.StatusServiceUnavailable: - return loomapi.CreateDatasetGeneration503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil + return loomapi.CreateDatasetGeneration503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(body)}, nil default: return nil, unexpectedResponseStatus("createDatasetGeneration", status) } } -func (r *HTTPRoutes) ActivateDatasetGeneration(ctx context.Context, _ loomapi.ActivateDatasetGenerationRequestObject) (loomapi.ActivateDatasetGenerationResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleActivateGeneration) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.ActivateDatasetGeneration200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr +func (r *HTTPRoutes) ActivateDatasetGeneration(ctx context.Context, request loomapi.ActivateDatasetGenerationRequestObject) (loomapi.ActivateDatasetGenerationResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.ActivateDatasetGeneration(ctx, request.Project, request.Generation, request.Params.DataframeExecutionId, optionalString(request.Params.AuthResourcePath), principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.ActivateDatasetGeneration200JSONResponse(value), nil } + status, body := mapServiceError(err, ctx) switch status { case http.StatusBadRequest: - return loomapi.ActivateDatasetGeneration400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil + return loomapi.ActivateDatasetGeneration400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(body)}, nil case http.StatusUnauthorized: - return loomapi.ActivateDatasetGeneration401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil + return loomapi.ActivateDatasetGeneration401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(body)}, nil case http.StatusForbidden: - return loomapi.ActivateDatasetGeneration403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil + return loomapi.ActivateDatasetGeneration403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(body)}, nil case http.StatusConflict: - return loomapi.ActivateDatasetGeneration409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil + return loomapi.ActivateDatasetGeneration409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(body)}, nil case http.StatusInternalServerError: - return loomapi.ActivateDatasetGeneration500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil + return loomapi.ActivateDatasetGeneration500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(body)}, nil case http.StatusServiceUnavailable: - return loomapi.ActivateDatasetGeneration503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil + return loomapi.ActivateDatasetGeneration503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(body)}, nil default: return nil, unexpectedResponseStatus("activateDatasetGeneration", status) } } -func (r *HTTPRoutes) UploadRawNDJSON(ctx context.Context, _ loomapi.UploadRawNDJSONRequestObject) (loomapi.UploadRawNDJSONResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleLoadRaw) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.UploadRawNDJSON200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case http.StatusBadRequest: - return loomapi.UploadRawNDJSON400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil - case http.StatusUnauthorized: - return loomapi.UploadRawNDJSON401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case http.StatusForbidden: - return loomapi.UploadRawNDJSON403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case http.StatusUnsupportedMediaType: - return loomapi.UploadRawNDJSON415JSONResponse{ServiceUnsupportedMediaTypeJSONResponse: loomapi.ServiceUnsupportedMediaTypeJSONResponse(value)}, nil - case http.StatusUnprocessableEntity: - return loomapi.UploadRawNDJSON422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil - case http.StatusInternalServerError: - return loomapi.UploadRawNDJSON500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case http.StatusServiceUnavailable: - return loomapi.UploadRawNDJSON503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("uploadRawNDJSON", status) - } -} - -func (r *HTTPRoutes) CreateSnapshot(ctx context.Context, _ loomapi.CreateSnapshotRequestObject) (loomapi.CreateSnapshotResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleCreateSnapshot) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.CreateSnapshot200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 400: - return loomapi.CreateSnapshot400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil - case 401: - return loomapi.CreateSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.CreateSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 409: - return loomapi.CreateSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil - case 500: - return loomapi.CreateSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.CreateSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("createSnapshot", status) - } -} - -func (r *HTTPRoutes) GetSnapshot(ctx context.Context, _ loomapi.GetSnapshotRequestObject) (loomapi.GetSnapshotResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleSnapshotStatus) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetSnapshot200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 401: - return loomapi.GetSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.GetSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 500: - return loomapi.GetSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.GetSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("getSnapshot", status) - } -} - -func (r *HTTPRoutes) AbortSnapshot(ctx context.Context, _ loomapi.AbortSnapshotRequestObject) (loomapi.AbortSnapshotResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleAbortSnapshot) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.AbortSnapshot200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 401: - return loomapi.AbortSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.AbortSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.AbortSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.AbortSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil - case 500: - return loomapi.AbortSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.AbortSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("abortSnapshot", status) - } -} - -func (r *HTTPRoutes) UploadSnapshotResource(ctx context.Context, _ loomapi.UploadSnapshotResourceRequestObject) (loomapi.UploadSnapshotResourceResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleUploadSnapshotResource) +func (r *HTTPRoutes) GetRecipeExecution(ctx context.Context, request loomapi.GetRecipeExecutionRequestObject) (loomapi.GetRecipeExecutionResponseObject, error) { + body, status := r.recipeExecution(ctx, request.Id) + value, err := rawJSON(body) if err != nil { return nil, err } if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.UploadSnapshotResource200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 400: - return loomapi.UploadSnapshotResource400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil - case 401: - return loomapi.UploadSnapshotResource401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.UploadSnapshotResource403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.UploadSnapshotResource404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.UploadSnapshotResource409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil - case 422: - return loomapi.UploadSnapshotResource422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.UploadSnapshotResource500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.UploadSnapshotResource503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("uploadSnapshotResource", status) - } -} - -func (r *HTTPRoutes) FinalizeSnapshot(ctx context.Context, _ loomapi.FinalizeSnapshotRequestObject) (loomapi.FinalizeSnapshotResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleFinalizeSnapshot) - if err != nil { - return nil, err + return loomapi.GetRecipeExecution200JSONResponse(value), nil } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.FinalizeSnapshot200JSONResponse](body) - return response, decodeErr + if status == http.StatusUnauthorized { + return loomapi.GetRecipeExecution401JSONResponse(value), nil } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr + if status == http.StatusForbidden { + legacy, legacyErr := legacyErrorFromMap(value) + if legacyErr != nil { + return nil, legacyErr + } + return loomapi.GetRecipeExecution403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(legacy)}, nil } - switch status { - case 400: - return loomapi.FinalizeSnapshot400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil - case 401: - return loomapi.FinalizeSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.FinalizeSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.FinalizeSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.FinalizeSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil - case 422: - return loomapi.FinalizeSnapshot422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.FinalizeSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.FinalizeSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("finalizeSnapshot", status) + if status == http.StatusNotFound { + legacy, legacyErr := legacyErrorFromMap(value) + if legacyErr != nil { + return nil, legacyErr + } + return loomapi.GetRecipeExecution404JSONResponse{LegacyNotFoundJSONResponse: loomapi.LegacyNotFoundJSONResponse(legacy)}, nil } + return nil, unexpectedResponseStatus("getRecipeExecution", status) } -func (r *HTTPRoutes) CreateRelease(ctx context.Context, _ loomapi.CreateReleaseRequestObject) (loomapi.CreateReleaseResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleCreateRelease) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.CreateRelease200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 400: - return loomapi.CreateRelease400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil - case 401: - return loomapi.CreateRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.CreateRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.CreateRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.CreateRelease409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil - case 422: - return loomapi.CreateRelease422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.CreateRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.CreateRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("createRelease", status) +func optionalString(value *string) string { + if value == nil { + return "" } + return *value } -func (r *HTTPRoutes) GetActiveRelease(ctx context.Context, _ loomapi.GetActiveReleaseRequestObject) (loomapi.GetActiveReleaseResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleActiveRelease) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetActiveRelease200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 401: - return loomapi.GetActiveRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.GetActiveRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetActiveRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 500: - return loomapi.GetActiveRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.GetActiveRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("getActiveRelease", status) - } +func mapServiceError(err error, ctx context.Context) (int, loomapi.ServiceErrorResponse) { + mapped := httpapi.MapDataframeError(err, httpapi.RequestIDFromContext(ctx)) + return mapped.Status, serviceErrorResponse(mapped.Body) } -func (r *HTTPRoutes) GetRelease(ctx context.Context, _ loomapi.GetReleaseRequestObject) (loomapi.GetReleaseResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleReleaseStatus) - if err != nil { - return nil, err +func rawJSON(value any) (loomapi.RawJSON, error) { + if value == nil { + return loomapi.RawJSON{}, nil } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetRelease200JSONResponse](body) - return response, decodeErr + if raw, ok := value.(loomapi.RawJSON); ok { + return raw, nil } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 401: - return loomapi.GetRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.GetRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 500: - return loomapi.GetRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.GetRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("getRelease", status) - } -} - -func (r *HTTPRoutes) ActivateRelease(ctx context.Context, _ loomapi.ActivateReleaseRequestObject) (loomapi.ActivateReleaseResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleActivateRelease) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.ActivateRelease200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 400: - return loomapi.ActivateRelease400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil - case 401: - return loomapi.ActivateRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.ActivateRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.ActivateRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.ActivateRelease409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil - case 422: - return loomapi.ActivateRelease422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.ActivateRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.ActivateRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("activateRelease", status) - } -} - -func (r *HTTPRoutes) ActivateReleaseCompatibility(ctx context.Context, _ loomapi.ActivateReleaseCompatibilityRequestObject) (loomapi.ActivateReleaseCompatibilityResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.load.HandleActivateReleaseCompatibility) + payload, err := json.Marshal(value) if err != nil { return nil, err } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.ActivateReleaseCompatibility200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 400: - return loomapi.ActivateReleaseCompatibility400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil - case 401: - return loomapi.ActivateReleaseCompatibility401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil - case 403: - return loomapi.ActivateReleaseCompatibility403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.ActivateReleaseCompatibility404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.ActivateReleaseCompatibility409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(value)}, nil - case 422: - return loomapi.ActivateReleaseCompatibility422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.ActivateReleaseCompatibility500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.ActivateReleaseCompatibility503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("activateReleaseCompatibility", status) - } -} - -func (r *HTTPRoutes) GetRecipeExecution(ctx context.Context, _ loomapi.GetRecipeExecutionRequestObject) (loomapi.GetRecipeExecutionResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.recipe.Handle) - if err != nil { + var result loomapi.RawJSON + if err := json.Unmarshal(payload, &result); err != nil { return nil, err } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetRecipeExecution200JSONResponse](body) - return response, decodeErr - } - if status == http.StatusUnauthorized { - response, decodeErr := decodeResponse[loomapi.GetRecipeExecution401JSONResponse](body) - return response, decodeErr - } - value, decodeErr := legacyError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 403: - return loomapi.GetRecipeExecution403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetRecipeExecution404JSONResponse{LegacyNotFoundJSONResponse: loomapi.LegacyNotFoundJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("getRecipeExecution", status) - } + return result, nil } diff --git a/internal/server/openapi_explorer_authoring_direct.go b/internal/server/openapi_explorer_authoring_direct.go new file mode 100644 index 00000000..630a13ad --- /dev/null +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -0,0 +1,271 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/http" + + loomapi "github.com/calypr/loom/generated/loomapi" + "github.com/calypr/loom/internal/authscope" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/lifecycle" +) + +func directAuthoringJSON[T any](value any) (T, error) { + var result T + raw, err := json.Marshal(value) + if err != nil { + return result, err + } + if err := json.Unmarshal(raw, &result); err != nil { + return result, err + } + return result, nil +} + +func (h *explorerHTTPHandlers) authoringReadDirect(ctx context.Context, project string) error { + if h == nil || h.authorizeRead == nil { + return explorerUnavailable("authorization", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") + } + principal, _ := authscope.PrincipalFromContext(ctx) + if err := h.authorizeRead(ctx, principal, project); err != nil { + return &explorer.AuthoringError{Status: http.StatusForbidden, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err} + } + return nil +} + +func authResourcePathFromParam(value *loomapi.AuthResourcePath) string { + if value == nil { + return "" + } + return string(*value) +} + +func (h *explorerHTTPHandlers) authoringWriteDirect(ctx context.Context, project, authResourcePath string) error { + if h == nil || h.authorizer == nil { + return explorerUnavailable("authorization", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") + } + principal, _ := authscope.PrincipalFromContext(ctx) + if err := h.authorizer.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { + return &explorer.AuthoringError{Status: http.StatusForbidden, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err} + } + return nil +} + +func (h *explorerHTTPHandlers) getAuthoringCapabilityDirect(ctx context.Context, project string) (loomapi.AuthoringCapability, error) { + var result loomapi.AuthoringCapability + if err := h.authoringReadDirect(ctx, project); err != nil { + return result, err + } + return loomapi.AuthoringCapability{ApiVersion: loomapi.LoomCalyprOrgexplorerAuthoringv2, Kind: loomapi.ExplorerAuthoringCapabilities, Operations: []loomapi.AuthoringCapabilityOperations{loomapi.Builder, loomapi.Suggestions, loomapi.Preview, loomapi.Publish, loomapi.Commands, loomapi.Reconcile}, PreviewLimits: []int{10, 25, 50, 100}, Features: loomapi.AuthoringFeatures{EmissionFilters: true, EmissionCharts: true}}, nil +} + +func (h *explorerHTTPHandlers) searchAuthoringSuggestionsDirect(ctx context.Context, project, explorerID string, body *loomapi.SearchExplorerCandidatesJSONRequestBody) (loomapi.CandidateSearchResponse, error) { + var result loomapi.CandidateSearchResponse + if err := h.authoringReadDirect(ctx, project); err != nil { + return result, err + } + if body == nil { + return result, malformedRouteError("suggestions", errors.New("request body is required")) + } + query := "" + if body.Query != nil { + query = *body.Query + } + value, err := h.application.Suggestions(ctx, lifecycle.SuggestionsRequest{Project: project, ExplorerID: explorerID, SnapshotToken: body.SnapshotToken, NodeID: body.NodeId, Query: query}) + if err != nil { + return result, err + } + candidates := make([]loomapi.CatalogCandidate, 0, len(value.Candidates)) + for _, candidate := range value.Candidates { + wire, convertErr := directAuthoringJSON[loomapi.CatalogCandidate](candidate) + if convertErr != nil { + return result, convertErr + } + candidates = append(candidates, wire) + } + return loomapi.CandidateSearchResponse{ApiVersion: loomapi.LoomCalyprOrgexplorerAuthoringv2, Kind: loomapi.ExplorerBuilderCandidateSuggestions, SnapshotToken: value.SnapshotToken, NodeId: value.NodeID, Candidates: candidates, Diagnostics: []loomapi.Diagnostic{}}, nil +} + +func (h *explorerHTTPHandlers) getAuthoringBuilderDirect(ctx context.Context, project, explorerID string) (loomapi.BuilderState, error) { + var result loomapi.BuilderState + if err := h.authoringReadDirect(ctx, project); err != nil { + return result, err + } + value, err := h.application.Builder(ctx, lifecycle.BuilderRequest{Project: project, ExplorerID: explorerID}) + if err != nil { + return result, err + } + return directAuthoringJSON[loomapi.BuilderState](value) +} + +func (h *explorerHTTPHandlers) applyAuthoringCommandsDirect(ctx context.Context, project, explorerID, authResourcePath string, body *loomapi.ApplyExplorerBuilderCommandsJSONRequestBody) (loomapi.ApplyCommandsResponse, error) { + var result loomapi.ApplyCommandsResponse + if err := h.authoringWriteDirect(ctx, project, authResourcePath); err != nil { + return result, err + } + if body == nil { + return result, malformedRouteError("commands", errors.New("request body is required")) + } + request, err := directAuthoringJSON[authoringv2.ApplyCommandsRequest](*body) + if err != nil { + return result, malformedRouteError("commands", err) + } + value, err := h.application.ApplyCommands(ctx, project, explorerID, request, subjectFromContext(ctx)) + if err != nil { + return result, err + } + return directAuthoringJSON[loomapi.ApplyCommandsResponse](value) +} + +func (h *explorerHTTPHandlers) reconcileAuthoringDirect(ctx context.Context, project, explorerID, authResourcePath string, body *loomapi.ReconcileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { + var result loomapi.CompileResponse + if err := h.authoringWriteDirect(ctx, project, authResourcePath); err != nil { + return result, err + } + if body == nil { + return result, malformedRouteError("reconcile", errors.New("request body is required")) + } + receipt, err := h.application.Reconcile(ctx, lifecycle.ReconcileRequest{Project: project, ExplorerID: explorerID, SnapshotToken: body.SnapshotToken, DraftVersion: body.DraftVersion, DraftDigest: body.DraftDigest}) + if err != nil { + return result, err + } + return v2ReceiptResponse(receipt, authoringv2.Workspace{}), nil +} + +func (h *explorerHTTPHandlers) previewAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.PreviewExplorerJSONRequestBody) (loomapi.PreviewResponse, error) { + var result loomapi.PreviewResponse + if err := h.authoringReadDirect(ctx, project); err != nil { + return result, err + } + if body == nil { + return result, malformedRouteError("preview", errors.New("receiptId and outputId are required")) + } + limit := dataframeexecution.DefaultPreviewLimit + if body.Limit != nil { + limit = *body.Limit + if limit == 0 { + limit = -1 + } + } + var finish func() ([]byte, error) + _, err := h.application.Preview(ctx, lifecycle.PreviewRequest{Project: project, ExplorerID: explorerID, ReceiptID: body.ReceiptId, OutputID: body.OutputId, Limit: limit, SinkFactory: func(receipt *explorer.CompilationReceipt, columns []explorer.EmittedColumn) (func(map[string]any) error, error) { + encoder, encoderErr := newPreviewResponseEncoder(receipt, body.OutputId, columns, maxExplorerPreviewResponseBytes) + if encoderErr != nil { + return nil, encoderErr + } + finish = encoder.Finish + return encoder.Visit, nil + }}) + if err != nil { + return result, previewRouteError(err) + } + if finish == nil { + return result, previewRouteError(errors.New("native preview response sink was not configured")) + } + encoded, err := finish() + if err != nil { + return result, previewRouteError(err) + } + if err := json.Unmarshal(encoded, &result); err != nil { + return result, err + } + return result, nil +} + +func (h *explorerHTTPHandlers) publishAuthoringDirect(ctx context.Context, project, explorerID, authResourcePath string, body *loomapi.PublishExplorerJSONRequestBody) (loomapi.PublishResponse, error) { + var result loomapi.PublishResponse + if err := h.authoringWriteDirect(ctx, project, authResourcePath); err != nil { + return result, err + } + if body == nil { + return result, malformedRouteError("publish", errors.New("receiptId is required")) + } + value, err := h.application.Publish(ctx, lifecycle.PublishRequest{Project: project, ExplorerID: explorerID, ReceiptID: body.ReceiptId, Actor: subjectFromContext(ctx)}) + if err != nil { + return result, err + } + outputs := make([]loomapi.PublicationOutput, 0, len(value.Revision.Materializations)) + for _, materialization := range value.Revision.Materializations { + outputs = append(outputs, loomapi.PublicationOutput{OutputId: firstNonEmpty(materialization.OutputID, materialization.Output), State: "READY", MaterializationId: materialization.MaterializationID}) + } + return loomapi.PublishResponse{ApiVersion: loomapi.LoomCalyprOrgexplorerAuthoringv2, Kind: loomapi.ExplorerBuilderPublication, ReceiptId: value.Receipt.ID, RevisionId: value.Revision.ID, State: string(value.Revision.Status), Outputs: outputs, Diagnostics: []loomapi.Diagnostic{}}, nil +} + +func authoringErrorForOpenAPI(ctx context.Context, operation string, err error) (int, loomapi.ErrorResponse) { + requestID := requestIDFromContext(ctx) + status := http.StatusInternalServerError + code, message, stage, path := "INTERNAL_ERROR", "internal server error", "internal", "" + var details map[string]any + var cause error = err + var lifecycleErr *lifecycle.Error + var authoringErr *explorer.AuthoringError + if errors.As(err, &lifecycleErr) { + status = lifecycleErrorStatus(lifecycleErr.Class) + code, message, stage, path, details, cause = lifecycleErr.Code, lifecycleErr.Message, lifecycleErr.Stage, lifecycleErr.Path, lifecycleErr.Details, lifecycleErr.Cause + } else if errors.As(err, &authoringErr) { + status = authoringErr.Status + code, message, stage, path, details = authoringErr.Diagnostic.Code, authoringErr.Diagnostic.Message, authoringErr.Diagnostic.Stage, authoringErr.Diagnostic.JSONPath, authoringErr.Diagnostic.Details + if authoringErr.Cause != nil { + cause = authoringErr.Cause + } + } else if errors.Is(err, explorer.ErrNotFound) { + status, code, message = http.StatusNotFound, "NOT_FOUND", "Explorer resource not found" + } + if stage == "" { + stage = operation + } + if code == "" { + code = "INTERNAL_ERROR" + } + if message == "" { + message = "internal server error" + } + if cause != nil && status >= 500 { + slog.Error("Explorer authoring request failed", "request_id", requestID, "operation", operation, "status", status, "stage", stage, "code", code, "json_path", path, "cause", cause, "cause_type", fmt.Sprintf("%T", cause)) + } + diagnostic := loomapi.Diagnostic{Severity: "error", Stage: stage, Code: code, Message: message} + if path != "" { + diagnostic.JsonPath = &path + } + if requestID != "" { + diagnostic.RequestId = &requestID + } + if details != nil { + converted := map[string]interface{}(details) + diagnostic.Details = &converted + } + diagnostics := []loomapi.Diagnostic{diagnostic} + body := loomapi.ErrorBody{Code: code, Message: message, Diagnostic: &diagnostic} + if requestID != "" { + body.RequestId = &requestID + } + if details != nil { + body.AdditionalProperties = map[string]interface{}{"details": details} + } + return status, loomapi.ErrorResponse{Error: body, Diagnostics: &diagnostics} +} + +func lifecycleErrorStatus(class lifecycle.ErrorClass) int { + switch class { + case lifecycle.ClassMalformed: + return http.StatusBadRequest + case lifecycle.ClassForbidden: + return http.StatusForbidden + case lifecycle.ClassNotFound: + return http.StatusNotFound + case lifecycle.ClassConflict: + return http.StatusConflict + case lifecycle.ClassUnprocessable: + return http.StatusUnprocessableEntity + case lifecycle.ClassUnavailable: + return http.StatusServiceUnavailable + default: + return http.StatusInternalServerError + } +} diff --git a/internal/server/openapi_explorer_conversion.go b/internal/server/openapi_explorer_conversion.go new file mode 100644 index 00000000..c24e1abe --- /dev/null +++ b/internal/server/openapi_explorer_conversion.go @@ -0,0 +1,63 @@ +package server + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + loomapi "github.com/calypr/loom/generated/loomapi" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + explorercompilation "github.com/calypr/loom/internal/explorer/compilation" +) + +func decodeStrict(raw []byte, value any) error { + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(value); err != nil { + return err + } + var trailing any + if err := decoder.Decode(&trailing); err == nil { + return fmt.Errorf("multiple JSON values") + } + return nil +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + return "Explorer" +} + +func v2ReceiptResponse(receipt *explorer.CompilationReceipt, workspace authoringv2.Workspace) loomapi.CompileResponse { + if receipt != nil && len(receipt.NormalizedBundle) != 0 { + if normalized, err := authoringv2.DecodeWorkspace(receipt.NormalizedBundle); err == nil { + workspace = normalized + } + } + outputs := make([]loomapi.ReceiptOutput, 0, len(workspace.Documents)) + for _, document := range workspace.Documents { + rowGrain := "" + for _, output := range receipt.Bundle.Outputs { + if output.Name == document.Output.ID { + rowGrain = output.RowGrain + break + } + } + columns := make([]loomapi.ContractColumn, 0) + for _, column := range receipt.EmittedColumns { + if column.OutputID != document.Output.ID { + continue + } + label := firstNonEmpty(column.Label, column.PublicColumn) + columns = append(columns, loomapi.ContractColumn{Column: column.PublicColumn, Label: label, LogicalType: column.LogicalType, Filterable: column.Filterable, Chartable: column.Chartable}) + } + outputs = append(outputs, loomapi.ReceiptOutput{OutputId: document.Output.ID, Title: document.Output.Title, RowGrain: rowGrain, Columns: columns}) + } + return loomapi.CompileResponse{ApiVersion: loomapi.LoomCalyprOrgexplorerAuthoringv2, Kind: loomapi.ExplorerBuilderReceipt, ReceiptId: receipt.ID, SnapshotToken: receipt.SnapshotToken, Generation: receipt.SourceGeneration, IntentDigest: receipt.IntentDigest, CompilerVersion: explorer.CurrentCompilerContractVersion + "+" + explorercompilation.TranslationVersion, Builder: workspace, Outputs: outputs, Diagnostics: []loomapi.Diagnostic{}} +} diff --git a/internal/server/openapi_explorer_direct.go b/internal/server/openapi_explorer_direct.go new file mode 100644 index 00000000..d2d6f9a9 --- /dev/null +++ b/internal/server/openapi_explorer_direct.go @@ -0,0 +1,222 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strings" + + loomapi "github.com/calypr/loom/generated/loomapi" + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/lifecycle" + "github.com/calypr/loom/internal/projectid" +) + +// explorerLegacyOperationError carries the legacy Explorer response contract. +// It is a server-side transport value; lifecycle errors are mapped into it at +// the generated OpenAPI boundary. +type explorerLegacyOperationError struct { + status int + code string + message string + stringValue bool + cause error +} + +func (e *explorerLegacyOperationError) Error() string { + if e == nil { + return "Explorer operation failed" + } + return e.message +} + +func (e *explorerLegacyOperationError) Unwrap() error { + if e == nil { + return nil + } + return e.cause +} + +func legacyOperationFailure(status int, code, message string, cause error) error { + return &explorerLegacyOperationError{status: status, code: code, message: message, cause: cause} +} + +func legacyStringFailure(status int, message string, cause error) error { + return &explorerLegacyOperationError{status: status, message: message, stringValue: true, cause: cause} +} + +func lifecycleLegacyFailure(err error) error { + if err == nil { + return nil + } + var value *lifecycle.Error + if !errors.As(err, &value) { + return err + } + status := http.StatusInternalServerError + switch value.Class { + case lifecycle.ClassMalformed: + status = http.StatusBadRequest + case lifecycle.ClassForbidden: + status = http.StatusForbidden + case lifecycle.ClassNotFound: + status = http.StatusNotFound + case lifecycle.ClassConflict: + status = http.StatusConflict + case lifecycle.ClassUnprocessable: + status = http.StatusUnprocessableEntity + case lifecycle.ClassUnavailable: + status = http.StatusServiceUnavailable + } + return legacyOperationFailure(status, value.Code, value.Message, err) +} + +func explorerLegacyResponse(err error) (int, loomapi.LegacyErrorResponse) { + var operation *explorerLegacyOperationError + if errors.As(err, &operation) { + if operation.stringValue { + var response loomapi.LegacyErrorResponse + _ = response.Error.FromLegacyErrorResponseError0(operation.message) + return operation.status, response + } + var response loomapi.LegacyErrorResponse + _ = response.Error.FromLegacyErrorBody(loomapi.LegacyErrorBody{Code: operation.code, Message: operation.message}) + return operation.status, response + } + var response loomapi.LegacyErrorResponse + _ = response.Error.FromLegacyErrorBody(loomapi.LegacyErrorBody{Code: "INTERNAL_ERROR", Message: "internal server error"}) + return http.StatusInternalServerError, response +} + +func (r *explorerHTTPHandlers) listExplorers(ctx context.Context, rawProject string) (loomapi.ListExplorers200JSONResponse, error) { + var result loomapi.ListExplorers200JSONResponse + if r == nil || r.application == nil || r.authorizeRead == nil { + return result, legacyOperationFailure(http.StatusInternalServerError, "INTERNAL_ERROR", "Explorer lifecycle is not configured", nil) + } + project := projectid.Canonical(rawProject) + principal, _ := authscope.PrincipalFromContext(ctx) + if err := r.authorizeRead(ctx, principal, project); err != nil { + return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) + } + value, err := r.application.List(ctx, project) + if err != nil { + return result, lifecycleLegacyFailure(err) + } + raw, err := json.Marshal(value.Summaries) + if err != nil { + return result, err + } + if err := json.Unmarshal(raw, &result); err != nil { + return result, err + } + return result, nil +} + +func (r *explorerHTTPHandlers) createExplorer(ctx context.Context, rawProject, authResourcePath string, body *loomapi.CreateExplorerJSONRequestBody) (loomapi.CreateExplorer201JSONResponse, error) { + var result loomapi.CreateExplorer201JSONResponse + if r == nil || r.application == nil || r.authorizer == nil { + return result, legacyOperationFailure(http.StatusInternalServerError, "INTERNAL_ERROR", "Explorer lifecycle is not configured", nil) + } + project := projectid.Canonical(rawProject) + principal, _ := authscope.PrincipalFromContext(ctx) + if err := r.authorizer.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { + return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) + } + if body == nil { + return result, legacyOperationFailure(http.StatusBadRequest, "MALFORMED_REQUEST", "name is required", nil) + } + var request struct { + Name string `json:"name"` + Title string `json:"title,omitempty"` + SourceExplorerID string `json:"sourceExplorerId,omitempty"` + } + raw, err := json.Marshal(*body) + if err != nil || decodeStrict(raw, &request) != nil { + return result, legacyOperationFailure(http.StatusBadRequest, "MALFORMED_REQUEST", "name is required", err) + } + value, err := r.application.Create(ctx, lifecycle.CreateRequest{Project: project, Name: request.Name, Title: request.Title, SourceExplorerID: request.SourceExplorerID, Actor: subjectFromContext(ctx)}) + if err != nil { + return result, lifecycleLegacyFailure(err) + } + raw, err = json.Marshal(value) + if err != nil { + return result, err + } + if err := json.Unmarshal(raw, &result); err != nil { + return result, err + } + return result, nil +} + +func (r *explorerHTTPHandlers) getExplorer(ctx context.Context, rawProject, rawID string) (loomapi.GetExplorer200JSONResponse, error) { + var result loomapi.GetExplorer200JSONResponse + if r == nil || r.application == nil || r.authorizeRead == nil { + return result, legacyOperationFailure(http.StatusInternalServerError, "INTERNAL_ERROR", "Explorer lifecycle is not configured", nil) + } + project := projectid.Canonical(rawProject) + principal, _ := authscope.PrincipalFromContext(ctx) + if err := r.authorizeRead(ctx, principal, project); err != nil { + return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) + } + value, err := r.application.Get(ctx, project, rawID) + if err != nil { + return result, lifecycleLegacyFailure(err) + } + raw, err := json.Marshal(value) + if err != nil { + return result, err + } + if err := json.Unmarshal(raw, &result); err != nil { + return result, err + } + return result, nil +} + +func (r *explorerHTTPHandlers) publishRepositoryExplorerConfig(ctx context.Context, request loomapi.PublishRepositoryExplorerConfigRequestObject) (loomapi.PublishRepositoryExplorerConfig200JSONResponse, error) { + var result loomapi.PublishRepositoryExplorerConfig200JSONResponse + if r == nil || r.application == nil || r.authorizer == nil { + return result, legacyStringFailure(http.StatusInternalServerError, "Explorer lifecycle is not configured", nil) + } + project := projectid.Canonical(string(request.Project)) + generation := strings.TrimSpace(string(request.Generation)) + if project == "" || generation == "" { + return result, legacyStringFailure(http.StatusBadRequest, "project and generation are required", nil) + } + authResourcePath := "" + if request.Params.AuthResourcePath != nil { + authResourcePath = authscope.NormalizeAuthResourcePath(strings.TrimSpace(*request.Params.AuthResourcePath)) + } + principal, _ := authscope.PrincipalFromContext(ctx) + if err := r.authorizer.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { + return result, legacyStringFailure(http.StatusForbidden, "forbidden", err) + } + if request.Body == nil { + return result, legacyStringFailure(http.StatusUnprocessableEntity, "workspace is required", nil) + } + workspace := authoringv2.Workspace(*request.Body) + if err := workspace.ValidateForPublication(); err != nil { + return result, legacyStringFailure(http.StatusUnprocessableEntity, err.Error(), err) + } + commit := strings.TrimSpace(request.Params.XLoomSourceCommit) + if commit == "" { + return result, legacyStringFailure(http.StatusBadRequest, "X-Loom-Source-Commit is required", nil) + } + value, err := r.application.PublishRepository(ctx, lifecycle.RepositoryPublishRequest{Project: project, Generation: generation, Workspace: workspace, Commit: commit, Actor: subjectFromContext(ctx)}) + if err != nil { + status, legacy := explorerLegacyResponse(lifecycleLegacyFailure(err)) + return result, legacyStringFailure(status, legacyErrorMessage(legacy), err) + } + return loomapi.PublishRepositoryExplorerConfig200JSONResponse(loomapi.RawJSON{"project": project, "generation": generation, "explorerId": "default", "receiptId": value.Receipt.ID, "revisionId": value.Revision.ID, "executionId": value.Execution.ID, "recipe": value.Receipt.Bundle.Name, "translationVersion": value.Receipt.Bundle.TranslationVersion, "activated": true}), nil +} + +func legacyErrorMessage(value loomapi.LegacyErrorResponse) string { + if message, err := value.Error.AsLegacyErrorResponseError0(); err == nil { + return message + } + if body, err := value.Error.AsLegacyErrorBody(); err == nil && strings.TrimSpace(body.Message) != "" { + return body.Message + } + return "Explorer lifecycle failed" +} diff --git a/internal/server/openapi_explorer_direct_test.go b/internal/server/openapi_explorer_direct_test.go new file mode 100644 index 00000000..0bf9867a --- /dev/null +++ b/internal/server/openapi_explorer_direct_test.go @@ -0,0 +1,42 @@ +package server + +import ( + "context" + "net/http" + "testing" + + loomapi "github.com/calypr/loom/generated/loomapi" + "github.com/calypr/loom/internal/explorer/lifecycle" +) + +func TestLegacyErrorMessagePreservesStructuredLifecycleMessage(t *testing.T) { + var response loomapi.LegacyErrorResponse + if err := response.Error.FromLegacyErrorBody(loomapi.LegacyErrorBody{Code: "COMPILATION_FAILED", Message: "catalog field is unavailable"}); err != nil { + t.Fatal(err) + } + if got := legacyErrorMessage(response); got != "catalog field is unavailable" { + t.Fatalf("legacy error message = %q", got) + } +} + +func TestAuthoringErrorMapsPublicationInProgressToConflict(t *testing.T) { + status, response := authoringErrorForOpenAPI(context.Background(), "publish", &lifecycle.Error{ + Class: lifecycle.ClassConflict, + Stage: "materialize", + Code: "PUBLICATION_IN_PROGRESS", + Message: "Explorer publication is already in progress; retry after it completes", + Details: map[string]any{"executionId": "execution-a", "retryable": true}, + }) + if status != http.StatusConflict { + t.Fatalf("status = %d, want %d", status, http.StatusConflict) + } + if response.Error.Code != "PUBLICATION_IN_PROGRESS" || response.Error.Diagnostic == nil { + t.Fatalf("error response = %#v", response) + } + if got := response.Error.AdditionalProperties["details"].(map[string]any)["executionId"]; got != "execution-a" { + t.Fatalf("executionId = %v, want execution-a", got) + } + if got := (*response.Error.Diagnostic.Details)["retryable"]; got != true { + t.Fatalf("retryable = %v, want true", got) + } +} diff --git a/internal/server/openapi_explorer_routes.go b/internal/server/openapi_explorer_routes.go index 762491e5..d8a88089 100644 --- a/internal/server/openapi_explorer_routes.go +++ b/internal/server/openapi_explorer_routes.go @@ -2,510 +2,300 @@ package server import ( "context" + "fmt" "net/http" loomapi "github.com/calypr/loom/generated/loomapi" - "github.com/gofiber/fiber/v3" ) -func (r *HTTPRoutes) ListExplorers(ctx context.Context, _ loomapi.ListExplorersRequestObject) (loomapi.ListExplorersResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.lifecycle.list) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.ListExplorers200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := legacyError(body) - if decodeErr != nil { - return nil, decodeErr +func (r *HTTPRoutes) ListExplorers(ctx context.Context, request loomapi.ListExplorersRequestObject) (loomapi.ListExplorersResponseObject, error) { + value, err := r.explorer.listExplorers(ctx, string(request.Project)) + if err == nil { + return loomapi.ListExplorers200JSONResponse(value), nil } + status, legacy := explorerLegacyResponse(err) switch status { - case 403: - return loomapi.ListExplorers403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(value)}, nil - case 500: - return loomapi.ListExplorers500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(value)}, nil + case http.StatusForbidden: + return loomapi.ListExplorers403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(legacy)}, nil + case http.StatusInternalServerError: + return loomapi.ListExplorers500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(legacy)}, nil default: return nil, unexpectedResponseStatus("listExplorers", status) } } -func (r *HTTPRoutes) CreateExplorer(ctx context.Context, _ loomapi.CreateExplorerRequestObject) (loomapi.CreateExplorerResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.lifecycle.create) - if err != nil { - return nil, err - } - if status == http.StatusCreated { - response, decodeErr := decodeResponse[loomapi.CreateExplorer201JSONResponse](body) - return response, decodeErr - } - value, decodeErr := legacyError(body) - if decodeErr != nil { - return nil, decodeErr +func (r *HTTPRoutes) CreateExplorer(ctx context.Context, request loomapi.CreateExplorerRequestObject) (loomapi.CreateExplorerResponseObject, error) { + value, err := r.explorer.createExplorer(ctx, string(request.Project), authResourcePathFromParam(request.Params.AuthResourcePath), request.Body) + if err == nil { + return loomapi.CreateExplorer201JSONResponse(value), nil } + status, legacy := explorerLegacyResponse(err) switch status { - case 400: - return loomapi.CreateExplorer400JSONResponse{LegacyBadRequestJSONResponse: loomapi.LegacyBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.CreateExplorer403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(value)}, nil - case 409: - return loomapi.CreateExplorer409JSONResponse{LegacyConflictJSONResponse: loomapi.LegacyConflictJSONResponse(value)}, nil - case 422: - return loomapi.CreateExplorer422JSONResponse{LegacyUnprocessableJSONResponse: loomapi.LegacyUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.CreateExplorer500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(value)}, nil + case http.StatusBadRequest: + return loomapi.CreateExplorer400JSONResponse{LegacyBadRequestJSONResponse: loomapi.LegacyBadRequestJSONResponse(legacy)}, nil + case http.StatusForbidden: + return loomapi.CreateExplorer403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(legacy)}, nil + case http.StatusConflict: + return loomapi.CreateExplorer409JSONResponse{LegacyConflictJSONResponse: loomapi.LegacyConflictJSONResponse(legacy)}, nil + case http.StatusUnprocessableEntity: + return loomapi.CreateExplorer422JSONResponse{LegacyUnprocessableJSONResponse: loomapi.LegacyUnprocessableJSONResponse(legacy)}, nil + case http.StatusInternalServerError: + return loomapi.CreateExplorer500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(legacy)}, nil default: return nil, unexpectedResponseStatus("createExplorer", status) } } -func (r *HTTPRoutes) GetExplorer(ctx context.Context, _ loomapi.GetExplorerRequestObject) (loomapi.GetExplorerResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.lifecycle.get) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetExplorer200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := legacyError(body) - if decodeErr != nil { - return nil, decodeErr +func (r *HTTPRoutes) GetExplorer(ctx context.Context, request loomapi.GetExplorerRequestObject) (loomapi.GetExplorerResponseObject, error) { + value, err := r.explorer.getExplorer(ctx, string(request.Project), string(request.ExplorerId)) + if err == nil { + return loomapi.GetExplorer200JSONResponse(value), nil } + status, legacy := explorerLegacyResponse(err) switch status { - case 403: - return loomapi.GetExplorer403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetExplorer404JSONResponse{LegacyNotFoundJSONResponse: loomapi.LegacyNotFoundJSONResponse(value)}, nil - case 500: - return loomapi.GetExplorer500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(value)}, nil + case http.StatusForbidden: + return loomapi.GetExplorer403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(legacy)}, nil + case http.StatusNotFound: + return loomapi.GetExplorer404JSONResponse{LegacyNotFoundJSONResponse: loomapi.LegacyNotFoundJSONResponse(legacy)}, nil + case http.StatusInternalServerError: + return loomapi.GetExplorer500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(legacy)}, nil default: return nil, unexpectedResponseStatus("getExplorer", status) } } -func (r *HTTPRoutes) PublishRepositoryExplorerConfig(ctx context.Context, _ loomapi.PublishRepositoryExplorerConfigRequestObject) (loomapi.PublishRepositoryExplorerConfigResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.publishRepositoryConfig) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.PublishRepositoryExplorerConfig200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := legacyError(body) - if decodeErr != nil { - return nil, decodeErr +func (r *HTTPRoutes) PublishRepositoryExplorerConfig(ctx context.Context, request loomapi.PublishRepositoryExplorerConfigRequestObject) (loomapi.PublishRepositoryExplorerConfigResponseObject, error) { + value, err := r.explorer.publishRepositoryExplorerConfig(ctx, request) + if err == nil { + return loomapi.PublishRepositoryExplorerConfig200JSONResponse(value), nil } + status, legacy := explorerLegacyResponse(err) switch status { - case 400: - return loomapi.PublishRepositoryExplorerConfig400JSONResponse{LegacyBadRequestJSONResponse: loomapi.LegacyBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.PublishRepositoryExplorerConfig403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(value)}, nil - case 409: - return loomapi.PublishRepositoryExplorerConfig409JSONResponse{LegacyConflictJSONResponse: loomapi.LegacyConflictJSONResponse(value)}, nil - case 422: - return loomapi.PublishRepositoryExplorerConfig422JSONResponse{LegacyUnprocessableJSONResponse: loomapi.LegacyUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.PublishRepositoryExplorerConfig500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.PublishRepositoryExplorerConfig503JSONResponse{LegacyUnavailableJSONResponse: loomapi.LegacyUnavailableJSONResponse(value)}, nil + case http.StatusBadRequest: + return loomapi.PublishRepositoryExplorerConfig400JSONResponse{LegacyBadRequestJSONResponse: loomapi.LegacyBadRequestJSONResponse(legacy)}, nil + case http.StatusForbidden: + return loomapi.PublishRepositoryExplorerConfig403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(legacy)}, nil + case http.StatusConflict: + return loomapi.PublishRepositoryExplorerConfig409JSONResponse{LegacyConflictJSONResponse: loomapi.LegacyConflictJSONResponse(legacy)}, nil + case http.StatusUnprocessableEntity: + return loomapi.PublishRepositoryExplorerConfig422JSONResponse{LegacyUnprocessableJSONResponse: loomapi.LegacyUnprocessableJSONResponse(legacy)}, nil + case http.StatusInternalServerError: + return loomapi.PublishRepositoryExplorerConfig500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(legacy)}, nil + case http.StatusServiceUnavailable: + return loomapi.PublishRepositoryExplorerConfig503JSONResponse{LegacyUnavailableJSONResponse: loomapi.LegacyUnavailableJSONResponse(legacy)}, nil default: return nil, unexpectedResponseStatus("publishRepositoryExplorerConfig", status) } } -func (r *HTTPRoutes) GetRepositoryExplorerConfig(ctx context.Context, _ loomapi.GetRepositoryExplorerConfigRequestObject) (loomapi.GetRepositoryExplorerConfigResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.getRepositoryConfig) - if err != nil { - return nil, err - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetRepositoryExplorerConfig200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := legacyError(body) - if decodeErr != nil { - return nil, decodeErr - } - switch status { - case 403: - return loomapi.GetRepositoryExplorerConfig403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetRepositoryExplorerConfig404JSONResponse{LegacyNotFoundJSONResponse: loomapi.LegacyNotFoundJSONResponse(value)}, nil - case 500: - return loomapi.GetRepositoryExplorerConfig500JSONResponse{LegacyInternalErrorJSONResponse: loomapi.LegacyInternalErrorJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("getRepositoryExplorerConfig", status) - } -} - -func (r *HTTPRoutes) GetExplorerAuthoringCapability(ctx context.Context, _ loomapi.GetExplorerAuthoringCapabilityRequestObject) (loomapi.GetExplorerAuthoringCapabilityResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.getCapability) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.AuthoringCapability](body) - return loomapi.GetExplorerAuthoringCapability200JSONResponse(value), e +func (r *HTTPRoutes) GetExplorerAuthoringCapability(ctx context.Context, request loomapi.GetExplorerAuthoringCapabilityRequestObject) (loomapi.GetExplorerAuthoringCapabilityResponseObject, error) { + if r == nil || r.explorer == nil { + _, value := authoringErrorForOpenAPI(ctx, "getExplorerAuthoringCapability", fmt.Errorf("Explorer authoring is not configured")) + return loomapi.GetExplorerAuthoringCapability500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e + value, err := r.explorer.getAuthoringCapabilityDirect(ctx, string(request.Project)) + if err == nil { + return loomapi.GetExplorerAuthoringCapability200JSONResponse(value), nil } + status, failure := authoringErrorForOpenAPI(ctx, "getExplorerAuthoringCapability", err) switch status { - case 403: - return loomapi.GetExplorerAuthoringCapability403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 500: - return loomapi.GetExplorerAuthoringCapability500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil + case http.StatusForbidden: + return loomapi.GetExplorerAuthoringCapability403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil + case http.StatusInternalServerError: + return loomapi.GetExplorerAuthoringCapability500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil default: return nil, unexpectedResponseStatus("getExplorerAuthoringCapability", status) } } -func (r *HTTPRoutes) SearchExplorerCandidates(ctx context.Context, _ loomapi.SearchExplorerCandidatesRequestObject) (loomapi.SearchExplorerCandidatesResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.searchSuggestions) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.CandidateSearchResponse](body) - return loomapi.SearchExplorerCandidates200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e - } - switch status { - case 400: - return loomapi.SearchExplorerCandidates400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.SearchExplorerCandidates403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 409: - return loomapi.SearchExplorerCandidates409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 500: - return loomapi.SearchExplorerCandidates500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.SearchExplorerCandidates503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(value)}, nil - default: +func (r *HTTPRoutes) SearchExplorerCandidates(ctx context.Context, request loomapi.SearchExplorerCandidatesRequestObject) (loomapi.SearchExplorerCandidatesResponseObject, error) { + if r == nil || r.explorer == nil { + status, failure := authoringErrorForOpenAPI(ctx, "searchExplorerCandidates", explorerUnavailable("suggestions", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) + if status == http.StatusServiceUnavailable { + return loomapi.SearchExplorerCandidates503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil + } return nil, unexpectedResponseStatus("searchExplorerCandidates", status) } -} - -func (r *HTTPRoutes) GetExplorerCandidateSuggestions(ctx context.Context, _ loomapi.GetExplorerCandidateSuggestionsRequestObject) (loomapi.GetExplorerCandidateSuggestionsResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.getCandidateSuggestions) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.CandidateSuggestions](body) - return loomapi.GetExplorerCandidateSuggestions200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e + value, err := r.explorer.searchAuthoringSuggestionsDirect(ctx, string(request.Project), string(request.ExplorerId), request.Body) + if err == nil { + return loomapi.SearchExplorerCandidates200JSONResponse(value), nil } + status, failure := authoringErrorForOpenAPI(ctx, "searchExplorerCandidates", err) switch status { - case 400: - return loomapi.GetExplorerCandidateSuggestions400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.GetExplorerCandidateSuggestions403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetExplorerCandidateSuggestions404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.GetExplorerCandidateSuggestions409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 500: - return loomapi.GetExplorerCandidateSuggestions500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.GetExplorerCandidateSuggestions503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(value)}, nil + case http.StatusBadRequest: + return loomapi.SearchExplorerCandidates400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil + case http.StatusForbidden: + return loomapi.SearchExplorerCandidates403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil + case http.StatusConflict: + return loomapi.SearchExplorerCandidates409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil + case http.StatusInternalServerError: + return loomapi.SearchExplorerCandidates500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil + case http.StatusServiceUnavailable: + return loomapi.SearchExplorerCandidates503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil default: - return nil, unexpectedResponseStatus("getExplorerCandidateSuggestions", status) + return nil, unexpectedResponseStatus("searchExplorerCandidates", status) } } -func (r *HTTPRoutes) GetExplorerBuilder(ctx context.Context, _ loomapi.GetExplorerBuilderRequestObject) (loomapi.GetExplorerBuilderResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.getBuilder) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.BuilderState](body) - return loomapi.GetExplorerBuilder200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e - } - switch status { - case 403: - return loomapi.GetExplorerBuilder403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.GetExplorerBuilder404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.GetExplorerBuilder409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 500: - return loomapi.GetExplorerBuilder500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.GetExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(value)}, nil - default: +func (r *HTTPRoutes) GetExplorerBuilder(ctx context.Context, request loomapi.GetExplorerBuilderRequestObject) (loomapi.GetExplorerBuilderResponseObject, error) { + if r == nil || r.explorer == nil { + status, failure := authoringErrorForOpenAPI(ctx, "getExplorerBuilder", explorerUnavailable("builder", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) + if status == http.StatusServiceUnavailable { + return loomapi.GetExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil + } return nil, unexpectedResponseStatus("getExplorerBuilder", status) } -} - -func (r *HTTPRoutes) SaveExplorerDraft(ctx context.Context, _ loomapi.SaveExplorerDraftRequestObject) (loomapi.SaveExplorerDraftResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.saveDraft) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.SaveDraftResponse](body) - return loomapi.SaveExplorerDraft200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e - } - switch status { - case 400: - return loomapi.SaveExplorerDraft400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.SaveExplorerDraft403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.SaveExplorerDraft404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.SaveExplorerDraft409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 422: - return loomapi.SaveExplorerDraft422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.SaveExplorerDraft500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.SaveExplorerDraft503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(value)}, nil - default: - return nil, unexpectedResponseStatus("saveExplorerDraft", status) - } -} - -func (r *HTTPRoutes) ExportExplorerWorkspace(ctx context.Context, _ loomapi.ExportExplorerWorkspaceRequestObject) (loomapi.ExportExplorerWorkspaceResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.exportWorkspace) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.Workspace](body) - return loomapi.ExportExplorerWorkspace200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e + value, err := r.explorer.getAuthoringBuilderDirect(ctx, string(request.Project), string(request.ExplorerId)) + if err == nil { + return loomapi.GetExplorerBuilder200JSONResponse(value), nil } + status, failure := authoringErrorForOpenAPI(ctx, "getExplorerBuilder", err) switch status { - case 403: - return loomapi.ExportExplorerWorkspace403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.ExportExplorerWorkspace404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.ExportExplorerWorkspace409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 500: - return loomapi.ExportExplorerWorkspace500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil + case http.StatusForbidden: + return loomapi.GetExplorerBuilder403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil + case http.StatusNotFound: + return loomapi.GetExplorerBuilder404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(failure)}, nil + case http.StatusConflict: + return loomapi.GetExplorerBuilder409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil + case http.StatusInternalServerError: + return loomapi.GetExplorerBuilder500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil + case http.StatusServiceUnavailable: + return loomapi.GetExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil default: - return nil, unexpectedResponseStatus("exportExplorerWorkspace", status) + return nil, unexpectedResponseStatus("getExplorerBuilder", status) } } -func (r *HTTPRoutes) ApplyExplorerBuilderCommands(ctx context.Context, _ loomapi.ApplyExplorerBuilderCommandsRequestObject) (loomapi.ApplyExplorerBuilderCommandsResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.applyCommands) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.ApplyCommandsResponse](body) - return loomapi.ApplyExplorerBuilderCommands200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e - } - switch status { - case 400: - return loomapi.ApplyExplorerBuilderCommands400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.ApplyExplorerBuilderCommands403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 409: - return loomapi.ApplyExplorerBuilderCommands409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 422: - return loomapi.ApplyExplorerBuilderCommands422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.ApplyExplorerBuilderCommands500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.ApplyExplorerBuilderCommands503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(value)}, nil - default: +func (r *HTTPRoutes) ApplyExplorerBuilderCommands(ctx context.Context, request loomapi.ApplyExplorerBuilderCommandsRequestObject) (loomapi.ApplyExplorerBuilderCommandsResponseObject, error) { + if r == nil || r.explorer == nil { + status, failure := authoringErrorForOpenAPI(ctx, "applyExplorerBuilderCommands", explorerUnavailable("commands", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) + if status == http.StatusServiceUnavailable { + return loomapi.ApplyExplorerBuilderCommands503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil + } return nil, unexpectedResponseStatus("applyExplorerBuilderCommands", status) } -} - -func (r *HTTPRoutes) compileExplorer(ctx context.Context, handler fiber.Handler) (loomapi.CompileResponse, loomapi.ErrorResponse, int, error) { - status, body, err := runFiberHandler(ctx, handler) - if err != nil { - return loomapi.CompileResponse{}, loomapi.ErrorResponse{}, 0, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.CompileResponse](body) - return value, loomapi.ErrorResponse{}, status, e - } - value, _, e := authoringError(body, status) - return loomapi.CompileResponse{}, value, status, e -} - -func (r *HTTPRoutes) CompileExplorerBuilder(ctx context.Context, _ loomapi.CompileExplorerBuilderRequestObject) (loomapi.CompileExplorerBuilderResponseObject, error) { - value, failure, status, err := r.compileExplorer(ctx, r.explorer.authoring.compileBuilder) - if err != nil { - return nil, err - } - if status == http.StatusOK { - return loomapi.CompileExplorerBuilder200JSONResponse(value), nil - } - switch status { - case 400: - return loomapi.CompileExplorerBuilder400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil - case 403: - return loomapi.CompileExplorerBuilder403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil - case 409: - return loomapi.CompileExplorerBuilder409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil - case 422: - return loomapi.CompileExplorerBuilder422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil - case 500: - return loomapi.CompileExplorerBuilder500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil - case 503: - return loomapi.CompileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil - default: - return nil, unexpectedResponseStatus("compileExplorerBuilder", status) - } -} - -func (r *HTTPRoutes) CompileExplorer(ctx context.Context, _ loomapi.CompileExplorerRequestObject) (loomapi.CompileExplorerResponseObject, error) { - value, failure, status, err := r.compileExplorer(ctx, r.explorer.authoring.compile) - if err != nil { - return nil, err - } - if status == http.StatusOK { - return loomapi.CompileExplorer200JSONResponse(value), nil + value, err := r.explorer.applyAuthoringCommandsDirect(ctx, string(request.Project), string(request.ExplorerId), authResourcePathFromParam(request.Params.AuthResourcePath), request.Body) + if err == nil { + return loomapi.ApplyExplorerBuilderCommands200JSONResponse(value), nil } + status, failure := authoringErrorForOpenAPI(ctx, "applyExplorerBuilderCommands", err) switch status { - case 400: - return loomapi.CompileExplorer400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil - case 403: - return loomapi.CompileExplorer403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil - case 409: - return loomapi.CompileExplorer409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil - case 422: - return loomapi.CompileExplorer422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil - case 500: - return loomapi.CompileExplorer500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil - case 503: - return loomapi.CompileExplorer503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil + case http.StatusBadRequest: + return loomapi.ApplyExplorerBuilderCommands400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil + case http.StatusForbidden: + return loomapi.ApplyExplorerBuilderCommands403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil + case http.StatusConflict: + return loomapi.ApplyExplorerBuilderCommands409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil + case http.StatusUnprocessableEntity: + return loomapi.ApplyExplorerBuilderCommands422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil + case http.StatusInternalServerError: + return loomapi.ApplyExplorerBuilderCommands500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil + case http.StatusServiceUnavailable: + return loomapi.ApplyExplorerBuilderCommands503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil default: - return nil, unexpectedResponseStatus("compileExplorer", status) + return nil, unexpectedResponseStatus("applyExplorerBuilderCommands", status) } } -func (r *HTTPRoutes) ReconcileExplorerBuilder(ctx context.Context, _ loomapi.ReconcileExplorerBuilderRequestObject) (loomapi.ReconcileExplorerBuilderResponseObject, error) { - value, failure, status, err := r.compileExplorer(ctx, r.explorer.authoring.reconcile) - if err != nil { - return nil, err +func (r *HTTPRoutes) ReconcileExplorerBuilder(ctx context.Context, request loomapi.ReconcileExplorerBuilderRequestObject) (loomapi.ReconcileExplorerBuilderResponseObject, error) { + project := string(request.Project) + explorerID := string(request.ExplorerId) + if r == nil || r.explorer == nil { + _, failure := authoringErrorForOpenAPI(ctx, "reconcileExplorerBuilder", explorerUnavailable("reconcile", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) + return loomapi.ReconcileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil } - if status == http.StatusOK { + value, err := r.explorer.reconcileAuthoringDirect(ctx, project, explorerID, authResourcePathFromParam(request.Params.AuthResourcePath), request.Body) + if err == nil { return loomapi.ReconcileExplorerBuilder200JSONResponse(value), nil } + status, failure := authoringErrorForOpenAPI(ctx, "reconcileExplorerBuilder", err) switch status { - case 400: + case http.StatusBadRequest: return loomapi.ReconcileExplorerBuilder400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil - case 403: + case http.StatusForbidden: return loomapi.ReconcileExplorerBuilder403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil - case 404: + case http.StatusNotFound: return loomapi.ReconcileExplorerBuilder404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(failure)}, nil - case 409: + case http.StatusConflict: return loomapi.ReconcileExplorerBuilder409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil - case 422: + case http.StatusUnprocessableEntity: return loomapi.ReconcileExplorerBuilder422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil - case 500: + case http.StatusInternalServerError: return loomapi.ReconcileExplorerBuilder500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil - case 503: + case http.StatusServiceUnavailable: return loomapi.ReconcileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil default: return nil, unexpectedResponseStatus("reconcileExplorerBuilder", status) } } -func (r *HTTPRoutes) PreviewExplorer(ctx context.Context, _ loomapi.PreviewExplorerRequestObject) (loomapi.PreviewExplorerResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.preview) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.PreviewResponse](body) - return loomapi.PreviewExplorer200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e - } +func (r *HTTPRoutes) PreviewExplorer(ctx context.Context, request loomapi.PreviewExplorerRequestObject) (loomapi.PreviewExplorerResponseObject, error) { + project := string(request.Project) + explorerID := string(request.ExplorerId) + if r == nil || r.explorer == nil { + _, failure := authoringErrorForOpenAPI(ctx, "previewExplorer", explorerUnavailable("preview", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) + return loomapi.PreviewExplorer503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil + } + value, err := r.explorer.previewAuthoringDirect(ctx, project, explorerID, request.Body) + if err == nil { + return loomapi.PreviewExplorer200JSONResponse(value), nil + } + status, failure := authoringErrorForOpenAPI(ctx, "previewExplorer", err) switch status { - case 400: - return loomapi.PreviewExplorer400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.PreviewExplorer403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.PreviewExplorer404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.PreviewExplorer409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 413: - return loomapi.PreviewExplorer413JSONResponse{AuthoringPayloadTooLargeJSONResponse: loomapi.AuthoringPayloadTooLargeJSONResponse(value)}, nil - case 422: - return loomapi.PreviewExplorer422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(value)}, nil - case 429: - return loomapi.PreviewExplorer429JSONResponse{AuthoringTooManyRequestsJSONResponse: loomapi.AuthoringTooManyRequestsJSONResponse(value)}, nil + case http.StatusBadRequest: + return loomapi.PreviewExplorer400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil + case http.StatusForbidden: + return loomapi.PreviewExplorer403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil + case http.StatusNotFound: + return loomapi.PreviewExplorer404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(failure)}, nil + case http.StatusConflict: + return loomapi.PreviewExplorer409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil + case http.StatusRequestEntityTooLarge: + return loomapi.PreviewExplorer413JSONResponse{AuthoringPayloadTooLargeJSONResponse: loomapi.AuthoringPayloadTooLargeJSONResponse(failure)}, nil + case http.StatusUnprocessableEntity: + return loomapi.PreviewExplorer422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil + case http.StatusTooManyRequests: + return loomapi.PreviewExplorer429JSONResponse{AuthoringTooManyRequestsJSONResponse: loomapi.AuthoringTooManyRequestsJSONResponse(failure)}, nil case 499: - return loomapi.PreviewExplorer499JSONResponse{AuthoringClientClosedRequestJSONResponse: loomapi.AuthoringClientClosedRequestJSONResponse(value)}, nil - case 500: - return loomapi.PreviewExplorer500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.PreviewExplorer503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(value)}, nil - case 504: - return loomapi.PreviewExplorer504JSONResponse{AuthoringGatewayTimeoutJSONResponse: loomapi.AuthoringGatewayTimeoutJSONResponse(value)}, nil + return loomapi.PreviewExplorer499JSONResponse{AuthoringClientClosedRequestJSONResponse: loomapi.AuthoringClientClosedRequestJSONResponse(failure)}, nil + case http.StatusInternalServerError: + return loomapi.PreviewExplorer500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil + case http.StatusServiceUnavailable: + return loomapi.PreviewExplorer503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil + case http.StatusGatewayTimeout: + return loomapi.PreviewExplorer504JSONResponse{AuthoringGatewayTimeoutJSONResponse: loomapi.AuthoringGatewayTimeoutJSONResponse(failure)}, nil default: return nil, unexpectedResponseStatus("previewExplorer", status) } } -func (r *HTTPRoutes) PublishExplorer(ctx context.Context, _ loomapi.PublishExplorerRequestObject) (loomapi.PublishExplorerResponseObject, error) { - status, body, err := runFiberHandler(ctx, r.explorer.authoring.publish) - if err != nil { - return nil, err - } - if status == http.StatusOK { - value, e := decodeResponse[loomapi.PublishResponse](body) - return loomapi.PublishExplorer200JSONResponse(value), e - } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e - } +func (r *HTTPRoutes) PublishExplorer(ctx context.Context, request loomapi.PublishExplorerRequestObject) (loomapi.PublishExplorerResponseObject, error) { + project := string(request.Project) + explorerID := string(request.ExplorerId) + if r == nil || r.explorer == nil { + _, failure := authoringErrorForOpenAPI(ctx, "publishExplorer", explorerUnavailable("publish", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) + return loomapi.PublishExplorer503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil + } + value, err := r.explorer.publishAuthoringDirect(ctx, project, explorerID, authResourcePathFromParam(request.Params.AuthResourcePath), request.Body) + if err == nil { + return loomapi.PublishExplorer200JSONResponse(value), nil + } + status, failure := authoringErrorForOpenAPI(ctx, "publishExplorer", err) switch status { - case 400: - return loomapi.PublishExplorer400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(value)}, nil - case 403: - return loomapi.PublishExplorer403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(value)}, nil - case 404: - return loomapi.PublishExplorer404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(value)}, nil - case 409: - return loomapi.PublishExplorer409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(value)}, nil - case 422: - return loomapi.PublishExplorer422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(value)}, nil - case 500: - return loomapi.PublishExplorer500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(value)}, nil - case 503: - return loomapi.PublishExplorer503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(value)}, nil + case http.StatusBadRequest: + return loomapi.PublishExplorer400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil + case http.StatusForbidden: + return loomapi.PublishExplorer403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil + case http.StatusNotFound: + return loomapi.PublishExplorer404JSONResponse{AuthoringNotFoundJSONResponse: loomapi.AuthoringNotFoundJSONResponse(failure)}, nil + case http.StatusConflict: + return loomapi.PublishExplorer409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil + case http.StatusUnprocessableEntity: + return loomapi.PublishExplorer422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil + case http.StatusInternalServerError: + return loomapi.PublishExplorer500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil + case http.StatusServiceUnavailable: + return loomapi.PublishExplorer503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil default: return nil, unexpectedResponseStatus("publishExplorer", status) } diff --git a/internal/server/openapi_explorer_write_scope_test.go b/internal/server/openapi_explorer_write_scope_test.go new file mode 100644 index 00000000..7a7e5eee --- /dev/null +++ b/internal/server/openapi_explorer_write_scope_test.go @@ -0,0 +1,84 @@ +package server + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + loomapi "github.com/calypr/loom/generated/loomapi" + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/explorer/lifecycle" + "github.com/gofiber/fiber/v3" +) + +type recordingWriteAuthorizer struct { + paths []string +} + +func (a *recordingWriteAuthorizer) AuthorizeWrite(_ context.Context, _ *authscope.Principal, _, authResourcePath string) error { + a.paths = append(a.paths, authResourcePath) + return errors.New("stop after authorization") +} + +func generatedMutationTestApp(authorizer authscope.Authorizer) *fiber.App { + app := fiber.New() + routes := &HTTPRoutes{explorer: &explorerHTTPHandlers{ + authorizer: authorizer, + authorizeRead: func(context.Context, *authscope.Principal, string) error { return nil }, + application: &lifecycle.Service{}, + }} + loomapi.RegisterHandlersWithOptions(app, loomapi.NewStrictHandler(routes, nil), loomapi.FiberServerOptions{}) + return app +} + +func TestGeneratedExplorerMutationRoutesForwardAuthResourcePath(t *testing.T) { + authorizer := &recordingWriteAuthorizer{} + app := generatedMutationTestApp(authorizer) + const scope = "/programs/HTAN_INT/projects/BForePC" + tests := []struct { + name string + path string + body string + }{ + {name: "create", path: "/api/v1/projects/HTAN_INT%252FBForePC/explorers", body: `{"name":"test"}`}, + {name: "commands", path: "/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/commands", body: `{}`}, + {name: "reconcile", path: "/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/reconcile", body: `{}`}, + {name: "publish", path: "/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/publish", body: `{}`}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + request := httptest.NewRequest(http.MethodPost, test.path+"?auth_resource_path=%2Fprograms%2FHTAN_INT%2Fprojects%2FBForePC", strings.NewReader(test.body)) + request.Header.Set("Content-Type", "application/json") + response, err := app.Test(request) + if err != nil { + t.Fatal(err) + } + _ = response.Body.Close() + if response.StatusCode != http.StatusForbidden { + t.Fatalf("status=%d, want %d", response.StatusCode, http.StatusForbidden) + } + if got := authorizer.paths[len(authorizer.paths)-1]; got != scope { + t.Fatalf("auth resource path=%q, want %q", got, scope) + } + }) + } +} + +func TestGeneratedExplorerMutationRoutePreservesNoAuthOmission(t *testing.T) { + authorizer := &recordingWriteAuthorizer{} + app := generatedMutationTestApp(authorizer) + request := httptest.NewRequest(http.MethodPost, "/api/v1/projects/NCPI_ACCEPTANCE/explorers", strings.NewReader(`{"name":"test"}`)) + request.Header.Set("Content-Type", "application/json") + response, err := app.Test(request) + if err != nil { + t.Fatal(err) + } + _ = response.Body.Close() + if len(authorizer.paths) != 1 || authorizer.paths[0] != "" { + t.Fatalf("auth resource paths=%q, want one empty path", authorizer.paths) + } +} diff --git a/internal/server/openapi_graphql_routes.go b/internal/server/openapi_graphql_routes.go index e00a0dc0..43c6b8f2 100644 --- a/internal/server/openapi_graphql_routes.go +++ b/internal/server/openapi_graphql_routes.go @@ -3,72 +3,96 @@ package server import ( "bytes" "context" + "encoding/json" + "io" "net/http" + "net/http/httptest" loomapi "github.com/calypr/loom/generated/loomapi" + httpapi "github.com/calypr/loom/internal/api/http" "github.com/gofiber/fiber/v3" - fiberadaptor "github.com/gofiber/fiber/v3/middleware/adaptor" ) -func (r *HTTPRoutes) runGraphQL(ctx context.Context, handler http.Handler) (int, []byte, error) { +// serveGraphQL invokes the native GraphQL http.Handler through a bounded +// request/response adapter. GraphQL remains a native HTTP transport; unlike +// the removed REST path this does not create a Fiber context or re-enter a +// Fiber handler. +func serveGraphQL(ctx context.Context, handler http.Handler, body any, method string) (int, []byte, error) { if handler == nil { return http.StatusNotFound, []byte(`{"error":"not found"}`), nil } - return runFiberHandler(ctx, fiberadaptor.HTTPHandlerWithContext(handler)) + var input io.Reader + if body != nil { + payload, err := json.Marshal(body) + if err != nil { + return 0, nil, err + } + input = bytes.NewReader(payload) + } + request := httptest.NewRequest(method, "/graphql/graph", input).WithContext(ctx) + request.Header.Set("Content-Type", "application/json") + if requestID := httpapi.RequestIDFromContext(ctx); requestID != "" { + request.Header.Set("X-Request-ID", requestID) + } + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + return response.Code, append([]byte(nil), response.Body.Bytes()...), nil } -func (r *HTTPRoutes) ExecuteGraphQL(ctx context.Context, _ loomapi.ExecuteGraphQLRequestObject) (loomapi.ExecuteGraphQLResponseObject, error) { - status, body, err := r.runGraphQL(ctx, r.graphql.Handler) +func (r *HTTPRoutes) ExecuteGraphQL(ctx context.Context, request loomapi.ExecuteGraphQLRequestObject) (loomapi.ExecuteGraphQLResponseObject, error) { + status, body, err := serveGraphQL(ctx, r.graphql.Handler, request.Body, http.MethodPost) if err != nil { return nil, err } if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.ExecuteGraphQL200JSONResponse](body) - return response, decodeErr + value, conversionErr := rawJSONBytes(body) + return loomapi.ExecuteGraphQL200JSONResponse(value), conversionErr } if status == http.StatusUnauthorized { - value, decodeErr := serviceError(body) + var value loomapi.ServiceErrorResponse + decodeErr := json.Unmarshal(body, &value) return loomapi.ExecuteGraphQL401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, decodeErr } - value, decodeErr := decodeResponse[loomapi.RawJSON](body) + value, decodeErr := rawJSONBytes(body) if decodeErr != nil { return nil, decodeErr } switch status { - case 400: + case http.StatusBadRequest: return loomapi.ExecuteGraphQL400JSONResponse{GraphQLBadRequestJSONResponse: loomapi.GraphQLBadRequestJSONResponse(value)}, nil - case 422: + case http.StatusUnprocessableEntity: return loomapi.ExecuteGraphQL422JSONResponse{GraphQLUnprocessableJSONResponse: loomapi.GraphQLUnprocessableJSONResponse(value)}, nil - case 500: + case http.StatusInternalServerError: return loomapi.ExecuteGraphQL500JSONResponse{GraphQLInternalErrorJSONResponse: loomapi.GraphQLInternalErrorJSONResponse(value)}, nil default: return nil, unexpectedResponseStatus("executeGraphQL", status) } } -func (r *HTTPRoutes) ExecuteDataframeGraphQL(ctx context.Context, _ loomapi.ExecuteDataframeGraphQLRequestObject) (loomapi.ExecuteDataframeGraphQLResponseObject, error) { - status, body, err := r.runGraphQL(ctx, r.graphql.Handler) +func (r *HTTPRoutes) ExecuteDataframeGraphQL(ctx context.Context, request loomapi.ExecuteDataframeGraphQLRequestObject) (loomapi.ExecuteDataframeGraphQLResponseObject, error) { + status, body, err := serveGraphQL(ctx, r.graphql.Handler, request.Body, http.MethodPost) if err != nil { return nil, err } if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.ExecuteDataframeGraphQL200JSONResponse](body) - return response, decodeErr + value, conversionErr := rawJSONBytes(body) + return loomapi.ExecuteDataframeGraphQL200JSONResponse(value), conversionErr } if status == http.StatusUnauthorized { - value, decodeErr := serviceError(body) + var value loomapi.ServiceErrorResponse + decodeErr := json.Unmarshal(body, &value) return loomapi.ExecuteDataframeGraphQL401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, decodeErr } - value, decodeErr := decodeResponse[loomapi.RawJSON](body) + value, decodeErr := rawJSONBytes(body) if decodeErr != nil { return nil, decodeErr } switch status { - case 400: + case http.StatusBadRequest: return loomapi.ExecuteDataframeGraphQL400JSONResponse{GraphQLBadRequestJSONResponse: loomapi.GraphQLBadRequestJSONResponse(value)}, nil - case 422: + case http.StatusUnprocessableEntity: return loomapi.ExecuteDataframeGraphQL422JSONResponse{GraphQLUnprocessableJSONResponse: loomapi.GraphQLUnprocessableJSONResponse(value)}, nil - case 500: + case http.StatusInternalServerError: return loomapi.ExecuteDataframeGraphQL500JSONResponse{GraphQLInternalErrorJSONResponse: loomapi.GraphQLInternalErrorJSONResponse(value)}, nil default: return nil, unexpectedResponseStatus("executeDataframeGraphQL", status) @@ -76,7 +100,7 @@ func (r *HTTPRoutes) ExecuteDataframeGraphQL(ctx context.Context, _ loomapi.Exec } func (r *HTTPRoutes) GetGraphQLPlayground(ctx context.Context, _ loomapi.GetGraphQLPlaygroundRequestObject) (loomapi.GetGraphQLPlaygroundResponseObject, error) { - status, body, err := r.runGraphQL(ctx, r.graphql.Playground) + status, body, err := serveGraphQL(ctx, r.graphql.Playground, nil, http.MethodGet) if err != nil { return nil, err } @@ -87,7 +111,7 @@ func (r *HTTPRoutes) GetGraphQLPlayground(ctx context.Context, _ loomapi.GetGrap } func (r *HTTPRoutes) GetApolloSandbox(ctx context.Context, _ loomapi.GetApolloSandboxRequestObject) (loomapi.GetApolloSandboxResponseObject, error) { - status, body, err := r.runGraphQL(ctx, r.graphql.Sandbox) + status, body, err := serveGraphQL(ctx, r.graphql.Sandbox, nil, http.MethodGet) if err != nil { return nil, err } @@ -96,3 +120,11 @@ func (r *HTTPRoutes) GetApolloSandbox(ctx context.Context, _ loomapi.GetApolloSa } return loomapi.GetApolloSandbox200TexthtmlResponse{HTMLTexthtmlResponse: loomapi.HTMLTexthtmlResponse{Body: bytes.NewReader(body), ContentLength: int64(len(body))}}, nil } + +func rawJSONBytes(body []byte) (loomapi.RawJSON, error) { + var value loomapi.RawJSON + if err := json.Unmarshal(body, &value); err != nil { + return nil, err + } + return value, nil +} diff --git a/internal/server/openapi_recipe_execution.go b/internal/server/openapi_recipe_execution.go new file mode 100644 index 00000000..f9dcf4b5 --- /dev/null +++ b/internal/server/openapi_recipe_execution.go @@ -0,0 +1,86 @@ +package server + +import ( + "context" + "errors" + "net/http" + "strings" + + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/publication" +) + +// recipeExecution returns the legacy execution document consumed by ETL. A +// denied execution remains indistinguishable from a missing catalog entry. +func (r *HTTPRoutes) recipeExecution(ctx context.Context, id string) (map[string]any, int) { + if r.releases == nil { + return map[string]any{"error": "recipe execution not found"}, http.StatusNotFound + } + id = strings.TrimSpace(id) + execution, err := r.releases.GetExecution(ctx, id) + if err != nil || execution.ID == "" { + return map[string]any{"error": "recipe execution not found"}, http.StatusNotFound + } + if r.scopes != nil { + principal, _ := authscope.PrincipalFromContext(ctx) + if _, scopeErr := r.scopes.ResolveReadScopeForGeneration(ctx, principal, execution.Project, execution.DatasetGeneration, execution.AuthResourcePaths); scopeErr != nil { + status := http.StatusForbidden + if errors.Is(scopeErr, authscope.ErrUnauthenticated) { + status = http.StatusUnauthorized + } + return map[string]any{"error": "recipe execution not found"}, status + } + } + outputs := make([]map[string]any, 0, len(execution.Outputs)) + for _, output := range execution.Outputs { + columns := make([]map[string]any, 0, len(output.Columns)) + for _, column := range output.Columns { + logical, nullable, repeated, filterable, sortable, aggregatable := columnCapabilities(column.ClickHouse) + if column.LogicalType != "" { + logical = column.LogicalType + } + nullable = nullable || column.Nullable + repeated = repeated || column.Repeated + columns = append(columns, map[string]any{"name": column.Name, "semanticPath": column.SemanticPath, "clickhouseType": column.ClickHouse, "logicalType": logical, "nullable": nullable, "repeated": repeated, "filterable": filterable, "sortable": sortable, "aggregatable": aggregatable}) + } + outputs = append(outputs, map[string]any{"name": output.Name, "state": recipeExecutionHTTPState(output.State), "rowCount": output.RowCount, "columns": columns}) + } + return map[string]any{"id": execution.ID, "projectId": execution.Project, "datasetGeneration": execution.DatasetGeneration, "recipeDigest": execution.RecipeDigest, "schemaDigest": execution.SchemaDigest, "resolvedSchemaDigest": execution.SchemaDigest, "state": recipeExecutionHTTPState(execution.State), "outputs": outputs}, http.StatusOK +} + +func recipeExecutionHTTPState(state publication.BundleState) string { + if state == publication.BundlePublished { + return string(publication.BundleReady) + } + return string(state) +} + +func columnCapabilities(clickHouseType string) (logical string, nullable, repeated, filterable, sortable, aggregatable bool) { + typ := clickHouseType + if strings.HasPrefix(typ, "Nullable(") && strings.HasSuffix(typ, ")") { + nullable = true + typ = strings.TrimSuffix(strings.TrimPrefix(typ, "Nullable("), ")") + } + if strings.HasPrefix(typ, "Array(") && strings.HasSuffix(typ, ")") { + repeated = true + typ = strings.TrimSuffix(strings.TrimPrefix(typ, "Array("), ")") + } + switch { + case strings.HasPrefix(typ, "Bool"): + logical = "boolean" + case strings.HasPrefix(typ, "Int"), strings.HasPrefix(typ, "UInt"): + logical = "integer" + case strings.HasPrefix(typ, "Float"), strings.HasPrefix(typ, "Decimal"): + logical = "number" + case strings.HasPrefix(typ, "Date"): + logical = "date" + case strings.HasPrefix(typ, "String"), strings.HasPrefix(typ, "FixedString"), strings.HasPrefix(typ, "UUID"): + logical = "string" + default: + logical = "json" + } + filterable = true + sortable = !repeated + aggregatable = !repeated && logical != "json" + return +} diff --git a/internal/api/http/recipe_execution_test.go b/internal/server/openapi_recipe_execution_test.go similarity index 96% rename from internal/api/http/recipe_execution_test.go rename to internal/server/openapi_recipe_execution_test.go index 6838d3ae..c004770a 100644 --- a/internal/api/http/recipe_execution_test.go +++ b/internal/server/openapi_recipe_execution_test.go @@ -1,4 +1,4 @@ -package httpapi +package server import ( "testing" diff --git a/internal/server/openapi_route_inventory_test.go b/internal/server/openapi_route_inventory_test.go index a1a71c48..dce8a1d0 100644 --- a/internal/server/openapi_route_inventory_test.go +++ b/internal/server/openapi_route_inventory_test.go @@ -12,7 +12,7 @@ import ( func TestGeneratedRoutesExactlyMatchOpenAPISpec(t *testing.T) { app := fiber.New() - handler := loomapi.NewStrictHandler(&HTTPRoutes{}, []loomapi.StrictMiddlewareFunc{strictFiberContextMiddleware}) + handler := loomapi.NewStrictHandler(&HTTPRoutes{}, nil) loomapi.RegisterHandlersWithOptions(app, handler, loomapi.FiberServerOptions{}) want := map[string]bool{} @@ -47,8 +47,8 @@ func TestGeneratedRoutesExactlyMatchOpenAPISpec(t *testing.T) { t.Errorf("registered route is absent from OpenAPI: %s", route) } } - if len(got) != 39 || len(want) != 39 { - t.Errorf("route count got=%d spec=%d, want 39", len(got), len(want)) + if len(got) != 22 || len(want) != 22 { + t.Errorf("route count got=%d spec=%d, want 22", len(got), len(want)) } } @@ -117,8 +117,8 @@ func TestOpenAPIDocumentsOperationSpecificFailureStatuses(t *testing.T) { } } - assertStatuses("PUT", "/api/v1/projects/{project}/resources/{resourceType}", "400", "401", "403", "415", "422", "500", "503") assertStatuses("POST", "/api/v1/datasets/{project}/generations/{generation}", "400", "401", "403", "409", "415", "422", "500", "503") + assertStatuses("GET", "/api/v1/datasets/{project}/generations/{generation}", "400", "401", "403", "404", "500", "503") assertStatuses("POST", "/api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/preview", "400", "401", "403", "404", "409", "413", "422", "429", "499", "500", "503", "504") assertStatuses("POST", "/api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/publish", "400", "401", "403", "404", "409", "422", "500", "503") } diff --git a/internal/server/openapi_routes.go b/internal/server/openapi_routes.go index d654ab10..4e766d02 100644 --- a/internal/server/openapi_routes.go +++ b/internal/server/openapi_routes.go @@ -1,81 +1,53 @@ package server import ( - "context" - "encoding/json" "fmt" - "net/http" loomapi "github.com/calypr/loom/generated/loomapi" loadapi "github.com/calypr/loom/internal/api/bulk/load" graphapi "github.com/calypr/loom/internal/api/graphql/graph" httpapi "github.com/calypr/loom/internal/api/http" - "github.com/gofiber/fiber/v3" + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/publication" ) -type strictFiberContextKey struct{} - type HTTPRoutes struct { server *httpapi.HTTPServer load *loadapi.Handler - recipe httpapi.RecipeExecutionHandler + releases publication.BundleCatalog + scopes *authscope.ScopeResolver graphql graphapi.RouteConfig explorer *explorerHTTPHandlers } var _ loomapi.StrictServerInterface = (*HTTPRoutes)(nil) -func strictFiberContextMiddleware(next loomapi.StrictHandlerFunc, _ string) loomapi.StrictHandlerFunc { - return func(c fiber.Ctx, args any) (any, error) { - c.SetContext(context.WithValue(c.Context(), strictFiberContextKey{}, c)) - return next(c, args) +func serviceErrorResponse(body httpapi.ErrorResponse) loomapi.ServiceErrorResponse { + details := body.Error.Details + requestID := body.Error.RequestID + retryable := body.Error.Retryable + var detailsPtr *map[string]interface{} + if details != nil { + converted := map[string]interface{}(details) + detailsPtr = &converted } -} - -func fiberContext(ctx context.Context) (fiber.Ctx, error) { - c, ok := ctx.Value(strictFiberContextKey{}).(fiber.Ctx) - if !ok || c == nil { - return nil, fmt.Errorf("generated route is missing Fiber request context") + var requestIDPtr *string + if requestID != "" { + requestIDPtr = &requestID } - return c, nil + return loomapi.ServiceErrorResponse{Error: loomapi.ServiceErrorBody{Code: body.Error.Code, Message: body.Error.Message, Details: detailsPtr, RequestId: requestIDPtr, Retryable: &retryable}} } -func runFiberHandler(ctx context.Context, handler fiber.Handler) (int, []byte, error) { - c, err := fiberContext(ctx) - if err != nil { - return 0, nil, err - } - if handler == nil { - return http.StatusNotFound, []byte(`{"error":"not found"}`), nil +func legacyErrorFromMap(body loomapi.RawJSON) (loomapi.LegacyErrorResponse, error) { + value, ok := body["error"].(string) + if !ok { + return loomapi.LegacyErrorResponse{}, fmt.Errorf("legacy error response is missing a string error") } - if err := handler(c); err != nil { - return 0, nil, err - } - status := c.Response().StatusCode() - body := append([]byte(nil), c.Response().Body()...) - c.Response().ResetBody() - return status, body, nil -} - -func decodeResponse[T any](body []byte) (T, error) { - var value T - if err := json.Unmarshal(body, &value); err != nil { - return value, fmt.Errorf("decode generated response: %w", err) + var result loomapi.LegacyErrorResponse + if err := result.Error.FromLegacyErrorResponseError0(value); err != nil { + return loomapi.LegacyErrorResponse{}, err } - return value, nil -} - -func authoringError(body []byte, status int) (loomapi.ErrorResponse, int, error) { - value, err := decodeResponse[loomapi.ErrorResponse](body) - return value, status, err -} - -func serviceError(body []byte) (loomapi.ServiceErrorResponse, error) { - return decodeResponse[loomapi.ServiceErrorResponse](body) -} - -func legacyError(body []byte) (loomapi.LegacyErrorResponse, error) { - return decodeResponse[loomapi.LegacyErrorResponse](body) + return result, nil } func unexpectedResponseStatus(operation string, status int) error { diff --git a/internal/server/openapi_test_helpers_test.go b/internal/server/openapi_test_helpers_test.go new file mode 100644 index 00000000..c73178bf --- /dev/null +++ b/internal/server/openapi_test_helpers_test.go @@ -0,0 +1,18 @@ +package server + +import ( + "context" + + loomapi "github.com/calypr/loom/generated/loomapi" + "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/lifecycle" + "github.com/gofiber/fiber/v3" +) + +// registerGeneratedExplorerTestRoutes exercises the same generated strict +// adapter used in production; tests no longer maintain a second route tree. +func registerGeneratedExplorerTestRoutes(router fiber.Router, authorizer authscope.Authorizer, authorizeRead func(context.Context, *authscope.Principal, string) error, service *explorer.Service, config lifecycle.Config) { + routes := &HTTPRoutes{explorer: newExplorerHTTPHandlers(authorizer, authorizeRead, service, config)} + loomapi.RegisterHandlersWithOptions(router, loomapi.NewStrictHandler(routes, nil), loomapi.FiberServerOptions{}) +} diff --git a/internal/server/options.go b/internal/server/options.go index d561f8c5..4b1a721a 100644 --- a/internal/server/options.go +++ b/internal/server/options.go @@ -2,10 +2,10 @@ package server import ( "encoding/json" + "errors" "flag" "fmt" "os" - "strconv" "strings" "time" @@ -28,12 +28,11 @@ type ServerConfig struct { Dataframer DataframerConfig `yaml:"dataframer"` RecipeBatchRows int `yaml:"recipe_batch_rows"` RecipeBatchBytes int `yaml:"recipe_batch_bytes"` + RecipeQueryPageRows int `yaml:"recipe_query_page_rows"` AllowUnauthenticated bool `yaml:"allow_unauthenticated"` RequiredDataframeSelectors []dataset.DataframeSelector `yaml:"required_dataframe_selectors"` - SnapshotDirectory string `yaml:"snapshot_directory"` - SnapshotRetention time.Duration `yaml:"snapshot_retention"` - PublicationWorkerLease time.Duration `yaml:"publication_worker_lease"` - PublicationMaxAttempts int `yaml:"publication_max_attempts"` + LocalWorkspaceWriteback string `yaml:"local_workspace_writeback"` + LocalWorkspaceProject string `yaml:"local_workspace_project"` } type ClickHouseConfig struct { @@ -70,9 +69,7 @@ func DefaultConfig() Config { Server: ServerConfig{ Listen: ":8080", URL: "http://127.0.0.1:8529", Database: "fhir_proto", Schema: "schemas/graph-fhir.json", ClickHouse: ClickHouseConfig{Enabled: true, URL: "clickhouse://127.0.0.1:9000", Database: "loom", Username: "default"}, - RecipeBatchRows: 1000, RecipeBatchBytes: 4 << 20, - SnapshotDirectory: ".loom-snapshots", SnapshotRetention: 7 * 24 * time.Hour, - PublicationWorkerLease: 2 * time.Minute, PublicationMaxAttempts: 3, + RecipeBatchRows: 1000, RecipeBatchBytes: 4 << 20, RecipeQueryPageRows: 25, }, Auth: AuthConfig{Mode: "basic", Calypr: CalyprAuthConfig{RequestTimeout: 5 * time.Second, CacheTTL: 30 * time.Second}}, } @@ -80,19 +77,22 @@ func DefaultConfig() Config { func parseServerOptions(args []string, handling flag.ErrorHandling) (Config, error) { var ( - configPath string - listen string - noAuth bool - url string - database string - schema string - clickhouseURL string - clickhouseDatabase string - clickhouseUsername string - clickhousePassword string - dataframerRecipe string - recipeBatchRows int - recipeBatchBytes int + configPath string + listen string + noAuth bool + url string + database string + schema string + clickhouseURL string + clickhouseDatabase string + clickhouseUsername string + clickhousePassword string + dataframerRecipe string + recipeBatchRows int + recipeBatchBytes int + recipeQueryPageRows int + localWorkspaceWriteback string + localWorkspaceProject string ) fs := flag.NewFlagSet("arango-fhir-server", handling) fs.StringVar(&configPath, "config", "", "YAML server configuration file") @@ -108,6 +108,9 @@ func parseServerOptions(args []string, handling flag.ErrorHandling) (Config, err fs.StringVar(&dataframerRecipe, "dataframer-recipe", "", "dataframer recipe JSON file (required when ClickHouse is enabled)") fs.IntVar(&recipeBatchRows, "recipe-batch-rows", 1000, "maximum recipe materialization rows per ClickHouse batch") fs.IntVar(&recipeBatchBytes, "recipe-batch-bytes", 4<<20, "maximum recipe materialization bytes per ClickHouse batch") + fs.IntVar(&recipeQueryPageRows, "recipe-query-page-rows", 25, "root documents per bounded dataframe query page; zero disables paging") + fs.StringVar(&localWorkspaceWriteback, "local-workspace-writeback", "", "exact local CONFIG workspace file updated after successful publication (no-auth development only)") + fs.StringVar(&localWorkspaceProject, "local-workspace-project", "", "only this project may update the local CONFIG workspace") if err := fs.Parse(args); err != nil { return Config{}, err } @@ -125,6 +128,10 @@ func parseServerOptions(args []string, handling flag.ErrorHandling) (Config, err cfg.Server.AllowUnauthenticated = true cfg.Auth.AllowUnauthenticated = true } + if strings.TrimSpace(localWorkspaceWriteback) != "" { + cfg.Server.LocalWorkspaceWriteback = localWorkspaceWriteback + cfg.Server.LocalWorkspaceProject = localWorkspaceProject + } if err := cfg.Validate(); err != nil { return Config{}, fmt.Errorf("invalid server config: %w", err) } @@ -146,6 +153,9 @@ func parseServerOptions(args []string, handling flag.ErrorHandling) (Config, err cfg.Server.Dataframer.Recipe = dataframerRecipe cfg.Server.RecipeBatchRows = recipeBatchRows cfg.Server.RecipeBatchBytes = recipeBatchBytes + cfg.Server.RecipeQueryPageRows = recipeQueryPageRows + cfg.Server.LocalWorkspaceWriteback = localWorkspaceWriteback + cfg.Server.LocalWorkspaceProject = localWorkspaceProject if noAuth { cfg.Server.AllowUnauthenticated = true cfg.Auth.AllowUnauthenticated = true @@ -185,49 +195,22 @@ func applyEnvironment(cfg Config) (Config, error) { return Config{}, fmt.Errorf("parse LOOM_REQUIRED_DATAFRAME_SELECTORS: %w", err) } } - if value := strings.TrimSpace(os.Getenv("LOOM_SNAPSHOT_RETENTION")); value != "" { - retention, err := time.ParseDuration(value) - if err != nil { - return Config{}, fmt.Errorf("parse LOOM_SNAPSHOT_RETENTION: %w", err) - } - cfg.Server.SnapshotRetention = retention - } - if value := strings.TrimSpace(os.Getenv("LOOM_SNAPSHOT_DIRECTORY")); value != "" { - cfg.Server.SnapshotDirectory = value - } - if value := strings.TrimSpace(os.Getenv("LOOM_PUBLICATION_WORKER_LEASE")); value != "" { - lease, err := time.ParseDuration(value) - if err != nil { - return Config{}, fmt.Errorf("parse LOOM_PUBLICATION_WORKER_LEASE: %w", err) - } - cfg.Server.PublicationWorkerLease = lease - } - if value := strings.TrimSpace(os.Getenv("LOOM_PUBLICATION_MAX_ATTEMPTS")); value != "" { - attempts, err := strconv.Atoi(value) - if err != nil { - return Config{}, fmt.Errorf("parse LOOM_PUBLICATION_MAX_ATTEMPTS: %w", err) - } - cfg.Server.PublicationMaxAttempts = attempts - } return cfg, nil } func (c Config) Validate() error { cfg := c - if cfg.Server.ClickHouse.Enabled && strings.TrimSpace(cfg.Server.Dataframer.Recipe) == "" { - return fmt.Errorf("server.dataframer.recipe is required when server.clickhouse.enabled is true") - } - if strings.TrimSpace(cfg.Server.SnapshotDirectory) == "" { - return fmt.Errorf("server.snapshot_directory is required") + if cfg.Server.RecipeQueryPageRows < 0 { + return errors.New("server.recipe_query_page_rows cannot be negative") } - if cfg.Server.SnapshotRetention <= 0 { - return fmt.Errorf("server.snapshot_retention must be positive") + if strings.TrimSpace(cfg.Server.LocalWorkspaceWriteback) != "" && !(cfg.Server.AllowUnauthenticated || cfg.Auth.AllowUnauthenticated) { + return errors.New("server.local_workspace_writeback requires unauthenticated local-development mode") } - if cfg.Server.PublicationWorkerLease <= 0 { - return fmt.Errorf("server.publication_worker_lease must be positive") + if (strings.TrimSpace(cfg.Server.LocalWorkspaceWriteback) == "") != (strings.TrimSpace(cfg.Server.LocalWorkspaceProject) == "") { + return errors.New("server.local_workspace_writeback and server.local_workspace_project must be configured together") } - if cfg.Server.PublicationMaxAttempts <= 0 { - return fmt.Errorf("server.publication_max_attempts must be positive") + if cfg.Server.ClickHouse.Enabled && strings.TrimSpace(cfg.Server.Dataframer.Recipe) == "" { + return fmt.Errorf("server.dataframer.recipe is required when server.clickhouse.enabled is true") } for index, selector := range cfg.Server.RequiredDataframeSelectors { if err := selector.Validate(); err != nil { diff --git a/internal/server/project_id.go b/internal/server/project_id.go deleted file mode 100644 index 9cf0d263..00000000 --- a/internal/server/project_id.go +++ /dev/null @@ -1,20 +0,0 @@ -package server - -import ( - "net/url" - "strings" - - "github.com/calypr/loom/internal/projectid" - "github.com/gofiber/fiber/v3" -) - -// explorerProjectParam decodes the percent-encoded slash used inside the -// existing /projects/:project route and exposes the canonical public ID to -// the rest of the Explorer HTTP layer. -func explorerProjectParam(c fiber.Ctx) string { - raw := strings.TrimSpace(c.Params("project")) - if decoded, err := url.PathUnescape(raw); err == nil { - raw = decoded - } - return projectid.Canonical(raw) -} diff --git a/internal/server/project_id_test.go b/internal/server/project_id_test.go deleted file mode 100644 index 81450292..00000000 --- a/internal/server/project_id_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package server - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gofiber/fiber/v3" -) - -func TestExplorerProjectParamDecodesEncodedSlash(t *testing.T) { - app := fiber.New() - app.Get("/api/v1/projects/:project", func(c fiber.Ctx) error { - return c.SendString(explorerProjectParam(c)) - }) - - request := httptest.NewRequest(http.MethodGet, "/api/v1/projects/HTAN_INT%2FBForePC", nil) - response, err := app.Test(request) - if err != nil { - t.Fatal(err) - } - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - t.Fatalf("status = %d, want %d", response.StatusCode, http.StatusOK) - } - body, err := io.ReadAll(response.Body) - if err != nil { - t.Fatal(err) - } - if got := string(body); got != "HTAN_INT/BForePC" { - t.Fatalf("decoded project = %q, want %q", got, "HTAN_INT/BForePC") - } -} diff --git a/internal/server/recipe_publication.go b/internal/server/recipe_publication.go index ed567f8b..830b23cd 100644 --- a/internal/server/recipe_publication.go +++ b/internal/server/recipe_publication.go @@ -4,20 +4,12 @@ import ( "context" "crypto/sha256" "encoding/hex" - "errors" - "fmt" - "log/slog" "sort" "strings" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" - "github.com/calypr/loom/internal/authscope" - dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" publication "github.com/calypr/loom/internal/dataframe/publication" - materializationarango "github.com/calypr/loom/internal/dataframe/publication/arango" - publicationclickhouse "github.com/calypr/loom/internal/dataframe/publication/clickhouse" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" ) func recipeScopeDigest(bindings recipe.RuntimeBindings) string { @@ -31,7 +23,7 @@ func recipeScopeDigest(bindings recipe.RuntimeBindings) string { // compiler schema to the backend-neutral publication schema. Publication must // not reconstruct nested names from semantic recipe nodes because those names // are finalized by physical lowering. -func recipeOutputLogicalColumns(plan engine.Resolved, outputName string) []publication.LogicalColumn { +func recipeOutputLogicalColumns(plan dataframeexecution.Resolved, outputName string) []publication.LogicalColumn { for _, output := range plan.Compiled.Outputs { if output.Name != outputName { continue @@ -67,14 +59,18 @@ func recipeOutputLogicalColumns(plan engine.Resolved, outputName string) []publi if column.Discovered { provenance = publication.ColumnDiscovered } - columns = append(columns, publication.LogicalColumn{Name: publication.FlatColumnName(output.RootResourceType, column.Name), SemanticPath: semanticPath, Kind: kind, Repeated: column.Cardinality == "many", Nullable: column.Nullable, Provenance: provenance}) + name := column.Name + if output.RootColumnNaming != recipe.RootColumnNamingExact { + name = publication.FlatColumnName(output.RootResourceType, name) + } + columns = append(columns, publication.LogicalColumn{Name: name, SemanticPath: semanticPath, Kind: kind, Repeated: column.Cardinality == "many", Nullable: column.Nullable, Provenance: provenance}) } return columns } return []publication.LogicalColumn{{Name: "__loom_row_id", Kind: "string", IsIdentity: true}} } -func recipeOutputRootResourceType(plan engine.Resolved, outputName string) string { +func recipeOutputRootResourceType(plan dataframeexecution.Resolved, outputName string) string { for _, output := range plan.Compiled.Outputs { if output.Name == outputName { return output.RootResourceType @@ -83,7 +79,16 @@ func recipeOutputRootResourceType(plan engine.Resolved, outputName string) strin return "" } -func publishResolvedRecipe(ctx context.Context, recipeEngine *engine.Engine, target publication.Target, name string, bindings recipe.RuntimeBindings, full engine.Resolved, batchRows, batchBytes int) (publication.BundleIdentity, error) { +func recipeOutputUsesExactRootColumns(plan dataframeexecution.Resolved, outputName string) bool { + for _, output := range plan.Compiled.Outputs { + if output.Name == outputName { + return output.RootColumnNaming == recipe.RootColumnNamingExact + } + } + return false +} + +func publishResolvedRecipe(ctx context.Context, recipeEngine *dataframeexecution.Engine, target publication.Target, name string, bindings recipe.RuntimeBindings, full dataframeexecution.Resolved, batchRows, batchBytes int) (publication.BundleIdentity, error) { streams, err := recipeEngine.Streams(ctx, full) if err != nil { return publication.BundleIdentity{}, err @@ -102,10 +107,14 @@ func publishResolvedRecipe(ctx context.Context, recipeEngine *engine.Engine, tar stream := stream columns := recipeOutputLogicalColumns(full, stream.Name) rootResourceType := recipeOutputRootResourceType(full, stream.Name) + exactRootColumns := recipeOutputUsesExactRootColumns(full, stream.Name) streamInputs = append(streamInputs, publication.OutputStream{ Name: stream.Name, Columns: columns, Stream: func(streamCtx context.Context, visit func(map[string]any) error) error { _, err := stream.Stream(streamCtx, func(row map[string]any) error { + if exactRootColumns { + return visit(row) + } qualified, err := publication.QualifyFlatRow(rootResourceType, row) if err != nil { return err @@ -129,105 +138,9 @@ func publishResolvedRecipe(ctx context.Context, recipeEngine *engine.Engine, tar return identity, err } -func incrementalPublicationOutput(bindings recipe.RuntimeBindings, streams []engine.OutputStream) string { +func incrementalPublicationOutput(bindings recipe.RuntimeBindings, streams []dataframeexecution.OutputStream) string { if len(bindings.OutputNames) != 1 || len(streams) != 1 { return "" } return streams[0].Name } - -func recipeResolvedDynamicColumnCounts(plan engine.Resolved) map[string]int { - counts := make(map[string]int, len(plan.Semantic.ResolvedColumns)) - for dynamicMap, columns := range plan.Semantic.ResolvedColumns { - counts[dynamicMap] = len(columns) - } - return counts -} - -func recipeMaterializer(recipeEngine *engine.Engine, bundleTarget publication.Target, registry *materializationarango.Registry, degradation error, logger *slog.Logger, batchRows, batchBytes int) func(context.Context, string, recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) { - return func(ctx context.Context, name string, bindings recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) { - if bundleTarget == nil { - cause := degradation - if cause == nil { - cause = dataframeerrors.ErrBackendUnavailable - } - return graphresolver.RecipeExecution{}, dataframeerrors.Wrap(cause, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) - } - bindings.IncludeAuthResourcePath = true - var identity publication.BundleIdentity - _, err := recipeEngine.Materialize(ctx, name, bindings, func(ctx context.Context, full engine.Resolved) error { - logger.Info("recipe materialization resolved", - "name", name, - "project", bindings.Project, - "resolved_schema_digest", full.ResolvedSchemaDigest, - "dynamic_column_counts", recipeResolvedDynamicColumnCounts(full), - ) - var err error - identity, err = publishResolvedRecipe(ctx, recipeEngine, bundleTarget, name, bindings, full, batchRows, batchBytes) - return err - }) - if err != nil { - var dynamicDrift *engine.DynamicDriftError - if errors.As(err, &dynamicDrift) { - logger.Error("recipe materialization dynamic schema drift", - "name", name, - "project", bindings.Project, - "dynamic_map", dynamicDrift.DynamicName, - "unexpected_key", dynamicDrift.Key, - "frozen_key_count", dynamicDrift.FrozenKeyCount, - ) - err = dataframeerrors.Wrap( - err, - dataframeerrors.CodeSchemaConflict, - "recipe dynamic-column schema does not match observed data", - dataframeerrors.WithDetails(map[string]any{ - "dynamicMap": dynamicDrift.DynamicName, - "unexpectedKey": dynamicDrift.Key, - "frozenKeyCount": dynamicDrift.FrozenKeyCount, - }), - ) - } - logger.Error("recipe materialization failed", "name", name, "project", bindings.Project, "error", err.Error()) - return graphresolver.RecipeExecution{}, err - } - published, err := registry.FindExecutionByKey(ctx, identity.Key()) - if err != nil { - logger.Error("load published recipe execution failed", "name", name, "project", bindings.Project, "error", err.Error()) - return graphresolver.RecipeExecution{}, fmt.Errorf("load published recipe execution: %w", err) - } - outputs := make([]graphresolver.RecipeExecutionOutput, 0, len(published.Outputs)) - for _, output := range published.Outputs { - rowCount := int(output.RowCount) - outputs = append(outputs, graphresolver.RecipeExecutionOutput{ - Name: output.Name, State: string(output.State.Canonical()), RowCount: &rowCount, - Columns: append([]publication.PhysicalColumn(nil), output.Columns...), - }) - } - return graphresolver.RecipeExecution{ID: published.ID, Name: name, RecipeDigest: published.RecipeDigest, ResolvedSchemaDigest: published.SchemaDigest, SourceGeneration: published.DatasetGeneration, State: string(published.State.Canonical()), Outputs: outputs}, nil - } -} - -func recipePublicationProcessor(recipeEngine *engine.Engine, logger *slog.Logger, batchRows, batchBytes int) publicationclickhouse.ExecutionProcessor { - return func(ctx context.Context, execution publication.BundleExecution, target publication.Target) error { - mode := authscope.ReadScopeMode(execution.AuthScopeMode) - if mode == "" { // Compatibility for executions created before scope mode was persisted. - mode = authscope.ReadScopeUnrestricted - if len(execution.AuthResourcePaths) != 0 { - mode = authscope.ReadScopeRestricted - } - } - bindings := recipe.RuntimeBindings{ - Project: execution.Project, DatasetGeneration: execution.DatasetGeneration, - AuthResourcePaths: append([]string(nil), execution.AuthResourcePaths...), - AuthScopeMode: mode, IncludeAuthResourcePath: true, - } - _, err := recipeEngine.MaterializeVersion(ctx, execution.Name, execution.TranslationVersion, bindings, func(ctx context.Context, full engine.Resolved) error { - _, publishErr := publishResolvedRecipe(ctx, recipeEngine, target, execution.Name, bindings, full, batchRows, batchBytes) - return publishErr - }) - if err != nil && logger != nil { - logger.Error("asynchronous recipe materialization failed", "execution_id", execution.ID, "recipe", execution.Name, "translation_version", execution.TranslationVersion, "project", execution.Project, "generation", execution.DatasetGeneration, "error", err.Error()) - } - return err - } -} diff --git a/internal/server/release_verifier.go b/internal/server/release_verifier.go index a5810605..1ad83176 100644 --- a/internal/server/release_verifier.go +++ b/internal/server/release_verifier.go @@ -6,19 +6,13 @@ import ( "github.com/calypr/loom/internal/dataframe/publication" "github.com/calypr/loom/internal/dataset" - "github.com/calypr/loom/internal/store/arango" ) // publicationVerificationStore is a narrow integration adapter over durable // execution metadata. Publication owns the record shape; release activation // consumes only its frozen exact-selector contract. -type releaseQueryClient interface { - QueryRows(context.Context, string, int, map[string]interface{}, arango.RowVisitor) error -} - type publicationVerificationStore struct { executions publication.ExactExecutionCatalog - query releaseQueryClient } func (s publicationVerificationStore) VerifyPublication(ctx context.Context, project, generation string, selector dataset.DataframeSelector) (dataset.PublicationVerification, error) { @@ -40,29 +34,4 @@ func (s publicationVerificationStore) VerifyPublication(ctx context.Context, pro }, nil } -func (s publicationVerificationStore) ListExecutionProjects(ctx context.Context) ([]string, error) { - if s.query == nil { - return nil, fmt.Errorf("publication project inventory is unavailable") - } - projects := make([]string, 0) - err := s.query.QueryRows(ctx, listExecutionProjectsAQL, 32, map[string]any{"@collection": "loom_dataframe_bundle_executions"}, func(row map[string]any) error { - project, _ := row["project"].(string) - if project != "" { - projects = append(projects, project) - } - return nil - }) - return projects, err -} - -const listExecutionProjectsAQL = ` -FOR execution IN @@collection - LET project = execution.project != null ? execution.project : execution.Project - FILTER project != null - COLLECT value = project - SORT value - RETURN {project: value} -` - var _ dataset.PublicationVerifier = publicationVerificationStore{} -var _ dataset.ExecutionProjectSource = publicationVerificationStore{} diff --git a/internal/server/routes.go b/internal/server/routes.go index 492d31a1..22b3f163 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -10,13 +10,12 @@ import ( api "github.com/calypr/loom/internal/api/http" "github.com/calypr/loom/internal/authscope" publication "github.com/calypr/loom/internal/dataframe/publication" - "github.com/calypr/loom/internal/dataset" ) -func registerRoutes(server *api.HTTPServer, resourceService *loadapi.Service, snapshotService *loadapi.SnapshotService, releaseService *dataset.ReleaseService, authorizer authscope.Authorizer, graphResolver *resolver.Resolver, explorerHandlers *explorerHTTPHandlers, optional ...any) error { - resourceHandler, err := loadapi.NewHandler(loadapi.Config{Service: resourceService, Authorizer: authorizer, Snapshots: snapshotService, Releases: releaseService}) +func registerRoutes(server *api.HTTPServer, generationService *loadapi.Service, authorizer authscope.Authorizer, graphResolver *resolver.Resolver, explorerHandlers *explorerHTTPHandlers, optional ...any) error { + loadHandler, err := loadapi.NewHandler(loadapi.Config{Service: generationService, Authorizer: authorizer}) if err != nil { - return fmt.Errorf("create resource load handler: %w", err) + return fmt.Errorf("create generation load handler: %w", err) } var releases publication.BundleCatalog var scopes *authscope.ScopeResolver @@ -30,12 +29,13 @@ func registerRoutes(server *api.HTTPServer, resourceService *loadapi.Service, sn } routes := &HTTPRoutes{ server: server, - load: resourceHandler, - recipe: api.RecipeExecutionHandler{Catalog: releases, Scopes: scopes}, + load: loadHandler, + releases: releases, + scopes: scopes, graphql: graphapi.RouteConfig{Handler: graphapi.NewHandler(graphResolver, server.Logger()), Playground: graphapi.NewPlaygroundHandler("/graphql/graph"), Sandbox: graphapi.NewApolloSandboxHandler("/graphql/graph")}, explorer: explorerHandlers, } - handler := loomapi.NewStrictHandler(routes, []loomapi.StrictMiddlewareFunc{strictFiberContextMiddleware}) + handler := loomapi.NewStrictHandler(routes, nil) loomapi.RegisterHandlersWithOptions(server.App(), handler, loomapi.FiberServerOptions{}) return nil } diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go index 667b654d..014899f4 100644 --- a/internal/server/routes_test.go +++ b/internal/server/routes_test.go @@ -16,7 +16,7 @@ import ( type routeRunner struct{} -func (routeRunner) Run(context.Context, loadapi.ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { +func (routeRunner) RunGeneration(context.Context, loadapi.GenerationLoadRequest, ingest.EventSink) (ingest.LoadSummary, error) { return ingest.LoadSummary{}, nil } @@ -28,11 +28,11 @@ func TestRegisterRoutesExposesGenerationReleaseWorkflow(t *testing.T) { if err != nil { t.Fatal(err) } - resourceService, err := loadapi.NewService(loadapi.ServiceConfig{Runner: routeRunner{}}) + resourceService, err := loadapi.NewService(loadapi.ServiceConfig{LoadGeneration: routeRunner{}.RunGeneration}) if err != nil { t.Fatal(err) } - if err := registerRoutes(server, resourceService, nil, nil, authscope.AllowAllAuthorizer{}, &resolver.Resolver{}, &explorerHTTPHandlers{lifecycle: &explorerLifecycleHandlers{}, authoring: &explorerAuthoringHandlers{}}); err != nil { + if err := registerRoutes(server, resourceService, authscope.AllowAllAuthorizer{}, &resolver.Resolver{}, &explorerHTTPHandlers{}); err != nil { t.Fatal(err) } @@ -55,6 +55,7 @@ func TestRegisterRoutesExposesGenerationReleaseWorkflow(t *testing.T) { }{ {http.MethodGet, "/api/v1/datasets/project/generations/generation/export"}, {http.MethodPost, "/loom/api/v1/dataframe/export"}, + {http.MethodPut, "/api/v1/raw"}, } { resp, err := server.App().Test(httptest.NewRequest(request.method, request.path, nil)) if err != nil { @@ -70,8 +71,6 @@ func TestRegisterRoutesExposesGenerationReleaseWorkflow(t *testing.T) { path string want int }{ - {http.MethodGet, "/api/v1/raw", http.StatusMethodNotAllowed}, - {http.MethodPut, "/api/v1/raw", http.StatusUnsupportedMediaType}, {http.MethodPost, "/api/v1/datasets/project/generations/generation", http.StatusUnsupportedMediaType}, {http.MethodPost, "/api/v1/datasets/project/generations/generation/activate", http.StatusBadRequest}, } { @@ -89,7 +88,7 @@ func TestRegisterRoutesExposesGenerationReleaseWorkflow(t *testing.T) { t.Fatal(err) } _ = resp.Body.Close() - if resp.StatusCode != http.StatusUnsupportedMediaType { - t.Fatalf("resource route status = %d, want %d", resp.StatusCode, http.StatusUnsupportedMediaType) + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("removed resource route status = %d, want %d", resp.StatusCode, http.StatusNotFound) } } diff --git a/internal/server/server.go b/internal/server/server.go index 6b8c1da5..7317905f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,15 +4,12 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "errors" "flag" "fmt" "log/slog" "os" "os/signal" - "strconv" - "strings" "syscall" "time" @@ -24,23 +21,21 @@ import ( "github.com/calypr/loom/internal/catalog" catalogarango "github.com/calypr/loom/internal/catalog/arango" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" publication "github.com/calypr/loom/internal/dataframe/publication" bundlearango "github.com/calypr/loom/internal/dataframe/publication/arango" publicationclickhouse "github.com/calypr/loom/internal/dataframe/publication/clickhouse" "github.com/calypr/loom/internal/dataframe/published" "github.com/calypr/loom/internal/dataframe/recipe" - "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/dataframe/recipe/exec" recipearango "github.com/calypr/loom/internal/dataframe/recipe/exec/arango" - dataframeruntime "github.com/calypr/loom/internal/dataframe/runtime" publicationcontract "github.com/calypr/loom/internal/dataset" publicationarango "github.com/calypr/loom/internal/dataset/arango" "github.com/calypr/loom/internal/explorer" explorerarango "github.com/calypr/loom/internal/explorer/arango" "github.com/calypr/loom/internal/explorer/capability" - explorercompilation "github.com/calypr/loom/internal/explorer/compilation" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/calypr/loom/internal/ingest" - "github.com/calypr/loom/internal/projectid" arangostore "github.com/calypr/loom/internal/store/arango" clickhousestore "github.com/calypr/loom/internal/store/clickhouse" ) @@ -75,6 +70,37 @@ func recordDegradation(logger *slog.Logger, current error, stage string, cause e return errors.Join(current, fmt.Errorf("%s: %w", stage, cause)) } +func classifyDataframeQueryError(err error) error { + if err == nil { + return err + } + switch { + case arangostore.IsQueryMemoryLimitExceeded(err): + return dataframeerrors.Wrap( + err, + dataframeerrors.CodeQueryMemoryLimitExceeded, + "", + dataframeerrors.WithDetails(map[string]any{"backend": "arangodb", "resource": "query_memory"}), + ) + case arangostore.IsQueryResourceLimitExceeded(err): + return dataframeerrors.Wrap( + err, + dataframeerrors.CodeQueryResourceLimitExceeded, + "", + dataframeerrors.WithDetails(map[string]any{"backend": "arangodb", "resource": "query"}), + ) + case arangostore.IsQueryOutOfMemory(err): + return dataframeerrors.Wrap( + err, + dataframeerrors.CodeQueryBackendOutOfMemory, + "", + dataframeerrors.WithDetails(map[string]any{"backend": "arangodb", "resource": "memory"}), + ) + default: + return err + } +} + func run(ctx context.Context, serverConfig Config) error { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) connOpts := arangostore.ConnectionOptions{ @@ -141,8 +167,8 @@ func run(ctx context.Context, serverConfig Config) error { } authenticator, authorizer, scopeResolver := auth.authenticator, auth.authorizer, auth.scopeResolver - dataframes := dataframeruntime.NewService(dataframeruntime.ServiceConfig{QueryRows: func(ctx context.Context, query string, batch int, binds map[string]any, visit func(map[string]any) error) error { - return lifecycleClient.QueryRows(ctx, query, batch, binds, visit) + dataframes := dataframeexecution.NewService(dataframeexecution.ServiceConfig{QueryRows: func(ctx context.Context, query string, batch int, binds map[string]any, visit func(map[string]any) error) error { + return classifyDataframeQueryError(lifecycleClient.QueryRows(ctx, query, batch, binds, visit)) }}) // The lifecycle client already owns this Arango database. Reusing it avoids // a second connection that can fail independently during optional startup. @@ -176,7 +202,7 @@ func run(ctx context.Context, serverConfig Config) error { if err != nil { return fmt.Errorf("create recipe revision registry: %w", err) } - recipeEngine, err := engine.New(engine.Config{ + recipeEngine, err := dataframeexecution.New(dataframeexecution.Config{ Registry: recipeRegistry, Revisions: recipeRevisions, ResolveBundle: recipeSchemaResolver(catalogStore.DiscoverFields, discoveryCache), @@ -189,18 +215,18 @@ func run(ctx context.Context, serverConfig Config) error { fields := []any{"query_id", queryID, "query_bytes", len(query), "bind_vars", len(bindVars), "seconds", time.Since(started).Seconds()} if err != nil { logger.Error("dataframe AQL failed", append(fields, "error", err.Error())...) - return err + return classifyDataframeQueryError(err) } logger.Info("dataframe AQL complete", fields...) return nil }, - ScopeDigest: recipeScopeDigest, + ScopeDigest: recipeScopeDigest, + RootPageRows: serverConfig.Server.RecipeQueryPageRows, }) if err != nil { return fmt.Errorf("create dataframe recipe engine: %w", err) } var bundleTarget publication.Target - var publicationWorker *publicationclickhouse.Worker if serverConfig.Server.ClickHouse.Enabled && publicationReady { bundleStore, err := publicationclickhouse.NewBundleStore(clickhouse, publishedRegistry) if err != nil { @@ -211,18 +237,11 @@ func run(ctx context.Context, serverConfig Config) error { publicationReady = false } if publicationReady { - bundleTarget, err = publicationclickhouse.New(bundleStore) - if err != nil { - return fmt.Errorf("create dataframe publication target: %w", err) - } - publicationWorker, err = publicationclickhouse.NewWorker(bundleStore, recipePublicationProcessor(recipeEngine, logger, serverConfig.Server.RecipeBatchRows, serverConfig.Server.RecipeBatchBytes), publicationclickhouse.WorkerConfig{Lease: serverConfig.Server.PublicationWorkerLease, MaxAttempts: serverConfig.Server.PublicationMaxAttempts}) - if err != nil { - return fmt.Errorf("create dataframe publication worker: %w", err) - } + bundleTarget = bundleStore } } - verificationStore := publicationVerificationStore{executions: publishedRegistry, query: lifecycleClient} - releaseService := &publicationcontract.ReleaseService{Snapshots: lifecycleStore, Manifests: lifecycleStore, Releases: lifecycleStore, Verifier: verificationStore, Required: serverConfig.Server.RequiredDataframeSelectors} + verificationStore := publicationVerificationStore{executions: publishedRegistry} + releaseService := &publicationcontract.ReleaseService{Manifests: lifecycleStore, Releases: lifecycleStore, Verifier: verificationStore, Required: serverConfig.Server.RequiredDataframeSelectors} activateExplorerRelease := func(ctx context.Context, project, generation string, selectors []publicationcontract.DataframeSelector) error { expectedRevision := int64(0) active, err := releaseService.Active(ctx, project) @@ -237,63 +256,30 @@ func run(ctx context.Context, serverConfig Config) error { }) return err } - validateExplorerReleaseGeneration := func(ctx context.Context, project, generation string) error { - return releaseService.ValidateGeneration(ctx, project, generation) - } - noAuthEnabled := serverConfig.Server.AllowUnauthenticated || serverConfig.Auth.AllowUnauthenticated - var candidateProjects func(context.Context) ([]string, error) - if noAuthEnabled { - inventory := publicationcontract.ProjectInventory{Snapshots: lifecycleStore, Releases: lifecycleStore, Executions: verificationStore} - candidateProjects = func(ctx context.Context) ([]string, error) { - return inventory.ExpectedProjects(ctx, nil, true) - } - } - if materializationReader != nil { - statusResolver := releaseProjectStatusResolver{releases: lifecycleStore, executions: publishedRegistry} - materializationReader.ProjectStatusResolver = statusResolver - materializationReader.ReleaseExecutionResolver = statusResolver - materializationReader.FederationSnapshotResolver = statusResolver - } - var exactStarter graphresolver.ExactMaterializationStarter - if publicationWorker != nil { - exactStarter = func(ctx context.Context, selector publicationcontract.DataframeSelector, bindings recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) { - identity := publication.BundleIdentity{ - Name: selector.Recipe, TranslationVersion: selector.TranslationVersion, - Project: bindings.Project, DatasetGeneration: bindings.DatasetGeneration, - ScopeDigest: recipeScopeDigest(bindings), EngineVersion: "loom-recipe-v1", - AuthScopeMode: string(bindings.AuthScopeMode), - AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), - } - execution, err := publicationWorker.Enqueue(ctx, identity) - if err != nil { - return graphresolver.RecipeExecution{}, err - } - return graphresolver.RecipeExecution{ - ID: execution.ID, Name: execution.Name, TranslationVersion: execution.TranslationVersion, - SourceGeneration: execution.DatasetGeneration, State: string(execution.State.Canonical()), - Outputs: []graphresolver.RecipeExecutionOutput{{Name: selector.Output, State: string(execution.State.Canonical()), Selector: selector}}, - }, nil + prepareExplorerRelease := func(ctx context.Context, project, generation string, selectors []publicationcontract.DataframeSelector) (publicationcontract.ProjectRelease, int64, error) { + expectedRevision := int64(0) + active, err := releaseService.Active(ctx, project) + if err == nil { + expectedRevision = active.Revision + } else if !errors.Is(err, publicationcontract.ErrNoActiveRelease) { + return publicationcontract.ProjectRelease{}, 0, err } + release, err := releaseService.Create(ctx, publicationcontract.ActivationRequest{ + Project: project, Generation: generation, GitCommit: generation, + OptionalSelectors: selectors, + }) + return release, expectedRevision, err } - releaseActivator := func(ctx context.Context, project, releaseID, expectedRevision string) (graphresolver.ProjectRelease, error) { - revision := int64(0) - if strings.TrimSpace(expectedRevision) != "" { - parsed, err := strconv.ParseInt(expectedRevision, 10, 64) - if err != nil || parsed < 0 { - return graphresolver.ProjectRelease{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - revision = parsed - } - active, err := releaseService.ActivateExisting(ctx, project, releaseID, revision) - if err != nil { - return graphresolver.ProjectRelease{}, err - } - return graphresolver.ProjectRelease{ID: active.Release.ID, Project: active.Release.Project, Generation: active.Release.Generation, Revision: strconv.FormatInt(active.Revision, 10), State: "ACTIVE"}, nil + validateExplorerReleaseGeneration := func(ctx context.Context, project, generation string) error { + return releaseService.ValidateGeneration(ctx, project, generation) } explorerStore, err := explorerarango.New(lifecycleClient) if err != nil { return fmt.Errorf("create Explorer store: %w", err) } + if err := explorerStore.MigrateLegacyRepositoryConfigs(ctx); err != nil { + return fmt.Errorf("migrate legacy Explorer repository configs: %w", err) + } capabilitySnapshots, err := explorerarango.NewCapabilitySnapshotStore(lifecycleClient) if err != nil { return fmt.Errorf("create Explorer capability snapshot store: %w", err) @@ -306,7 +292,6 @@ func run(ctx context.Context, serverConfig Config) error { if err != nil { return fmt.Errorf("create Explorer service: %w", err) } - explorerMaterializer := explorerBundleMaterializer(recipeEngine, bundleTarget, publishedRegistry, degradation, logger, serverConfig.Server.RecipeBatchRows, serverConfig.Server.RecipeBatchBytes) resolver := graphresolver.NewResolver(graphresolver.ResolverConfig{ DataframeQuery: queryapi.Config{ DiscoverReferences: discoverReferences, @@ -314,31 +299,26 @@ func run(ctx context.Context, serverConfig Config) error { Dataframes: dataframes, ScopeResolver: scopeResolver, ActiveManifestResolver: activeManifestResolver, - Explain: func(ctx context.Context, compiled dataframeruntime.CompiledQuery) error { + Explain: func(ctx context.Context, compiled dataframeexecution.CompiledQuery) error { _, err := explainCompiledQuery(ctx, lifecycleClient, compiled) return err }, }, MaterializationReader: materializationReader, Logger: logger, - RecipeControl: engine.Control{Engine: recipeEngine, ExplainConnection: func(ctx context.Context, compiled dataframeruntime.CompiledQuery) (engine.ExplainAssessment, error) { + RecipeControl: dataframeexecution.Control{Engine: recipeEngine, ExplainConnection: func(ctx context.Context, compiled dataframeexecution.CompiledQuery) (dataframeexecution.ExplainAssessment, error) { return explainCompiledQuery(ctx, lifecycleClient, compiled) }}, - RecipeAuthorizer: recipeAuthorization{resolver: scopeResolver}, - RecipeRevisions: recipeRevisions, - RecipeExecutions: graphresolver.NewAuthorizedRecipeExecutionReader(publishedRegistry, scopeResolver), - RecipeMaterialize: recipeMaterializer(recipeEngine, bundleTarget, publishedRegistry, degradation, logger, serverConfig.Server.RecipeBatchRows, serverConfig.Server.RecipeBatchBytes), - ExactMaterializationStarter: exactStarter, - ProjectReleaseActivator: releaseActivator, - CandidateProjects: candidateProjects, + RecipeAuthorizer: recipeAuthorization{resolver: scopeResolver}, + RecipeRevisions: recipeRevisions, + RecipeExecutions: graphresolver.NewAuthorizedRecipeExecutionReader(publishedRegistry, scopeResolver), }) ingestRunner := loadapi.IngestRunner{BaseOptions: ingest.LoadOptions{ ConnectionOptions: connOpts, Schema: serverConfig.Server.Schema, }} - resourceService, err := loadapi.NewService(loadapi.ServiceConfig{ - Runner: ingestRunner, - GenerationRunner: ingestRunner, + generationService, err := loadapi.NewService(loadapi.ServiceConfig{ + LoadGeneration: ingestRunner.RunGeneration, GenerationActivator: lifecycleStore, DataframeReleases: publishedRegistry, Logger: logger, @@ -350,141 +330,49 @@ func run(ctx context.Context, serverConfig Config) error { }, }) if err != nil { - return fmt.Errorf("create resource load service: %w", err) - } - snapshotService := &loadapi.SnapshotService{Repository: lifecycleStore, Blobs: loadapi.LocalSnapshotBlobs{Root: serverConfig.Server.SnapshotDirectory}, Runner: ingestRunner} - deletedGenerations, retentionErr := (publicationcontract.RetentionService{ - Repository: lifecycleStore, - Blobs: loadapi.LocalSnapshotBlobs{Root: serverConfig.Server.SnapshotDirectory}, - Retention: serverConfig.Server.SnapshotRetention, - }).RunOnce(ctx) - if retentionErr != nil { - logger.Error("snapshot retention cleanup failed", "error", retentionErr) - } else if len(deletedGenerations) != 0 { - logger.Info("snapshot retention cleanup complete", "deleted_generations", len(deletedGenerations)) - } - // CompileReceipt is the native V2 path. - compileReceipt := func(ctx context.Context, request ExplorerV2ReceiptCompileRequest) (*explorer.CompilationReceipt, error) { - started := time.Now() - authorized := request.Authorized.Clone() - if strings.TrimSpace(authorized.Snapshot.Token) == "" { - var err error - authorized, err = capabilityResolver.ResolveForCompilation(ctx, request.Project, request.SnapshotToken) - if err != nil { - return nil, err - } - } else if authorized.Snapshot.Identity.Project != projectid.Canonical(request.Project) || authorized.Snapshot.ValidateToken(request.SnapshotToken) != nil { - return nil, capability.ErrStaleSnapshot - } - snapshot := authorized.Snapshot - if err := validateAuthorizedReadScope(authorized.Scope, snapshot.Identity.AuthorizationScopeDigest); err != nil { - return nil, capability.ErrStaleSnapshot - } - workspace := request.Workspace.NormalizePresentationOrders() - intentDigest, err := workspace.Digest() - if err != nil { - return nil, err - } - normalized, err := workspace.CanonicalJSON() - if err != nil { - return nil, err - } - translated, err := explorercompilation.CompileWorkspace(ctx, request.Project, request.ExplorerID, workspace, snapshot) - if err != nil { - return nil, err - } - resolved, err := recipeEngine.CompileResolvedBundle(ctx, translated.Bundle, recipe.RuntimeBindings{Project: projectid.Legacy(request.Project), DatasetGeneration: snapshot.Identity.Generation, AuthResourcePaths: append([]string(nil), authorized.Scope.AuthResourcePaths...), AuthScopeMode: authorized.Scope.Mode}) - if err != nil { - return nil, err - } - emitted := make([]explorer.EmittedColumn, 0, len(translated.EmittedColumns)) - presentationByEmission := map[string]explorercompilation.PresentationColumn{} - for _, presentation := range translated.Presentations { - for _, column := range presentation.Columns { - presentationByEmission[column.EmissionID] = column - } - } - for _, column := range translated.EmittedColumns { - emitted = append(emitted, explorer.EmittedColumn{EmissionID: column.EmissionID, OutputID: column.OutputID, NodeID: column.NodeID, SelectionID: column.SelectionID, CandidateID: column.CandidateID, OccurrenceID: column.OccurrenceID, ProjectionMode: column.ProjectionMode, PublicColumn: column.PublicColumn, Label: presentationByEmission[column.EmissionID].Label, LogicalType: column.LogicalType, Filterable: column.Filterable, Chartable: column.Chartable}) - } - mappings := make([]explorer.IdentityMapping, 0, len(translated.IdentityMappings)) - for _, mapping := range translated.IdentityMappings { - mappings = append(mappings, explorer.IdentityMapping{OutputID: mapping.OutputID, CandidateID: mapping.CandidateID, OccurrenceID: mapping.OccurrenceID, ProjectionMode: mapping.ProjectionMode, EmissionIDs: append([]string(nil), mapping.EmissionIDs...)}) - } - contract, err := json.Marshal(explorer.PublicOutputContracts{Outputs: translated.OutputContracts}) - if err != nil { - return nil, err - } - contractDigest, err := explorer.CompilationArtifactDigest(contract) - if err != nil { - return nil, err - } - resolvedRecipeDigest, err := resolved.Bundle.Digest() - if err != nil { - return nil, err - } - compiledConfig, err := compiledExplorerWorkspaceConfigV2(request.Project, request.ExplorerID, translated) - if err != nil { - return nil, err - } - fingerprints := resolvedOutputFingerprints(resolved) - receipt := explorer.CompilationReceipt{ReceiptFormatVersion: explorer.CurrentReceiptFormatVersion, CompilerContractVersion: explorer.CurrentCompilerContractVersion, Project: projectid.Canonical(request.Project), ExplorerID: request.ExplorerID, IntentDigest: intentDigest, SnapshotToken: request.SnapshotToken, AuthorizationScopeDigest: snapshot.Identity.AuthorizationScopeDigest, CapabilitySchemaDigest: snapshot.Identity.SchemaDigest, SourceGeneration: snapshot.Identity.Generation, RecipeDigest: resolved.StoredRecipeDigest, ResolvedRecipeDigest: resolvedRecipeDigest, ResolvedSchemaDigest: resolved.ResolvedSchemaDigest, OutputContractDigest: contractDigest, NormalizedBundle: normalized, Bundle: resolved.Bundle, CompiledConfig: compiledConfig, PublicOutputContract: contract, IdentityMappings: mappings, EmittedColumns: emitted, OutputFingerprints: fingerprints, RequestID: request.RequestID, CreatedAt: time.Now().UTC()} - receipt.CompilationKey, err = explorer.CompilationKey(receipt) - if err != nil { - return nil, err - } - receipt.ID, err = explorer.ReceiptID(receipt) - if err != nil { - return nil, err - } - stored, err := explorerService.StoreCompilationReceipt(ctx, receipt) - if err != nil { - return nil, err - } - verificationBindings := recipe.RuntimeBindings{Project: projectid.Legacy(request.Project), DatasetGeneration: snapshot.Identity.Generation, AuthResourcePaths: append([]string(nil), authorized.Scope.AuthResourcePaths...), AuthScopeMode: authorized.Scope.Mode} - if _, err := compileValidatedReceiptResolution(ctx, recipeEngine, stored, verificationBindings); err != nil { - return nil, explorerConflict("compile", "COMPILATION_NONDETERMINISTIC", "the compiler could not reproduce the stored receipt artifact", map[string]any{"receiptId": stored.ID}) - } - receiptBytes := 0 - if raw, marshalErr := json.Marshal(stored); marshalErr == nil { - receiptBytes = len(raw) - } - logger.Info("Explorer receipt compiled", "project", receipt.Project, "explorer_id", receipt.ExplorerID, "receipt_id", receipt.ID, "duration_ms", time.Since(started).Milliseconds(), "receipt_bytes", receiptBytes, "output_count", len(receipt.Bundle.Outputs), "column_count", len(receipt.EmittedColumns)) - return stored, nil + return fmt.Errorf("create generation load service: %w", err) + } + compileReceipt := func(ctx context.Context, request lifecycle.CompileReceiptRequest) (*explorer.CompilationReceipt, error) { + return compileExplorerReceipt(ctx, request, capabilityResolver, recipeEngine, explorerService, logger) } - lifecycleConfig := ExplorerV2LifecycleConfig{ + persistPublishedWorkspace, err := localWorkspaceWriter(serverConfig.Server.LocalWorkspaceWriteback, serverConfig.Server.LocalWorkspaceProject) + if err != nil { + return fmt.Errorf("configure local workspace writeback: %w", err) + } + lifecycleConfig := lifecycle.Config{ CompileReceipt: compileReceipt, - Capability: func(ctx context.Context, project, _ string, generation string) (capability.Snapshot, error) { - return capabilityResolver.Resolve(ctx, project, generation) + Capability: lifecycle.CapabilityResolver{ + Current: func(ctx context.Context, project, _ string, generation string) (capability.Snapshot, error) { + return capabilityResolver.Resolve(ctx, project, generation) + }, + Token: capabilityResolver.ResolveToken, + ForCompilation: capabilityResolver.ResolveForCompilation, + ForExecution: capabilityResolver.ResolveForExecution, + Catalog: authoringV2Catalog, }, - CapabilityToken: capabilityResolver.ResolveToken, - AuthorizedCapabilityCompile: capabilityResolver.ResolveForCompilation, - AuthorizedCapabilityExecution: capabilityResolver.ResolveForExecution, - Logger: logger, ReceiptLookup: func(ctx context.Context, project, explorerID, receiptID string) (*explorer.CompilationReceipt, error) { return explorerService.CompilationReceiptForExplorer(ctx, project, explorerID, receiptID) }, - Preview: func(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (map[string][]map[string]any, error) { - return recipeEngine.PreviewBundle(ctx, bundle, bindings) - }, - PreviewReceipt: func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (engine.PreviewSummary, error) { + PreviewReceipt: func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { if receipt == nil { - return engine.PreviewSummary{}, fmt.Errorf("compilation receipt is required") + return dataframeexecution.PreviewSummary{}, fmt.Errorf("compilation receipt is required") } resolved, err := compileValidatedReceiptResolution(ctx, recipeEngine, receipt, bindings) if err != nil { - return engine.PreviewSummary{}, &receiptPreviewResolutionError{Err: err} + logger.Error("Explorer receipt preview resolution failed", "receipt_id", receipt.ID, "error", err) + return dataframeexecution.PreviewSummary{}, classifyReceiptPreviewResolutionError(receipt.ID, err) } output := "" if len(bindings.OutputNames) > 0 { output = bindings.OutputNames[0] } - return recipeEngine.PreviewOutput(ctx, resolved, engine.PreviewRequest{Output: output, Limit: bindings.PreviewLimit}, visit) + return recipeEngine.PreviewOutput(ctx, resolved, dataframeexecution.PreviewRequest{Output: output, Limit: bindings.PreviewLimit}, visit) }, - Materialize: explorerMaterializer, MaterializeReceipt: explorerReceiptMaterializer(recipeEngine, bundleTarget, publishedRegistry, degradation, logger, serverConfig.Server.RecipeBatchRows, serverConfig.Server.RecipeBatchBytes), ValidateReleaseGeneration: validateExplorerReleaseGeneration, ActivateRelease: activateExplorerRelease, + PrepareRelease: prepareExplorerRelease, + PersistPublishedWorkspace: persistPublishedWorkspace, } server, err := httpapi.NewHTTPServer(httpapi.HTTPConfig{Authenticator: authenticator, Authorizer: authorizer, Logger: logger, CoreReadyCheck: func(ctx context.Context) error { @@ -507,21 +395,10 @@ func run(ctx context.Context, serverConfig Config) error { return nil } return scopeResolver.AuthorizeReadProject(ctx, principal, project) - }, explorerService, explorerMaterializer, lifecycleConfig) - if err := registerRoutes(server, resourceService, snapshotService, releaseService, authorizer, resolver, explorerHandlers, publishedRegistry, scopeResolver); err != nil { + }, explorerService, lifecycleConfig) + if err := registerRoutes(server, generationService, authorizer, resolver, explorerHandlers, publishedRegistry, scopeResolver); err != nil { return fmt.Errorf("register HTTP routes: %w", err) } - if publicationWorker != nil { - go func() { - err := publicationWorker.Run(ctx, time.Second, func(workerErr error) { - logger.Error("dataframe publication worker iteration failed", "error", workerErr) - }) - if err != nil && !errors.Is(err, context.Canceled) { - logger.Error("dataframe publication worker stopped", "error", err) - } - }() - } - errCh := make(chan error, 1) go func() { logger.Info("starting HTTP server", "listen", serverConfig.Server.Listen, "database", serverConfig.Server.Database, "no_auth", serverConfig.Server.AllowUnauthenticated || serverConfig.Auth.AllowUnauthenticated) diff --git a/internal/store/arango/client.go b/internal/store/arango/client.go index 97c85d5a..c927594a 100644 --- a/internal/store/arango/client.go +++ b/internal/store/arango/client.go @@ -26,18 +26,60 @@ type Client struct { client *http.Client } -// Transaction is the narrow query surface used by stores that need to make -// several collection updates visible as one Arango transaction. -type Transaction interface { +// RowQueryer is the narrow row-query surface shared by Arango adapters. It is +// also the only capability exposed to transaction callbacks. +type RowQueryer interface { QueryRows(context.Context, string, int, map[string]interface{}, RowVisitor) error } +// BatchInserter is the narrow raw-document write surface shared by Arango +// adapters. +type BatchInserter interface { + InsertBatchRaw(context.Context, string, []json.RawMessage, bool, string) error +} + +// AQLExecutor is the narrow surface used for non-row AQL statements. +type AQLExecutor interface { + ExecuteAQL(context.Context, string, map[string]interface{}) error +} + +// RowBatchClient is the narrow composite used by registries that only need +// query and raw-document batch-write capabilities. +type RowBatchClient interface { + RowQueryer + BatchInserter +} + +// IsQueryMemoryLimitExceeded reports whether ArangoDB rejected a query because +// it exceeded the configured query memory limit. +func IsQueryMemoryLimitExceeded(err error) bool { + if !IsQueryResourceLimitExceeded(err) { + return false + } + ok, arangoErr := shared.IsArangoError(err) + return ok && strings.Contains(strings.ToLower(arangoErr.ErrorMessage), "memory") +} + +// IsQueryResourceLimitExceeded reports whether ArangoDB rejected a query at a +// configured resource limit. Callers can use IsQueryMemoryLimitExceeded first +// when they need the narrower diagnosis. +func IsQueryResourceLimitExceeded(err error) bool { + return shared.IsArangoErrorWithErrorNum(err, shared.ErrResourceLimit) +} + +// IsQueryOutOfMemory reports whether ArangoDB ran out of memory while +// executing a query. This is distinct from rejecting a query at a configured +// memory limit. +func IsQueryOutOfMemory(err error) bool { + return shared.IsArangoErrorWithErrorNum(err, shared.ErrOutOfMemory) +} + type TransactionCollections struct { Read []string Write []string } -type TransactionFunc func(context.Context, Transaction) error +type TransactionFunc func(context.Context, RowQueryer) error var bufferPool = sync.Pool{ New: func() any { diff --git a/internal/store/arango/client_queryrows_test.go b/internal/store/arango/client_queryrows_test.go index 515b246b..b4199985 100644 --- a/internal/store/arango/client_queryrows_test.go +++ b/internal/store/arango/client_queryrows_test.go @@ -3,6 +3,7 @@ package arango import ( "context" "errors" + "fmt" "strings" "testing" @@ -82,6 +83,40 @@ func TestQueryRowsWrapsDriverQueryError(t *testing.T) { } } +func TestIsQueryMemoryLimitExceededRecognizesWrappedArangoResourceLimit(t *testing.T) { + driverErr := shared.ArangoError{ + HasError: true, + Code: 500, + ErrorNum: shared.ErrResourceLimit, + ErrorMessage: "AQL: query would use more memory than allowed", + } + err := fmt.Errorf("arango query: %w", driverErr) + + if !IsQueryMemoryLimitExceeded(err) { + t.Fatalf("IsQueryMemoryLimitExceeded(%v) = false, want true", err) + } + if !IsQueryResourceLimitExceeded(err) { + t.Fatalf("IsQueryResourceLimitExceeded(%v) = false, want true", err) + } + if IsQueryMemoryLimitExceeded(errors.New("unrelated query failure")) { + t.Fatal("unrelated query failure was classified as a memory-limit error") + } + outOfMemory := shared.ArangoError{HasError: true, Code: 500, ErrorNum: shared.ErrOutOfMemory, ErrorMessage: "out of memory"} + if IsQueryMemoryLimitExceeded(outOfMemory) { + t.Fatal("database out-of-memory was classified as a configured query-memory limit") + } + if !IsQueryOutOfMemory(outOfMemory) { + t.Fatal("database out-of-memory was not recognized") + } + otherResource := shared.ArangoError{HasError: true, Code: 500, ErrorNum: shared.ErrResourceLimit, ErrorMessage: "resource limit exceeded"} + if IsQueryMemoryLimitExceeded(otherResource) { + t.Fatal("non-memory resource limit was classified as a memory limit") + } + if !IsQueryResourceLimitExceeded(otherResource) { + t.Fatal("non-memory resource limit was not recognized") + } +} + func TestQueryRowsClosesCursorOnVisitorError(t *testing.T) { want := errors.New("visitor stopped") cursor := &fakeCursor{rows: []map[string]any{{"id": "1"}}} diff --git a/openapi/README.md b/openapi/README.md index db1598d2..9a443982 100644 --- a/openapi/README.md +++ b/openapi/README.md @@ -1,7 +1,7 @@ # Loom OpenAPI contract [`openapi.yaml`](openapi.yaml) is the canonical contract for every Loom HTTP -route. It currently defines all 39 REST, health, GraphQL transport, and browser +route. It currently defines all 22 REST, health, GraphQL transport, and browser UI operations registered by the server. The files in this directory are source files: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 567c8e69..68839943 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -21,34 +21,6 @@ paths: '401': {$ref: '#/components/responses/ServiceUnauthorized'} '403': {$ref: '#/components/responses/AuthoringForbidden'} '500': {$ref: '#/components/responses/AuthoringInternalError'} - /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/capabilities/{snapshotToken}/candidates/{candidateId}/suggestions: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/ExplorerId' - - name: snapshotToken - in: path - required: true - schema: {type: string, minLength: 1} - - name: candidateId - in: path - required: true - schema: {type: string, minLength: 1} - get: - operationId: getExplorerCandidateSuggestions - responses: - '200': - description: Bounded values for one candidate in an immutable snapshot. - content: - application/json: - schema: - $ref: '#/components/schemas/CandidateSuggestions' - '400': {$ref: '#/components/responses/AuthoringBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/AuthoringForbidden'} - '404': {$ref: '#/components/responses/AuthoringNotFound'} - '409': {$ref: '#/components/responses/AuthoringConflict'} - '500': {$ref: '#/components/responses/AuthoringInternalError'} - '503': {$ref: '#/components/responses/AuthoringUnavailable'} /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/suggestions: parameters: - $ref: '#/components/parameters/Project' @@ -93,60 +65,14 @@ paths: '409': {$ref: '#/components/responses/AuthoringConflict'} '500': {$ref: '#/components/responses/AuthoringInternalError'} '503': {$ref: '#/components/responses/AuthoringUnavailable'} - post: - operationId: compileExplorerBuilder - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CompileRequest' - responses: - '200': - description: Immutable compilation receipt. - content: - application/json: - schema: - $ref: '#/components/schemas/CompileResponse' - '400': {$ref: '#/components/responses/AuthoringBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/AuthoringForbidden'} - '409': {$ref: '#/components/responses/AuthoringConflict'} - '422': {$ref: '#/components/responses/AuthoringUnprocessable'} - '500': {$ref: '#/components/responses/AuthoringInternalError'} - '503': {$ref: '#/components/responses/AuthoringUnavailable'} - /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/compile: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/ExplorerId' - post: - operationId: compileExplorer - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CompileRequest' - responses: - '200': - description: Immutable compilation receipt. - content: - application/json: - schema: - $ref: '#/components/schemas/CompileResponse' - '400': {$ref: '#/components/responses/AuthoringBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/AuthoringForbidden'} - '409': {$ref: '#/components/responses/AuthoringConflict'} - '422': {$ref: '#/components/responses/AuthoringUnprocessable'} - '500': {$ref: '#/components/responses/AuthoringInternalError'} - '503': {$ref: '#/components/responses/AuthoringUnavailable'} /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/commands: parameters: - $ref: '#/components/parameters/Project' - $ref: '#/components/parameters/ExplorerId' post: operationId: applyExplorerBuilderCommands + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -173,6 +99,8 @@ paths: - $ref: '#/components/parameters/ExplorerId' post: operationId: reconcileExplorerBuilder + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -194,51 +122,6 @@ paths: '422': {$ref: '#/components/responses/AuthoringUnprocessable'} '500': {$ref: '#/components/responses/AuthoringInternalError'} '503': {$ref: '#/components/responses/AuthoringUnavailable'} - /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/draft: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/ExplorerId' - put: - operationId: saveExplorerDraft - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SaveDraftRequest' - responses: - '200': - description: Canonical server-owned V2 workspace draft. - content: - application/json: - schema: - $ref: '#/components/schemas/SaveDraftResponse' - '400': {$ref: '#/components/responses/AuthoringBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/AuthoringForbidden'} - '404': {$ref: '#/components/responses/AuthoringNotFound'} - '409': {$ref: '#/components/responses/AuthoringConflict'} - '422': {$ref: '#/components/responses/AuthoringUnprocessable'} - '500': {$ref: '#/components/responses/AuthoringInternalError'} - '503': {$ref: '#/components/responses/AuthoringUnavailable'} - /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/export: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/ExplorerId' - get: - operationId: exportExplorerWorkspace - responses: - '200': - description: Canonical portable V2 workspace. - content: - application/json: - schema: - $ref: '#/components/schemas/Workspace' - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/AuthoringForbidden'} - '404': {$ref: '#/components/responses/AuthoringNotFound'} - '409': {$ref: '#/components/responses/AuthoringConflict'} - '500': {$ref: '#/components/responses/AuthoringInternalError'} /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/preview: parameters: - $ref: '#/components/parameters/Project' @@ -276,6 +159,8 @@ paths: - $ref: '#/components/parameters/ExplorerId' post: operationId: publishExplorer + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -337,40 +222,28 @@ paths: content: application/json: schema: {$ref: '#/components/schemas/RawJSON'} - /api/v1/projects/{project}/resources/{resourceType}: + /api/v1/datasets/{project}/generations/{generation}: parameters: - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/ResourceType' - put: - operationId: uploadProjectResource - summary: Upload one multipart NDJSON resource file. - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - required: [file] - properties: - file: {type: string, format: binary} - auth_resource_path: {type: string} + - $ref: '#/components/parameters/Generation' + get: + operationId: getDatasetGenerationStatus + summary: Read a dataset generation's persisted load state. + description: This preflight uses write authorization because it determines whether a subsequent generation upload may proceed. + parameters: + - $ref: '#/components/parameters/AuthResourcePath' responses: '200': - description: Successful response. + description: The persisted generation state and whether it can be reused. content: application/json: schema: {$ref: '#/components/schemas/RawJSON'} '400': {$ref: '#/components/responses/ServiceBadRequest'} '401': {$ref: '#/components/responses/ServiceUnauthorized'} '403': {$ref: '#/components/responses/ServiceForbidden'} - '415': {$ref: '#/components/responses/ServiceUnsupportedMediaType'} - '422': {$ref: '#/components/responses/ServiceUnprocessable'} + '404': {$ref: '#/components/responses/ServiceNotFound'} '500': {$ref: '#/components/responses/ServiceInternalError'} '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/datasets/{project}/generations/{generation}: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/Generation' post: operationId: createDatasetGeneration summary: Upload a multipart dataset generation. @@ -429,255 +302,6 @@ paths: '409': {$ref: '#/components/responses/ServiceConflict'} '500': {$ref: '#/components/responses/ServiceInternalError'} '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/raw: - put: - operationId: uploadRawNDJSON - summary: Stream raw NDJSON into a generation. - parameters: - - name: project - in: query - schema: {type: string} - - name: generation - in: query - schema: {type: string} - - name: auth_resource_path - in: query - schema: {type: string} - requestBody: - required: true - content: - application/x-ndjson: - schema: {type: string, format: binary} - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '400': {$ref: '#/components/responses/ServiceBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '415': {$ref: '#/components/responses/ServiceUnsupportedMediaType'} - '422': {$ref: '#/components/responses/ServiceUnprocessable'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/generations/{generation}: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/Generation' - post: - operationId: createSnapshot - summary: Create or resume an immutable generation snapshot. - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '400': {$ref: '#/components/responses/ServiceBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '409': {$ref: '#/components/responses/ServiceConflict'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - get: - operationId: getSnapshot - summary: Read generation snapshot status. - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - delete: - operationId: abortSnapshot - summary: Abort a generation snapshot. - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '409': {$ref: '#/components/responses/ServiceConflict'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/generations/{generation}/resources/{resourceType}: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/Generation' - - $ref: '#/components/parameters/ResourceType' - put: - operationId: uploadSnapshotResource - summary: Stream one resource type into an immutable snapshot. - parameters: - - name: X-Content-SHA256 - in: header - schema: {type: string} - requestBody: - required: true - content: - application/octet-stream: - schema: {type: string, format: binary} - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '400': {$ref: '#/components/responses/ServiceBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '409': {$ref: '#/components/responses/ServiceConflict'} - '422': {$ref: '#/components/responses/ServiceUnprocessable'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/generations/{generation}/finalize: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/Generation' - post: - operationId: finalizeSnapshot - summary: Validate and finalize an immutable generation snapshot. - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '400': {$ref: '#/components/responses/ServiceBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '409': {$ref: '#/components/responses/ServiceConflict'} - '422': {$ref: '#/components/responses/ServiceUnprocessable'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/releases: - parameters: - - $ref: '#/components/parameters/Project' - post: - operationId: createRelease - summary: Create a durable project release candidate. - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '400': {$ref: '#/components/responses/ServiceBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '409': {$ref: '#/components/responses/ServiceConflict'} - '422': {$ref: '#/components/responses/ServiceUnprocessable'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/releases/active: - parameters: - - $ref: '#/components/parameters/Project' - get: - operationId: getActiveRelease - summary: Read the active project release. - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/releases/{release}: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/Release' - get: - operationId: getRelease - summary: Read a project release by ID. - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/releases/{release}/activate: - parameters: - - $ref: '#/components/parameters/Project' - - $ref: '#/components/parameters/Release' - post: - operationId: activateRelease - summary: Activate an existing durable release. - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '400': {$ref: '#/components/responses/ServiceBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '409': {$ref: '#/components/responses/ServiceConflict'} - '422': {$ref: '#/components/responses/ServiceUnprocessable'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} - /api/v1/projects/{project}/releases/activate: - parameters: - - $ref: '#/components/parameters/Project' - post: - operationId: activateReleaseCompatibility - summary: Create and activate a release in one legacy operation. - deprecated: true - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '400': {$ref: '#/components/responses/ServiceBadRequest'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/ServiceForbidden'} - '404': {$ref: '#/components/responses/ServiceNotFound'} - '409': {$ref: '#/components/responses/ServiceConflict'} - '422': {$ref: '#/components/responses/ServiceUnprocessable'} - '500': {$ref: '#/components/responses/ServiceInternalError'} - '503': {$ref: '#/components/responses/ServiceUnavailable'} /api/v1/dataframe/recipe-executions/{id}: parameters: - name: id @@ -775,6 +399,8 @@ paths: post: operationId: createExplorer summary: Create an interactive Explorer. + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -842,22 +468,6 @@ paths: '422': {$ref: '#/components/responses/LegacyUnprocessable'} '500': {$ref: '#/components/responses/LegacyInternalError'} '503': {$ref: '#/components/responses/LegacyUnavailable'} - /api/v1/projects/{project}/explorer-config: - parameters: - - $ref: '#/components/parameters/Project' - get: - operationId: getRepositoryExplorerConfig - summary: Read the active repository-owned Explorer config. - responses: - '200': - description: Successful response. - content: - application/json: - schema: {$ref: '#/components/schemas/RawJSON'} - '401': {$ref: '#/components/responses/ServiceUnauthorized'} - '403': {$ref: '#/components/responses/LegacyForbidden'} - '404': {$ref: '#/components/responses/LegacyNotFound'} - '500': {$ref: '#/components/responses/LegacyInternalError'} components: parameters: Project: @@ -870,21 +480,17 @@ components: in: path required: true schema: {type: string, minLength: 1} + AuthResourcePath: + name: auth_resource_path + in: query + required: false + description: Optional Calypr resource scope used to authorize durable Explorer mutations. + schema: {type: string} Generation: name: generation in: path required: true schema: {type: string, minLength: 1} - ResourceType: - name: resourceType - in: path - required: true - schema: {type: string, minLength: 1} - Release: - name: release - in: path - required: true - schema: {type: string, minLength: 1} responses: AuthoringBadRequest: description: The JSON document is malformed, contains unknown fields, or omits required authoring input. @@ -1105,7 +711,7 @@ components: kind: {type: string, enum: [ExplorerAuthoringCapabilities]} operations: type: array - items: {type: string, enum: [builder, compile, suggestions, preview, publish, draft, export, commands, reconcile]} + items: {type: string, enum: [builder, suggestions, preview, publish, commands, reconcile]} previewLimits: type: array items: {type: integer, minimum: 1, maximum: 1000} @@ -1300,13 +906,6 @@ components: type: {type: string, minLength: 1} title: {type: string} order: {type: integer, minimum: 0} - CompileRequest: - type: object - additionalProperties: false - required: [workspace, snapshotToken] - properties: - workspace: {$ref: '#/components/schemas/Workspace'} - snapshotToken: {type: string, minLength: 1} ApplyCommandsRequest: type: object additionalProperties: false @@ -1327,7 +926,7 @@ components: properties: type: type: string - enum: [CREATE_TABLE, DUPLICATE_TABLE, DELETE_TABLE, RENAME_TABLE, REORDER_TABLES, SET_TABLE_ROOT, ADD_ROUTE, REMOVE_ROUTE, ADD_COLUMN, UPDATE_COLUMN, REMOVE_COLUMN] + enum: [CREATE_TABLE, DUPLICATE_TABLE, DELETE_TABLE, RENAME_TABLE, REORDER_TABLES, SET_TABLE_ROOT, ADD_ROUTE, UPDATE_ROUTE_EDGE, REMOVE_ROUTE, ADD_COLUMN, UPDATE_COLUMN, REMOVE_COLUMN] outputId: {type: string, minLength: 1} sourceOutputId: {type: string, minLength: 1} title: {type: string, minLength: 1} @@ -1376,23 +975,6 @@ components: snapshotToken: {type: string, minLength: 1} draftVersion: {type: integer, format: int64, minimum: 1} draftDigest: {type: string, minLength: 1} - SaveDraftRequest: - type: object - additionalProperties: false - required: [workspace, snapshotToken, expectedDraftVersion] - properties: - workspace: {$ref: '#/components/schemas/Workspace'} - snapshotToken: {type: string, minLength: 1} - expectedDraftVersion: {type: integer, format: int64, minimum: 0} - expectedDraftDigest: {type: string} - SaveDraftResponse: - type: object - additionalProperties: false - required: [workspace, draftVersion, draftDigest] - properties: - workspace: {$ref: '#/components/schemas/Workspace'} - draftVersion: {type: integer, format: int64, minimum: 1} - draftDigest: {type: string, minLength: 1} BuilderState: type: object additionalProperties: false @@ -1474,20 +1056,6 @@ components: maxSteps: {type: integer, minimum: 0} allowRepeatedEdges: {type: boolean} allowSelfLoops: {type: boolean} - CandidateSuggestions: - type: object - additionalProperties: false - required: [apiVersion, kind, snapshotToken, candidateId, values, complete, truncated] - properties: - apiVersion: {$ref: '#/components/schemas/APIVersion'} - kind: {type: string, enum: [ExplorerCandidateSuggestions]} - snapshotToken: {type: string, minLength: 1} - candidateId: {type: string, minLength: 1} - values: - type: array - items: {type: string} - complete: {type: boolean} - truncated: {type: boolean} CandidateSearchRequest: type: object additionalProperties: false diff --git a/scripts/acceptance-performance.sh b/scripts/acceptance-performance.sh new file mode 100755 index 00000000..821900c1 --- /dev/null +++ b/scripts/acceptance-performance.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repo_root" + +base_ref=${LOOM_ACCEPTANCE_BASE_REF:-} +if [[ -z "$base_ref" && -n "${GITHUB_BASE_SHA:-}" ]]; then base_ref=$GITHUB_BASE_SHA; fi +if [[ -z "$base_ref" && -n "${GITHUB_EVENT_BEFORE:-}" && ! "$GITHUB_EVENT_BEFORE" =~ ^0+$ ]]; then base_ref=$GITHUB_EVENT_BEFORE; fi +if [[ -z "$base_ref" ]]; then base_ref=HEAD; fi + +comparison_id=${LOOM_ACCEPTANCE_COMPARISON_ID:-$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')} +[[ "$comparison_id" =~ ^[a-f0-9]{16}$ ]] || { echo "invalid comparison ID" >&2; exit 2; } +artifacts=${LOOM_ACCEPTANCE_ARTIFACTS:-"$repo_root/.artifacts/acceptance/performance-$comparison_id"} +cache=${LOOM_ACCEPTANCE_FIXTURE_CACHE:-"$repo_root/.cache/acceptance/fixture"} +mkdir -p "$artifacts" "$cache" + +temporary_root=$(mktemp -d "${TMPDIR:-/tmp}/loom-acceptance-base.XXXXXX") +cleanup() { rm -rf "$temporary_root"; } +trap cleanup EXIT +base_source="$temporary_root/base" +mkdir -p "$base_source" + +base_unavailable() { + local reason=$1 + jq -n --arg status BASE_UNAVAILABLE --arg base_ref "$base_ref" --arg reason "$reason" \ + '{status:$status,base_ref:$base_ref,reason:$reason,observations:[]}' >"$artifacts/performance.json" + if [[ "${LOOM_ACCEPTANCE_ALLOW_BASE_UNAVAILABLE:-false}" == "true" ]]; then + echo "acceptance performance BASE_UNAVAILABLE: $reason" >&2 + return 0 + fi + echo "acceptance performance base unavailable: $reason" >&2 + return 1 +} + +if ! git cat-file -e "${base_ref}^{commit}" 2>/dev/null; then + base_unavailable "base commit is not available in the checkout" + exit $? +fi +if ! git archive "$base_ref" | tar -x -C "$base_source"; then + base_unavailable "base source archive could not be created" + exit $? +fi + +if [[ -n "${LOOM_ACCEPTANCE_GOCACHE:-}" ]]; then + GOCACHE="$LOOM_ACCEPTANCE_GOCACHE" go run ./cmd/loom-acceptance --fixture-only --fixture-cache "$cache" +else + go run ./cmd/loom-acceptance --fixture-only --fixture-cache "$cache" +fi + +run_variant() { + local name=$1 source_root=$2 + LOOM_ACCEPTANCE_ARTIFACTS="$artifacts/$name" \ + LOOM_ACCEPTANCE_FIXTURE_CACHE="$cache" \ + LOOM_ACCEPTANCE_SERVER_SOURCE_ROOT="$source_root" \ + "$repo_root/scripts/acceptance-real.sh" +} + +if [[ ! -f "$base_source/cmd/loom-acceptance/main.go" ]]; then + run_variant current "$repo_root" + LOOM_ACCEPTANCE_ALLOW_BASE_UNAVAILABLE=true base_unavailable "base commit predates the acceptance protocol" + exit 0 +fi + +head_key=$(git rev-parse HEAD 2>/dev/null || printf '0') +last_hex=${head_key: -1} +if (( 16#$last_hex % 2 == 0 )); then + first_order=base-current + if ! run_variant base "$base_source"; then base_unavailable "base server did not complete the current acceptance protocol"; exit $?; fi + run_variant current "$repo_root" +else + first_order=current-base + run_variant current "$repo_root" + if ! run_variant base "$base_source"; then base_unavailable "base server did not complete the current acceptance protocol"; exit $?; fi +fi + +compare=(go run ./cmd/loom-acceptance + --performance-base-report "$artifacts/base/report.json" + --performance-current-report "$artifacts/current/report.json" + --performance-output "$artifacts/performance.json") +if [[ -n "${LOOM_ACCEPTANCE_GOCACHE:-}" ]]; then compare=(env GOCACHE="$LOOM_ACCEPTANCE_GOCACHE" "${compare[@]}"); fi +set +e +"${compare[@]}" +compare_status=$? +set -e +if (( compare_status != 3 )); then exit "$compare_status"; fi + +if [[ "$first_order" == base-current ]]; then + run_variant current-repeat "$repo_root" + if ! run_variant base-repeat "$base_source"; then base_unavailable "repeat base run failed"; exit $?; fi +else + if ! run_variant base-repeat "$base_source"; then base_unavailable "repeat base run failed"; exit $?; fi + run_variant current-repeat "$repo_root" +fi + +final_compare=(go run ./cmd/loom-acceptance + --performance-base-report "$artifacts/base/report.json" + --performance-current-report "$artifacts/current/report.json" + --performance-repeat-base-report "$artifacts/base-repeat/report.json" + --performance-repeat-current-report "$artifacts/current-repeat/report.json" + --performance-output "$artifacts/performance.json") +if [[ -n "${LOOM_ACCEPTANCE_GOCACHE:-}" ]]; then final_compare=(env GOCACHE="$LOOM_ACCEPTANCE_GOCACHE" "${final_compare[@]}"); fi +"${final_compare[@]}" diff --git a/scripts/acceptance-real.sh b/scripts/acceptance-real.sh new file mode 100755 index 00000000..9cbd9d03 --- /dev/null +++ b/scripts/acceptance-real.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repo_root" + +run_id=${LOOM_ACCEPTANCE_RUN_ID:-$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')} +if [[ ! "$run_id" =~ ^[a-f0-9]{16}$ ]]; then echo "invalid run ID" >&2; exit 2; fi +arango_database="loom_acceptance_${run_id}" +clickhouse_database="loom_acceptance_${run_id}" +if [[ ! "$arango_database" =~ ^loom_acceptance_[a-f0-9]{16}$ || ! "$clickhouse_database" =~ ^loom_acceptance_[a-f0-9]{16}$ ]]; then echo "unsafe generated database name" >&2; exit 2; fi + +artifacts=${LOOM_ACCEPTANCE_ARTIFACTS:-"$repo_root/.artifacts/acceptance/$run_id"} +cache=${LOOM_ACCEPTANCE_FIXTURE_CACHE:-"$repo_root/.cache/acceptance/fixture"} +server_source_root=${LOOM_ACCEPTANCE_SERVER_SOURCE_ROOT:-$repo_root} +[[ -f "$server_source_root/go.mod" ]] || { echo "server source root has no go.mod: $server_source_root" >&2; exit 2; } +mkdir -p "$artifacts" "$cache" +processes=() +server_pid="" +status=0 +arango_url="" +clickhouse_http_url="" +clickhouse_native_url="" +clickhouse_username="" +clickhouse_password="" +forward_port="" +build_dir="" +server_config="" +clickhouse_curl_config="" + +cleanup() { + local previous=$? cleanup_status=0; status=$previous + set +e + if [[ -n "$server_pid" ]]; then kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true; fi + if [[ "$arango_database" =~ ^loom_acceptance_[a-f0-9]{16}$ ]]; then + [[ -n "$arango_url" ]] && curl -fsS -X DELETE "${arango_url}/_api/database/${arango_database}" -o "$artifacts/arango-drop.json" 2>"$artifacts/arango-drop.err" || cleanup_status=1 + if [[ -n "$arango_url" ]]; then + curl -fsS "${arango_url}/_api/database" -o "$artifacts/arango-databases-after-drop.json" 2>/dev/null || cleanup_status=1 + jq -e --arg database "$arango_database" '.result | index($database) == null' "$artifacts/arango-databases-after-drop.json" >/dev/null 2>&1 || cleanup_status=1 + fi + else cleanup_status=1; fi + if [[ "$clickhouse_database" =~ ^loom_acceptance_[a-f0-9]{16}$ ]]; then + if [[ -n "$clickhouse_http_url" ]]; then + clickhouse_http_request "DROP DATABASE IF EXISTS \`$clickhouse_database\`" >"$artifacts/clickhouse-drop.txt" 2>"$artifacts/clickhouse-drop.err" || cleanup_status=1 + clickhouse_http_request "SELECT name FROM system.databases WHERE name = '$clickhouse_database'" >"$artifacts/clickhouse-check.txt" 2>/dev/null || cleanup_status=1 + [[ ! -s "$artifacts/clickhouse-check.txt" ]] || cleanup_status=1 + else + cleanup_status=1 + fi + else cleanup_status=1; fi + # Keep the database forwards alive until both drops and absence probes finish. + for pid in "${processes[@]+${processes[@]}}"; do kill "$pid" 2>/dev/null || true; done + for pid in "${processes[@]+${processes[@]}}"; do wait "$pid" 2>/dev/null || true; done + if [[ -n "$server_config" && -f "$server_config" ]]; then rm -f -- "$server_config"; fi + if [[ -n "$clickhouse_curl_config" && -f "$clickhouse_curl_config" ]]; then rm -f -- "$clickhouse_curl_config"; fi + if [[ -n "$build_dir" && -d "$build_dir" ]]; then rm -rf -- "$build_dir"; fi + printf '{"status":"%s","cleanup_status":%d}\n' "$status" "$cleanup_status" >"$artifacts/cleanup.json" + if (( status == 0 && cleanup_status != 0 )); then status=$cleanup_status; fi + exit "$status" +} +trap cleanup EXIT + +kubectl_bin=${KUBECTL:-kubectl} +namespace_args=() +if [[ -n "${KUBE_NAMESPACE:-}" ]]; then namespace_args=(-n "$KUBE_NAMESPACE"); fi + +resolve_service() { + local fallback=$1 selector=${2:-} value + value="" + if [[ -n "$selector" ]]; then value=$($kubectl_bin "${namespace_args[@]+${namespace_args[@]}}" get service -l "$selector" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true); fi + if [[ -z "$value" ]]; then value=$($kubectl_bin "${namespace_args[@]+${namespace_args[@]}}" get service "$fallback" -o jsonpath='{.metadata.name}' 2>/dev/null || true); fi + [[ -n "$value" ]] || { echo "could not resolve Kubernetes service $fallback" >&2; return 1; } + printf '%s' "$value" +} + +start_forward() { + local name=$1 remote=$2 label=${3:-$1} + local log="$artifacts/$label-port-forward.log" line + "$kubectl_bin" "${namespace_args[@]+${namespace_args[@]}}" port-forward --address 127.0.0.1 "service/$name" "0:$remote" >"$log" 2>&1 & + local pid=$!; processes+=("$pid") + for _ in $(seq 1 100); do + line=$(grep -m1 'Forwarding from 127.0.0.1:' "$log" 2>/dev/null || true) + if [[ -n "$line" ]]; then forward_port=$(sed -E 's/.*127\.0\.0\.1:([0-9]+).*/\1/' <<<"$line"); return 0; fi + kill -0 "$pid" 2>/dev/null || { echo "port-forward $name failed; see $log" >&2; return 1; } + sleep 0.1 + done + echo "timed out starting port-forward $name; see $log" >&2; return 1 +} + +clickhouse_http_request() { + local query=$1 + curl -fsS --config "$clickhouse_curl_config" --data-binary "$query" "${clickhouse_http_url}/" +} + +load_local_clickhouse_credentials() { + if [[ -n "$clickhouse_username" && -n "$clickhouse_password" ]]; then return 0; fi + command -v yq >/dev/null 2>&1 || { + echo "local acceptance needs yq to discover missing ClickHouse credentials from Kubernetes secret loom-config (config.yaml); install yq or set LOOM_ACCEPTANCE_CLICKHOUSE_USERNAME and LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD" >&2 + return 1 + } + + local secret_name=${LOOM_ACCEPTANCE_KUBE_CONFIG_SECRET:-loom-config} + local secret_data secret_config discovered_username discovered_password + secret_data=$($kubectl_bin "${namespace_args[@]+${namespace_args[@]}}" get secret "$secret_name" -o jsonpath='{.data.config\.yaml}' 2>/dev/null) || { + echo "could not read Kubernetes secret $secret_name key config.yaml for ClickHouse credentials" >&2 + return 1 + } + [[ -n "$secret_data" ]] || { + echo "Kubernetes secret $secret_name has no config.yaml key for ClickHouse credentials" >&2 + return 1 + } + if base64 --decode /dev/null 2>&1; then + secret_config=$(printf '%s' "$secret_data" | base64 --decode) + elif base64 -d /dev/null 2>&1; then + secret_config=$(printf '%s' "$secret_data" | base64 -d) + elif base64 -D /dev/null 2>&1; then + secret_config=$(printf '%s' "$secret_data" | base64 -D) + else + echo "could not find a base64 decoder for Kubernetes secret $secret_name key config.yaml" >&2 + return 1 + fi + discovered_username=$(printf '%s\n' "$secret_config" | yq -r '.server.clickhouse.username // ""') + discovered_password=$(printf '%s\n' "$secret_config" | yq -r '.server.clickhouse.password // ""') + [[ -n "$clickhouse_username" ]] || clickhouse_username=$discovered_username + [[ -n "$clickhouse_password" ]] || clickhouse_password=$discovered_password + if [[ -z "$clickhouse_username" || -z "$clickhouse_password" ]]; then + echo "Kubernetes secret $secret_name config.yaml did not provide both server.clickhouse.username and server.clickhouse.password; set explicit acceptance credential overrides" >&2 + return 1 + fi +} + +if [[ "${LOOM_ACCEPTANCE_MODE:-local}" == "github" ]]; then + arango_url=${LOOM_ACCEPTANCE_ARANGO_URL:-http://127.0.0.1:8529} + clickhouse_http_url=${LOOM_ACCEPTANCE_CLICKHOUSE_HTTP_URL:-http://127.0.0.1:8123} + clickhouse_native_url=${LOOM_ACCEPTANCE_CLICKHOUSE_NATIVE_URL:-clickhouse://127.0.0.1:9000} + clickhouse_username=${LOOM_ACCEPTANCE_CLICKHOUSE_USERNAME:-loom_ci} + clickhouse_password=${LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD:-loom_ci_password} +else + arango_service=${LOOM_ACCEPTANCE_ARANGO_SERVICE:-$(resolve_service arangodb 'app=arangodb')} + clickhouse_service=${LOOM_ACCEPTANCE_CLICKHOUSE_SERVICE:-$(resolve_service clickhouse 'app=clickhouse')} + start_forward "$arango_service" 8529 + arango_port=$forward_port + start_forward "$clickhouse_service" 8123 clickhouse-http + clickhouse_http_port=$forward_port + start_forward "$clickhouse_service" 9000 clickhouse-native + clickhouse_native_port=$forward_port + arango_url=${LOOM_ACCEPTANCE_ARANGO_URL:-"http://127.0.0.1:$arango_port"} + clickhouse_http_url=${LOOM_ACCEPTANCE_CLICKHOUSE_HTTP_URL:-"http://127.0.0.1:$clickhouse_http_port"} + clickhouse_native_url=${LOOM_ACCEPTANCE_CLICKHOUSE_NATIVE_URL:-"clickhouse://127.0.0.1:$clickhouse_native_port"} + clickhouse_username=${LOOM_ACCEPTANCE_CLICKHOUSE_USERNAME:-} + clickhouse_password=${LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD:-} + load_local_clickhouse_credentials || exit 1 +fi + +clickhouse_curl_config=$(mktemp "${TMPDIR:-/tmp}/loom-acceptance-curl.XXXXXX.conf") +chmod 600 "$clickhouse_curl_config" +LOOM_ACCEPTANCE_CURL_USER="$clickhouse_username:$clickhouse_password" \ + jq -nr '"user = " + ($ENV.LOOM_ACCEPTANCE_CURL_USER | @json)' >"$clickhouse_curl_config" + +listen_port=${LOOM_ACCEPTANCE_LOOM_PORT:-$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')} +loom_url="http://127.0.0.1:$listen_port" +build_dir=$(mktemp -d "${TMPDIR:-/tmp}/loom-acceptance-bin.XXXXXX") +go_build() { + if [[ -n "${LOOM_ACCEPTANCE_GOCACHE:-}" ]]; then + GOCACHE="$LOOM_ACCEPTANCE_GOCACHE" go build "$@" + else + go build "$@" + fi +} +if [[ -n "${LOOM_ACCEPTANCE_GOCACHE:-}" ]]; then + GOCACHE="$LOOM_ACCEPTANCE_GOCACHE" go -C "$server_source_root" build -o "$build_dir/arango-fhir-server" ./cmd/arango-fhir-server +else + go -C "$server_source_root" build -o "$build_dir/arango-fhir-server" ./cmd/arango-fhir-server +fi +go_build -o "$build_dir/loom-acceptance" ./cmd/loom-acceptance + +server_config=$(mktemp "${TMPDIR:-/tmp}/loom-acceptance-server-config.XXXXXX.json") +chmod 600 "$server_config" +LOOM_ACCEPTANCE_CONFIG_CLICKHOUSE_PASSWORD="$clickhouse_password" jq -n \ + --arg listen "127.0.0.1:$listen_port" \ + --arg arango_url "$arango_url" \ + --arg arango_database "$arango_database" \ + --arg clickhouse_url "$clickhouse_native_url" \ + --arg clickhouse_database "$clickhouse_database" \ + --arg clickhouse_username "$clickhouse_username" \ + --arg schema "$server_source_root/schemas/graph-fhir.json" \ + --arg recipe "$repo_root/testdata/acceptance/ncpi-tcga-brca/recipe.json" \ + '{server: {listen: $listen, url: $arango_url, database: $arango_database, schema: $schema, clickhouse: {enabled: true, url: $clickhouse_url, database: $clickhouse_database, username: $clickhouse_username, password: $ENV.LOOM_ACCEPTANCE_CONFIG_CLICKHOUSE_PASSWORD}, dataframer: {recipe: $recipe}}, auth: {allow_unauthenticated: true}}' \ + >"$server_config" +"$build_dir/arango-fhir-server" --config "$server_config" --no-auth >"$artifacts/loom.log" 2>&1 & +server_pid=$! +for _ in $(seq 1 180); do curl -fsS "$loom_url/readyz" >"$artifacts/readyz.json" 2>/dev/null && break; kill -0 "$server_pid" 2>/dev/null || { echo "Loom stopped; see $artifacts/loom.log" >&2; exit 1; }; sleep 1; done +curl -fsS "$loom_url/readyz" >"$artifacts/readyz-final.json" +rm -f -- "$server_config" +server_config="" + +LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD="$clickhouse_password" "$build_dir/loom-acceptance" --run-id "$run_id" --loom-url "$loom_url" --arango-url "$arango_url" --clickhouse-url "$clickhouse_native_url" --clickhouse-username "$clickhouse_username" --fixture-lock testdata/acceptance/ncpi-tcga-brca/fixture.lock.json --fixture-cache "$cache" --workspace testdata/acceptance/ncpi-tcga-brca/workspace.json --oracle testdata/acceptance/ncpi-tcga-brca/oracle.json --artifacts "$artifacts" diff --git a/scripts/check_dataframe_package_boundaries.sh b/scripts/check_dataframe_package_boundaries.sh index f319143b..69062ac7 100755 --- a/scripts/check_dataframe_package_boundaries.sh +++ b/scripts/check_dataframe_package_boundaries.sh @@ -16,7 +16,7 @@ fail_if_matches() { } fail_if_matches "spec imports runtime/compiler implementation" \ - 'internal/dataframe/(runtime|compiler|semantic|materialization|template|errors)' \ + 'internal/dataframe/(execution|compiler|semantic|materialization|template|errors)' \ "$root/internal/dataframe/spec" fail_if_matches "semantic imports physical compiler implementation" \ @@ -24,23 +24,23 @@ fail_if_matches "semantic imports physical compiler implementation" \ "$root/internal/dataframe/semantic" fail_if_matches "ir imports lowering, optimization, rendering, or runtime" \ - 'internal/dataframe/compiler/(lower|optimize|render)|internal/dataframe/runtime|internal/store/arango' \ + 'internal/dataframe/compiler/(lower|optimize|render)|internal/dataframe/execution|internal/store/arango' \ "$root/internal/dataframe/compiler/ir" fail_if_matches "optimizer imports renderer, lowerer, runtime, or schema routes" \ - 'internal/dataframe/compiler/(lower|render)|internal/dataframe/runtime|ResolveStorageRoute|fhirschema' \ + 'internal/dataframe/compiler/(lower|render)|internal/dataframe/execution|ResolveStorageRoute|fhirschema' \ "$root/internal/dataframe/compiler/optimize" fail_if_matches "renderer imports lowerer, optimizer, runtime, or Arango" \ - 'internal/dataframe/compiler/(lower|optimize)|internal/dataframe/runtime|internal/store/arango|ResolveStorageRoute' \ + 'internal/dataframe/compiler/(lower|optimize)|internal/dataframe/execution|internal/store/arango|ResolveStorageRoute' \ "$root/internal/dataframe/compiler/render/aql" fail_if_matches "stored recipe domain imports execution or storage layers" \ - 'internal/dataframe/(compiler|runtime|materialization|publication|store)' \ + 'internal/dataframe/(compiler|execution|materialization|publication|store)' \ "$root"/internal/dataframe/recipe/*.go -fail_if_matches "publication targets import dataframe runtime orchestration" \ - 'internal/dataframe/runtime|internal/dataframe/published|internal/dataframe/materialization' \ +fail_if_matches "publication targets import dataframe execution orchestration" \ + 'internal/dataframe/execution|internal/dataframe/published|internal/dataframe/materialization' \ "$root"/internal/dataframe/publication echo "dataframe package boundaries: ok" diff --git a/scripts/demo-browser-smoke.sh b/scripts/demo-browser-smoke.sh new file mode 100755 index 00000000..5a92b53a --- /dev/null +++ b/scripts/demo-browser-smoke.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ui_host=${LOOM_DEMO_UI_HOST:-127.0.0.1} +[[ $ui_host == *:* ]] && ui_host="[$ui_host]" +ui_url=${LOOM_DEMO_UI_URL:-http://$ui_host:${LOOM_DEMO_UI_PORT:-3080}} +project=${LOOM_DEMO_PROJECT:-NCPI_ACCEPTANCE} +output_title=${LOOM_DEMO_OUTPUT_TITLE:-TCGA-BRCA patient cohort} +expected_column=${LOOM_DEMO_EXPECTED_COLUMN_LABEL:-Patient ID} +expected_cell=${LOOM_DEMO_EXPECTED_CELL:-TCGA-} +expected_resources=${LOOM_DEMO_EXPECTED_RESOURCES:-Patient Condition Specimen Observation ResearchStudy} +browser_url=${LOOM_DEMO_BROWSER_URL:-$ui_url/?project=$project&explorer=default} + +find_chrome() { + if [[ -n ${CHROME_BIN:-} ]]; then + printf '%s\n' "$CHROME_BIN" + return + fi + + local candidate + for candidate in \ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + google-chrome \ + chromium \ + chromium-browser; do + if [[ -x $candidate ]] || command -v "$candidate" >/dev/null 2>&1; then + printf '%s\n' "$candidate" + return + fi + done + + echo "Chrome or Chromium is required. Set CHROME_BIN to its executable." >&2 + return 1 +} + +chrome=$(find_chrome) +if [[ -n ${LOOM_BROWSER_ARTIFACT_DIR:-} ]]; then + work_dir=$LOOM_BROWSER_ARTIFACT_DIR + mkdir -p "$work_dir" +else + work_dir=$(mktemp -d "${TMPDIR:-/tmp}/loom-browser-smoke.XXXXXX") + trap 'rm -rf -- "$work_dir"' EXIT +fi + +dump_page() { + local mode=$1 + local output=$2 + local ready_text=$3 + local profile="$work_dir/profile-$mode" + local browser_pid + + "$chrome" \ + --headless=new \ + --no-sandbox \ + --disable-gpu \ + --disable-background-networking \ + --disable-component-update \ + --no-first-run \ + --no-default-browser-check \ + --user-data-dir="$profile" \ + --virtual-time-budget=5000 \ + --dump-dom \ + "$browser_url&mode=$mode" >"$output" 2>"$work_dir/$mode.stderr" & + browser_pid=$! + + for _ in $(seq 1 60); do + if grep -Fq "$ready_text" "$output" || \ + grep -Fq 'class="loom-error"' "$output" || \ + grep -Fq 'role="alert"' "$output"; then + break + fi + if ! kill -0 "$browser_pid" 2>/dev/null; then + wait "$browser_pid" + return + fi + sleep 0.5 + done + + if kill -0 "$browser_pid" 2>/dev/null; then + kill "$browser_pid" + wait "$browser_pid" 2>/dev/null || true + fi +} + +assert_contains() { + local file=$1 + local text=$2 + if ! grep -Fq "$text" "$file"; then + echo "Expected $text in $(basename "$file")" >&2 + return 1 + fi +} + +assert_no_ui_error() { + local file=$1 + if grep -Fq 'class="loom-error"' "$file" || grep -Fq 'role="alert"' "$file"; then + echo "Loom rendered an error in $(basename "$file")" >&2 + return 1 + fi +} + +assert_served_graph_styles() { + local index css_href css_url css + index=$(curl --fail --silent --show-error --location "$ui_url/") + css_href=$(printf '%s' "$index" | grep -oE 'href="[^"]+\.css"' | head -n 1 | sed -E 's/^href="//; s/"$//') + if [[ -z $css_href ]]; then + echo "Standalone UI did not serve a stylesheet" >&2 + return 1 + fi + if [[ $css_href == /* ]]; then + css_url="${ui_url%/}$css_href" + else + css_url="${ui_url%/}/$css_href" + fi + css=$(curl --fail --silent --show-error --location "$css_url") + for selector in '.react-flow{' '.react-flow__renderer{' '.react-flow__viewport{' '.react-flow__node{' ; do + if ! grep -Fq "$selector" <<<"$css"; then + echo "Expected React Flow layout selector $selector in served stylesheet $css_href" >&2 + return 1 + fi + done +} + +viewer_dom="$work_dir/viewer.html" +builder_dom="$work_dir/builder.html" +assert_served_graph_styles +dump_page viewer "$viewer_dom" "$expected_cell" +assert_no_ui_error "$viewer_dom" +assert_contains "$viewer_dom" "$output_title" +assert_contains "$viewer_dom" "$expected_column" +assert_contains "$viewer_dom" "$expected_cell" + +dump_page builder "$builder_dom" "Publish" +assert_no_ui_error "$builder_dom" +assert_contains "$builder_dom" "FHIR Explorer Studio" +assert_contains "$builder_dom" "Preview" +assert_contains "$builder_dom" "Publish" +for resource in $expected_resources; do + assert_contains "$builder_dom" "$resource" +done + +echo "Standalone Viewer and Builder rendered the seeded Explorer in headless Chrome" diff --git a/scripts/demo-down.sh b/scripts/demo-down.sh new file mode 100755 index 00000000..5c855c65 --- /dev/null +++ b/scripts/demo-down.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repo_root" + +compose_project=${LOOM_DEMO_COMPOSE_PROJECT:-loom-demo} +[[ $compose_project =~ ^[a-z0-9][a-z0-9_-]*$ ]] || { echo "invalid LOOM_DEMO_COMPOSE_PROJECT: $compose_project" >&2; exit 2; } + +docker compose --project-name "$compose_project" down "$@" diff --git a/scripts/demo-smoke.sh b/scripts/demo-smoke.sh new file mode 100755 index 00000000..6b1687dc --- /dev/null +++ b/scripts/demo-smoke.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +api_host=${LOOM_DEMO_API_HOST:-127.0.0.1} +ui_host=${LOOM_DEMO_UI_HOST:-127.0.0.1} +[[ $api_host == *:* ]] && api_host="[$api_host]" +[[ $ui_host == *:* ]] && ui_host="[$ui_host]" +api_url=${LOOM_DEMO_API_URL:-http://$api_host:${LOOM_DEMO_API_PORT:-8080}} +ui_url=${LOOM_DEMO_UI_URL:-http://$ui_host:${LOOM_DEMO_UI_PORT:-3080}} +compose_project=${LOOM_DEMO_COMPOSE_PROJECT:-loom-demo} +project=${LOOM_DEMO_PROJECT:-NCPI_ACCEPTANCE} +generation=${LOOM_DEMO_GENERATION:-tcga-brca-locked} +management=${LOOM_DEMO_MANAGEMENT:-REPOSITORY} +output_id=${LOOM_DEMO_OUTPUT_ID:-tcga_brca_cohort} +output_title=${LOOM_DEMO_OUTPUT_TITLE:-TCGA-BRCA patient cohort} + +[[ $compose_project =~ ^[a-z0-9][a-z0-9_-]*$ ]] || { echo "invalid LOOM_DEMO_COMPOSE_PROJECT: $compose_project" >&2; exit 2; } + +curl -fsS "$api_url/readyz" >/dev/null +docker compose --project-name "$compose_project" exec -T loom-api \ + /app/loom-acceptance \ + --smoke-only \ + --project "$project" \ + --smoke-management "$management" \ + --generation "$generation" \ + --smoke-output-id "$output_id" \ + --smoke-output-title "$output_title" \ + --oracle /app/testdata/oracle.json +curl -fsS "$ui_url/" | grep -qi '' + +echo "Loom API, expected seeded Explorer, and standalone UI are healthy" diff --git a/scripts/demo-up.sh b/scripts/demo-up.sh new file mode 100755 index 00000000..ac08587f --- /dev/null +++ b/scripts/demo-up.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repo_root" + +command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 1; } +docker compose version >/dev/null 2>&1 || { echo "docker compose v2 is required" >&2; exit 1; } + +compose_project=${LOOM_DEMO_COMPOSE_PROJECT:-loom-demo} +api_host=${LOOM_DEMO_API_HOST:-127.0.0.1} +api_port=${LOOM_DEMO_API_PORT:-8080} +ui_host=${LOOM_DEMO_UI_HOST:-127.0.0.1} +ui_port=${LOOM_DEMO_UI_PORT:-3080} + +[[ $compose_project =~ ^[a-z0-9][a-z0-9_-]*$ ]] || { echo "invalid LOOM_DEMO_COMPOSE_PROJECT: $compose_project" >&2; exit 2; } +[[ $api_port =~ ^[0-9]+$ ]] && ((api_port >= 1 && api_port <= 65535)) || { echo "invalid LOOM_DEMO_API_PORT: $api_port" >&2; exit 2; } +[[ $ui_port =~ ^[0-9]+$ ]] && ((ui_port >= 1 && ui_port <= 65535)) || { echo "invalid LOOM_DEMO_UI_PORT: $ui_port" >&2; exit 2; } + +export LOOM_COMPOSE_PROJECT_NAME=$compose_project +export LOOM_API_HOST=$api_host +export LOOM_API_PORT=$api_port +export LOOM_UI_HOST=$ui_host +export LOOM_UI_PORT=$ui_port + +api_url_host=$api_host +ui_url_host=$ui_host +[[ $api_url_host == *:* ]] && api_url_host="[$api_url_host]" +[[ $ui_url_host == *:* ]] && ui_url_host="[$ui_url_host]" +api_url=${LOOM_DEMO_API_URL:-http://$api_url_host:$api_port} +ui_url=${LOOM_DEMO_UI_URL:-http://$ui_url_host:$ui_port} +compose=(docker compose --project-name "$compose_project") + +"${compose[@]}" up --build -d arangodb clickhouse loom-api + +for _ in $(seq 1 180); do + if curl -fsS "$api_url/readyz" >/dev/null 2>&1; then + break + fi + sleep 1 +done +curl -fsS "$api_url/readyz" >/dev/null + +"${compose[@]}" run --rm --no-deps demo-seed +"${compose[@]}" up --build -d loom-ui + +for _ in $(seq 1 120); do + if curl -fsS "$ui_url/" >/dev/null 2>&1; then + break + fi + sleep 1 +done +curl -fsS "$ui_url/" >/dev/null + +echo "Loom demo is ready at $ui_url" diff --git a/scripts/loom-repo-up.sh b/scripts/loom-repo-up.sh new file mode 100755 index 00000000..490f72dc --- /dev/null +++ b/scripts/loom-repo-up.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +loom_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +repository=$PWD + +arguments=() +while (($#)); do + case "$1" in + --repository) + (($# >= 2)) || { echo "--repository requires a path" >&2; exit 2; } + repository=$2 + shift 2 + ;; + *) + arguments+=("$1") + shift + ;; + esac +done + +cd "$loom_root" +set +u +GOTOOLCHAIN=auto go run ./cmd/loom-repository launch \ + --loom-root "$loom_root" \ + --repository "$repository" \ + "${arguments[@]}" diff --git a/scripts/package_audit.py b/scripts/package_audit.py new file mode 100644 index 00000000..a6961e2f --- /dev/null +++ b/scripts/package_audit.py @@ -0,0 +1,567 @@ +#!/usr/bin/env python3 +"""Generate Loom's repository-wide Go package audit lookup tables.""" + +from __future__ import annotations + +import csv +import re +from collections import defaultdict +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MODULE = "github.com/calypr/loom" +CSV_PATH = ROOT / "docs" / "PACKAGE_AUDIT.csv" +DECISIONS_CSV_PATH = ROOT / "docs" / "PACKAGE_AUDIT_DECISIONS.csv" +MARKDOWN_PATH = ROOT / "docs" / "PACKAGE_AUDIT.md" + +EXCLUDED_PARTS = { + ".git", + ".codex", + ".agents", + "graphify-out", + "META_SMALL", + "data.broken.20260706-full-load", +} + +# These sentences are deliberately curated. The generator refuses to emit a row +# without one so a directory listing cannot masquerade as a semantic inventory. +RESPONSIBILITIES = { + ".": "Start the default Loom server process through the internal server composition root.", + "cmd/arango-fhir-proto": "Provide operator commands for FHIR loading, catalog diagnostics, generation repair, and activation.", + "cmd/arango-fhir-server": "Start the explicit Loom HTTP server binary.", + "cmd/dataframe-profile": "Run and compare developer-facing dataframe profiling workloads.", + "cmd/dataframe-query": "Execute a checked-in GraphQL dataframe request and report response timing and diagnostics.", + "cmd/explorer-config-v2-convert": "Convert legacy Explorer configuration input into the V2 authoring representation offline.", + "cmd/generate": "Generate FHIR models, schema metadata, validation, extraction, and GraphQL artifacts from source schemas.", + "cmd/gqlgenfix": "Post-process gqlgen output to enforce Loom-specific generated-code invariants.", + "cmd/loom-acceptance": "Run the locked real-data acceptance scenario and emit machine-readable verification artifacts.", + "conformance/compiler": "Load and exercise the canonical dataframe compiler oracle corpus for conformance and performance checks.", + "generated/fhir": "Expose generated FHIR resource models and their extraction, validation, and GraphQL helpers.", + "generated/fhirschema": "Expose generated raw FHIR schema metadata tables.", + "generated/graphql/graph/executor": "Provide gqlgen-generated GraphQL executable schema and execution plumbing.", + "generated/graphql/graph/model": "Provide gqlgen-generated GraphQL transport model types.", + "generated/loomapi": "Provide OpenAPI-generated HTTP request, response, and strict-server contracts.", + "internal/api/bulk/load": "Adapt multipart HTTP generation uploads and activation requests to ingest and dataset services.", + "internal/api/graphql/graph": "Own GraphQL HTTP handler construction, error presentation, and generated executor binding.", + "internal/api/graphql/graph/dataframe": "Adapt published ClickHouse dataframe discovery, row, aggregate, and export operations to GraphQL.", + "internal/api/graphql/graph/query": "Resolve and authorize Arango-backed FHIR dataframe and graph query requests for GraphQL.", + "internal/api/graphql/graph/resolver": "Implement gqlgen field resolvers and map GraphQL values to query and lifecycle services.", + "internal/api/http": "Own shared Fiber server setup, middleware, request IDs, errors, health, and readiness behavior.", + "internal/acceptance": "Fetch and cache the locked FHIR fixture, drive the real-data acceptance scenario, and verify its outputs and performance evidence.", + "internal/authscope": "Represent request principals and resolve authorized project, generation, and resource scopes.", + "internal/catalog": "Own persistence-neutral observed FHIR field, pivot, relationship, and capability facts.", + "internal/catalog/arango": "Persist and retrieve catalog facts in ArangoDB.", + "internal/dataframe/compiler": "Orchestrate semantic planning, physical lowering, optimization, rendering, and compiled-query assembly.", + "internal/dataframe/compiler/capability": "Probe which dataframe language capabilities the compiler can prove and support.", + "internal/dataframe/compiler/ir": "Define typed physical operations, values, scope proofs, policies, and validation.", + "internal/dataframe/compiler/lower": "Lower logical FHIR dataframe plans into storage-aware physical IR.", + "internal/dataframe/compiler/optimize": "Apply explainable semantics-preserving rewrites to validated physical IR.", + "internal/dataframe/compiler/render/aql": "Render validated physical IR as deterministic parameterized AQL.", + "internal/dataframe/errors": "Define and normalize stable structured dataframe errors for transport presentation.", + "internal/dataframe/execution": "Prepare, authorize, execute, explain, and post-process compiled dataframe requests.", + "internal/dataframe/expression": "Define and validate the typed backend-neutral expression AST used by recipes.", + "internal/dataframe/publication": "Own backend-neutral dataframe publication contracts, bundles, and publication orchestration.", + "internal/dataframe/publication/arango": "Persist dataframe publication registry and bundle metadata in ArangoDB.", + "internal/dataframe/publication/clickhouse": "Materialize and inspect published dataframe bundles in ClickHouse.", + "internal/dataframe/published": "Read, page, aggregate, and export authorized published ClickHouse dataframes.", + "internal/dataframe/recipe": "Define, canonicalize, expand, encode, and validate persistence-neutral recipe documents.", + "internal/dataframe/recipe/exec": "Define durable recipe-version storage contracts and immutable registration behavior.", + "internal/dataframe/recipe/exec/arango": "Persist recipe versions and execution-use records in ArangoDB.", + "internal/dataframe/recipe/schema": "Resolve catalog-backed recipe declarations into concrete validated schemas.", + "internal/dataframe/semantic": "Convert validated dataframe requests and recipes into backend-neutral logical FHIR plans.", + "internal/dataframe/spec": "Define the backend-independent dataframe request, filter, grain, relationship, and selector language.", + "internal/dataset": "Define immutable dataset generation manifests, releases, active pointers, and lifecycle validation.", + "internal/dataset/arango": "Persist immutable dataset generation lifecycle state in ArangoDB.", + "internal/explorer": "Define Explorer aggregate identities, configs, revisions, receipts, output contracts, stores, and core service operations.", + "internal/explorer/arango": "Persist Explorer drafts, revisions, receipts, publications, capabilities, and repository state in ArangoDB.", + "internal/explorer/authoringv2": "Define, validate, canonicalize, and mutate the V2 Explorer Builder authoring workspace.", + "internal/explorer/capability": "Build the immutable compiler-backed capability contract exposed to Explorer Builder.", + "internal/explorer/compilation": "Translate validated Explorer authoring intent into native recipe and compilation artifacts.", + "internal/explorer/lifecycle": "Coordinate transport-neutral Explorer authoring, preview, publication, query, and policy operations.", + "internal/fhir/schema": "Add handwritten selector, traversal, pivot, and compiler semantics to generated FHIR metadata.", + "internal/ingest": "Preflight and load FHIR NDJSON into generation-scoped Arango vertices, edges, and catalog facts.", + "internal/projectid": "Define canonical and legacy-compatible public project identity normalization.", + "internal/server": "Compose storage, domain services, generated OpenAPI handlers, GraphQL adapters, and HTTP routes.", + "internal/store/arango": "Provide the low-level typed ArangoDB client, query streaming, profiling, and Explain assessment boundary.", + "internal/store/clickhouse": "Provide the low-level typed ClickHouse connection and database-management boundary.", +} + +# Completed decisions are deliberately curated rather than inferred from the +# current source tree. Removed-package records preserve the evidence needed to +# understand why a source disappeared; keep records pin the reviewed boundary +# to a live package. +COMPLETED_DECISIONS = ( + { + "source": "internal/api/graphql", + "disposition": "merged", + "destination": "internal/api/graphql/graph", + "rationale": "one production importer and error presentation is transport policy owned by graph", + "contracts": "public error codes/messages/redaction/request IDs/cause/path/location", + "verification": "focused graph+server tests; full repository verification; extension-map isolation regression", + "status": "complete", + }, + { + "source": "internal/api/bulk/load", + "disposition": "keep", + "destination": "internal/api/bulk/load", + "rationale": "owns multipart staging, authorization, ingest error translation, generation reuse, and release activation apart from generated OpenAPI response mapping", + "contracts": "multipart fields; JSON result fields; auth scope; stable error codes; activation safety", + "verification": "focused load+server tests; full repository verification", + "status": "complete", + }, + { + "source": "internal/api/graphql/graph", + "disposition": "keep", + "destination": "internal/api/graphql/graph", + "rationale": "owns gqlgen handler construction, GraphQL error presentation, and response-status adaptation apart from server composition and generated execution", + "contracts": "GraphQL error shape; request IDs; partial-data HTTP 200; failure status lifting; playground and sandbox handlers", + "verification": "focused graph+server tests; full repository verification; extension-map isolation regression", + "status": "complete", + }, +) + +DECISION_FIELDS = [ + "source", + "disposition", + "destination", + "rationale", + "contracts", + "verification", + "status", +] + +PACKAGE_RE = re.compile(r"(?m)^\s*package\s+([A-Za-z_]\w*)") +IMPORT_BLOCK_RE = re.compile(r"(?ms)^\s*import\s*\((.*?)^\s*\)") +IMPORT_SINGLE_RE = re.compile(r'(?m)^\s*import\s+(?:[._A-Za-z]\w*\s+)?"([^"]+)"') +QUOTED_IMPORT_RE = re.compile(r'(?:^|\s)(?:[._A-Za-z]\w*\s+)?"([^"]+)"') +EXPORTED_RE = re.compile( + r"(?m)^\s*(?:func\s+(?:\([^\n)]*\)\s*)?|type\s+)([A-Z][A-Za-z0-9_]*)" +) + + +def rel(path: Path) -> str: + value = path.relative_to(ROOT).as_posix() + return "." if value == "." else value + + +def included(path: Path) -> bool: + return not any(part in EXCLUDED_PARTS or part.startswith("data.broken.") for part in path.parts) + + +def imports(text: str) -> set[str]: + result = set(IMPORT_SINGLE_RE.findall(text)) + for block in IMPORT_BLOCK_RE.findall(text): + result.update(QUOTED_IMPORT_RE.findall(block)) + return result + + +def is_generated(text: str) -> bool: + return bool(re.search(r"(?m)^// Code generated\b", "\n".join(text.splitlines()[:20]))) + + +def package_kind(location: str, generated_files: list[str], production_file_count: int) -> str: + if location == "." or location.startswith("cmd/"): + return "executable" + if location.startswith("generated/") or ( + production_file_count > 0 and len(generated_files) == production_file_count + ): + return "generated" + if location.startswith("conformance/"): + return "conformance" + if location.startswith("internal/api/"): + return "transport adapter" + if location.endswith("/arango") or location.endswith("/clickhouse") or location.startswith("internal/store/"): + return "storage adapter" + return "domain/library" + + +def display(values: list[str], limit: int | None = None) -> str: + if not values: + return "—" + if limit is not None and len(values) > limit: + return "; ".join(values[:limit]) + f"; … (+{len(values) - limit})" + return "; ".join(values) + + +def markdown_cell(value: object) -> str: + return str(value).replace("|", "\\|").replace("\n", " ") + + +def import_label(import_path: str) -> str: + prefix = MODULE + "/" + return import_path[len(prefix) :] if import_path.startswith(prefix) else import_path + + +def decision_evidence(row: dict[str, object]) -> str: + kind = str(row["kind"]) + location = str(row["location"]) + importers = list(row["production_importers_raw"]) + prod_files = list(row["production_files_raw"]) + evidence: list[str] = [] + if kind == "generated": + evidence.append("generated boundary; change its source/generator, not emitted code") + elif kind == "executable": + evidence.append("executable boundary; compare orchestration with sibling commands before combining") + elif kind == "conformance": + evidence.append("non-runtime conformance boundary") + elif not importers: + evidence.append("no in-repo production importer; verify runtime/tooling reachability") + elif len(importers) == 1: + evidence.append(f"one production importer ({importers[0]}); inspect ownership/inline potential") + if len(prod_files) >= 12 and kind not in {"generated", "conformance"}: + evidence.append(f"broad surface ({len(prod_files)} production files); inspect cohesion") + if kind == "storage adapter": + evidence.append("storage adapter; preserve interface and persisted-data contracts") + if row["generated_files_raw"] and kind != "generated": + evidence.append( + f"mixed handwritten/generated package ({len(row['generated_files_raw'])} generated file(s)); preserve regeneration boundary" + ) + if location in {"internal/dataframe/errors", "internal/projectid"}: + evidence.append("small shared semantic contract; compare direct callers before inlining") + if row["runtime_signals"] != "—": + evidence.append("runtime/build signals present; static importer counts are insufficient for deletion") + return "; ".join(evidence) if evidence else "no automatic consolidation signal; perform semantic review" + + +def validate_completed_decisions(known_locations: set[str]) -> None: + """Ensure each completed decision agrees with the live package inventory.""" + seen_sources: set[str] = set() + for decision in COMPLETED_DECISIONS: + source = str(decision["source"]) + disposition = str(decision["disposition"]) + destination = str(decision["destination"]) + if source in seen_sources: + raise SystemExit(f"duplicate completed decision source: {source}") + seen_sources.add(source) + if disposition == "keep": + if source not in known_locations: + raise SystemExit(f"kept decision source missing: {source}") + if destination not in known_locations: + raise SystemExit(f"kept decision destination missing: {destination}") + if source != destination: + raise SystemExit( + f"kept decision source/destination differ: {source} != {destination}" + ) + continue + if source in known_locations: + raise SystemExit(f"completed decision source still exists: {source}") + if destination not in known_locations: + raise SystemExit(f"completed decision destination missing: {destination}") + + +def main() -> None: + go_files = sorted(path for path in ROOT.rglob("*.go") if included(path.relative_to(ROOT))) + grouped: dict[Path, list[Path]] = defaultdict(list) + for path in go_files: + grouped[path.parent].append(path) + + locations = sorted(rel(directory) for directory in grouped) + missing = sorted(set(locations) - set(RESPONSIBILITIES)) + stale = sorted(set(RESPONSIBILITIES) - set(locations)) + if missing or stale: + raise SystemExit(f"responsibility map mismatch: missing={missing}, stale={stale}") + + rows: list[dict[str, object]] = [] + imports_by_location: dict[str, set[str]] = {} + test_imports_by_location: dict[str, set[str]] = {} + + for directory, paths in grouped.items(): + location = rel(directory) + production = [path for path in paths if not path.name.endswith("_test.go")] + tests = [path for path in paths if path.name.endswith("_test.go")] + texts = {path: path.read_text(encoding="utf-8") for path in paths} + package_source = production[0] if production else paths[0] + package_match = PACKAGE_RE.search(texts[package_source]) + package_name = package_match.group(1) if package_match else "unknown" + generated = [path.name for path in production if is_generated(texts[path])] + + prod_imports = set().union(*(imports(texts[path]) for path in production)) if production else set() + test_imports = set().union(*(imports(texts[path]) for path in tests)) if tests else set() + imports_by_location[location] = { + import_label(value) for value in prod_imports if value == MODULE or value.startswith(MODULE + "/") + } + test_imports_by_location[location] = { + import_label(value) for value in test_imports if value == MODULE or value.startswith(MODULE + "/") + } + + all_prod_text = "\n".join(texts[path] for path in production) + exported = sorted(set(EXPORTED_RE.findall(all_prod_text))) + external = sorted( + value + for value in prod_imports + if "." in value.split("/")[0] and not value.startswith(MODULE) + ) + runtime_signals: list[str] = [] + if re.search(r"(?m)^\s*func\s+init\s*\(", all_prod_text): + runtime_signals.append("init") + if "//go:embed" in all_prod_text: + runtime_signals.append("go:embed") + if "//go:generate" in all_prod_text: + runtime_signals.append("go:generate") + if re.search(r"(?m)^//go:build", all_prod_text): + runtime_signals.append("build tags") + interface_count = len(re.findall(r"(?m)^\s*type\s+[A-Z]\w*\s+interface\s*{", all_prod_text)) + if interface_count: + runtime_signals.append(f"{interface_count} exported interface(s)") + + compatibility_signals: list[str] = [] + for label, pattern in ( + ("legacy", r"(?i)\blegacy\b"), + ("deprecated", r"(?i)\bdeprecated\b"), + ("migration", r"(?i)\bmigrat(?:e|es|ed|ing|ion|ions)\b"), + ("compatibility", r"(?i)\bcompatib(?:ility|le)\b"), + ): + count = len(re.findall(pattern, all_prod_text)) + if count: + compatibility_signals.append(f"{label}={count}") + + if location == ".": + other_files = ["go.mod"] + else: + other_files = sorted( + path.name + for path in directory.iterdir() + if path.is_file() + and path.suffix in {".graphql", ".json", ".md", ".tmpl", ".txt", ".yaml", ".yml"} + and not path.name.startswith(".") + ) + prod_loc = sum(len(texts[path].splitlines()) for path in production) + test_loc = sum(len(texts[path].splitlines()) for path in tests) + import_path = MODULE if location == "." else f"{MODULE}/{location}" + row: dict[str, object] = { + "location": location, + "import_path": import_path, + "package_name": package_name, + "kind": package_kind(location, generated, len(production)), + "responsibility": RESPONSIBILITIES[location], + "production_files_raw": sorted(path.name for path in production), + "test_files_raw": sorted(path.name for path in tests), + "other_files_raw": other_files, + "generated_files_raw": sorted(generated), + "production_loc": prod_loc, + "test_loc": test_loc, + "exported_symbols_raw": exported, + "external_dependencies_raw": external, + "runtime_signals": display(runtime_signals), + "compatibility_signals": display(compatibility_signals), + "production_importers_raw": [], + "test_importers_raw": [], + } + rows.append(row) + + rows.sort(key=lambda row: (row["location"] != ".", row["location"])) + + known_locations = set(locations) + validate_completed_decisions(known_locations) + decisions_by_source = { + str(decision["source"]): decision for decision in COMPLETED_DECISIONS + } + + for row in rows: + location = str(row["location"]) + row["internal_dependencies_raw"] = sorted(imports_by_location[location] & known_locations) + row["test_dependencies_raw"] = sorted(test_imports_by_location[location] & known_locations) + row["production_importers_raw"] = sorted( + source for source, dependencies in imports_by_location.items() if location in dependencies + ) + row["test_importers_raw"] = sorted( + source + for source, dependencies in test_imports_by_location.items() + if location in dependencies and source not in row["production_importers_raw"] + ) + row["decision_evidence"] = decision_evidence(row) + row["test_scope"] = "go test ." if location == "." else f"go test ./{location}" + decision = decisions_by_source.get(location) + row["audit_status"] = ( + f"completed/{decision['disposition']}" + if decision is not None + else "inventory complete; semantic disposition pending" + ) + + fieldnames = [ + "location", + "import_path", + "package_name", + "kind", + "responsibility", + "production_files", + "test_files", + "other_files", + "generated_files", + "production_loc", + "test_loc", + "exported_symbol_count", + "exported_symbols", + "internal_dependencies", + "test_dependencies", + "production_importers", + "test_only_importers", + "external_dependencies", + "runtime_signals", + "compatibility_signals", + "decision_evidence", + "test_scope", + "audit_status", + ] + csv_rows = [] + for row in rows: + csv_rows.append( + { + "location": row["location"], + "import_path": row["import_path"], + "package_name": row["package_name"], + "kind": row["kind"], + "responsibility": row["responsibility"], + "production_files": display(row["production_files_raw"]), + "test_files": display(row["test_files_raw"]), + "other_files": display(row["other_files_raw"]), + "generated_files": display(row["generated_files_raw"]), + "production_loc": row["production_loc"], + "test_loc": row["test_loc"], + "exported_symbol_count": len(row["exported_symbols_raw"]), + "exported_symbols": display(row["exported_symbols_raw"]), + "internal_dependencies": display(row["internal_dependencies_raw"]), + "test_dependencies": display(row["test_dependencies_raw"]), + "production_importers": display(row["production_importers_raw"]), + "test_only_importers": display(row["test_importers_raw"]), + "external_dependencies": display(row["external_dependencies_raw"]), + "runtime_signals": row["runtime_signals"], + "compatibility_signals": row["compatibility_signals"], + "decision_evidence": row["decision_evidence"], + "test_scope": row["test_scope"], + "audit_status": row["audit_status"], + } + ) + + CSV_PATH.parent.mkdir(parents=True, exist_ok=True) + with CSV_PATH.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(csv_rows) + + with DECISIONS_CSV_PATH.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=DECISION_FIELDS) + writer.writeheader() + writer.writerows(COMPLETED_DECISIONS) + + kind_counts: dict[str, int] = defaultdict(int) + for row in rows: + kind_counts[str(row["kind"])] += 1 + no_importers = [ + str(row["location"]) + for row in rows + if row["kind"] not in {"executable", "generated", "conformance"} + and not row["production_importers_raw"] + ] + one_importer = [ + str(row["location"]) + for row in rows + if row["kind"] not in {"executable", "generated", "conformance"} + and len(row["production_importers_raw"]) == 1 + ] + + lines = [ + "# Package audit lookup table", + "", + "This is the repository-wide evidence index for package-boundary decisions. It is generated from the current Go source tree by `scripts/package_audit.py`; package responsibilities are curated in that script, while files, imports, importers, line counts, exported symbols, and signals are measured. The sortable source of record is [`PACKAGE_AUDIT.csv`](PACKAGE_AUDIT.csv).", + "", + "A row marked as a consolidation candidate is **not** deletion authorization. Before moving or deleting code, read the package and its tests, repeat call-site searches, check interface/runtime registration, confirm compatibility constraints, and run the listed focused test plus affected importer tests.", + "", + "## Snapshot", + "", + f"- Packages: {len(rows)}", + f"- Production Go files: {sum(len(row['production_files_raw']) for row in rows)}", + f"- Test Go files: {sum(len(row['test_files_raw']) for row in rows)}", + f"- Production Go lines: {sum(int(row['production_loc']) for row in rows):,}", + f"- Test Go lines: {sum(int(row['test_loc']) for row in rows):,}", + "- Package kinds: " + ", ".join(f"{kind}={count}" for kind, count in sorted(kind_counts.items())), + f"- Non-entrypoint handwritten packages with no production importer: {display(no_importers)}", + f"- Non-entrypoint handwritten packages with one production importer: {display(one_importer)}", + "", + "## How to use the table", + "", + "- Start with **Responsibility**, **Depends on**, and **Imported by** to judge whether the boundary has a distinct job and correct dependency direction.", + "- Use **Contents** and **API** to estimate the change surface; the API list is a top-level function/type estimate, not a compatibility guarantee.", + "- Treat **Decision evidence** as a review queue. It reports structural facts and prompts, not a final keep/merge/delete verdict.", + "- Treat generated, persisted, migration, compatibility, `init`, build-tag, embedded-data, and interface signals as deletion blockers until verified.", + "- The full CSV also records test-only dependencies/importers, external dependencies, generated files, compatibility marker counts, and exact focused test commands.", + "", + "## Regression-safety workflow", + "", + "Every package decision uses a before/after verification bracket. Use `regression-guard:verify-change` as the controller when available. The table identifies the initial change surface; pstack's `blast-radius` skill checks the contracts that static imports miss.", + "", + "1. Invoke `regression-guard:verify-change`, then run `pstack:blast-radius` for the proposed package move, merge, or deletion. Name the behavior that must remain true and require an executable proof.", + "2. Before editing, run `python3 scripts/package_audit_verify.py `. This tests the package and all direct production and test-only importers recorded in the CSV.", + "3. Make one package-boundary change. Do not batch independent package decisions.", + "4. Run the same focused command after the change. For a move, merge, or deletion, also run `python3 scripts/package_audit_verify.py --full`.", + "5. Regenerate this table with `python3 scripts/package_audit.py`, inspect the changed dependency/importer rows, and record the decision and proof before starting the next package.", + "", + "A green test run proves only the exercised contracts. Runtime registration, generated-code ownership, persisted data, wire formats, external consumers, and deployment behavior still require the specific proof identified by the blast-radius review.", + "", + "## Completed decisions", + "", + "These records preserve verified package-boundary decisions for both live packages and packages removed from the current inventory. The sortable source of record is [`PACKAGE_AUDIT_DECISIONS.csv`](PACKAGE_AUDIT_DECISIONS.csv).", + "", + "| Source | Disposition | Destination | Rationale | Contracts preserved | Verification | Status |", + "| --- | --- | --- | --- | --- | --- | --- |", + ] + for decision in COMPLETED_DECISIONS: + lines.append( + "| " + + " | ".join( + markdown_cell(decision[field]) + for field in DECISION_FIELDS + ) + + " |" + ) + + lines.extend( + [ + "", + "## Complete package table", + "", + "| Location | Kind | Responsibility | Contents | LOC (prod/test) | API | Depends on | Imported by (production) | Decision evidence |", + "| --- | --- | --- | --- | ---: | --- | --- | --- | --- |", + ] + ) + for row in rows: + contents = ( + f"P: {display(row['production_files_raw'])}; " + f"T: {display(row['test_files_raw'])}; " + f"Other: {display(row['other_files_raw'])}" + ) + api = f"{len(row['exported_symbols_raw'])}: {display(row['exported_symbols_raw'], 12)}" + cells = [ + f"`{row['location']}`", + row["kind"], + row["responsibility"], + contents, + f"{row['production_loc']:,}/{row['test_loc']:,}", + api, + display(row["internal_dependencies_raw"]), + display(row["production_importers_raw"]), + row["decision_evidence"], + ] + lines.append("| " + " | ".join(markdown_cell(cell) for cell in cells) + " |") + + lines.extend( + [ + "", + "## Regeneration and validation", + "", + "```bash", + "python3 scripts/package_audit.py", + "python3 -m unittest scripts.tests.test_package_audit", + "```", + "", + "Regenerate after adding, removing, or moving a Go package. A generator failure means the curated responsibility map and the source tree disagree; resolve that mismatch rather than emitting an unclassified row.", + ] + ) + MARKDOWN_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8") + + print(f"wrote {len(rows)} packages to {CSV_PATH.relative_to(ROOT)} and {MARKDOWN_PATH.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/package_audit_verify.py b/scripts/package_audit_verify.py new file mode 100644 index 00000000..9fe9d5d3 --- /dev/null +++ b/scripts/package_audit_verify.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Run the regression checks selected by the package audit. + +Focused mode tests a package and every direct in-repository importer recorded in +the generated audit CSV. Full mode runs the repository maintenance check and the +entire Go test suite, and is required after a package move, merge, or deletion. +""" + +from __future__ import annotations + +import argparse +import csv +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +AUDIT_CSV = ROOT / "docs" / "PACKAGE_AUDIT.csv" +EMPTY = "—" + + +def values(cell: str) -> list[str]: + if not cell or cell == EMPTY: + return [] + return [value.strip() for value in cell.split(";") if value.strip()] + + +def test_target(location: str) -> str: + return "." if location == "." else f"./{location}" + + +def load_package(location: str) -> dict[str, str]: + with AUDIT_CSV.open(encoding="utf-8", newline="") as handle: + for row in csv.DictReader(handle): + if row["location"] == location: + return row + raise ValueError( + f"package {location!r} is absent from {AUDIT_CSV.relative_to(ROOT)}; " + "regenerate the audit or use --full after an intentional deletion" + ) + + +def focused_locations(row: dict[str, str]) -> list[str]: + locations = { + row["location"], + *values(row["production_importers"]), + *values(row["test_only_importers"]), + } + return sorted(locations, key=lambda location: (location != ".", location)) + + +def focused_command(row: dict[str, str]) -> list[str]: + targets = [test_target(location) for location in focused_locations(row)] + return ["go", "test", *targets, "-count=1"] + + +def run(command: list[str], dry_run: bool) -> None: + print("$ " + " ".join(command), flush=True) + if not dry_run: + subprocess.run(command, cwd=ROOT, check=True) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run focused or repository-wide checks for a package audit decision." + ) + parser.add_argument( + "location", + nargs="?", + help="package location from docs/PACKAGE_AUDIT.csv; required unless --full is used", + ) + parser.add_argument( + "--full", + action="store_true", + help="run the audit consistency test and complete Go suite", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="print the selected commands without executing them", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + if args.full: + run( + [sys.executable, "-m", "unittest", "scripts.tests.test_package_audit"], + args.dry_run, + ) + run(["make", "test"], args.dry_run) + return + + if not args.location: + raise SystemExit("a package location is required unless --full is used") + + try: + row = load_package(args.location) + except ValueError as error: + raise SystemExit(str(error)) from error + + impacted = focused_locations(row) + print("Impacted package tests: " + ", ".join(impacted), flush=True) + run(focused_command(row), args.dry_run) + + +if __name__ == "__main__": + main() diff --git a/scripts/release-loom-ui.sh b/scripts/release-loom-ui.sh new file mode 100755 index 00000000..989a0fed --- /dev/null +++ b/scripts/release-loom-ui.sh @@ -0,0 +1,533 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +repo_root=$(cd -- "$script_dir/.." && pwd -P) +ui_root="$repo_root/ui" +loom_ui_dir="$ui_root/packages/loom-ui" +demo_dir="$ui_root/apps/demo" +package_name='@calypr/loom-ui' +demo_package_name='@calypr/loom-demo' +registry='https://registry.npmjs.org/' +default_consumer_root='/Users/peterkor/Desktop/FFNEW/IDP-Frontend' +release_npm_cache='' +upstream_remote='' +upstream_branch='' +upstream_ref='' + +# These are the only paths the automated release commit may ever include. +# Keep this as a literal array: the allowlist is part of the release contract. +owned_paths=( + Makefile + scripts/release-loom-ui.sh + scripts/tests/test_release_loom_ui.py + ui/packages/loom-ui/package.json + ui/apps/demo/package.json + ui/package-lock.json +) + +cleanup() { + if [[ -n "$release_npm_cache" && -d "$release_npm_cache" ]]; then + rm -rf -- "$release_npm_cache" + fi +} +trap cleanup EXIT + +usage() { + cat <<'EOF' +Usage: + scripts/release-loom-ui.sh release VERSION + scripts/release-loom-ui.sh prepare VERSION + scripts/release-loom-ui.sh publish VERSION + scripts/release-loom-ui.sh upgrade-consumer VERSION [IDP-FRONTEND-PATH] + +Stages: + release Run preflight, authenticate, prepare when needed, commit + only release-owned files, push, and publish or verify. + prepare Synchronize versions, install, test, build, pack, inspect, + and run npm publish --dry-run. It never publishes. + publish Require a clean committed release, repeat all checks, then + publish the exact inspected tarball as public/latest and + verify the registry result. + upgrade-consumer Install the published exact version in @gen3/frontend, + run its focused ExplorerBuilder test, and build frontend + and sampleCommons when those workspaces exist. + +The npm registry is fixed to https://registry.npmjs.org/ and every stage uses +a private temporary npm cache. npm credentials are read by npm itself and are +never printed by this command. VERSION must be a stable X.Y.Z release. +EOF +} + +die() { + printf 'release-loom-ui: %s\n' "$*" >&2 + exit 1 +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +require_layout() { + [[ -d "$ui_root" ]] || die "UI workspace not found: $ui_root" + [[ -f "$ui_root/package.json" ]] || die "UI workspace package.json not found" + [[ -f "$ui_root/package-lock.json" ]] || die "UI workspace package-lock.json not found" + [[ -f "$loom_ui_dir/package.json" ]] || die "Loom UI package.json not found" + [[ -f "$demo_dir/package.json" ]] || die "demo package.json not found" +} + +validate_version() { + local version=$1 + if ! node --input-type=module - "$version" <<'NODE' +const version = process.argv[2] ?? ''; +if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) process.exit(1); +NODE + then + return 1 + fi + [[ -n "$version" ]] || die 'VERSION is required' +} + +version_compare() { + local left=$1 right=$2 + node --input-type=module - "$left" "$right" <<'NODE' +const [left, right] = [process.argv[2], process.argv[3]].map((value) => value.split('.').map(BigInt)); +for (let index = 0; index < 3; index += 1) { + if (left[index] > right[index]) process.stdout.write('1'); + else if (left[index] < right[index]) process.stdout.write('-1'); + else continue; + process.exit(0); +} +process.stdout.write('0'); +NODE +} + +json_value() { + local path=$1 field=$2 + node --input-type=module - "$path" "$field" <<'NODE' +import { readFileSync } from 'node:fs'; + +const path = process.argv[2]; +const field = process.argv[3]; +const value = field.split('.').reduce((current, key) => current?.[key], JSON.parse(readFileSync(path, 'utf8'))); +if (typeof value !== 'string') process.exit(1); +process.stdout.write(value); +NODE +} + +check_node_and_npm() { + local node_version npm_version node_major npm_major + node_version=$(node --version) + npm_version=$(npm --version) + node_major=${node_version#v}; node_major=${node_major%%.*} + npm_major=${npm_version%%.*} + [[ "$node_major" =~ ^[0-9]+$ && "$node_major" -ge 20 ]] || die "Node >=20 is required (found $node_version)" + [[ "$npm_major" =~ ^[0-9]+$ && "$npm_major" -ge 10 ]] || die "npm >=10 is required (found $npm_version)" +} + +check_registry() { + local configured + configured=$(npm config get registry 2>/dev/null | tr -d '\r\n') + [[ "$configured" == "$registry" ]] || die "npm registry must be $registry (configured: $configured)" +} + +check_npm_auth() { + if ! npm whoami --registry "$registry" >/dev/null 2>&1; then + die 'npm authentication check failed; run npm login for https://registry.npmjs.org/ and retry' + fi +} + +authenticate_release() { + if npm whoami --registry "$registry" >/dev/null 2>&1; then + return 0 + fi + + printf 'npm authentication required; complete the browser login when prompted.\n' + npm login --auth-type=web --registry "$registry" + check_npm_auth +} + +bootstrap() { + local require_auth=${1:-yes} + require_layout + for command in node npm git tar mktemp chmod mkdir rm tr grep; do command_exists "$command"; done + + # Never inherit ~/.npm. npm_config_cache is intentionally exported only for + # this process and its children. The directory is an exact mktemp target. + if [[ -z "$release_npm_cache" || ! -d "$release_npm_cache" ]]; then + release_npm_cache=$(mktemp -d "${TMPDIR:-/tmp}/loom-release-npm-cache.XXXXXX") + chmod 700 "$release_npm_cache" + fi + export npm_config_cache="$release_npm_cache" + + check_node_and_npm + check_registry + if [[ "$require_auth" == yes ]]; then check_npm_auth; fi +} + +require_clean_ui_scope() { + local path + while IFS= read -r -d '' path; do + case "$path" in + ui/packages/loom-ui/package.json|ui/apps/demo/package.json|ui/package-lock.json) + ;; + *) + die "dirty UI path is outside the release metadata allowlist: $path" + ;; + esac + done < <( + git -C "$repo_root" diff --name-only -z -- ui + git -C "$repo_root" diff --cached --name-only -z -- ui + git -C "$repo_root" ls-files --others --exclude-standard -z -- ui + ) +} + +require_release_git_state() { + local branch merge counts local_only remote_only + branch=$(git -C "$repo_root" symbolic-ref --quiet --short HEAD) || die 'release requires a non-detached branch' + + [[ ! -e "$(git -C "$repo_root" rev-parse --git-path MERGE_HEAD)" ]] || die 'release cannot run during a merge' + [[ ! -d "$(git -C "$repo_root" rev-parse --git-path rebase-merge)" ]] || die 'release cannot run during a rebase' + [[ ! -d "$(git -C "$repo_root" rev-parse --git-path rebase-apply)" ]] || die 'release cannot run during a rebase' + + require_clean_ui_scope + + upstream_remote=$(git -C "$repo_root" config --get "branch.$branch.remote") || die 'release requires a configured upstream' + merge=$(git -C "$repo_root" config --get "branch.$branch.merge") || die 'release requires a configured upstream' + [[ "$upstream_remote" != '.' && "$merge" == refs/heads/* ]] || die 'release requires a remote upstream branch' + upstream_branch=${merge#refs/heads/} + upstream_ref=$(git -C "$repo_root" rev-parse --symbolic-full-name "${upstream_remote}/${upstream_branch}" 2>/dev/null) || die 'release requires an existing upstream branch' + + git -C "$repo_root" fetch --quiet "$upstream_remote" "$upstream_branch" || die 'release could not refresh the configured upstream' + upstream_ref=$(git -C "$repo_root" rev-parse --symbolic-full-name "${upstream_remote}/${upstream_branch}") + counts=$(git -C "$repo_root" rev-list --left-right --count "HEAD...$upstream_ref") + read -r local_only remote_only <<<"$counts" + if [[ "$remote_only" != 0 && "$local_only" != 0 ]]; then + die 'release cannot push a diverged branch; reconcile the configured upstream first' + fi + [[ "$remote_only" == 0 ]] || die 'release cannot push while the configured upstream is ahead' +} + +commit_owned_changes() { + local version=$1 status + status=$(git -C "$repo_root" status --porcelain=v1 --untracked-files=all -- "${owned_paths[@]}") + [[ -n "$status" ]] || return 0 + + # Stage only the literal allowlist, then --only protects unrelated staged + # entries from being included in this release commit. + git -C "$repo_root" add -- "${owned_paths[@]}" + git -C "$repo_root" commit --only "${owned_paths[@]}" -m "release(ui): @calypr/loom-ui $version" +} + +push_release_head() { + local head remote_head + git -C "$repo_root" push "$upstream_remote" "HEAD:refs/heads/$upstream_branch" + git -C "$repo_root" fetch --quiet "$upstream_remote" "$upstream_branch" || die 'release could not verify the pushed upstream' + head=$(git -C "$repo_root" rev-parse HEAD) + remote_head=$(git -C "$repo_root" rev-parse "$upstream_ref") + [[ "$head" == "$remote_head" ]] || die 'release push completed but upstream does not equal HEAD' +} + +check_metadata() { + local expected=$1 actual_loom actual_demo lock_demo lock_loom + actual_loom=$(json_value "$loom_ui_dir/package.json" version) || die "cannot read Loom UI package version" + actual_demo=$(json_value "$demo_dir/package.json" dependencies.@calypr/loom-ui) || die "demo does not declare $package_name" + lock_demo=$(json_value "$ui_root/package-lock.json" packages.apps/demo.dependencies.@calypr/loom-ui) || die "package-lock does not declare demo dependency $package_name" + lock_loom=$(json_value "$ui_root/package-lock.json" packages.packages/loom-ui.version) || die 'package-lock does not contain the Loom UI package version' + + [[ "$(json_value "$loom_ui_dir/package.json" name)" == "$package_name" ]] || die "Loom UI package name is not $package_name" + [[ "$(json_value "$demo_dir/package.json" name)" == "$demo_package_name" ]] || die "demo package name is not $demo_package_name" + [[ "$actual_loom" == "$expected" ]] || die "Loom UI package version is $actual_loom, expected $expected" + [[ "$actual_demo" == "$expected" ]] || die "demo dependency is $actual_demo, expected $expected" + [[ "$lock_demo" == "$expected" ]] || die "package-lock demo dependency is $lock_demo, expected $expected" + [[ "$lock_loom" == "$expected" ]] || die "package-lock Loom UI version is $lock_loom, expected $expected" +} + +registry_version_exists() { + local version=$1 output + output=$(mktemp "${TMPDIR:-/tmp}/loom-release-registry.XXXXXX") + if npm view "$package_name@$version" version --json --registry "$registry" >"$output" 2>&1; then + rm -f -- "$output" + return 0 + fi + if ! grep -Eq 'E404|404 Not Found|code E404' "$output"; then + rm -f -- "$output" + die "could not prove $package_name@$version is absent from the npm registry" + fi + rm -f -- "$output" + return 1 +} + +registry_latest_version() { + local output error_file + error_file=$(mktemp "${TMPDIR:-/tmp}/loom-release-latest.XXXXXX") + if ! output=$(npm view "$package_name" dist-tags.latest --json --registry "$registry" 2>"$error_file"); then + if grep -Eq 'E404|404 Not Found|code E404' "$error_file"; then + rm -f -- "$error_file" + printf '' + return 0 + fi + rm -f -- "$error_file" + die 'could not read the npm latest dist-tag' + fi + rm -f -- "$error_file" + node --input-type=module - "$output" <<'NODE' +const actual = JSON.parse(process.argv[2]); +if (typeof actual !== 'string') process.exit(1); +process.stdout.write(actual); +NODE +} + +require_target_after_latest() { + local target=$1 latest + latest=$(registry_latest_version) + if [[ -n "$latest" && "$(version_compare "$target" "$latest")" -le 0 ]]; then + die "$target must be newer than the npm latest version $latest" + fi +} + +synchronize_versions() { + local version=$1 current_loom current_demo + current_loom=$(json_value "$loom_ui_dir/package.json" version) || die 'cannot read current Loom UI version' + if [[ "$current_loom" != "$version" ]]; then + (cd "$ui_root" && npm version "$version" --workspace "$package_name" --no-git-tag-version --ignore-scripts) + fi + + current_demo=$(json_value "$demo_dir/package.json" dependencies.@calypr/loom-ui) || true + if [[ "$current_demo" != "$version" ]]; then + (cd "$ui_root" && npm pkg set "dependencies.@calypr/loom-ui=$version" --workspace "$demo_package_name") + fi + + (cd "$ui_root" && npm install --package-lock-only --ignore-scripts --no-audit --no-fund) + check_metadata "$version" +} + +run_ui_checks() { + (cd "$ui_root" && npm ci --ignore-scripts --no-audit --no-fund) + (cd "$ui_root" && npm test) + (cd "$ui_root" && npm run build) +} + +package_slug=${package_name#@} +package_slug=${package_slug//\//-} + +pack_and_inspect() { + local version=$1 artifact_dir=$2 tarball contents metadata + mkdir -p "$artifact_dir" + tarball="$artifact_dir/$package_slug-$version.tgz" + (cd "$ui_root" && npm pack "$loom_ui_dir" --pack-destination "$artifact_dir" --json --ignore-scripts >"$artifact_dir/pack.json") + [[ -f "$tarball" ]] || die "npm pack did not create expected tarball: $tarball" + + contents="$artifact_dir/contents.txt" + tar -tzf "$tarball" >"$contents" + grep -Fxq 'package/package.json' "$contents" || die 'packed tarball has no package/package.json' + grep -Fxq 'package/dist/loom-ui.js' "$contents" || die 'packed tarball has no dist/loom-ui.js' + grep -Fxq 'package/dist/styles.css' "$contents" || die 'packed tarball has no dist/styles.css' + + metadata="$artifact_dir/package.json" + tar -xOf "$tarball" package/package.json >"$metadata" + [[ "$(json_value "$metadata" name)" == "$package_name" ]] || die 'packed package name is incorrect' + [[ "$(json_value "$metadata" version)" == "$version" ]] || die 'packed package version is incorrect' + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$tarball" >"$artifact_dir/sha256.txt" + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$tarball" >"$artifact_dir/sha256.txt" + fi + printf '%s\n' "$tarball" +} + +dry_run_publish() { + local tarball=$1 + npm publish "$tarball" --dry-run --access public --tag latest --registry "$registry" --ignore-scripts +} + +require_clean_release() { + local status + status=$(git -C "$repo_root" status --porcelain -- ui scripts/release-loom-ui.sh) + [[ -z "$status" ]] || { + printf '%s\n' "$status" >&2 + die 'publish requires committed UI release files and a committed release script' + } +} + +published_version() { + local version=$1 output latest + output=$(npm view "$package_name@$version" version --json --registry "$registry" 2>/dev/null) || die "$package_name@$version is not published" + if ! node --input-type=module - "$output" "$version" <<'NODE' +const actual = JSON.parse(process.argv[2]); +if (actual !== process.argv[3]) process.exit(1); +NODE + then + die "registry returned an unexpected version for $package_name@$version" + fi + latest=$(npm view "$package_name" dist-tags.latest --json --registry "$registry" 2>/dev/null) || die 'could not read the npm latest dist-tag' + if ! node --input-type=module - "$latest" "$version" <<'NODE' +const actual = JSON.parse(process.argv[2]); +if (actual !== process.argv[3]) process.exit(1); +NODE + then + die "$package_name latest dist-tag is not $version" + fi +} + +tarball_integrity() { + local tarball=$1 + node --input-type=module - "$tarball" <<'NODE' +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; + +process.stdout.write(`sha512-${createHash('sha512').update(readFileSync(process.argv[2])).digest('base64')}`); +NODE +} + +registry_integrity() { + local version=$1 output + output=$(npm view "$package_name@$version" dist.integrity --json --registry "$registry" 2>/dev/null) || die "could not read registry integrity for $package_name@$version" + node --input-type=module - "$output" <<'NODE' +const actual = JSON.parse(process.argv[2]); +if (typeof actual !== 'string') process.exit(1); +process.stdout.write(actual); +NODE +} + +repair_or_verify_existing_release() { + local version=$1 tarball=$2 local_integrity remote_integrity latest comparison + local_integrity=$(tarball_integrity "$tarball") + remote_integrity=$(registry_integrity "$version") + [[ "$local_integrity" == "$remote_integrity" ]] || die "registry integrity for $package_name@$version does not match the local tarball" + + latest=$(registry_latest_version) + if [[ -z "$latest" ]]; then + comparison=1 + else + comparison=$(version_compare "$version" "$latest") + fi + case "$comparison" in + 1) + # The explicit publish stage authorizes repairing a missing/stale latest + # tag, but never permits moving latest backward. + npm dist-tag add "$package_name@$version" latest --registry "$registry" + ;; + 0) + ;; + -1) + die "refusing to move npm latest backward from $latest to $version" + ;; + *) + die "could not compare $package_name@$version with npm latest $latest" + ;; + esac + published_version "$version" + printf 'Existing %s matches the local tarball and latest is verified\n' "$package_name@$version" +} + +prepare() { + local version=$1 artifact_dir tarball current + bootstrap yes + current=$(json_value "$loom_ui_dir/package.json" version) || die 'cannot read current Loom UI version' + [[ "$(version_compare "$version" "$current")" -ge 0 ]] || die "cannot prepare $version below the checked-in Loom UI version $current" + if registry_version_exists "$version"; then + die "$package_name@$version already exists on the npm registry" + fi + require_target_after_latest "$version" + synchronize_versions "$version" + run_ui_checks + artifact_dir="$repo_root/.artifacts/releases/loom-ui/$version" + tarball=$(pack_and_inspect "$version" "$artifact_dir") + dry_run_publish "$tarball" + printf 'Prepared %s\n' "$package_name@$version" + printf 'Commit the synchronized UI manifests, lockfile, and release script, then run:\n' + printf ' %s publish %s\n' "$script_dir/release-loom-ui.sh" "$version" +} + +publish() { + local version=$1 artifact_dir tarball current + bootstrap yes + require_clean_release + current=$(json_value "$loom_ui_dir/package.json" version) || die 'cannot read current Loom UI version' + [[ "$(version_compare "$version" "$current")" -ge 0 ]] || die "cannot publish $version below the checked-in Loom UI version $current" + check_metadata "$version" + run_ui_checks + artifact_dir="$repo_root/.artifacts/releases/loom-ui/$version" + tarball=$(pack_and_inspect "$version" "$artifact_dir") + if registry_version_exists "$version"; then + repair_or_verify_existing_release "$version" "$tarball" + return 0 + fi + require_target_after_latest "$version" + dry_run_publish "$tarball" + printf 'Publishing %s (this is the explicit irreversible registry step)\n' "$package_name@$version" + npm publish "$tarball" --access public --tag latest --registry "$registry" --ignore-scripts + printf 'npm accepted %s\n' "$package_name@$version" +} + +release() { + local version=$1 + + # Keep this sequence explicit. A registry mutation is reachable only after + # the branch is checked, authentication succeeds, owned changes are pushed, + # and publish performs its normal verification. + require_release_git_state + bootstrap no + authenticate_release + + if registry_version_exists "$version"; then + printf '%s already exists; skipping prepare mutations\n' "$package_name@$version" + check_metadata "$version" + else + prepare "$version" + fi + + # Preparation is allowed to mutate only the three UI metadata files. Check + # again before staging so a toolchain-generated source or workspace change + # cannot slip past the release boundary. + require_clean_ui_scope + commit_owned_changes "$version" + push_release_head + publish "$version" +} + +upgrade_consumer() { + local version=$1 consumer_arg=${2:-$default_consumer_root} consumer_root frontend_name + bootstrap no + published_version "$version" + [[ -d "$consumer_arg" ]] || die "consumer repository not found: $consumer_arg" + consumer_root=$(cd -- "$consumer_arg" && pwd -P) + [[ -f "$consumer_root/package.json" ]] || die "consumer package.json not found: $consumer_root" + [[ -f "$consumer_root/packages/frontend/package.json" ]] || die "consumer frontend workspace not found: $consumer_root/packages/frontend" + frontend_name=$(json_value "$consumer_root/packages/frontend/package.json" name) || die 'could not read consumer frontend workspace name' + [[ "$frontend_name" == '@gen3/frontend' ]] || die "unexpected consumer frontend workspace: $frontend_name" + + (cd "$consumer_root" && npm install "$package_name@$version" --workspace '@gen3/frontend' --save-exact --ignore-scripts --no-audit --no-fund) + [[ "$(json_value "$consumer_root/packages/frontend/package.json" dependencies.@calypr/loom-ui)" == "$version" ]] || die 'consumer dependency was not synchronized exactly' + (cd "$consumer_root" && npm ls "$package_name" --workspace '@gen3/frontend' --depth=0) + (cd "$consumer_root" && npm run test:all --workspace '@gen3/frontend' -- --runInBand src/features/ExplorerBuilder/ExplorerBuilderPage.unit.test.tsx) + + if [[ -f "$consumer_root/packages/frontend/package.json" ]]; then + (cd "$consumer_root" && npm run build --workspace '@gen3/frontend') + fi + if [[ -f "$consumer_root/packages/sampleCommons/package.json" ]]; then + (cd "$consumer_root" && npm run build --workspace '@gen3/samplecommons') + fi + printf 'Upgraded and verified @gen3/frontend against %s\n' "$package_name@$version" +} + +main() { + local command=${1:-} + case "$command" in + --help|-h|'') usage; [[ -n "$command" ]] || exit 2; return 0 ;; + release|prepare|publish|upgrade-consumer) ;; + *) usage >&2; die "unknown command: $command" ;; + esac + + local version=${2:-} + validate_version "$version" || die "invalid npm version: ${version:-}" + case "$command" in + release) [[ $# -eq 2 ]] || die 'release accepts exactly VERSION'; release "$version" ;; + prepare) [[ $# -eq 2 ]] || die 'prepare accepts exactly VERSION'; prepare "$version" ;; + publish) [[ $# -eq 2 ]] || die 'publish accepts exactly VERSION'; publish "$version" ;; + upgrade-consumer) [[ $# -le 3 ]] || die 'upgrade-consumer accepts VERSION and optional consumer path'; upgrade_consumer "$version" "${3:-$default_consumer_root}" ;; + esac +} + +main "$@" diff --git a/scripts/tests/__init__.py b/scripts/tests/__init__.py new file mode 100644 index 00000000..15b87556 --- /dev/null +++ b/scripts/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for repository maintenance scripts.""" diff --git a/scripts/tests/test_acceptance_real.py b/scripts/tests/test_acceptance_real.py new file mode 100644 index 00000000..862f2582 --- /dev/null +++ b/scripts/tests/test_acceptance_real.py @@ -0,0 +1,73 @@ +import subprocess +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts" / "acceptance-real.sh" +WORKFLOW = ROOT / ".github" / "workflows" / "acceptance.yaml" +PERFORMANCE = ROOT / "scripts" / "acceptance-performance.sh" + + +class AcceptanceRealScriptTest(unittest.TestCase): + def test_shell_syntax(self) -> None: + subprocess.run(["bash", "-n", str(SCRIPT)], cwd=ROOT, check=True) + subprocess.run(["bash", "-n", str(PERFORMANCE)], cwd=ROOT, check=True) + + def test_local_and_github_wiring_contract(self) -> None: + source = SCRIPT.read_text(encoding="utf-8") + + self.assertIn('start_forward "$clickhouse_service" 8123 clickhouse-http', source) + self.assertIn('start_forward "$clickhouse_service" 9000 clickhouse-native', source) + self.assertIn('--clickhouse-url "$clickhouse_native_url"', source) + self.assertIn('--run-id "$run_id"', source) + self.assertIn('server_config=$(mktemp "${TMPDIR:-/tmp}/loom-acceptance-server-config.XXXXXX.json")', source) + self.assertIn('chmod 600 "$server_config"', source) + self.assertIn("jq -n \\", source) + self.assertIn('--arg schema "$server_source_root/schemas/graph-fhir.json"', source) + self.assertIn('"$build_dir/arango-fhir-server" --config "$server_config" --no-auth', source) + self.assertIn('LOOM_ACCEPTANCE_CLICKHOUSE_PASSWORD="$clickhouse_password" "$build_dir/loom-acceptance"', source) + self.assertNotIn('--clickhouse-password "$clickhouse_password"', source) + self.assertNotIn('--arg clickhouse_password "$clickhouse_password"', source) + self.assertNotIn('curl -fsS --user "$clickhouse_username:$clickhouse_password"', source) + self.assertIn('LOOM_ACCEPTANCE_CONFIG_CLICKHOUSE_PASSWORD="$clickhouse_password" jq -n', source) + self.assertIn('--config "$clickhouse_curl_config"', source) + self.assertIn("LOOM_ACCEPTANCE_KUBE_CONFIG_SECRET:-loom-config", source) + self.assertIn("get secret \"$secret_name\" -o jsonpath='{.data.config\\.yaml}'", source) + self.assertIn(".server.clickhouse.username", source) + self.assertIn(".server.clickhouse.password", source) + self.assertIn('LOOM_ACCEPTANCE_CURL_USER="$clickhouse_username:$clickhouse_password"', source) + cleanup = source.split("cleanup() {", 1)[1].split("trap cleanup EXIT", 1)[0] + self.assertLess(cleanup.index("clickhouse_http_request"), cleanup.index('for pid in "${processes')) + self.assertNotIn("arango-fhir-proto", source) + self.assertNotIn("$repo_root/.gocache", source) + + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn("http://127.0.0.1:8529/_api/version", workflow) + self.assertNotIn("http://localhost:8529/_api/version", workflow) + self.assertIn("\n pull_request:\n", workflow) + self.assertNotIn("\n push:\n", workflow) + self.assertIn("CLICKHOUSE_USER: loom_ci", workflow) + self.assertIn("CLICKHOUSE_PASSWORD: loom_ci_password", workflow) + self.assertIn("--user=loom_ci --password=loom_ci_password", workflow) + self.assertIn("fetch-depth: 0", workflow) + self.assertIn("run: make acceptance-performance", workflow) + self.assertIn("- name: Summarize acceptance evidence", workflow) + self.assertIn("GITHUB_STEP_SUMMARY", workflow) + self.assertIn("current/report.json", workflow) + self.assertIn("vertices_inserted", workflow) + self.assertIn("row_digest", workflow) + self.assertIn("cleanup_status", workflow) + self.assertNotIn("LOOM_ACCEPTANCE_ALLOW_BASE_UNAVAILABLE", workflow) + + performance = PERFORMANCE.read_text(encoding="utf-8") + self.assertIn('git archive "$base_ref"', performance) + self.assertIn('LOOM_ACCEPTANCE_SERVER_SOURCE_ROOT="$source_root"', performance) + self.assertIn('if [[ ! -f "$base_source/cmd/loom-acceptance/main.go" ]]', performance) + self.assertIn('LOOM_ACCEPTANCE_ALLOW_BASE_UNAVAILABLE=true base_unavailable "base commit predates the acceptance protocol"', performance) + self.assertIn("--performance-repeat-base-report", performance) + self.assertIn("--performance-repeat-current-report", performance) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_demo_configuration.py b/scripts/tests/test_demo_configuration.py new file mode 100644 index 00000000..f70eb9be --- /dev/null +++ b/scripts/tests/test_demo_configuration.py @@ -0,0 +1,174 @@ +import os +import pathlib +import subprocess +import tempfile +import textwrap +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[2] + + +class DemoConfigurationTest(unittest.TestCase): + def test_demo_up_uses_custom_project_ports_and_dataset(self): + with tempfile.TemporaryDirectory() as temporary: + temporary_path = pathlib.Path(temporary) + log = temporary_path / "docker.log" + self._write_executable( + temporary_path / "docker", + """ + #!/usr/bin/env bash + printf '%s\n' "$*" >> "$DEMO_TEST_DOCKER_LOG" + """, + ) + self._write_executable(temporary_path / "curl", "#!/usr/bin/env bash\nexit 0\n") + environment = os.environ.copy() + environment.update( + { + "PATH": f"{temporary_path}:{environment['PATH']}", + "DEMO_TEST_DOCKER_LOG": str(log), + "LOOM_DEMO_COMPOSE_PROJECT": "alternate-demo", + "LOOM_DEMO_API_HOST": "127.0.0.2", + "LOOM_DEMO_API_PORT": "18080", + "LOOM_DEMO_UI_HOST": "127.0.0.3", + "LOOM_DEMO_UI_PORT": "13080", + "LOOM_DEMO_PROJECT": "EXAMPLE_PROJECT", + "LOOM_DEMO_GENERATION": "example-generation", + } + ) + + result = subprocess.run( + [str(ROOT / "scripts/demo-up.sh")], + cwd=ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + commands = log.read_text() + self.assertIn("compose --project-name alternate-demo up --build -d arangodb clickhouse loom-api", commands) + self.assertIn("compose --project-name alternate-demo run --rm --no-deps demo-seed", commands) + self.assertIn("compose --project-name alternate-demo up --build -d loom-ui", commands) + self.assertIn("http://127.0.0.3:13080", result.stdout) + + def test_demo_down_uses_custom_project(self): + with tempfile.TemporaryDirectory() as temporary: + temporary_path = pathlib.Path(temporary) + log = temporary_path / "docker.log" + self._write_executable( + temporary_path / "docker", + """ + #!/usr/bin/env bash + printf '%s\n' "$*" >> "$DEMO_TEST_DOCKER_LOG" + """, + ) + environment = os.environ.copy() + environment.update( + { + "PATH": f"{temporary_path}:{environment['PATH']}", + "DEMO_TEST_DOCKER_LOG": str(log), + "LOOM_DEMO_COMPOSE_PROJECT": "alternate-demo", + } + ) + + result = subprocess.run( + [str(ROOT / "scripts/demo-down.sh"), "--volumes"], + cwd=ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(log.read_text().strip(), "compose --project-name alternate-demo down --volumes") + + def test_demo_up_brackets_ipv6_urls(self): + with tempfile.TemporaryDirectory() as temporary: + temporary_path = pathlib.Path(temporary) + log = temporary_path / "docker.log" + self._write_executable(temporary_path / "docker", "#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$DEMO_TEST_DOCKER_LOG\"\n") + self._write_executable(temporary_path / "curl", "#!/usr/bin/env bash\nexit 0\n") + environment = os.environ.copy() + environment.update( + { + "PATH": f"{temporary_path}:{environment['PATH']}", + "DEMO_TEST_DOCKER_LOG": str(log), + "LOOM_DEMO_API_HOST": "::1", + "LOOM_DEMO_UI_HOST": "::1", + } + ) + + result = subprocess.run( + [str(ROOT / "scripts/demo-up.sh")], + cwd=ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("http://[::1]:3080", result.stdout) + + def test_demo_smoke_passes_the_expected_contract_to_the_verifier(self): + with tempfile.TemporaryDirectory() as temporary: + temporary_path = pathlib.Path(temporary) + log = temporary_path / "docker.log" + self._write_executable( + temporary_path / "docker", + """ + #!/usr/bin/env bash + printf '%s\n' "$*" >> "$DEMO_TEST_DOCKER_LOG" + """, + ) + self._write_executable( + temporary_path / "curl", + """ + #!/usr/bin/env bash + printf '' + """, + ) + environment = os.environ.copy() + environment.update( + { + "PATH": f"{temporary_path}:{environment['PATH']}", + "DEMO_TEST_DOCKER_LOG": str(log), + "LOOM_DEMO_COMPOSE_PROJECT": "alternate-demo", + "LOOM_DEMO_PROJECT": "EXAMPLE_PROJECT", + "LOOM_DEMO_GENERATION": "example-generation", + "LOOM_DEMO_MANAGEMENT": "REPOSITORY", + "LOOM_DEMO_OUTPUT_ID": "example_output", + "LOOM_DEMO_OUTPUT_TITLE": "Example output", + } + ) + + result = subprocess.run( + [str(ROOT / "scripts/demo-smoke.sh")], + cwd=ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + command = log.read_text().strip() + self.assertIn("compose --project-name alternate-demo exec -T loom-api /app/loom-acceptance --smoke-only", command) + self.assertIn("--project EXAMPLE_PROJECT", command) + self.assertIn("--smoke-management REPOSITORY", command) + self.assertIn("--generation example-generation", command) + self.assertIn("--smoke-output-id example_output", command) + self.assertIn("--smoke-output-title Example output", command) + self.assertIn("--oracle /app/testdata/oracle.json", command) + + @staticmethod + def _write_executable(path, content): + path.write_text(textwrap.dedent(content).lstrip()) + path.chmod(0o755) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_package_audit.py b/scripts/tests/test_package_audit.py new file mode 100644 index 00000000..fb9a16b5 --- /dev/null +++ b/scripts/tests/test_package_audit.py @@ -0,0 +1,123 @@ +import csv +import subprocess +import sys +import unittest +from pathlib import Path + +from scripts import package_audit + + +ROOT = Path(__file__).resolve().parents[2] + + +class PackageAuditTest(unittest.TestCase): + def test_generator_covers_each_go_package_once(self) -> None: + subprocess.run( + [sys.executable, "scripts/package_audit.py"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + with (ROOT / "docs" / "PACKAGE_AUDIT.csv").open(encoding="utf-8", newline="") as handle: + rows = list(csv.DictReader(handle)) + locations = [row["location"] for row in rows] + expected_locations = sorted( + { + package_audit.rel(path.parent) + for path in ROOT.rglob("*.go") + if package_audit.included(path.relative_to(ROOT)) + } + ) + self.assertEqual(len(locations), len(set(locations))) + self.assertEqual(expected_locations, sorted(locations)) + self.assertTrue(all(row["responsibility"].endswith(".") for row in rows)) + self.assertTrue(all(row["audit_status"] for row in rows)) + resolver = next(row for row in rows if row["location"] == "internal/api/graphql/graph/resolver") + self.assertEqual("transport adapter", resolver["kind"]) + self.assertNotEqual("—", resolver["generated_files"]) + load = next(row for row in rows if row["location"] == "internal/api/bulk/load") + self.assertEqual("completed/keep", load["audit_status"]) + + def test_completed_decisions_match_live_or_removed_packages(self) -> None: + locations = { + package_audit.rel(path.parent) + for path in ROOT.rglob("*.go") + if package_audit.included(path.relative_to(ROOT)) + } + with (ROOT / "docs" / "PACKAGE_AUDIT_DECISIONS.csv").open( + encoding="utf-8", newline="" + ) as handle: + decisions = list(csv.DictReader(handle)) + + self.assertEqual(len(package_audit.COMPLETED_DECISIONS), len(decisions)) + self.assertEqual(package_audit.DECISION_FIELDS, list(decisions[0])) + for decision in decisions: + if decision["disposition"] == "keep": + self.assertIn(decision["source"], locations) + self.assertIn(decision["destination"], locations) + self.assertEqual(decision["source"], decision["destination"]) + else: + self.assertNotIn(decision["source"], locations) + self.assertIn(decision["destination"], locations) + self.assertEqual("complete", decision["status"]) + + def test_regeneration_is_deterministic(self) -> None: + paths = [ + ROOT / "docs" / "PACKAGE_AUDIT.csv", + ROOT / "docs" / "PACKAGE_AUDIT_DECISIONS.csv", + ROOT / "docs" / "PACKAGE_AUDIT.md", + ] + + subprocess.run( + [sys.executable, "scripts/package_audit.py"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + first = [path.read_bytes() for path in paths] + subprocess.run( + [sys.executable, "scripts/package_audit.py"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + second = [path.read_bytes() for path in paths] + + self.assertEqual(first, second) + + def test_verifier_selects_package_and_direct_importers(self) -> None: + result = subprocess.run( + [ + sys.executable, + "scripts/package_audit_verify.py", + "internal/api/graphql/graph", + "--dry-run", + ], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + self.assertIn("internal/server", result.stdout) + self.assertIn( + "go test ./internal/api/graphql/graph ./internal/server -count=1", + result.stdout, + ) + + def test_verifier_full_mode_runs_both_repository_checks(self) -> None: + result = subprocess.run( + [sys.executable, "scripts/package_audit_verify.py", "--full", "--dry-run"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + self.assertIn("-m unittest scripts.tests.test_package_audit", result.stdout) + self.assertIn("$ make test", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_release_loom_ui.py b/scripts/tests/test_release_loom_ui.py new file mode 100644 index 00000000..fedffdd3 --- /dev/null +++ b/scripts/tests/test_release_loom_ui.py @@ -0,0 +1,485 @@ +import json +import base64 +import hashlib +import os +import shutil +import stat +import subprocess +import tarfile +import tempfile +import unittest +from pathlib import Path +from typing import Optional + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts" / "release-loom-ui.sh" + + +FAKE_NPM = r'''#!/usr/bin/env python3 +import json +import base64 +import hashlib +import os +import sys +import tarfile +from io import BytesIO +from pathlib import Path + + +args = sys.argv[1:] +log_path = Path(os.environ["FAKE_NPM_LOG"]) +with log_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(args) + "\n") + +if args == ["--version"]: + print("10.9.4") + raise SystemExit(0) +if args[:3] == ["config", "get", "registry"]: + print("https://registry.npmjs.org/") + raise SystemExit(0) +if args and args[0] == "whoami": + if os.environ.get("FAKE_AUTH_FAIL") == "1" and not Path(os.environ["FAKE_LOGIN_MARKER"]).exists(): + print("npm error code ENEEDAUTH", file=sys.stderr) + raise SystemExit(1) + print("release-bot") + raise SystemExit(0) +if args and args[0] == "login": + Path(os.environ["FAKE_LOGIN_MARKER"]).write_text("logged-in", encoding="utf-8") + raise SystemExit(0) +if args and args[0] == "view": + if any(argument.startswith("@calypr/loom-ui@") for argument in args): + if os.environ.get("FAKE_PUBLISHED") == "1" or ( + os.environ.get("FAKE_PUBLISHED_ON_REAL") == "1" + and Path(os.environ["FAKE_REAL_PUBLISH_MARKER"]).exists() + ): + version = next(argument.rsplit("@", 1)[1] for argument in args if argument.startswith("@calypr/loom-ui@")) + if "dist.integrity" in args: + tarball = Path(os.environ["FAKE_TARBALL"]) + integrity = "sha512-" + base64.b64encode(hashlib.sha512(tarball.read_bytes()).digest()).decode() + print(json.dumps(integrity)) + raise SystemExit(0) + print(json.dumps(version)) + raise SystemExit(0) + print("npm error code E404", file=sys.stderr) + raise SystemExit(1) + repaired_tag = Path(os.environ.get("FAKE_TAG_MARKER", "")) + if repaired_tag.is_file(): + print(json.dumps(repaired_tag.read_text(encoding="utf-8"))) + elif os.environ.get("FAKE_PUBLISHED_ON_REAL") == "1" and Path(os.environ["FAKE_REAL_PUBLISH_MARKER"]).exists(): + print(json.dumps(os.environ.get("FAKE_PUBLISHED_VERSION", "0.1.2"))) + else: + print(json.dumps(os.environ.get("FAKE_LATEST", "0.1.1"))) + raise SystemExit(0) +if args and args[0] == "pack": + package_dir = Path(args[1]) + version = json.loads((package_dir / "package.json").read_text(encoding="utf-8"))["version"] + destination = Path(args[args.index("--pack-destination") + 1]) + destination.mkdir(parents=True, exist_ok=True) + tarball = destination / f"calypr-loom-ui-{version}.tgz" + with tarfile.open(tarball, "w:gz") as archive: + for name, content in { + "package/package.json": (package_dir / "package.json").read_bytes(), + "package/dist/loom-ui.js": b"export {};\n", + "package/dist/styles.css": b":root {}\n", + }.items(): + info = tarfile.TarInfo(name) + info.size = len(content) + archive.addfile(info, BytesIO(content)) + print(json.dumps([{ "filename": str(tarball) }])) + raise SystemExit(0) +if args and args[0] == "publish": + if "--dry-run" not in args: + if os.environ.get("FAKE_REQUIRE_PUSH") == "1" and not Path(os.environ["FAKE_PUSH_MARKER"]).exists(): + print("publish happened before push", file=sys.stderr) + raise SystemExit(88) + Path(os.environ["FAKE_REAL_PUBLISH_MARKER"]).write_text("called", encoding="utf-8") + if os.environ.get("FAKE_REAL_PUBLISH_SUCCEEDS") == "1": + raise SystemExit(0) + raise SystemExit(99) + raise SystemExit(0) +if args[:2] == ["dist-tag", "add"]: + package_version = args[2].rsplit("@", 1)[1] + Path(os.environ["FAKE_TAG_MARKER"]).write_text(package_version, encoding="utf-8") + raise SystemExit(0) +if args and args[0] in {"ci", "test", "build", "install", "version", "pkg"}: + raise SystemExit(0) +raise SystemExit(0) +''' + + +class ReleaseScriptTest(unittest.TestCase): + def setUp(self) -> None: + self.tempdir = tempfile.TemporaryDirectory(prefix="loom-release-test-") + self.root = Path(self.tempdir.name) + self.script = self.root / "scripts" / "release-loom-ui.sh" + self.script.parent.mkdir(parents=True) + shutil.copy2(SCRIPT, self.script) + self.script.chmod(self.script.stat().st_mode | stat.S_IXUSR) + + loom_ui = self.root / "ui" / "packages" / "loom-ui" + demo = self.root / "ui" / "apps" / "demo" + loom_ui.mkdir(parents=True) + demo.mkdir(parents=True) + (loom_ui / "package.json").write_text( + json.dumps({"name": "@calypr/loom-ui", "version": "0.1.2"}), encoding="utf-8" + ) + (demo / "package.json").write_text( + json.dumps( + { + "name": "@calypr/loom-demo", + "version": "0.1.0", + "dependencies": {"@calypr/loom-ui": "0.1.2"}, + } + ), + encoding="utf-8", + ) + (self.root / "ui" / "package.json").write_text( + json.dumps({"name": "@calypr/loom-ui-workspace", "workspaces": ["packages/loom-ui", "apps/demo"]}), + encoding="utf-8", + ) + (self.root / "ui" / "package-lock.json").write_text( + json.dumps( + { + "name": "@calypr/loom-ui-workspace", + "lockfileVersion": 3, + "packages": { + "apps/demo": { + "dependencies": {"@calypr/loom-ui": "0.1.2"} + }, + "packages/loom-ui": {"name": "@calypr/loom-ui", "version": "0.1.2"}, + }, + } + ), + encoding="utf-8", + ) + (self.root / "Makefile").write_text("release fixture\n", encoding="utf-8") + fixture_tests = self.root / "scripts" / "tests" / "test_release_loom_ui.py" + fixture_tests.parent.mkdir(parents=True, exist_ok=True) + fixture_tests.write_text("release fixture test\n", encoding="utf-8") + (self.root / ".gitignore").write_text(".artifacts/\n", encoding="utf-8") + + self.fake_bin = self.root / "fake-bin" + self.fake_bin.mkdir() + fake_npm = self.fake_bin / "npm" + fake_npm.write_text(FAKE_NPM, encoding="utf-8") + fake_npm.chmod(fake_npm.stat().st_mode | stat.S_IXUSR) + self.npm_log = self.root / "npm.log" + self.real_publish_marker = self.root / "real-publish-called" + self.login_marker = self.root / "npm-login-called" + self.push_marker = self.root / "push-received" + self.tag_marker = self.root / "tag-repaired" + self.env = os.environ.copy() + self.env["PATH"] = f"{self.fake_bin}{os.pathsep}{self.env['PATH']}" + self.env["FAKE_NPM_LOG"] = str(self.npm_log) + self.env["FAKE_REAL_PUBLISH_MARKER"] = str(self.real_publish_marker) + self.env["FAKE_LOGIN_MARKER"] = str(self.login_marker) + self.env["FAKE_PUSH_MARKER"] = str(self.push_marker) + self.env["FAKE_TAG_MARKER"] = str(self.tag_marker) + self.env["FAKE_TARBALL"] = str(self.root / ".artifacts/releases/loom-ui/0.1.2/calypr-loom-ui-0.1.2.tgz") + self.env["TMPDIR"] = str(self.root) + + subprocess.run(["git", "init", "-q"], cwd=self.root, check=True) + subprocess.run(["git", "config", "user.email", "release-test@example.invalid"], cwd=self.root, check=True) + subprocess.run(["git", "config", "user.name", "Release test"], cwd=self.root, check=True) + subprocess.run(["git", "add", "."], cwd=self.root, check=True) + subprocess.run(["git", "commit", "-qm", "fixture"], cwd=self.root, check=True) + + def tearDown(self) -> None: + self.tempdir.cleanup() + + def configure_upstream(self, hook: Optional[str] = None) -> Path: + remote = self.root / "upstream.git" + subprocess.run(["git", "init", "--bare", "-q", str(remote)], cwd=self.root, check=True) + subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=self.root, check=True) + subprocess.run(["git", "push", "-q", "-u", "origin", "HEAD"], cwd=self.root, check=True) + if hook is not None: + receive_hook = remote / "hooks" / "pre-receive" + receive_hook.write_text(hook, encoding="utf-8") + receive_hook.chmod(receive_hook.stat().st_mode | stat.S_IXUSR) + else: + receive_hook = remote / "hooks" / "post-receive" + receive_hook.write_text( + f"#!/bin/sh\nprintf pushed > {self.push_marker!s}\n", encoding="utf-8" + ) + receive_hook.chmod(receive_hook.stat().st_mode | stat.S_IXUSR) + return remote + + def commit_pending_owned_change(self) -> None: + (self.root / "Makefile").write_text("release fixture changed\n", encoding="utf-8") + + def git_output(self, *args: str) -> str: + return subprocess.check_output(["git", *args], cwd=self.root, text=True).strip() + + def run_script(self, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(self.script), *args], + cwd=self.root, + env=self.env, + text=True, + capture_output=True, + ) + + def npm_calls(self) -> list[list[str]]: + if not self.npm_log.exists(): + return [] + return [json.loads(line) for line in self.npm_log.read_text(encoding="utf-8").splitlines()] + + def test_help_and_invalid_version_are_local_and_loud(self) -> None: + help_result = self.run_script("--help") + self.assertEqual(help_result.returncode, 0) + self.assertIn("prepare VERSION", help_result.stdout) + + invalid_result = self.run_script("prepare", "01.2.3") + self.assertNotEqual(invalid_result.returncode, 0) + self.assertIn("invalid npm version", invalid_result.stderr) + self.assertEqual(self.npm_calls(), []) + + prerelease_result = self.run_script("prepare", "0.1.2-rc.1") + self.assertNotEqual(prerelease_result.returncode, 0) + self.assertIn("invalid npm version", prerelease_result.stderr) + + def test_prepare_rejects_downgrade_and_stale_latest(self) -> None: + downgrade_result = self.run_script("prepare", "0.1.1") + self.assertNotEqual(downgrade_result.returncode, 0) + self.assertIn("below the checked-in Loom UI version 0.1.2", downgrade_result.stderr) + + publish_downgrade_result = self.run_script("publish", "0.1.1") + self.assertNotEqual(publish_downgrade_result.returncode, 0) + self.assertIn("cannot publish 0.1.1 below the checked-in Loom UI version 0.1.2", publish_downgrade_result.stderr) + + self.env["FAKE_LATEST"] = "0.1.2" + stale_result = self.run_script("prepare", "0.1.2") + self.assertNotEqual(stale_result.returncode, 0) + self.assertIn("must be newer than the npm latest version 0.1.2", stale_result.stderr) + + self.env["FAKE_LATEST"] = "0.1.3" + stale_result = self.run_script("prepare", "0.1.3") + self.assertNotEqual(stale_result.returncode, 0) + self.assertIn("must be newer than the npm latest version 0.1.3", stale_result.stderr) + + def test_prepare_only_routes_to_dry_run_publish(self) -> None: + result = self.run_script("prepare", "0.1.2") + self.assertEqual(result.returncode, 0, result.stderr) + calls = self.npm_calls() + publish_calls = [call for call in calls if call and call[0] == "publish"] + self.assertEqual(len(publish_calls), 1) + self.assertIn("--dry-run", publish_calls[0]) + self.assertFalse(self.real_publish_marker.exists()) + self.assertTrue((self.root / ".artifacts/releases/loom-ui/0.1.2/contents.txt").exists()) + self.assertEqual(list(self.root.glob("loom-release-npm-cache.*")), []) + + def test_publish_refuses_dirty_release_before_publish(self) -> None: + dirty_file = self.root / "ui" / "packages" / "loom-ui" / "README.md" + dirty_file.write_text("uncommitted change\n", encoding="utf-8") + + result = self.run_script("publish", "0.1.2") + self.assertNotEqual(result.returncode, 0) + self.assertIn("publish requires committed UI release files", result.stderr) + self.assertFalse(self.real_publish_marker.exists()) + self.assertFalse(any(call and call[0] == "publish" for call in self.npm_calls())) + + def test_publish_retry_verifies_integrity_without_republishing(self) -> None: + self.env["FAKE_PUBLISHED"] = "1" + self.env["FAKE_LATEST"] = "0.1.2" + result = self.run_script("publish", "0.1.2") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("matches the local tarball", result.stdout) + self.assertFalse(self.real_publish_marker.exists()) + self.assertFalse(any(call and call[0] == "publish" for call in self.npm_calls())) + self.assertTrue(any(call[:1] == ["view"] and "dist.integrity" in call for call in self.npm_calls())) + + def test_publish_retry_repairs_an_older_latest_tag(self) -> None: + self.env["FAKE_PUBLISHED"] = "1" + self.env["FAKE_LATEST"] = "0.1.1" + result = self.run_script("publish", "0.1.2") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(self.tag_marker.read_text(encoding="utf-8"), "0.1.2") + self.assertFalse(self.real_publish_marker.exists()) + self.assertFalse(any(call and call[0] == "publish" for call in self.npm_calls())) + + def test_release_uses_web_login_when_whoami_fails(self) -> None: + self.configure_upstream() + self.env["FAKE_AUTH_FAIL"] = "1" + self.env["FAKE_PUBLISHED"] = "1" + self.env["FAKE_LATEST"] = "0.1.2" + + result = self.run_script("release", "0.1.2") + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(self.login_marker.exists()) + calls = self.npm_calls() + whoami = [index for index, call in enumerate(calls) if call[:1] == ["whoami"]] + login = [index for index, call in enumerate(calls) if call[:1] == ["login"]] + self.assertGreaterEqual(len(whoami), 2) + self.assertEqual(len(login), 1) + self.assertTrue(whoami[0] < login[0] < whoami[-1]) + self.assertIn("--auth-type=web", calls[login[0]]) + self.assertNotIn("OTP", result.stdout + result.stderr) + + def test_release_commits_only_allowlist_and_preserves_index_and_untracked_files(self) -> None: + self.configure_upstream() + self.env["FAKE_PUBLISHED"] = "1" + self.env["FAKE_LATEST"] = "0.1.2" + self.commit_pending_owned_change() + staged_unrelated = self.root / "unrelated-staged.txt" + staged_unrelated.write_text("keep staged\n", encoding="utf-8") + subprocess.run(["git", "add", str(staged_unrelated)], cwd=self.root, check=True) + untracked_unrelated = self.root / "unrelated-untracked.txt" + untracked_unrelated.write_text("keep untracked\n", encoding="utf-8") + + result = self.run_script("release", "0.1.2") + + self.assertEqual(result.returncode, 0, result.stderr) + committed_paths = self.git_output("show", "--format=", "--name-only", "HEAD").splitlines() + self.assertEqual(committed_paths, ["Makefile"]) + self.assertEqual(self.git_output("diff", "--cached", "--name-only"), "unrelated-staged.txt") + self.assertTrue(untracked_unrelated.exists()) + self.assertEqual(self.git_output("rev-parse", "HEAD"), self.git_output("rev-parse", "@{upstream}")) + + def test_release_pushes_before_real_publish(self) -> None: + self.configure_upstream() + self.env["FAKE_PUBLISHED_ON_REAL"] = "1" + self.env["FAKE_REQUIRE_PUSH"] = "1" + self.env["FAKE_REAL_PUBLISH_SUCCEEDS"] = "1" + self.env["FAKE_PUBLISHED_VERSION"] = "0.1.2" + self.env["FAKE_LATEST"] = "0.1.1" + self.commit_pending_owned_change() + + result = self.run_script("release", "0.1.2") + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(self.push_marker.exists()) + self.assertTrue(self.real_publish_marker.exists()) + calls = self.npm_calls() + publish_index = next( + index + for index, call in enumerate(calls) + if call[:1] == ["publish"] and "--dry-run" not in call + ) + self.assertFalse(any(call[:1] == ["view"] for call in calls[publish_index + 1 :])) + + def test_release_push_failure_prevents_real_publish(self) -> None: + self.configure_upstream("#!/bin/sh\nexit 1\n") + self.env["FAKE_PUBLISHED"] = "1" + self.env["FAKE_LATEST"] = "0.1.2" + self.commit_pending_owned_change() + + result = self.run_script("release", "0.1.2") + + self.assertNotEqual(result.returncode, 0) + self.assertFalse(self.real_publish_marker.exists()) + self.assertFalse(any(call and call[0] == "publish" for call in self.npm_calls())) + + def test_release_rerun_does_not_create_duplicate_commit(self) -> None: + self.configure_upstream() + self.env["FAKE_PUBLISHED"] = "1" + self.env["FAKE_LATEST"] = "0.1.2" + self.commit_pending_owned_change() + + first = self.run_script("release", "0.1.2") + first_head = self.git_output("rev-parse", "HEAD") + first_count = int(self.git_output("rev-list", "--count", "HEAD")) + second = self.run_script("release", "0.1.2") + + self.assertEqual(first.returncode, 0, first.stderr) + self.assertEqual(second.returncode, 0, second.stderr) + self.assertEqual(self.git_output("rev-parse", "HEAD"), first_head) + self.assertEqual(int(self.git_output("rev-list", "--count", "HEAD")), first_count) + + def test_make_release_ui_accepts_positional_version_and_delegates(self) -> None: + makefile = self.root / "Makefile" + shutil.copy2(ROOT / "Makefile", makefile) + marker = self.root / "make-release-args" + script = self.root / "scripts" / "release-loom-ui.sh" + script.write_text( + f"#!/bin/sh\nprintf '%s\\n' \"$@\" > {marker!s}\n", encoding="utf-8" + ) + script.chmod(script.stat().st_mode | stat.S_IXUSR) + + result = subprocess.run( + ["make", "release-ui", "0.1.2"], + cwd=self.root, + text=True, + capture_output=True, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(marker.read_text(encoding="utf-8").splitlines(), ["release", "0.1.2"]) + + def test_make_release_ui_rejects_ambiguous_or_missing_versions_before_running(self) -> None: + makefile = self.root / "Makefile" + shutil.copy2(ROOT / "Makefile", makefile) + marker = self.root / "make-release-args" + script = self.root / "scripts" / "release-loom-ui.sh" + script.write_text( + f"#!/bin/sh\nprintf '%s\\n' \"$@\" > {marker!s}\n", encoding="utf-8" + ) + script.chmod(script.stat().st_mode | stat.S_IXUSR) + + for args in ( + ["release-ui"], + ["release-ui", "0.1.2", "extra"], + ["release-ui", "0.1.2", "VERSION=0.2.0"], + ): + result = subprocess.run( + ["make", *args], + cwd=self.root, + text=True, + capture_output=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("Usage", result.stderr) + self.assertFalse(marker.exists()) + + inherited = os.environ.copy() + inherited["VERSION"] = "0.2.0" + result = subprocess.run( + ["make", "release-ui", "0.1.2"], + cwd=self.root, + env=inherited, + text=True, + capture_output=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(marker.read_text(encoding="utf-8").splitlines(), ["release", "0.1.2"]) + + def test_release_requires_a_branch_and_upstream_before_authentication(self) -> None: + missing_upstream = self.run_script("release", "0.1.2") + self.assertNotEqual(missing_upstream.returncode, 0) + self.assertIn("configured upstream", missing_upstream.stderr) + self.assertEqual(self.npm_calls(), []) + + subprocess.run(["git", "checkout", "--detach", "-q"], cwd=self.root, check=True) + detached = self.run_script("release", "0.1.2") + self.assertNotEqual(detached.returncode, 0) + self.assertIn("non-detached branch", detached.stderr) + self.assertEqual(self.npm_calls(), []) + + def test_release_existing_publication_skips_prepare_mutation_and_verifies_integrity(self) -> None: + self.configure_upstream() + self.env["FAKE_PUBLISHED"] = "1" + self.env["FAKE_LATEST"] = "0.1.2" + result = self.run_script("release", "0.1.2") + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(self.real_publish_marker.exists()) + calls = self.npm_calls() + self.assertFalse(any(call[:1] == ["version"] for call in calls)) + self.assertFalse(any(call[:1] == ["pkg"] for call in calls)) + self.assertTrue(any(call[:1] == ["view"] and "dist.integrity" in call for call in calls)) + + def test_release_rejects_dirty_ui_source_before_authentication(self) -> None: + self.configure_upstream() + dirty_source = self.root / "ui" / "packages" / "loom-ui" / "README.md" + dirty_source.write_text("not release metadata\n", encoding="utf-8") + + result = self.run_script("release", "0.1.2") + + self.assertNotEqual(result.returncode, 0) + self.assertIn("outside the release metadata allowlist", result.stderr) + self.assertEqual(self.npm_calls(), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/testdata/acceptance/ncpi-tcga-brca/fixture.lock.json b/testdata/acceptance/ncpi-tcga-brca/fixture.lock.json new file mode 100644 index 00000000..7e829ea5 --- /dev/null +++ b/testdata/acceptance/ncpi-tcga-brca/fixture.lock.json @@ -0,0 +1,34328 @@ +{ + "version": 1, + "endpoint": "https://google-fhir.fhir-aggregator.org", + "study": { + "id": "638f6162-e000-5167-8216-962d86b74a98", + "version": "MTc1NjMyNzczOTk2NTk0NDAwMA", + "digest": "sha256:f7b7cc199961b6bb6f52a61f2f1068053af623cd52eb98180e2ddaf331a689ca" + }, + "selection": { + "resource_type": "Patient", + "search": "part-of-study=ResearchStudy/638f6162-e000-5167-8216-962d86b74a98", + "order": "id ascending", + "limit": 100 + }, + "resources": { + "Condition": [ + { + "id": "00618ad4-3433-56d7-9856-45736525b9f1", + "version": "MTc1NjMyNzkyOTUyNjM0NzAwMA", + "digest": "sha256:e12b52776c5b1dfe3b805a09185b5338a2ab19e1919cfaae2d9d977a131bd2c1" + }, + { + "id": "022a4c85-2a74-5a96-975d-423bab21813e", + "version": "MTc1NjMyNzkyNzAwMzUwMDAwMA", + "digest": "sha256:81dca710c5863e719da19abfe226f318fdaa6a2b98485a30f5a4b55b0de299f8" + }, + { + "id": "034d6bac-8397-5a44-ae91-c691b9f00cd8", + "version": "MTc1NjMyNzkyODMyNDEwNjAwMA", + "digest": "sha256:fcf39d76dbe4efee15451674a86905b691886a4e53a94bc67262062ec8c7d60c" + }, + { + "id": "03b49433-6efb-5929-89e0-253186d8bf29", + "version": "MTc1NjMyNzkyOTk0OTg5NDAwMA", + "digest": "sha256:5e6058dd8394153a97ae80adb04f76d5e6a8e98a8adf0cb3a8cfd2d9e8e96953" + }, + { + "id": "0679af8c-57dd-5e84-8635-f98b115fc6ed", + "version": "MTc1NjMyNzkyNzg0MTYxMjAwMA", + "digest": "sha256:b258464f86166436a9d5ace4f8dd3e46da8691fef4b5340e5389028132933aa6" + }, + { + "id": "07f6cdf1-3b14-5f99-819e-a7cf37570b3c", + "version": "MTc1NjMyNzkzMDI2MTMwNTAwMA", + "digest": "sha256:2527f013efe618ced6029872311540be67b80f00dbaad650bb929f8c9a670dcb" + }, + { + "id": "09384d11-12b4-5e14-aad4-a68845683482", + "version": "MTc1NjMyNzkyODE4NzQ0MjAwMA", + "digest": "sha256:7c29b5d5370a00aaadea28e8e92a3ad0fa8d8d8d5a927e94a2874dc712bb016f" + }, + { + "id": "0960aefb-212a-5e87-92a9-682638e37347", + "version": "MTc1NjMyNzkyOTI0NjY4MjAwMA", + "digest": "sha256:190fa4654d8edd084d3482ab72875c013f4a086c01b8354539193972ec235cb9" + }, + { + "id": "0bdb9235-ad8d-5210-b57a-be08ca1f7ace", + "version": "MTc1NjMyNzkyODUzMTcxNTAwMA", + "digest": "sha256:c6c0bea0d0714806ee2d6574be1529f1a161862afd149de05fcd785c0fe60a13" + }, + { + "id": "0db75762-3b36-5bb2-914b-23232da26488", + "version": "MTc1NjMyNzkzMDI1OTU0MDAwMA", + "digest": "sha256:f7281bb5f4d15e1fd60c699ebf04731a217402921cdf38e6f779b62a66dce93c" + }, + { + "id": "0f178a32-1f62-5405-9f21-11b41561d1c0", + "version": "MTc1NjMyNzg3NDc0MTUzMTAwMA", + "digest": "sha256:085bd2a2f26197d98269ea1f19ea26ccdd7395145605a1c611985e1bbf115f25" + }, + { + "id": "0f4a5107-1297-5e7d-8bf0-7f1b0a001111", + "version": "MTc1NjMyNzkyOTgyODAxNDAwMA", + "digest": "sha256:7fcb8461c485fa114a71017ff43a94364d53be3c67797e3cd542c40df7b36e4c" + }, + { + "id": "147672cc-7e10-51a6-aded-a8b3bbd126a8", + "version": "MTc1NjMyNzkzMDAyNjIyODAwMA", + "digest": "sha256:0c454021bbb4d9bfc7f447a676b184779d0b69f72e7a04005e9abddcda4e9cfe" + }, + { + "id": "18696c26-9ea2-5a70-91d3-ae0a071aa589", + "version": "MTc1NjMyNzkyOTkxMzkxNzAwMA", + "digest": "sha256:748d654233e54fb5d8a95a21f119970bc0bb96c06b427848c78ff6270126588d" + }, + { + "id": "18bdc310-518e-5983-820a-b4592b4d82f9", + "version": "MTc1NjMyNzkyODAyNjU3NzAwMA", + "digest": "sha256:32d638fb4b6f0d0c92ab5016bdeb0f613605807ddde2b0c7425c0ce9500077b5" + }, + { + "id": "1a8434da-db1d-5c1e-bef0-ef70d5445ac7", + "version": "MTc1NjMyNzkyOTkzNDA3MzAwMA", + "digest": "sha256:b2faeeeca841aca80f7ac60c79200c863557080531e59c07c021c19edaa440a2" + }, + { + "id": "1f50c984-54ba-5b93-9233-e27ca8937cdb", + "version": "MTc1NjMyNzkyODI1MDEyODAwMA", + "digest": "sha256:e96116ad257cf41e243aebe80bd760b1f61f8273055888a68d6e69c620324089" + }, + { + "id": "1fe4f3b7-5574-5126-a2ec-94c3a5a4bb9d", + "version": "MTc1NjMyNzkyODk0NDQ4OTAwMA", + "digest": "sha256:a7fed0ee7c89b90e6dc74704c6b257617f32e599536cb916fc865ee72ae08a41" + }, + { + "id": "2132edf7-a375-5b7e-8e54-e78efa7a0d8d", + "version": "MTc1NjMyNzkyOTkzNjg5ODAwMA", + "digest": "sha256:583b1bd2096d80e81bb706d7ff735bd3a97041b656e5f5168dd8e0eee99604b8" + }, + { + "id": "22123f85-9e2f-5cfe-af39-7576259ac9aa", + "version": "MTc1NjMyNzkyODgxMjk4NTAwMA", + "digest": "sha256:2aaa67fc925b7da79202b609861b075f2ddbb9a2a53116fe8855de0e40d85cfa" + }, + { + "id": "23f2f529-329f-5148-abc7-233bfafade63", + "version": "MTc1NjMyNzkyNzMzODc1MjAwMA", + "digest": "sha256:a00662b7ffa9f0821c03aef0d57bfa884301852d9692a87cd80ab3af46b06bf4" + }, + { + "id": "287d831c-89c0-5340-a307-10281514919c", + "version": "MTc1NjMyNzkzMDQzMTUyMzAwMA", + "digest": "sha256:19f9dd21d4e222e82c9677ca3c513481851f03bed93a8cb512aaa794b063fcad" + }, + { + "id": "28eb58ca-004d-56a9-a568-0f0db48605af", + "version": "MTc1NjMyNzkyODk0OTk2MTAwMA", + "digest": "sha256:faae8b1fe7db13fc942645af73d7b3b0f245603301acdb5443199919a2b929dd" + }, + { + "id": "28f3e13b-8bf9-59b0-a5b6-554e6ea8e13b", + "version": "MTc1NjMyNzg3NDAyNzc1MDAwMA", + "digest": "sha256:58cfdc1553c9abf32449d8cf08d918e28fec923f90386330ff94b8459a2522c7" + }, + { + "id": "2b75d07b-7a6f-51cb-9a88-d88bb46c7d56", + "version": "MTc1NjMyNzg3NDU1OTA1MjAwMA", + "digest": "sha256:1272af6c1eecc7de2325a1e2d6f666a4cca69c5bc14032de133ce82f476871cc" + }, + { + "id": "324f988c-6cbd-5e48-853f-0db617154b21", + "version": "MTc1NjMyNzc0OTEzMjg1MTAwMA", + "digest": "sha256:c79006543a7854ff0234361710ba7a0e25462ba3a3e91e2cd29bb03b55790996" + }, + { + "id": "38e144da-3b11-59a1-baaa-45f8c88be348", + "version": "MTc1NjMyNzg3Mzg3NDM4NzAwMA", + "digest": "sha256:0f9bc192cc04f75232e856fc1781ff4e9b2685fe9522885776d1b64e40b67b40" + }, + { + "id": "391c1f32-9b08-5636-b25f-537678f8ba9c", + "version": "MTc1NjMyNzkzMDExMjQ2ODAwMA", + "digest": "sha256:e8219fb5e80b669e22dee48cbffab3a8b305e26a29fd792dee6de6da1d109ef6" + }, + { + "id": "3bdb28b3-7c13-56a4-af47-4c429133ff8f", + "version": "MTc1NjMyNzkzMDI4MDMwODAwMA", + "digest": "sha256:99a77e72b90a653598ebdee8266eecf1300a2217a19316a20d6c9f5cef51f657" + }, + { + "id": "3c51826d-00e7-5af3-a93b-eec13fc748f7", + "version": "MTc1NjMyNzg3MzczMzA1MTAwMA", + "digest": "sha256:e89021629e3e1b877ff8ba595be0617f44ab9ea7bf1ca7a645f9d5a8f04475f4" + }, + { + "id": "3d280738-d3b5-5351-8ca2-1e0276a34958", + "version": "MTc1NjMyNzkyODEwOTc2MTAwMA", + "digest": "sha256:60d860acf14887ca6bc39be0ccc050c2d09afe96bbcdb67acdce1f5f245284fd" + }, + { + "id": "3d8b1851-205b-515d-b599-c01ea2db39de", + "version": "MTc1NjMyNzkyNzc4MzM2NDAwMA", + "digest": "sha256:1cf9dd33e227ceed2c3609c29fb6fe525606527fde8202106ab000868b8a1063" + }, + { + "id": "3daf41e0-593b-5a2d-8174-ebce5afac048", + "version": "MTc1NjMyNzkzMDU1MDQ3MTAwMA", + "digest": "sha256:c17eae0b4ea0105f571bf1b11b7db1b1e8826dd76fff7476335456677b46db51" + }, + { + "id": "4095680e-958d-59ec-84d5-ebe978c864e1", + "version": "MTc1NjMyNzg3Mzc3ODUwNDAwMA", + "digest": "sha256:18aa0f356e8497d667e103c9ca8bee9e17d4e0583850020f2669ca331a010778" + }, + { + "id": "40a8b04a-c30b-5eab-b5a5-3ebee491540c", + "version": "MTc1NjMyNzkyNzQ2NDg3NjAwMA", + "digest": "sha256:4e05bc70c6809a67583660ee59dbc8e5f942c5f00b1cbad864d20ca8cb78b1a9" + }, + { + "id": "410a0684-eff4-5657-adfb-52a4475f6e25", + "version": "MTc1NjMyNzkyOTY1MDMyMzAwMA", + "digest": "sha256:c594bc694e320848a20b2e931a43e282bc14a0a52980d45cbddfda77b6e6159a" + }, + { + "id": "41102600-923c-58fe-9de4-c50e4b2a72bf", + "version": "MTc1NjMyNzkyNjg2NzYxMjAwMA", + "digest": "sha256:fc7bac1292e00290a6e6e49ad8321ed837d2d84019144c96e738a8a0eb4a742e" + }, + { + "id": "423d5adf-8e5b-5e50-8198-ee30db741040", + "version": "MTc1NjMyNzkzMDUyNzkxMjAwMA", + "digest": "sha256:35b05cefda9accce6932f52ad00061bacc056fb0d9e0ad204a2dfd27b696074c" + }, + { + "id": "43f4691d-c11e-54fc-8e8d-afe29196eee6", + "version": "MTc1NjMyNzkyOTA1MTc1ODAwMA", + "digest": "sha256:f162ff53fbc80da16c6c678ae33febb4185d70b34f932d07f635da4e5f7bb700" + }, + { + "id": "46a5c8a2-8ed9-5873-8dc2-6346bac61723", + "version": "MTc1NjMyNzg3NDc0NjMyMjAwMA", + "digest": "sha256:3922177a2ecad0b39cb78a91ebea029751eb179c742f70489cec127051b9f491" + }, + { + "id": "46de9dc9-6698-5103-8c75-68fb0024feb4", + "version": "MTc1NjMyNzkyODkzNTU4MTAwMA", + "digest": "sha256:d5c03b660e31609d6eeb0be35dde9d8fac9cdddbec40a2e109ead7da350330ff" + }, + { + "id": "48362e34-b4ca-519c-a218-08be5b888df9", + "version": "MTc1NjMyNzkyOTcxMDM3NzAwMA", + "digest": "sha256:a9e7e300d9886d7c8237dad4a69c2ec82d0499f854a045bf11ecdf33a3f55acc" + }, + { + "id": "4b2b6a8a-1347-5b5f-9704-5e94bd33f6e3", + "version": "MTc1NjMyNzkyNjkyMDU0MDAwMA", + "digest": "sha256:7c880f16fe2c4945c6ebc661258c77fa050e550f5dc6ea479c81b4063d91b3b9" + }, + { + "id": "4bc69b23-e0f3-5fbf-b8e9-6306ee6b5164", + "version": "MTc1NjMyNzkyODE4MjM3MjAwMA", + "digest": "sha256:a76ee38a81e9970e5b6e732638287110d390f8930116b79a7bf9ee4449b8be29" + }, + { + "id": "4cc05756-423c-5943-ac44-d57d75d5b8a7", + "version": "MTc1NjMyNzkzMDYxMDQzMjAwMA", + "digest": "sha256:fca841519a220b4fd7baf34dbe7b84fdc671cf57ea7f8100eb36aeafb046c5b5" + }, + { + "id": "4e980f24-fb69-570c-a2c1-fc3655e5048e", + "version": "MTc1NjMyNzkyNzAzOTYzOTAwMA", + "digest": "sha256:bc4046fd2950ecf9226dc7def28f320dd190b58e68d3e3ccd035c0c5a8304577" + }, + { + "id": "4f0a4755-857c-5403-8d7c-9730c8f0eb81", + "version": "MTc1NjMyNzkyODk2MDM5NTAwMA", + "digest": "sha256:e89c5654ee8bd32f26e73887b6083931041df10fd6959929b375006fe51b2bc7" + }, + { + "id": "51b79e45-0686-5c98-8210-c3c27ab96a4e", + "version": "MTc1NjMyNzkyNzkwOTQ0MTAwMA", + "digest": "sha256:485c0306abfffadfcdf56e1b03bc0155ec6b6e0e9bafa16611af4aac7f48d31e" + }, + { + "id": "51f0cd94-ecc7-5c9e-ba8f-f48f45fa3ef1", + "version": "MTc1NjMyNzkyODY1Njk3NDAwMA", + "digest": "sha256:04831090528bf713b2fc3ef4728a5769979425ea864903efa8b3ce8574f6451f" + }, + { + "id": "52992f8e-fd5c-5aad-98fa-14a0ceb1a03d", + "version": "MTc1NjMyNzkyNzMxNjYzMTAwMA", + "digest": "sha256:30176685fb82d566402f5ed304b0be99df3b61a24f79b3e7fb6d57c1cf48c4d4" + }, + { + "id": "53e2956a-ec1b-50e7-98fa-61be380cd2ab", + "version": "MTc1NjMyNzkyOTMxNDUwNjAwMA", + "digest": "sha256:14bc4ee192c9d99b684424ff7b41f31dc3e228e4ba23008428eee6f9665c80e0" + }, + { + "id": "56494317-c0a8-5c26-9c50-7070f0de2342", + "version": "MTc1NjMyNzkyNzg0NzkyNzAwMA", + "digest": "sha256:166c4676fba99a21bc66e5fcbea50268721e40ad20f8fa49e80d02f7bd2fe607" + }, + { + "id": "5650952f-79b9-52fa-a5f6-50a707bfa6ec", + "version": "MTc1NjMyNzg3Mzg3MDE4MzAwMA", + "digest": "sha256:8bb23a0d5f9072f687cf0f6487442b6ffd048e4d599e1d13311e3321b7a92106" + }, + { + "id": "5e732c14-99c5-50bc-9d11-914a4cb05cef", + "version": "MTc1NjMyNzkyNzkwMDUzNjAwMA", + "digest": "sha256:183ab5a43ca25566e2184afec626ddeeb76721e3cdb53b4cbcf583cb77447564" + }, + { + "id": "5ede396e-05d1-5a4e-9397-ef645ab1dbc4", + "version": "MTc1NjMyNzkyOTk0OTk5MzAwMA", + "digest": "sha256:ffc9fece68e45d65c971c2de0f49bc1c4036c466255ec399a638810778df8f7a" + }, + { + "id": "5fefd01e-3930-5d71-a55c-e61554d94b99", + "version": "MTc1NjMyNzkyOTk0ODY5MzAwMA", + "digest": "sha256:4e646aab757adf35668f72bb5929b2df8f744a5b979ca0756f2900a027ea7b02" + }, + { + "id": "61333771-4e9d-5a1b-a150-1fbfb260522e", + "version": "MTc1NjMyNzkyNzM1Mjc0MzAwMA", + "digest": "sha256:6b505229f6b41ef26f213bc235f52be8506176bda73441e713be3979b4b3d4e1" + }, + { + "id": "63f2cf2e-a360-5e2d-bf46-a3abfb4d1a0a", + "version": "MTc1NjMyNzkyOTE4ODI4OTAwMA", + "digest": "sha256:3d6956a1c5146af405653b6b4bc5da2735e970e20cdb725105bde4e901d3d97f" + }, + { + "id": "643007f1-78ec-5b9d-802c-154870a3912a", + "version": "MTc1NjMyNzkyOTE2NjM4MTAwMA", + "digest": "sha256:e4226225054109b03b695f74e52091d6b47dc78a8cffc20069218ea1bbfd9a55" + }, + { + "id": "6506d82a-a25c-5081-a09a-efef8bb3257c", + "version": "MTc1NjMyNzkyODE4NjIzMTAwMA", + "digest": "sha256:25f3ca0eba514027404d12297b44798a617200de5a83effa518ec3579170ebc5" + }, + { + "id": "659ec4ce-35be-550d-be7b-175c2082dfcb", + "version": "MTc1NjMyNzkzMDUwOTQ0OTAwMA", + "digest": "sha256:70ae542afac1c8688a9a82ad6a400399433ef902eedc9941316cd93e3b16330b" + }, + { + "id": "66e73167-d03f-51fb-ba30-4ed6518ea622", + "version": "MTc1NjMyNzkyODQ0MzMxNjAwMA", + "digest": "sha256:2e9aa647425962e9e416a8aeda9e797e95ea278e65e718decc6ed3f30d38c006" + }, + { + "id": "66efafce-78e3-5a39-970f-6bc30ae51e5d", + "version": "MTc1NjMyNzg3Mzk1Mzc5NDAwMA", + "digest": "sha256:4d180e5b2ff32e382d5ab4220dbe9026f55dd5e087449aca55300b11571023e3" + }, + { + "id": "6745bc1a-bd8d-5a47-9d22-6be934935d51", + "version": "MTc1NjMyNzkzMDM0MTcwMDAwMA", + "digest": "sha256:f93c3f2e86d743d4ca8e9bb7c50f7ae2c8fa2312e30ab69063f1c1db8b4482f0" + }, + { + "id": "6767618f-6077-544d-93c6-0ef8daf63558", + "version": "MTc1NjMyNzg3NDg0Njk0MTAwMA", + "digest": "sha256:06fd10814128eb6c2bfeb6aabb5a93a3d9006d54c0bf46e2f157cec591d45a5b" + }, + { + "id": "6e340ee4-ef09-5c1c-b4e1-85c2aceb6226", + "version": "MTc1NjMyNzg3NDcwMDc4MDAwMA", + "digest": "sha256:80b2f1d44b1927bdf83813114434449277c6a2e29690569a5d963ce47f9f1091" + }, + { + "id": "723533f2-6e21-5af9-b7b7-742d105ae051", + "version": "MTc1NjMyNzkyNzExNzgxODAwMA", + "digest": "sha256:1cec4f81b30955390506881925ffadf42f8af4095947384a6dac4653c93b95e7" + }, + { + "id": "72bdbb88-8188-5a32-b0b0-18f4a7738260", + "version": "MTc1NjMyNzg3NDc0NDk3MDAwMA", + "digest": "sha256:47082914f3e1f05a21371854848d89474d9fe567ae2f94ecb77b196e6481d57d" + }, + { + "id": "75d29360-d02a-5506-8425-36a12e4c06bd", + "version": "MTc1NjMyNzkyOTUxNTAzNDAwMA", + "digest": "sha256:ab1cbc4fb9b9271a1f62fc783110ded002653a3bcd311ffcc0d0594e44d8f9df" + }, + { + "id": "76991861-90d4-5392-9267-857aa6d46472", + "version": "MTc1NjMyNzkyODI1OTk2MzAwMA", + "digest": "sha256:7e520834ec543ee05e8b9dd9d27ffeccf268ab27e0822d870d34af7775e5c5c7" + }, + { + "id": "7b51ea4d-c2e4-5881-a789-ec23863f76ec", + "version": "MTc1NjMyNzkzMDY3MTI1OTAwMA", + "digest": "sha256:dd26d42a203434c649a2240699355bc7833b6017ac32baadf42e865a76e418e2" + }, + { + "id": "7ced9519-0bf4-57c4-945b-6f9f624125aa", + "version": "MTc1NjMyNzkzMDYwNzExNTAwMA", + "digest": "sha256:7c91f7ac458cb3c8c2de3335b3255dc8c7ad568d193b262f128442756d9eba7f" + }, + { + "id": "7d07ae0f-f4a6-5556-880c-efcbe2167fd2", + "version": "MTc1NjMyNzkyNzcyNzc1MzAwMA", + "digest": "sha256:62cd49ceb6d05de87c9708ed41338a389aec6395bb30b6c3927f4cb200a8aa2e" + }, + { + "id": "7eb5651e-f758-5f5a-bc64-6637292bbb32", + "version": "MTc1NjMyNzg3NDczNjY0NjAwMA", + "digest": "sha256:c14834e08db229779fce2b7610feba8767143ddb298b60a29dd0befbfeea76bb" + }, + { + "id": "7f492c36-05a1-56cc-a7a9-b07a9de9529a", + "version": "MTc1NjMyNzkyOTA5MjkyODAwMA", + "digest": "sha256:6e36dece6edeaa91c6edcf9110b82677e5df1185ce535438938b8ba77df25d20" + }, + { + "id": "812e977c-a53b-5c6e-94b0-b6400a5e814b", + "version": "MTc1NjMyNzkyODIyMTg1ODAwMA", + "digest": "sha256:5a6eaf02947d5af3144eace4e6e5a8a68d3f004fcabe3c8b9de116f081540982" + }, + { + "id": "8514ce86-90c7-5a23-9249-42a8c6dbf96f", + "version": "MTc1NjMyNzkyODQ3MDA0NDAwMA", + "digest": "sha256:0183f1ad4fff84be71ed26d3657b2d6b9659ab93934a4fe1ebfe24853f023dab" + }, + { + "id": "8685233d-ff9b-5814-ad9b-72313a93455b", + "version": "MTc1NjMyNzkyODgwMzIxOTAwMA", + "digest": "sha256:63b17b068dfa7990449a58323a21d7ef3e7dc93ec32bd574e774b341e0c64e1f" + }, + { + "id": "898ccf9e-2426-5409-92cb-5c089c5bea82", + "version": "MTc1NjMyNzc0NTQyOTEwMDAwMA", + "digest": "sha256:7fba388da17ec363db0ed046dd3b72e4c2619f6009b9538052a231015adc05e0" + }, + { + "id": "8a7bec72-e94b-5188-9fbe-b62a29fd54b5", + "version": "MTc1NjMyNzkyNzg4MzEwNjAwMA", + "digest": "sha256:2940be0bf75f225104a3578d19b241e0922cd2ec56ea02a04ae9fe4ece3efd74" + }, + { + "id": "8c70d018-f64b-5b89-b39e-136a2770c1fa", + "version": "MTc1NjMyNzkzMDY5NTM5NTAwMA", + "digest": "sha256:8ee536765501bd9b54d4394bfa497564f99e1e909da5fea5e05a7f33d335d984" + }, + { + "id": "8eaedeb9-fe3e-50ca-88e0-5ffd879f4051", + "version": "MTc1NjMyNzkyNzg1MzE5NDAwMA", + "digest": "sha256:af652ba5e7084df7b8c996eeb5c8169bbb4c873de40a1d2b6a7b520f78bced97" + }, + { + "id": "8f94046b-f88d-5249-81ab-5aab5986b9b0", + "version": "MTc1NjMyNzkzMDM5Mzg1OTAwMA", + "digest": "sha256:f6b2b9021e555f49e8947ff6eb39f74bef16ee1164684df1f508a328d75a8493" + }, + { + "id": "912bb8b9-527d-5af3-a216-00afb4b81b85", + "version": "MTc1NjMyNzkyNzQ3MzQ5NjAwMA", + "digest": "sha256:9307f8c7f99a73d7aa46e8c2c080ad48baab0b114a951e1a45cfe1981475d8fe" + }, + { + "id": "9317d164-474a-521c-a360-348b9b0222b9", + "version": "MTc1NjMyNzkyODY2MTMwNTAwMA", + "digest": "sha256:e04de31a2d7e1bd5d7c4e4b03ed34c44bd15f80e6e62be162c8cd06cfb4065bc" + }, + { + "id": "933aea01-6446-55df-aa34-436a773712b9", + "version": "MTc1NjMyNzc0NTgwMTE4OTAwMA", + "digest": "sha256:e84a52d05efa790bc8ccd6f2840facbb7b896effcbdef4fddf1e3d70636ea6c9" + }, + { + "id": "979369b6-9368-582c-b582-c1e5ecbbc5fd", + "version": "MTc1NjMyNzkyNzEyMTk0MzAwMA", + "digest": "sha256:75200993013741627b5a5b74b45148d8d8189e22ad3936dfeef12c5abe69ec6b" + }, + { + "id": "9bd29148-7ed9-5a15-a1dc-9c96dc4de0a1", + "version": "MTc1NjMyNzkyNzA3NDA4MTAwMA", + "digest": "sha256:e51684ead634c3c8dacf64c891a3da21f2a2c89d7d1f3df9213f18d0d24457ca" + }, + { + "id": "9d707ae2-85c2-56e9-8d91-c441d725ebd8", + "version": "MTc1NjMyNzkyNzUwMDE1NTAwMA", + "digest": "sha256:706eadc75a89a53552f1b865b232be95f2b0cfbaa7456b8a289f2c3d9c8e677e" + }, + { + "id": "9da1e50f-b5b8-540f-9d3d-a4efe6659c38", + "version": "MTc1NjMyNzkyNjcyNTg5NTAwMA", + "digest": "sha256:28b6c9cf9a7e89cdf72673544e7935a028b3ed6ee2a7117fa83dbcdd94a74579" + }, + { + "id": "9f96dd69-a77f-5a8a-a2ff-69ae35d2199f", + "version": "MTc1NjMyNzkzMDI1Mjc4NDAwMA", + "digest": "sha256:f39d3ff555eb9503f323b0e2c0c77275f7e6422babb9cbf0cea097c74222ec9a" + }, + { + "id": "a3a605fa-0362-580f-9aa9-d330bab1c035", + "version": "MTc1NjMyNzkyNzQxNzM0NDAwMA", + "digest": "sha256:3612a3faab4b2395601b23f556bf807571f76eb2209550e5682bacfec7173f3d" + }, + { + "id": "a7e1eaa0-072f-59b1-849f-b08e41cde656", + "version": "MTc1NjMyNzkyNzE4NTA0NTAwMA", + "digest": "sha256:74b5457d099fb708ae61ae331c380ac9a375e9617e929c803fb5e95163c1682c" + }, + { + "id": "a82319b7-1445-5343-8c7f-8da13be9caed", + "version": "MTc1NjMyNzkyNzYxMzgyMDAwMA", + "digest": "sha256:98914a2f9a85134ad60ce043913abfe48f4d24116a8e7819754e7ddd9fd2c0d9" + }, + { + "id": "a9c8936e-014a-523d-812a-079bf03704e1", + "version": "MTc1NjMyNzkyOTExMjI0MjAwMA", + "digest": "sha256:7cb981d4e74599bd79be1c0d641428340290f7d4394ae4533cc17b05221fd11d" + }, + { + "id": "ac949cec-c629-5e3a-8936-b01361c574b1", + "version": "MTc1NjMyNzkyNzY5NTU2MDAwMA", + "digest": "sha256:1ffc602755f415c5244449f656ee40b9083730807a8d511a0990a56d8539ef7b" + }, + { + "id": "ae53f62d-8f4f-5f50-9205-9835c1abad07", + "version": "MTc1NjMyNzg3NDkzNjQ5MTAwMA", + "digest": "sha256:53884c3aef59aea8444f89cab2296b1d8d795546ba6d591bb15b2961072d5b77" + }, + { + "id": "ae7fdf9f-200a-5cc2-8aa0-46419a3e56f4", + "version": "MTc1NjMyNzg3MzkxMjUzOTAwMA", + "digest": "sha256:5b937ccd8f6417f06fd7fe2765aa16b7ee94933b308abaf50efc1d6666fb7664" + }, + { + "id": "af7938cf-aa43-5c42-9d06-690865d8f9b1", + "version": "MTc1NjMyNzkyODcwNzIxOTAwMA", + "digest": "sha256:30bb1f84034b25317665087ac56b3bc9598985fda7837ce468368fa51c153dec" + }, + { + "id": "b12a2829-a0fb-5960-8111-0ba4c054786b", + "version": "MTc1NjMyNzkzMDgzMzI2OTAwMA", + "digest": "sha256:90049f8b30ce65671832990bec76d2d9a8f023e7ca03eef42b90d7eac9787972" + }, + { + "id": "b2e419c2-6188-5e62-afd3-4e0e54aa1ab9", + "version": "MTc1NjMyNzkyNzYzOTU4OTAwMA", + "digest": "sha256:f6e32c26a1011ebcaa220f952f5660bf0ccf48406a89cfe5083fa02c9f585367" + }, + { + "id": "ba66b558-8e11-57ea-b80b-af28e8f40320", + "version": "MTc1NjMyNzkyODQ5NzIwOTAwMA", + "digest": "sha256:2d3b96c975d2fc02f1376fe58759226473f5ab42977dac26b3a6916e9f7a42c3" + }, + { + "id": "bd22eee1-9a0b-5627-8405-dbd04d61e661", + "version": "MTc1NjMyNzkyNzM1ODk4MDAwMA", + "digest": "sha256:b3828944766076d1915245f5933ba5c9b455b4da9a95cba370ed3e58c2eb1368" + }, + { + "id": "c3c98444-47cf-57d1-a324-4a8eaeb5881c", + "version": "MTc1NjMyNzkyOTMwOTQ3NzAwMA", + "digest": "sha256:e7e87517d1bd77049ef43fc9432b6143889f716062609d60d343833bf338167e" + }, + { + "id": "c667909c-1993-5814-9a23-73eacc51ba83", + "version": "MTc1NjMyNzkyNzc1MjkyOTAwMA", + "digest": "sha256:12d32a36426d9f780ccc6e1242b3427b4775cbe42d6030f0e504d3e9b5166147" + }, + { + "id": "c70690e6-7663-5a25-9bd0-1e0c04099c54", + "version": "MTc1NjMyNzkyODAyODI4NjAwMA", + "digest": "sha256:c03eb6b50363236f5619072f4d9e3b1b11f03b3a0291c17b7abf5e9a077e125b" + }, + { + "id": "cb43e1ad-19d4-5120-96dc-255a31ce2e0f", + "version": "MTc1NjMyNzg3NDY1MzYwOTAwMA", + "digest": "sha256:c82f2a73c7309b120865eabd3ccdd96ccf8fd8ecde1dde24c5a23141e2a3f304" + }, + { + "id": "cc04542e-57bc-57ea-a1a0-7467d807e90f", + "version": "MTc1NjMyNzkzMDgyMjU2ODAwMA", + "digest": "sha256:98643165799c7613684c4f374a783c389faf9a948b54cbdbcce869a9f8741c8e" + }, + { + "id": "cc1f271e-bb0d-5dee-9833-133c2d2b4d8a", + "version": "MTc1NjMyNzkzMDM2OTU4MjAwMA", + "digest": "sha256:2b14143a31a35a5c05936c7046ff0754e1ca3afc5a47f179c037dea62fc6e5d4" + }, + { + "id": "ccf37d14-1174-58bd-a96e-3d1928233503", + "version": "MTc1NjMyNzkyNjY4MzQ2MjAwMA", + "digest": "sha256:3f71d5a09e14b1e310bdfc969d9a4b4133025ec515df70ceaf089c741680f9a7" + }, + { + "id": "ce43660b-3670-58e9-8b37-f64ee4fb053c", + "version": "MTc1NjMyNzkyNzk5ODU5MjAwMA", + "digest": "sha256:802aebf1f80dec72df5fe6d84b7b8a17d7b14196355b5a79532d0b87ec48d595" + }, + { + "id": "d101e322-534b-5a6f-a8ea-e6442dc2b562", + "version": "MTc1NjMyNzkyODc5MDM5ODAwMA", + "digest": "sha256:ae3e752174273102db4d2db7ae23137f2cd39340031db7571b15589b92c145c7" + }, + { + "id": "d14f7655-35dd-5a9b-97b9-def790215bfd", + "version": "MTc1NjMyNzkyODA4MzE5NzAwMA", + "digest": "sha256:b3adcc118f7f35a027561b43d1920e73b38429151dcd328e174e0f24417252f3" + }, + { + "id": "d192911d-a6eb-5460-b849-972e4dfd3289", + "version": "MTc1NjMyNzc0NTgzOTQwNDAwMA", + "digest": "sha256:0afa08052dcb522b1929e96843910666654106ba423b5961b675a24908c75821" + }, + { + "id": "d6c23b77-3dbc-5c44-beac-94db73e8184b", + "version": "MTc1NjMyNzg3MzkzNjc2OTAwMA", + "digest": "sha256:3f45c77e6d63fd1155969eb5d064932e6278d524b0f3e002fb761fcdb097db73" + }, + { + "id": "d787ba15-116c-5099-a32b-d5b9890ba995", + "version": "MTc1NjMyNzkzMDYxMTgzNTAwMA", + "digest": "sha256:3d794dc8f6775308ffd812731c7cc790e950666d7ba071e0d49aca0ca441f567" + }, + { + "id": "da832a9c-ca52-55a4-92b9-9f9d61d77fe4", + "version": "MTc1NjMyNzkyNzYzMzQxODAwMA", + "digest": "sha256:78c8bb28c72803ac0fe208acc5db9a5399c4146d7bfc1b4fa4b9e1b53b547379" + }, + { + "id": "dcc28e7d-8748-518f-a28c-1f338209ec5f", + "version": "MTc1NjMyNzc0OTIwNDU0MjAwMA", + "digest": "sha256:c57cdd44a7e6863c961c2db34243c5fddf6e0ba6e396e786b545b7095388923a" + }, + { + "id": "dd3df5ff-8679-5e3b-acc8-2cb3d3c23045", + "version": "MTc1NjMyNzkyNzA3MzgxODAwMA", + "digest": "sha256:6a003a0c4e639f265a45682c42fd1cdba3e25bf511c1e452ec4d7532c2e111b6" + }, + { + "id": "dd6a8eeb-69ea-57fd-926d-6e2f5cf4da79", + "version": "MTc1NjMyNzkyNjk0MDMzMzAwMA", + "digest": "sha256:1d257f930db61dc44cbe78b5663c107c435f80d2fa39fddeb9e8a5d5889d9552" + }, + { + "id": "de52adef-1634-5d1d-bf76-5b829e7c3a3f", + "version": "MTc1NjMyNzkyOTk3MDQ3NDAwMA", + "digest": "sha256:2397f18df601110949ab30badbef0b682cf5222c21c55392c7d44911d2904cd0" + }, + { + "id": "e10f81a6-898c-50b2-b539-d2ce34cd6990", + "version": "MTc1NjMyNzc0OTIwNDI4MTAwMA", + "digest": "sha256:603d2b92e4263f4d8df8612112349817bd38b7ec74a6ed8465f3d58102374f11" + }, + { + "id": "e1233ef7-32e3-57d0-b88a-3e51c3721b13", + "version": "MTc1NjMyNzc0NTA3NTI1OTAwMA", + "digest": "sha256:fee6b457463a917fc51b7f4605c2c2b4b0a5ecef47e18a64b8597b9e7950183d" + }, + { + "id": "e17701c1-72f1-5807-bc46-d7d72c3ef840", + "version": "MTc1NjMyNzkyOTM5NDEzNjAwMA", + "digest": "sha256:044a44b18d1cf306387e9008d964614a4c444c913a02f97a679fcbaf5653f3ff" + }, + { + "id": "e4d464f0-f63f-5f2b-a529-b7ea43821680", + "version": "MTc1NjMyNzkyOTU4NzM1NDAwMA", + "digest": "sha256:8058cb64b7d434e14495aaf96a0c1ff06f6058090a0806c4dd15deaf11408963" + }, + { + "id": "e5215eb5-3bd5-5923-808f-8bae45190278", + "version": "MTc1NjMyNzkzMDYwODY5MTAwMA", + "digest": "sha256:d39bed12b0f5d996e6b642626039e1c0acb373111550362097d08da56e95c464" + }, + { + "id": "e5c7595a-4e88-5382-a088-663786d954e7", + "version": "MTc1NjMyNzkyOTU0MTI1OTAwMA", + "digest": "sha256:b3549ff9029f7de2bc2a32a6711a4f769f3ffd1ca370c89bc41acb9d66366128" + }, + { + "id": "ecb0e37b-69aa-5b68-8929-77ae0473440d", + "version": "MTc1NjMyNzkyNzI3NTQ2NjAwMA", + "digest": "sha256:05e376d1860737ecc934d1ba03a422d51e783e033d0893371b13f00d61b45ba2" + }, + { + "id": "ecefb88b-d034-546c-9173-6bbc3e06a4eb", + "version": "MTc1NjMyNzc0NTExOTE4MjAwMA", + "digest": "sha256:7a5cc2c640ee39c5fb15c32b46f364b732d31257a65baf9956915a35d43b5b24" + }, + { + "id": "ed8d9694-12b9-5481-b3db-5434cb10c374", + "version": "MTc1NjMyNzkzMDc0ODkzNDAwMA", + "digest": "sha256:ca2dc82274f02966cf1b2ed66dbde96c5351badcb9529dc94a3cefd373405513" + }, + { + "id": "f7b76217-3baf-53a2-a0c5-8246dcbabae1", + "version": "MTc1NjMyNzg3NDEyNTIyMzAwMA", + "digest": "sha256:a6ed5618e3537a7479f2489c383f435efcbe73ffce31f57031bcea13c56ef0b3" + }, + { + "id": "f7d07fb2-9e01-5106-9538-2eac8d426db9", + "version": "MTc1NjMyNzc0OTkyMjI5MjAwMA", + "digest": "sha256:686fef54d816c986776df3cdd70b8470084d10e5b9f2339a449178cff47201ca" + }, + { + "id": "facc005e-ad7b-5b40-9842-071df1f8af47", + "version": "MTc1NjMyNzkyOTk4NjAwNTAwMA", + "digest": "sha256:b68a76daf3344e4fffdf0667c27f460ee72b526f4a9895bb983b9bd3bc9b832e" + }, + { + "id": "fd25db6a-da90-5195-8a5f-f4e0f00f2d78", + "version": "MTc1NjMyNzkyOTEzMjE0NjAwMA", + "digest": "sha256:feef672643d560cdf9bdca160af1aaa9ff688ca482b63241448336c5f41d93c5" + }, + { + "id": "fde87ba2-c39e-51a6-9d97-8ff8fff0cf53", + "version": "MTc1NjMyNzkyNjg3NTc0NjAwMA", + "digest": "sha256:799eebc34cefb22337ebefe9da8db1c0b3f17b2183b527a2a4ff7a0d5be4baf3" + }, + { + "id": "feda0517-a5a8-5c01-a66a-5abe3b758181", + "version": "MTc1NjMyNzkyODMwODU0NzAwMA", + "digest": "sha256:11c648c5f5d0ab0c54b0977f20ee67052b6fbca1113479fff10925ec4eb2cf2f" + } + ], + "DocumentReference": [], + "Observation": [ + { + "id": "000b6a1a-d5b4-53ab-b393-6a77fe70158b", + "version": "MTc1NjMyODE3NzY4MDAyNjAwMA", + "digest": "sha256:92abf7f946db1435efb005520c298f361742a715a46b260a4fc308f781d0bbae" + }, + { + "id": "000cc233-a845-5cff-b3fa-b283d78e909c", + "version": "MTc1NjMyNzg0MTIxMDMzOTAwMA", + "digest": "sha256:15eb36da2f933eb93684b2b45f3998956b2ae05ab517206d4fade1a9b1c1d324" + }, + { + "id": "002b1293-8cea-5306-90ef-c6451a64e166", + "version": "MTc1NjMyNzgzNTIxNDM3MDAwMA", + "digest": "sha256:94a0840c9ebbaf08f2bd95643ef5288a3e1f7a04ed8fa962013b1195dd3e417c" + }, + { + "id": "0039c704-b513-51a8-80a8-b05f6708f6a7", + "version": "MTc1NjMyODA3NTgyOTE1OTAwMA", + "digest": "sha256:cff63942b3cbd65f1731a9f3eb9393d9f3a0824360cbc86c2ede2613dff824be" + }, + { + "id": "003d73d5-7372-51f2-ab7d-d7b29005aeba", + "version": "MTc1NjMyNzc5NTIxNjA3NjAwMA", + "digest": "sha256:351a81ca544a8792d4b9b425fe5d2238fb297c85e14943f59a2423add0cedce6" + }, + { + "id": "005ec153-b188-5047-a4c0-d83ef7fe9927", + "version": "MTc1NjMyNzc0NzcwNDEzNzAwMA", + "digest": "sha256:f19ebf12e5eac92ce7d6693619157117d94c60f387c65a8558515b046d9a387b" + }, + { + "id": "00606667-f7c0-5cbb-9db8-9c76c7785e9a", + "version": "MTc1NjMyODE2NjkxMTYzNDAwMA", + "digest": "sha256:8e3b8cda0ac83e6b1de497ba6be8d7669589e1c1022b221bf1c136996ea420eb" + }, + { + "id": "00682768-8264-5310-99c8-ffc934d46148", + "version": "MTc1NjMyNzc1NzUyMjY5MTAwMA", + "digest": "sha256:6fc6198f76b8bbfee249c64cd3d92217aeb93fb1f0854beeeb6295b1a11c14ad" + }, + { + "id": "006e9045-80b3-5779-8ba6-e70fd4046d1b", + "version": "MTc1NjMyNzg4MjA3NzQyMjAwMA", + "digest": "sha256:3a6ccc80a09b7869003243ed10aa83d73c446f9b0a04aa11a8a9d72970b8c2d9" + }, + { + "id": "00716caf-ef74-5f7c-be34-32d04e886045", + "version": "MTc1NjMyNzc4MDg5OTk0NTAwMA", + "digest": "sha256:b6aca4e1b02ef13a2229d88ac65a473330a6ad6d7b419774d3acb8c8f45cf53d" + }, + { + "id": "007b1761-7589-5373-adc7-bf14d53c76d9", + "version": "MTc1NjMyNzg2MDI2NzQ0NzAwMA", + "digest": "sha256:38d5b3c0671e69e3e2735fe163281ed1dbcd3c9d349af8379d92a3b981f7f099" + }, + { + "id": "007dcfd0-f82e-5bb6-9585-0b5132a47983", + "version": "MTc1NjMyODE3Njc4NDQzNTAwMA", + "digest": "sha256:aa27251db3874d30b37a60b92fa9ccb23be42a32d60a0c3d9d93198e4e308323" + }, + { + "id": "00afed72-5743-51ec-ac35-c102c89d4757", + "version": "MTc1NjMyODE2NDkyMzA0NzAwMA", + "digest": "sha256:2a9b204397191655ffc569638ff770be045f5f966d5e4289f5aeb99e0d8d4926" + }, + { + "id": "00b882ae-f20a-54c1-b222-152cc92493f0", + "version": "MTc1NjMyNzc1MzYzMTMzNTAwMA", + "digest": "sha256:434f01ea5d678859b51bd6dbea04ec1744ffdb6a9a9f35deec803af430a32223" + }, + { + "id": "00fba232-6533-5ca5-865b-52d0c2c4564e", + "version": "MTc1NjMyNzc0NzY1MTgxNTAwMA", + "digest": "sha256:58202e9d7e6f6f39f870de6f3f4ee0acde14bbb29720f21030fdf6a9aa8b7028" + }, + { + "id": "0101ccd7-6996-5db6-b3ec-e67fd27afabf", + "version": "MTc1NjMyNzc5NjA0ODA1MTAwMA", + "digest": "sha256:757e9f80548d935c4c25753b7b0628f9197b91d2cdee97a1da68b2f3fbc59444" + }, + { + "id": "010d101b-2180-5d37-84ab-e0ae01924e70", + "version": "MTc1NjMyNzczODAzODY1MzAwMA", + "digest": "sha256:95dc7c6454f5e472e31def6e674d4529f11ab091ee71caf868d64b052bc9de2c" + }, + { + "id": "0119e7fe-2420-5bc6-ad4f-37f6368f0b40", + "version": "MTc1NjMyNzgzODY4NjQ0ODAwMA", + "digest": "sha256:858f13319a72743ce654496582d6087ae3b11a9b0437b12fe2802166428156b7" + }, + { + "id": "011e5f5d-c387-5fd0-a259-c4a908a243b1", + "version": "MTc1NjMyODA3MTU4NDI2NjAwMA", + "digest": "sha256:5c12a2bcfabe539c9f3332e707e1a3632cfd769b0e4cb53d3b77391afa558103" + }, + { + "id": "0127ce70-20a0-5cab-a419-739de721fce1", + "version": "MTc1NjMyODA3MjIyOTEyMTAwMA", + "digest": "sha256:9fb26c277161ab799721a33845fc456219ebfb5764a1322867a00c806c82060c" + }, + { + "id": "012ec656-9eb0-5807-8aa7-ef225864a837", + "version": "MTc1NjMyNzgyMjg3NjI3MDAwMA", + "digest": "sha256:6935fd1737758d5be12818e540c4668ea389bad5c34245ca912c64b140e457ec" + }, + { + "id": "0138b9f9-d85e-5427-87d9-6e1e6fa7487d", + "version": "MTc1NjMyNzg1MzM1OTE3NTAwMA", + "digest": "sha256:4b36f95ca366529a279abe71d9ac6b0d5b6f477f31d1abd14ebecb1757949ef0" + }, + { + "id": "014ed983-998e-58e8-8ccf-ffbf8754dc37", + "version": "MTc1NjMyODE1ODE2Njc1MzAwMA", + "digest": "sha256:38ac33b36c214d5b279f59dc1bc015de60ce2f06c367374351e93f1a9dd71e00" + }, + { + "id": "0160e012-773e-5860-8649-9cf0c38babab", + "version": "MTc1NjMyNzg2NzU3NTQ0MzAwMA", + "digest": "sha256:520fa35d3e26286f8d0c689f5cfbfff80e8928b88b6e84dff4768e76dbde7d8d" + }, + { + "id": "0162ccf0-43ac-5fd5-8250-3c6347d9e08e", + "version": "MTc1NjMyODA3NDU1MzQ5MDAwMA", + "digest": "sha256:84f1d4fd0b2289551cea4ced40ea60963260a0e3d5190e8db8df13d5aedb8724" + }, + { + "id": "017575af-03ab-58ce-9304-fe8563ae081b", + "version": "MTc1NjMyODE2OTk1OTMzODAwMA", + "digest": "sha256:616ee4e73642cf6699672890b3b337c5d47cad7c7ac906d2365f95a220f8cd45" + }, + { + "id": "0179a225-8d53-5664-9bfb-c8ac4f4ed351", + "version": "MTc1NjMyNzc0OTkxODY2NjAwMA", + "digest": "sha256:a2b0221e26bff5c7bc9202fe4476fea2292c324f6249e388f358bcb70d6c45ca" + }, + { + "id": "0187efb4-aa8b-5df6-b0d6-aab874d9e0e2", + "version": "MTc1NjMyNzc2NjQ2MDg1NTAwMA", + "digest": "sha256:758a7a90423e2f24a15c24c6a54b0e41970a54616bde4be27532e0e552ad75c6" + }, + { + "id": "01dae8b1-aecb-5fd2-bdfa-d36a33008c47", + "version": "MTc1NjMyODE3NDM3MzIwMjAwMA", + "digest": "sha256:4241a3fd48e6bb6392e375145fdbdf7b13b4f2b3dac996c4923a9cc7c0866cc6" + }, + { + "id": "01f11380-7136-518e-9ed8-283753e6bbc6", + "version": "MTc1NjMyNzc5NTM2MjU2MTAwMA", + "digest": "sha256:f2aa8c5162865dc2e2ff0aea583fd7cd125797342a480df856fcbb1528b2e191" + }, + { + "id": "01fd7197-2af5-5120-b0e3-5d99df929e4d", + "version": "MTc1NjMyNzgxMjc0NTc5NDAwMA", + "digest": "sha256:d274d0918aa10fcc177ec22fb6872584d290392485a75140941fd576983feae8" + }, + { + "id": "021a744c-8fcf-5f47-9642-b938e6793d07", + "version": "MTc1NjMyODE4MTg5MzAxODAwMA", + "digest": "sha256:e38ffbbc0db8bc9ee699ae92c144853bee485499783fb4220982f82df2020f53" + }, + { + "id": "02248f85-e624-58d2-b5a2-df9a87e6ca2c", + "version": "MTc1NjMyNzc3MjYzNzM1MTAwMA", + "digest": "sha256:9f87bc0dd8a352b08381be7a25ba69e51b27a42202c92b916b96b6ee5c2c5e9f" + }, + { + "id": "022cb122-9ddb-59ac-9556-8d14a3c2154b", + "version": "MTc1NjMyNzc3MDU0ODU0MTAwMA", + "digest": "sha256:3a51a0c445321aff02e665e02a2de9c202c334379c3a5bbc0a2e6f6415d9cb45" + }, + { + "id": "0237846b-6c3f-5a41-b257-7b9a9b90163e", + "version": "MTc1NjMyNzgzNjM1NTczNjAwMA", + "digest": "sha256:f9bfc65fed0383ae28c421073470bd3aab533c33085953624b65bd5656a5b4e4" + }, + { + "id": "0248346a-a55d-5b35-8216-512d90197bf0", + "version": "MTc1NjMyNzc4MjgyMzMwOTAwMA", + "digest": "sha256:f31736b4ef865ff3651d66db281bab4bd7d1d8e059a308313990f3b36b05924d" + }, + { + "id": "0250313e-bd8a-5ae6-a273-d731246bf3b4", + "version": "MTc1NjMyNzc5MDI1NDY5NDAwMA", + "digest": "sha256:371df8d22bc53f24afd032c7bfd71566a2c5b6d098249070a6a0261e2886f23c" + }, + { + "id": "0256a22c-eafa-50af-aa7d-79dfe74d42d2", + "version": "MTc1NjMyNzg1NTgzMDUwODAwMA", + "digest": "sha256:4aa82813fefee687572bf2f5c8703dba7585e67cbe664286c18409457900cc1c" + }, + { + "id": "028c1e02-6e0e-59f7-b699-864a51ff0b08", + "version": "MTc1NjMyNzkxMDkyMzk1NDAwMA", + "digest": "sha256:a38670e2c989fd7797b4b75019ae749b7062fa5066e63420ddd8ac599ff85fe9" + }, + { + "id": "02925dfe-3dbc-570e-b7e3-88c56be70b18", + "version": "MTc1NjMyNzc0MjM2NzkxMDAwMA", + "digest": "sha256:581ca7a62dc8b4acece973fb1ea2bf3d831032e4a443bf44e0213dfa8effad2f" + }, + { + "id": "02a6146d-a050-5dcc-adce-e89437a11ffa", + "version": "MTc1NjMyODE1ODE5MzkzNzAwMA", + "digest": "sha256:89bdd4387289ad7a200ce34fee0e8830b784dfeaab1b1347bee42dcf788de448" + }, + { + "id": "02b02f08-fcb5-524a-acaf-f01054fbe2f5", + "version": "MTc1NjMyNzc3OTkyMTYyNzAwMA", + "digest": "sha256:ccc8f75572bca6858a3005b4770dccf7c8dcedf6b160f855fb8cefa2bb91f21e" + }, + { + "id": "02b3de53-9dea-599c-93d1-97c7e63f2522", + "version": "MTc1NjMyNzc2MzcxODY3MzAwMA", + "digest": "sha256:5c6a7efc443387a8b6b719e9f13789055e71f97b13c827ca66ddeef4ede628f5" + }, + { + "id": "02cf850b-53e3-54cf-80fb-d93d6ce0ffa0", + "version": "MTc1NjMyNzgyMjg4MzIwMzAwMA", + "digest": "sha256:eadfea167bd32b002531ac0c658dda63f42b3f746f78b7b0574e448e6f9e5824" + }, + { + "id": "02d8f2e5-4f16-577a-af37-4f155ebf4f0d", + "version": "MTc1NjMyODE2ODk1NDE4NDAwMA", + "digest": "sha256:914de31ee867f7a3f0ba8fdc9fce4fb3614ec3977d27ad06cda1df181c985a93" + }, + { + "id": "02edfeb6-0bba-543b-97a1-51632a18cd10", + "version": "MTc1NjMyNzc3MDU1NzczMDAwMA", + "digest": "sha256:a183690b763c0b7f7b2940a2079991fc9765ec2fcccc4c4af97f8ff053314db1" + }, + { + "id": "02f0eb6b-2fae-567c-bd52-69ed462fd21e", + "version": "MTc1NjMyNzgzNjM2MTEwMDAwMA", + "digest": "sha256:364f2fb743ffa3bf2a50241d4f35465a40f444c508ff8df3a554a7f7bd519524" + }, + { + "id": "02fda134-dad5-51dc-b939-c1e524d14c65", + "version": "MTc1NjMyNzg3NDkyOTk3OTAwMA", + "digest": "sha256:5a12f6ce8f7009c7d11fa9786b7ac529590bd02d9208059e76d8b8d5e2cd93e9" + }, + { + "id": "030fff19-a44d-52c0-9ab5-0bc497184c51", + "version": "MTc1NjMyNzc1MzYxODU0OTAwMA", + "digest": "sha256:f7e25ec0c0d43f1c082c1b01488ed01eb63dfd56f07f9a97673d476a8e54224b" + }, + { + "id": "031d2c11-edd0-5a3b-9f69-bd9b66300cb0", + "version": "MTc1NjMyNzgxNTU1NzM3ODAwMA", + "digest": "sha256:d1de79f83f6fbdea0c87eb7da6cb1af7a607bedb6d1cb2de7628a7188c9cfe4f" + }, + { + "id": "033fde72-2794-535b-9bb5-ce60444e2ab0", + "version": "MTc1NjMyNzc5NDc1MDE1NjAwMA", + "digest": "sha256:f003f5059754bc6b1e3f45e9736c416a06978d356a4c0a337289a16f07266e5a" + }, + { + "id": "036ceeb5-3450-5318-a613-f5c577f2b1c8", + "version": "MTc1NjMyODE2NDk0MTQ0NDAwMA", + "digest": "sha256:2c40a864161a24e69de64f54346e47a040a56e7962c0232bccff0040153b9f2f" + }, + { + "id": "0376c935-e664-5dc1-a5b6-834e153a4761", + "version": "MTc1NjMyNzgxNjQ0ODkzMzAwMA", + "digest": "sha256:883b114a93317d6cccc9c936667165a77b510a71d7c06f7b9bd5cbd9064a65e4" + }, + { + "id": "03815153-6d0c-5f99-a270-7c62a38bb6e1", + "version": "MTc1NjMyNzg3OTk3NDU5MjAwMA", + "digest": "sha256:d53b7a18b079fe17ec491e3dc45456d13bf754b0bbd0aa9d44dc41eb30c214c0" + }, + { + "id": "038f56c5-a507-5b24-b574-a1b423a3f705", + "version": "MTc1NjMyNzg3MTYxMDQ4MjAwMA", + "digest": "sha256:8c644b9b181640c0d3b600b4b27914409bdcfb0a35d477af46913e8b412d6960" + }, + { + "id": "03c64760-828a-5813-a8ea-8bf4bb02272d", + "version": "MTc1NjMyNzgxMTg5ODY2NjAwMA", + "digest": "sha256:813b26aaa5d398e04322a7f06373845a6afbc4632dcff71527a14ee696a6a94c" + }, + { + "id": "04119a5d-a441-5c6c-9623-0897b4930c99", + "version": "MTc1NjMyNzc0MTY3NjA4MDAwMA", + "digest": "sha256:cec45efc19cee3c959e700ed71b5e73e3b9f961572e63aa9659f21d9050c34c3" + }, + { + "id": "043a7fc1-2af4-5f4c-bdef-8246b8a73f86", + "version": "MTc1NjMyNzc5MDI0MjY4MTAwMA", + "digest": "sha256:87cbd29246b8d6d22870e1d9b17e4abbaab2fa9b19db922d1026d80c60fccde6" + }, + { + "id": "04450691-c953-5ad1-95bf-3eacf73ba594", + "version": "MTc1NjMyNzc0OTkzMjYwMDAwMA", + "digest": "sha256:f28326f4f2ae9071ed4f06010a2b6fb742831710556002f7efbbe7e61e1f19b3" + }, + { + "id": "044b4e1f-d927-513c-9b08-dd23b69c7240", + "version": "MTc1NjMyNzgyNjI3NDEwODAwMA", + "digest": "sha256:3f674b8f29b7472b64d8863f208f8df5a1e2f7c555a4e3603557d3bf6c54ae38" + }, + { + "id": "0499e46e-68b1-58c8-872d-40829c19fcdf", + "version": "MTc1NjMyNzgxMjYyNDc5ODAwMA", + "digest": "sha256:d3a5615c79e3ee6e76cb2e9d90926dc51e4ab1f5b14ea8bcebc1cf18a307947c" + }, + { + "id": "049d2e27-52af-5eb5-ba20-d1f51bdb5ac3", + "version": "MTc1NjMyNzc1MzUyMDk2ODAwMA", + "digest": "sha256:3888749989b2f201fb6dcabe9b44cf8ee55a7bdb657bf0cb5527d61524d32b5a" + }, + { + "id": "04a5634f-357d-590a-8046-e695dfcb1d47", + "version": "MTc1NjMyNzg4MTkwMDk3MDAwMA", + "digest": "sha256:65c3106da9617f354417068c76072a44c8c2bcd7bea74cac32319c0805adf717" + }, + { + "id": "04a6ca61-b3ba-575d-800e-684450351b17", + "version": "MTc1NjMyNzg1NTgyNTQ2NDAwMA", + "digest": "sha256:fc2d188962c5177e2a9d46889646ef4b6e4c6316b4ece2587cfa140989417603" + }, + { + "id": "04b566d6-85f1-50cc-9157-cbe0ca0384df", + "version": "MTc1NjMyODA3NzA3NDU3NjAwMA", + "digest": "sha256:b49012297c1b48b7c389a5e6d06e7f8fd866de2fc56b73ca8cada2d5c4b8e474" + }, + { + "id": "04cdbfe4-6ccb-5dd1-9994-0345757e8a73", + "version": "MTc1NjMyNzgwOTMxNTk4MDAwMA", + "digest": "sha256:43c73395b9a7cc0820dbd6439ecd2095d946e0b6a1d7c37bb6cd44165692b97a" + }, + { + "id": "04eff124-cdd6-5eae-b052-e5a0d8a8b693", + "version": "MTc1NjMyNzc3OTgwODE5NDAwMA", + "digest": "sha256:f13fb00f134026673cefdc1ccb1b61980dffc71c02919d268aea0218a515a40f" + }, + { + "id": "04fd8173-c694-58a2-bfca-f253a7e613af", + "version": "MTc1NjMyODA3MzQ1NzAyMTAwMA", + "digest": "sha256:e79bfafe98588786c5db04b8641592a542a667bbf6107ad443e4c282928ffa9a" + }, + { + "id": "051ca0ad-cf4e-5bd1-a184-b95effed6d6c", + "version": "MTc1NjMyNzc1NjkyMTA0ODAwMA", + "digest": "sha256:b71bc1d0b5bf4b605cce1d34224b37150d76af53be9a1d4d5e56e7f1fac62941" + }, + { + "id": "051cbc74-8ad6-50f6-b5cd-1f2eed86164e", + "version": "MTc1NjMyNzkyODg0NTU0MTAwMA", + "digest": "sha256:32647c8a5b0a72595a4737d486ea7554fd1295341cf92e16ad06c309400dede5" + }, + { + "id": "0547d492-6976-5464-b14b-cc4a08caedd9", + "version": "MTc1NjMyODE4MDQyNTY5NjAwMA", + "digest": "sha256:92d8b711b25e0c5685e0a7100ccaa64e1273f881ddfe68e4659b64fe60b1bad5" + }, + { + "id": "0547e6fa-538a-5a2d-b240-c56f98be9564", + "version": "MTc1NjMyNzgwNDU4MTg0MjAwMA", + "digest": "sha256:2609d1680662ce29294428edc847b9e4ec6430021258d950b1050ac07733936d" + }, + { + "id": "054878da-ffb5-5d0d-9b3e-b7f535c22126", + "version": "MTc1NjMyODE4MjE3MTU0NDAwMA", + "digest": "sha256:53ab82e3f49b693bfc33a628ace779e3f165be343df640f936480337beb3fe4e" + }, + { + "id": "057f3fb0-e73c-53b3-84eb-7e56be59604e", + "version": "MTc1NjMyNzg0Nzg5MTU0NTAwMA", + "digest": "sha256:288936238f88c07d97c2d47283b3ea9ff173b8210301446f14bfc873e459fcd8" + }, + { + "id": "058673ec-6162-5092-b63b-fc4fc4eab7d8", + "version": "MTc1NjMyNzg4Njc2OTg1MDAwMA", + "digest": "sha256:43ad189c05e9072a14987c7d7cc0f878e6d8b0a80736ce2b772858e76c33dda0" + }, + { + "id": "05ac2232-fa5e-5c31-b269-6247336c94a0", + "version": "MTc1NjMyNzgxNTY0NDkxNTAwMA", + "digest": "sha256:3eff5f1186d61a57c48ae80e9ab0b06ea96cdefa6937402130b92de8dbcfc176" + }, + { + "id": "05d9d2fd-0248-5200-87eb-39b77db0c52b", + "version": "MTc1NjMyNzg2MDI1NzU3MDAwMA", + "digest": "sha256:c40ad963844826db1a8c4427a65a2c38b566b4afd116edc10502dfdd39fa8453" + }, + { + "id": "05e20983-c378-5852-ac65-70cf78126b38", + "version": "MTc1NjMyODA3NDU1MDg2NjAwMA", + "digest": "sha256:a89251a2510d91c7f1d0b77040f19eaa71c9c54917ad8db45f5309931599860f" + }, + { + "id": "05ea7b52-872a-58d7-a9da-ea4c5f13606b", + "version": "MTc1NjMyNzg2MDM2MjEzMDAwMA", + "digest": "sha256:7dff5379f3988b82eb6b9fe853561af4e71dd669c958441a9b5edfc2dfcc2292" + }, + { + "id": "05ebbc32-9f5b-50df-85f6-05a2d0bc0531", + "version": "MTc1NjMyNzkwODk2NTA0MDAwMA", + "digest": "sha256:ab75f13aa24606ea7c3c101bbd44b0b7c1b2e14767f14eb0a0751ac5dc51f2b7" + }, + { + "id": "0600e5e5-40e1-58d0-b5fe-4102264cd0ed", + "version": "MTc1NjMyNzc3MDY4NTYzMjAwMA", + "digest": "sha256:d253332e6b66ea331b8310a8dbcaf50a08f6063430c7f1d286cd7d397342b06f" + }, + { + "id": "063cc69d-4173-5bd1-aeb7-805c51446e9b", + "version": "MTc1NjMyNzg4MjA3NTU5NDAwMA", + "digest": "sha256:7273be4ea62e06a861d505fcd489c05f79355752406696ab6a8782b50fcf85ae" + }, + { + "id": "06508381-7559-5db1-95e0-a200dad9e049", + "version": "MTc1NjMyNzgwMzA1NjQwNjAwMA", + "digest": "sha256:f271427c8c05e3e9b93916d8c11d3b3ac81afa0b9eb81a23fae95a7191a2f88d" + }, + { + "id": "065bee63-3601-5476-ac83-2cdc3510496c", + "version": "MTc1NjMyODE2MzM3OTc1MTAwMA", + "digest": "sha256:210690d56203709dd45000488d30e21de671e67934b45520ecf46781cf554847" + }, + { + "id": "0668db90-a136-51c4-8f39-4b481f60b79e", + "version": "MTc1NjMyNzc0MjQ2MTU3NTAwMA", + "digest": "sha256:f652e35785e86c10b5c47286b44c0fe1ffba109423836ef421ec8c7a938adb03" + }, + { + "id": "066e35c7-0048-5976-a42b-d29ba2fd9328", + "version": "MTc1NjMyODE3Njc5OTg0MDAwMA", + "digest": "sha256:938bd65e9dea27e21544de0162b7467b093c86064d75f2eb440f40f3a2bb37e3" + }, + { + "id": "066e8b3a-6eb7-5ae7-ac33-2361b2f99237", + "version": "MTc1NjMyNzc4MDg2Njc4NzAwMA", + "digest": "sha256:2c93f285d47894688a4bbb095f14f22c45e9b152535ead896139146da837cd8a" + }, + { + "id": "0670bdd4-42a4-5b9e-bda2-fd6d357e54ea", + "version": "MTc1NjMyNzczODQ3NTE3OTAwMA", + "digest": "sha256:3c619f714b1f060e5e2625bff749ebbf2a025c88d52416e9f8d074eafaa0b71d" + }, + { + "id": "069c2207-6ade-51f4-89c9-cf906206d85a", + "version": "MTc1NjMyNzgxODgyNjI4NjAwMA", + "digest": "sha256:a97a4cd012185dce14abbee25ed251236446cfd2a367c94fb3f64bbf9d8ff929" + }, + { + "id": "06b14dd2-99cd-587d-a880-19ba519efe56", + "version": "MTc1NjMyODE4MTkwMTQxMzAwMA", + "digest": "sha256:d2d4175ec06edf25bd1385bf4d4bc4567e55e11763da1552667bed5a9c83ccf5" + }, + { + "id": "06e9f3d9-0a9b-5fa2-b62a-a9427bd084c4", + "version": "MTc1NjMyODE2NTg2Mzk0NzAwMA", + "digest": "sha256:31306fe75347521778a789844bfa3931da2487294cb4a3a31e5a6714c95ae957" + }, + { + "id": "07048b3c-53ee-5ded-9534-6b32304fe308", + "version": "MTc1NjMyNzg3MjAxMTExNjAwMA", + "digest": "sha256:14780d12b5c9795697552b296e74b3905914e1322247823239254d4bd978cbd9" + }, + { + "id": "07191dc1-874b-592f-af9a-f9d9473c4bb4", + "version": "MTc1NjMyNzg1NDQ2MTE5MDAwMA", + "digest": "sha256:acda81dd8c6f73a947bbda3ebe074416bce6fa8a82548fd1f634a0fa8c48435a" + }, + { + "id": "072591e6-f8fd-5dd0-9a08-6f9e48b1b107", + "version": "MTc1NjMyNzg0MTExNzgxMDAwMA", + "digest": "sha256:a912b5e6ba1d21814b6469320d021bc27879835abad74df5504166c23596898b" + }, + { + "id": "0730bf43-8d5d-5897-a7d4-ef1eaa4cddda", + "version": "MTc1NjMyODA3MTM3MTEzODAwMA", + "digest": "sha256:8005b3094b4d44ca1dfb31a32816132b5ec187f263e85923e89efa45aca2eab5" + }, + { + "id": "073adcfd-4365-5d14-acda-c0c2a788c368", + "version": "MTc1NjMyODE4MjE0MTQyNzAwMA", + "digest": "sha256:5b3e0071afcc6b4f4408af8d54157159a6c9d68911e34256209e536c65b52169" + }, + { + "id": "074d6358-ef13-5532-89df-e68b85d5dad0", + "version": "MTc1NjMyNzg3MDQyMDM1OTAwMA", + "digest": "sha256:624a93782de63d498581ac41ac21865ba3a4ffb0b6b9bff8af6c1afa3dab0ed7" + }, + { + "id": "0752ef36-8c26-51ec-a66c-a305cf5188bc", + "version": "MTc1NjMyNzc2NjI3ODY5NDAwMA", + "digest": "sha256:7cace3eef4a876c1c7223fb72474754a916984e00c39c5a6e5ebcf47766463e6" + }, + { + "id": "07621b67-4267-50d6-b18a-f3be0c0314dd", + "version": "MTc1NjMyODE3ODA5MTAyNDAwMA", + "digest": "sha256:817e12fe1da51d2487cfdaebb73830c17bca40cc8993d1d24aa43638807f4866" + }, + { + "id": "07625f74-6aa4-5cdb-88c5-a002772e76d8", + "version": "MTc1NjMyODE2NDQ5MzIyNTAwMA", + "digest": "sha256:628a654168457bfb48a9200e80ce354787398b15e4a42e81e94d9306381df2f9" + }, + { + "id": "076fdec0-86a9-53b0-9725-33a040db8fec", + "version": "MTc1NjMyNzc3MTU1MDM3MjAwMA", + "digest": "sha256:3e49d7432e8a555b84d1c5af758576a3b2504e3b008aaf5a804a1a67a312de25" + }, + { + "id": "0774a166-c044-58a7-b3e4-3d5dd78b94bc", + "version": "MTc1NjMyNzgwMzcyNDExMDAwMA", + "digest": "sha256:5e41fa8f6e5b9412669ca0a15f103d6c7ee0222f21e55d99c727fa81538f654c" + }, + { + "id": "0777698a-7abc-579e-a4b3-7da1334fc2fb", + "version": "MTc1NjMyNzg1MzEwNjAzOTAwMA", + "digest": "sha256:16f7425962327ec279cb8f512ed30a4cb44d9cd85a77cbf91db831396228cb63" + }, + { + "id": "078ca619-e6b0-535f-a1e6-4b8a16cf1c09", + "version": "MTc1NjMyNzc0MTY3NjI1NzAwMA", + "digest": "sha256:c632b7a291b95c2d526c6ea7809159d1b61db1e3fa7877abb14d049d514bcedb" + }, + { + "id": "07b50554-d088-51a7-93b8-33505cbb21e4", + "version": "MTc1NjMyODA3NjkxNjEzNzAwMA", + "digest": "sha256:08ed319175a5448e7dbd42c24c72bbf36298cf6659219591e7d55f9a7de8af3d" + }, + { + "id": "07d91119-aca7-5c2e-b430-3a9f59532e2e", + "version": "MTc1NjMyODA3MzI0Njg5MjAwMA", + "digest": "sha256:b803a5c02d3cfd0ca0e80d1be2d4d7168145dc38618c71c5afc6d8d52fabdbfd" + }, + { + "id": "07db08dd-8fa3-516e-bb07-06d101c8e1ea", + "version": "MTc1NjMyNzc1NzY3ODI5MjAwMA", + "digest": "sha256:b7696047128d408b850ddabb30e15f7924e5fb9388ae9ab274c564a5d40a9fb2" + }, + { + "id": "07dd3ba8-2dc5-5ee3-9304-49e563d052d8", + "version": "MTc1NjMyODE2ODk3NTA3NzAwMA", + "digest": "sha256:4d582082e1ef058bdd79370389912a7a48e41c876e0210ea3a4cb9afdf7d7734" + }, + { + "id": "07df2946-2ff7-59eb-a5f5-c0578d3ce5ab", + "version": "MTc1NjMyNzc0MTY3NjMyNDAwMA", + "digest": "sha256:c5908845e55d967c179f3553ae25e1a8d64af34f518c879373fbe95a0d9c15a4" + }, + { + "id": "07e7db21-7af8-5442-94c9-d2b20e5e5b2a", + "version": "MTc1NjMyNzc2NjMyMDEwODAwMA", + "digest": "sha256:350e4b27c1df142bd1579605a5f84baa04087707f2ddd1d7c5edabc4972da846" + }, + { + "id": "08028b29-86cf-5dc0-b911-35e4ddb18648", + "version": "MTc1NjMyNzc1MzU3NTUzNzAwMA", + "digest": "sha256:af067f67dfa733b98eb5c93a8b76ec366422262d28ca80100d456cc496ee8865" + }, + { + "id": "0818c7b5-767f-5688-a4ce-d7cf86f98844", + "version": "MTc1NjMyODE3NzY3MTIyODAwMA", + "digest": "sha256:59611dc15c668005ce1baf527ae376d1c0410f65959827394630637bc6f18187" + }, + { + "id": "0822cd4a-c019-58f9-ae04-2415f4297efa", + "version": "MTc1NjMyNzc3MDMzOTI0NTAwMA", + "digest": "sha256:3633ffcd0ea15aba1e76b2281b46f89b8b2aeede44d04fc2f6950e79d46addd5" + }, + { + "id": "082e6377-b546-5a9a-bb37-33bc761b1f86", + "version": "MTc1NjMyNzc3OTcyNjg4NzAwMA", + "digest": "sha256:3d91483bfa1f1b4608e3665860b84d553e928ffe29193c5ca26d083cbaf9fde4" + }, + { + "id": "083b69ee-a091-5110-9c31-4a39cfdcd72b", + "version": "MTc1NjMyODE3NjkwMjE4NTAwMA", + "digest": "sha256:22216607d826ee69fb8d55c5bd0ab492798aa030eacc17440ea6e7c155963fd6" + }, + { + "id": "084a7031-697e-5822-97ef-1e429bf57965", + "version": "MTc1NjMyODE2ODkyMDgzMjAwMA", + "digest": "sha256:a718b31c62e443b993f85cfdca5aa6f66cf87b118d6ee24ee7274e7f9bd187eb" + }, + { + "id": "0854f742-aa00-5142-a81f-53bf5263b498", + "version": "MTc1NjMyNzc4MjgyNzg3NjAwMA", + "digest": "sha256:7bbfe9c2b0fab80a986768de4eb628d17cc1db473dc6c17a238128e9b0446274" + }, + { + "id": "0859b57a-c426-55f9-9d3c-2e78d9a427f5", + "version": "MTc1NjMyODE4MTg5NDEwMTAwMA", + "digest": "sha256:91ed2df73e2262427e86c49d62e7008391a37a169175b4868c76a3612f3cf09a" + }, + { + "id": "086f71c5-fa4d-5f37-9b30-f5a7b078332f", + "version": "MTc1NjMyNzg0MzA1OTY0NTAwMA", + "digest": "sha256:9b57e8b9daa9062ecbd866433c0ea620bf5a915f97d6b53c420eb3f7d2e91cb7" + }, + { + "id": "08a3f6ba-74ee-5e7c-8616-de07eae5592d", + "version": "MTc1NjMyNzczODQ5MTk1OTAwMA", + "digest": "sha256:26c0c21cd35a7cb0c5c2e14c62bf174f71027870984a2c551b2b4ad39a3799d3" + }, + { + "id": "08bb2a8b-2899-5120-8c46-72af9bea781f", + "version": "MTc1NjMyODE3NzU3Mzg2NDAwMA", + "digest": "sha256:861a207c1bf2785883d2a0412759519b365a36533b258b01c8fc1f1513b86717" + }, + { + "id": "08c077f1-3ab4-5240-8ea9-884d055cb511", + "version": "MTc1NjMyODE4MjA4NDc2ODAwMA", + "digest": "sha256:c54688a7b6d1eda9a862cb304c5909b0e5c7604655448748f50262396fc08594" + }, + { + "id": "08ffa1ef-1923-56ad-bf0e-4114eb8d956a", + "version": "MTc1NjMyODE4MTk0MTk3MzAwMA", + "digest": "sha256:7ce91c33b7bdb04d4513be5410aaea2df14adc02ab0eaa53e97de32cc3061a5e" + }, + { + "id": "090eaf85-8978-5089-be34-88e2a30a8c2d", + "version": "MTc1NjMyNzc3OTkyMTc5NzAwMA", + "digest": "sha256:0584db440e1b86ab51fd4af005b8898d43a39a1ace4dac4e2a26242ce651029c" + }, + { + "id": "0917a922-b304-562c-8dac-e7268151f5a6", + "version": "MTc1NjMyNzg3MjAwNTYzMzAwMA", + "digest": "sha256:77f0a72e9522a572374e9319e91c3847645f8018728818d3e9d0270bc9913ca7" + }, + { + "id": "095092a7-afa0-5af6-b5fe-1cc9029f570d", + "version": "MTc1NjMyNzgwMjg1NTAzMDAwMA", + "digest": "sha256:26482fbe0895d1be4e6c1117a96e38430573ea122c4d1cd0bf09fda58fb367c2" + }, + { + "id": "09541fbc-beaf-53c6-a8c0-59136b8e36ac", + "version": "MTc1NjMyNzc3NTIyMDgxNDAwMA", + "digest": "sha256:c25fbf8a9e44670fd77210404893fba356b982fa1814a1032d1a862b0f60146d" + }, + { + "id": "0957df0b-a760-5efc-b377-3875272d6129", + "version": "MTc1NjMyNzg3NjU0MDUxMzAwMA", + "digest": "sha256:99963b8cef3e20ff3cc104776af38b71fed1f7b7b7abab09bf70c90647158565" + }, + { + "id": "09722cb7-d146-5a0d-86af-1905f21c7750", + "version": "MTc1NjMyNzc4MDg5MDAxNjAwMA", + "digest": "sha256:bd2efce08ed91ff9cb4c898d8fa0649ee65553c3f992e815d6e4dd48487d052e" + }, + { + "id": "09918f3e-c6bf-5675-99da-06d506c6db9f", + "version": "MTc1NjMyNzczODQ4MjI5NzAwMA", + "digest": "sha256:138554875f0c06180be30a53b83d463d63bcf5160661c3c4e6c2d97b088e52a9" + }, + { + "id": "09942e8b-b97d-58fc-b9ad-11e95d9344b1", + "version": "MTc1NjMyNzgwMjk3MjMxMjAwMA", + "digest": "sha256:edbe178aa932ae7b7c7c011c85177041eb1f08c1f9984f35b485f2caecae3edd" + }, + { + "id": "09f5a45e-6c39-50a9-9095-4a23bab15da6", + "version": "MTc1NjMyNzg4Njc5OTg2ODAwMA", + "digest": "sha256:0165c53180cba77ab85dfc9ee64874b6a9a4f997d7f3ca158cb699f23fffd02e" + }, + { + "id": "0a05c100-2032-5d31-9895-dee70e86adb9", + "version": "MTc1NjMyNzc0NzcyNjE0OTAwMA", + "digest": "sha256:e36309eb23e9aab900370cda83d8f5a127e0cdd82534507d76dd2fe4667baa3f" + }, + { + "id": "0a0c2bfb-8348-55f8-9f90-6b195b42d741", + "version": "MTc1NjMyNzkwODU1MzYyNDAwMA", + "digest": "sha256:de577ee453ac4297a87c2287883bf41250b35c9f65de3b113bc9291e5da60b5e" + }, + { + "id": "0a0d17de-927a-58af-a90b-08f7e5b5c9e3", + "version": "MTc1NjMyODE2MzA5ODU0NDAwMA", + "digest": "sha256:3ae9dd2014839bfbb9ddd99ff053062577bc40d13525f22db641eaa5aceed4f7" + }, + { + "id": "0a3b6107-3e44-534e-ac72-b14d3d3fdf82", + "version": "MTc1NjMyNzg0Nzg3NTI0ODAwMA", + "digest": "sha256:bdf06ee6d2e5a3424a7800dbcce394928272293ef0fe42a8315913620245297e" + }, + { + "id": "0a50356f-4937-503d-b059-8d0bbb0dd6cd", + "version": "MTc1NjMyNzgyNjQwNTQ4NjAwMA", + "digest": "sha256:80d3c0e745f7b2da17b14a7a12091fa744e74d74a6d3afccb54c71a34ed20980" + }, + { + "id": "0a5f48e5-047d-535c-bee8-c9df35cd1e46", + "version": "MTc1NjMyODE2MzAwOTA2NzAwMA", + "digest": "sha256:45101b3f19093b77d6cefb0620060303436f2498769a7a392beb6441bee3d8c6" + }, + { + "id": "0a64e12c-e781-5ac6-9000-0abc5b3373d6", + "version": "MTc1NjMyNzc2MzcyNzExNTAwMA", + "digest": "sha256:6cae2d79aa76d06380e516e490fc66403fe0f775e2e9e31ed81efef57cb659f7" + }, + { + "id": "0a83a733-d9cb-5876-8716-11d5fc209668", + "version": "MTc1NjMyNzc0NzcwMTg0ODAwMA", + "digest": "sha256:95ba1f60d661c3534e57177bfad30664a80841e35e99f154bda10afa9cadf6a4" + }, + { + "id": "0aa6d433-addf-5ed6-935c-e907b1641cae", + "version": "MTc1NjMyODE3MTE2NzU3NjAwMA", + "digest": "sha256:7e24112eb80c61c27f15143ec7ec9188eab8973938db811b68f8adc47656d134" + }, + { + "id": "0ad38e89-9819-5079-8bfc-b7dacd1dee40", + "version": "MTc1NjMyNzkxMzA1NDA4MTAwMA", + "digest": "sha256:009bdf24366f37e118492387b6daec9358707737165165e8e32d7aaf9d299ab7" + }, + { + "id": "0addfefc-6452-5c53-8b7b-78c051767602", + "version": "MTc1NjMyNzg4MDI4MjY3MjAwMA", + "digest": "sha256:b7dd83717b38515742924bade48021577b3a3ccb8a6423f510cb3e5d3ebe8e9c" + }, + { + "id": "0aec1fbd-fe18-598d-915b-dca4ad469839", + "version": "MTc1NjMyNzgwNDU4NDE4OTAwMA", + "digest": "sha256:1ba728e36c5d3761ecb2c4280a619b27722e8e43e9492f88dc6dee03089bf1aa" + }, + { + "id": "0aefa520-11cb-57f8-81f9-daef3a2c959f", + "version": "MTc1NjMyODE3NzY4MTgyOTAwMA", + "digest": "sha256:f8dbd3a04d26b14d04108401b4b3d3049f19b2e72a02aa45ff6c4a2ef65db484" + }, + { + "id": "0afccd12-63be-5ebc-8773-46185028a9b4", + "version": "MTc1NjMyNzg4MjA3MDQwMjAwMA", + "digest": "sha256:e814f7764d63e21346cee468546252aac36316785e17cb943cb2ad02beadfde9" + }, + { + "id": "0b0c2e21-d297-5cc3-9284-150c458bf7d3", + "version": "MTc1NjMyODA3MDc5MTYyMzAwMA", + "digest": "sha256:f045b98cbcb09c8197072aca18dc94ba4f957e094faf347e3c372c0d60069ec9" + }, + { + "id": "0b135ec9-2a69-5ccc-9988-36af3ad249fb", + "version": "MTc1NjMyNzc2MjM4NDkzMjAwMA", + "digest": "sha256:9a2fab72fe0eb5850f6dbd110e3ebc02a86244c5db99b72365d6641045a743b6" + }, + { + "id": "0b3ae633-3fe0-5e3a-bbf9-19750b5a7fc9", + "version": "MTc1NjMyNzg3NDU1MjUwNzAwMA", + "digest": "sha256:c0ab0fbdedf3b80c7b1a7cada0a0bc7e6bd12a80252eb8d98036e2d4c7dbf511" + }, + { + "id": "0b4be159-5709-5089-a477-2da1c7f7bb1c", + "version": "MTc1NjMyNzg1NTczOTM5NzAwMA", + "digest": "sha256:61572e2c8b911e704d0a49df9680dbfbff88001573cfb339ff359e54a8863504" + }, + { + "id": "0b89ccea-6a56-5149-9461-34fe9f1a1e4b", + "version": "MTc1NjMyODE3NDQ0MTcwMzAwMA", + "digest": "sha256:d1e4fabd082ff8f176f41d28bed8bf6bfa2756d19742a03fc64727e59f580bfe" + }, + { + "id": "0b94bfb8-1ce3-5599-9576-a607f84d008a", + "version": "MTc1NjMyNzgxNTUxMDcyNjAwMA", + "digest": "sha256:1446df3c21645937bc1a70e27e92535eb880d4e4cd7eaa0da9190c76693e54ff" + }, + { + "id": "0b95b561-6781-57a4-842e-84ab61ab0f7c", + "version": "MTc1NjMyNzg3NDkzMDQ2NzAwMA", + "digest": "sha256:a258a8a5b0dfdac6f6aeb0a9e0d078b99df1666101270bd132e6be9c0b3e17ef" + }, + { + "id": "0bb4e2e2-ffae-5e5c-b6be-8b8c8dd6c5a8", + "version": "MTc1NjMyODE3ODA5NjM5MTAwMA", + "digest": "sha256:f46568dabef55baf74551505b48235dbd8db7828a57a1e7adbbfe214d63d78bf" + }, + { + "id": "0bc86b14-e4e6-54d9-85e1-fbfb1c3b3afa", + "version": "MTc1NjMyNzc1NzYxNDA3ODAwMA", + "digest": "sha256:9c5128ac380732e409803831dec99238be0aaafcfd7b430a7d29f782befb8e4d" + }, + { + "id": "0bd83031-4db1-5e41-9045-970d6a0b5058", + "version": "MTc1NjMyNzg3NjgyNzUzNDAwMA", + "digest": "sha256:f87cda927bcbf00dac32004425f5dd744ed6a7ce79819839844fbd87ad74505b" + }, + { + "id": "0bd8b63d-eb6a-573c-80b5-e06cb72d2316", + "version": "MTc1NjMyNzc5NDcyNTY2MDAwMA", + "digest": "sha256:30009750dcb8f51fcb38ea009bed53b30dc6484bf6b71e8d79adf13eb81355ad" + }, + { + "id": "0c25502b-baa3-5f86-b79b-41d95eefbfde", + "version": "MTc1NjMyNzg2NjI0NzY2OTAwMA", + "digest": "sha256:4f15baef85de911d793a45ac08580f7f4e3e9b91dff4b3f2427478c25d1118db" + }, + { + "id": "0c638990-20e6-537e-b1af-2428c664fa04", + "version": "MTc1NjMyODE2OTAwNTIwOTAwMA", + "digest": "sha256:02ce116b8e5e8f9552cfaf4938e3de3f7975777840d7644cc2a275e02fad9738" + }, + { + "id": "0c6a7f4b-2f6d-52da-bd74-7e5bb4cda955", + "version": "MTc1NjMyODA3NzE0MjAyODAwMA", + "digest": "sha256:1d6cd2c7798e6184f062a284ab47d591e6ba4ae372095377318be576f5234e0a" + }, + { + "id": "0c9e46b8-9e74-5787-8d8c-57d83f3c2387", + "version": "MTc1NjMyNzc0MTY3MzI4MTAwMA", + "digest": "sha256:130c14dfc3c494a00a2d665f07aa6ff2b2a7fdcfb7eca65f13a368444203ef51" + }, + { + "id": "0ca323c9-0bd1-5d9a-ba89-c4aa9155c0b6", + "version": "MTc1NjMyODA3NTg0NTQyOTAwMA", + "digest": "sha256:40c8ad19dc4589cbc3711d4846408c23c0d7fb7c29f4580a818477c4d9aa3e4d" + }, + { + "id": "0ca4f65e-50ad-5da3-8a57-f6b38224e678", + "version": "MTc1NjMyNzc5NDc3NjU1NzAwMA", + "digest": "sha256:a0faa62443ec9c2bb0197f7cfb4e5f7f209857f4bfa4b93121537e2f85a766a7" + }, + { + "id": "0cb25dca-3632-5d07-9659-87d1d93c5cb2", + "version": "MTc1NjMyNzc4MDg2NjY2ODAwMA", + "digest": "sha256:0a1be2b6017f8e7c4356238e0e16495a05c79f6408f6ffb1003c243d08ca5485" + }, + { + "id": "0cc0479d-cfe4-5209-8e25-044f6f03c4aa", + "version": "MTc1NjMyNzc5NDc0OTU4NzAwMA", + "digest": "sha256:255864fae609395132e2bd3beba6b6cc805adabb43628803465af14395d424c3" + }, + { + "id": "0cc1bfa3-b226-533a-b974-fa135ea2d528", + "version": "MTc1NjMyNzc2NzYzNDM3ODAwMA", + "digest": "sha256:4e91e69bb00837ba1f392e115b32248a2ad4ff8b9b71d7410c93303fe41b80a2" + }, + { + "id": "0cf9564d-d25a-52ff-a9ff-56839b65d50e", + "version": "MTc1NjMyODE2NDU0NjkwMjAwMA", + "digest": "sha256:c767bc5619a8c2d7bf9c4c0c3c7a8e22c400657ce9277d40f28380c13fbaeb0b" + }, + { + "id": "0d015b74-31eb-597e-b46b-10d25d52a529", + "version": "MTc1NjMyODE2MzA4NTE2NDAwMA", + "digest": "sha256:8b1d203db93ad87260d78b30ae3fa11303b65eeeb47396ec0edc021d0ca6981b" + }, + { + "id": "0d1976a5-84d1-5e1d-a41b-5a5523721121", + "version": "MTc1NjMyNzg1MzI2MzIzOTAwMA", + "digest": "sha256:7f54d28034c49ca4c5f1e0b52182f9babb48419c1b01d854a8cf32811d1f4f1d" + }, + { + "id": "0d6a4632-c25e-511f-9fcf-2462e1db4d7f", + "version": "MTc1NjMyNzgzNTY2MzU2NDAwMA", + "digest": "sha256:0a487f4dc7ef511ae31ad87d0f0e4df3a97e27be31bb026d847b3aee9de1263b" + }, + { + "id": "0d6d663b-2747-54e1-bdc6-fd2be666fad9", + "version": "MTc1NjMyODE3MTIwNjkxODAwMA", + "digest": "sha256:bed8538797d2b0c9423fa3caad7fa7c376bc81d0fd55ec283c2088ca9c7301b3" + }, + { + "id": "0d74cec8-7ea0-57d6-af23-fd2012fe7f79", + "version": "MTc1NjMyNzg3MTUwODcxMjAwMA", + "digest": "sha256:6e61618045486464d2b14f4310df6ec956e61dc455b704d2dce5fdfc5246294b" + }, + { + "id": "0d7e6d7e-a95d-52ff-8fd6-180ef4a70684", + "version": "MTc1NjMyNzg3MTQ5MTg2NDAwMA", + "digest": "sha256:0361d05f31213673d6111dc53480adf5f331da796eae1e38e95dc83a7c7abc5b" + }, + { + "id": "0d96d76f-c54c-5716-8a31-54e5471dc7f5", + "version": "MTc1NjMyNzg2NjM1NzY2MTAwMA", + "digest": "sha256:f18cc85db1aac7500b594adf9cf11ddf7eb14ffd4013dd68a39498e7a5b0d06b" + }, + { + "id": "0da943aa-b5eb-5f40-a046-ac49ed3aebf6", + "version": "MTc1NjMyNzg3OTk2MTQ0MjAwMA", + "digest": "sha256:94ff76ee8b2aea6473d564902ebecbfa888d5ec63268deeb6677b48b18ac356d" + }, + { + "id": "0dabe2b7-1541-56b9-9bae-ad8b6b571446", + "version": "MTc1NjMyNzc1NTg0OTgyOTAwMA", + "digest": "sha256:dd15aed825c76806f60a12aca73e12859b7ab5f167287eb0ff3ff3f4aa000e26" + }, + { + "id": "0dbdde44-620e-586a-b576-f87cc72e4035", + "version": "MTc1NjMyNzc2NjMyMDE2ODAwMA", + "digest": "sha256:1270ec2a91a48e524337dc067454dd12b30191ef5b09489cae9f3e1e918d28f6" + }, + { + "id": "0ddf7475-c1e2-5f1c-b817-3e7845cada31", + "version": "MTc1NjMyNzc5NDc4MDgwMzAwMA", + "digest": "sha256:060f78ee92e943262e6a318bbe603b7437856891e789a5fbb5c505b462de9de2" + }, + { + "id": "0df355c4-ed6a-5318-b2ad-3c87e162e926", + "version": "MTc1NjMyNzg2MDI2Mzc2NjAwMA", + "digest": "sha256:42c07b8c1f5b24c6a62e8712af79e293fde60fd121eba58d3658f2b8d8f70681" + }, + { + "id": "0df9b435-4c4c-54c4-9dbb-df1e0e44846f", + "version": "MTc1NjMyNzc1MzYzMTUxMTAwMA", + "digest": "sha256:b3793ba20939c611b6e7ec0195c0296eb13ba33763ff16d6cf089306269a4de7" + }, + { + "id": "0e15fc52-e17d-5c13-b46c-676e129b79d7", + "version": "MTc1NjMyNzc4MTk4MjEzMTAwMA", + "digest": "sha256:f95c46d9a603003f194b724ea4190b2a774ddcb4c44dc152d42114b72fb65722" + }, + { + "id": "0e3d3e7e-44e9-50d5-bf01-ff0cd19a68a2", + "version": "MTc1NjMyNzgzNTY5NjE4NjAwMA", + "digest": "sha256:bf7fab685d905a10e34428c9ec1ef3fd8186557ba949091186d057752dffbc48" + }, + { + "id": "0e3f61a2-f936-58fb-b95a-c9b9c0c25886", + "version": "MTc1NjMyNzg3NjYxMTYyMzAwMA", + "digest": "sha256:977d326201c9e17c2cddc70b6e42a7a9b943fa1d58af182dc43ba560ed38b9dc" + }, + { + "id": "0e53057c-8039-5727-90e4-cdb287ce0d48", + "version": "MTc1NjMyNzc2MzcxOTA4NjAwMA", + "digest": "sha256:e8b24d4c4c141d7e47de8b79bb9b835ba6f4758b318996038c4038f9c1f03cb9" + }, + { + "id": "0e583783-23c0-5f09-b829-687e48fd7650", + "version": "MTc1NjMyODE3NzY3NDU3NDAwMA", + "digest": "sha256:b3c2aae579fcd83a14927d9d0f5cfa37ad6be8f655d4d6acfe94153b318cc682" + }, + { + "id": "0e6e0be2-3e11-5d5c-ba3c-c54a8e5a271b", + "version": "MTc1NjMyODE2NDk4NjQwNjAwMA", + "digest": "sha256:53b47561f66e5a068f70e1cca68201f945e35213ac3250ccc040c1f3fd261e49" + }, + { + "id": "0e6f20e2-1b40-5f26-a766-43d210d9c917", + "version": "MTc1NjMyNzc4Mjc5OTY3NjAwMA", + "digest": "sha256:bcd68ae06a43ec460a9dd0c53062ae784e5037a38eba0789e1105c49b36c0dbe" + }, + { + "id": "0e70ffa8-f444-528d-9726-dfc53c970ba7", + "version": "MTc1NjMyNzc5MDMwNjIxNDAwMA", + "digest": "sha256:93f056e70b59d038f5cbba1ec988cbf00d373694a3feb4ccb6b52bff03dfc1d7" + }, + { + "id": "0e7610d9-57ec-592f-9bf5-98bf854cd1c4", + "version": "MTc1NjMyODA3MDcwMTk2MzAwMA", + "digest": "sha256:c4815a81c7700e83836f6ae54642ef51d1911ca093a93040cc67fa35c70b4108" + }, + { + "id": "0eaa321f-76df-54db-9b89-6b8ff7930faa", + "version": "MTc1NjMyNzg2NzQ2ODAxOTAwMA", + "digest": "sha256:2c35371a6047bb81b47b20b3a8f1d79973b9f55b3fb21eb71b0707ab6e69e508" + }, + { + "id": "0ec29639-0d82-5dc6-99a8-ea0a056b7067", + "version": "MTc1NjMyNzc1NzU2MDI0OTAwMA", + "digest": "sha256:a02c3c796529856d76f588a3c3d3abe7f487bae535b5cee72bc26d91527b5d4f" + }, + { + "id": "0ec59487-b4b6-58df-8dc0-720bc87e4343", + "version": "MTc1NjMyNzg2NzYzMTk0MDAwMA", + "digest": "sha256:345b1bd2b698db2a0c2a16f07cef2afb59715e127c4d73e5bca0dc09d367f32f" + }, + { + "id": "0edea84f-fb1b-516c-a11f-f48a6e526848", + "version": "MTc1NjMyODE2MzA4NzUyMzAwMA", + "digest": "sha256:5522008b626a24569d9255aec2f900ca18007c83bb531461f37f5c20dd5ea763" + }, + { + "id": "0ee472fa-0d7c-5bc8-b345-23132f33f375", + "version": "MTc1NjMyNzc4NzAxOTY4MTAwMA", + "digest": "sha256:754d6ff3b7231d675922471d8f88ae30e512148df61a0e4b4883a3b2227b0a0c" + }, + { + "id": "0eeee10d-20bf-56ad-9e9f-490245a0aa93", + "version": "MTc1NjMyNzc2NjQyNjA4OTAwMA", + "digest": "sha256:417ba7e270cb0e82c514713fda3a72268370e276c8f1c6d3dcb313b48a43993b" + }, + { + "id": "0eefe595-a2b9-5e8b-a3de-153faff9f754", + "version": "MTc1NjMyNzc5MDMwNzg2NzAwMA", + "digest": "sha256:ec5cefddad0f22d18f9c585a1e647c3d210fb1d728011386a84164d781b8a48a" + }, + { + "id": "0ef94903-36df-50b9-9e32-e6fe0a9dd8f8", + "version": "MTc1NjMyNzg1MzcwMDMzNDAwMA", + "digest": "sha256:7bd8931f644f2b10499f1739f6764bb9866f2a9606dc683e1ac8c5940ea3a343" + }, + { + "id": "0f02ce3c-7b1e-5d5d-920d-fa39c3a8d56c", + "version": "MTc1NjMyNzc2MzgxNDc2NTAwMA", + "digest": "sha256:2af04adeabff16764d89b61b1986bcb4ed46abb938a363eda03c935aaee114b7" + }, + { + "id": "0f107f42-06c0-5edc-9b17-55aeb8dd1d80", + "version": "MTc1NjMyODE4MjE3ODQ5NjAwMA", + "digest": "sha256:a967bbb28279750f4d02421fefcce9572cbac8a76c6faba791e53a6e7b6b7406" + }, + { + "id": "0f250ad7-5c9a-5390-b719-9d8221a076fd", + "version": "MTc1NjMyNzg1MzEzNjA2MDAwMA", + "digest": "sha256:3b8f752a610291b0db3f72bd40e5bbc23504ebb55c1df9499cf5ccc3110f2d56" + }, + { + "id": "0f25a8ce-67c3-5183-a429-bcd700b0e49a", + "version": "MTc1NjMyODE2OTk3MjM2MzAwMA", + "digest": "sha256:7653f737977cc09c97570c21b1348ef7502740f37df1d99cdc3c8331783e239a" + }, + { + "id": "0f37460b-4072-5daf-b68c-3b54ac59432c", + "version": "MTc1NjMyNzc1NzQ0Njc0MTAwMA", + "digest": "sha256:1f364ab81a3b84dcb79bc0f1de446b84de420d308b2a1cce01cc2e6ffe98511f" + }, + { + "id": "0f38d297-0a31-53c1-ab80-180698f07f47", + "version": "MTc1NjMyNzc3MzAyMzcxNjAwMA", + "digest": "sha256:fc9ff511e35ad54702b7c5922b68e58213f26f00c980843805bb182c11b25a3f" + }, + { + "id": "0f406da8-b09d-5744-989e-522b931dab91", + "version": "MTc1NjMyNzc1NjA1MTU0OTAwMA", + "digest": "sha256:e3ccbd5a0673102eb1841f64f0ddbe9b9ab4f81925447e2e516a2a723380390d" + }, + { + "id": "0f4227e5-8a6d-5227-afa6-c5338fe2d4a4", + "version": "MTc1NjMyNzc3MTU3MzkzNjAwMA", + "digest": "sha256:a31c2e0596142e7a0140638f6eb0711b0e2d1c7b047a330f29ed64f3581505b9" + }, + { + "id": "0f4e732c-9a28-5b2f-9ac2-1edc9ad27a7c", + "version": "MTc1NjMyNzczODQxMjI2NzAwMA", + "digest": "sha256:890ff90f7d635f4baaab9eebf6516a1fec2af8228e058bd4e6cdffdd188ad3b4" + }, + { + "id": "0f5276c5-5148-5439-ba5e-fbb7f7e3ed97", + "version": "MTc1NjMyNzg4NjczNjkxNDAwMA", + "digest": "sha256:8a7b450601a6510f9d33fdfb9d684e0ec4383ed30ac3cad550facff9d35e83ef" + }, + { + "id": "0f54c66c-35f6-5cc2-ae94-b94c768747de", + "version": "MTc1NjMyNzgwOTMxNjE1NDAwMA", + "digest": "sha256:3c879e893b9cc7028d443ab54bc6c107c26e9699e0376a11983dcf1d9a2a80a3" + }, + { + "id": "0f583fa4-4532-53a7-9d7a-d2ded6beb971", + "version": "MTc1NjMyNzg3NDE0NjgzMjAwMA", + "digest": "sha256:de3081a93892bcce3f65a6d1cb540723647e2d41f6b294c28675bc5d8bcec915" + }, + { + "id": "0f7cb3f6-4c35-59dc-871e-fe2aa20b97c9", + "version": "MTc1NjMyNzgyMjMxMTE1MzAwMA", + "digest": "sha256:57fc48f912fe3206aec6ec2668a2aa28189d7108f14423104d479534042e39a1" + }, + { + "id": "0f868681-dd5f-543b-bde3-4a174559de0e", + "version": "MTc1NjMyNzc3Mjg4MTMwMzAwMA", + "digest": "sha256:76549528fc043d1333b87f25f8e7bc5c2199b2114ab63a358a2a9233fd55450d" + }, + { + "id": "0f8de55d-6629-5ce8-a8f0-5eb408cfd6b0", + "version": "MTc1NjMyNzg1MzM4MTQ1NjAwMA", + "digest": "sha256:7696cebd4655db7ed9612564f5a0d73bbd8c743bce2b48d8b17624cc78e0e17c" + }, + { + "id": "0f996c3e-8ebc-5ff9-b40a-4bd7868514da", + "version": "MTc1NjMyNzc0NzcwMzQ0NjAwMA", + "digest": "sha256:2d5a2df21007fa12fe6cdce7b0b346dd3d6f6f148e9b8539afa496bed7d9b951" + }, + { + "id": "0fc5fea7-4ce7-5f0b-84ac-c8b4d2dcbc73", + "version": "MTc1NjMyNzg4NjcwNTYzNTAwMA", + "digest": "sha256:360c5a71b364325d48a78ef7795d4304a5ade49e0f035b99bd82442205a76782" + }, + { + "id": "104ffb90-fa02-529d-8a92-52aca2d24d46", + "version": "MTc1NjMyNzc5NjE3NDQ0MDAwMA", + "digest": "sha256:4e922fe786a45a42aa343b188fc209c4280753c9392dc723ab195e6e98f859c0" + }, + { + "id": "1083ddaa-11bf-511c-a8a5-f770d51e7fed", + "version": "MTc1NjMyNzg2NjYyNjE2NTAwMA", + "digest": "sha256:a1bbfc566108145a85e696b11a7765b64386478960c0eb14bb4fbd0a0c266409" + }, + { + "id": "10927ad2-ff56-5abc-8c86-f4d74532d3ed", + "version": "MTc1NjMyODE2ODk4MzEzMDAwMA", + "digest": "sha256:a9fa540a10ec1afeee060b617a99d5c6057339f3f0bfa40cd80049a4ca9b135c" + }, + { + "id": "10c9a293-335c-5b44-b10f-be22d006fc9c", + "version": "MTc1NjMyODE2MzQyNzQ0NjAwMA", + "digest": "sha256:87b034e82c418369da11617602e8ebfb108281a7ca0990d889fc8acdc76ba1df" + }, + { + "id": "10ce5d92-bfb3-5bc8-8b88-bd2eeb35e12a", + "version": "MTc1NjMyODE2Njk5NzY0NDAwMA", + "digest": "sha256:13b5a8cc4838768924a3d48455d8676b56ff5fa7371f39cc721bb09c62716cbf" + }, + { + "id": "10e0e56b-e8c8-5901-ae50-2018e87c790e", + "version": "MTc1NjMyNzc1NjA4OTkzOTAwMA", + "digest": "sha256:77266a572aebd2329ae94e5f7522c3bdb6fc9d5a94f89326d9ace16b57e06cfa" + }, + { + "id": "10edbf6f-bd39-5187-9142-a912a94b4967", + "version": "MTc1NjMyODA2Nzc3NjE2NDAwMA", + "digest": "sha256:4edd7e348b8387afd094c0d751c5d43367a46697adf2770f4e5929b2bd7ede45" + }, + { + "id": "110fcbc5-9b1d-5af0-bd8e-43fc92ee0078", + "version": "MTc1NjMyNzg0Nzk5MDIyNzAwMA", + "digest": "sha256:99f9eec3a685d5bbd30038622681c0952377ad079b2353d26aa4f52a914dd916" + }, + { + "id": "11496097-d1a7-5786-bb7e-92fff6a7d393", + "version": "MTc1NjMyNzc1NzY3NzU3NzAwMA", + "digest": "sha256:841de98961afa5424c74074f82594f0e62fbc34fc5e3342dcdbcf87972f95ff9" + }, + { + "id": "11593eb3-caef-5547-9a48-12b79cea0e87", + "version": "MTc1NjMyNzc3MjUyNTIyNjAwMA", + "digest": "sha256:0c315967a694ae7d5e6e1a7c34f1ed60ae6b9e928e6566b2a7ad24731e0d3a6e" + }, + { + "id": "116219c5-3032-5d60-a6e8-f9584144627f", + "version": "MTc1NjMyNzg1NDQ1NTg5MzAwMA", + "digest": "sha256:8d43121a4a0c254af206d9e5fe245dffaa5d4af71df23c3cf54ddc1e643af316" + }, + { + "id": "11666e42-785d-5fb5-985a-bb47e6e6eb7c", + "version": "MTc1NjMyNzc1NTk0ODI0MTAwMA", + "digest": "sha256:68cb78627dab2019e6333dd3fdd9fea56a4f8b67c1e527ba5fdedd54a1842bac" + }, + { + "id": "11833d33-8139-5276-9bed-e1db14bb14c5", + "version": "MTc1NjMyODAyMzQ3Mjk2NDAwMA", + "digest": "sha256:cf45c28e6f3857453d68352f88c69107ffb98748138773c76479b4611e83cff5" + }, + { + "id": "11a9e240-e7ee-5b7b-a21b-4d67fded244d", + "version": "MTc1NjMyNzg3NDE0OTA2ODAwMA", + "digest": "sha256:4e8edca6015d7f3544c82535fdb2c3db52e0e30d4cbf42a3cec16473bf0912e3" + }, + { + "id": "11edfbc4-25a9-5d7d-89f2-622a29e476bf", + "version": "MTc1NjMyNzg3MjAyNzY2NTAwMA", + "digest": "sha256:245416329dcc61d6c2d2908888e44b2e917a1710be3f0c7af9158ffc76437e37" + }, + { + "id": "11f08ee3-ec14-5ea1-b00f-1b4d05acf67f", + "version": "MTc1NjMyNzg1MzgwNjI4MjAwMA", + "digest": "sha256:041396b067d55dabc7b945663e154106af5b2a85b11858ce836eb64800f7426b" + }, + { + "id": "11faf664-0fca-5df2-8e26-2a5c203ec8a5", + "version": "MTc1NjMyNzg3NDg2MTE0MDAwMA", + "digest": "sha256:a62645600cb7105247cb448fa3117bfacf8e383746b14e354370730e62a228bb" + }, + { + "id": "12014d0c-aec4-53e1-a15a-e3a99d07c597", + "version": "MTc1NjMyNzc5NTk3OTU5MjAwMA", + "digest": "sha256:38addec32e86ee3f0ba5996457e89055e5548a8e35d058786adb6221e8fb8f92" + }, + { + "id": "1225737c-3cdf-504f-9b29-bca6b6e0dc2d", + "version": "MTc1NjMyNzgzODgyMzA1NzAwMA", + "digest": "sha256:da2539842ba5e833f528a0df9f2d4816d6a503e87e4d19b557e0ae80b0edd85b" + }, + { + "id": "1227e705-44f1-5859-992f-f730e1716e8a", + "version": "MTc1NjMyNzgxNjM5NTM5ODAwMA", + "digest": "sha256:908e619f83130bc5234279c4031272eb841a4b57ca2cbad8cdd5bda71e520944" + }, + { + "id": "124263a4-8008-5108-9a7f-b9fba0e56f95", + "version": "MTc1NjMyNzc2MjQzMjkyODAwMA", + "digest": "sha256:79de780fc7767c37c3178560ffa74b0a66bd00d7d4a796ee68e8f61d304726f9" + }, + { + "id": "12546273-266c-545a-b78f-6b04d67a1a8a", + "version": "MTc1NjMyNzgyNjU3Mzk1NjAwMA", + "digest": "sha256:d817a8a597185402d3ca59c36e9231c73036166e03e64d8f7a36a6b96e44e68b" + }, + { + "id": "12611d34-49f2-5116-a4b4-91ecf5428d0a", + "version": "MTc1NjMyNzg3NDk0MTAyMDAwMA", + "digest": "sha256:79989e7915eb41acc467da773b072a9d5376f978a93ab794481312a745299bc2" + }, + { + "id": "1267a5f7-3923-566c-aba4-9f47db2e8a0f", + "version": "MTc1NjMyNzc1Njk0ODE1MTAwMA", + "digest": "sha256:9bf456d20c3b0c2b627930e0a36e66e156a7b92875a1f278577eddf7760e0cff" + }, + { + "id": "128bda21-ede2-5c62-9cde-1c63a4d91366", + "version": "MTc1NjMyNzg4MDM3NzU3MjAwMA", + "digest": "sha256:f4229800bf0f424bbee5236053312e059767393cf3771142cd6661c6ea537c6d" + }, + { + "id": "12bd9aea-6e9a-5db8-b07b-47b25c1b6997", + "version": "MTc1NjMyNzc1MzY2ODM3ODAwMA", + "digest": "sha256:44c98fdb6ecc4f5f9ba6d47b4ad8d12d90969fee297381a9eb3f7161e3b2e4bd" + }, + { + "id": "12cd27e0-ada3-534f-9d16-643f11a5f54a", + "version": "MTc1NjMyNzc3MzI4MzIxOTAwMA", + "digest": "sha256:74c6b3d28fa61f84ca3c943e53496ec333c4eb2db118869f990cf0c783297b97" + }, + { + "id": "12dc6f09-8c7b-5cda-b345-7a5fff5da11d", + "version": "MTc1NjMyODE3MDAxNDU2NDAwMA", + "digest": "sha256:63384ef157725faa65ede5f4e77997313135b343975a843c6296e8d1470603ae" + }, + { + "id": "12fe9622-c129-5daf-b3a3-f1bcb2d4c05b", + "version": "MTc1NjMyNzc1Njc4OTYxNjAwMA", + "digest": "sha256:1ee82138de6d1d3771aef01180caaca0b2f76708d2917f524824a69d81a69418" + }, + { + "id": "130f2ef9-6155-5c35-9d80-b04d16fcb3fa", + "version": "MTc1NjMyNzg3MDM1OTQwODAwMA", + "digest": "sha256:e25dd868bf97c42f3d7806e1918d5e6dd36d4488612d15b5bf1bdefaadcbb1c6" + }, + { + "id": "13458f8f-fdfd-5aa2-af66-4ebb9295518c", + "version": "MTc1NjMyNzgzNTg5MDM3MDAwMA", + "digest": "sha256:38ea62784376936b9af22580b6508e9b5c2e7e21f2089e457117740d5b1fbbf5" + }, + { + "id": "136ce94a-a3d4-5bfc-8695-2b06f809cf31", + "version": "MTc1NjMyODE1NjQwMTAzMTAwMA", + "digest": "sha256:6fbd8eff02209a00fd52a9de6e37dbd031bf1dcc3b6d7ec1a93c8e32065e3c07" + }, + { + "id": "136fd4e8-126a-5e02-aef7-7f0fe7c44777", + "version": "MTc1NjMyODE4MDQxOTkyMDAwMA", + "digest": "sha256:9e55711c8994f5e6ccecb83b0cf962ca57bebc1091a75a5106882564e7b14f7f" + }, + { + "id": "13b4ca0d-79c5-59fe-bcfb-e67d63af48e4", + "version": "MTc1NjMyNzg2NzYzMjA2NjAwMA", + "digest": "sha256:27f793b96bc1419a022f9645be5cb2beb2c4747c8a6e8683c07d0d5a3f19cbc9" + }, + { + "id": "13b5b8c3-c557-5438-8349-85cd734f7c03", + "version": "MTc1NjMyNzg4NjU4NTcxNTAwMA", + "digest": "sha256:2af3b06d126197b3f81f3484a65d9a8a3954a1794181c0e509d8954b9b01baff" + }, + { + "id": "13d547b2-cf21-5cc4-8f1c-44201fb850cc", + "version": "MTc1NjMyNzg3NDk0MDU4MDAwMA", + "digest": "sha256:3a67dc718bf7e50454bca1749c56fa3c38911645b7903754830d051fa1a3d895" + }, + { + "id": "13e94445-8292-5250-896d-319430d97b55", + "version": "MTc1NjMyODE1ODI0MjU2ODAwMA", + "digest": "sha256:7637a89a029ab932b7c12006e4011e6b68f2e36ea77ac0e4a0cb94e7721ebb44" + }, + { + "id": "13ec3de9-e6e9-56b8-b91e-ca7c6dd3d4c3", + "version": "MTc1NjMyNzc1MDA1MTU0MjAwMA", + "digest": "sha256:4bf76ca27fc706bbcd4d0b2106d0b33c1cc7e44c4695a5cdb05f67a682bbf3bb" + }, + { + "id": "140ba3ab-771d-5968-aa28-45f4b1f4ede5", + "version": "MTc1NjMyNzc3MzAzODkyMjAwMA", + "digest": "sha256:ccd8afb80ae63aa92bde9e8fa4090880ba97f4c95187c05050173f67bacb1749" + }, + { + "id": "14174260-65c9-575b-b073-6a30ba41a771", + "version": "MTc1NjMyNzc1MzkwOTI2NDAwMA", + "digest": "sha256:65692a265d847a702253a292ad564590cd3b4a3681a31c40c20c42c27ac31939" + }, + { + "id": "14175a9e-ed0f-5417-a5c9-e057c886e980", + "version": "MTc1NjMyODE2NTg5Mjc5MTAwMA", + "digest": "sha256:bf4bac26538af1f1ac8bbaf7d292ac555a58230345a457f1e097ba20758c65c2" + }, + { + "id": "141b5160-3c37-581e-94f0-0d2a4270f2df", + "version": "MTc1NjMyODE3MTE4MDE2NzAwMA", + "digest": "sha256:e04018d156428d7dd93aad59c0292737c4e0eb1b51b35493e121ed1bbb07ba29" + }, + { + "id": "14233061-a9a6-5def-b2a7-b415512624d8", + "version": "MTc1NjMyNzc1NTk0OTAzMDAwMA", + "digest": "sha256:036db504318db3fb665c2c01c19de44d824d10436734639b7212764778aca7e9" + }, + { + "id": "1429d3b2-cdb9-5119-b0a6-36456a88b822", + "version": "MTc1NjMyNzg2NjM1NzM0NzAwMA", + "digest": "sha256:7bf92f314ad56ceee0846e4eac0156cc5b55d70540ca50767f3530fb936a3086" + }, + { + "id": "143ad539-3899-59f0-a72a-973c06634b38", + "version": "MTc1NjMyNzgyMjgyOTMzNzAwMA", + "digest": "sha256:695753a307dab3259bf436ae0b47e8547113cb4ddc61286dd68ca80516dc534c" + }, + { + "id": "143fa256-7d87-5dca-bbaf-66550e9a643b", + "version": "MTc1NjMyNzg2OTQ1Mzk0NzAwMA", + "digest": "sha256:ded1c2b2298fc40f9aeb2574bbd01ec10124bb3ff4841bbda892501518034bdc" + }, + { + "id": "14590643-70b5-580b-8ea7-77446bedc6d6", + "version": "MTc1NjMyODA2Nzg5MDU5NTAwMA", + "digest": "sha256:69434e166d18bcfe976b0aa8f6cd83715496d12bf69efc0ea8e82bb3b1695c0f" + }, + { + "id": "146ac439-758d-5fc0-b9f6-87e93566bdc1", + "version": "MTc1NjMyNzg1NjExNTU5MjAwMA", + "digest": "sha256:92d6f341eb9ec765bbdee44f2aeb0195935c6a1e73559d48a4582cc7109fbf18" + }, + { + "id": "14712022-dd2b-56ed-a4bf-aec4078a0b90", + "version": "MTc1NjMyNzg3MjEwODQ0NjAwMA", + "digest": "sha256:4a67cbd07814a45cebc72b8284514901d830b2f2cbdc49c67bebc0c4c13ff3e6" + }, + { + "id": "1473b6e6-99fa-5865-8cda-7946a18bf88f", + "version": "MTc1NjMyNzc1NzUzMzg5NjAwMA", + "digest": "sha256:df045fe41a7ea9c728d2195b2985683a74bcb3c2807a7965256b4578b7f71267" + }, + { + "id": "147bb527-e10e-5e47-8e0b-7bdb0125e252", + "version": "MTc1NjMyNzg0MzA4Nzc1NTAwMA", + "digest": "sha256:d16e5e716c559d2ded7125b170f9ed955b1841b4f0071abd8638594a4079e906" + }, + { + "id": "1484be3d-145c-56ac-b066-dc2bc6f5ece4", + "version": "MTc1NjMyNzczODQ0MDE1NTAwMA", + "digest": "sha256:9b9ec308776c7a45cf0e95cd7b115ff4514d8dce22a257a8233a9a3a484f6744" + }, + { + "id": "1485bcb1-76a5-5367-8514-7365e039f728", + "version": "MTc1NjMyODE4MDQyMzc3MzAwMA", + "digest": "sha256:9259bf17c2028a14abf2d8122666732bf6a6e2e7126b82550472e95f25afeb4f" + }, + { + "id": "149370d2-9880-5483-85ca-320607966306", + "version": "MTc1NjMyNzc2Nzc0MTU4NDAwMA", + "digest": "sha256:ceae2721e3e976b8c1f83bfa995f143510f7d1f3266ad6493b8e765120dc889b" + }, + { + "id": "14e8ac45-2ea4-526c-b987-6bfe8a64edb8", + "version": "MTc1NjMyNzc1MzYzMTg0OTAwMA", + "digest": "sha256:dc60280b063ebfdfa75cb24024facf5179727fcee19915454d7c3fa82f3abfdf" + }, + { + "id": "14f79a2b-8fcc-5e9a-a806-d188a349912a", + "version": "MTc1NjMyNzc0MjI3MzkzODAwMA", + "digest": "sha256:c6ca0b2de678ab9587fafc8d13117d1137e369c8c08cf1ba81215ed6cfa2e093" + }, + { + "id": "1508945e-022a-5e89-bae3-7bd49519b6c0", + "version": "MTc1NjMyODE2NDU3NjIwNDAwMA", + "digest": "sha256:4a5edc888b5962452edea3fcc77d02062ea37af34e5f620c4bec81184d7474d2" + }, + { + "id": "15143c6f-dd52-56da-ac33-341d08e262b8", + "version": "MTc1NjMyODE2ODk3Mjg5ODAwMA", + "digest": "sha256:b8bb047d071d1abdee0b9a5a1e9afe1e3df8485036c20831b54c6a9fc7eea391" + }, + { + "id": "151736f1-ac2f-53f5-a9b5-3038f7a48ce0", + "version": "MTc1NjMyODE2OTA0MzAwNTAwMA", + "digest": "sha256:b61ec42b1b094be80eab6be46151d8222a62ff629856d964222697f8312ecfe7" + }, + { + "id": "1522e085-2238-58ab-99fc-02b128b81d52", + "version": "MTc1NjMyODA3NzU3ODU2ODAwMA", + "digest": "sha256:808677fd3c1bb9c414952ec0deef413d805b17028608081724a682fb2f55aa87" + }, + { + "id": "15260f9a-eafa-55fd-8c82-62e17868bf1d", + "version": "MTc1NjMyODE4MTk1MDA2NzAwMA", + "digest": "sha256:eab4474944e318a66ac435c4d1bfd9d104eb55e06c9e99e16ae42834ac2aac49" + }, + { + "id": "152a6501-cb94-5d70-af6c-5c2d7b0d1645", + "version": "MTc1NjMyNzg1MzgwNTkzNDAwMA", + "digest": "sha256:75b00f0ead6b5915d13d15d2acb47e72e843db857d6093f3f9d7389ef06becc3" + }, + { + "id": "152cba09-fed0-566c-8371-f655d7574c55", + "version": "MTc1NjMyNzc5NTI0ODAyNDAwMA", + "digest": "sha256:234d4db642e8a83e012c6dba4ef7b2c4650d9d5db62d39c47922230aa113c154" + }, + { + "id": "1541a694-784a-5425-a7b4-93ac26de9fa3", + "version": "MTc1NjMyNzc3NDY0MTM5NDAwMA", + "digest": "sha256:8f900063c240d163f58f8d4b283201def49ef3c0de0016750eeb8d256ea328cf" + }, + { + "id": "154d9b85-112a-5eeb-950b-f117833cdc77", + "version": "MTc1NjMyNzc4MTA0NzE0MzAwMA", + "digest": "sha256:2021b308e2cedd53d3a75bbbc0975943fd632363175d2ceb28fd9f30c4274290" + }, + { + "id": "15529adf-f5ab-524e-b2ac-3035de1089b1", + "version": "MTc1NjMyNzgxMzUzNjEyNDAwMA", + "digest": "sha256:1a964f1552e6f659524ab7e5ea01f9f47b193c409d721e4a677ea4300d4fa362" + }, + { + "id": "156a294c-dd01-5897-a05c-7dbf9e16f3cf", + "version": "MTc1NjMyODA3NzcxNDU2MTAwMA", + "digest": "sha256:4fb2564a77e93e4f66b87800ab6ae3c3a9f883a82aaf6f31288199e10a5be3f5" + }, + { + "id": "158de20b-6a8a-58b6-9f7f-e8464746af41", + "version": "MTc1NjMyODA3Mjc3MTU4MTAwMA", + "digest": "sha256:effda3d38447f43e901a3349581d7c6ae4621c0da85c9ba21bb510e70291b56c" + }, + { + "id": "15aa28e3-286c-5957-8cd3-18f47726bf7c", + "version": "MTc1NjMyODE3MTEyMjM5MDAwMA", + "digest": "sha256:8f30adb10d2f225f5d72d6a6a9de34aebebc29ce71558b835463cb9861cb32c7" + }, + { + "id": "15b32fbb-1563-5fd2-90b0-f00f5a7189a5", + "version": "MTc1NjMyNzc4NjkzNTAxNTAwMA", + "digest": "sha256:72a3ebf1a71d46248a4efb7b5d80f1c1918749255fcd9c1518ac37c2ea98c50c" + }, + { + "id": "15cedf37-675c-507d-b1b2-2bda6e8a944b", + "version": "MTc1NjMyODA2NTY0MDg1MDAwMA", + "digest": "sha256:a207cc91a4c18c053805e5e4a2b4871f67d3977bd453d0ff42369b14df2a6126" + }, + { + "id": "15dac6c9-b301-5ef3-9cfd-3bc996a7b701", + "version": "MTc1NjMyODA2NDkxOTA3NzAwMA", + "digest": "sha256:0385f16f8df214ff433a4e6bc6c4fae2bd6859d5fe224883a37bb4cdbd1a8a18" + }, + { + "id": "15e478a6-c899-57e3-84dc-05bfc0afc26e", + "version": "MTc1NjMyNzc0NDQ4NzA2MjAwMA", + "digest": "sha256:970c1b3c152df06799f63b772110c3d33a4a362f3c6053dfbb522df66c42b4dd" + }, + { + "id": "15f2a7d1-6485-59f3-a96a-051fb4e2145e", + "version": "MTc1NjMyNzc1MzYyNTg3NTAwMA", + "digest": "sha256:58226b1bd16a7c24583d21a401ec1c367a64a6547117c822612909b7b4f3854d" + }, + { + "id": "15f6a935-e694-5a62-a79c-649d17e9ae7e", + "version": "MTc1NjMyNzgwOTI1NTg5NDAwMA", + "digest": "sha256:f58fcc3a3c0a83f8563342ec28cf8a3c1a16dbd464b9baef20fe55b40e255d5e" + }, + { + "id": "1615ce70-7bbb-5219-a7e5-46258bf9d5c7", + "version": "MTc1NjMyODA3NDk0NDc5ODAwMA", + "digest": "sha256:50a77daa3e57468a0d3469aa9a5122afa79d7616f5a81e85109343256d02a565" + }, + { + "id": "16292010-5c26-55ad-a11b-f917b9108df3", + "version": "MTc1NjMyNzgxMTk4MTUyMTAwMA", + "digest": "sha256:b73f04f61c8cf6bf5296572abdb1efaea91918952e2bf7a06a5cca76bd1c88eb" + }, + { + "id": "16379872-8221-559a-b370-0de20b801050", + "version": "MTc1NjMyODA3MjIyNjE3MzAwMA", + "digest": "sha256:2652fc65eba3839bdcbb6b6f7701cf5b135573946f62aad6a0a264b83b2f5d4e" + }, + { + "id": "164068db-2ff3-5bc5-bbb6-bb948b8f7bfc", + "version": "MTc1NjMyNzg4NzA4Mjc2MjAwMA", + "digest": "sha256:46d85b5ce44ede6cc563d34ae294895e7efbc9c358e41c49a56732b5a1ed8268" + }, + { + "id": "16511b5b-ff88-5047-89d6-eb846b05ee79", + "version": "MTc1NjMyODA2NDMwNDAxNDAwMA", + "digest": "sha256:5aff36dce0028a2547a8594306914fc5d48e5761a8ba1c8097754cbcdc160564" + }, + { + "id": "169d450c-13cd-5437-a086-dab574b94785", + "version": "MTc1NjMyNzc2NjQ0NjA2NzAwMA", + "digest": "sha256:ae8269c44612deecdd45c06b8c2b642533f84324ad6d44b2f7ea4ee17e83c111" + }, + { + "id": "16c1fbec-b791-56d3-bb2d-ab1da5619419", + "version": "MTc1NjMyNzc1Mzk3NDk4MTAwMA", + "digest": "sha256:8e29a81151f30f78dbab309c60dc0f52f2363bd2ab5861f4754e34a8ab7653c3" + }, + { + "id": "16de2545-c933-5088-b27c-a564200ef5ee", + "version": "MTc1NjMyNzc0MjQ1NTUyNjAwMA", + "digest": "sha256:3095b2942ae4af99160d5c8334b1a20ee5e1acc1b00c9c2effd3fd9787554426" + }, + { + "id": "16e4388d-9646-5a1b-9403-41bddfb087d4", + "version": "MTc1NjMyNzc4MTA0MDA4MTAwMA", + "digest": "sha256:bc2ddaf2e66f618a5766946e8f59f867784269a53beb41373bc438f97ebd0409" + }, + { + "id": "16ee69ba-037c-55c0-a59a-ff87a6fa3927", + "version": "MTc1NjMyNzc5NTMwMTUxMzAwMA", + "digest": "sha256:e57cab6b8e7c3b908468e50437c01e836b40a75ddf0a470dbb41ee00b59b0a2e" + }, + { + "id": "1727d781-9658-53c6-ae0e-4ca3e37e314e", + "version": "MTc1NjMyNzg4MDMwNjk0NDAwMA", + "digest": "sha256:87683f31eccdfd8f2b0b9c6338c751bdd6dbcc8029cc74d9bb890edb8c64bdc0" + }, + { + "id": "1735fd69-a0f3-5ee9-b8a1-3a3d4a7f3726", + "version": "MTc1NjMyNzc2NjQ3MTk3NTAwMA", + "digest": "sha256:295567ea3fabba40f56cd34e29e998a0db9fc3e61c22c92e9b1a525ef93e2555" + }, + { + "id": "1740d3b3-1ddc-5f64-9661-5f6eb81b6817", + "version": "MTc1NjMyNzgwNDQ5Nzc1NTAwMA", + "digest": "sha256:3140570da98d27b4b335d823ff61e92d1ca7e96df8afd46eaa995603ab1bdf97" + }, + { + "id": "1748414f-1d5b-547a-9758-797e259fdbbc", + "version": "MTc1NjMyNzgxNjQ0ODEzNTAwMA", + "digest": "sha256:ce3ffea0fce45b9aea23610b52779fc2b09e7e56d96116f5017ef599fa2544d4" + }, + { + "id": "177f826c-b0b1-55c8-ab1c-87a2eb45945b", + "version": "MTc1NjMyODA3NDUxNzg2ODAwMA", + "digest": "sha256:b3030985a9072166241c94669d74c5031f47d25b89cdec9e493b2477983fe536" + }, + { + "id": "1782f1af-dffc-56da-a19e-8853ffef5430", + "version": "MTc1NjMyODAyMzc2Mzk1MjAwMA", + "digest": "sha256:059fbd88ade362233bdf3639735a8df38bab7b30e0ed4c5f6a492f6e09e4ded5" + }, + { + "id": "1788755a-7fa3-534a-82b2-19b562717ac7", + "version": "MTc1NjMyNzc1NjE5NDY4MzAwMA", + "digest": "sha256:e11e318fa2ab651c43186356ef0320441b6755baadf981dae21d38e5014f2dc4" + }, + { + "id": "178b9c17-971d-51a3-80b6-396b986c25b6", + "version": "MTc1NjMyNzc1MDA5NzYzNjAwMA", + "digest": "sha256:10ef74d60ad19bde423d8346bc22bdd9cab0caad26df50458733c0bf92b89f84" + }, + { + "id": "17ada67d-fa6e-5434-8bdb-f264a12ecc45", + "version": "MTc1NjMyNzgyMjUyMTI5NzAwMA", + "digest": "sha256:181f0f7ee0446c4f762ac39ca8f53c394ebde7bc21e9259a6c4d98649a2d5828" + }, + { + "id": "17b8db58-890a-5ae7-b8d1-bd9e45e1c75e", + "version": "MTc1NjMyNzc1NTg0NTE0NTAwMA", + "digest": "sha256:2e4c64b5f213c23b637aeae31838a450a5f651fa41e4dd620446726e653b66ef" + }, + { + "id": "17de0837-11c8-50ae-ba52-a0770a1ed1d5", + "version": "MTc1NjMyODAyMzc5MjQ3NDAwMA", + "digest": "sha256:e5050a50c13c6e6be97ff06e40fb945125545c5e28744bac4d1c73d5e12b66c3" + }, + { + "id": "17eb6852-be8d-527b-b275-c571acc3e5b7", + "version": "MTc1NjMyNzgzNTcyMzA1ODAwMA", + "digest": "sha256:11a80554262f409a0ec224a7c33db751003379de4bd6f78a59f635791c1b3d44" + }, + { + "id": "17eda263-67f5-5744-b927-f33646d3cf1c", + "version": "MTc1NjMyNzc1Njg4Nzg2ODAwMA", + "digest": "sha256:0dcaf08291751094f762409b2582c7474a1c07ff094ebebbbc4c593c313e4cb5" + }, + { + "id": "182058e0-ad47-5f85-a46d-a669fca59088", + "version": "MTc1NjMyNzgwOTIzMjkyMjAwMA", + "digest": "sha256:48ae66833e20918fef2c1ea573b475ee4e9745bdb5f76ae7202481505085ee97" + }, + { + "id": "18287df6-1f8c-5039-a3d7-a495686496e3", + "version": "MTc1NjMyNzgzNTIwMzYzNDAwMA", + "digest": "sha256:0a9cf7de2c195555c85b0710148b8d395cd699385e633ee7c602ed1e7d599e87" + }, + { + "id": "185acc6a-9caf-5e9c-bfdf-5986bec96584", + "version": "MTc1NjMyNzc0MjUxOTc2ODAwMA", + "digest": "sha256:c1904ac47443d601e524d70f3246b31e0ebf06a4149d72c2dbdcdf6b6e342913" + }, + { + "id": "187decf3-a668-5f1e-bb59-fc3e40f78f2f", + "version": "MTc1NjMyNzg3NjY3NzQ4MjAwMA", + "digest": "sha256:ebf6337eb99bf3cae6b8da8632c098d869ead3a85ab63746c728b05de8841266" + }, + { + "id": "18873fa0-8f0c-530f-882a-ad6da8682973", + "version": "MTc1NjMyNzc3NDU0NjcwNjAwMA", + "digest": "sha256:759708e06a0a8f3a089d6dece237f97a00022d8e0e9e47b008fa2d1de7bee547" + }, + { + "id": "189c3a6d-e9f9-5d0d-88c0-36eb0edcd24d", + "version": "MTc1NjMyODE3MTEzMDc3ODAwMA", + "digest": "sha256:255053808f0c092dd9523faa57426f72f0d24abc5683b8666ce546bd78fc2475" + }, + { + "id": "18a3271b-6d46-500a-96da-702dbc4fd184", + "version": "MTc1NjMyNzc4MTAzOTY5ODAwMA", + "digest": "sha256:628a0bf0ea4c6537aec3f1bf0d07cd2d9dec3b7decdc7555f52eb93fcfce243a" + }, + { + "id": "18c0ef92-fe20-57a7-826b-eaead6ed7ea3", + "version": "MTc1NjMyODE4MDUwOTMyODAwMA", + "digest": "sha256:feb95cab9f13b6a3c67bf3628adc682e01210836e8fc49f7bdd6e54ec318c986" + }, + { + "id": "18c4ef4b-5b45-5b88-83a9-44774b76e0be", + "version": "MTc1NjMyNzk0MjMzNzAxMTAwMA", + "digest": "sha256:2e13dcfe7aafab074cc9962afac649e9dfe068eb110703b9d603ffddbb9b1b32" + }, + { + "id": "18edbc37-2434-5e7d-a706-98e8241e1dcc", + "version": "MTc1NjMyNzc0NTczNzYwOTAwMA", + "digest": "sha256:7b0dab5f587ef656ca4c763c93193a15b6fe641d55daae6a0a2b4d8834f1182d" + }, + { + "id": "1905326f-96b1-537f-8f85-75b09aa948f9", + "version": "MTc1NjMyNzc4MjgyODQyMDAwMA", + "digest": "sha256:7a22f0871fa625e3350dc46f28ddbeaed562da1c5bfbe5710fd6696bb3af9184" + }, + { + "id": "191a2808-8313-556e-a759-db0ad0eb7bbb", + "version": "MTc1NjMyNzg3NDkwODk2NzAwMA", + "digest": "sha256:b697f57cc60d5cf185ad284927c0b140f15e1026954f23d8b676d997b10f59c7" + }, + { + "id": "191f8330-7faf-5be4-97e9-05ef896948d5", + "version": "MTc1NjMyNzgxNTUxMDI1NDAwMA", + "digest": "sha256:a0a13097d777e521df4ece1364c426b628645634eb73fe98fa5162d0f473335e" + }, + { + "id": "192977b2-0e4a-562e-bcaf-4325e3a6ccaa", + "version": "MTc1NjMyNzc1Mzk5OTg1OTAwMA", + "digest": "sha256:d63755fa42a1b5633f04ae90047a4c2ece1321dd021d9806e01e958e3ed87cc7" + }, + { + "id": "19379b1f-c996-5938-a358-ed0c53d0f48c", + "version": "MTc1NjMyNzc3MDMxOTQ2ODAwMA", + "digest": "sha256:2e84afb693231e32f4fdca42d41f4defcfb316f0ed5e367daace87c619607ec3" + }, + { + "id": "1939c452-1278-5eeb-810a-b6f2c269c4c2", + "version": "MTc1NjMyNzgwMjk4OTY5MjAwMA", + "digest": "sha256:47398c5abdba365a93d1fee13e32f5b67e151ae02b2fb3e50b4e313318f44f89" + }, + { + "id": "194635bc-c7d1-5d5a-9de2-eaccd89d83cc", + "version": "MTc1NjMyNzc1NzQ3Mzg1NDAwMA", + "digest": "sha256:816900b872d5525f0f31271937ed17d6b7f282aef2b3ca17157b87595e9b5933" + }, + { + "id": "194dd278-537a-5652-ac1a-86af3f0e376c", + "version": "MTc1NjMyNzc5NTExNDMzODAwMA", + "digest": "sha256:806a480a146cf75caf3d4294964002d2246623a2dc05138c9ff8d5f56ad57ef3" + }, + { + "id": "19576da5-e6fc-581f-868d-23bfca271685", + "version": "MTc1NjMyNzgzNTIxNDIyNjAwMA", + "digest": "sha256:e81d600886279db8873035ef0b2f37efdd238e7abe77b15ec4a63ff487a8cf75" + }, + { + "id": "197f247f-195b-56e4-8b48-61dbb6163675", + "version": "MTc1NjMyNzgxMzYwNDE1NDAwMA", + "digest": "sha256:5ad4bedfd4c19e64fea0c0bbdeed34cd4607d0a01a989fad6739fc24590d7dc3" + }, + { + "id": "1990b391-edab-51a1-8e3f-ebbfbfa3d569", + "version": "MTc1NjMyNzg4MTkwMDI4OTAwMA", + "digest": "sha256:1ddea98b02cecb74c26a6ec7d4d78d283574f542d12ce7954e4551d2b5c33bf3" + }, + { + "id": "199a58e4-5478-5a6d-8a3b-d810306a1b10", + "version": "MTc1NjMyNzc0NDM3ODkzOTAwMA", + "digest": "sha256:165e1077846261921445de0dfe2641415f06d3d2e10bd6e25bf3e743c44f6fe8" + }, + { + "id": "19a64621-e5c5-5760-9450-3c15433b5281", + "version": "MTc1NjMyNzg3NDE0NzEzMjAwMA", + "digest": "sha256:8d5ca5a48101618a9fe9958ded8a62cd6320597b7b7b62b82eec62cc85d69764" + }, + { + "id": "19b7df3b-b8dc-50da-95a4-b09473ff25fc", + "version": "MTc1NjMyODE3NjE2MzQ3NDAwMA", + "digest": "sha256:9cfe0dceaa082c7d14489ed2483eb2c70ded5db126e35a5ec81dc6afb232a1fa" + }, + { + "id": "19b8351a-bff6-5cd7-b228-02fe12001d4e", + "version": "MTc1NjMyNzgyNjI3MTU5MTAwMA", + "digest": "sha256:e089beef565ec84252ad95f8a9adc76b349af66132f145c0c8f53afba279a672" + }, + { + "id": "19d31429-2354-506b-841c-53469ced4c82", + "version": "MTc1NjMyNzg2NzYzMjAzMzAwMA", + "digest": "sha256:7afb470ea4116709ef2b54b830a53c46ff87920e9521168f61431b5ba8e7b3d7" + }, + { + "id": "19f906d2-c67f-5a36-8584-6b8f5c6166f1", + "version": "MTc1NjMyODA2OTMxNTQ1MzAwMA", + "digest": "sha256:039146abf06bee917ce6a7e919ea04cc73c2e6029e186b85b749ae0b493b0219" + }, + { + "id": "1a1441d9-ac4c-5918-8d9c-62889331a663", + "version": "MTc1NjMyNzgwMzY2MTAwMDAwMA", + "digest": "sha256:34b74cb63b841d067837c783ff7a79b2a24baa4df2e1fef6ac63cd12cd76c0ac" + }, + { + "id": "1a1537d4-e009-5fdf-958f-bb4d1c933d9d", + "version": "MTc1NjMyODE2NjkzMzI3NDAwMA", + "digest": "sha256:c188b686c9e3f0a1ce5c0fbf9df30362bcb9433f38ebc9867b96d19647507ca9" + }, + { + "id": "1a1a3821-ee75-5334-a831-628c089a0a7d", + "version": "MTc1NjMyNzc2MzY5MDkxMDAwMA", + "digest": "sha256:bc7dc35f2e9b09e798199558cd28a2469deaecc7a5d116fd1481f27c1bf020ec" + }, + { + "id": "1a22c713-efa0-5545-b1d4-3acac7c0d0c6", + "version": "MTc1NjMyNzgxMjc2MTA4NDAwMA", + "digest": "sha256:0045f4082bec58d005eb451b35e17aa3c8aba3bf0a78c8df74fa1c0337e692c1" + }, + { + "id": "1a3c4c1a-0734-594e-9fda-51421b6672bd", + "version": "MTc1NjMyNzc4Mjg5MzQzMTAwMA", + "digest": "sha256:7ed391ebe1a07ee8a488afc5e4d7938d1aa10ced75770ebceeacf4f6c5ccf367" + }, + { + "id": "1a46a028-e640-58bd-ad68-0fa4689e6719", + "version": "MTc1NjMyNzgxMTk0MTI1NzAwMA", + "digest": "sha256:c6e169ebcaefbbf2cacf78438073e3496158e2969d8d8f9cdae5ed90d863b077" + }, + { + "id": "1a5aeac4-e344-5fd6-91b2-5ec867572f27", + "version": "MTc1NjMyNzc4MTkwNjk1MTAwMA", + "digest": "sha256:54a0f035859838f432b344a546f20a7b064d5d7044b992e3668df907238f360b" + }, + { + "id": "1a782dd4-3dfc-5d7c-8165-17cc9ba46255", + "version": "MTc1NjMyNzg2OTUyMTU4MTAwMA", + "digest": "sha256:f4538c711e6c00eb1beea49453006eb9a02570130f752b9cbea25c4d044a4b64" + }, + { + "id": "1a927ee3-f3f9-54d6-9274-1d94cc459f9a", + "version": "MTc1NjMyODE4MTkzNDEyMTAwMA", + "digest": "sha256:94933586970a66d93041e6421cefae2c7de5b51ef87b06039e3fc7bc6ba0052f" + }, + { + "id": "1a974f12-4e6d-56a8-99d7-c707fe1788f8", + "version": "MTc1NjMyNzc4MTA0NDk3NDAwMA", + "digest": "sha256:f85599651178bcfefe4b3df40f60428b22d105584870c0eb8c3fc51270b6d268" + }, + { + "id": "1aaae478-c465-5fdb-a8ef-073422b879f5", + "version": "MTc1NjMyNzg2MDI3ODg1NDAwMA", + "digest": "sha256:b1b551378d59cd2827a8966160e4b7b4745773e2127c2f91e0e30e7f77920d34" + }, + { + "id": "1aceb082-57d3-5d8b-89e1-9c077909871f", + "version": "MTc1NjMyODE1NjMzNzQ4NDAwMA", + "digest": "sha256:9e46f29d20362a81b5a335b876ac6d2e59d3228ad011eb374d545de9fb171032" + }, + { + "id": "1ad97e61-2051-54bd-9542-229c5ca808a5", + "version": "MTc1NjMyNzg0MTA3MzEyNjAwMA", + "digest": "sha256:b9a2bd292e5c8bdc98059364b4beefb7ce32400d4e72efe4bfd9a9385b57123e" + }, + { + "id": "1ada1d5a-32d4-5ebb-88c2-8dc895b473df", + "version": "MTc1NjMyODE4MTg1OTI1ODAwMA", + "digest": "sha256:5ff4cfb971fdf603f247588af07f4131f26cfbe0fd02d242e75891f70949fe1a" + }, + { + "id": "1b1560c1-0401-5b7f-9637-366dfb5f2061", + "version": "MTc1NjMyNzgwMzUxMzAxNzAwMA", + "digest": "sha256:ba21563255e111d34377a47f1b01b22057654594c193e2880445a688e4e0a39a" + }, + { + "id": "1b3b988d-8486-5a6e-9641-808c743bdc30", + "version": "MTc1NjMyNzg3NDEwMzk5MzAwMA", + "digest": "sha256:5a2a5e133724b0583ce3d7219c98a0f3fee2871a8eda2919740f8ffcdb52b4d1" + }, + { + "id": "1b49c813-3598-5ed0-9588-54b1279f2890", + "version": "MTc1NjMyNzgzNTY4NTE3MzAwMA", + "digest": "sha256:0d9823cdb46cb7fd3cf6aa493d75eff86fdd203dce06e2549483295931f4bc9b" + }, + { + "id": "1b4e1e97-10ca-5743-ad63-8e0c31ae91e0", + "version": "MTc1NjMyNzg2NjI0NzM1MjAwMA", + "digest": "sha256:f41399ddfce02d079ccffe8631059b95e392224e8124f28ca3897899d952a48d" + }, + { + "id": "1b70fbc9-715d-542b-ae58-d26924a9e431", + "version": "MTc1NjMyNzgzNTc4MDU0MTAwMA", + "digest": "sha256:6ab2a5cc58416f657d121234dff161e85d5c6a0c09b493b3759212cd4dbfd608" + }, + { + "id": "1b768233-9fcf-5333-bb36-8da9ab7a3c80", + "version": "MTc1NjMyNzgxMzQ4MjkxMzAwMA", + "digest": "sha256:dd1725f009014cd119bd1c9d5c3cc70c6af9f99d49f40dfa6bbfac60184c871b" + }, + { + "id": "1b8ec29d-5454-5bbb-8ac4-572d0a2e4726", + "version": "MTc1NjMyODE3NjIxNzk1NjAwMA", + "digest": "sha256:a224b93b0ac44d3e3b66dee722ec34478915d10a0e41357283b9ecc9735a65f0" + }, + { + "id": "1b9c9fa5-a448-514a-86e5-35796d17c3bd", + "version": "MTc1NjMyNzg2MDI3OTUzNDAwMA", + "digest": "sha256:c31e9a2ea373d0e018e2ec528b49a6996838ff349c6fd348badbba5cf7fbca3d" + }, + { + "id": "1b9d7607-f8a4-56f4-a301-bdb1d7fd78c7", + "version": "MTc1NjMyNzg3NjY3NzQ2NzAwMA", + "digest": "sha256:7a80676e441b7fa59d1eaa5b08525557afd35e7aa23a79708c285f27081a5be8" + }, + { + "id": "1bbc514f-227e-5418-95d0-36dfe2dc2e34", + "version": "MTc1NjMyNzgxMzY1MTM0MTAwMA", + "digest": "sha256:75345daf640e37fa35b02f2a80d1bd1115f8d7a3cae3444bdd169ddcf08cde16" + }, + { + "id": "1bc75252-83e0-585c-b9da-e8b893f8c958", + "version": "MTc1NjMyNzc0MTc0NDE1MDAwMA", + "digest": "sha256:67108c24c92b4a4eaf64cb50a75f03266dafda17449d3ac701344460120543ee" + }, + { + "id": "1bc75f2e-9ddb-5210-88c0-dda78c7050a1", + "version": "MTc1NjMyNzg2MDI4MTgyMTAwMA", + "digest": "sha256:373ae00eb06eadb46c703dde00b4dd7fe69ddb56014f54b51c4fbb5168627e3c" + }, + { + "id": "1bd03b2a-dc84-5219-96ce-711862f78a6a", + "version": "MTc1NjMyNzc5NTE1ODc3ODAwMA", + "digest": "sha256:a6280ab316b8e852c9b23e2e473f28dec9e6498f029467ab77499e48002525b4" + }, + { + "id": "1bd89bd6-6b06-5448-82c5-665c76dbacce", + "version": "MTc1NjMyNzc3NDUxODg1NjAwMA", + "digest": "sha256:8ca6c363abc508bbe630ca943b00269862a7b3b09f7715e2f483230873a4de4a" + }, + { + "id": "1bdeb8ca-2ab6-500f-b539-e369957e8f8d", + "version": "MTc1NjMyNzgwMjkxOTI2NTAwMA", + "digest": "sha256:1a9838d4831d02301d4d67295e80806caea3097d1f447c304cc04aef3a58842c" + }, + { + "id": "1be27a2f-ceb3-53a4-aaa0-e559a859b76f", + "version": "MTc1NjMyNzg3MDY3NzE0OTAwMA", + "digest": "sha256:90aab5b006ed4ac808ba29cf2180262e4e2384cc4c5e2a4e6e2c3c0c26613c37" + }, + { + "id": "1be35afd-71eb-56c5-a5d3-561c2a6d3e0b", + "version": "MTc1NjMyNzc3NTA1NjU5MDAwMA", + "digest": "sha256:b5a72eacaa06e269a5bc8593439beef0cd1040e7f702fbbd9bec787fd48d8704" + }, + { + "id": "1c0e255f-0885-504b-8ede-95f3f51335f8", + "version": "MTc1NjMyNzg3MTUwMzkzMjAwMA", + "digest": "sha256:8a6272f9270832646e5e91678acf23c2435700581908f93c0d31d48530c9091e" + }, + { + "id": "1c213383-9cac-58d1-913c-d2b725abda6d", + "version": "MTc1NjMyNzgxMjczODcxNTAwMA", + "digest": "sha256:d1795bc446002f8d2f298062d9b95ad33f2a965ab35f7728f21bb77a578d205f" + }, + { + "id": "1c38b1a9-4347-50ad-9775-21a98e45f821", + "version": "MTc1NjMyNzg0MTIzODYyMDAwMA", + "digest": "sha256:a9a0d9eeeb0567f0c760d3bbe7ec38cd909803f942de42f44f660ec8a04db369" + }, + { + "id": "1c3d09bb-dda8-54c6-a413-d8bbb13b1c3c", + "version": "MTc1NjMyNzc0MTc2NTY1NzAwMA", + "digest": "sha256:5c9e350d835bbd16ce919b09e55d7bf851c5da09e37e69f641336869f492029d" + }, + { + "id": "1c3e790b-5b8f-5729-990a-7663c44e12f9", + "version": "MTc1NjMyNzc4NzQ2Mzc2MzAwMA", + "digest": "sha256:d5c6d7b123770265ee6b4adf0e71e44818b868c209af5ed6329e0835134172de" + }, + { + "id": "1c4ce200-2e7c-578f-a8a2-01856cc0a89e", + "version": "MTc1NjMyNzc1NTk2MTUxMzAwMA", + "digest": "sha256:3ca79bcae53f6837ec4846cba095ba6b1a40fe0b4d7132c9df1638273baa3a0d" + }, + { + "id": "1c54bf62-ca5a-5295-870f-38739f405f22", + "version": "MTc1NjMyODE4MDQxMDU0MTAwMA", + "digest": "sha256:d04b312aa3faa1f6e6e3b9011dd26de75ef83ee40155e4fde9b60706069e7172" + }, + { + "id": "1c6be51f-6b3d-51ba-b646-02f9ae705598", + "version": "MTc1NjMyNzc4NjkyOTQxOTAwMA", + "digest": "sha256:56f7840960f50422d8c47e3318b4a1a36c96844ff82d94ff5462ff7f4e9113a3" + }, + { + "id": "1c7ee845-18a1-5bc2-9c29-5dead4ec9bda", + "version": "MTc1NjMyODE2NDU4MDI0NTAwMA", + "digest": "sha256:38a66b21fc0a845c158690d26658d6671e052abd21c66eb0c78eea2ed443935a" + }, + { + "id": "1c913a11-5559-5f89-b013-4d82f2113382", + "version": "MTc1NjMyNzg0MzE0NzA3MDAwMA", + "digest": "sha256:9a684597bbce61bdb88e26c2095e283a9fbaa7b124ce5a4a5b060188aa4cc766" + }, + { + "id": "1c9cf0d4-0920-521b-8b30-301a053e6bde", + "version": "MTc1NjMyODA3MjI5ODM5NDAwMA", + "digest": "sha256:3c07c59f72efd1c3551fd9cdde3ec7b8de7101e0306ff47b3873d72dac3b4671" + }, + { + "id": "1c9f7e6a-31ef-545e-993a-c99935b931fa", + "version": "MTc1NjMyNzgyMjc2MTAzNjAwMA", + "digest": "sha256:c25412393a8832307284acb22aa26397776b396d73d942e545619b829326113a" + }, + { + "id": "1cb540b4-2d66-5e24-a5f9-9e274c44fffa", + "version": "MTc1NjMyNzc4NzQxNDgzOTAwMA", + "digest": "sha256:3cca6e9166dda1f29244bb522f16c36cd11f805d2b0d738023cefc62073d0fed" + }, + { + "id": "1cdcd9ee-14e2-5e60-be3c-1439b241eada", + "version": "MTc1NjMyNzg1MjgzMDQ4NjAwMA", + "digest": "sha256:0b5cc5dd5c4ef3c7c0f57e8d8c9e9fcbe9b92e4f21002e104fdaf63182c9c27e" + }, + { + "id": "1ce55e44-9233-5a8c-898e-e18842013d0d", + "version": "MTc1NjMyNzc3NDYzMDcyMDAwMA", + "digest": "sha256:2d3efe60dcadefe417e4dda96fb74e940a34229729e56590e67046a4ca516cb9" + }, + { + "id": "1d3bff17-bc15-573e-8a88-f0bdb5563e25", + "version": "MTc1NjMyNzg4MDMwNjAwNzAwMA", + "digest": "sha256:df8721073bee7bb728e3aadefdfd70520242da89bdc3e19ade479ae398c5d021" + }, + { + "id": "1d555be5-215f-5ce2-aa4a-8faa681a0f08", + "version": "MTc1NjMyNzgzNTE3NDM3ODAwMA", + "digest": "sha256:c280756b129f5c533b7ee6962140857e81e5cdbd04fcb286b0b9e9e763f038ff" + }, + { + "id": "1d55b265-e276-5b46-9e87-c75fda3f0d02", + "version": "MTc1NjMyNzc2NjQyMzkyMzAwMA", + "digest": "sha256:984f5fafe301f4cc5c3ceb51aff0fe4e8343ae5bf1bb403ac56ec1a851f18e9c" + }, + { + "id": "1d78472e-8553-588a-b202-116793419f86", + "version": "MTc1NjMyODE2MzM0NzMwNzAwMA", + "digest": "sha256:ba40c43cc8d5e33eb307529e60a3f583ebd38cd9f3aaffa0eff0849bd8f5d569" + }, + { + "id": "1d86b7ce-57c1-5461-b334-5a1512b166f9", + "version": "MTc1NjMyNzc5NDc3NDAwMjAwMA", + "digest": "sha256:2e619aa342ce7a5fc85946efb21e02c327dc3edf69cc4dc4e73046164e54563f" + }, + { + "id": "1d8bcd03-e8ae-531b-a76c-eeafce385de9", + "version": "MTc1NjMyNzgzNjM2MDc3NDAwMA", + "digest": "sha256:155d5ca18b4bd30a8736a5d405e82d8ca4986836845b2f8b1fcfa9a7dbed7942" + }, + { + "id": "1dba640b-9348-5ba8-8b0a-3922a07165ed", + "version": "MTc1NjMyNzkwODQ5NjIyNTAwMA", + "digest": "sha256:1c897d0d6c3ef1d014530e729b42da7950c116ac7891ee82ff0cfb6051b6887c" + }, + { + "id": "1dd8195d-815c-5c8f-8405-2acf3995af8d", + "version": "MTc1NjMyNzgxMjA0MTcwMzAwMA", + "digest": "sha256:78a85c12b084920af886050fe6826dd2aa0cbd89d451b507e336e761beace4ef" + }, + { + "id": "1de02d98-1657-5329-8e0a-003dcb854ce7", + "version": "MTc1NjMyNzgzNTg5MTEyNDAwMA", + "digest": "sha256:98dc570dd85a62f523aa9156d072f3b2cdc3580feb644495ab360744031d59f1" + }, + { + "id": "1de10356-0ac3-5f0e-83db-d1ffb3093f31", + "version": "MTc1NjMyODE1ODE4MjM1NjAwMA", + "digest": "sha256:d4ae808d2ed455658f563474a6657d16f3de44872292504588919b862dc40b5f" + }, + { + "id": "1de66ede-d959-5ebc-878a-7c6cb973089a", + "version": "MTc1NjMyNzgxNjQ1Mzk3ODAwMA", + "digest": "sha256:beab07b917de7fc47f727ff1428033b4b2de6f59dd87ac57e7b33d45c039cd35" + }, + { + "id": "1de945a5-6838-5bbb-b919-d70b95b9afaa", + "version": "MTc1NjMyNzgxMjYzMzg3OTAwMA", + "digest": "sha256:4f5f349ee9e4e96b80a309176df5d6ddeeace246e23d69f3eba0434004d077ca" + }, + { + "id": "1de99df9-d5e5-5d35-a158-265dbfb74723", + "version": "MTc1NjMyNzc2NjM3MzAxMjAwMA", + "digest": "sha256:0cba9bd163721e3aeffdb6d3e9f608a392a287a61fe397634000f3e703eb8a2d" + }, + { + "id": "1dfd7d37-9c9d-5182-bfd0-561cfe696c47", + "version": "MTc1NjMyNzc2MjUzOTEwOTAwMA", + "digest": "sha256:6e2f2f9d80e59c5f3415ff2502b0555d2ff9d900ded15c0d38e7aeb0f5ae3749" + }, + { + "id": "1e05cb75-0dae-58a1-b7f1-8b7debc9ef0f", + "version": "MTc1NjMyNzgxMjY4NDEzODAwMA", + "digest": "sha256:e29b202f9882c00d0d1b4084d228d87190e06116d513e368c6d4b0e09387df55" + }, + { + "id": "1e419ace-b382-5300-a1f7-730112557fb3", + "version": "MTc1NjMyNzc4NzM0MjU1NzAwMA", + "digest": "sha256:e204abe41cc9be1c86768e5ffaf804088f0503b49064803f65b1ac4c75624981" + }, + { + "id": "1e427e14-7d8a-5315-9368-8bed7519bee9", + "version": "MTc1NjMyNzc1Njg2NTE5NjAwMA", + "digest": "sha256:12ce13cf5b16196b96201d7f42f82f704fbf71ab54ca709dcb93ba43c7b8854a" + }, + { + "id": "1e467796-0e1d-5dc3-93dd-3428d6055807", + "version": "MTc1NjMyNzg4MDQxMTUzOTAwMA", + "digest": "sha256:4cb4c029bf678ed72da12c3b1da00931d64435079ad10e6aa6c502d96a368ffc" + }, + { + "id": "1e48e0a8-77a1-560e-bfc2-f6f87b3ddcb2", + "version": "MTc1NjMyNzc4Njk0NDMyNjAwMA", + "digest": "sha256:7f071685bff499372ff01a2c1c8b9dc548e8e72183c4383e2bd64c01d5240655" + }, + { + "id": "1e5bd449-659b-59d9-bc43-46f3f9ecaae3", + "version": "MTc1NjMyNzg2NjY0MTQ0MDAwMA", + "digest": "sha256:a95eedb7b9d38f20e13aa6268f5b5b0d12236dde7ef018ae0476640c0c604a95" + }, + { + "id": "1e6a21b2-e677-5c5f-a3ce-c8846e4fe6e5", + "version": "MTc1NjMyNzg3OTk1OTY5NzAwMA", + "digest": "sha256:9c065e7fe6b0e92a8da5f00848198fe56a762a239d559562492c8a029e076df5" + }, + { + "id": "1e75c625-e110-57f7-ab6f-bb1b75efda1b", + "version": "MTc1NjMyNzgzNTY4NDM3MTAwMA", + "digest": "sha256:5ff4e69f6a925ac59c1e45cbd258185bbe2ecb35f6b00e032970d4875b17c6bc" + }, + { + "id": "1e9495c8-bd27-5fe3-bc71-5a0f2e078658", + "version": "MTc1NjMyODE2NjkxMjgwODAwMA", + "digest": "sha256:6f0621458720981a2226772a9fde294e28f6335edd18e7c094c48f1aa66ee2ed" + }, + { + "id": "1e9852a4-7b30-54c5-9c48-af4d06e03f49", + "version": "MTc1NjMyODA3MTY1NTkyNjAwMA", + "digest": "sha256:6a047d6363189ba4a46373fd0df75efa13db5f375908f1a643fc205cbdf52732" + }, + { + "id": "1e9d6685-45eb-5a11-b5a9-6347f580ee9e", + "version": "MTc1NjMyODE1NjM0MzcxODAwMA", + "digest": "sha256:933febb19f37c9a6c4c6a8a8b7284fe21d421e514b68e3587114feb1de947a6c" + }, + { + "id": "1eb04d12-26a1-50a6-ae8b-6def074071b1", + "version": "MTc1NjMyNzgxNTYyODQ0MTAwMA", + "digest": "sha256:28b83378395f65561923be26f8c8f4f010b85ffea25f42e13499def121094627" + }, + { + "id": "1eb69b6c-af80-5e82-a750-faf8d32cc95a", + "version": "MTc1NjMyNzc2Mzc0NjkxNzAwMA", + "digest": "sha256:3443c163a2558b108c725d911b31849cf1e4fa6d8435a0aad6a7f6ecd7d83907" + }, + { + "id": "1ec6e71b-b7f4-5177-92f5-c639f4178e4e", + "version": "MTc1NjMyNzc3MjU0MDA2ODAwMA", + "digest": "sha256:f7ad51814953b1d255cd3cc3e5ccc3ced800b94f416b1839d84b6d65b540edf2" + }, + { + "id": "1ecd997f-5911-5f2f-9850-31c7ea63f097", + "version": "MTc1NjMyNzgxNTYyNzk1OTAwMA", + "digest": "sha256:95b34d34d72cbb1a99a512e6c52e6b99e35236c0870067e1e88bf405a74f35a1" + }, + { + "id": "1eecd9fe-0428-50df-bfe8-8ac7b52c8cbd", + "version": "MTc1NjMyNzc5MDI3MzkwMDAwMA", + "digest": "sha256:3881e66bafa6c321d0a8e90695941215b648ce8abe714e7c9a0f92bb1e2659c2" + }, + { + "id": "1ef2afb5-5070-544f-8ccf-3c5b5c93d0f4", + "version": "MTc1NjMyODE2NDg5NjM5MzAwMA", + "digest": "sha256:f1645c116b416cafac94867acf6e999884be733866d421a3b53716220a76d046" + }, + { + "id": "1ef825f1-4787-59d0-bba4-b58c556b2f80", + "version": "MTc1NjMyNzgzNjQwMTkxMjAwMA", + "digest": "sha256:3a07f0be718f4208136c233937b9c37ed17de1357ed117442e5b9f4f6344fba6" + }, + { + "id": "1f01d8b9-b54c-5ad0-b63d-123d9dfce521", + "version": "MTc1NjMyNzc3MjYxMjAwMjAwMA", + "digest": "sha256:7d7a71017b18fb518aab5f76a123211d833c58376c91f3864307862ca0c69e98" + }, + { + "id": "1f10dd63-08cc-5ddb-b1ce-0b19ab058dbc", + "version": "MTc1NjMyNzc4Njk0NDQyMDAwMA", + "digest": "sha256:32a3a02378506d61783de359288181291da6489c18cdb12ff1afb85498f8f4c0" + }, + { + "id": "1f269892-93d4-5599-b77f-85c6656eee50", + "version": "MTc1NjMyNzczODQyNjA1OTAwMA", + "digest": "sha256:ce0da25445bc9293c2d4dd6f7d6cc28a93eace31d8ed8359888e31ccdd6ed7c5" + }, + { + "id": "1f413ed8-93b8-5132-89a3-04b0c6e5d4cc", + "version": "MTc1NjMyNzc5NDcxMzk5MjAwMA", + "digest": "sha256:82e5bdc7bb1e45f1316e3fd5c315c68dfdac6d1bd71191ff89c2deca03028ff4" + }, + { + "id": "1f976548-cedb-5e1d-b3c3-4cf75a84e715", + "version": "MTc1NjMyNzgzNjI3MzMyNTAwMA", + "digest": "sha256:93db306d7ca062adc442453ffb47e4ac932a1e24acf10759c491d0a8610bb25c" + }, + { + "id": "1fb79c85-e545-5fc2-8b9f-c9b8d93e451b", + "version": "MTc1NjMyNzc5MDM3MzM3MjAwMA", + "digest": "sha256:c75066e82f167da4961dd4b47b997a857dc717786569a79a185d66203dfcaf4f" + }, + { + "id": "1fde24ce-cbaa-5254-b092-f567a264d11f", + "version": "MTc1NjMyNzg3NTUwMjU0OTAwMA", + "digest": "sha256:8a3f3da849dd3ef31aa1a206286e9b499ec792091c535cb580a8f9a92252bca6" + }, + { + "id": "1fe7755b-e1f0-57dd-b26e-5cf7e9487a63", + "version": "MTc1NjMyNzc1NjAxMzcwMzAwMA", + "digest": "sha256:5ecd9819a4cd7ab7b08546070217d61e469c6eef06c383f61ae4af0706a24e4a" + }, + { + "id": "1fee6b7d-346c-5eb8-b50e-b80dc457c2c4", + "version": "MTc1NjMyODE4MjE4MTc3NzAwMA", + "digest": "sha256:0d7a5e5de03712ebc235808c44a7e515c380649f071ac268bc3f01acbba51e06" + }, + { + "id": "20146367-04a8-5035-84b2-96629bfad102", + "version": "MTc1NjMyODE1NjMwOTI0MTAwMA", + "digest": "sha256:6859f3b6c525342a768e78a67182c4d683ab54c2dc100537ea49a6274785fb57" + }, + { + "id": "202b6a4e-30fe-56c9-ae44-90288cef6480", + "version": "MTc1NjMyODA3NDQ5ODg2MjAwMA", + "digest": "sha256:7c9e87420f89543641ad7684085e50eb33811ac21a38a7ed4950ad5bbb03e631" + }, + { + "id": "202cd409-1662-511e-a8ff-53528ccfda98", + "version": "MTc1NjMyNzg1MzM3NzIxNjAwMA", + "digest": "sha256:08da819c3e65be8bfa6f9146e47b6294efa860a0809aac7bcf0aba9061c7e213" + }, + { + "id": "2032a377-b320-56c5-b44b-fb017d0e821b", + "version": "MTc1NjMyODE3NjIyODIyMjAwMA", + "digest": "sha256:980a748f07a35ab88a07189b74274b5b30d19c4c7a45a8a535d4931c1b601338" + }, + { + "id": "20332610-f8de-5c01-8cde-13b44cdf5f51", + "version": "MTc1NjMyNzc1Njk5MzkzNjAwMA", + "digest": "sha256:91f642ce686e686b5af240158a9484e4a4f0be0b84efae62a07a7e6ed44e2e0f" + }, + { + "id": "203ddbe9-8449-53b5-9faa-6158d421bfa3", + "version": "MTc1NjMyODE2NDUyMDE1MDAwMA", + "digest": "sha256:66240eb691948ad75f8babed9ef6c4e934cb8e90b0813c7fe454cef76bf75078" + }, + { + "id": "2041ed22-00ec-5aea-a383-ca4d059ebaf6", + "version": "MTc1NjMyNzgxNjM5NDMwMjAwMA", + "digest": "sha256:88fc4819d49fd0e14c517891b3ff9c17333d420bf9ad34a29b72f9fe8e3c4eae" + }, + { + "id": "204d7ea2-0c3b-5ae8-9631-6d238d1d957c", + "version": "MTc1NjMyNzgwOTIxMDU4ODAwMA", + "digest": "sha256:7f896fd4bdd0525f096beff5c01d834fa14cecbc40fb8783340500e3c0d7d974" + }, + { + "id": "20673f3d-ea3c-5064-aee9-0081579aec23", + "version": "MTc1NjMyNzc0OTk0NDA2NjAwMA", + "digest": "sha256:a29b0aeb264b08368a31766b9a12a20af2bedb870a192c97c207edf53b5fed9b" + }, + { + "id": "207a73cd-ef4f-5c99-904a-dd757e52ca7c", + "version": "MTc1NjMyNzg3NjU4NDIwOTAwMA", + "digest": "sha256:13c02f854b59c7212299551071613feab5384d84d220ebc1a3316836a8b39828" + }, + { + "id": "2097b1d0-c3d9-56e6-b9e5-8d5eda56020b", + "version": "MTc1NjMyNzgxNjQ1MzgwNDAwMA", + "digest": "sha256:ba3d4787f6e423b58da8e4f244999e47064aac1a8f45b54feda6b42f022a2a69" + }, + { + "id": "20a3fa52-e15d-5716-ab3c-a94085cac309", + "version": "MTc1NjMyNzc0MTc2NzMwNTAwMA", + "digest": "sha256:76cb93210399366fdc56de0e50d93f411df5569a6b2dd199b5ee4e9ed45e3648" + }, + { + "id": "20ba02ce-3cb6-5473-9c87-0e49845ab3b0", + "version": "MTc1NjMyNzgwMjkzMTk1NDAwMA", + "digest": "sha256:35578a4f73594cb6b1f502956ba856156e5241cedee3188bad8f7188777c6f7c" + }, + { + "id": "20de163d-5517-5b7d-b7c6-cfc8de78ce1b", + "version": "MTc1NjMyNzc3MzAyMzQ5ODAwMA", + "digest": "sha256:89a69f33c6502f3e4aaf88ea2185c3562b0216f553304852691455947546d409" + }, + { + "id": "20de42fd-bcca-5f3a-99fa-b3a60d24034b", + "version": "MTc1NjMyNzc3NDU5NzQ4NDAwMA", + "digest": "sha256:207e06974720deaae75f79c67a1edbdc73bf88c5dde1bb56eaeba6dcaa32c082" + }, + { + "id": "20f54f40-fa51-5064-ace0-51ff6bf82869", + "version": "MTc1NjMyNzkyNDExMDM1NTAwMA", + "digest": "sha256:5e36136eb8cee8c903cfaa077304860fd00862dc13601e5858251bdff19fe53e" + }, + { + "id": "21069df6-0434-52d4-99e5-15e7a1f3cd61", + "version": "MTc1NjMyNzc4NzQxNjQ0NTAwMA", + "digest": "sha256:63f21009af12c5d715a29b7ced1032bf63a82f972fc477d5daa5c6ed4ecff19f" + }, + { + "id": "21074086-97e9-5064-ae84-21d2f18ea47d", + "version": "MTc1NjMyODAyMzgyNTcwNTAwMA", + "digest": "sha256:a4269b40b4be05b67bdd2dcef6006d18fdc4b265acb7ce3a872264ba64770d12" + }, + { + "id": "211e473c-ed40-5a66-9d3c-03f6ec40aace", + "version": "MTc1NjMyNzg2MDI3ODc3ODAwMA", + "digest": "sha256:1e5379e4452afe5e8b9a139bf093beb1bbe6d68bdc9eea50c098f0f2d5352a88" + }, + { + "id": "212d8ba0-c360-593e-8ba6-7d2cd4e02b49", + "version": "MTc1NjMyNzc1MzUwMTM4NzAwMA", + "digest": "sha256:4dbe9f25fa8e61f8ac0f90373e647815a7fcf854dc14b5dd93e9dcdfaecc4bf4" + }, + { + "id": "2154597e-b839-574b-ae92-500e673951ba", + "version": "MTc1NjMyNzgyNjQwNjYwODAwMA", + "digest": "sha256:8fc82594aee61134549b6d7d0f72ee985bd5253a73e3251b3b30e0579b8d3ebd" + }, + { + "id": "21666903-1fe0-5199-a573-1be51a9b84a6", + "version": "MTc1NjMyNzc4MTAzMzYwMzAwMA", + "digest": "sha256:9686e712e534a6dbefdcaeca89bc2142280666ba8bd9168559388df48f9e8e1a" + }, + { + "id": "2198a940-7e2f-53d5-b72f-4796f30163d6", + "version": "MTc1NjMyNzc3MDQ1MzUzMzAwMA", + "digest": "sha256:40f202a9140e25d68dcfddfbbed944b00a9f3b984e1dbb18992c13ce28e3071e" + }, + { + "id": "21aca1f6-fba6-5f55-9590-7ec73fbc2a4d", + "version": "MTc1NjMyNzg2NjY4MTM1MjAwMA", + "digest": "sha256:cdeeba25b16f3d4cce4f63655ff692a942bdad8f6fb2e1836bb124bbf45d43f8" + }, + { + "id": "21aeec7f-48ea-5862-8197-e9668ca685c2", + "version": "MTc1NjMyODA3MzQ5NTA3NDAwMA", + "digest": "sha256:8a1f53458e27d356e2bb4509935029103fef3c39535d535d6fb10e81b948ad9e" + }, + { + "id": "2207d384-ac96-51b0-9296-5845caf6d2bd", + "version": "MTc1NjMyODAyMzQ3OTU3NzAwMA", + "digest": "sha256:4d81583bf85922db8ffbab3dbca3b6e68f2ed9ac62921c5a0f12f98deeb5479e" + }, + { + "id": "225341e0-fff9-5092-9dad-3055832a171d", + "version": "MTc1NjMyODA2OTc2NDIxMTAwMA", + "digest": "sha256:4fb98725cf16f7740c20b369ab5b39e0239685e1692922b0e37340896cdd6139" + }, + { + "id": "22a5f281-2045-5781-aeb6-9d416fc30df1", + "version": "MTc1NjMyNzg0Nzk0NDYwMzAwMA", + "digest": "sha256:7a3b4d939b6ed539583674cffb42ce1c4bf99e0485e5fbee1f2c9fb8fa023ec6" + }, + { + "id": "22c98c8f-39a4-50d9-ac4b-e4dc8b73c374", + "version": "MTc1NjMyNzg2NzU3MTE2NTAwMA", + "digest": "sha256:09c61307549bf29b285cc1bb63594cc17d596c4a4dbddb5eb1e16ee58db88cec" + }, + { + "id": "22da6aec-6202-55c5-a145-2ec7223e47a2", + "version": "MTc1NjMyNzgwMjk3MjAxODAwMA", + "digest": "sha256:cc583a616b02ff41ff6985702da7bf584120ef7b22f187a67fa7ba2c42b49075" + }, + { + "id": "22e2637a-0b3d-5182-a576-19fdf2a9d922", + "version": "MTc1NjMyNzc0MTc2ODYyNTAwMA", + "digest": "sha256:f2f0320985991e6c2d32cef0ff720bc7877642364f2e8171850521a20530ed90" + }, + { + "id": "22e580dd-4b1b-5b23-a355-050e62f4ad0f", + "version": "MTc1NjMyODE2OTk1ODI0ODAwMA", + "digest": "sha256:cdd47f258c0531ba606d9b7c5d751bcb923e2db209876537ae376db847e79710" + }, + { + "id": "233be9f3-ccb0-593f-ab8f-c6d2d067db33", + "version": "MTc1NjMyNzgxODc3OTczOTAwMA", + "digest": "sha256:92d4cdd425ae51e967812430869893d2ea4978f7499911a9a3933fc232434916" + }, + { + "id": "234e897d-76b5-5757-8cf7-6dae6af4c3ac", + "version": "MTc1NjMyODE4MjA5MTE1MDAwMA", + "digest": "sha256:787c9387bf1ceab7a96ca5ddbd2571af008e1ab75471eb98f531966a51bb6bfa" + }, + { + "id": "235e5996-8ffd-5bf3-9118-4b9221fbbf9a", + "version": "MTc1NjMyNzgwNDU3NDM0ODAwMA", + "digest": "sha256:ddacdcac014d932b45bc4501eabffe1691e53163e68e7589b041611950e03ec3" + }, + { + "id": "23613c26-13ff-566b-90ae-3a07fd0ef6bc", + "version": "MTc1NjMyNzc5NDcxMzk5MDAwMA", + "digest": "sha256:862ff4171ed4cffe977c65bd5b35e84f8359f1e14a84600505bee75cc8c1e85c" + }, + { + "id": "2367ef0f-8f39-52c1-a0d6-af9f7c40836f", + "version": "MTc1NjMyNzc0NDUyNDcyNDAwMA", + "digest": "sha256:72091f1d0de0928473267b5cb434a716116282f834be4258410c67f0c7a9891c" + }, + { + "id": "236b1564-c8bb-5681-abb1-0253a525dd05", + "version": "MTc1NjMyNzc1NzUzNDA1ODAwMA", + "digest": "sha256:745b0bf84cfb5fb524afa48d7332fd51beb61e38e6b7db10fce1ef21722df1c8" + }, + { + "id": "236b3d14-c1dc-5ff0-b6fc-cdb994693f7e", + "version": "MTc1NjMyNzg3NTQ2Mzc4MzAwMA", + "digest": "sha256:22fac3ce87f85be34b0aea6da9d8cf838bbf265c45119a69fcd1121a8489c389" + }, + { + "id": "236dd599-9b9b-59b8-a14f-79dda90e8b5f", + "version": "MTc1NjMyNzc0MjUyODE4MzAwMA", + "digest": "sha256:ee2c3af6e97cf9dcb39bdb37dd3a90b100f6fd1a5ab98516af485523ae2df9b0" + }, + { + "id": "237cf53c-6a8a-5d11-a2ee-5dd0692e8ec9", + "version": "MTc1NjMyODE3NjI0NDgxNzAwMA", + "digest": "sha256:bda16f683d9bfc0d1c9d5180c5daecdecddec5409f1bc70e36d0dc38afdb97c2" + }, + { + "id": "2389594d-9ab3-5d83-9f7b-673cd803521a", + "version": "MTc1NjMyNzkxMzIxNDgzMzAwMA", + "digest": "sha256:17d064b1d6d24ce61e59a8686f0a426f250f54f677163262c3246b33a9971f6b" + }, + { + "id": "23a7afe7-8ef1-5d1f-a0ea-977f4bf6ae50", + "version": "MTc1NjMyNzc1NjAxMzgzMTAwMA", + "digest": "sha256:0b9773b19ebf7f6a7ade87b1953cb5c8e0eb699639a7d3932aa3f12e39c00aac" + }, + { + "id": "23bc9d7e-cf2d-5cf2-953a-4c7d57ca60b8", + "version": "MTc1NjMyODE3MTIxMDQ3ODAwMA", + "digest": "sha256:daba01f16fbcfd8845dcb34027a8b16c5e11283cbf8b5455907b20e1e744323a" + }, + { + "id": "23e5bcc4-acd7-52c1-8445-a6cb2f34312d", + "version": "MTc1NjMyNzgyMjQ2NDYwMDAwMA", + "digest": "sha256:e8d16120e6978b0a87324dec5b3bf08edbd1939396dbef89ddca257f9c73c1da" + }, + { + "id": "24108a83-dcb5-5954-9710-71340ce4ecf2", + "version": "MTc1NjMyNzg2NjczNzAyMjAwMA", + "digest": "sha256:24f45068f58eb48451c62e596f52b04b2438fe3efab322e7a426946b51597908" + }, + { + "id": "24180605-c215-5f91-8b8e-b4fc441fca4a", + "version": "MTc1NjMyODE3Nzk5OTU4MDAwMA", + "digest": "sha256:87a48c45ee7166b42699483f353032f4f1c165e0bcacbe00a903157c0fb60d46" + }, + { + "id": "24228583-1ec7-5a41-bf1d-e6a43d972893", + "version": "MTc1NjMyODE4MTg3NzE4MTAwMA", + "digest": "sha256:8f17e074526463f31c646137158e59477cc84fcd6c71d5e8a889c06e5fa38c79" + }, + { + "id": "2429bbf3-af5b-5cbf-bada-38ee39a99143", + "version": "MTc1NjMyODE3NDQxNTQwNjAwMA", + "digest": "sha256:208ccf1ef057642e7e6b262a9527ce8eedb4f456a8669ac49baae3e40dc3f8fb" + }, + { + "id": "242ddd30-1aaf-56d8-8dd1-1a893b9be430", + "version": "MTc1NjMyODA3NzQ5MDE4ODAwMA", + "digest": "sha256:07fce30fe4ccaf45312b5b626d108d8e5bda083b3fe6650cfbd54939688a3c3b" + }, + { + "id": "2438dc79-3d4b-565b-a3f8-0c91af5ba28c", + "version": "MTc1NjMyNzc5MDMzNTc2NTAwMA", + "digest": "sha256:2081f8f68bf3ef249724817b4a8450f147de50c4584c203e6003f4768835e605" + }, + { + "id": "244d95ed-67d1-5307-a91d-c06b0bd8a6d4", + "version": "MTc1NjMyNzc1NDA5MTY1NjAwMA", + "digest": "sha256:50af9630c6936fcfe8ca419119bcb631ef22a84433f62b466c24cc1612943df0" + }, + { + "id": "245b89e9-7bcb-5a3e-9c34-3168b94895c3", + "version": "MTc1NjMyNzgwMzY2MTI2MTAwMA", + "digest": "sha256:ef3c4e5c667a09967821ff1b0a8b2ea72eac82a00e4ef0d6d7ca9616cb374507" + }, + { + "id": "248b3c31-630f-5fed-af33-430c341e369f", + "version": "MTc1NjMyNzc3Mjk5MTIxMjAwMA", + "digest": "sha256:47432c37caa2c0e4019b831ed00ee45bf60151d80af50f7ceb823933b25ed59c" + }, + { + "id": "248c37d9-5081-57c5-aec4-f2cc2cb65c38", + "version": "MTc1NjMyODE2OTk4ODc1NjAwMA", + "digest": "sha256:a157dc16240b60641c7b0b9bb89d85dddb3cfaed3d4224fa50a879aaca5d1f2c" + }, + { + "id": "2498e413-9e4f-5410-8beb-4fde7a205415", + "version": "MTc1NjMyNzgzODg4MDk5NzAwMA", + "digest": "sha256:80c48d48bc79aedc13deebbeef4dbece1d4a1d8bf986953df5f3d0ecd5cce254" + }, + { + "id": "24aa4680-5ad6-55e6-9061-15c0421a6856", + "version": "MTc1NjMyODE3MTE2MTU2ODAwMA", + "digest": "sha256:ec08e241bc6eef82b02707062467940d985798f643445f7f28c442aaf1592ae6" + }, + { + "id": "24ab833d-5f04-52da-ac89-6a42911a64db", + "version": "MTc1NjMyNzgyMjgzNTQzNDAwMA", + "digest": "sha256:4f404b5f5c4b6cce7609f446c07fb7c82fbde5b90c66f23b228f273675a8cc81" + }, + { + "id": "24bd3ad5-511e-5cd2-84be-b4f8acc99f9e", + "version": "MTc1NjMyNzg1MzM2ODgxMDAwMA", + "digest": "sha256:6bc4b09fe7642daec414f496a13609dd117938c2dacaf3b621a919625415ec56" + }, + { + "id": "24cd3c67-05a7-5a6c-8810-8563248e58fb", + "version": "MTc1NjMyODE1ODE5NTcwOTAwMA", + "digest": "sha256:d7e153a9602e60af686845c3f7788134283e1a004aaeb3085439bdecb26062c5" + }, + { + "id": "24dc240f-abb8-5ade-98ae-3eb46083f248", + "version": "MTc1NjMyNzc0MjUzMTg5MjAwMA", + "digest": "sha256:7c61a97e361d7e38e207120b3a3127641ef555d987cb99c2ea977639d34946b5" + }, + { + "id": "24ec7a0d-61d8-5638-8f6c-224707cbec65", + "version": "MTc1NjMyNzg4MDM0MjUwMDAwMA", + "digest": "sha256:c2262cffdb4a087f707b57fa96c08d8dc86f4ed252704d8e5256c91c8c707c2e" + }, + { + "id": "24eefd78-3fd1-593c-8406-348f18420b38", + "version": "MTc1NjMyNzgyMjg4NTIwNzAwMA", + "digest": "sha256:e1d1484ad53ca273f3e2a60034b3f2add9eeaa161a1b6663a482f2a59d26d504" + }, + { + "id": "2509cedc-9106-5651-a21a-fe57aa771a0d", + "version": "MTc1NjMyNzg1MzM2OTA1MDAwMA", + "digest": "sha256:9c481358a8ae16f651879bb133dbeb19f461147cdaeed4df04ba31bd0a24f922" + }, + { + "id": "2516d7a6-38cf-5998-b4a1-119d7402fc6a", + "version": "MTc1NjMyODA3NjIzMDQzMzAwMA", + "digest": "sha256:20fe9970b8422df9b514926dfc1677d82c9161ac421a403027d40ab7b3916471" + }, + { + "id": "25223604-a41f-5810-ab6f-784e4f10ee07", + "version": "MTc1NjMyNzc1MzYxOTI0NDAwMA", + "digest": "sha256:7497fc4251f127bea43e3d7fcc4b338448a362725221759dad8a225bfbb8a05a" + }, + { + "id": "25297c26-db51-5b31-9e54-570462050a57", + "version": "MTc1NjMyODE2NTg3NDg4OTAwMA", + "digest": "sha256:80c85dcc70942010cdb6b621fb745d52ce48330ba42e2291e9963181fc7e2bcf" + }, + { + "id": "253c5d1e-2ba1-51c9-a450-21f4eef19d68", + "version": "MTc1NjMyNzgyNjU3NjYxMjAwMA", + "digest": "sha256:3f95bfea384b703236b7120dd020cadc8088fea61c50d3a682062f380fa6f222" + }, + { + "id": "255aaa00-6740-5bf4-b80d-24dc6483c718", + "version": "MTc1NjMyNzc4MTkwNjE3MjAwMA", + "digest": "sha256:ae46e8b034d5ae7f3f1f35a80c2a8cee5e2eed35036196786a304aef01025d80" + }, + { + "id": "256a9b27-318a-5d1a-83cf-9ef9b2a02a95", + "version": "MTc1NjMyNzc2NjQ4NjIzMTAwMA", + "digest": "sha256:01c789c86649b38eb2de50eaf985190c0253c27d7351b977a11de2618979a58a" + }, + { + "id": "256c5719-950e-5a10-a974-bf8b3cf3438a", + "version": "MTc1NjMyNzg4NjU3MzQ4MjAwMA", + "digest": "sha256:6cb029a792ff0017b206f95c7dd19ec738a90e822a6547314756a8af9283665c" + }, + { + "id": "257b1e1e-f2d2-5128-b3a8-03dc5f85b4c8", + "version": "MTc1NjMyNzg3NTQ0NzE4ODAwMA", + "digest": "sha256:e718c2531f0582af01a0f5293a00a4b8cfeff7e4572b212f0c125a7bd11ec517" + }, + { + "id": "257c6a5a-1865-5b30-9301-907bcb7c3c75", + "version": "MTc1NjMyNzc5NTIwNjUwNTAwMA", + "digest": "sha256:5af98c97ae9d8c14ac14a6c44540ae0c05e401e11d1b4b25615c75d8292bded7" + }, + { + "id": "2581873c-cbb4-5700-b431-92fdbf34b9c4", + "version": "MTc1NjMyNzc0NDM3OTgxOTAwMA", + "digest": "sha256:f83197685eee5425a69ba1b33132f336ddfd79306e4627ddb5b53df203fb1636" + }, + { + "id": "259b6313-1a1e-560a-bffa-7d89baa502e6", + "version": "MTc1NjMyODE2MzM1MjgyMTAwMA", + "digest": "sha256:7e5cb5f1293c1ffb41a5212d1dc9e1638943299ef2ec9d9ede9323ae4f143497" + }, + { + "id": "25a70e2d-7f9c-5779-b328-6d97b70eac41", + "version": "MTc1NjMyNzc5NDcxMzI2OTAwMA", + "digest": "sha256:a64541fae5f77f565fdb8002c84f05adb0912d062ca85ffb6ee57e75c48df9c8" + }, + { + "id": "25aaca44-2415-53ff-8b2a-1064e642f47d", + "version": "MTc1NjMyNzc3NDQ5MzkyODAwMA", + "digest": "sha256:819dbb71eb406f13c666749b1b430bb0cc1868ccf702a35ae0cf52d8b16f0fd1" + }, + { + "id": "25c6dfe9-ecb5-5c13-8d7a-265a8880ba58", + "version": "MTc1NjMyNzg3NjYzMzk3ODAwMA", + "digest": "sha256:60e1ccca701b898b4655913c4b5f1ddd7ca709b2321cd5662e0917ac0c1fdc45" + }, + { + "id": "25dfb954-8ac5-56c2-83d2-3b2ac4c2629d", + "version": "MTc1NjMyNzgxMjYyNTIzMzAwMA", + "digest": "sha256:485e4a2de413a1cca2781b9a9999475187a967fd42b265b6512de0320580d1e8" + }, + { + "id": "25efc5b8-d0e8-5e86-ac9e-30208ff80332", + "version": "MTc1NjMyNzc1Mzc1NDA2NzAwMA", + "digest": "sha256:cedbf8a07f630ae3d6f8f046d28caf41541489760edd9e28bf045dcb8faae2b7" + }, + { + "id": "25f43324-6494-57c7-8cbe-68c2f9f1232b", + "version": "MTc1NjMyODE2Njg5NDM1MzAwMA", + "digest": "sha256:95a4f04671fb9ea360c77b10a7f08775c696cbeccbc9653b32ce9551c2527eb5" + }, + { + "id": "25f84170-a16e-5793-b417-f0a4dcccb089", + "version": "MTc1NjMyODE2OTk4ODEyMjAwMA", + "digest": "sha256:a0ac9b980ab241d9e3847fab42ac46dbaf9cfb20f564ec81decc7db98a786b88" + }, + { + "id": "2613d1a4-3b1f-5877-8484-4942fce40063", + "version": "MTc1NjMyNzc5NTIxODY0MzAwMA", + "digest": "sha256:759a00f837abe38abdefb209c1fae7154d94c20368c721bf553370a314e643d7" + }, + { + "id": "26241f66-1441-5059-b02c-c57fc3036fa9", + "version": "MTc1NjMyNzg1MzgwODU5NjAwMA", + "digest": "sha256:1a6afaed1b48df47b8b6b92e3b764b78b7f9374f5597e1cc8bf71bac7363e467" + }, + { + "id": "263eff1c-6d57-5cba-82b2-5900524f3b8e", + "version": "MTc1NjMyNzc1MDEwMDA4NjAwMA", + "digest": "sha256:9d8303929eb24177a4c031794678eec8db69f8153c1a0f43b2a6fcf4b632f6a0" + }, + { + "id": "2647542f-74e5-5548-aa40-deb4299ab244", + "version": "MTc1NjMyNzg1NTY5Njg3NDAwMA", + "digest": "sha256:d86809559f50deecf490d1abcbaaaad0a159b1013bd16f6ad3592fd672117cdf" + }, + { + "id": "264bd01b-cc0c-5ad4-b92d-b3ff5bee50c1", + "version": "MTc1NjMyNzg4Njc4MjcxNzAwMA", + "digest": "sha256:68a8fa8451ff84d6ee7bf543a4d42930ebbe284e6f26bc99e5f298c476f9a7a1" + }, + { + "id": "26538eb8-91ae-5bc7-9fa5-cf6007df61cf", + "version": "MTc1NjMyNzg1MjczMjkyMjAwMA", + "digest": "sha256:77e31ea838035b4b77e701c5c8c55b73aef843d5c6c92579b6eabdbc96bfff41" + }, + { + "id": "26560d1a-4f0f-5d40-993b-88c898ae348e", + "version": "MTc1NjMyNzgwMzcxODM5MjAwMA", + "digest": "sha256:3102d122c9318491acd76ce0f4c7721833b0ff7603b487952e7c743d251ae604" + }, + { + "id": "2657345b-ebed-5b96-b634-797d7b806549", + "version": "MTc1NjMyODE4MjE3NTQ1NjAwMA", + "digest": "sha256:86f5b24804489e6ec5a3dbaf4cbe8f336dd7a42ee715135270e304ff89807d82" + }, + { + "id": "265f86f3-a90a-5f65-9d1c-66ddbee36561", + "version": "MTc1NjMyNzgwOTQxMTEzODAwMA", + "digest": "sha256:776c92472ea26d4b9c42ca3c0234231cd3fad14701664bac5bb511688f4d9552" + }, + { + "id": "26800e37-166c-5c54-a4f5-af7728aaae65", + "version": "MTc1NjMyODE2NjkxNzYyMTAwMA", + "digest": "sha256:d3d77363a521d630e874df841eb41f67e5ae577f23b0e9b56b03f6c84a8844b9" + }, + { + "id": "268be5b6-f465-5f21-8694-83d53a352aa2", + "version": "MTc1NjMyNzg3NjgyNzY3MzAwMA", + "digest": "sha256:952f2bf1752fc742582a75f10307cf86ed74bba23796e6cd5f6b3ada75040420" + }, + { + "id": "26965f16-3d72-59dd-af15-d9768d83dc46", + "version": "MTc1NjMyNzgxMzU4MjE2MzAwMA", + "digest": "sha256:bad969e343102ac8f289a1efc5f0114a70e540cbd6ff4f47001c447a211e5f92" + }, + { + "id": "2699bce4-9d11-5cdf-b872-8ea35f6f3e2b", + "version": "MTc1NjMyODE2ODkyOTc5MjAwMA", + "digest": "sha256:10e7ddacd190d07e86ff93f21c39bef838605603b87e8de76f8711659c5c8227" + }, + { + "id": "26b4163f-89de-5dff-863c-d6b716d7d8ef", + "version": "MTc1NjMyNzc3MDQ0ODkxMjAwMA", + "digest": "sha256:07cccd2cac2c22b24a2d5aa235e61900f5c26859ef8b51ecc70ff8725b105ce7" + }, + { + "id": "26df6c32-b3d5-52a3-a813-c00f72322d6b", + "version": "MTc1NjMyNzg3NTUwODI3NDAwMA", + "digest": "sha256:ef00f5d181a1b5595d08fb0b4275ccf2abcb9e793d56a736aa4683cd599c4d12" + }, + { + "id": "26f12463-4d7b-5046-a8e2-eee008e3a5cd", + "version": "MTc1NjMyNzc1NjE5MjAwODAwMA", + "digest": "sha256:9b8b34e76e1094a2f5ca267c248f97bfd8e4954e05c633236c15442f74abd301" + }, + { + "id": "26f297fb-8c0e-5e6d-9a8a-63b9f315b7ac", + "version": "MTc1NjMyNzg0MzA3NjgwMTAwMA", + "digest": "sha256:028ae1c5be9cd145b4f45ed6460704c4853f0dfc9abb7c2ffec08b11c1865427" + }, + { + "id": "26fd8192-9b15-5f8e-be21-aef5ed3552ce", + "version": "MTc1NjMyNzg1NTc5MTU0MzAwMA", + "digest": "sha256:d361e7ebb43570cda038ff9f8aa7747aaf2e6b2f75e788d4023a4b4b78b9ac01" + }, + { + "id": "2714fd08-fedd-503c-a1e8-497c4aa7d2dc", + "version": "MTc1NjMyNzg2NjMxMjY5MTAwMA", + "digest": "sha256:e07cb0333499de2e24650a15992c235b103c4927e970c7c2afa479b6ad0524e5" + }, + { + "id": "2718af30-3145-5588-8197-af9565891cac", + "version": "MTc1NjMyNzc1MzUxMzU3MTAwMA", + "digest": "sha256:8c0a2b0ccc95a7bb33996295489270f7961284ce7aa5255d2ce50636e7cef1fe" + }, + { + "id": "27261780-1ccf-564a-ba8c-8a3e6028364c", + "version": "MTc1NjMyNzc3NTEwNTEwMjAwMA", + "digest": "sha256:34a49acb4d00b6de2305f1cd4603f53ffb90f779ef9b3202db1fe93468f9c083" + }, + { + "id": "272680fb-244c-59c0-8410-244ee52e0ed8", + "version": "MTc1NjMyNzg3OTk3NzQzMDAwMA", + "digest": "sha256:35dcf505037f25fa9f0395872539b013ea0ce687885f9198fd6f362204c2c563" + }, + { + "id": "27376d9b-7c68-5c57-84b7-8cbc17fb7028", + "version": "MTc1NjMyNzc4MDg2OTExNDAwMA", + "digest": "sha256:815715e5d31ee3534b4247e08b5a5f0bab829a96b885d01275cb42bb25e895ad" + }, + { + "id": "2746245f-7682-5a63-bd9d-7e99fa9d74e4", + "version": "MTc1NjMyNzc5MDI4NTc4MDAwMA", + "digest": "sha256:21f0afca768e19d8a63222d3e1c84a2cdf114574166081ae22e40dabc7657821" + }, + { + "id": "27543c41-8801-5e2a-9b19-83c8669c75d3", + "version": "MTc1NjMyNzc1Njc4OTU1MDAwMA", + "digest": "sha256:68c2b1400b6e6318ec09abe96c49c68ee3d88e691b2b1abdfac527a771445004" + }, + { + "id": "27713ea8-1f8a-54f3-99cd-81b31de32eb9", + "version": "MTc1NjMyNzg4NjU0MTQ5NzAwMA", + "digest": "sha256:e8a685df5c9c1deedd12cf48e1e220b84f90524179b5ea044b77540d7d551629" + }, + { + "id": "279249a9-87fb-5f01-87f3-ce75955a5bf4", + "version": "MTc1NjMyNzgzODkzODgwNDAwMA", + "digest": "sha256:d931cb52dc2fc30e01d6cb6f9a4dae0def6865c76d1769f5d78362fbb895a73e" + }, + { + "id": "27ccb341-1601-5754-bcfe-ce8cb0ff6758", + "version": "MTc1NjMyODE2ODk1Mzg2NTAwMA", + "digest": "sha256:5e7ed15d4bb213a10bf196211e70e554b2b53275426595a51b669a9b5cf9ec6d" + }, + { + "id": "27d5c8ae-3c03-5598-99dd-49573d834012", + "version": "MTc1NjMyNzg0ODAxMDgzNDAwMA", + "digest": "sha256:aa29ebbeddac82f0df066349262549a0be5a6f2724ce66c2acc2b507bd308cc1" + }, + { + "id": "27e5e991-45d5-5879-ad29-68e034e549c2", + "version": "MTc1NjMyNzc2MjM3NDIzMDAwMA", + "digest": "sha256:15695f286522dc217a7e714cb88ba6091be80a522b012a0e82ebe803da6afe73" + }, + { + "id": "27ea4e9f-7616-56e1-bc75-33374335f780", + "version": "MTc1NjMyODE3MTEyMzkyODAwMA", + "digest": "sha256:8c49c093f49b5cc8242751dd77b2bec9c40c120e155e18e2b89f345a8f35ac9b" + }, + { + "id": "2800abe2-89f1-58a4-bf8c-fac676af8d41", + "version": "MTc1NjMyNzc1MDAzNzU4NjAwMA", + "digest": "sha256:a87b816d7c2461e59f03054c8316870bea32db8fdc6fabb549f1b7149de648e8" + }, + { + "id": "2801bba6-b6fe-5d07-9df6-508b49c448a8", + "version": "MTc1NjMyNzg4MDMyNDc0OTAwMA", + "digest": "sha256:61a4efe0f3ec35870d1c1afef965e6c9a8087a51f51a3c15472fffe8cafd5608" + }, + { + "id": "2813d931-c9ea-5647-bbe3-30f6670539e9", + "version": "MTc1NjMyNzg3NjcwNjM2MTAwMA", + "digest": "sha256:957a0ed497b570317dbd5b0f2f75a45cbcfd212591f1eaf955b6ad2876c8e3f3" + }, + { + "id": "283d1555-bf93-523e-ac93-dab17924e208", + "version": "MTc1NjMyNzg3NDU2NTEyODAwMA", + "digest": "sha256:116cf1d3856f6337d1b12cc9b5b3f215b30c3f305e5e6f6a7d32b8ac08c85993" + }, + { + "id": "28451f3a-4e83-51d6-b27d-cf2678d07576", + "version": "MTc1NjMyNzgyMjQ0NDUzODAwMA", + "digest": "sha256:1ce1c243cb8a7cba3ccbb2a07d7d6a343d212566bd17ae018e8f58a63d9ca503" + }, + { + "id": "284a0def-cfa2-5c5f-8bc5-15b487eff266", + "version": "MTc1NjMyODE4MDQ5MDcyNjAwMA", + "digest": "sha256:8a6c13ec0df7eb2ead6630acde56e175d52f5c33d8d84faa241bc1b514b4fce3" + }, + { + "id": "2863cb4b-8e0d-5884-a3cd-7fea34ebd692", + "version": "MTc1NjMyNzg4MjA3NTYzNzAwMA", + "digest": "sha256:f8bf69063d62fd684789a93f79c66302f90ad833891053b9dce358c8fc3d38ea" + }, + { + "id": "288654a1-bc39-5d47-8dc6-f168b30da6aa", + "version": "MTc1NjMyNzc5NTI0NzQ1MTAwMA", + "digest": "sha256:a9620eca35eef7d9ad7f9b8be2ff002c2488663148658856d9c80845d7b19461" + }, + { + "id": "28a51ed5-0e4c-53c7-b902-78dd8f77f82d", + "version": "MTc1NjMyODA3Mjg4NjE0MTAwMA", + "digest": "sha256:7cdb59cf2e3ed99e902f6a3d4deb64c26acbd4abe1d948fc5ad8d722d3e4a390" + }, + { + "id": "28aa8737-8016-5bdd-9c3b-5760f361312d", + "version": "MTc1NjMyODA3NDgyMDEyOTAwMA", + "digest": "sha256:fed60e032865436409780a94db189a649a591ffc7e65add037984a7acd4bcd22" + }, + { + "id": "28b2f62d-5e18-55d6-b3f0-2ae6959e181a", + "version": "MTc1NjMyODE3ODAwMTI0MjAwMA", + "digest": "sha256:8e7acb5cd9c2cf1a2d0e6dadc4ba8e15146c65d3747d479e3e747f699a71e4e5" + }, + { + "id": "28bd64c4-0c7b-5525-9474-6be27b38efad", + "version": "MTc1NjMyNzc3OTc0MDgyNjAwMA", + "digest": "sha256:2988c3e0d041363ad18a5a4d90df56719bd0e243d2d6a603f643acc24674e748" + }, + { + "id": "28c7072c-1299-547d-ae9e-24f5999e611c", + "version": "MTc1NjMyODE2NDk0OTE3OTAwMA", + "digest": "sha256:7709fbcd52580908edcb6db4fb99b41157fbf62134492bc0f7adad7e027fd438" + }, + { + "id": "28dd3489-4917-5e51-a1ba-2a89ff421629", + "version": "MTc1NjMyODE4MTg0ODQzNzAwMA", + "digest": "sha256:3ec45765cfa3860d0283e9c7c71c7b7196bba260923528fcf31a04f556ee217d" + }, + { + "id": "28e6c8e4-328f-507e-b321-250b0b018d2e", + "version": "MTc1NjMyODE2MzAyMDU2MjAwMA", + "digest": "sha256:234c914d42c1a1338a7bce9f023597d4993222bb2360f8b552a45e75857d2c19" + }, + { + "id": "28f461b6-cb6e-539c-a532-d3a5ed0840d2", + "version": "MTc1NjMyODE3NjgyNjc2MjAwMA", + "digest": "sha256:ea4ca32dff6832b5860319320323b57d7b83f32e75e1d026d478eb9fd26055b1" + }, + { + "id": "2905f8b1-7724-57e1-8589-51d85b329925", + "version": "MTc1NjMyNzgyNjU3Njk3OTAwMA", + "digest": "sha256:0b70740f3303f43b3c7f7203790999d719a29b13ca534251715b30c6b9859bb3" + }, + { + "id": "2929d961-b865-5ecc-9d2b-4ef3f3c039cd", + "version": "MTc1NjMyODE2NDUwMjc1NjAwMA", + "digest": "sha256:e7747298890018adbd47532486b856c2777223d0d5e80f5e9e05861725844495" + }, + { + "id": "293671a5-3c15-5e10-bbbf-58840ad9f9ca", + "version": "MTc1NjMyNzg3MTU3NzgzMDAwMA", + "digest": "sha256:75f1c6dcde95b998f0d292143cbb01c9d0c4b983471328038708500f83f068be" + }, + { + "id": "294075d0-371e-5ba3-a36c-217dbcd8ce2e", + "version": "MTc1NjMyNzc0MjUwNDkwNTAwMA", + "digest": "sha256:173bec7313bf1240469cb0953431728f782ed5ef9160d7adb6194bb9e46ea40d" + }, + { + "id": "29478c0e-ee8a-5d49-8d05-e328b6570415", + "version": "MTc1NjMyODE4MjE1MDc1MjAwMA", + "digest": "sha256:7461daac57727cbe621f7117a1e200ebe8838ce0525afe37b7ee2c3cf98634fa" + }, + { + "id": "295c4785-5c2f-59c7-8865-e9cdd528e7db", + "version": "MTc1NjMyNzgxODc3NTIzMjAwMA", + "digest": "sha256:ca85b6525478c461787108cf467c75b1736f82cc0c89f5a84272c9fe7cc149e6" + }, + { + "id": "296defbb-8bf6-5c84-abc3-d93b2afac2a9", + "version": "MTc1NjMyODE3ODA3NTEzMDAwMA", + "digest": "sha256:f756a73520c0af3920d0d6540ac1e929285fd6f7eff95fcfa76ad862f54a3ae9" + }, + { + "id": "29a6fe8e-05a3-51ac-82f8-fe05d604489f", + "version": "MTc1NjMyODE3MTEwMjg1MTAwMA", + "digest": "sha256:a241373e0748794b43d48579af458022028580d6b1aa12d4b21aa048e7a07ff7" + }, + { + "id": "29ab66ce-dd05-5f43-9cc4-27b1a6f2922a", + "version": "MTc1NjMyNzgwMjk0Nzk0OTAwMA", + "digest": "sha256:ad2b47e2d42aba288a1b9cea1a973d4e138772456683c150b7fd0dace71157d5" + }, + { + "id": "29cdd3d0-76b9-5a89-a8fb-0d6127b98dde", + "version": "MTc1NjMyNzg4MjA3NTYyNzAwMA", + "digest": "sha256:fc27927b71e0d5b54b9157b19b3bb243fc6b4929590d0d75d6e193b98c7712d8" + }, + { + "id": "29d4b749-2941-559e-853c-928dcd904b3a", + "version": "MTc1NjMyNzg3Njc3MDQ5OTAwMA", + "digest": "sha256:d48f153d8ddc85be6a144676d7e478d6b71165c2f49ee044179be678535079c1" + }, + { + "id": "29f02768-ec77-5e77-b8ec-ba7fea096aa2", + "version": "MTc1NjMyNzgwNDU4NDQwMzAwMA", + "digest": "sha256:0f4c5c484765e20ea8d7c4e1752d7fed17b5c1ffa9761444a4241dcb72fb489f" + }, + { + "id": "2a1dc034-3d48-5353-a080-0529ae9ffaf0", + "version": "MTc1NjMyODA2ODc5MTA2NDAwMA", + "digest": "sha256:53d2e8012fb5f70337603a301ca14f0092400097cc613bed58b15b5a88a6ee73" + }, + { + "id": "2a2f86a8-f82c-57b5-95f8-de71edaddbe7", + "version": "MTc1NjMyODE2ODk2NjM2OTAwMA", + "digest": "sha256:da5aa4f51d0c2f8e3434092ea25ffd3247fc7bbad6076f909ece54eb64e344c4" + }, + { + "id": "2a3dce45-b13e-530f-be5a-61f1e8d6100e", + "version": "MTc1NjMyODE2MzM5NzQ5NzAwMA", + "digest": "sha256:fd6adb368d2f8c500c84f87a11c4a363e3024e0d8016f2d4fa8660f9738a2271" + }, + { + "id": "2a3ede75-a623-5b8f-bd76-d6660b665bd7", + "version": "MTc1NjMyODE4MDUwMTU5NjAwMA", + "digest": "sha256:c070284bb5b4dec72c7b033dd9aa26e8536e9e3f47d85a95df8208d2cfb5c835" + }, + { + "id": "2a43735a-2c5e-55a5-b380-739a2273e578", + "version": "MTc1NjMyNzc5NTI3NDc5MDAwMA", + "digest": "sha256:d9004dba379aeb558144bac2ad2111849dd243863cec8ac9cefbf06ce070b348" + }, + { + "id": "2a489b1d-32cc-51d0-959f-fbe4b8811d5f", + "version": "MTc1NjMyODA2NTE5ODIwNjAwMA", + "digest": "sha256:f40e813c964e28a0434a75542535125c876a6f1553073e4dd2e5529145c05dcc" + }, + { + "id": "2a4f03f7-fbb0-5209-afd8-727a5f366584", + "version": "MTc1NjMyNzc2MjM3ODkzMDAwMA", + "digest": "sha256:de4f708baf60c1c554d997f961ecb6c059ac6e8da16830be4d78ecdb52e56e37" + }, + { + "id": "2a6aad93-031e-5c86-b736-62edc48ad51f", + "version": "MTc1NjMyODA2NDAyMDc5NjAwMA", + "digest": "sha256:92b49191d750fde5f897094fb7616c74a91286fc64e36dfdeb828ceb464ae714" + }, + { + "id": "2a78b18e-eb08-5346-8af6-a42d9a87ab76", + "version": "MTc1NjMyNzgxODg1MDEyNTAwMA", + "digest": "sha256:5be27a50f86db14f5a4c3c5a9551f1c518bc210cb4e02b71f7505889f92d394a" + }, + { + "id": "2a822b9f-5913-53f0-952a-d6b63d46456e", + "version": "MTc1NjMyNzc1MzUxMzA3NzAwMA", + "digest": "sha256:ca6fd8e47d925e975fd18ba3ac33c170c1dd90cb069a5f9e2bdbe73d2cd7b032" + }, + { + "id": "2aa4614e-b737-59c0-bcb1-b0c91fb7239f", + "version": "MTc1NjMyNzg3MDQyMTEyMjAwMA", + "digest": "sha256:0c9b1fae738ddf9bdbd609eb86e9b08bb0890b4d74e5d99406a1f50365ec49e3" + }, + { + "id": "2ad08e3e-e4b0-534d-b362-2caebf4cd00f", + "version": "MTc1NjMyNzg2NjY4MzEyODAwMA", + "digest": "sha256:ca3b5a53ba96f8821f20a52f962464d189cd9ae751fe114dd78149ae7754cae1" + }, + { + "id": "2ae08f13-dcd5-5631-9898-d04d361a4ea6", + "version": "MTc1NjMyODA2NzkwMzExNTAwMA", + "digest": "sha256:ab8fa527ed1560c21cd03bfe208614e13197272fc7fc052bd17f61d37ff37831" + }, + { + "id": "2b0f7a9b-f56b-5de1-90e1-195bc2625fb5", + "version": "MTc1NjMyNzc3NTE2NjkxNzAwMA", + "digest": "sha256:bc44280492c17142e0ffaf7e1fcd9e082303510073ad8b74e5501ea6e11b6226" + }, + { + "id": "2b123272-f4c4-5970-a70e-bb6698f877a1", + "version": "MTc1NjMyNzg2NzYwOTQ3MDAwMA", + "digest": "sha256:4adcf15e27745f9c4ddf754d93043e69baee3d662c02286c83dd3f65cfbe98ce" + }, + { + "id": "2b274824-1514-557e-86c2-d3efee2283e7", + "version": "MTc1NjMyODE2NzAxMzA5NDAwMA", + "digest": "sha256:e6d37908f9504ebad22d2c46e8135c15b947a8dfc1e3d5d58b2d01ce9859d422" + }, + { + "id": "2b2d40d5-0c15-59cb-b949-f7253a723eb6", + "version": "MTc1NjMyNzg0NzkzMjU1NDAwMA", + "digest": "sha256:ba863da7ffc28d93e9569738d169866eb02d567b58da220bf88cafae579b8183" + }, + { + "id": "2b2f72fe-60e7-5ba8-870b-0f2e6323a654", + "version": "MTc1NjMyNzg3NDg0NTAxMzAwMA", + "digest": "sha256:ededabe30c1b4895c6ef10e3a2d83010cb24c87aaefaf11797408f19be11f590" + }, + { + "id": "2b463dd6-8d7a-547b-9f2e-aae330e23d6b", + "version": "MTc1NjMyNzc4Njk0MDQwMDAwMA", + "digest": "sha256:a3b1f0b89153943e0012d0ea3c62c8a4fb8c4ea6dd674b09c8a44048805817bb" + }, + { + "id": "2b53f0a8-5efb-5ad9-ab21-bb8ea71d2543", + "version": "MTc1NjMyNzc5NTIwMTc4NzAwMA", + "digest": "sha256:3a294cc4817b82c19104ce6583e85657218e9bf8a17f7b30d7c4a8d4e5d79fb6" + }, + { + "id": "2b87bf5e-1312-5b45-919d-43d47d8ba0ed", + "version": "MTc1NjMyNzgzODcxOTkwMTAwMA", + "digest": "sha256:22d77925abd8cec282070181a5d6f0150f4b39e704b187563fabb4007350e81b" + }, + { + "id": "2ba438be-570d-5310-9aa2-b0bdefc1b607", + "version": "MTc1NjMyNzg3NDk1OTMxMjAwMA", + "digest": "sha256:340770db538e98deacd466a112eae1462c54a55186c6413337967aeb62c23048" + }, + { + "id": "2bade97a-e283-59cc-bbe5-81f8dfc304c9", + "version": "MTc1NjMyNzg1NTc2OTY3MDAwMA", + "digest": "sha256:949f01a59a964a352204e4e5c4811480b6ccd18573daf01d17807a0baa58f28d" + }, + { + "id": "2bc3b1fc-4a26-54d7-947b-1132897fff40", + "version": "MTc1NjMyNzgwOTIzMjA5NjAwMA", + "digest": "sha256:f8b108c6228a313d18c2756fa6f73a537fb2b6cfebebb84d98e9e4016769200c" + }, + { + "id": "2bf85d12-4f51-5a15-ab0e-2d7885058f65", + "version": "MTc1NjMyNzgxMjA0MDYzMjAwMA", + "digest": "sha256:5116b56d93c7d362fd4070eb7e5d4bec9f83613c8f9d3b5fce4270ea30c06220" + }, + { + "id": "2c0d6127-1420-5406-b75c-e270153715ef", + "version": "MTc1NjMyODE2NDk4MTU4ODAwMA", + "digest": "sha256:2692ed3c11e96c684a987f117ac5bf3195078ee078c2ecf8fd2c64ad6a1b5b5b" + }, + { + "id": "2c26df05-c6ed-5f7c-a6a7-db55730ded9a", + "version": "MTc1NjMyODE1ODIzNjA1MDAwMA", + "digest": "sha256:78d0c4c6cccad1ab38e3a558fceb2284c17443a0c9820210e4165b266a5e2608" + }, + { + "id": "2c5cb721-5a7b-5c7e-abab-90d315e399e5", + "version": "MTc1NjMyODI1OTI2ODA2MDAwMA", + "digest": "sha256:980eed8d4ce2c5a66fca63f781d0e1073cdbccd0d2745362bced38277ac6a8a1" + }, + { + "id": "2c6f6ff8-ba65-543d-98c7-380b9f41b0c5", + "version": "MTc1NjMyNzg3NDU1MjUwNjAwMA", + "digest": "sha256:58a0f6c0a375ddb62ceb4988bd688b09dff826bb8d56cfd90c9631573a10e728" + }, + { + "id": "2ca96b98-a70a-5f63-a20e-eaccd9d7ca80", + "version": "MTc1NjMyNzc4NjkyNzgwNjAwMA", + "digest": "sha256:dd10f596a4b1a720b98da99502d94275c2dc2f397054b290213c7719ebffb453" + }, + { + "id": "2cb87037-c6d5-5f58-af4f-20916256b604", + "version": "MTc1NjMyNzgyNjM5OTM1NDAwMA", + "digest": "sha256:e573316b9e5be3dcc7b4ca7f17cb7c9e002097661ab4e304a66375e2b289458b" + }, + { + "id": "2cc32444-d9dd-5e6d-ac7f-9db5d1afdd01", + "version": "MTc1NjMyNzgzODgyNDAzOTAwMA", + "digest": "sha256:af53f798002b6f676b5bd3e0cc5dab520d8c0acc03e8fb2d73ca58a4e478db14" + }, + { + "id": "2cecf9dd-393e-58ba-96c6-aa83271e97a9", + "version": "MTc1NjMyNzc4MTA0MzU1ODAwMA", + "digest": "sha256:11143d1e9d98d58449fe79b4ca1aacf3493e7310421fabc4c85b5cb3a33916c2" + }, + { + "id": "2d10e4e7-56c2-59c1-ac80-920dbdc808d1", + "version": "MTc1NjMyODA3MDExNjg3MjAwMA", + "digest": "sha256:823287eeecf1b889b8a945292e22e3556ac0285d7ca9da1ebc63193e0fbcef72" + }, + { + "id": "2d127150-a9d5-52dc-ac97-5db27339bb6a", + "version": "MTc1NjMyNzgwOTMxNTkxNDAwMA", + "digest": "sha256:fab94f6e33d6b009d1dbc001db733629b51a2dd4f36aeb40f92bff03a598ee97" + }, + { + "id": "2d38c9ae-e1d3-5933-980b-823d5933fb03", + "version": "MTc1NjMyNzc2NjQzODIxNDAwMA", + "digest": "sha256:eb564299e60d04a270e562381b34bdb8fe3da9d289ac8cc81af951fa7147e2a7" + }, + { + "id": "2d459879-f0ba-5847-a8bb-c1093361bed2", + "version": "MTc1NjMyODE2NjkyNDE1NjAwMA", + "digest": "sha256:a62ce894aade39a252786256b14eee06538d39c5926a13103630ad7db71c92ee" + }, + { + "id": "2d4954a7-fa29-582f-957d-bab8f9a64815", + "version": "MTc1NjMyNzg1MjcyNjQ3NTAwMA", + "digest": "sha256:528682ce1354bb55e90e349e9c20ae0a23848644a0528a230f278bc3dc4da83f" + }, + { + "id": "2d51240d-5048-5c79-8f64-e6d6358add55", + "version": "MTc1NjMyODE4MDQ1MzU5NjAwMA", + "digest": "sha256:29b226d03daeafb9eac187a916e5d654cfa78fb0e432ee222dd3aa1ed1988dea" + }, + { + "id": "2d58825e-0bb7-5b63-a6d5-770afd9bec9e", + "version": "MTc1NjMyNzc5MDM2NDM0MDAwMA", + "digest": "sha256:e332b3c33a882d3dc90adc9847c6ee55b93391f60ffc8f5f757f36cc7a8f23b3" + }, + { + "id": "2d6f4389-6e00-54f8-ae8a-87efca8c1bac", + "version": "MTc1NjMyNzc5NDY4OTMzODAwMA", + "digest": "sha256:11665a9c4c8526623cd33e1694e1c009258971892c9963b7e1932d389b07c83b" + }, + { + "id": "2d9b1b09-2fd9-5eb0-9d1e-0f2365ffdb88", + "version": "MTc1NjMyNzg1MjE3NjUxMzAwMA", + "digest": "sha256:ebd96eb7ceffcf545cc4a8d53acf11454abe317a7da6f45a4d3917f195aa20ee" + }, + { + "id": "2da4fac4-9096-57f6-92fb-24f6d904d595", + "version": "MTc1NjMyNzc1NjgxODA0NDAwMA", + "digest": "sha256:9c59bf6eb90b2a3ffd35f641b6bde0e5662046e931bdc83bb9b6c28296a6760a" + }, + { + "id": "2da93b01-1ca2-5a41-a419-b12cf32702d4", + "version": "MTc1NjMyNzg1MzEwNjgwOTAwMA", + "digest": "sha256:b1c66a3cf5f09372ed189af8d6f0fe2dfd4a0154fe9535b8d1daad97bf5e29e5" + }, + { + "id": "2db86e80-bff6-5ca4-8882-e01a76d8f021", + "version": "MTc1NjMyNzgyMjgwNjk2MTAwMA", + "digest": "sha256:247727b1f7b00b09c27fa9e9733c05b2ff27ef50eee1caa78650b2b9de56d1bd" + }, + { + "id": "2dc6a1ca-b032-5646-a41a-6d0293b01e06", + "version": "MTc1NjMyODE3Njg5MzE4MjAwMA", + "digest": "sha256:56578980cdd506e655082ba1e6cc4934ecc4b8c797f6fe37e012d8fbf14a074c" + }, + { + "id": "2dffdd85-d62d-5644-8ef1-0b6bbfad7dc3", + "version": "MTc1NjMyODA3NzY4MDgzODAwMA", + "digest": "sha256:76f3a0adb29b66888261a17db8c7f6935cd7fc24588c06314bfcb67d8ad26ca8" + }, + { + "id": "2e14ae82-b4d8-5209-aff9-9574c8bd5017", + "version": "MTc1NjMyNzczODgyMzY5OTAwMA", + "digest": "sha256:e93b996683781eb50008f0d10dd25e588bc7534a4724cb638e575ab40cdf326d" + }, + { + "id": "2e6556ae-04cd-57ed-8b36-1e6aac2aa697", + "version": "MTc1NjMyNzg0NzkzODk4NjAwMA", + "digest": "sha256:d4863b26afe54dc26e7f5f36e75fa5ac57678dc1957da56defe406a242643dc6" + }, + { + "id": "2e76c18a-7bc1-59ac-98bf-1a30c13c95d8", + "version": "MTc1NjMyNzgzNTIzMjc1MzAwMA", + "digest": "sha256:2fa4a456972c63a595036e04ecbd625f0ce7b55c1dc075ebe5bf6dde4c9a3491" + }, + { + "id": "2e819b29-1f6f-5cce-bdaa-82061572c2ed", + "version": "MTc1NjMyNzc0MjI2MTgzNTAwMA", + "digest": "sha256:1ab648b9c1ce11b313c8b8970b0b7ce2d8d833935f91e345e5ae7f81a2a60106" + }, + { + "id": "2ea0a0c0-0e27-5ad1-83b4-fa30642c2b8b", + "version": "MTc1NjMyNzgwMzU5OTI3MzAwMA", + "digest": "sha256:9108004741e8f7722214573a70c08ed6a142b481d0b5c75e3a8a242205fc030e" + }, + { + "id": "2eab6674-f7a2-5692-a55f-52ab918d2b1c", + "version": "MTc1NjMyNzg1MjEyMzE0NTAwMA", + "digest": "sha256:518d9b43ad0d04e5ea44c043a6c88bf123fcceaf1218e3823e142a52dc60761c" + }, + { + "id": "2eb2c809-885e-5465-bb1e-dcebe26c74ae", + "version": "MTc1NjMyNzg0MzAzNDM4MDAwMA", + "digest": "sha256:6caceadefa16ce52fa1ed2f5641607ab59fc4113d36bedc155e09754aa4c35f5" + }, + { + "id": "2edfb95e-d6b8-5410-b589-8aa4b4603db8", + "version": "MTc1NjMyNzg3OTk1ODMyMDAwMA", + "digest": "sha256:9c4da6d279e680f868f6e0725ac4050a9c68f2ef2c5477fd6c6c038e8ff9f43c" + }, + { + "id": "2f0e275d-1c0a-574e-bb23-94ea73fd3950", + "version": "MTc1NjMyNzc3OTc4NzMxMzAwMA", + "digest": "sha256:bc108c7d004a0df9c49294e35237afc7decbbfa6fc1e0cd9d32bc571aff3b28a" + }, + { + "id": "2f305f00-8172-58f3-afa8-26eaf4491448", + "version": "MTc1NjMyNzc3MTUyNzUyNzAwMA", + "digest": "sha256:74205cc3eec01f32e4eb70207c9d0fc21a80cf15eac91fd1837568dc6f123f29" + }, + { + "id": "2f3643cd-ba56-569d-b019-bc17c51da969", + "version": "MTc1NjMyNzkxNzAwODcwNTAwMA", + "digest": "sha256:77dbd8dfa22bb39976550c4855b71ce9bd87a967d4322ee80650db80c8f3e8a1" + }, + { + "id": "2f380b6b-c37b-56d2-9a6c-09b3cbfee197", + "version": "MTc1NjMyNzg2MDMxNzg0MTAwMA", + "digest": "sha256:e4bfd705b97f4908e4c1f1d0e01693def8b002edd269829c950d67be40875b68" + }, + { + "id": "2f4f7ce9-7da5-52cf-a863-2d7eadc40c75", + "version": "MTc1NjMyNzg4Njc4MzA0MTAwMA", + "digest": "sha256:9707395752b47ae80a7436db14b34b3416a9d311c8798f9bb81011e403de078a" + }, + { + "id": "2f7143c5-7fe7-5b69-aa7a-e630d34db025", + "version": "MTc1NjMyNzc3MDY4NjUzMTAwMA", + "digest": "sha256:f04f780dd8942c2dbfa040a6d063ec5456c1cd2d54fa2072eb3f430ec39e4c94" + }, + { + "id": "2fa135d3-35f2-51a2-b956-a06dd9dcc762", + "version": "MTc1NjMyNzg1NTc4MDc0NjAwMA", + "digest": "sha256:9cd8b316a77f6091de3c94ef68204b3f101748337ff055ca5ae5a2cf7440df10" + }, + { + "id": "2fc019bc-9327-502e-9d05-eeadfee83d36", + "version": "MTc1NjMyNzgwNDUyNTg4MzAwMA", + "digest": "sha256:7ddd86cda3e5c17db87d745ce7cc4a32b58c4b2019b21d08e71afd78aae76248" + }, + { + "id": "2fdf0724-1c51-515a-8d38-c898e557f7f2", + "version": "MTc1NjMyNzc2Nzc4OTM0MjAwMA", + "digest": "sha256:eeb0bc6bd9865c3bd71cebee3dd490870813c4acd84206e2906e69b322e7d4ba" + }, + { + "id": "2ff31495-6f56-5c19-b555-9b22336bdbc1", + "version": "MTc1NjMyNzc1NDA5MjE4OTAwMA", + "digest": "sha256:2caab131b0efe091073d77026f85a085b9bf2b34b79e8f52833fb4bbb14fbd3c" + }, + { + "id": "2ff6ba74-9fd0-549f-a34c-534b2e70834e", + "version": "MTc1NjMyNzgzNTIxNDgzODAwMA", + "digest": "sha256:98f914aee1e4a1fc4a91d5f90b77f62b4102b5e328c8770c1a5a189d2fbd3005" + }, + { + "id": "3022fb12-ff47-5b10-a675-b03b1d195dda", + "version": "MTc1NjMyNzkwNzI2ODU5NDAwMA", + "digest": "sha256:e4fa8bdf362c2a1f10c3cb554cab193ddad35c04efa24f99f3b5a4d7c255b7dd" + }, + { + "id": "302feb01-70ba-577c-bedc-11c100e4cd83", + "version": "MTc1NjMyNzc1NDAyNDc5NjAwMA", + "digest": "sha256:5f2e1ef7b3345f1a541058b4b874c018b4f7d7f6d057226fc7a8c35f1279a00b" + }, + { + "id": "3036c37c-7840-5d25-95c4-c3be764508b1", + "version": "MTc1NjMyODA2NTc5ODM0MDAwMA", + "digest": "sha256:4a68884e687c851317c2e070bdf82ce3f17b0eac7ae3f7fc13383a66a6892f4c" + }, + { + "id": "3051e3fd-4734-5a8d-87cb-3528bdab0437", + "version": "MTc1NjMyNzc1MDAzMTkxMTAwMA", + "digest": "sha256:50775b2374aae324798e9241e99a346f9ea2c9ef96020ed789eefcb3be85b77a" + }, + { + "id": "3061f569-0806-5ae3-9703-645e08cfc507", + "version": "MTc1NjMyNzc4NzMwMTk3MzAwMA", + "digest": "sha256:ac9c40571cd70c5ebca469adf0bdf2ac372ba8609d5d84d91fb40f407898c798" + }, + { + "id": "3062b911-319e-56a2-a25f-0fbc167ca40e", + "version": "MTc1NjMyNzczODQ3NDg3MDAwMA", + "digest": "sha256:c8505afc43ae506e62530b376eb68803805d1b995a6b3bb64e03f817ad51e3b4" + }, + { + "id": "3087c075-1fb3-53ee-b95e-bd5c8f07be91", + "version": "MTc1NjMyNzc5NTE0NjQzOTAwMA", + "digest": "sha256:fa91735c26d23fb591b39c5c324c982d316fb75310822c736d9b942c83d4521c" + }, + { + "id": "30b082b3-0d21-57a3-8ba1-2961780a4932", + "version": "MTc1NjMyNzgzODg4MjQ0NDAwMA", + "digest": "sha256:d7e2e35d905c65b7ca9ee9d58b57b41e2f81b551f31f015ded0f820c27944aa4" + }, + { + "id": "30cf0ef0-8d43-5a91-9fb6-64e5c5a2bc8c", + "version": "MTc1NjMyNzc0NzUzNTIwMzAwMA", + "digest": "sha256:c0a87def6a7e20ca846b9c3b52df127621e8549a614834e111dd6277d40708e9" + }, + { + "id": "30cf3a76-6e1c-5ea8-92a8-0626210833cf", + "version": "MTc1NjMyNzc5NjExMDY3ODAwMA", + "digest": "sha256:f8f5e4617cf73cab4cd1e78db37eb4742092e5db77a3b4d6fb17992c935edec7" + }, + { + "id": "30d8350b-c932-57e3-bee6-8aa60ae13bd2", + "version": "MTc1NjMyODE4MDQxNDIzMjAwMA", + "digest": "sha256:b160c6a26ed7e517f4b86c2846f66cbb2c283f098f371b1f8863bc3037a0eed9" + }, + { + "id": "30d8a18e-8e17-5a83-a1a3-649c43d249f7", + "version": "MTc1NjMyNzgwOTIzMjkxOTAwMA", + "digest": "sha256:63c912d2233536e1694a80e942d1660d84e69265816a8588d9cef7e15a6b97b3" + }, + { + "id": "30e396df-3580-51a6-a971-0bf7afeb1ce7", + "version": "MTc1NjMyNzkxNjk4NTYwNTAwMA", + "digest": "sha256:e656d428d42e7eb37d5cf7e17a14c085f4c535928c084d296b511efa0c67e0e8" + }, + { + "id": "30f02a37-45ab-5bfb-be13-a2f500859190", + "version": "MTc1NjMyNzc2MjM3OTM1MjAwMA", + "digest": "sha256:35064cb87826201b67f624579ceff28144849168a45c87671e7ccafaa0e220d4" + }, + { + "id": "30f6c8b8-5494-57cd-9845-5a507668e10a", + "version": "MTc1NjMyNzc1NzQ1ODgxNzAwMA", + "digest": "sha256:9a0e8a143fb7c62bea38e074a2add85008bcc2faeb1fca7a4c768309a5340653" + }, + { + "id": "30fc4813-e385-52dc-b152-6ea12ee3066f", + "version": "MTc1NjMyNzc3NDUxODgxNzAwMA", + "digest": "sha256:d3aa07ffae352fa03fe69748e27f7bad175525f45524860be0fff40e4d946bf4" + }, + { + "id": "31029f64-337b-5512-9a4d-fabc518a35e8", + "version": "MTc1NjMyODE3MTEzOTI2MTAwMA", + "digest": "sha256:1dee5eb6cfe5e513fb7427fa7ccc923cfe18e3fc4d5d59bd928dd070580d2b82" + }, + { + "id": "3120bf1a-2057-51d3-918a-318a72e2b065", + "version": "MTc1NjMyNzkyOTI1MzgxNzAwMA", + "digest": "sha256:65330c720a0377ba7196f82a4e93fd72df2658909e129f4e60988689f533f32a" + }, + { + "id": "31332aa5-3166-5573-ad0a-5f5c7e198f94", + "version": "MTc1NjMyNzgwNDU3OTA3OTAwMA", + "digest": "sha256:3afec5389af6ab52dcb48919b2eb9bc4d312d0d8aeaed9a04bce04dae2592017" + }, + { + "id": "3133ab34-4add-5b5a-8442-f4f2e007ac29", + "version": "MTc1NjMyNzkwOTE3NDI4OTAwMA", + "digest": "sha256:7610c83a9c6b5a28a1ec6cfc4c14f3387f45111ac19c665499f7e23e4861cb33" + }, + { + "id": "3140b2e7-268a-512c-ba1a-c06c0659eedd", + "version": "MTc1NjMyODE2NDUzMDkzNDAwMA", + "digest": "sha256:7b9f459c9bf60075f7e7e3a4760ecf64a09b597ce918d7c7854e4186eb2fd959" + }, + { + "id": "3143258b-2a26-5644-9132-00899547d22d", + "version": "MTc1NjMyODE1ODE2MTc4OTAwMA", + "digest": "sha256:e945615b2764630848fca9c59c144c059dfccb33dcfa6453940069232c5b7a63" + }, + { + "id": "315394a5-902b-5520-87bb-3718a60906f6", + "version": "MTc1NjMyNzc0MTY3NjQ1MjAwMA", + "digest": "sha256:91098030a630aa96b8b715dc3bf521a4832b0d396e07f0adf3a8add79d3cc7de" + }, + { + "id": "318c4ec9-b28c-5af2-992f-55db1a3db3b5", + "version": "MTc1NjMyNzgwNDU4ODgxODAwMA", + "digest": "sha256:3bcaac996ed0d60d54caf089bb035bebbe633218ea30579fe66e42917940576d" + }, + { + "id": "319d80ec-f771-5017-9d99-a8d77243ed07", + "version": "MTc1NjMyNzc4Mjg2MDUzMzAwMA", + "digest": "sha256:8356505a954a27272a637d7c94e3c729b0e6b39f285e782009fc55ae556d288f" + }, + { + "id": "319efdd1-a777-5287-bca0-0e9a8430dc4a", + "version": "MTc1NjMyODE3NjIxNTE2ODAwMA", + "digest": "sha256:b8922d66eeaf2a3d9579e2b893cd916132d9b817d76712e6bbe90d289d588506" + }, + { + "id": "31afc1a4-d5a9-55ee-b669-3bfde5120983", + "version": "MTc1NjMyNzc1NzQ1NDQ1MjAwMA", + "digest": "sha256:6e373016227a1689373f619adb59cfaade5ce2328476f61dd9b178c06ce409e7" + }, + { + "id": "31b57a7b-ceb5-5709-b4ae-c1f0c4f73c47", + "version": "MTc1NjMyNzc2MjM1OTk2NDAwMA", + "digest": "sha256:920fdb93c984758b66ea1e3e64033778dc7f21975f55e12cc3e261dfecbf77f6" + }, + { + "id": "31bd5121-3578-5b4e-9b88-be864f0eef8a", + "version": "MTc1NjMyODA3NTc5MDQwMDAwMA", + "digest": "sha256:8806dd5f5016b3f5e133134b049dbaa52ccf256f6f31d73e072ddac9de48d554" + }, + { + "id": "31d44e9d-e99f-5b21-820f-c795ec9bcfb5", + "version": "MTc1NjMyNzc2NzcxNzQwMDAwMA", + "digest": "sha256:5841711c8d4209b03e717652ee88083a02479ac04e8776231864b80e425683ac" + }, + { + "id": "31db5588-4d5c-5a47-ab12-7f4c4441f85f", + "version": "MTc1NjMyODE2NDkyMDUwMDAwMA", + "digest": "sha256:972ec6b7fe84ffe42a45bf64e78b89d0028946df44217737dc5b8cddcf1e14be" + }, + { + "id": "31f9fd66-2486-521d-b555-998526714877", + "version": "MTc1NjMyNzc2Mzc4NjUxNTAwMA", + "digest": "sha256:9b96e0c328f79bcf1bd4d2c1efd4d7840d9fcde5f033ba6550b4f999ca9f3e86" + }, + { + "id": "320dc945-a0ab-55d0-970b-cf853eac140d", + "version": "MTc1NjMyODE3ODA4MzQ0ODAwMA", + "digest": "sha256:9d898a3210f02a2043f8205528595d4e0e073b1984332f3fe80c2645ad951ce1" + }, + { + "id": "321c5b25-0d60-5566-9152-716cbdc4b0de", + "version": "MTc1NjMyNzc3OTc5MDYxODAwMA", + "digest": "sha256:ae27c28d11c349d9554e948902559e2ae0d624b5e6ae15faeff5a77333405914" + }, + { + "id": "3222f583-b4fb-5a3e-b806-7c0010d02e37", + "version": "MTc1NjMyNzg1MzM3NDUzNTAwMA", + "digest": "sha256:55584e33bfff0f2030fa62e6996cf0ed72848d7389b866d0f95b668cad676d28" + }, + { + "id": "322c216e-ac1a-50e3-b1e4-cbdbe88c3100", + "version": "MTc1NjMyODE3NjE2ODY4MDAwMA", + "digest": "sha256:89d8bb178e1a7e79a40a318269f771bf9240e3aad3ee2cc63acc63114904ecbd" + }, + { + "id": "325e452a-9d67-5a5e-b00c-cfb09602c773", + "version": "MTc1NjMyNzczODgzNTc5NzAwMA", + "digest": "sha256:4da0d9e157c97c2b6a07fb63f37e7964cc533ea67b22dcedfc8005ac9356898e" + }, + { + "id": "326cda5f-fd61-5c70-ab64-41e6d01cb371", + "version": "MTc1NjMyNzc1NzY3ODUyOTAwMA", + "digest": "sha256:0e2aa733d11450407c51e5fcfcd6f353506011a8120357c7461bd212caff8a39" + }, + { + "id": "32795428-4203-51b5-adda-b19cbebc29f2", + "version": "MTc1NjMyODE3Njc4NDQ4MzAwMA", + "digest": "sha256:11b5564efa473465f4bc1bd767e7911d9703c2ac76ccbd974d29ad40de0622e0" + }, + { + "id": "327d66a7-d9f6-526c-9ae7-7c55dcef9044", + "version": "MTc1NjMyODE4MjA4MzY3NjAwMA", + "digest": "sha256:0c986a4e21d3b6ed3d223af7ba3831a50b9cd7c30f2d9844daf572641e3254de" + }, + { + "id": "3290ec7a-31d1-54f4-bfef-65df4a61dcb1", + "version": "MTc1NjMyNzgwOTQxMDQ5MzAwMA", + "digest": "sha256:d9a201a97dd41f0cd50e69adb877991b02ec1795ba64877ba5ee20c279fd0f6f" + }, + { + "id": "32933555-0750-55f0-978b-06956908f3a9", + "version": "MTc1NjMyNzc4NzM0MTEwMTAwMA", + "digest": "sha256:98616aab65115896bb1bf45a8a5ea6344371e1784b825ee22b1c42ea2940842f" + }, + { + "id": "32a046c7-0efc-5aee-8add-b6645e4b73fb", + "version": "MTc1NjMyNzc0NTAyOTM0MjAwMA", + "digest": "sha256:d60b0bb135d3dedb3014a3481a3a51b388a34de546f39b630cbe3b07f0d57821" + }, + { + "id": "32a9ff78-21e8-5ff4-b4c5-eb6f00b7ef86", + "version": "MTc1NjMyNzg0MTA3MjM3MDAwMA", + "digest": "sha256:4b61859410c248d67f251b4d9cd294c2a147fbdc38f211633c1a3eb459ce54bf" + }, + { + "id": "32ad824a-eff5-5e86-9fcf-344335ac2e6b", + "version": "MTc1NjMyNzc0MjUyOTM2MTAwMA", + "digest": "sha256:e6f8d88aa525cc191e81ddd5242631318d5bdb3e5b56da26d1f75f5b672616dd" + }, + { + "id": "32b4d1b8-279f-53dc-9653-16a7598184d0", + "version": "MTc1NjMyNzc0OTkxNjgzMzAwMA", + "digest": "sha256:cc9eb35c49dbdff689b5c2bfeda89c59ece5dd16b3f89caa4fc4ca20c01311c6" + }, + { + "id": "32c1291d-8814-534e-a0e4-f27368da9e65", + "version": "MTc1NjMyNzc2MzczMjA1NzAwMA", + "digest": "sha256:c7c52e63f4a3fd533adb13d009e059236409832af312f7ad37014b62789b9224" + }, + { + "id": "32dcf427-652c-5048-b594-5c8cfb8f3436", + "version": "MTc1NjMyNzc2MzY2OTgxNjAwMA", + "digest": "sha256:8931fd839d672d07785e5094357d8b80d556b4f79679fdd49ba3357cf12fc56a" + }, + { + "id": "32e0f33f-ba1f-5991-932f-d7d7bbea54aa", + "version": "MTc1NjMyNzc3MjkyMjkwNjAwMA", + "digest": "sha256:e9a0bb5fab4c75ae2fd98bade9cd32a253569bf595a165ce4df8671e19bed576" + }, + { + "id": "32e333b6-7e73-5c70-93c2-62c2940cc140", + "version": "MTc1NjMyODE3Njg5NzQzNDAwMA", + "digest": "sha256:dc1c85711692eb44fd2ae750dd474c68b7868a158de7f448ec852d95c9e15181" + }, + { + "id": "32e39419-b408-53a6-be1e-a88fd26c1bba", + "version": "MTc1NjMyNzg3MDQ0NzMzNTAwMA", + "digest": "sha256:82831665494d01876bf0e34544f1f532c28f0d016b967469292f60fda55120de" + }, + { + "id": "32f5e94b-758c-5431-8817-042ebd0645c4", + "version": "MTc1NjMyNzc0NzY1Nzk3OTAwMA", + "digest": "sha256:cabb59be98d4a9fc0737f64ef25f414f0f0c4000937037d8dacef1d642520a45" + }, + { + "id": "330727ee-fd8a-501c-bdc6-3b64e03700d2", + "version": "MTc1NjMyNzc0NDUxMDc5NjAwMA", + "digest": "sha256:3d08f01ef22b2342a41be9158f494f853c1aa92bf8bad76bbb04412d056b248a" + }, + { + "id": "3314bf47-5ef6-5562-8594-c38b36c6ab04", + "version": "MTc1NjMyNzc3Mjk2MTY1NDAwMA", + "digest": "sha256:04e907c41781736c2a1d162cbb6a653c37656d0c0262b3fb6001721923a760db" + }, + { + "id": "331931c6-9b4d-50dc-912d-c1d511fe5498", + "version": "MTc1NjMyNzc1NDA2NjEyMzAwMA", + "digest": "sha256:1ee7f9119c5a0bced526a560fbd284d71f8cf7dee4427abf43cd4908c5756242" + }, + { + "id": "331d022c-547c-55a6-acd0-ae17213b19f0", + "version": "MTc1NjMyNzc3NDUxODM5MTAwMA", + "digest": "sha256:d6a695c376439d28c516d6ad59de65da921c0035c3802b1283129214b4ae9aac" + }, + { + "id": "3332521f-d43d-58bb-a0de-8e8bd86c1673", + "version": "MTc1NjMyNzc3MTUxNTE1NjAwMA", + "digest": "sha256:2d95aae0faad2c48cc412f935932d550d8c77ab3ff8aee63a2a1be734ae1e41a" + }, + { + "id": "33476062-b884-5471-9d39-94892e9947a4", + "version": "MTc1NjMyNzg0MzA3ODM3NDAwMA", + "digest": "sha256:326009e2b3cb8d117ed3f260f629243d2e2622a9a19eb6475244c2ba67e65eea" + }, + { + "id": "3384bfe1-aafd-5517-bea8-52e89898eef8", + "version": "MTc1NjMyODE3NzU1MjM0MTAwMA", + "digest": "sha256:61c491bf542c5d67f2d11c8d2d92896b4360735830d7da6466dfe34c9743256c" + }, + { + "id": "338a9243-9992-57d7-ae78-c5366752c743", + "version": "MTc1NjMyNzc0MjI5NjQ3ODAwMA", + "digest": "sha256:7b6c213bf6e0022cc3ca3a7620630dd218cc1617ac462d70dfee9b39b746eb65" + }, + { + "id": "33985675-cf0b-527b-a11f-fdd293c35f94", + "version": "MTc1NjMyODA3MzA3MTk5NzAwMA", + "digest": "sha256:5ae9fadeaaf7d6249b9e07cac2818c5e9085b20532458c3072cde4d02e9565f8" + }, + { + "id": "33a5e07e-43eb-5920-9e20-44de6be92ec6", + "version": "MTc1NjMyNzg2NjU0MzQ5NTAwMA", + "digest": "sha256:4983e57bb8737c4fdd78da67612ad62bca850fc0a1271c9c0befd0e761a510b6" + }, + { + "id": "33b20a44-3b4f-5df8-b1a2-3e8a3fd9c557", + "version": "MTc1NjMyNzg1MjA5MDg4ODAwMA", + "digest": "sha256:bbe4aa07683350acedb4ecf2bae2ffd04348bd974e041d22bef158f857e19553" + }, + { + "id": "33c12283-801c-59da-814b-ed8b4dc0c4b2", + "version": "MTc1NjMyODE4MDQ4NzM2MDAwMA", + "digest": "sha256:0ab976a1f315e6fe08c9952bc7e6af785aa3921cad628a65e5e2d75db77c4eda" + }, + { + "id": "33d7542a-49e1-520e-83a7-3ea7ae975d55", + "version": "MTc1NjMyNzgyMjMxNTA0OTAwMA", + "digest": "sha256:9583e31e47ccb032031c23b296e1dac7125d25036fc45d95160349f9dfa285fe" + }, + { + "id": "33ebd6d6-c116-5f16-92e2-2a7ba93ebb26", + "version": "MTc1NjMyODE3NzY4NDc0NjAwMA", + "digest": "sha256:2136aa8c6b02a73e65abe281755a6f38d42600161f38f83ee0bf60475af601a4" + }, + { + "id": "33ed873f-3019-53c6-8d9b-0dbf60ca4eb2", + "version": "MTc1NjMyNzc5NjE5NjM0MDAwMA", + "digest": "sha256:8ecb119b5dab5e65ca404b37826ca61ffac0695ae26c4918b5f3675a3a098856" + }, + { + "id": "3408e32c-9c48-5ed5-8e61-c301145cf0c7", + "version": "MTc1NjMyNzg4Njc1NTYxNjAwMA", + "digest": "sha256:69797fb0c67854e95e5312966b6d768321e34ee9c9f3832b033307e06de6fd2f" + }, + { + "id": "340dedf7-5a89-515d-a2b3-ec964f9d85b1", + "version": "MTc1NjMyNzkwMzY3NjU1MTAwMA", + "digest": "sha256:32fdf36651cd1aff8fa5bde7bc8075ddba8816b4d471bf5b61be9ea2a47f9bd5" + }, + { + "id": "34396c65-731e-549c-9851-c3756f819372", + "version": "MTc1NjMyNzc5NjAxODE2ODAwMA", + "digest": "sha256:ca8c7dedaf21bb465bfe47215c237c6f65074b09a32f60eed8a9de6d49b2201c" + }, + { + "id": "34448ba9-0874-515b-ad5d-194a08e89d61", + "version": "MTc1NjMyNzc4MTA0OTA2MzAwMA", + "digest": "sha256:d7663b11d9f2eef739ac217b6c3fd2178076b7c9e57210399047cbd3bf64caa2" + }, + { + "id": "3452dadf-7e89-53f1-91cd-f6aa2da0ba3f", + "version": "MTc1NjMyNzg0MTE5ODA4ODAwMA", + "digest": "sha256:87db29316a1aa24b3cd961c88d548adaf13cca3a0922abb9e4c818490142e89e" + }, + { + "id": "3470ac72-b671-5121-ada6-8c959496e781", + "version": "MTc1NjMyNzg3NjU1ODM4NjAwMA", + "digest": "sha256:96dfd10892636137706536dd51d95f0340b6b40e719139986ec8584e1089262c" + }, + { + "id": "348a615e-708c-5149-8eae-193b18d543df", + "version": "MTc1NjMyNzc3NDUwMDIyNTAwMA", + "digest": "sha256:8ad9028a613a06a40ae8e70df900eee5c8a0ad47f00089209d2b6ccd2ea920fa" + }, + { + "id": "34c37e00-2ec2-543c-8add-f09be685495d", + "version": "MTc1NjMyNzc5NjA0ODI0OTAwMA", + "digest": "sha256:e804c1a9eff8582ff4d642a9898823e06670a6def9270906fd49e6be02bec235" + }, + { + "id": "34d46b63-6932-5809-9b8d-c73a556f2b1f", + "version": "MTc1NjMyNzc4NzM3ODQ1OTAwMA", + "digest": "sha256:b113ced0576741f15c5da00cfa0f0c2c569c5e1d8f1319d67cbf3d00c32a893d" + }, + { + "id": "35320167-fe61-5b89-8075-6879696758bb", + "version": "MTc1NjMyODE1ODIyNDUzNjAwMA", + "digest": "sha256:16a35ae83e9297dd62a9fb3fef225f570487363f2b086cbfcb2986cca1629820" + }, + { + "id": "3540be41-3adb-55dc-9855-9e0a6524a1ce", + "version": "MTc1NjMyNzg3NDg1MDAzNTAwMA", + "digest": "sha256:93eab8b12edd07dd87fc8c8c51c27d6d896f67c9c861b13c695d214cea2dc2db" + }, + { + "id": "355e226f-359e-540d-b1fa-f3e820a7d921", + "version": "MTc1NjMyNzc4MTAzODIwNDAwMA", + "digest": "sha256:3120a25145cbfbd0042031da01107ccb0385c4f5d61019c623ae71e692891640" + }, + { + "id": "358266b7-15d4-5623-b19e-5695805a2109", + "version": "MTc1NjMyNzg1MjEyMzE5NjAwMA", + "digest": "sha256:034d2a12afc06567ae5b05f451d09dfa80776f041cfdd275d5c9307fb643365f" + }, + { + "id": "35be72c3-c0f2-509d-96cb-6def79a5aecd", + "version": "MTc1NjMyNzc1Mzk0MjI1OTAwMA", + "digest": "sha256:509a43d31b7af754eba55984fb3abffe4da5782906edf08690dc243453d63bee" + }, + { + "id": "35d36809-2671-5536-962f-0da26dd77a6b", + "version": "MTc1NjMyNzc0MjUzMDQyNDAwMA", + "digest": "sha256:6693af5e0a852ca0c792bbd04dcd0df6bf05763c697f38bdc91acb1321766814" + }, + { + "id": "3613b5d2-b46d-5e78-85d7-3133dbf05172", + "version": "MTc1NjMyNzg4MDQxMTMzMTAwMA", + "digest": "sha256:fedb09493af3fd8d345397333535d47f803e45c5f254e7207e87c506e1ba0628" + }, + { + "id": "36210c96-4c19-56df-b49b-6aa08e14d5c8", + "version": "MTc1NjMyNzc4MTk2MTMxODAwMA", + "digest": "sha256:85c428e5a471fd89a5ec0b9cbef75708e958378055a55e5b77a57231a48981f5" + }, + { + "id": "3624543e-b9ca-57a8-ba83-b142fdf888bc", + "version": "MTc1NjMyNzc5NTI3MDU1NDAwMA", + "digest": "sha256:52849448abf502ccc6063ff221f5e444fcf5db7effed347ce3a4d9ec81666e28" + }, + { + "id": "36279199-8b5c-5699-a3b8-3791b45b7dce", + "version": "MTc1NjMyNzc3MDM5NDY1ODAwMA", + "digest": "sha256:16d229d57755370c6eb824f4d7189b9168e74817151ee3c766a6dc356ac892ad" + }, + { + "id": "36291952-84bb-5504-bbd0-0f59bad287ad", + "version": "MTc1NjMyNzgyMjgzNDUxMjAwMA", + "digest": "sha256:87ee145708321339508a9b8838587ec8d30b4611f4bd069194a06e5b68bc0e69" + }, + { + "id": "362dbd3b-9da8-54ad-adf5-5d582bef23fb", + "version": "MTc1NjMyNzc1NjkyMDgyMjAwMA", + "digest": "sha256:a9086f184ab8349820d41ac01a225ec467940d51ed61db17dc98b6e9c01cda5c" + }, + { + "id": "362dd25d-bd04-5220-8576-64bcfc69baa7", + "version": "MTc1NjMyNzc4NzI5MzQzNjAwMA", + "digest": "sha256:679e29aac2e8bf14f14aa6bfb529f03d9b8e552e5b2ac3c48aa655b51121196a" + }, + { + "id": "36382e28-0ebd-549e-b6c0-92f31337d48a", + "version": "MTc1NjMyODE2MzA4MjY3NzAwMA", + "digest": "sha256:6a3811d4218b723c59dd0e0473197b431d36c7488f910b1cd799580430a0779f" + }, + { + "id": "36387aea-34ab-589a-95a6-6f7b684334dd", + "version": "MTc1NjMyODE3NjkwOTA4MzAwMA", + "digest": "sha256:6a60a4a2e425b872dde07fe90e47b855091a7c7ecad8bc5960dd8a6807dfcde1" + }, + { + "id": "363b02cf-340b-59a2-8b5d-2b01dbf8680f", + "version": "MTc1NjMyNzgwOTI1MDIxMjAwMA", + "digest": "sha256:c0be7fc5062a9c984239235a711d55caafbf9a87be0b311100e30abc84bd2fc1" + }, + { + "id": "363f296e-ce9b-5cc6-9046-ffbeedcb48be", + "version": "MTc1NjMyODA2NzQyNTU3NjAwMA", + "digest": "sha256:3b6fb5045f1c667761af820305598219f35b83f5635c40de57cc9edf84e57afa" + }, + { + "id": "364a4f67-696b-5a54-8b07-b0c36b58a831", + "version": "MTc1NjMyODE2NjkzNDM4NjAwMA", + "digest": "sha256:5d5e52288e4d483252daefb02b7a08032fb09d4f5587baaa68fd3bb434cf7b1c" + }, + { + "id": "365a548b-56c1-5646-a0e7-8fc94f241b6e", + "version": "MTc1NjMyNzc5NTMwMjcwNzAwMA", + "digest": "sha256:cccc68137238a401b9ee41673a4702e5580593046942b1cdeb0424d607c49720" + }, + { + "id": "3660ba7e-2dd9-523e-a54f-c54054203ce2", + "version": "MTc1NjMyNzg3NDkyOTk0MTAwMA", + "digest": "sha256:ab1e8f15a2cb2dc503dca79c32a3a2d5b37797fd30961966ade92ac84f37d276" + }, + { + "id": "36625457-f9ed-59d5-a8d0-95b9ec8d5c16", + "version": "MTc1NjMyNzg3OTk1MDg5NjAwMA", + "digest": "sha256:a171019a1fabfde6e94009d3bfc4abe44b7c3aa0284fb682cf618e66267a0528" + }, + { + "id": "3684902a-b4e4-54c6-8df4-9dc7a6cce98d", + "version": "MTc1NjMyNzc0NTcyNDE0NTAwMA", + "digest": "sha256:9e640651154c99a5d87fd96ab370d5a848ecc51d1c2218c5075a9e509e05a0f2" + }, + { + "id": "36b49483-9008-519a-aa8e-6ca2c607cddd", + "version": "MTc1NjMyNzc3MDQzODYzMzAwMA", + "digest": "sha256:030055d11250b3e1c8a603b268a639b66e49af85b65bf3d70fa5beb277b71000" + }, + { + "id": "36c05d0a-c97f-5524-a08c-d98740ac221e", + "version": "MTc1NjMyNzc1NjkwNjE3MDAwMA", + "digest": "sha256:66a3fb6a26e22cdb24dd736d6b14d422924b8492aed864766e050628c2677f5c" + }, + { + "id": "36c260bd-9bef-557b-a95d-13c6befab409", + "version": "MTc1NjMyNzg3NjUxNTg5MzAwMA", + "digest": "sha256:a42b176c64aec3ab0ffc1fb9a1fb538829379d2c21ad17ea9a234dad950929bd" + }, + { + "id": "36d335a5-67e7-5470-8d27-e1812506aaf4", + "version": "MTc1NjMyODE4MDQ3MDQzNzAwMA", + "digest": "sha256:3d72fed17530c047d877b3a4d96322c092a69d0d5498ea0539adf731864843b7" + }, + { + "id": "371264bb-dd5c-5240-a195-91a50e1d21be", + "version": "MTc1NjMyNzg1MjYwOTM0NTAwMA", + "digest": "sha256:73978efa8f27a8f10df7340fedb9642173b031ae4b752ac6e2c6e091824f1efc" + }, + { + "id": "37379d85-164d-5aa0-84a0-945d5a0bdd27", + "version": "MTc1NjMyNzgwNDQxMzM2MDAwMA", + "digest": "sha256:63f57bdd732fe99c29fbd80e32d723f3710ade9acee8e588ef26c86f17f0aee2" + }, + { + "id": "3739754b-6bf4-5fae-a0f1-7eb3c156dc6c", + "version": "MTc1NjMyNzc1NjkwNjQ0NjAwMA", + "digest": "sha256:78ea82791e1ab3b5766e6a0104ff7305818e3ccf4c16734376f8191c0811c118" + }, + { + "id": "374a147c-a2d1-5182-85e4-cc750a45a194", + "version": "MTc1NjMyNzg1NTczNDU0MDAwMA", + "digest": "sha256:b662dbe44f66ac923bc8f27b49aa4e41f16ddb0fdf2dc2e11ede0a27f7092749" + }, + { + "id": "3763b786-224e-5e77-9b8d-3078bdc9a847", + "version": "MTc1NjMyNzgxMTk1MzQ0MjAwMA", + "digest": "sha256:f2216d777d82b3e2a358c3e39cf81f809f182593fb2c07af6bff5cf59084f3f0" + }, + { + "id": "3779c3f0-dffc-575e-9d27-030ceb0405a2", + "version": "MTc1NjMyNzg3MDQyMDU5NDAwMA", + "digest": "sha256:ca6eae2abc2ef01d2a7988022b797a8eb6e55e9beb783e91eb0c86fc28d72c52" + }, + { + "id": "3782cb37-2fa2-533c-a688-fffd103cf256", + "version": "MTc1NjMyODA3NTg3NjA0MDAwMA", + "digest": "sha256:59e702ae907a5388e19e8f1988501442a38394c45a88e7520569e6453d534009" + }, + { + "id": "37951598-7064-5777-af39-32c5ac866005", + "version": "MTc1NjMyNzkyODg0MDI3MjAwMA", + "digest": "sha256:23b996227e6bcc5dbf55d611c3d5d6c6cf1512effdee52ae69655d09cf9cca5e" + }, + { + "id": "37ab3357-b5dc-58a8-bbf2-dcc8906f3d46", + "version": "MTc1NjMyNzg1MzM2MDc0MzAwMA", + "digest": "sha256:b488fc3bea931675392ccb2f329ec7a733c39346538440bbd52c95f148ad5c6b" + }, + { + "id": "37b1d7b0-31d5-5ee5-83e3-dfd53fe8b998", + "version": "MTc1NjMyODE3NjgzMDI0OTAwMA", + "digest": "sha256:80c00095fa2e098d44953f703a46da1fc79b4cf0dd32ab34516ecb9cfbfb2717" + }, + { + "id": "37b7a52b-686a-5999-8bb1-0533692da386", + "version": "MTc1NjMyNzgwMjg1NDg2NjAwMA", + "digest": "sha256:26c547d5ba767c019c4faa94d6ae8f2eb4489f158e039233718b124f582d6143" + }, + { + "id": "37c73c7e-8326-57b9-8646-0d7c32023647", + "version": "MTc1NjMyODAyMzM1NzI0OTAwMA", + "digest": "sha256:225c6037c264c475ff932818e58fa3d3cc7f2f491bea56fb977b710ac4f6cc08" + }, + { + "id": "37cc81ba-5e07-51cb-9432-196218f11987", + "version": "MTc1NjMyODA2ODE1MjAxNzAwMA", + "digest": "sha256:c0f5b40d54e24052ff66024a792dbba75400f82e0206f7bf1eb8c86dc1fc7dde" + }, + { + "id": "37e671e4-b7be-5d2c-8dcb-bbac09b5a25e", + "version": "MTc1NjMyNzc3MTQ1NDkwMTAwMA", + "digest": "sha256:6195da3c1727aefa8823861d108d44a925dd6fe9a9bb3afa7ec2f16ad0943cbc" + }, + { + "id": "3803547c-3c77-5509-9c0e-09ada5e94276", + "version": "MTc1NjMyNzgxNTQ5MzgyMjAwMA", + "digest": "sha256:2bf5b54b827a60eaf5bd3153a73cd799dcf1b6ae36e2f5a9029da6399ee335bb" + }, + { + "id": "380a856c-b61d-5b9c-8592-c042f6433980", + "version": "MTc1NjMyNzg4MDQwMTQ0NzAwMA", + "digest": "sha256:9871bc8691eaeddb095249ce2f7d29e96a0baddb94b9946e5548768d65efe1c2" + }, + { + "id": "3815eb5b-40ca-5cca-ab29-0cc72ad2c26b", + "version": "MTc1NjMyNzc5NTMyOTkwMjAwMA", + "digest": "sha256:6028f11ef4b2d02f4025c5f46e93c5364d561723f490584965bf323eaded53c9" + }, + { + "id": "381bc2e9-0ed6-5a38-8425-cdb98f89b4de", + "version": "MTc1NjMyNzc3MDQ0OTUxNjAwMA", + "digest": "sha256:d3cdd4c48d5069f2160b2446f28ac8df221e9e49267786fe444beadd852a4da8" + }, + { + "id": "381f8a60-cd15-5773-ba03-e79c1feb21eb", + "version": "MTc1NjMyODE2MzAxMjU1NDAwMA", + "digest": "sha256:502fae596b60900e7ca730e7f19f4f33227ebe61eb46c737487f38bb2eca4fd9" + }, + { + "id": "3832a3f4-5d01-5760-8de5-18b96ed977d3", + "version": "MTc1NjMyODA2NTU1NTA4NTAwMA", + "digest": "sha256:876d7d05c5f65170dfe80f3825dfc66c11b537543ca6300e3ffd10affc1a6d40" + }, + { + "id": "3840ebe5-f562-5f12-ae2d-26bc26bae2e8", + "version": "MTc1NjMyODE3NzYwODYyMTAwMA", + "digest": "sha256:6dda4858df62e1ce29b23ade02fc3cc55e13a493b2b49902ca2371845c23d292" + }, + { + "id": "3841c116-8417-554b-8797-359eaf58baa9", + "version": "MTc1NjMyNzg3NjcwMzAwNTAwMA", + "digest": "sha256:e3bc40cd58a1ab05477c9fb170f76b9ad14ce74a370fa17cbfa50fadab04edf9" + }, + { + "id": "3841f2cd-ec72-54ef-a0b0-87799c65de94", + "version": "MTc1NjMyNzc5NTE2MzY4MDAwMA", + "digest": "sha256:2c88f73b5afea25e949df30866eb8beaa67a4fdb528de3966d49921ff84a20fb" + }, + { + "id": "384c7286-eff0-5987-84ff-45a379326b40", + "version": "MTc1NjMyODE3MTE0NTUwMDAwMA", + "digest": "sha256:fdc25f532e5cb388f41884addec779820b1ee4a3a3923fa44b8a1535a8774039" + }, + { + "id": "385b59da-8cc8-5c96-9d39-0e4c9931cf87", + "version": "MTc1NjMyNzc1NzU2NjI4NTAwMA", + "digest": "sha256:b01834fe80caa6023e49df347a5dbd23024a980d2cbe0b8b63eba0051cd1304f" + }, + { + "id": "38744444-6a5e-589b-bfba-1ea8220410b1", + "version": "MTc1NjMyODE4MTkzMTcwMTAwMA", + "digest": "sha256:77673d3c6e3ea42f92ffb3ba210765f10f1af8519186984068136840a8b2f7e9" + }, + { + "id": "3889c4ee-a5cf-560f-bfad-a4894a75fc72", + "version": "MTc1NjMyNzg1NTY5NjIyNDAwMA", + "digest": "sha256:8a46d93a90220024470cce4a8976f19fc27397750b939e71b4c4f8751348ec2d" + }, + { + "id": "3898fce2-0d05-5b8f-945f-602f3e28e998", + "version": "MTc1NjMyNzgwMzU5OTgxOTAwMA", + "digest": "sha256:83f5c2ab21006015c72514f254de875bec9e641782be5ab41dcdae40db666edd" + }, + { + "id": "38abacfe-d414-57ac-b9d3-1af004d8c690", + "version": "MTc1NjMyNzc0MjUyNjUyNTAwMA", + "digest": "sha256:7ff874140d9a05fe3eb31b75683405cc4f70d6788e5b6b5e5389c29f8c49650f" + }, + { + "id": "38ac44f4-de84-5103-b642-f0d67518cd6e", + "version": "MTc1NjMyODE2MzQxMjg2NDAwMA", + "digest": "sha256:e2ad3dcdd006fd676194d98b7a7c47bee525831b1cbc2ae9862bb0d97b62f9e7" + }, + { + "id": "38c78857-4586-5bb0-bb5b-00ae975b4b05", + "version": "MTc1NjMyNzc3NTE5NDA2MDAwMA", + "digest": "sha256:ded25f4b4aa48938d1b51ee9eb9794f43b5649c8ef0a6f15504648e7fe36de0d" + }, + { + "id": "38e02742-bf81-5b46-8818-0ab49d940498", + "version": "MTc1NjMyNzc0MTY0MjYyNjAwMA", + "digest": "sha256:50a1ff3b0f46b4d7ad13ca75c616edc1017642190ca6ccd69dc634138a9a5dfb" + }, + { + "id": "391114ca-3878-5464-9bea-0bd460995440", + "version": "MTc1NjMyNzc3OTkzOTU3NDAwMA", + "digest": "sha256:87fdc9bf92be9c3f04f896772b716766ca1e967ce7b0c7e686c80a4bbfa8bddf" + }, + { + "id": "391d1fcb-e2e4-56dc-bd40-44828f7c2d2a", + "version": "MTc1NjMyNzc1MzYzMzgyNjAwMA", + "digest": "sha256:65c751d1c2eb7091a7b6dae6196ad6eb2f879f7bbca0ee6444930f09025940e6" + }, + { + "id": "392ca1b2-932e-5dad-bd5c-74121978df94", + "version": "MTc1NjMyODE4MjEzMDM3ODAwMA", + "digest": "sha256:d59810c0af830848936ceada4fa7f80b7a574d2dcde83ed1211d873caab611e9" + }, + { + "id": "394b9d17-04b0-5d26-9e79-95a099032754", + "version": "MTc1NjMyNzc4MTA0Njc5NzAwMA", + "digest": "sha256:0c841712faf587d9a0fde4f9a9164b76de52c1b73a619e65edebcae7d58e9fc9" + }, + { + "id": "394e2528-922b-5486-b605-e262d1ab46f8", + "version": "MTc1NjMyNzc0MjQ2MDk4NDAwMA", + "digest": "sha256:bf4de86c3efe0b6d34e95c0bc5089d69d5a88507a5b4e9417629c2ec0690cf58" + }, + { + "id": "398a6a89-6b6c-522a-a56d-fcb0fc559006", + "version": "MTc1NjMyNzg3NTUwNDc2MDAwMA", + "digest": "sha256:bdcaf0971f1201298b280d4f964c0aa0bcc98369f96a31e85e347dcfadbebfc2" + }, + { + "id": "39a3b835-1746-592f-b594-978185369df0", + "version": "MTc1NjMyNzg1MzM3NzQ3MjAwMA", + "digest": "sha256:7ac6e43d79bf263ec30f672827384b195ffcbdadb9bff01192c7300d02fcbeb4" + }, + { + "id": "39b2d3e6-b680-5c41-be64-4ad29f85a44d", + "version": "MTc1NjMyNzg4NzA2MjA0NDAwMA", + "digest": "sha256:6212d2bef76f318d7a0f2f6c72b081b619e6b10334ad2e7e873d81fada175d2b" + }, + { + "id": "39bc83d2-464e-5199-832d-66298e142ebc", + "version": "MTc1NjMyNzgwNDU3NDA4MjAwMA", + "digest": "sha256:8345d3a0cfa0709ee4dddb0fda548b4ec539031f1e5c4885edc10fb97829c1cd" + }, + { + "id": "39c0102f-6bba-564b-a540-664d672438dc", + "version": "MTc1NjMyNzkwNzM0ODY0MjAwMA", + "digest": "sha256:2e5d14883dfeabfc733f0ef85cc371a02c7b55fbddbf57c3c95967b0958e9e4c" + }, + { + "id": "39dde72a-188a-5df8-9a3d-276eb35d61f5", + "version": "MTc1NjMyNzg0Nzg0NTkxNjAwMA", + "digest": "sha256:825a7a211eb93a5d346d5985f8f245e051f71dd71fbc888c4ba8d6a7d64aee58" + }, + { + "id": "39e97923-6f06-581c-9f5a-fbd0665f14b1", + "version": "MTc1NjMyNzgzNjMxMTc1NDAwMA", + "digest": "sha256:543a7528874c7bdd9ba4e954c7e8ee6e8adee39eb700afc19941880dd271d6d3" + }, + { + "id": "39fd4bab-8598-5a65-8fc9-a27086c93cb4", + "version": "MTc1NjMyODE2NTg5NzE1MDAwMA", + "digest": "sha256:24ad4d918bf87c7cc668ca40eb34e3814c21740022533222fe6b6acc6dccd953" + }, + { + "id": "3a02ec7c-2132-5e2f-b670-eb4c649d0be3", + "version": "MTc1NjMyODA3MzAxNDkxNTAwMA", + "digest": "sha256:e9b5d3e0a85f699315615ef1bf5874fcbe821ac58ba729c95da46e67007c2606" + }, + { + "id": "3a0d9837-9e2b-538c-9a6a-e80642d81ced", + "version": "MTc1NjMyNzg3MjAwNTE0NDAwMA", + "digest": "sha256:69c98653441f322c6785528a3fac875310f53b403071d312394be6625c445e7e" + }, + { + "id": "3a1c28e4-18be-543e-a55b-be929ea1a246", + "version": "MTc1NjMyNzg3MjEwNzM4NzAwMA", + "digest": "sha256:cea3398bdd9beaf11a20af9ca5353b842b5f4db09d88aeee203cac21640969de" + }, + { + "id": "3a1ce6d7-e83d-571a-858d-e87ef5d1974d", + "version": "MTc1NjMyNzg1MzA2MzM2ODAwMA", + "digest": "sha256:b8180984262eba6232c3b2745a515598835af42f0b26d3f3a6b15ba4c9794a93" + }, + { + "id": "3a2d75d2-e95b-5936-b1b0-4cf8a293d730", + "version": "MTc1NjMyNzg4NzM2ODUxNTAwMA", + "digest": "sha256:961a45898b616c9b56f18fccff2d9305df6e3a256b7d30db2285a3463891d999" + }, + { + "id": "3a3a0d0e-e97a-5153-ae4f-a94f98e2c2b0", + "version": "MTc1NjMyNzg3NjcxMDI0NTAwMA", + "digest": "sha256:86b2cd26d4386761a7afe5b7fb8c9a4b1e47e0649e43b616a52d02b4b2f0bbad" + }, + { + "id": "3a59bedd-42aa-5738-8eab-cc6bcf531447", + "version": "MTc1NjMyODE3NzYzOTA1OTAwMA", + "digest": "sha256:dc349d6086469ffd511f97ffe80c6b79c55643452e425715b3cbaaed4d38f87c" + }, + { + "id": "3a8820f4-5e8e-507d-ad76-3835851a2224", + "version": "MTc1NjMyNzg3NjY5MDE5NjAwMA", + "digest": "sha256:6d8ce2e0b5eacd126de6afc492a27f007a956c9e325ef9a8da7637729becd0c4" + }, + { + "id": "3acd4710-c5da-5a47-aea1-b512307aa965", + "version": "MTc1NjMyODA3ODAwMzg5MTAwMA", + "digest": "sha256:142993fe83b79c39f193ce47f4979d9d3363e10e830a33c79912cb0a2148eb47" + }, + { + "id": "3acfab73-14ef-55cf-a95a-01471831f6f9", + "version": "MTc1NjMyNzgxMzUxNTM4MjAwMA", + "digest": "sha256:8b75f11c0ffdc5bca117358762b55ce4e3ac178e6a4008ad239fc075c46a5955" + }, + { + "id": "3afa02ab-2cfc-590c-88bd-a36a7d31965b", + "version": "MTc1NjMyNzc0NTY4NzY3NDAwMA", + "digest": "sha256:d33a7598d7dae98874ed2236aa56d516912fe3eecee8dfa683137209bf821b1a" + }, + { + "id": "3b206789-0924-5b6c-91f3-db0352be65ef", + "version": "MTc1NjMyNzkwNzMyMjIwMTAwMA", + "digest": "sha256:f40196cadf045feeac742cb1ca9a74d96c3d9960c9b5fd265ac21172cd350098" + }, + { + "id": "3b2175b0-e64f-5b48-877b-155ed506dee3", + "version": "MTc1NjMyNzg2NjI0Nzc2MzAwMA", + "digest": "sha256:f12b32c909ee0419fe7ecfac9447662b0ecdeb5fe989d72f843956277ac38e74" + }, + { + "id": "3b333ff5-1510-55af-a061-4b9f6e4c6fcd", + "version": "MTc1NjMyNzc4NzMxNzY4NDAwMA", + "digest": "sha256:e368c43dd0509b58b5c3c902eabf14f9f3cd3ff9b689f852d09d5be3f8494cb6" + }, + { + "id": "3b40c1d1-0ce0-5ea0-9227-bd5f69e8dbbc", + "version": "MTc1NjMyODA3ODAzNjgzNTAwMA", + "digest": "sha256:6725e7fb4437f43603d4b5b9981f99a6b6dd32a3a5876066835cca89087f5049" + }, + { + "id": "3b4be570-803a-553b-8f22-f507f059f840", + "version": "MTc1NjMyNzc1NzM2OTAxNTAwMA", + "digest": "sha256:4c7f3451faa1404167f7c00c0bf2d58087c8b77f13fd2d2b01357781e0a5d628" + }, + { + "id": "3b517037-5c4b-5b46-9a72-9f019caed2e2", + "version": "MTc1NjMyNzc4NzAwMDQxNjAwMA", + "digest": "sha256:4865c4a3f37e1e52db7d4c0f271ac03183e8407a64acf9b78cba40b4d8b5c079" + }, + { + "id": "3b6743bd-4572-50f5-8346-eb145bb9de57", + "version": "MTc1NjMyNzc1NTk0OTg0NzAwMA", + "digest": "sha256:8ba486a90f1b6c355b37fa884d77f1ebf0ec309987817a95f47ed5712a4dff87" + }, + { + "id": "3b9a9a32-494d-5b13-9fb9-c883dec90dd2", + "version": "MTc1NjMyNzc5NTk4Nzk5NzAwMA", + "digest": "sha256:2619c8f9cd3a827055043717c156419a3d96c60e1e31fda138f9e44e82ba02b6" + }, + { + "id": "3ba22eef-58a3-50b2-95bc-9856ef26e801", + "version": "MTc1NjMyNzc5NTE1OTE5MjAwMA", + "digest": "sha256:d85ffdf41722d302fc68294efd8bd3e919f4c032cb2a20a7b7ecfadf1848628c" + }, + { + "id": "3bb41b29-c134-5c2a-9e11-ddefc4cb0365", + "version": "MTc1NjMyNzgxODkzMTIyMDAwMA", + "digest": "sha256:0ad7ec5f1402783cc2b9d95aa26c38c13afeaa3884ded50ab22f6b672a16f742" + }, + { + "id": "3bb69550-aef1-52a6-b531-726aeeeb234a", + "version": "MTc1NjMyODE3NzYyMjAwMjAwMA", + "digest": "sha256:b915f713f3f8b8dd7202a1143d5202acd94f99f26ef47c074c7353870dbacb02" + }, + { + "id": "3bba6b9c-6b3c-5cf2-a2f2-d7ba859f1704", + "version": "MTc1NjMyNzg1MzgwNjA1ODAwMA", + "digest": "sha256:1d9431b9b66c50032089e7d51b22120968049f75fd59f51d24a57ac1c945f5a5" + }, + { + "id": "3bef7d86-f02a-5fd0-b185-f65e322ba65a", + "version": "MTc1NjMyNzc0MjI1NjM3MjAwMA", + "digest": "sha256:98f0276616dbf6e14413a98c40a7b4d3770e08bef04429075a1f2944765a92c8" + }, + { + "id": "3c1fd453-b6dd-510c-bf15-994ddfd2f0b3", + "version": "MTc1NjMyNzc5NDc0OTUxMTAwMA", + "digest": "sha256:fae175114e9fc7e98e89a72b087e0135bcc60b44bf192b2c0571b729a78d954f" + }, + { + "id": "3c21aca9-0761-5d08-8809-a34b042d592d", + "version": "MTc1NjMyNzc1NTg1NzI1NjAwMA", + "digest": "sha256:ff47477c809c9cb2431960cadf3850e180003c4370d2d782136fb84ff071fe21" + }, + { + "id": "3c3a2d3b-86ab-59de-974b-e6e520f1540c", + "version": "MTc1NjMyODA3NDQ5MzczMTAwMA", + "digest": "sha256:958ec8de6a2af677c4409863429deb790c6335b57cfd92f8b7e075c716e93f3c" + }, + { + "id": "3c3af3ba-450d-56a8-a4b7-76a4964cb47d", + "version": "MTc1NjMyNzc0NzU2MTc2MjAwMA", + "digest": "sha256:e1afc027b431b324f2f983604fcaab529a40b3b06583345d1d9fa43b217c682e" + }, + { + "id": "3c451dd0-b0f4-566a-aa3f-3e7936cdba56", + "version": "MTc1NjMyNzc2MjM4NTAxNzAwMA", + "digest": "sha256:4c3fada565b19901eee5a6caf7319d9804ad8218148272c26f9917e0820a5c11" + }, + { + "id": "3c640b22-e450-5e79-8422-1d373a9f8dd6", + "version": "MTc1NjMyNzc2NjQ3MTY4MTAwMA", + "digest": "sha256:498433eec5869986cf091e7c01c082cd82c6c3fb43b5c08945baa51922774a18" + }, + { + "id": "3c6d3c05-b267-5972-b957-1d2725fd89f7", + "version": "MTc1NjMyODA3MDg3OTg1NjAwMA", + "digest": "sha256:7a97a7dcf56ebb4bfb5a4aa3fccbdaf1663ed6b316ebe99d6bfa1700d174ba68" + }, + { + "id": "3c6e00a9-f220-50c7-80b2-be825c74e85b", + "version": "MTc1NjMyODAyMzk0MjExOTAwMA", + "digest": "sha256:3f7f0f7cfaa254c9bf409472ecd295fd9f077ba1e1972b29d07ad5822705e5b4" + }, + { + "id": "3c8c2d2b-680e-5765-8322-e72eba340e2b", + "version": "MTc1NjMyNzc1NDAwNzAzMjAwMA", + "digest": "sha256:bc16d2686d237fbb325aadd1e42d8d01d71ad44a164d74827bd4d73637e193b7" + }, + { + "id": "3c9f03e2-bfdc-5c32-b6f0-bc7723323c2a", + "version": "MTc1NjMyODE3NjgyMjk2NjAwMA", + "digest": "sha256:e8c8bfea63960c84ecd2955d1b972afde71cb801bbba11e318568d633e7cbc0b" + }, + { + "id": "3cc5b974-7442-5b89-97c3-b11c74909968", + "version": "MTc1NjMyNzc0MjQ2NjkxNzAwMA", + "digest": "sha256:f315695ae35c0c52a60f58322289bb4bf8fc2387593b3c15371986e43a16a036" + }, + { + "id": "3cc6d02a-e064-5613-91c8-f49236bbf0a0", + "version": "MTc1NjMyNzc0OTkwMDM1MjAwMA", + "digest": "sha256:658f0eff6808f227184a0ea32db77a07d2c3a7211e75fffdec2d698123c646c6" + }, + { + "id": "3cde4033-b8bb-5fa2-9536-56258acce2c7", + "version": "MTc1NjMyODE4MDQ0MTcxNjAwMA", + "digest": "sha256:525332e993e53ab2e64f973cc4d6fd49a4424d832e723383a53df3e643a87ec9" + }, + { + "id": "3d26ffbe-390c-56bc-956f-ca44fcacac16", + "version": "MTc1NjMyNzg3NjU1MTA5NDAwMA", + "digest": "sha256:84ebdef96b012679b5e5716515e772fa16a00e92340321925a8eeafac17b4ec4" + }, + { + "id": "3d275b24-36ca-5015-a753-f20736b5edce", + "version": "MTc1NjMyODE2OTA0NzA5MzAwMA", + "digest": "sha256:d164896d35cfd0f7b3978a0a4f87e07ae8b51563442875aa9d66f3490ef39e54" + }, + { + "id": "3d31e817-113e-5941-a445-6746e31e6cf0", + "version": "MTc1NjMyNzc3MTUxNTE2MDAwMA", + "digest": "sha256:2a555cb04a2b9b0a090dafafcdc090f28f163933a752944445dfa8f18d773a62" + }, + { + "id": "3d497890-e7bd-5231-9421-df3fedf3b368", + "version": "MTc1NjMyNzgyMjg2NDQxNzAwMA", + "digest": "sha256:5b709570e6f8d298ce50e644e50aa0ac9fda7df9af9cfe5d411ef1eaade9e975" + }, + { + "id": "3d5c083d-0f03-5daf-8231-ca1f53bd6976", + "version": "MTc1NjMyODE2NDUyOTU3OTAwMA", + "digest": "sha256:b7f8287b2fd2a65c8f62d0c8819d0761e3ed4948aad0286fa27bc4df2d974797" + }, + { + "id": "3d651dd1-387b-56d5-a28b-d43eef59533b", + "version": "MTc1NjMyNzg3MDM3ODExMzAwMA", + "digest": "sha256:e51aabcc2cfdc829304e9ab5b968624368daee35693c3692672a685984b34d41" + }, + { + "id": "3d6bf8c8-19ab-531d-b912-c50d519ac0ba", + "version": "MTc1NjMyODE3NzU1OTE3MjAwMA", + "digest": "sha256:0f9dca5938a4196b28c9a620a664c86fd56693d0e99790d107a5b29f6cf7338c" + }, + { + "id": "3d909f2e-2192-54d9-97bb-2b45ec0bc65b", + "version": "MTc1NjMyNzg2MDI1OTI5NzAwMA", + "digest": "sha256:3befc63b5dafd01401beece145c16cfaf7f7c818b200769f71003695f66fa549" + }, + { + "id": "3d9e64ee-f574-57af-b70c-ed07ff52f0f5", + "version": "MTc1NjMyNzc4MDg3MTEzNDAwMA", + "digest": "sha256:f8d4b14d879a6932cdba1de209b30e4de254186fdd1035ea3fb6db5e71f6e679" + }, + { + "id": "3dd0c156-0491-5e20-a0db-8c39c0968786", + "version": "MTc1NjMyODE2MzM1MzMyNjAwMA", + "digest": "sha256:34b6d40d1af0ac532f3985c50984b2231e2ba3256ed7c03f6422f08bafefce03" + }, + { + "id": "3de2595a-6fb1-51d7-b8e2-c4c95f3741d9", + "version": "MTc1NjMyODE2MzA5Mjg4MTAwMA", + "digest": "sha256:9c6709279f3326acf094ec98859b05cdee2d9cd675c78412cd8fb517e2d6e39c" + }, + { + "id": "3e093b21-f61b-5626-845f-af5184a67a26", + "version": "MTc1NjMyODE2NTg5MDY2OTAwMA", + "digest": "sha256:ded13384aefd3c5595c8ee0fe189dfc832a3031ede749bb015961287d8a4fdff" + }, + { + "id": "3e0ccfd6-e126-5d2d-82e0-c8d35a595e92", + "version": "MTc1NjMyODE3NjEyNzk5MzAwMA", + "digest": "sha256:98bfef0c9d23f11ce473a268080d42570ee3e7880469dc70de177ad6be1c5635" + }, + { + "id": "3e1f58fe-66f8-5b8b-99ac-a97fb8dc0f34", + "version": "MTc1NjMyNzc4Mjg5MjcwMDAwMA", + "digest": "sha256:76f9a4fa0cd11efe21ec691048c0043bcfe311493297bbe9cb7992e0dcba1f3e" + }, + { + "id": "3e22edf5-fbde-51bc-bd2a-e8c38f1ca85c", + "version": "MTc1NjMyNzgxMjY4NzEzNjAwMA", + "digest": "sha256:8fa9766caf8a98ba9bb415c93a266dd31c58b60e0b7cea7bcb5ad0f2c04cc846" + }, + { + "id": "3e24be8c-6e87-5873-86e4-c5f9868f0913", + "version": "MTc1NjMyODE3NDM3NDU2NjAwMA", + "digest": "sha256:ec404cf29d6befa92c5320f77a69b35045a2eecd52bdb7d3a7ed2af84f0efefd" + }, + { + "id": "3e33ace8-5213-5d70-9c16-c2793e0b0878", + "version": "MTc1NjMyODE3NzU3ODg3ODAwMA", + "digest": "sha256:be173427c0fa501fe154269f5c0ceb6df5b9c400bcc1d6de50ced4bd21e6ac12" + }, + { + "id": "3e40fafb-3746-5e85-91a6-1e46eca7dadf", + "version": "MTc1NjMyNzg3OTk2OTk2ODAwMA", + "digest": "sha256:1c40e0fefa760c5bcc185b972348962d2dc16a46ae9aa763cb73c6fae92e867e" + }, + { + "id": "3e46f244-a8bb-57e0-b282-6e34e3edc717", + "version": "MTc1NjMyNzc4NzAyMDM0NDAwMA", + "digest": "sha256:bb044588e7b4e36dbce7548845420dba308fc9e8b551634840d03e33df4992e0" + }, + { + "id": "3e5af752-9588-5de6-a8e3-d23f92ed33d1", + "version": "MTc1NjMyNzc3MDM5NTYyODAwMA", + "digest": "sha256:1797f72a40e622e715293e726bf11dc170025ef057d27caee037ff35d038d4fd" + }, + { + "id": "3e689952-2709-5eda-8c92-52642efaaaa0", + "version": "MTc1NjMyODE2Njk1NTYzNDAwMA", + "digest": "sha256:9335487b8ba69afd9c41618f01bf0582e0195995f0587ff4f1c786c7de556d31" + }, + { + "id": "3e6a98b3-a29e-5239-8137-b1243d8a59b8", + "version": "MTc1NjMyNzg1MjY3NzI5MjAwMA", + "digest": "sha256:6deba693f19497e9fc12253d89d65b7025536d6fd00a12830177b3f452dd9173" + }, + { + "id": "3ea3212a-e7d9-5efb-97ec-4b16fe65e840", + "version": "MTc1NjMyNzg3MTU2OTQ3NTAwMA", + "digest": "sha256:d38e181aa78b3d069bde7dd47e90fe79e5c7554880d34cae7a8a2ca6ae197d43" + }, + { + "id": "3ea7a43a-049a-5ea3-bb35-f7aab339a897", + "version": "MTc1NjMyNzg1Mjg3MDc4NjAwMA", + "digest": "sha256:8b30584ab2efdaf07e30c5c768b140388f37a2dd33167919f9edaab1842408a3" + }, + { + "id": "3ec0ac18-3f26-54b2-bbe0-8f1cea995294", + "version": "MTc1NjMyODA3NzIxMDM5MzAwMA", + "digest": "sha256:61704b295fe467512226468df82a8cb3e0d397a6c95d9f4dc70705911e6d1c61" + }, + { + "id": "3ecaf531-c260-52f7-a5d5-ef8b57ead1cf", + "version": "MTc1NjMyNzg0Nzg0MDI0NjAwMA", + "digest": "sha256:34b6fb21af4a3469648291929865f76837b1e5fe24b4caae9e6bf11fae51b290" + }, + { + "id": "3ecd6b2b-9f66-5de3-824f-73069880e11d", + "version": "MTc1NjMyNzg2MDMzNjAzNzAwMA", + "digest": "sha256:eda05a9d7a7791b70ecd68af3b3df708abccf16143ef5740a3d28119c87d509d" + }, + { + "id": "3ee73873-20cf-54d4-b378-133f69b2ee07", + "version": "MTc1NjMyODE2MzA2MzQzNjAwMA", + "digest": "sha256:98b3c40ec439bee80e5725829f31a5ebac58e69b17134193c3d2a428d14bd771" + }, + { + "id": "3ef492be-5788-5d01-b592-ceb9ad162736", + "version": "MTc1NjMyNzc1NzUzNzIxNzAwMA", + "digest": "sha256:f1af4157a726a84c6723060707da50cf3ff9b3cda6667d12f8e2baa911c2adf3" + }, + { + "id": "3ef80599-3b58-5835-94ec-1002b40ff1d7", + "version": "MTc1NjMyNzczODYxMDA2NzAwMA", + "digest": "sha256:9d3e935182343a7913b2b97c4357c26384ad159daf602f1beeab6ebc5f22caef" + }, + { + "id": "3f0af973-a8ca-572e-bb1d-7d00f13bf57f", + "version": "MTc1NjMyNzc2NjI3NTI0NTAwMA", + "digest": "sha256:112e9c6096c153b0bf0736f489b8295d57ba4b80d6e9ead07f9046fc154fa792" + }, + { + "id": "3f13a5a5-daa5-502c-a33d-459fce2d521c", + "version": "MTc1NjMyNzg3NTQxMDI2OTAwMA", + "digest": "sha256:8b9f213ac43b2dbe0e0cfc50a71b9a46ffb55f8b680b04801a956d6ef59e4b9f" + }, + { + "id": "3f270ea9-c823-554e-8e17-b31b83cdb41b", + "version": "MTc1NjMyNzc5MDI1NzE5MTAwMA", + "digest": "sha256:20d950a07d89f7040a2c28d5ae3fdc433c52bbe1e08cc05e5cc71325500ce91a" + }, + { + "id": "3f35d486-7229-5897-9742-177b767108c8", + "version": "MTc1NjMyNzc3MTQ0NzcyMTAwMA", + "digest": "sha256:90ac5d88ebdde02613d20b60dd7cdc2718f1b346273e93f3500f1244952fc5a5" + }, + { + "id": "3f4a05cc-f0bf-588f-8ec9-682c065f3316", + "version": "MTc1NjMyODA3MjU3MTAwNDAwMA", + "digest": "sha256:07ee80841b8ccf346b93b972e77ddbd3a5674bf62882172f2b27735621a40975" + }, + { + "id": "3f6c73ee-a41f-54ca-8d31-6497e48ad233", + "version": "MTc1NjMyNzc0MjUxOTk5MzAwMA", + "digest": "sha256:5260f1b06f97ac07141515ce9a41e0bbd9e9ae5a9cf1fa76381ade154e62ebcc" + }, + { + "id": "3f86453d-1aaa-57cb-9a3b-be332da01cec", + "version": "MTc1NjMyODE3Nzk5ODA1ODAwMA", + "digest": "sha256:559308f0e12dbbf81e74be0cc04832fdb1cd0a74ea9192d310642b0c331dcfe7" + }, + { + "id": "3fa0c547-af8c-5724-9242-e50024614e3e", + "version": "MTc1NjMyNzgxODkyNTgxNzAwMA", + "digest": "sha256:e2a2cb6cf381bac386fa4cd55a57f131c0ec384c750b905553ee1202a997be2e" + }, + { + "id": "3fa62986-4d6c-54e0-89f0-1ad476956275", + "version": "MTc1NjMyODAyMzUwNjI5MDAwMA", + "digest": "sha256:95f5313c68cf561fec9249fe1f0d271c387859b7d3dc585c3b1d788c46b6161c" + }, + { + "id": "3fab83f1-8008-5d48-bd71-04a3a3381c1e", + "version": "MTc1NjMyODE2ODk3NjY0MDAwMA", + "digest": "sha256:bce99cf196bd355d202097d53330542449dd7b3155200c74a0c47e09f217363a" + }, + { + "id": "3fca216b-46ef-50f6-9276-8ea0c2236233", + "version": "MTc1NjMyNzc5NDY4OTk3MDAwMA", + "digest": "sha256:a345473f6e8834b9e65682157ed32546323c93af024dfb99341fe908d57f8921" + }, + { + "id": "3fd9de8f-8f57-5a68-a928-9db7fdb2cf2d", + "version": "MTc1NjMyNzg2OTM1OTYyNzAwMA", + "digest": "sha256:46e7efd584c57b3b62a8a87893ce712300e9d93a19c796891abc63f2eb68dcac" + }, + { + "id": "4006edc4-bf81-51d3-8a52-abb7daf97ee5", + "version": "MTc1NjMyNzgxMjc0NDU4NjAwMA", + "digest": "sha256:8aa473117d983d8129984b7a7bb3e3440e2ccecbf6c17887b8954948468513ac" + }, + { + "id": "401f5514-b018-57cd-8262-5bde2f48792d", + "version": "MTc1NjMyNzc2NjMwMzY5NDAwMA", + "digest": "sha256:6b169e484f73992fd44ee12245637574ae501220ae61da12a7460aa25c5d9081" + }, + { + "id": "4023a646-1832-5bd1-991a-f404f576357d", + "version": "MTc1NjMyNzg1Mjg3MzYyMzAwMA", + "digest": "sha256:52246c4723b9136d16f3f729ca93957a74ef5af079e4544699beab224d557276" + }, + { + "id": "40342e73-e1c3-5c70-a399-fa61300692a0", + "version": "MTc1NjMyNzc3MDM2NjE3MDAwMA", + "digest": "sha256:997abe945aee5cf6cfc0e72d88503e7387e1976a0c6ff852cea99f1ea81825a3" + }, + { + "id": "403fd544-cf9c-5ea1-9abb-4f856e2479a7", + "version": "MTc1NjMyNzg0Nzc0MjY5ODAwMA", + "digest": "sha256:c07c2e40452246fcc1ab165155653b3caa6d9006c9d10f248794fe6c25eab6c4" + }, + { + "id": "4042b87b-dede-52c4-9f43-31fb442e3ab4", + "version": "MTc1NjMyNzc3MDY0OTc0NzAwMA", + "digest": "sha256:05103b9e74eec4d6d288e05d2ba2b174b3ed8eb7f5d971039867a99b8693ef2b" + }, + { + "id": "4045ed2f-0e19-5687-9b06-3c1cd676d93f", + "version": "MTc1NjMyODE3NzY1Njc1NTAwMA", + "digest": "sha256:282f47c82411ec66590fb037c165de5a0e2dd800d5ae45173967c778fc535430" + }, + { + "id": "40504f3b-8bd2-5967-ac71-09e982d50b3c", + "version": "MTc1NjMyNzc4Mjg1OTAzMDAwMA", + "digest": "sha256:9e72477ae6b6b2d8ec50509527e1088e06c9709508c5469346c951f29e7608ea" + }, + { + "id": "4058036f-b661-5faa-b068-173d61a4e24f", + "version": "MTc1NjMyNzg2NjI0NzQ5MTAwMA", + "digest": "sha256:90ea70b3fbc050cd034c012a6b3032651bee9a8d26eaeeff11d2db635ebc7f31" + }, + { + "id": "405dae9f-e76b-563f-baa1-9b224b7d03e8", + "version": "MTc1NjMyNzczODM5NjE0ODAwMA", + "digest": "sha256:dd29ea9d965df426eea256082087f4dcf54f125edde0d3c801581cc9f1979152" + }, + { + "id": "4078b9d9-c238-5d8b-9a49-893d79cbf21e", + "version": "MTc1NjMyODE3NzY1MDc5NDAwMA", + "digest": "sha256:69647d4656e9d85e2ec79f95b08ce99b9a3c311bfa500f6da80aeba9c38dd587" + }, + { + "id": "40b98942-579a-5860-b2c9-9a8db9687ac0", + "version": "MTc1NjMyODE2Njk1NzU3MTAwMA", + "digest": "sha256:baf18a53aecd2c6075666aa8290fce9b72f4d6d50c7c84ebfe6ff606d4188293" + }, + { + "id": "40d71245-851b-5061-99dc-d37180deb725", + "version": "MTc1NjMyODA3MDcxNDk3NjAwMA", + "digest": "sha256:28b5976ba49a895762a703025dd49d75a5be9246eb5549d351dfdaff18d3a1bc" + }, + { + "id": "40df485e-9213-54a4-9061-c5ceb3e6add1", + "version": "MTc1NjMyNzg2NzYzMjQ3NjAwMA", + "digest": "sha256:cd85e72964d35080a4dae7461bb77f584d891a71faf16a7fac41aa6639371692" + }, + { + "id": "4101e69d-fdac-5e17-b107-dc32262b553e", + "version": "MTc1NjMyNzg3NDU1MTgzNDAwMA", + "digest": "sha256:30214b872021eb0fe7673e82fb29f7a9df37d0e10c02dd6a6872e1fe6fe6a08a" + }, + { + "id": "4102adde-0bcc-5c3f-ac7e-39a1473ec1d0", + "version": "MTc1NjMyNzc3NTE5Mzg2ODAwMA", + "digest": "sha256:cbceaafed28ff10968a4e5fea000fee54d84d97e10f350dae88ad95bc40cd55f" + }, + { + "id": "41066dd7-b9be-53d0-a0fb-f53e562ae6e1", + "version": "MTc1NjMyNzc4NzA0MTQwOTAwMA", + "digest": "sha256:bfbe67e001e0b0c9251dbe5d69406edac29220e55a779757906abe672b8ce544" + }, + { + "id": "413ea9a6-4b17-52cc-bb9d-3e052c558c38", + "version": "MTc1NjMyNzc0MjM3MjA1MDAwMA", + "digest": "sha256:a7e452a119802f95ab184633f788e2c8b66d7a80e4935e17910530a552fd685b" + }, + { + "id": "4150285f-aa43-5500-856a-7ae3e1f718d4", + "version": "MTc1NjMyODE2OTA1MDEyNjAwMA", + "digest": "sha256:452ae3fa715dc82d7ebadb459d73e0b6888cfc6a536748dce22bd1299c7e7ce1" + }, + { + "id": "4161ff06-38a1-51ad-9977-b369450a5dfb", + "version": "MTc1NjMyODE2Mjk5Mzk0ODAwMA", + "digest": "sha256:25e07982c8a5e04506933a17be6214c9cd9a5276eab6ae87fbc1ae618fe0a420" + }, + { + "id": "41746620-61db-55e6-a817-616a3c7311ac", + "version": "MTc1NjMyNzgxNjM5NDgzNTAwMA", + "digest": "sha256:67118f6c8f5aea7102141e7c38bf91e34256bf26b6c20b136e690e83fabd3599" + }, + { + "id": "417f6d31-3bd9-5074-b57d-5038f62b438f", + "version": "MTc1NjMyNzc3MDU3MjQzNDAwMA", + "digest": "sha256:387d70b83471290a7903f9573b4a9211ba6653ce6f16bd6fc57eadeade199607" + }, + { + "id": "418bf5af-3452-5741-b607-689f677dbcfd", + "version": "MTc1NjMyODE3NDQxMDc5MzAwMA", + "digest": "sha256:dd2efad7b7a97303e4df59de081903b10435cdcce3b22a9fe820e075f7c33bc9" + }, + { + "id": "419877cc-e5f6-554c-94ed-a5935010513a", + "version": "MTc1NjMyNzg0MzA1OTMxNjAwMA", + "digest": "sha256:bdd35956bab872f9734c48d914f965c464389075d3c87dea69d94c2b615efd81" + }, + { + "id": "419ac3f9-c01f-5afd-9666-a1e5a4efbb81", + "version": "MTc1NjMyODE2MzQwNDI0MzAwMA", + "digest": "sha256:883b32fca2697e83045cc6f3821a3f96f34a3c4cea386161808063723e07b27a" + }, + { + "id": "41a0c907-6e77-562c-9fda-a8d01050d20a", + "version": "MTc1NjMyODE2Njk1NTU1OTAwMA", + "digest": "sha256:4e6c6f30c369a518f4c811db9d261a8db70b0c5280c51f080b5d85fef376a607" + }, + { + "id": "41b4a95c-a6b4-55f7-abfb-9fe0292d1143", + "version": "MTc1NjMyNzc3MjUwNDI0MTAwMA", + "digest": "sha256:9e1db810aca65d76871817b817855b93811fd5d1f90b7b85b284fcee32ab8134" + }, + { + "id": "41b753bb-ed79-5126-ab78-60b6232c835d", + "version": "MTc1NjMyNzc4NzQxNTk3NjAwMA", + "digest": "sha256:7bdccc1dad12dfb1b4f3a5c47bf07e69414605ef60ad2e908839626ab623b4f3" + }, + { + "id": "41e76eff-b0ad-5f73-b2a2-f8bd22896d98", + "version": "MTc1NjMyODE2NDU1MDU2OTAwMA", + "digest": "sha256:1f664c55bca8efd2e091f47842ac2bfd925d7ee4105f022c7336d35487225165" + }, + { + "id": "41f27d02-c3f0-5e8f-bcff-a8aaaf7a8d73", + "version": "MTc1NjMyNzczODM5ODU3NDAwMA", + "digest": "sha256:c704e0d58466dd8bfc63cb7a3761ff3492573318fe24e475df5479d8605c2f73" + }, + { + "id": "41fbf36e-3dfa-52e3-bc4a-d6c4209fb35a", + "version": "MTc1NjMyNzg1MzExNjg2ODAwMA", + "digest": "sha256:52be6610cebaa2d1eebbf4f0397dd36f27752cdf85a16b315fac39ffcd3bf01a" + }, + { + "id": "4217527e-4c25-55fc-be62-53ffa6ade159", + "version": "MTc1NjMyNzg3NTUwNDA2NjAwMA", + "digest": "sha256:e04b6938fc2f656f6709c67f8898c19fd57abee34cae31096190dacd22b45fc7" + }, + { + "id": "4219d5b1-a929-5261-b94a-3ba2e819fada", + "version": "MTc1NjMyNzc1Mzc1MDY0NDAwMA", + "digest": "sha256:747c23cbf28c29dc96a84c4335025906dec478ca9fa2c465ca42049546af704e" + }, + { + "id": "421c35c2-d771-5331-bd56-558d32625ccc", + "version": "MTc1NjMyNzgxMTk1MzA4NjAwMA", + "digest": "sha256:41e2fdb56f51d6c2eb0929d9db31c25997e1c1d19744cc90aeeb460f97dc231e" + }, + { + "id": "421de115-4dba-56cc-ae32-80c250e177d6", + "version": "MTc1NjMyNzc3MDM0MjI0MzAwMA", + "digest": "sha256:789f3ddcecafdaa4fff7baaa5235ea882cc0132c9471661f14561e225b57b003" + }, + { + "id": "421fbbc8-1749-58a4-8a4b-3b4de08738cd", + "version": "MTc1NjMyNzc4MTAzOTg4MDAwMA", + "digest": "sha256:09fb5a725c2b300dd4619948c55aa66197792b4c668ecbccb2a99696bae307d5" + }, + { + "id": "4226f740-b397-51f8-b617-7aa9d8ce792e", + "version": "MTc1NjMyNzc1Mzk2NTc5OTAwMA", + "digest": "sha256:06b93ea1e560581d7a6c22475f726e62395a974b265e254ce126d5ace69dd47d" + }, + { + "id": "4230faaf-7710-5065-9d44-347845587dd9", + "version": "MTc1NjMyNzg2MDMzNjYzNTAwMA", + "digest": "sha256:385386dbc5ea65fe2a8eb3bcaabb72960c6de67a21164f31d719a1cb439fd711" + }, + { + "id": "42511905-84d5-539b-9f4d-422ec1699cf8", + "version": "MTc1NjMyODE3MTEwNDkwOTAwMA", + "digest": "sha256:6a54b674fedaf64d33d97863abeeb1128f7f3c9454191eeac0743d45382e66bf" + }, + { + "id": "425e73e0-d27c-59cc-bf99-b69f2ceaeb1e", + "version": "MTc1NjMyNzg3MDQyMTMwMDAwMA", + "digest": "sha256:50d4439c4308a23a12c124f3433f5ef02410ddce7ee00d9dc344094812b9ea54" + }, + { + "id": "42ad605a-e892-555f-b496-23c98580b640", + "version": "MTc1NjMyNzc0OTkxMjA3NzAwMA", + "digest": "sha256:b0594d684b29c75dc55447eca6822c9ea971f9d005f5d1bf1962bb4f6f753581" + }, + { + "id": "42bdf7f5-6ba1-5524-a855-91bf8c30bfd1", + "version": "MTc1NjMyNzgxMjA1NTA5MjAwMA", + "digest": "sha256:d1e5c3a8a04cea3696d741e78d090337935ce7a78e881bb5ceb4b6971d118921" + }, + { + "id": "42cd984a-2a2b-55db-835e-87b3cb41ab6b", + "version": "MTc1NjMyNzg1MjcyNDQ0MjAwMA", + "digest": "sha256:4f4b0b551433d8dfea3542f659c71959d8d212912aabdcba6061a5f412d59a85" + }, + { + "id": "42dee378-a50e-5108-bab3-552a6f4f2d79", + "version": "MTc1NjMyODE1ODIxMDEzMjAwMA", + "digest": "sha256:fd50a29d98bc899135b37113a8624a22b2ed3cda7d70f30006c76954a8a11240" + }, + { + "id": "430978af-0de3-5ef5-931c-237ab6655da2", + "version": "MTc1NjMyODA3NTY3MDczNDAwMA", + "digest": "sha256:663ff88fbc5315036e989f964a534919e758c69114f1f91c3b958904bae70d3e" + }, + { + "id": "4316aaf7-83a4-5571-964e-d7a7af4eb5c2", + "version": "MTc1NjMyODA3NjIyNjI1OTAwMA", + "digest": "sha256:f24268913a0a76eacc77333927a58fd0dbbe0101bf67f05524429a736400c25b" + }, + { + "id": "431a2923-b5e5-5a96-a53f-6b43c96f531b", + "version": "MTc1NjMyNzc5MDI3Mzk4ODAwMA", + "digest": "sha256:38bff84183da1afc0678bec20d8a44092676376c8ed42b8de591a3bcd2070ed0" + }, + { + "id": "433996dd-d922-59f9-8a31-838942cd9a5b", + "version": "MTc1NjMyNzc5NjE3NTA1OTAwMA", + "digest": "sha256:fe023f7309c4888e486ea80a5d25a186969107f5588141537b0b02f8ed621c1f" + }, + { + "id": "436ecbf5-83f0-5801-a653-b0b936422982", + "version": "MTc1NjMyNzc2Mzc4NTczNDAwMA", + "digest": "sha256:d7e7059dffb410abe596ea63328f233ecf2d3b3d8b795f4b64b29ccc27da06d8" + }, + { + "id": "43706ca2-07e8-5d6a-90ef-5b1ee2d6b648", + "version": "MTc1NjMyODE3NjE3ODk4NTAwMA", + "digest": "sha256:01a5f99f662071de778bdda7b2b76955301f688597cfcc9fc6666977d2dc86f0" + }, + { + "id": "43755ef3-27ae-59ff-a191-45d794515f55", + "version": "MTc1NjMyNzc1Mzc1NjE1ODAwMA", + "digest": "sha256:095cd9330e9d15bb115f013060b424c77c60b9745f94d5e5fbf215295df59750" + }, + { + "id": "437f4caa-26e7-5a44-b590-55a90dea69e7", + "version": "MTc1NjMyNzc4MTk2MTA1ODAwMA", + "digest": "sha256:5d24059212b821a5e6efdfd7a69d537dad8397f3cfba6d466f403c7dbac01eea" + }, + { + "id": "43aa44aa-7ee4-5d47-b62e-a77c290437a0", + "version": "MTc1NjMyNzc4NjkzMTcxNTAwMA", + "digest": "sha256:4d084ca81672bf5132630b3f322c6aba0ba65e0db06b37438b33d0dcd60f6561" + }, + { + "id": "43bafa7a-9220-52e4-915a-2c8b4506ea2f", + "version": "MTc1NjMyNzg3NjU0MDc0MTAwMA", + "digest": "sha256:cf028ea7dff848e13b8513801b0185e4016de551bdcc9d9ffe6182871ee96123" + }, + { + "id": "43c7aa3e-0dcf-5944-a28f-48fee1766643", + "version": "MTc1NjMyNzc2MjM3OTM0MTAwMA", + "digest": "sha256:d53197f926312749a02c1b5d30152b0a3ecf82d09190c1642a523e7e98845d4c" + }, + { + "id": "43cbecf3-3ed4-5a6a-9df2-1f699632ddd9", + "version": "MTc1NjMyNzg3MTYwOTM4ODAwMA", + "digest": "sha256:3494d5428762e25be9f1dbda4c6d2326f25eb8d44ad2d055c14fe0c5ac56cbc4" + }, + { + "id": "43cfe3f2-66d7-5e72-ada1-0a285690fba0", + "version": "MTc1NjMyNzgwMjkyMzI2NTAwMA", + "digest": "sha256:190708499590a15a6cb0e0d972d230030cb7ca4a1e1db1dd2ecb098a860b976a" + }, + { + "id": "442614d9-b897-54cc-998e-7e578de63610", + "version": "MTc1NjMyNzg2MDI4MTcwMDAwMA", + "digest": "sha256:81bdc37113599f61e1a2fc30b7ae3827e03e81165f55a0214c0b4ad443afd7a1" + }, + { + "id": "443d76b9-5e3a-5a8b-b6eb-c14ba1431c4d", + "version": "MTc1NjMyNzgwMzYwOTA1ODAwMA", + "digest": "sha256:6989ab7c26e33cf73d492f07836eeda35038a9e78e60798d59fe8d14456527aa" + }, + { + "id": "443e4604-4821-58ae-9e16-9e0eb1402420", + "version": "MTc1NjMyODE3NjE1NjQ0NDAwMA", + "digest": "sha256:88e833bc72ccc7cb0a760eec4425a716ef5550f4cf62d2a8732f4b92b2d38cde" + }, + { + "id": "445ef373-8ca2-540f-b054-eda8b8a3bc84", + "version": "MTc1NjMyNzgwNDUyNTgwMDAwMA", + "digest": "sha256:f82595d65b38bba0c31f307fa39fe1cd6900b6c720b8da426e5b34fd2c707b72" + }, + { + "id": "4470bce9-d5f7-5546-83a4-1e94f5dacec8", + "version": "MTc1NjMyODE3Njg2NzgxNDAwMA", + "digest": "sha256:b12e176c76c26e3b8e023adcea627ee185c73f6e590575cb4c3c918385cb457c" + }, + { + "id": "4471869d-2310-5913-95ee-0badc1ebad0e", + "version": "MTc1NjMyNzgyNjU3MTQzMTAwMA", + "digest": "sha256:06530852b43ff1a11cade5ce3d8d354d41ae556908eda6a37a0e07ba649e82d5" + }, + { + "id": "44749bb8-e6a3-5297-9ac7-aa495b1f9a43", + "version": "MTc1NjMyNzc0MjUyNzQ3NzAwMA", + "digest": "sha256:6cd289f8c030a641ac87b7258557f47535438f8ba615dec6ff05d0db3c81d459" + }, + { + "id": "44a2f31b-baaf-568a-8278-4899378d1afb", + "version": "MTc1NjMyNzc2NzcyMjM3MzAwMA", + "digest": "sha256:e84e7c367ea8a0ef2f3e104a4dc61fd3bc15e14331d59ef63fee818c59f1785b" + }, + { + "id": "44ca7b10-f33f-5e8b-b220-3458a03381bb", + "version": "MTc1NjMyNzkxMzEzNzcwNDAwMA", + "digest": "sha256:ed4c7733c7194299f57e8b7b729d5e112a51b9eb072a530e531e494e1be66d21" + }, + { + "id": "44d636e7-4e49-5b87-b4a3-1ed26cde3130", + "version": "MTc1NjMyODE2NDkzODEyMzAwMA", + "digest": "sha256:997f779a2f569b13b143b4b11e9b2c49a0c29559c678b389ff15e189e4277832" + }, + { + "id": "451ee17f-6a90-5042-a98a-53ab0a7784db", + "version": "MTc1NjMyNzc2Mzc2MTY0MDAwMA", + "digest": "sha256:0d6bf289464c690fb813deb95863dfa3f8d1182a958b1e9a063941d58f70f14c" + }, + { + "id": "4536887e-5ecf-5ba9-a085-8b99105f3de4", + "version": "MTc1NjMyODE4MTgzNTI2MjAwMA", + "digest": "sha256:bbf63d7d60937ffd8a1b88522038451deb79b16a6c0b33af4f0758f2a068c492" + }, + { + "id": "453c8930-17fd-584c-b997-e0fa93108657", + "version": "MTc1NjMyODE3ODA1ODQ0MDAwMA", + "digest": "sha256:e43e230304806a33f26e8b57cf552edece2a9e0078fabbac4664af48386d1287" + }, + { + "id": "45551d29-a013-5984-95c6-9f6eb7eb2e73", + "version": "MTc1NjMyNzg3MjAwNTYxMjAwMA", + "digest": "sha256:5dadb810d975cf37f14f26b5a1a7a9778eec777338e0f6a2197faa31debd4c24" + }, + { + "id": "4563e027-b75b-5d5a-8eab-14c035780738", + "version": "MTc1NjMyNzgwMjkyMzY2NjAwMA", + "digest": "sha256:7e667ae506e97dbbb616d32da114cc9f42df42472e3a1d77dd6c0a15f23c9978" + }, + { + "id": "4565de43-f6a0-577c-a405-85dccb5aa273", + "version": "MTc1NjMyNzg2OTQ0OTEyNjAwMA", + "digest": "sha256:95d18353ae3f1adeda8a1259251cb5d58c8f30491906055f1d54f2c056fc3bae" + }, + { + "id": "458a5716-4950-5e27-a4c0-828be7e7e98e", + "version": "MTc1NjMyNzg3MjEwODQxOTAwMA", + "digest": "sha256:1b39e3348cd6a6da22650da17dc401051686ed278b34828544c075fd02b8b635" + }, + { + "id": "459c9150-f844-5cc2-8f08-90d7b9406914", + "version": "MTc1NjMyNzkyODgxNzU5NzAwMA", + "digest": "sha256:e1c9d689e3396ea80ba159b69e75483499492b42c2f998e5941ae1672ed3177e" + }, + { + "id": "45ca09bc-5c52-5e92-9b0a-2a12acd4cc37", + "version": "MTc1NjMyNzc4NzQ2NDcxNzAwMA", + "digest": "sha256:678d98f25fac18f321fb00a51a895020f0f4387a50bda4c8bf3f2682f8e9a3ed" + }, + { + "id": "45e43008-ff5a-50ca-bdaa-c00e30f75f5d", + "version": "MTc1NjMyNzgyMjQ0NjE4MDAwMA", + "digest": "sha256:ce12208179c097a09a84f1556ef4f9148882088abab48d36c1d2b7b22bea26b5" + }, + { + "id": "45e60456-0ea8-5d5d-8bd1-cb32fb9d77c5", + "version": "MTc1NjMyNzc3NDU0NzQxNTAwMA", + "digest": "sha256:82b0247de556cd810bafaab978034748b4216948f715e321ac205fcb395af2ed" + }, + { + "id": "45ea5ab8-0eb1-5028-b7c2-b6705eae67d8", + "version": "MTc1NjMyNzc5MDMyNDA3MzAwMA", + "digest": "sha256:13564f4c8c2f8a287d4dfa41dd93290679c759c4a5f4a990de5417c5e354703f" + }, + { + "id": "45f84609-fc05-5b61-a761-48bd075469c1", + "version": "MTc1NjMyODE2ODk4MzMyMDAwMA", + "digest": "sha256:7848f02b3431259a4a3d33199e4554541290e9a22ca181c0c35a7a65147c18a0" + }, + { + "id": "4623b9aa-8afa-5c48-b3df-65e8611d3154", + "version": "MTc1NjMyNzg3OTk1MDQ3MDAwMA", + "digest": "sha256:21741b8b543a04cc7335dbbbc92f9f343bf08211d9e05d819ab8ec0f959f0296" + }, + { + "id": "464f57e2-289d-56cb-87b1-7c97ba5f0812", + "version": "MTc1NjMyNzc2Nzc0MjkxNjAwMA", + "digest": "sha256:b209bd8ac42f8cd3ce60d391307c9718395a50ca07b6527e8b804ccecc940d65" + }, + { + "id": "4652091d-51f6-54f8-a70d-a6a174d0a535", + "version": "MTc1NjMyNzgyMjkyMTg5ODAwMA", + "digest": "sha256:6b009dd99653d9b48ed3205b52fc2ad34a271f284a0566be4f904163cce5538c" + }, + { + "id": "4659e10d-9971-5d63-ace2-b02de0772085", + "version": "MTc1NjMyNzc3MDU4ODc0OTAwMA", + "digest": "sha256:b4bbd28c675a1327828986da07ff9f205754ca4ea167dc4dd7a499fe10975554" + }, + { + "id": "466f49aa-e2c6-567d-925b-58332d98b326", + "version": "MTc1NjMyNzc2Mzc5MjIyOTAwMA", + "digest": "sha256:1aedcb287d2adf8a149647939a349e69814dd860ebfffba7e89bf20f57521a88" + }, + { + "id": "4692788b-a65b-5aeb-bdc3-fc40453c36df", + "version": "MTc1NjMyNzc0NTczNzYwMDAwMA", + "digest": "sha256:2e8c8558e46e47aa5ee8e023a6a204744d0177f77689265260acc4682c0f4ac6" + }, + { + "id": "46b158ee-823a-5ea1-b8d9-54836689c898", + "version": "MTc1NjMyODE3NjI1MTQwMTAwMA", + "digest": "sha256:66bcfe29e892030cf93be03abebcf098b76593f30fe1c7f129d87ed4a1725844" + }, + { + "id": "46ba8da7-6cdc-59bf-8e90-1c32b85111ad", + "version": "MTc1NjMyNzg3Njc3NTM1MDAwMA", + "digest": "sha256:7c01c32209b5c400e8f9cdc027a480815f4fe87b50c2f146c184862cacfd40ed" + }, + { + "id": "46c03ee1-4a3f-55bf-8038-ff6216ea3762", + "version": "MTc1NjMyNzc1Mzk1Mzg1MDAwMA", + "digest": "sha256:22e5536c83188ff3a48a9a9216dc27ceef796c1ef175f2de26671d12ae1efdaf" + }, + { + "id": "46cfad6c-6edb-5e5c-8593-492ce2bf250d", + "version": "MTc1NjMyNzg3Njc4MjQ1MTAwMA", + "digest": "sha256:f2806d14284345ca2a16efc25d1889f67c6acf5bd1aa53506ac997db97291bcb" + }, + { + "id": "46d3f05a-55b9-58e9-975e-ce41d1353003", + "version": "MTc1NjMyODE1NjQxMTE1OTAwMA", + "digest": "sha256:096dd1d101ff3769c1c31586b7aea5d8ab4a7aca2d04129430c4830ca68be0bd" + }, + { + "id": "46edb9e1-e76f-5886-889b-35903a50ef0f", + "version": "MTc1NjMyODE2NzAwMTc3ODAwMA", + "digest": "sha256:ea2de8295af954a64f9ccd132d736c9bc53002f6cba4451fcaff28c801cab4e2" + }, + { + "id": "471f13da-7ade-5f53-ab77-cdeb23dd159d", + "version": "MTc1NjMyNzgyNjMwMTk2OTAwMA", + "digest": "sha256:5adbf994a987a733481d4d7cbd482ebbde83f7d9f4ff3ae4e1a02028bbed133d" + }, + { + "id": "473f82a1-e508-5f42-9a58-428317b1543d", + "version": "MTc1NjMyODE2OTAxMTA5MDAwMA", + "digest": "sha256:c03a3822dc0d9fb879a6e111062f31b16cfa8a37e7bd7ed5b73220dda11d771c" + }, + { + "id": "4745fb43-f66a-50ae-80be-7df384c2b32e", + "version": "MTc1NjMyNzg3NTUwOTAwMzAwMA", + "digest": "sha256:c4bc1cda661bcab75b583fc244a91170c7478529c54b431db98e912494497c4c" + }, + { + "id": "474bb4c0-3a9d-5a07-a7db-06b5a225ad0f", + "version": "MTc1NjMyODE1ODE4MDYyMTAwMA", + "digest": "sha256:d5ad26f500c3333d26192e1a5a77badd4aff99798b25bfd1465dba3ec90915a2" + }, + { + "id": "475032c7-b5a8-5954-a018-3a53468b5e0a", + "version": "MTc1NjMyODE2NDkyNzAyNjAwMA", + "digest": "sha256:3e767c8255350435febb03516c1b0bfe1455e9bd2ce14ea618ff5c2b46587ba2" + }, + { + "id": "475c9ea1-14b4-5fe4-940d-0fd38c39f52a", + "version": "MTc1NjMyODE3NzYzMDI0OTAwMA", + "digest": "sha256:c16bc681ffc0084221b974102231da01bcb5fd4e88a5b8d813a46011eec428a9" + }, + { + "id": "475d2b8f-fb27-5347-aace-5db2e1b34b77", + "version": "MTc1NjMyNzgxNTY0NDkxMzAwMA", + "digest": "sha256:441b8e8d4aa42b29b7164461dbb4cbe25ea56faf4d5b9f1c979ba062e9b8e166" + }, + { + "id": "475e97d7-c854-58a3-ae29-138937e4e40b", + "version": "MTc1NjMyNzc3MTUwNDAyODAwMA", + "digest": "sha256:22c3844df1c730dd9ce22d5fab364eba9f6cd83c3b332018e6906099b93d3102" + }, + { + "id": "476cbce4-e222-5220-8a51-51f17c0a3c8d", + "version": "MTc1NjMyNzczODQwMTc2MzAwMA", + "digest": "sha256:aa9f1e32f130e3238622b04cda2e28992c7c24041718f522ae63cda9c252e422" + }, + { + "id": "47748d28-6ff4-5901-9a4a-899fe26c682b", + "version": "MTc1NjMyODE3NDMzOTgwMjAwMA", + "digest": "sha256:7959beeebf1d253736fe4889e77f77d6da9ae3cc1d6a66791461c32137a8a424" + }, + { + "id": "4796c35c-dde0-5d7f-b5ae-700cc3a9ca43", + "version": "MTc1NjMyNzc4MDg4OTYyNDAwMA", + "digest": "sha256:cbb56f938b66ed20dcd6d8ae233611ffd9b2c5128c09590b6fe0db62b9e93511" + }, + { + "id": "47adde88-d638-5107-98fd-2b961800455e", + "version": "MTc1NjMyNzc2NjQwMzA4OTAwMA", + "digest": "sha256:58581391c2da2fbcff614dc7ffd255228f6bb297fd5694df1f2ac97d03ba8390" + }, + { + "id": "47af2b4e-828e-5427-87ea-02d55d34eac0", + "version": "MTc1NjMyODA3MTYwMDcwNzAwMA", + "digest": "sha256:b8427a709838060d1693175391e30beb27d7c325b593843f91411a5144aa8a18" + }, + { + "id": "47bc82e8-c550-5899-bff0-458fd1c8050f", + "version": "MTc1NjMyNzgwMzAzNjUxOTAwMA", + "digest": "sha256:0109e65afc258ac2f1b85c614e228d2aa27d89f2b78b35b84e2f1192a221a157" + }, + { + "id": "47bf53bb-ddc9-5a02-90b7-cb5dab7a35c2", + "version": "MTc1NjMyNzc3MDU0NjMyMzAwMA", + "digest": "sha256:02ebe62c4aa528aa7143c072578b489dc9b8b92ad07b89461119cddbc5c08c4e" + }, + { + "id": "47c3434e-e54f-55f1-a7f2-2c7f3a819891", + "version": "MTc1NjMyNzg3MTY2NDgxNjAwMA", + "digest": "sha256:927b8a1e4493457a460ae9e455bf20de6999103a53e7ba2666fa1931bcec424b" + }, + { + "id": "47c5e02a-7989-51e4-95dc-283969cc312a", + "version": "MTc1NjMyNzg2NjMwMTA3NTAwMA", + "digest": "sha256:dcdd20a4b36cfc3d1887497f4255ce934bba3a6a84d4fc54ac989745440df2d3" + }, + { + "id": "47e109db-4357-50ef-a026-09dbc5d8ef6b", + "version": "MTc1NjMyODA2NDk0NTg0MzAwMA", + "digest": "sha256:599f1d3b88aa3a8715f4fca98229ed64d37efdc2108d71065999c5bebda3e24e" + }, + { + "id": "47ef1fe0-2614-5be0-bdc6-0a51202559e2", + "version": "MTc1NjMyNzg4Njc3NDUwNTAwMA", + "digest": "sha256:d2be80ec0f50ec12f038ef5ae3d8b86ebf740a700d914a6b4acbe4019c07697f" + }, + { + "id": "480914fa-442c-52eb-9f1c-4bdc17d68ba2", + "version": "MTc1NjMyNzg1NTc3ODIwNjAwMA", + "digest": "sha256:092c53fa233c5e8732ca2dac08dd9b54bff89fcbd065f539fe9dd731283004ab" + }, + { + "id": "4824d77f-a166-5c57-a47d-5f5826a495ce", + "version": "MTc1NjMyODE3Njg1NDk2MDAwMA", + "digest": "sha256:4346a4739ea2245e00f54263e1bfef0e6f02a499fc37ea7f2ee7f9cc1a3c6cac" + }, + { + "id": "4831b507-f190-5bdd-9200-d5c4b0e069da", + "version": "MTc1NjMyODE3NDQwMTc4ODAwMA", + "digest": "sha256:c10aa75c4d39b619cf4b99af540e819b04916e6f5bfff1b24ca859316b628276" + }, + { + "id": "4855db8f-383c-50ef-b28b-bd47eae0d0d1", + "version": "MTc1NjMyNzc0MjQ2MTE1OTAwMA", + "digest": "sha256:6e0119130b987b4c6c2b153097d883eebc3beed12b21570254f9d08482d5761f" + }, + { + "id": "485d5186-2f17-5b7e-a65f-6bf72f665a99", + "version": "MTc1NjMyNzc3MDY1MDYxNjAwMA", + "digest": "sha256:103ec2ee6cce51726a6658991c6d0d6a49c43f3a88df7293c8e0d7f252275df4" + }, + { + "id": "4861a21d-3d56-51c9-8804-78395f588554", + "version": "MTc1NjMyODE1NjM1MzgzNzAwMA", + "digest": "sha256:7193292f75b603ca4abe47d4fe3df98e32516a52753c141acec181d6049a2cb4" + }, + { + "id": "4872630e-1403-5dbf-a01f-1c9f47d4293e", + "version": "MTc1NjMyNzc3MTU3Mzk1NTAwMA", + "digest": "sha256:8008492e3b31ed83a88493c44c8e5da6ab21822226de90f392823deb35eb7c6c" + }, + { + "id": "4878d177-5440-529e-be39-2be18036ca34", + "version": "MTc1NjMyNzc2NzgxNjEzNjAwMA", + "digest": "sha256:ea0f97ea9d5d52e3393fa4c18750dc6fc2bc07aa0331f83c3d3b4fb4dad26cb3" + }, + { + "id": "48921f51-ed47-5db9-9316-590a0ce99d90", + "version": "MTc1NjMyODE3NzU5MTY2NjAwMA", + "digest": "sha256:2553c257fca5bee99da69e8ece5c724daa409b9ffe8eba65528b00d4190caa3b" + }, + { + "id": "489685fc-2a06-59ee-86fc-9bd9cb50f59c", + "version": "MTc1NjMyODE2MzM5OTY3MTAwMA", + "digest": "sha256:3337e1b75a95c76c8667711fd8e6c7cb97d4950f9cd1c8cb101749a6515c0dbd" + }, + { + "id": "48c473df-4b2e-5099-b41e-4cadff0a80f4", + "version": "MTc1NjMyNzgzNjI5MDk1ODAwMA", + "digest": "sha256:17688ff042c738810b07e31233d0f53540832a37f9792b88f54c565d2150d087" + }, + { + "id": "490b43e2-6b61-58e1-b589-3566a1d810f6", + "version": "MTc1NjMyODA2NTQxMDk5NjAwMA", + "digest": "sha256:f69d6a4670b8b1ee7d13854ea7eac2c19825e700e3597c9515801e8902a38737" + }, + { + "id": "491a5038-0e36-5f27-996a-455cabcfce56", + "version": "MTc1NjMyODE3NjgxNzAwMDAwMA", + "digest": "sha256:5df09580bbf97567f46c84f8b2df05a1d946df43b08bdfe20b1aec471a4d8e8d" + }, + { + "id": "4929f28c-04fe-5bff-aeed-0f83f2b3bf4b", + "version": "MTc1NjMyNzg3NjY5MzA5MzAwMA", + "digest": "sha256:5bdb174f44f7585ae5a0a81e361f22d6cd8b9e5231a4681dac5e00aa86330222" + }, + { + "id": "492d349f-dc3e-5c06-8e0b-ba07368cbbf1", + "version": "MTc1NjMyNzc3OTc5MzY1MDAwMA", + "digest": "sha256:3a675df06ca889e4f88840078d174b0150e2d222ed82422e6e5db1a26a24e3c1" + }, + { + "id": "492d64c0-5efc-5d3c-bf83-1175a91dee39", + "version": "MTc1NjMyNzg0MTE0MDA1NTAwMA", + "digest": "sha256:2edc43df9f708547968320fe77b72ee85310f869920c884ec63ea371095f15fd" + }, + { + "id": "4931b0c7-25bc-5b8b-ae4b-e9f3c3929449", + "version": "MTc1NjMyODA2OTk4Mzg4NjAwMA", + "digest": "sha256:0e3cb28b00599b59d156bfc7a145097b678b298f3fee79a5b23f9c152ed07636" + }, + { + "id": "4935f311-45c1-5045-8692-81c55b726c4f", + "version": "MTc1NjMyNzc5MDMzODI0MTAwMA", + "digest": "sha256:732cdbf46c5ef1a5941e88f3f560a5ea2267847b23f6e45c44a2399be0dd9045" + }, + { + "id": "494f5139-fa48-5114-80fa-7f2d4b6a967a", + "version": "MTc1NjMyODE3ODAzMzcxODAwMA", + "digest": "sha256:c6843b9140b433fbdd5a31b5eabc7b20f28e4f139f770f44021701495be91271" + }, + { + "id": "495debf4-d7fe-5fea-8dcc-a7f86243e504", + "version": "MTc1NjMyODE3Nzk3NTE0OTAwMA", + "digest": "sha256:1a679f7e0dc876a911f42127d2f51ccbce3abd10b22602419e91c9aac21719bb" + }, + { + "id": "496a5ed1-cce9-57f2-b6d6-b3e387440951", + "version": "MTc1NjMyNzc1Njk5NDUzODAwMA", + "digest": "sha256:d924d5f45d4925132982f3f44d7b3466371ed59963bd38c34b0c6c357b4185e5" + }, + { + "id": "49975d27-5d44-5481-bd28-b25e77109575", + "version": "MTc1NjMyNzgxMTkzMTYwNDAwMA", + "digest": "sha256:87facdfcb2b4db6ff927a83974d6e104f0e69264efd88ba85f946264d0de2705" + }, + { + "id": "49ae6166-3a0e-5450-a067-d8520c86529a", + "version": "MTc1NjMyNzgxMTk4MTM1MDAwMA", + "digest": "sha256:017ba1b64fbef94e193df45cb1da3a6e60c4618bc0952fd3c3356180e335f3d8" + }, + { + "id": "49b237f3-b8d7-580d-b7b4-a0fa1037565d", + "version": "MTc1NjMyODI1OTI4Nzk2NTAwMA", + "digest": "sha256:98df42b49ba5cd953a11cdd0713a6a3ba6eb6266f07207cd3f29c6ae81507ff1" + }, + { + "id": "49db05a8-12a4-5591-bf20-328371eaed28", + "version": "MTc1NjMyNzc3NTIwMDk3OTAwMA", + "digest": "sha256:05a5b0c108e294025cd5449e24f3cb445b206ef78bd323160d0527075568a0aa" + }, + { + "id": "49e59770-a97b-5649-b99d-7a7536fadd40", + "version": "MTc1NjMyNzgwMjg1NDIzMzAwMA", + "digest": "sha256:fdcb035268a8c7c254dc8d7672981dbf936c08da8c663b032f3bfdd80ac054f3" + }, + { + "id": "49ed2a98-61ef-5de4-a51a-62979d4fb0bc", + "version": "MTc1NjMyNzc5NDc3OTg4MTAwMA", + "digest": "sha256:6f535668528dc0366c7295d86162891362296c51ae672e32b999755b56b20e3f" + }, + { + "id": "49fffb0e-9f9c-50d3-859e-7bf720a49c78", + "version": "MTc1NjMyNzgzNTIzMTYyOTAwMA", + "digest": "sha256:6c0b3adefd1f8fd5efb1fb147035bc403567c1ed0b934db83420524f571bd797" + }, + { + "id": "4a103cae-d204-5c96-be3c-0a391ac25e9f", + "version": "MTc1NjMyNzc2MjM3MjczODAwMA", + "digest": "sha256:159cdd8acf3c9ed917005847a4ef1d0d073d44bb37699fa5b66dc2c7fb45c820" + }, + { + "id": "4a151ae6-6f78-5d47-83f0-3385a4c1acc9", + "version": "MTc1NjMyNzg3MTU5ODU5MzAwMA", + "digest": "sha256:8d3b75f47f184d09d8b5ad05650aedd9510902eb6c27502b3e39c01c900592d3" + }, + { + "id": "4a158097-ab82-51fa-92c9-050b12c9ffba", + "version": "MTc1NjMyNzgwOTIzMTU4MjAwMA", + "digest": "sha256:cfa27cd419ab760c5da6930947bac600f2a9bca5c2c11e67c5e9ae1152db395b" + }, + { + "id": "4a1a7363-9f76-57a1-99de-049d346499e4", + "version": "MTc1NjMyODE2NTgxODI3OTAwMA", + "digest": "sha256:3ab0de1b4aac259812004085fddd1e9a63ccab3167d4c369cbb921b39a47f9aa" + }, + { + "id": "4a304792-1874-5653-a3aa-b6ad57d06007", + "version": "MTc1NjMyNzc0OTkzNDg4MzAwMA", + "digest": "sha256:29f163ff7c05a6725c7f30bd98bc38805b1927f441aecce94689235d0a442b36" + }, + { + "id": "4a383b73-eef2-59e5-9544-101c441cd51e", + "version": "MTc1NjMyNzc5NTEwNzkwOTAwMA", + "digest": "sha256:243a9143a69cd80604d4ca22a371758ef4b8c3ac5ead5b16f468e3cd6c2fef5d" + }, + { + "id": "4a5c923a-5804-5beb-b360-778d38559d5c", + "version": "MTc1NjMyNzczODQ1OTM4ODAwMA", + "digest": "sha256:55bcd360f6662b595f08b65a2ebb922e3c20105ecb97478e9b5b596a33fe86cc" + }, + { + "id": "4a6a2311-32d4-5abc-9700-d5aacc4e6cb2", + "version": "MTc1NjMyNzgxNTYyNzEzOTAwMA", + "digest": "sha256:ac1e8d078ee02dedc454d71293f726047541baac41de60713d71df1a08d5ea33" + }, + { + "id": "4a8f7531-208d-5566-a842-0de87e1b04f8", + "version": "MTc1NjMyNzg3MTQ5MjA4NTAwMA", + "digest": "sha256:50631c56c7e0e412e985e30035c35dd1a3f4a148b7cdf31178df0128aac9e254" + }, + { + "id": "4a8fd089-7fb1-5da9-b471-3c5c84124850", + "version": "MTc1NjMyNzc1MzkwOTQ2NzAwMA", + "digest": "sha256:0eeafcf0a1fcb3eeb31a78d713e165e17edb956e67e53f912d44fdda0d30078f" + }, + { + "id": "4aaf6226-5320-5abb-82ff-008b3e360feb", + "version": "MTc1NjMyNzgxMjAxNjc1MjAwMA", + "digest": "sha256:0f9e9f10912161fd472932ee06c3f85c5c349e91f3bd02d1f0fc594f5e39a6b3" + }, + { + "id": "4ab973fe-9a4a-5a10-877f-a73e35cc1bf0", + "version": "MTc1NjMyNzg1MzgwMDQ0MTAwMA", + "digest": "sha256:008217a3299ac6ce45546c95c5844d98133af930a1af80f230e947ce4dab4258" + }, + { + "id": "4acccd2a-812c-5727-abe0-6174f1d42879", + "version": "MTc1NjMyNzc5NjExMzExODAwMA", + "digest": "sha256:41233506d67107528ea27882ff0a62bd6024cadb764d72c101226e80f7a24c3f" + }, + { + "id": "4ad484ee-652d-5e98-bcc2-6d9f48509e9f", + "version": "MTc1NjMyNzg0MzA3ODM2MTAwMA", + "digest": "sha256:1c88cb62326455cc983c777aa269584d6d4aefc9cd70767265efdcd2609ae913" + }, + { + "id": "4af57915-4c19-5157-99fe-e908d0640da1", + "version": "MTc1NjMyNzkwOTA1MzgzMzAwMA", + "digest": "sha256:4b3ca60e50825bd18b4db9045c5e804f303e2839e042172b17b5ad6ddeeb469f" + }, + { + "id": "4b04d7d9-09b8-5c42-8118-5c2fcd63ca19", + "version": "MTc1NjMyNzg1MjEzMzY3MTAwMA", + "digest": "sha256:39d91a5d32ca6d3e28655bebbe5ab444de42e3067b78c20a1d8ab38e1d442049" + }, + { + "id": "4b0e35d9-37f4-549d-850c-b775a34053cf", + "version": "MTc1NjMyNzc3NDUyMDE1MjAwMA", + "digest": "sha256:1cebd72f34a7432e64cdb18b6f8094688f520c847d53dc074f11393b0b606448" + }, + { + "id": "4b10038e-5d02-5bfd-85ee-3b057a57f32a", + "version": "MTc1NjMyNzg3MDMxMTU2NDAwMA", + "digest": "sha256:f17148e42793a4c58380fbdc3e3a26b18537de5c9e4b273f0b55e4453a6e859a" + }, + { + "id": "4b1ccdd8-5779-50b1-85d8-3559b233828f", + "version": "MTc1NjMyNzg4MDM4NDA5NDAwMA", + "digest": "sha256:adc7c2e41e842ecd8991d565365b8b8620048325d5848f6fa9f527c5cff56172" + }, + { + "id": "4b2f2488-1a26-54ca-97cf-bdf46d70efb4", + "version": "MTc1NjMyNzgxMTk1NDMzMjAwMA", + "digest": "sha256:3547968defefda29b99cc25fb34f630039c0aec4d3ae02594e47b2845301645a" + }, + { + "id": "4b3a3de9-0093-5f5d-ad46-e2730878552d", + "version": "MTc1NjMyODA2OTk1MjM1MzAwMA", + "digest": "sha256:0eb5cfff9ee07cf9345d27fec95f0a8ce1eeefcc6954d63fafb94da11b26f5ec" + }, + { + "id": "4b4c4dee-965b-581d-8d78-13a8ef229beb", + "version": "MTc1NjMyNzgxMTk0ODA0NjAwMA", + "digest": "sha256:ce2efb33a61666fda97f7980ffc9a5196b4a79262777d33512550f1ceecc26aa" + }, + { + "id": "4b557781-6c80-571d-8aee-566f2b1cf63a", + "version": "MTc1NjMyODA2OTIyMDgxMDAwMA", + "digest": "sha256:b32ddbe3e43f88f58594c16131ee29632894bf8418dce8405f202bf38e83753a" + }, + { + "id": "4b7ce868-2d8b-56c4-a06e-f16df3bc148f", + "version": "MTc1NjMyNzc2NjQ3MjQ4ODAwMA", + "digest": "sha256:4373efbccc3dd9eb76abc7e16ad53e12df6a218a7b14aeefc609517ec004ed32" + }, + { + "id": "4badaf84-a8c9-50ac-88d8-8d4fa4dc7995", + "version": "MTc1NjMyNzg3MTUxNTY0NDAwMA", + "digest": "sha256:79d21e8f489d62081b4eddc68c10d2f9b22c507d71a427f878e99ca5d695f2fc" + }, + { + "id": "4bb140ef-62e0-5145-a435-e6d058962c46", + "version": "MTc1NjMyODE2OTAzMzYzOTAwMA", + "digest": "sha256:a5a82ca2039a336aa67bf5dd588731ebbe6728d99251bc0f9506ddf39cd73824" + }, + { + "id": "4bc0f7a4-1c0b-521e-bdd8-2ac5d643750c", + "version": "MTc1NjMyODE2NDk1NDkzNzAwMA", + "digest": "sha256:9d123b4a3ef07de93bfee39da858a4bde9563a5fbd664007fb50ac6c45e2f3ba" + }, + { + "id": "4bd1098c-5bc1-524e-9550-5c30756817da", + "version": "MTc1NjMyNzc4NzI5NzI2MTAwMA", + "digest": "sha256:799a13d89c398cbc503c081b713af61fbb2212984003d72c1d82fb597850dbd4" + }, + { + "id": "4bd20151-7224-5a8a-963d-f092bc8c03fa", + "version": "MTc1NjMyNzg0MTE4Mzc0NzAwMA", + "digest": "sha256:19a852637a68d089bfcfd58c702bac12dddf962d2294eda6c39e03d2a065c156" + }, + { + "id": "4bdea9c1-7294-5332-bad5-2edad42b999f", + "version": "MTc1NjMyNzc4MTA0NDg2NDAwMA", + "digest": "sha256:0b289862af2fafaf48b03e49aefe99d51fd00c76b43a1bbd7bd49aff07f67867" + }, + { + "id": "4bf1082c-36c3-50aa-882b-fac8b04fdb31", + "version": "MTc1NjMyNzc5MDMyODYwMzAwMA", + "digest": "sha256:a665739b71c951da078b654465683fa690a1f9171d3759c5aeea465c695c90dd" + }, + { + "id": "4bf47fbc-c4a4-5986-8daa-95c8ad764c29", + "version": "MTc1NjMyODE4MDQ1NDE2MDAwMA", + "digest": "sha256:0c5f86ad02b30986f0dcff0e227a3d373228fec4673ca1e92426deb5431603c1" + }, + { + "id": "4c0bc83b-5b9c-5aed-b68d-7b7ebc258083", + "version": "MTc1NjMyNzg4NjY2NzIyNTAwMA", + "digest": "sha256:fa4b611337c2ede84c3ee905be4a2215939b0299f8fd2c3f7113d1638233911e" + }, + { + "id": "4c13f92f-d39c-5f73-87c0-7ec8af1c7afd", + "version": "MTc1NjMyODA3NzU5MTMwNjAwMA", + "digest": "sha256:5169317c667c6d6ce0069913f79b0401794506607e4460a8b30c989df4513571" + }, + { + "id": "4c308eac-131e-5773-9749-78c236fd5ba9", + "version": "MTc1NjMyNzc1MDEyODE4NDAwMA", + "digest": "sha256:542ce2b65c8afd787bbb192fce374f0c1a43a84bac00885c6ec04a29c8e15e43" + }, + { + "id": "4c488704-b948-5545-8fa2-1b05a4e6efb4", + "version": "MTc1NjMyNzc1NzQ4MjA4NzAwMA", + "digest": "sha256:3a8bcf2aa5b16078b10508cdd8ed050a74fb45e0fe1a1ded0d330641970c14aa" + }, + { + "id": "4c58b1c8-c0f6-5d3a-b4b3-4e40bb711805", + "version": "MTc1NjMyNzg1NTY5NDQyODAwMA", + "digest": "sha256:9b560e2485b763b9ae8b2e7a0119129a0430f82831f73c7d183dc24bd318d9a9" + }, + { + "id": "4cb38425-0067-5c00-9b85-92e93abf5b25", + "version": "MTc1NjMyNzgxMTk0MzY3NDAwMA", + "digest": "sha256:9178ff1bd8926349e95503c7568f1db08266b2c4847ffd8e861207bb77a3f7fd" + }, + { + "id": "4cc9b3e5-985c-5f56-b05d-7e2539b718f9", + "version": "MTc1NjMyNzc3OTc4OTI0MjAwMA", + "digest": "sha256:26b070ca2a5a5fcb428f1764b58ac488bfe1ad4745629092be2e3cc6d8bf5e98" + }, + { + "id": "4cd25611-6455-5fb5-853d-b9b647645752", + "version": "MTc1NjMyNzc4NjgwMDk4OTAwMA", + "digest": "sha256:4c676f08b57e725bd1ced32f0c6bc9df9dd90a65dd57a6a1261f006f46ef508d" + }, + { + "id": "4cd70cd3-5283-571a-9376-98caa9944f2c", + "version": "MTc1NjMyNzc1NzUzODY3MDAwMA", + "digest": "sha256:10765b0258d58da8b24160a593eb74fdea2f39d24b7b44d268a6fbb97360435f" + }, + { + "id": "4d29f473-2a02-563c-8bb1-d11bbe418fa9", + "version": "MTc1NjMyNzc2MzcyNDUwMDAwMA", + "digest": "sha256:528c11182bf15ed5c9ef4e86e108f22b29ba7cd21e255de0b5bbbc615429a965" + }, + { + "id": "4d3426a2-cab1-5356-900a-3dba56e47877", + "version": "MTc1NjMyNzkzMjU5NjE1MzAwMA", + "digest": "sha256:886b7e23aa71d47e218fcb2875f34baf14aac0229e6cb3d0bbc6bbe6631e6ace" + }, + { + "id": "4d45b988-d97e-500c-8f8f-d2116a476bad", + "version": "MTc1NjMyODE3Njg0MzY1MjAwMA", + "digest": "sha256:562ba7e18233946785eceed44c7cb18a6d2b1a9b9a01d0215b02750ad755e4d1" + }, + { + "id": "4d5827d6-ba59-5206-a39b-0afdad6db32a", + "version": "MTc1NjMyNzgxNTQ5MDAxODAwMA", + "digest": "sha256:87147d2e7200862ff932bcc841380eaa0d87f2b8d18ce407cdb652b41898e78b" + }, + { + "id": "4d6ea70b-654a-566e-b3e2-a18ca3b328e0", + "version": "MTc1NjMyNzgxMjY4ODc3MzAwMA", + "digest": "sha256:ce6d0d8fe0d0402f91e2fb5b7be01393d3ed5f11845bb04ce32455093174d951" + }, + { + "id": "4d7ad064-04be-52d1-aae5-da0876a8e6a4", + "version": "MTc1NjMyNzg3MDM3NjEzNjAwMA", + "digest": "sha256:4cbc772cd00ae7b140e62146c512dacb752e8f2f6968373b1118945c5869f6b9" + }, + { + "id": "4d7ca07f-d5a0-57c4-bd55-5b32ed5f2a79", + "version": "MTc1NjMyNzc0MjUyNjc4MjAwMA", + "digest": "sha256:d8be876b6313411959b422b0af3e49e6265418ca768e713438df3674552ad7d9" + }, + { + "id": "4d995d37-e3d2-5d71-92c6-f40b355bc496", + "version": "MTc1NjMyNzg3NTQzMzc4NzAwMA", + "digest": "sha256:2a218154abed53e0435ba7465b2bdc420132a9738d9ea5bdba6920b4eccc225e" + }, + { + "id": "4dcc5ebc-4b31-518d-829d-1a9643ed60bc", + "version": "MTc1NjMyNzg2NjI0ODg4ODAwMA", + "digest": "sha256:8812bd8f9984fc82124372d776590be39993ef992543527f3beba3a910c32a72" + }, + { + "id": "4dd08bc1-4e44-5c68-8799-ef76aa6d2413", + "version": "MTc1NjMyNzc5NTMyOTkwNjAwMA", + "digest": "sha256:59e42cc67583ac982a4b5ac337bcc514421c260d5577f305d6d5ce34a808bebf" + }, + { + "id": "4dd4f414-43b6-5371-9448-8e9924027cc6", + "version": "MTc1NjMyNzc0MTYxMDU5NTAwMA", + "digest": "sha256:724d506a372d7b0e84ec41acc39b4098f1c056ead519156cf622cdbc8db7c328" + }, + { + "id": "4df37661-7e28-5434-a1af-410814f5fb87", + "version": "MTc1NjMyODA2OTg0NDk2NDAwMA", + "digest": "sha256:086145ea5dff86530d31ae481097ff7f63acfeba2e061a46874132ce51ba275d" + }, + { + "id": "4df7e33d-1206-56db-9650-004a95ebadf0", + "version": "MTc1NjMyNzgyMjMxMTU4NjAwMA", + "digest": "sha256:4ac45068639d7c93c3216b9543ad20a79f73ba62c3e8f4d5fe6d56dbe601e2a5" + }, + { + "id": "4e2109ae-595a-56cd-a076-2b7445ff2118", + "version": "MTc1NjMyNzgzNTIyNjIwNTAwMA", + "digest": "sha256:3f0c832250c37202e6d792d32b141f70c44713c7b08e8698a2defacc563a5718" + }, + { + "id": "4e3309e6-17bc-5f02-b67b-d43890be5dca", + "version": "MTc1NjMyNzc4NjkzODk4MTAwMA", + "digest": "sha256:4cad4649ba852e7d4742c2f35c4e4421a5c780d99ac44a8bc11bdeeea8d19fc4" + }, + { + "id": "4e3b55f4-89cf-5528-bd97-6d4afef28603", + "version": "MTc1NjMyNzg2MDMxMjgwMzAwMA", + "digest": "sha256:e1345bdfe88adf56e805df5d877608ec470604d5f6e09a4ea31f5aed550ed41c" + }, + { + "id": "4e4fc4cd-22bc-50cb-9a57-15e8beac52cd", + "version": "MTc1NjMyNzgzODk0Mzc0MTAwMA", + "digest": "sha256:3f464e53d093f7bcbcc251a34bd3bcbaf769205eb9560fe09f3f7ed141131e4c" + }, + { + "id": "4e4fc53c-480a-5b2e-b9fe-51a1a1fe1cdf", + "version": "MTc1NjMyODE3Njc5NzY1ODAwMA", + "digest": "sha256:bc7b9c5982f3704e88e29a948add3058eb478d800b47ad6009a2632791a59a7e" + }, + { + "id": "4e51c20d-5bf0-5b12-b803-c8b503cbcd00", + "version": "MTc1NjMyNzg0MzA4MDgyNDAwMA", + "digest": "sha256:74ed2f2b358311c25c83c3b6752d2292af706cfb889aa2944f974c7ee80c340c" + }, + { + "id": "4e606985-8831-5f4a-b974-1f7281a463ee", + "version": "MTc1NjMyNzc1NjA4NDk4NzAwMA", + "digest": "sha256:ed8050db7ede16f412260d3839966b3e9b601c78cfba99599146f7552d6f94bf" + }, + { + "id": "4e68c985-8332-5e26-b5d5-7e1ca4c9d044", + "version": "MTc1NjMyODE1ODIzNzkyODAwMA", + "digest": "sha256:ed771b6adf58bc7f79b5a2bfb5e2de06d11e9ddc8b842c8438ddfffbae26a4a9" + }, + { + "id": "4e81b4ee-bd44-5230-b22e-fea7c21c8db5", + "version": "MTc1NjMyNzc1Njg2NTM3OTAwMA", + "digest": "sha256:8f1636049b65650e83b78e58946d83bf974e841d90534e121f693958d318b725" + }, + { + "id": "4e9953cd-22fe-588f-8433-2be7d0ac6d89", + "version": "MTc1NjMyODE3NjE5OTM0NTAwMA", + "digest": "sha256:9d47f97b1f8d080b22d0562520b360e9bf4b6d8e9721b5b2e401fc80c1f2acac" + }, + { + "id": "4ebcb055-0548-564b-a22b-3127bbb7eee9", + "version": "MTc1NjMyODE2MzM3MDUwMjAwMA", + "digest": "sha256:932bd215c01f9e526f2108325299a2f2e56c8c651afa44ee9c053fe681a48037" + }, + { + "id": "4ee404ae-4d18-5d22-bbb9-880da58437c9", + "version": "MTc1NjMyNzc1Njk0NzcxODAwMA", + "digest": "sha256:32c72027bc46547ac3e157800cdfd47b0112cbda9ec06655bf567d2e12dd8b27" + }, + { + "id": "4ef1e6a2-bb2c-5d5b-a9ae-536e2df7a24c", + "version": "MTc1NjMyNzg0MTE3OTA0OTAwMA", + "digest": "sha256:3df123b806bae0a5537e03dc81f925406dc70e7a35fa67bb2d5f5b80449e4c76" + }, + { + "id": "4ef23ee4-b48e-5daa-9267-6064b6610739", + "version": "MTc1NjMyNzc4NzE5OTIxNDAwMA", + "digest": "sha256:81e2782f61f833091b1a995cb4fe6dacdf7e93a8a92288d544606b7bf926e4f8" + }, + { + "id": "4f1aa5ed-dabd-56da-80d1-c66af910dd6a", + "version": "MTc1NjMyNzg4NjUyNTc0OTAwMA", + "digest": "sha256:681a5dad422e4a54b32d10e11d8f7fdedf6dc205c026668e6171c6c4e03c9b1c" + }, + { + "id": "4f37251f-5588-5108-a85d-98ba37994856", + "version": "MTc1NjMyNzg1MzAyNzc0OTAwMA", + "digest": "sha256:26ef9201b01a075f6591d6a26bafb6576fafba98dfcb4a0d538ceccba72d5fcf" + }, + { + "id": "4f39ea15-16cf-5650-ac8d-dfdf4a59e14f", + "version": "MTc1NjMyNzc3MDY4OTM3MDAwMA", + "digest": "sha256:37dfbf56f40ed432725e7fa73da11731bfa384c683007b4e592ac167286a1853" + }, + { + "id": "4f3b1087-1b19-5a7f-acb9-9a9b4fc7a30c", + "version": "MTc1NjMyNzgwNDU0MjU5MzAwMA", + "digest": "sha256:de5f028b08967a7ebfed102d8dc1b73256bf985f2e628f2da9f756081a7daa03" + }, + { + "id": "4f3bd719-58c8-586f-b32d-5b06dbd1b7af", + "version": "MTc1NjMyNzgzNTkyMDQ1MTAwMA", + "digest": "sha256:f61415c4d2ae457de2c850fcf2d6d5c2c36fb2e45851223073142365011218fe" + }, + { + "id": "4f4358b0-c979-5042-8cdd-04e0331e7654", + "version": "MTc1NjMyNzc2Mzc5MTQyMTAwMA", + "digest": "sha256:3664e4a5fc14653272480907f696930e6ebd2213ed0591e619913195d3de65b7" + }, + { + "id": "4f4ebaec-b973-59ac-ac76-5b006e14b87d", + "version": "MTc1NjMyNzc0NDUyNDc2OTAwMA", + "digest": "sha256:3262b44730fc2a543093f635d435918d113d2422ac7fb5f1604a9347bbd4a24c" + }, + { + "id": "4f6a21b5-a3b4-59eb-90c4-9a11eb3e0135", + "version": "MTc1NjMyNzgxMzY0MTE1NzAwMA", + "digest": "sha256:caf4af58152d65f01c5d352deef2e19d78e95f06ff8e56349b6e1a31b9311985" + }, + { + "id": "4f73e576-6dbe-5f2f-86c1-e470dc8ef557", + "version": "MTc1NjMyODE3MTA5NDU3MjAwMA", + "digest": "sha256:4d9fa7d571f983495e709f27b99527363b852ed1c3731df79bee785117c0b31e" + }, + { + "id": "4f8cd24a-bf3d-5dc0-bad6-64c16453cb1a", + "version": "MTc1NjMyODE2ODk5MzY1NzAwMA", + "digest": "sha256:8abfea11c97825d6d0b7d45d2487d6c06d8d029b09beb6fbc46ae320446b571f" + }, + { + "id": "4f8f10c8-fb80-5931-998e-f4576b4e2d4a", + "version": "MTc1NjMyODE3MTA5MzMxMzAwMA", + "digest": "sha256:60e39d52d7078c24fd7c7472785f73f83dcc57f1b33b563b9f4757b2eda5c759" + }, + { + "id": "4f983e37-f038-511a-a862-a0b1400a0126", + "version": "MTc1NjMyNzgyMjQ0MzQ2MDAwMA", + "digest": "sha256:ce40cb6d15972f678bc269475ba4ca5f627c311947c4c4769a52903cb8ee378d" + }, + { + "id": "4f99c910-1bbd-5238-bdb3-bf650ec63d06", + "version": "MTc1NjMyNzgxODgzMTQ0NTAwMA", + "digest": "sha256:2804cd88f951cfdecf1149e79e3dca3d4941941c0945c11c1e6345426d08b1a3" + }, + { + "id": "4fb7e4f9-e23f-543b-b43d-bf24e9767079", + "version": "MTc1NjMyNzg1NTczMjc1MzAwMA", + "digest": "sha256:3dcd551521b49298ea59f57a08b84acf861ef80d4756483f1422cb277068a1ff" + }, + { + "id": "4fe38ba3-a63e-59fa-ae3f-e73dec7c8a68", + "version": "MTc1NjMyNzg2NjY5NDI1OTAwMA", + "digest": "sha256:60803d13fac536143a92b0c8a394df4ac63d4a7cc3829641a735b064d0a9e8fd" + }, + { + "id": "4fe618c2-fb42-5f7e-852c-066f7b50d31c", + "version": "MTc1NjMyODE2NDk4MjM4OTAwMA", + "digest": "sha256:cf9d9075f0b2f39ee20e56989f03c78f1bad38ef3d883e20b12d84cf6af2d0c7" + }, + { + "id": "4ff157a7-bc7e-512b-bc01-ee02a7e6a33d", + "version": "MTc1NjMyNzc5NDc0ODEwNDAwMA", + "digest": "sha256:31fe764acd28c9ddd1bb2bb6a8ee6219f1e3a2607ccc88bcbf29cf396de6ee43" + }, + { + "id": "4ff2e636-a84c-52f1-bb93-a3a9df8313fa", + "version": "MTc1NjMyNzc2NjQyNDU0OTAwMA", + "digest": "sha256:aa716cd8968b9c94a3611efb13ad0d8ace46b72991763e42f4fe162e0f10b231" + }, + { + "id": "50137b11-6894-577d-b67c-e0a37f5a06f2", + "version": "MTc1NjMyODE4MTkwNDkxMDAwMA", + "digest": "sha256:2193a38d8e6976e8f3bac5b552f2f63100608cb6384ca0c94bc862b4087acf42" + }, + { + "id": "5022f57a-e12c-51b5-b68a-2928d5c513ec", + "version": "MTc1NjMyNzc3MjYwNjUzMjAwMA", + "digest": "sha256:819c4f9d1037e66d69633cb19e476bfea4241978b8749b1e7866a658559e8ad9" + }, + { + "id": "5034c109-688b-59f3-a43c-27c80ec2396f", + "version": "MTc1NjMyNzgxMTk0ODM4MDAwMA", + "digest": "sha256:95b89a147e5985c5e1087714f49200d86d447d07e8f45b8dc2e218b9ee0a597b" + }, + { + "id": "504972e7-1e01-503c-b3b1-498767cada83", + "version": "MTc1NjMyNzc2NjI3MDU1NzAwMA", + "digest": "sha256:80779fac805fe8adee50c45efe0226e3c57a2e6aa24f85c083d1511f70d6b21f" + }, + { + "id": "5053b1da-eaad-52dc-8370-27bca94c1b7b", + "version": "MTc1NjMyNzkwNzI2MjkyMjAwMA", + "digest": "sha256:1af1c49af2ac9183f5204eab54030ceab79492cd1b850c7511341799a322e9bb" + }, + { + "id": "5058bd1c-108f-50d5-bb14-869a6837369c", + "version": "MTc1NjMyNzgxODg1MTA5NzAwMA", + "digest": "sha256:befe8a67f634f49550ccc917e4df64ecd045812161b051ba4a4cf9cee08f3ecc" + }, + { + "id": "505f9e65-3bbc-54d6-92b3-a80cf6b83231", + "version": "MTc1NjMyNzg2NjExNTgyNTAwMA", + "digest": "sha256:ae1845bf99ebd6a3231d2382f3135b99935f4b0748824a5a668d3ee96ec34f83" + }, + { + "id": "508d7f7c-4c00-5526-ba5c-1432bcf409fc", + "version": "MTc1NjMyNzg3Njc3NTA0MjAwMA", + "digest": "sha256:cfa223e74e5c051c2947894ba6fdb716dfea1fde135286cb542e0750901d60f7" + }, + { + "id": "509a6b89-67a2-542b-abec-82cadc5f952a", + "version": "MTc1NjMyNzg3MTk3Mzg5OTAwMA", + "digest": "sha256:ad1ec9fd71e60382b4d603c5ef5c2d92f883eb90550fb63cd471cb7ab5da241b" + }, + { + "id": "50ab3b01-99e9-53c7-bc53-3b441a7c0e56", + "version": "MTc1NjMyODE4MjExOTE2MTAwMA", + "digest": "sha256:2035f1474610e6547ec9d5c88987926b999ca556800fc66af3d370f0b1f007b7" + }, + { + "id": "50ba4b73-9e32-5ac5-b8de-afa8e7afb954", + "version": "MTc1NjMyNzc1MzUxMzU2NDAwMA", + "digest": "sha256:09b7d562af4e4a7af2dd2f357670be1ceef7b23c45c067d36402b136fa2bfe22" + }, + { + "id": "50d6cfe2-510d-570f-ad74-ca96cdfc0f83", + "version": "MTc1NjMyNzc1Mzc1MTU0MzAwMA", + "digest": "sha256:efa3c53aaa19a2ef4f5a9c313c6c5a3927508885155a0dd278bc0c2272c60aae" + }, + { + "id": "50e9b4b1-6953-57c7-9a3e-16ad4f927079", + "version": "MTc1NjMyNzczODQ2MDIzMjAwMA", + "digest": "sha256:9b613bfe8d4bb049d2867a279bacd8fb8f3efae54795b7c42f3471e87fd0bdb7" + }, + { + "id": "50ea9653-0a75-599f-8949-bc3e57896e69", + "version": "MTc1NjMyNzgwMjk5MTE0NjAwMA", + "digest": "sha256:1968d252503cc774d961cafeda53a2fedf956dd79a24054e39374ad010c0336f" + }, + { + "id": "50f2e574-c147-5249-8615-7bf746933fa0", + "version": "MTc1NjMyODE4MDQ4MTQ3NjAwMA", + "digest": "sha256:2923ebf1e674e41604b92acf2da0a48659c925a882760de3bf5317088bb3dfa3" + }, + { + "id": "50f30e9f-eb69-510b-8dac-8f05aee6f04a", + "version": "MTc1NjMyNzc0NDQyNTc0MDAwMA", + "digest": "sha256:be6287cc10dd37d4d7a318cd3cf64706980d3478216b84f402a552c83283dad1" + }, + { + "id": "50fde647-ad47-567d-98fc-cac698941180", + "version": "MTc1NjMyNzc5MDMwOTE5ODAwMA", + "digest": "sha256:45e31670eef2ab19fe90a0204721967ad15b106f6da605efbae1366979f4c7a5" + }, + { + "id": "51117735-f6a3-5a25-93d2-07fc973d1036", + "version": "MTc1NjMyODA2NzU5MTUxMjAwMA", + "digest": "sha256:fe997a960bef07e92ed7005c9dd0c9fac2d1fed73a61d2f222b22f373fce9b2c" + }, + { + "id": "512af67e-9e81-54fb-aadd-9d490a366c18", + "version": "MTc1NjMyNzg1NDQ1NTQyOTAwMA", + "digest": "sha256:cb8482d509757c7cce9462dec297e89b8216cdbe3540bae9a29025f2d4abec6b" + }, + { + "id": "5136999f-a2a6-52ab-91b5-4a672f63fade", + "version": "MTc1NjMyNzgwNDU4NDIwMDAwMA", + "digest": "sha256:327388770fc98a18f991908cc9fc2a6e1857b277887c1620e4cbd86d4f1940ff" + }, + { + "id": "5145b513-9a8e-5a11-90dd-ec411e49a5b5", + "version": "MTc1NjMyNzg2MDI1NzMzMzAwMA", + "digest": "sha256:309d96daead5160bdafe71e7fc9ffcc7422731d1331199508b5715ea4ffe8089" + }, + { + "id": "5150bf6a-8fb6-5343-a011-ebe297694b26", + "version": "MTc1NjMyODA2NzQzMjcxNDAwMA", + "digest": "sha256:01cc6dc044e542bb2d0086ca07ee135440b0b228e8b2d5db2e79c92a8ab89481" + }, + { + "id": "5159f023-49cd-5343-986f-4f9518086b2d", + "version": "MTc1NjMyNzg1MjIxMzQzMTAwMA", + "digest": "sha256:1ee8b9f296bfd7216038c9234c2216445dad3e48c295034e23200fa1bbd3e009" + }, + { + "id": "515c9c08-498a-5609-b70a-2180f6d710ff", + "version": "MTc1NjMyNzc5NjExMjUyMzAwMA", + "digest": "sha256:42765b366f5bf9f8f033c7989649c14fa545a1c8193e867deabda4328430d60d" + }, + { + "id": "516a3930-9907-54e5-9a78-0e575bed3087", + "version": "MTc1NjMyNzc5NTk3MDI0MDAwMA", + "digest": "sha256:a741cda15a139552ad5252904f65612d760ae8c45c0acd573ed9e06389709b76" + }, + { + "id": "519af253-047f-55bc-9edd-307fc79a3269", + "version": "MTc1NjMyODE4MjE0OTQ2NjAwMA", + "digest": "sha256:a1a7c39dfe5083b54fc2df9ff2555f9e171858606aacf8a6fc7282f0dfb1f820" + }, + { + "id": "519e38d9-6e96-5544-8e64-30d903099ef8", + "version": "MTc1NjMyODE2MzQxMzI0NjAwMA", + "digest": "sha256:1e92c77befb01bc70db5bf4938e37504b02997c1d6d149bec6c74499535cf5a1" + }, + { + "id": "51c180e9-5a95-50a5-91ab-68474291de68", + "version": "MTc1NjMyNzg0MTA3Mjk1MTAwMA", + "digest": "sha256:2bde307358b5bc8f0379267430dda30fb0332915af44e3a22b09b048a3104f5d" + }, + { + "id": "51e69455-aad6-50ed-ae6a-971881c2d9d6", + "version": "MTc1NjMyNzgxNTYyNjk3NzAwMA", + "digest": "sha256:d8c562787eeb0292c468dc4970f8bf1e25bb5d7e66831ba645629d826c0672be" + }, + { + "id": "52188ddd-f510-59b7-af45-8c080427f94b", + "version": "MTc1NjMyODA2ODEyOTU1ODAwMA", + "digest": "sha256:6f3a299e7d39c363cc0dff69ae510ea2142e50f9b422560e798a10725f503865" + }, + { + "id": "52366dff-1552-58be-8b35-18a72793003a", + "version": "MTc1NjMyNzg1MzM2ODE0MTAwMA", + "digest": "sha256:b6bd0081db144b76c8643f3fc9ac067725926c882179ce72e815803bb17f7f95" + }, + { + "id": "5238bfe5-e9eb-54b8-a4d7-a03e8dd1ef53", + "version": "MTc1NjMyNzc3NDUwMDQxNjAwMA", + "digest": "sha256:0bbdd8ee750bfb853a2a54c26d78aa495162767403a3c70a12116421a6b426e8" + }, + { + "id": "52811f3d-4136-57a4-9891-85ed489756bd", + "version": "MTc1NjMyNzg3MjEwNDU0MDAwMA", + "digest": "sha256:1b44481cb1dd12b5c12313de7616709de60519b74983ef1f0e186adc3b5fa8e8" + }, + { + "id": "529d4dc1-48f9-5582-a0a7-61c476ded5e3", + "version": "MTc1NjMyNzg3MDM5MDM2NjAwMA", + "digest": "sha256:598ee8633c8c7a5c8a98a084d5e5cb3163897a9a7cbaf7d8d6f1cb898762238c" + }, + { + "id": "52ac08b2-788d-58e6-9921-d7b5cfe27336", + "version": "MTc1NjMyNzg3MjAxNzI3ODAwMA", + "digest": "sha256:e7c9f899a4af6ce3ee4d3272c2e533dc3f85674d4b34007e9ffbd6e0bcae197f" + }, + { + "id": "52febd1a-adfc-5c96-8970-5fcc2f05575c", + "version": "MTc1NjMyNzgxNTUxMDYxNTAwMA", + "digest": "sha256:40ca2a02e7e1d2aeb49dfbf389427ed747aa0a50af64dc4f87ae4f4660c1fc44" + }, + { + "id": "531bbc5b-1e6b-5395-9918-017ea937edad", + "version": "MTc1NjMyNzg4MjA3NzQxMTAwMA", + "digest": "sha256:7c10f7039de889c17894a01bddec83566dcf40f8d073ab00b36dff68269d894c" + }, + { + "id": "5351fd54-93c4-53f2-999b-01767b49c627", + "version": "MTc1NjMyODE4MjA5NDA0NjAwMA", + "digest": "sha256:0aa56b2e0267c4edaff5609cf2b043b715d712b1db438edf8f2d9391fb7e977e" + }, + { + "id": "53734b0e-2e64-5140-90a5-918d84615585", + "version": "MTc1NjMyNzc5NDgzNDYzMTAwMA", + "digest": "sha256:6dc14fee6ea2fa95800b606f4b09670547eab40492b4f798c90a4e42446a0abd" + }, + { + "id": "5376228c-84c2-5bff-b84f-d8f03b3cae0c", + "version": "MTc1NjMyNzc1MzYxMTMxMDAwMA", + "digest": "sha256:702118555d7495400f3a171d00a588392e169b3478305b9affd773f154f12009" + }, + { + "id": "5390cee6-0332-5d23-b436-3d4404df6f58", + "version": "MTc1NjMyODE1NjQxMjA0NTAwMA", + "digest": "sha256:ea1d0cadf80d6487f4a45797f8c64d0693292aa5a64c859ad7d3ff2593ad3c3f" + }, + { + "id": "53a1a558-2179-5674-a720-96dc83981149", + "version": "MTc1NjMyODE1ODE2ODc2MzAwMA", + "digest": "sha256:dc97aae4fd7d0d06c3aaa79033691f565d65b931d2158bf111337b65eb8ea505" + }, + { + "id": "53ccf6c8-b811-5b4f-b90d-b130fb683f52", + "version": "MTc1NjMyNzg2NjM1NzYyMzAwMA", + "digest": "sha256:5e4832edceb870461b3fc1c6880167963ba68b408f6451d38297df9b517c11a9" + }, + { + "id": "53ecf251-3544-513b-9f36-16097fc7726e", + "version": "MTc1NjMyNzc3NTI1NjM0NzAwMA", + "digest": "sha256:f66ee2c946a3dc89b85fe631fb2016718968f8e02394e2f6d0d638c08533652f" + }, + { + "id": "541bdced-5cab-5b04-8d4b-7df2e4122d16", + "version": "MTc1NjMyODE4MTk0OTIyNTAwMA", + "digest": "sha256:399f9ebea46d1c26914a2f98c54e4c64488463c09480c4483a6f440f493b4309" + }, + { + "id": "542a7137-c61c-5638-a674-9f8cb43d4f36", + "version": "MTc1NjMyNzc3MDY1MDEyOTAwMA", + "digest": "sha256:654d7c04a43d952ab6249d6d54db1b25a275dde6ffa3698c5744fd2b1a93a44d" + }, + { + "id": "5434d305-3a07-5771-8312-560a21103dd8", + "version": "MTc1NjMyNzg1NDQ1NzIzNjAwMA", + "digest": "sha256:dbc7766bf51971f7878ebc9bd2cd46f1a41c366f146998897374cd17bed03b77" + }, + { + "id": "544da080-5395-5a53-a043-a90e66a7abdc", + "version": "MTc1NjMyNzg3NDE0NTM5NjAwMA", + "digest": "sha256:8477ce6cd086ee6f51089e97ab09d1748b096e17408fe848278e2d0a8e5aa009" + }, + { + "id": "5452fb31-318d-55d6-9113-9a7be6441725", + "version": "MTc1NjMyNzg3MDY3NzcxODAwMA", + "digest": "sha256:abb18eaff63f1124505124a589873bc41da5b1edd0aaae9d7cbe1b9ba46c4b9e" + }, + { + "id": "5455d277-7ea5-510e-b25b-9ad3eee74993", + "version": "MTc1NjMyODAyMzQzMTkzNjAwMA", + "digest": "sha256:d0bc4c650758aee99b60c70dc5086419ab4b05ba56c6cd50227881bf0c1b4684" + }, + { + "id": "5458c99e-0e08-5b4e-9ae2-7faed75f341e", + "version": "MTc1NjMyODA3NzY3NjQ2NDAwMA", + "digest": "sha256:8e774e30d5a42ef02c02319cbd1cdabb301947ae79384779b5045c42c0c28762" + }, + { + "id": "5460224f-adcd-55d6-851d-b3ef4a11d8bb", + "version": "MTc1NjMyNzc1Mzc2NjU4NzAwMA", + "digest": "sha256:2230ed1a3d611585017dbba85e5d31d82b51339244ea9948089734d7655c996d" + }, + { + "id": "5473d519-14e7-58da-bcfb-a4d9b7206336", + "version": "MTc1NjMyNzgzODk1Njk3MTAwMA", + "digest": "sha256:63ff079a9921d59c2912537082b0a3ec881c83d189012c32430350ddb96e48ef" + }, + { + "id": "548228b4-3123-5e48-a4cf-7da177d7e4f9", + "version": "MTc1NjMyNzg0Nzc3MjkxNjAwMA", + "digest": "sha256:a7cb325339f8e97dc078a5079aca10d271934305ec84edcfdb094ef4b8772282" + }, + { + "id": "5483a11d-1b94-5339-81f5-e16ef8812a35", + "version": "MTc1NjMyODE2MzM2MDkzNjAwMA", + "digest": "sha256:e1cd65999722bc8029e549ebd1a5c9e19ee47fbf9401320316704e400469f614" + }, + { + "id": "549777b2-2698-58bd-b133-f68a0144bb1c", + "version": "MTc1NjMyNzgwOTMxNTYzODAwMA", + "digest": "sha256:ae4324e6df3fc33c141c5be5001f3aa28c637132321f1f3b7b1fbff5b2f920c9" + }, + { + "id": "54a1122e-eed1-59d3-9e3a-0d3fd33221a0", + "version": "MTc1NjMyNzc1NzQ4MTcyMzAwMA", + "digest": "sha256:00cf5b423554f7b076c1ddc405c8f928ce1a823d72b217bdbfb6d476a9cae862" + }, + { + "id": "54a23415-f2f8-535d-bba5-ed31b186b1a4", + "version": "MTc1NjMyNzg2NjczNjMxMzAwMA", + "digest": "sha256:f678433bdad03048e275e5be7ca40af29c319523a0b77d5cfa31427f33036729" + }, + { + "id": "54a61abd-5192-5104-9bdf-1fda8ebb1e09", + "version": "MTc1NjMyNzg4NjU5MDM0MTAwMA", + "digest": "sha256:292a4285c6c1ed66465524c0cfef1079bbf599f173c5583ee0d3964cbd20ee95" + }, + { + "id": "54bdfe98-58ec-5389-9093-812d7caaa720", + "version": "MTc1NjMyODE4MjA3OTkzMzAwMA", + "digest": "sha256:18653fc55336577981b817fe9c1346bac948018f38afd98ca1964948d71c98b9" + }, + { + "id": "54dbb6d7-8f89-5e08-9088-8f173e748c79", + "version": "MTc1NjMyNzg3NjYxMjMxNzAwMA", + "digest": "sha256:85ee6e6d0f1ae38ba7e4944a793f4e920de548fba2a90e89714ee8ebcf6d5b78" + }, + { + "id": "54e576a4-fd49-5f2d-b33e-ff59a1efffeb", + "version": "MTc1NjMyNzg3OTk3OTI0NjAwMA", + "digest": "sha256:17969fa36c98f7692e0fcad0be4b6d5f2e34e9ace7d15825e1c26f4cc9a34abf" + }, + { + "id": "54e7c584-a362-5ad9-88d0-656e0f7a3ef0", + "version": "MTc1NjMyNzc3OTc5MTkzNjAwMA", + "digest": "sha256:2fc875787ab6753042e3acfd58be444ba37c2ec81ccded9cdbe72c8d148545b7" + }, + { + "id": "54e8b123-4049-5efe-9a99-62795cb3ae9f", + "version": "MTc1NjMyNzkwNDg2Mzk0NzAwMA", + "digest": "sha256:f9b8937ccc74f8a1aeddf498a99feb44ec030d288bb8402e18a61fd775971fe5" + }, + { + "id": "550e5a2a-5a87-55be-a8c1-6c519aea8235", + "version": "MTc1NjMyNzgzNTgyNjU4NjAwMA", + "digest": "sha256:0f6ab6766d5291dcf6264aa17da3b4fcd89c673028f5222c8e41581ff5e9be25" + }, + { + "id": "551eb274-4fd2-5d00-acfe-bb4ed595e8c3", + "version": "MTc1NjMyNzczODUwODI0NzAwMA", + "digest": "sha256:cad208b2fbe16e0674eca913a9472295fb1e08131ccd057ae20fa850db23bdd6" + }, + { + "id": "55283ac1-4877-5ae9-b48b-5627d90abb1e", + "version": "MTc1NjMyNzg2NzYxMDQwMTAwMA", + "digest": "sha256:1153dc799aa9450b34770fca8664b6f015d0796c77db7746fa1eff55c7c516f4" + }, + { + "id": "5531a27e-a3fd-5cba-9bf2-ac5b79c5f641", + "version": "MTc1NjMyODE2MzM2NTQxNDAwMA", + "digest": "sha256:24758bdc245058a68f8bc7fbf95b490fd550c0747bcb0770eb3fc0b3e3126549" + }, + { + "id": "55382e57-ffc7-5790-8084-4e073360f53d", + "version": "MTc1NjMyODE2Njk4MDQ2NDAwMA", + "digest": "sha256:1bb50e394499360f2e9971c7ed7afa4570e657e36b01e810758f2715f8c9f321" + }, + { + "id": "554e74c0-3779-55e3-bb50-c4c360e51ef1", + "version": "MTc1NjMyNzczODQ1MDQ3NDAwMA", + "digest": "sha256:e68cb6eb91c385feeddb50777683591a7e73411d083b22e4bc7455bf635f269e" + }, + { + "id": "55756bd9-9cb4-559f-8434-12107f8a70a5", + "version": "MTc1NjMyNzg4MDAxODAzNzAwMA", + "digest": "sha256:14b8bc146039d20362c609e0b26eb63ab40a3610ef4bc81ac762b06c54790716" + }, + { + "id": "559766f3-95c3-5f53-aba5-a29d0d418c15", + "version": "MTc1NjMyNzg2MDMzNjIwMzAwMA", + "digest": "sha256:4ddf1501260fd9ed5bb7280ebed3099c629a6e415db1cc68e8a37a1d13376ac8" + }, + { + "id": "55ab2363-3a29-5918-b5dd-426df886a078", + "version": "MTc1NjMyNzc3MTU0OTIyOTAwMA", + "digest": "sha256:87306ac339e1650e359c810a58477215427b1c34cff1df43446c3fde59f8a67c" + }, + { + "id": "56010563-3782-5d7e-ae2d-d64c8350014c", + "version": "MTc1NjMyNzgxMjc2MTQzMDAwMA", + "digest": "sha256:e3317c904fb99cb6f422be3962a5327ead02a12cd9debdb0e5bcecaac0a3562a" + }, + { + "id": "5617fff7-acb0-507c-aa2c-3c952763f919", + "version": "MTc1NjMyNzgxODgyNjE4MzAwMA", + "digest": "sha256:6887fb4775bb9746190b2eaf77aeb638912d1166c208f2f6c08dc32a917b6847" + }, + { + "id": "5620ad28-f4ed-5dc9-b11b-e664e92a6a31", + "version": "MTc1NjMyNzgxNjQzNTI0MzAwMA", + "digest": "sha256:e6fd0df20be0827f18ea39b5ffd14eaa3dfe08846711d3e3f64dffe2261fff8e" + }, + { + "id": "5621a301-50b8-5851-990c-0047555f1ea0", + "version": "MTc1NjMyNzg0Nzc1NjcwMTAwMA", + "digest": "sha256:d9ee8f0cb9ff8d600ef9f0059f46c35036ba7ff79bfaa99a7ff8bd7617cd7c50" + }, + { + "id": "5644db71-160b-5992-96ce-b65f8eb21ebd", + "version": "MTc1NjMyNzc0NTcyNTc0MDAwMA", + "digest": "sha256:667c84ae047dbb6e0584e65dac1c730e638706221a8d95071086f9cf5df61ac3" + }, + { + "id": "565d0338-2562-5ef5-b959-1bc78db8eba3", + "version": "MTc1NjMyODE2NDk2NzM3NjAwMA", + "digest": "sha256:4fbf5c0742064c8c277fd4917bcf263b5e81e5a274c10e1730ee566e4aa8b067" + }, + { + "id": "566b204e-b21a-54de-9d3d-3bc0fb1e8aea", + "version": "MTc1NjMyNzc0NzY5MTkwOTAwMA", + "digest": "sha256:5405555cf601025e6b072ceccb1ebe44359f3e2aca3b5beff20f9612bafcb168" + }, + { + "id": "5679c805-657e-541f-8392-eb002d415682", + "version": "MTc1NjMyNzc0OTk4NjM4NzAwMA", + "digest": "sha256:28d8db3632873dd18a9857ad7b41bef62c53b16608fe6cacf40858cf72043640" + }, + { + "id": "567d3eae-1f93-531b-9946-9018b773fdd3", + "version": "MTc1NjMyODE3MTIxNTg5NjAwMA", + "digest": "sha256:b5c9aa403d6b2749309c6246e08655d461feea8cdef1c1cc5a4c0c13bac43091" + }, + { + "id": "5686e860-59d5-5f37-a3aa-99308a33d6ef", + "version": "MTc1NjMyNzg2NjM1NTg4MjAwMA", + "digest": "sha256:077c016ed6c5e06ff805d1031e9f9e84f43a481aec44ce07660b45f977bd7eb1" + }, + { + "id": "5687ff8a-6ce4-5a76-b852-ed981c85844e", + "version": "MTc1NjMyNzc3MjQ3MDYyMjAwMA", + "digest": "sha256:6ba4b949edf1d3e7202eaa14f3f20c5265bb44a2b1aacd2478eafebdc82de713" + }, + { + "id": "568dc823-5594-56df-95eb-901a6e1ccde9", + "version": "MTc1NjMyNzg3MjAzNjk1MzAwMA", + "digest": "sha256:327f04255a73bd7eb7624c658a6a582e9b4c4e9ac984f670a833a018afc75535" + }, + { + "id": "56b1e8e5-f926-5d52-b27f-810603b7c937", + "version": "MTc1NjMyODE2MzAwOTc0NjAwMA", + "digest": "sha256:4d4f424fc674e9b5299bb936c5dda7688665c6d7e9ab3c6bc2ec6d8fbfecce9a" + }, + { + "id": "56b607f2-892e-50ee-b811-c3fedf8e8d8a", + "version": "MTc1NjMyODA3MzcyMzUxMjAwMA", + "digest": "sha256:5ea476dad86524ad0083532f753c7a7ededddb54544981630252d020e54e2c07" + }, + { + "id": "56dc924b-612e-5f5a-811b-1cd88078c7ad", + "version": "MTc1NjMyODE1ODE4OTUzMDAwMA", + "digest": "sha256:8f84809599b3be505415d2713088885906f111744a89729525ca8e7d5718878a" + }, + { + "id": "56ed79be-76c9-5074-abbd-ce7d96a0fb3a", + "version": "MTc1NjMyNzg1MzM3NjM2NjAwMA", + "digest": "sha256:caa68d7c880e300386388d16c0cea04bc8010594d42e75c1e99fc194ec0a2b08" + }, + { + "id": "56f9209e-6eb5-5869-8a73-f27d080d9f65", + "version": "MTc1NjMyNzc4MjAxMjgyNjAwMA", + "digest": "sha256:3f63c6cf4253c1ae1bf6438a399127c8d60bcd1c764a31e9bee0d8b42279398a" + }, + { + "id": "5706065c-4ed5-56c2-9487-0f0ea554b686", + "version": "MTc1NjMyODE3NzU0NDg1OTAwMA", + "digest": "sha256:57153d08e42eb77e40450bdd07094c147706326a0196d96d1cd0cc1918aab1da" + }, + { + "id": "572506fa-993a-5e8d-91d9-e3457c52b83b", + "version": "MTc1NjMyODE3NjIwNTUyMDAwMA", + "digest": "sha256:7017fc160ddf7e4343cbeeba129af164cbbfb04c853174b4c10e1ce95563d7bf" + }, + { + "id": "57285060-8c50-5448-9ff0-1af017cc0514", + "version": "MTc1NjMyNzczODQ4NzI0MTAwMA", + "digest": "sha256:c2d7f1764c582f17a339bac27786b27ae6f9fb7d5bb1388f37ca888a772eb258" + }, + { + "id": "572f0195-b6b7-555c-94f8-948d9335c2f5", + "version": "MTc1NjMyNzgwMzUyMDQyNjAwMA", + "digest": "sha256:1a42beca69fd63eacfa6c74c68174036d9492c72f2e361925d41d8518606735e" + }, + { + "id": "573e6dfc-9710-562c-adf2-e3aacbc1ca74", + "version": "MTc1NjMyNzg2NjczNjc0MTAwMA", + "digest": "sha256:4b64ae5d3aba42bc73fc4f1bd837614fa91a25aa19a83db2f52c3ec00a170cdb" + }, + { + "id": "5759026d-c92d-5e2f-afe9-7edf6f43014f", + "version": "MTc1NjMyNzc0NTc4MTM3MTAwMA", + "digest": "sha256:ac4ae1b59aac9587a2bba8777d03ddf936757ae5357d4e8be5e63c7514bf7aeb" + }, + { + "id": "57740e39-63d7-5561-b1d9-5a20fb8b1d25", + "version": "MTc1NjMyODE4MjE0MzA1OTAwMA", + "digest": "sha256:68219cc39f33e63de3f9d0a18289304ca2746bb13be43237f1a37c7b14c1ac03" + }, + { + "id": "577a1f76-3582-5ffa-9d0b-28211da5d7dd", + "version": "MTc1NjMyNzgxMTkxOTA1MTAwMA", + "digest": "sha256:b78373f000ae9ee6f1738fc842683172e487c4fff706f4e11b1a44dcfabe4931" + }, + { + "id": "5787e407-e389-53a9-bc45-05249532a9f8", + "version": "MTc1NjMyODE2NTkwMTk5MzAwMA", + "digest": "sha256:6efda777ebfde3ef3fc689c38e1d19665ca7af9089dd06950e01f971d18a7c03" + }, + { + "id": "5793f289-492f-5c31-bbb1-a317e38af990", + "version": "MTc1NjMyNzc3NTEwNjQwOTAwMA", + "digest": "sha256:a98eeb57ab2f17d53af4876b1f070be831754cd3018a66e76ff09aa726405cf0" + }, + { + "id": "57a159c5-d3c6-55b9-af92-2e40b3a13519", + "version": "MTc1NjMyNzg2NzY2NzI4NDAwMA", + "digest": "sha256:4df137198bf9e285ff5234442cdcee86e721a1be83384f88efbf1fa60eaac32b" + }, + { + "id": "57a4436a-d8be-5a87-9112-132b1a8b59df", + "version": "MTc1NjMyNzc1MzUyMDcwNTAwMA", + "digest": "sha256:b24930cd779fbafa25fd6b619f9e7ab2952bfb82b27c19a9a26ecdd792218229" + }, + { + "id": "57a94c52-7411-57e1-af1d-30d24af35d9d", + "version": "MTc1NjMyNzgxODg2MzIwMTAwMA", + "digest": "sha256:583adc3e7b201478c1e369621c2c6e32112a4d6f8f21fccd56610c3ca7449ec3" + }, + { + "id": "57b7af30-0751-59f5-bb17-fa037ddc75c8", + "version": "MTc1NjMyNzgxODg2OTc1MTAwMA", + "digest": "sha256:dbefde14af2434e2206dd21b2323160a79edf6467f02748f0da7bee30ffee63f" + }, + { + "id": "57d888ae-04a2-561c-b8b2-2756209e8f12", + "version": "MTc1NjMyNzg3NTU1NTgwODAwMA", + "digest": "sha256:0ca28fb805f68db05658d48197f40d305c443c7fc2e51787b93fcb10e65c982d" + }, + { + "id": "58030bfe-3540-5188-aebd-079db9a7514b", + "version": "MTc1NjMyNzg3NDUzNjI2NjAwMA", + "digest": "sha256:74995bc12b5c254e7eb6a70a95f80e8909aee7df519bebe91f0839862de7b1af" + }, + { + "id": "5825ab36-82d5-5871-a685-61de058b1121", + "version": "MTc1NjMyNzgwMzU5ODk1MjAwMA", + "digest": "sha256:a80120307d6ee494d867bb98bbb6446a53d9a8cab334b1054ee413a1a6033dbc" + }, + { + "id": "5830b43f-fd23-504d-8d52-856234c74b9e", + "version": "MTc1NjMyNzgzNTE1ODUwNzAwMA", + "digest": "sha256:6eab65e8f1542355436f9d99ab67d9a4b9a205850797cd05988b64be8bc5c1ad" + }, + { + "id": "583eabc1-d39b-5f37-95bc-9dd7cdd96878", + "version": "MTc1NjMyNzc5NTMwMTUzMzAwMA", + "digest": "sha256:4cb6b7a3f04252796c9a3464e248dbb9e7e7168b799195dec8115258cc0bd454" + }, + { + "id": "5855df55-afa6-5681-9d8f-7fd72446cc3b", + "version": "MTc1NjMyODE4MDQyOTIxMTAwMA", + "digest": "sha256:e96ac963e26555105b2e1b0392503549a898b8e77e59edf7720ae62d46c57d3c" + }, + { + "id": "585d2b65-6f93-5c2a-85e9-26569e516a19", + "version": "MTc1NjMyODA2NzA1NTY1NzAwMA", + "digest": "sha256:f5cad0b7992081bd59664b271bdcb36d9ae3e054c5d2d3252d2cddbeacf68213" + }, + { + "id": "5880e8c3-56a0-54f5-9807-1cdc913de9ef", + "version": "MTc1NjMyNzc1NjkxMDExNDAwMA", + "digest": "sha256:7f12ed67be67bf5bddd8543c4244ea6e3a3779c391a13d01a51dd9a55ccd3d61" + }, + { + "id": "5881a611-d356-5efd-b9b2-ace7d6676206", + "version": "MTc1NjMyNzc2NzczNDYyNDAwMA", + "digest": "sha256:2bb1c75e908755e151a8d0ffa4d82194e89916ab5f805c5a301e2cfdb710a373" + }, + { + "id": "58ad04fe-c9dd-5db8-8ddd-6026b19fec28", + "version": "MTc1NjMyODE4MjE0ODA3NzAwMA", + "digest": "sha256:b036c000f0ee1f4560648a2f88533e58cd23808abb68ddd9375273ebcd309cbc" + }, + { + "id": "58b17bca-1cc0-5cf0-907c-c80527700a1f", + "version": "MTc1NjMyODE3NzY2OTgyOTAwMA", + "digest": "sha256:89c2dcf88e4e8ae78b73fdf2b94cdfbe4a0ccb0ccc5dbedf1b103c66b350fb09" + }, + { + "id": "58b6f05f-237f-5c62-8c01-af6db78e191f", + "version": "MTc1NjMyNzkwNDYwOTg5OTAwMA", + "digest": "sha256:d2b4782986c6d530d15c9532661b7226285473ddc1b9e61906ff5e3df3f4ecac" + }, + { + "id": "58de9b3a-9ffc-5ae5-8dfc-38b66e7ec94a", + "version": "MTc1NjMyNzg2NzU1OTE3ODAwMA", + "digest": "sha256:e27eb09f2f346d5fd70a83a883a6a586d2837218a9a8480631fcfa0152533d8f" + }, + { + "id": "5905f2b6-be55-5b85-9ef9-08429c3400f3", + "version": "MTc1NjMyODE1NjM0MTQ1NDAwMA", + "digest": "sha256:3fc8dbe9076eb28f399355364a6c92a2fe57568516dbefe8c97b9e68398f1c03" + }, + { + "id": "592eea13-247a-50a3-87f3-88c46f8f0f5d", + "version": "MTc1NjMyODE1ODI1NTEwOTAwMA", + "digest": "sha256:f62ba000e702caca5002cf5e4b81fa96a4647fd07222ea7958bf385d456ec1fc" + }, + { + "id": "5939966b-567d-59c8-b446-db76c14050ab", + "version": "MTc1NjMyODE2Njk5MjUwNzAwMA", + "digest": "sha256:2aa156892815c70912e32e3159808164fcf178dfd6da6ec6ee242a149432a0f8" + }, + { + "id": "5949031c-21be-5717-bc33-08e9c67186e1", + "version": "MTc1NjMyNzc5NTE2MTY3NDAwMA", + "digest": "sha256:6efc824ca291d550c9cbad293c0a9c260309030d7013cb15f8fcde8b68254932" + }, + { + "id": "595144bc-e77e-5efd-8c94-e74f8c8e8673", + "version": "MTc1NjMyNzc3MDMxMzI3MjAwMA", + "digest": "sha256:2da0449f7e16aab717165102a0d30e37870b62f1c3ec519512f62050d7f778f0" + }, + { + "id": "59634916-f6f3-51fc-aed5-ae2289ca3892", + "version": "MTc1NjMyODE4MTk0NTMwMjAwMA", + "digest": "sha256:d3042628ecbe7271985bc04a4a8304bb82398b578ca586e550314018a40ff659" + }, + { + "id": "59739aeb-6745-5790-9f6d-6d3405aef6fa", + "version": "MTc1NjMyNzc2NjQ1NDUwMTAwMA", + "digest": "sha256:ead68a430f58bca6644098a30cd4d03a2ab6cc27fb38db38e964609815ad772a" + }, + { + "id": "59deaab9-8745-514d-a8af-5b4808731bf4", + "version": "MTc1NjMyNzc1NjkwOTU5MzAwMA", + "digest": "sha256:0f1869bf49f40e69d14e124c5dc5509d1b981a81c726a636787ae901fac0a072" + }, + { + "id": "59f05ab8-ff69-53c2-80cd-088d656bc0b3", + "version": "MTc1NjMyODA3MzA3MDAyMTAwMA", + "digest": "sha256:dbdb6bd7cbfb95afc4a6fa46529b07b07a042ad43f420553a27d89b68203b2c3" + }, + { + "id": "59fae964-c08d-5152-b5d6-c0685d5107b4", + "version": "MTc1NjMyNzg2MDIyNjM4NzAwMA", + "digest": "sha256:01666d177cdda26303b5cc50dbf3d78bc4594ffa330a0469d1215960698c97f9" + }, + { + "id": "5a1567ff-ca15-54c9-b9f7-6aacdb77c1ac", + "version": "MTc1NjMyNzc0NzY5ODE5NDAwMA", + "digest": "sha256:451170f94db8deb6e623add3d3581c886e273c5a5c2357df181131db3f4f4c39" + }, + { + "id": "5a190707-7539-56ed-9dd7-d4b935544300", + "version": "MTc1NjMyODE2OTk4NDI5MDAwMA", + "digest": "sha256:ab8d9d2e3d5ebb600bf41ac331c1e9e87c90ffd2d87dd6fe49379634eb8f5705" + }, + { + "id": "5a50d64a-daf4-50cb-a383-4db5c2192090", + "version": "MTc1NjMyNzkwOTE4NjY1NzAwMA", + "digest": "sha256:64a91c1de310706c55fda859bbd603a18f7f3aa458b8e0e716ae5c26f39a2035" + }, + { + "id": "5a608167-b156-557e-a81d-4ab6f99e834e", + "version": "MTc1NjMyNzg4NjU0MzcwNjAwMA", + "digest": "sha256:bd5e5549358cf7820fb2ed4c257d283dce5745777bb07f656c729e862e79a67e" + }, + { + "id": "5a6ca007-9255-5bfd-9f45-821bc20e77f3", + "version": "MTc1NjMyODA3MDc5MTQ0NzAwMA", + "digest": "sha256:2e5d8e4b6ad1f37a3e56cc6f6c0d2ab4aa08ab2b6e7d49c04503d63fc05bb009" + }, + { + "id": "5a81edf3-5d47-53ce-bed1-970f58167bb7", + "version": "MTc1NjMyODA2ODE0ODIyMTAwMA", + "digest": "sha256:0f5c9f2559ac8d1426e34f1e710fcc9bb8591bb9640810b144b292251c0c269a" + }, + { + "id": "5a906659-c877-5335-bf05-47b6de68d214", + "version": "MTc1NjMyODE4MTg5OTU0MTAwMA", + "digest": "sha256:fa3cade7a260b62265e21e34fb570a81e5d018ed2b542f26fa2292b49adbf173" + }, + { + "id": "5ab12b19-ada6-505e-adcb-831b045ba57e", + "version": "MTc1NjMyNzgwMjg1NDI0ODAwMA", + "digest": "sha256:e8ce40aca2506ee777645dc9cb273fd69a856f173130909c4ebfedf9b59a3e4d" + }, + { + "id": "5abfe557-fd34-575e-aa2c-28028e3d219c", + "version": "MTc1NjMyNzgxMjY4MzMxMDAwMA", + "digest": "sha256:8de525ce839cc8d6e04d4acf945805971a057d216a4ad722a0880fb4ab038741" + }, + { + "id": "5ac27495-7eab-5bbb-8b44-165b9947d12d", + "version": "MTc1NjMyNzg0MTIzNTIzOTAwMA", + "digest": "sha256:d5b6d150f7150c5a7b14f7f50248e1683cbc7e7da9090ed18189b6012f5bb8a5" + }, + { + "id": "5ad67a99-15d7-5e24-9391-f8a1f53268c1", + "version": "MTc1NjMyNzc5MDI2Mzc5NTAwMA", + "digest": "sha256:a6b3d275fecc35b34d721fd1774379de2ce6d80e7a775fe249fc38754c1941e5" + }, + { + "id": "5ade4fd4-551c-55c6-820f-8dd09f80fb20", + "version": "MTc1NjMyNzc1MDA1MDk3NDAwMA", + "digest": "sha256:42d4c0ed33d3fd738891643c4942b1009e1c9a62fd50acd66b8af9e8f4b3e23c" + }, + { + "id": "5af6f786-d9bd-5843-be62-d2420c209a87", + "version": "MTc1NjMyNzc5MDI2ODg0MzAwMA", + "digest": "sha256:c9004721209b12244f9d4dfadd7418244a8a7e09b5434ab3627a73a9a68179f7" + }, + { + "id": "5b156be9-9b1c-5693-9bd6-88a367e149a4", + "version": "MTc1NjMyNzg4NzMyNjQ3NzAwMA", + "digest": "sha256:813941a66f969fda4ccd07880fd9653d068fa379b59fd057790d79de0fc45d47" + }, + { + "id": "5b1709c1-c8c4-5001-bb7d-29e000810731", + "version": "MTc1NjMyNzc0NTcyNDA5MzAwMA", + "digest": "sha256:4c6f1871160bc7edb58fd28a901a9ede983a23ef9780e18aaaadb39db82b43ae" + }, + { + "id": "5b294ab7-4a4d-5f0b-9fbc-ca6461a31cd6", + "version": "MTc1NjMyNzk0MjI1NTkyMjAwMA", + "digest": "sha256:fdd387381b19170241b701557db74ce55b573fef7513070333bfdc6e10118305" + }, + { + "id": "5b3585a8-c25a-5b22-910c-2a589c92da33", + "version": "MTc1NjMyNzc3OTc5MTkyODAwMA", + "digest": "sha256:6a3c6c770bad251c8acf5eb3f2502040f86880e10bca7c760234cd21552f7f7e" + }, + { + "id": "5b66ddb5-62ef-5f53-a27e-ad7502ad84c7", + "version": "MTc1NjMyNzg4NjYyOTY0MTAwMA", + "digest": "sha256:4776d9e6d5aa915a0fb0127c7c0d079a4b39fd9b748e4900525ac0fe74ccb816" + }, + { + "id": "5b6984aa-6bf4-57e9-a220-cf87b2c369f7", + "version": "MTc1NjMyNzc1NDA5MTkzMzAwMA", + "digest": "sha256:fc9990f850a1bd84650888147f7e1ee0d8805b7d3c30114c105e2991a527b88f" + }, + { + "id": "5b86a8d4-0b1f-58ca-a0a4-ccbc5144944b", + "version": "MTc1NjMyNzg2NjY0MjQxNDAwMA", + "digest": "sha256:7872d8df2a35f645c088b3def2f28370c3ad39bcb5a0d6ab6d57d49a3b83106d" + }, + { + "id": "5b9497f2-0820-5ef6-a9e1-282e82c79700", + "version": "MTc1NjMyNzg2NjI0NzY5MjAwMA", + "digest": "sha256:d775556d91ee0aa4a8db618f58dea298f56f8109e6166788fffb41ee9e0be7c3" + }, + { + "id": "5bb57dd6-0a54-5c41-8b71-5a0aa71ecd8d", + "version": "MTc1NjMyNzc0NzUzNjg5MTAwMA", + "digest": "sha256:ef19a6efe35672c725851419903ec272346786ac4206e8b4e55ee4f1f478a756" + }, + { + "id": "5bcd8d73-a296-5516-a0ad-8d2fe50570bb", + "version": "MTc1NjMyNzc2NjQ4NzY0OTAwMA", + "digest": "sha256:8eb11e5e66e31196bc9dea52bca402fec66dd12c2529ed09103d489fd9be2cb1" + }, + { + "id": "5bd257b5-52af-5be7-8cba-422cd1f4f6c5", + "version": "MTc1NjMyNzgwNDU3ODYyNzAwMA", + "digest": "sha256:422ad4a134347cfff977c589b5f24cc25d02a1a959aca06293311f7193d8bbb5" + }, + { + "id": "5c13ad33-feab-5804-bd99-d02fa0c54879", + "version": "MTc1NjMyNzg3NjY3NTMxNzAwMA", + "digest": "sha256:01b5457dcfdf50e4e03554dd336621e37dbbe5ebfbd92d8f61e036dfb88fde70" + }, + { + "id": "5c14a791-766f-52d4-8bed-787887df4cd3", + "version": "MTc1NjMyNzc0NTAyNzg3NTAwMA", + "digest": "sha256:1e464474f2444d408afffa772f302e88195f85b7ec019fa527ce260dc2bae16f" + }, + { + "id": "5c19fca9-3b2a-5074-ab52-4d07111401c7", + "version": "MTc1NjMyODE2NDU2ODE5NTAwMA", + "digest": "sha256:434ac897bb429b52200db85ab1776153c236db8357e1d947ce689e0617de672d" + }, + { + "id": "5c337fcf-05f7-50fc-8274-bf15c26e15ad", + "version": "MTc1NjMyODE3MDAwNjA2OTAwMA", + "digest": "sha256:af55ec6c825d0560656e1481be6f8370c7713a598ce85e35c9e3c7b3a3b3c71b" + }, + { + "id": "5c3b6258-6ea6-5b39-8806-4effc215be6a", + "version": "MTc1NjMyNzg3OTkyNzk3MDAwMA", + "digest": "sha256:9c72c20282f41c332df4bc2ca2b324c567f1d674b1d8c385d46dd27dc480a8e9" + }, + { + "id": "5c41a7c9-43a8-577d-bc88-1244d7d3168c", + "version": "MTc1NjMyNzgwMjkxMTY3NzAwMA", + "digest": "sha256:2053411a92edcfc05b8b559773e76fa345c533ad3d1512233f3a6599685bc631" + }, + { + "id": "5ca3c7ba-ac39-56bb-b6f2-bdac19ca5f0c", + "version": "MTc1NjMyNzg2NzYzMjE1ODAwMA", + "digest": "sha256:635ac1402a0254456b418962fe6b8a53066237d45d2a485f4e28b63b5aeff964" + }, + { + "id": "5cb640e1-5e56-575a-979c-328bf23b9a63", + "version": "MTc1NjMyNzg1MjExNzc3NDAwMA", + "digest": "sha256:e0bd6628b440fe40b6ee8e91c32073abbe6d1e1453dc68f818502e7254c02514" + }, + { + "id": "5cd9f2cd-e278-5671-9281-aedf3c128229", + "version": "MTc1NjMyNzgwMjk2ODI3NjAwMA", + "digest": "sha256:7b2df2287e25e4072bbce6d9ea7f4b0cbe590329fe3db1cd233950a29ee71636" + }, + { + "id": "5cf21aff-cc9d-52cc-9747-cba977049d00", + "version": "MTc1NjMyNzc3MjkyMzA2MzAwMA", + "digest": "sha256:3728114649aeeaed46c9c6220fa48a70ccbab3be24802c0ce1ddc065fde11f8d" + }, + { + "id": "5cf32163-b696-5204-b60c-97f8dde71192", + "version": "MTc1NjMyODE2ODkyMzg1NzAwMA", + "digest": "sha256:39d26dbd6d648956d89dc6189582953de70aaee8fa8e231ab6c6457a9a3c008b" + }, + { + "id": "5cf5f692-9b96-53be-80b7-69439318b665", + "version": "MTc1NjMyNzg1MzM2MDI5NzAwMA", + "digest": "sha256:33bd32183b10056e146c7a04e082d77ff1afb5d55b7b835d97c70c7af862bb7e" + }, + { + "id": "5d0274aa-15ea-5495-97b0-58c1c9b7756c", + "version": "MTc1NjMyODE2OTAyMjA1OTAwMA", + "digest": "sha256:e89d6569ada0f80ae763afa19b31c42ce29a693dc377d28311f2d234e855fa2f" + }, + { + "id": "5d13e26d-4a60-51b6-941e-127d1ddb76a5", + "version": "MTc1NjMyNzc0NTcyNTE0NDAwMA", + "digest": "sha256:800a8cbe2764ab8ffae6c381daf1d3695f657bb42b9b010c7e9faf32c739d9f9" + }, + { + "id": "5d1a6a3e-52b5-5e5e-939b-c4cb17ad5d1c", + "version": "MTc1NjMyODE3NDM2MDExOTAwMA", + "digest": "sha256:04685a251461f0fb3a1bba308f84186462abf174aa98c7725d03e08deb1429b9" + }, + { + "id": "5d2fabaf-7063-5961-a86f-5cc45a607aaf", + "version": "MTc1NjMyNzgxMzUzMDYxNzAwMA", + "digest": "sha256:42005b24676388c8b99e07bc0efff220fabe86e84c08661946bde5bbdc9f3c2d" + }, + { + "id": "5d46d9e5-e7d7-58fa-a15c-e7f8f438a405", + "version": "MTc1NjMyNzc0OTkxNjM0OTAwMA", + "digest": "sha256:49b9e7e2af4169b8d765ea38beedf71ea1aebfe030da2ccb9b0c110826c56a35" + }, + { + "id": "5d59dd00-e6ce-5cd9-a276-696ce9d5c3e3", + "version": "MTc1NjMyNzg1MjEyNDcyNzAwMA", + "digest": "sha256:605b20f0343278ac0e921803f7fe85b76d9ad90f5a573c8116227573aeb59754" + }, + { + "id": "5d5dd795-91ba-5af5-b8af-95f8d9b6975a", + "version": "MTc1NjMyODE2ODkwMTgyNjAwMA", + "digest": "sha256:a204344720d3f26a2ef8ccf7c3480356c13d5e600aba9d811a6300ad50638dd8" + }, + { + "id": "5d606e37-4abf-54a4-bdd5-6e9b8fc2a1ad", + "version": "MTc1NjMyNzc0NzcyNjA1MzAwMA", + "digest": "sha256:be808081cae3abc57e5c3de8229935545703e7c01bfdccaaa3b16bfaf5b931d1" + }, + { + "id": "5d6b9cd0-f595-5f9f-a6e5-9e50d59d0fbc", + "version": "MTc1NjMyODE1NjMwMzQwMDAwMA", + "digest": "sha256:bfea2e0db3b89638795d7e306ed0f299dfce46bf7b706a9971a2beaf3e8bddab" + }, + { + "id": "5d76fc5c-23c6-53a9-8c46-dae9714ee76f", + "version": "MTc1NjMyODA3MTIzNjI4ODAwMA", + "digest": "sha256:c74e8e5702b161802a828b7ae487a0f5385a63c5be23546c2d3d3e961856c4d4" + }, + { + "id": "5d78d6a6-c0a8-556e-8c96-de9000d3e7d6", + "version": "MTc1NjMyNzc3MDM3ODU1NzAwMA", + "digest": "sha256:c3e360209bdcb21e01e5bb80b1f2145b0818f8b5e33aa4207973273b961de118" + }, + { + "id": "5d7be2d7-0bdb-51ad-997f-8b85d8a797d7", + "version": "MTc1NjMyODA2OTc1MzcxNzAwMA", + "digest": "sha256:3d97251fb5dd5201e52c89593f1f213f0927a24a8081753f7a48411bb3ba6bd7" + }, + { + "id": "5d7ccd0b-7900-5acb-8be4-eb91c2cc7dd1", + "version": "MTc1NjMyNzg1MzIxNDk5MzAwMA", + "digest": "sha256:99ef3f52a412760e428491c5b52053efcab88942855c7891c65c6a350e8e758f" + }, + { + "id": "5d7d562c-255c-534e-9e6b-b4d924f85653", + "version": "MTc1NjMyNzg2MDI1NzUwODAwMA", + "digest": "sha256:d463393c6dae267abd816b58f0c400d33b286eed2f7c4bd800f379cccd764594" + }, + { + "id": "5d8a02f4-0649-5088-8cb3-633dbe61db69", + "version": "MTc1NjMyNzg3MDY4Mzg4MzAwMA", + "digest": "sha256:2115dc8ebf0925c355b958b1d6b6a582473f087ff5e3d2e90a09a9fae85a5a8a" + }, + { + "id": "5d8de287-94f6-561b-8b5b-707d01f170ad", + "version": "MTc1NjMyODA3ODAxMDg3MzAwMA", + "digest": "sha256:67818828d3146497c8173781243ae76c267eb4696dac9d975255e91b5b0ca9fb" + }, + { + "id": "5d91d9fb-1e07-5c2b-96f6-6b615375e1c3", + "version": "MTc1NjMyODE2NjkwMDQxMDAwMA", + "digest": "sha256:ccd22b1978914afe5eceab0fee9aabdd3df47db0945a916be180768438839b67" + }, + { + "id": "5d98f0f8-6d53-5130-a3ec-1707b59a30ae", + "version": "MTc1NjMyNzg3NjcxODUxMzAwMA", + "digest": "sha256:c69a6b8244ddec3662a6f54034f4acf2c38d031c4e94e127b44d651bffa4f54e" + }, + { + "id": "5de9153c-3c0e-5ffa-ad8a-3fd889c24f83", + "version": "MTc1NjMyNzc3MDMzODY5NjAwMA", + "digest": "sha256:faebbda8c71f06d1972f1539f747308d1afe26fbe7d0468091bd3720035828fe" + }, + { + "id": "5dfd1a6a-ae4a-58a6-8a11-8e418d865839", + "version": "MTc1NjMyNzg4MjA3MDEwMjAwMA", + "digest": "sha256:5710e6f20d3c945bc53d6484ab0263ea09af3f27cc4be0306dcbb8290c52977f" + }, + { + "id": "5e0a4e3e-b537-5362-82f7-aa38ab986682", + "version": "MTc1NjMyNzc5MDI3OTQ0MDAwMA", + "digest": "sha256:633c164c3d74b3c3e096855369ae0f0a86e7b620b997d3741b4daf3120ac8b5f" + }, + { + "id": "5e3e2c70-2b51-5821-ad56-a080bfc09e3c", + "version": "MTc1NjMyNzgyNjMxNzAyNTAwMA", + "digest": "sha256:1cb9ebd4bf1283731da1f03088e89f2fb5e9c4deb612f36fe6e1282704b8bd34" + }, + { + "id": "5e5553de-76fa-5f54-bb14-f658dbae031d", + "version": "MTc1NjMyODE2ODk0MjMyODAwMA", + "digest": "sha256:2cbf505a234c1818d1816561e7c2691b609cea80df286fc9c6dd34cde1a425df" + }, + { + "id": "5e63ba94-2c53-5d40-8145-3b1568a1a70a", + "version": "MTc1NjMyODA3MzUwMTY0MTAwMA", + "digest": "sha256:08eb6aa61523407f4dcdce310adbc0c504195cf4658b9c0303b165a9d9b3b0b1" + }, + { + "id": "5e7c7715-f49f-5d81-9adb-e95f023fe893", + "version": "MTc1NjMyNzkxMjgwMjI1MjAwMA", + "digest": "sha256:5347962b53312d67d307343b3d7f68fb9f886f3478e8f85f3c0153df22565e87" + }, + { + "id": "5eadf892-3e70-51c7-83b7-9d3f692e6dc7", + "version": "MTc1NjMyODE2Mjk4Mjk5OTAwMA", + "digest": "sha256:2f3ef58fb84a90231b193381b574f9dd12a9293a3b8785ad64039690a1180a26" + }, + { + "id": "5eb1fd96-2f0a-5a2f-bdf6-edad362d9c32", + "version": "MTc1NjMyODE4MjA2MjU2ODAwMA", + "digest": "sha256:b55e6c1dd53c15668b829093901f57c2036a57211eb50a2f852c2d46b3344d2f" + }, + { + "id": "5eb4419e-e558-5876-a7ad-bda9e6050fc2", + "version": "MTc1NjMyNzc4NjkxNTQ2NTAwMA", + "digest": "sha256:c55c6db58830f0c4d8b5f5079460f12d5a207374ea15181c781af0be97bf87f5" + }, + { + "id": "5ebd86de-6a35-5873-90c0-2ce7482e2ead", + "version": "MTc1NjMyODE3NDMyODMzMjAwMA", + "digest": "sha256:3d5da2261c5a0aaca7a31610e46bf02179d8b8eab54020c575bcbdf2bf96e692" + }, + { + "id": "5ec1e6c0-d1db-5c2e-bcc0-b496e7bc23c3", + "version": "MTc1NjMyNzg4NzM2ODkyMzAwMA", + "digest": "sha256:d70bfd7793c07d25f93cd6882bd4ac196ed7ec33dd8b0df5b3a095b3e37eea8b" + }, + { + "id": "5edb0645-941f-5a29-b9cd-54824944b161", + "version": "MTc1NjMyNzg4MDAwMTE4NzAwMA", + "digest": "sha256:9282006c63cb493bd89c971bb7eac0c170ea71a774bc919bd746a486b4aa1fbe" + }, + { + "id": "5ee89c4d-5429-57bb-96df-1a68e213c04a", + "version": "MTc1NjMyNzg1MzM2NzUwMDAwMA", + "digest": "sha256:e692e993717cf82e5ef5d2e9c66cf3298e1a57be4e902dfa8d5ff974fe5d9a59" + }, + { + "id": "5ee9302e-2ab9-56b3-aea7-ca6b080f5601", + "version": "MTc1NjMyNzc3MjUwNTAzMzAwMA", + "digest": "sha256:16f98be4caae67fe0f1acf6100e23423a537cf78d4d4d61f1b2a3f535b62785a" + }, + { + "id": "5ee9de52-f97a-57f6-8131-55b6a9b40284", + "version": "MTc1NjMyNzc0MjU1NzU4ODAwMA", + "digest": "sha256:7ce5475ed3aa88cf2169c19514f0c565d0047bf80fa24860bc82b378d387a934" + }, + { + "id": "5f0c4e8f-634e-5af7-bd9a-37eb4010a013", + "version": "MTc1NjMyNzg2NzU3Njg3ODAwMA", + "digest": "sha256:7e9040c087e15ecbf4f7d1628719b4a31e6bf87593f08888353707a34a789a4a" + }, + { + "id": "5f11ea2b-6942-5449-bd24-9a9d5a82243e", + "version": "MTc1NjMyNzc2NzY2MTI0MDAwMA", + "digest": "sha256:f9e43bdf752df751c5168f386b71c8235ee1fb5a6128315cfd9381d23ec52719" + }, + { + "id": "5f1c4187-b09f-5eab-a61c-b3ea5596b24e", + "version": "MTc1NjMyNzc2NjI3NTc2NDAwMA", + "digest": "sha256:8b38192b0f9fc6512cff7f4fc02877382984e651f8850cc2f5aafb6b9957da64" + }, + { + "id": "5f4ccf21-c02e-5845-982c-94deaf80f3a8", + "version": "MTc1NjMyNzc1NzU5MTc2MzAwMA", + "digest": "sha256:7be9d7ce514fcec4f51efb98bcb32c20958833d1a18ea7370a06f61fb476195a" + }, + { + "id": "5f576a6a-bf0f-5793-8b95-06b92e11807d", + "version": "MTc1NjMyNzc3NTI1NjIxMTAwMA", + "digest": "sha256:96eb0659ef936f5d280136f203819e6ddd255409ec518f4060ecc205e128d18a" + }, + { + "id": "5f5f5272-418c-5fe8-937b-2399ae4019f4", + "version": "MTc1NjMyODE2NDkzMTY3NzAwMA", + "digest": "sha256:1ad64d55304822e291334ab73f8c86f7980116fa7392d970583b2af98d03bce6" + }, + { + "id": "5f6f1b26-f3e4-54a3-8109-aff5609fd534", + "version": "MTc1NjMyNzc5MDI4NTY2NjAwMA", + "digest": "sha256:a7d664733b7d365e19f513dcfcd8781d253de10978558916b1596479e8b80a0e" + }, + { + "id": "5f78d488-90ce-592e-a031-77de4879065e", + "version": "MTc1NjMyODE2ODkzMDYzNjAwMA", + "digest": "sha256:72502cd5bc93c4140e686035303ac773bf46e57ad1f45a40a80d95fbca431867" + }, + { + "id": "5f91c1fc-be8d-5a03-b9c3-aa7ab8ce483e", + "version": "MTc1NjMyNzg2NzU3MDUxNTAwMA", + "digest": "sha256:ce2848471185f668ed79530a0e708fd9ef667cfe826d759f85db50782d427609" + }, + { + "id": "5fa9329e-b8de-555c-b842-af3ee2c9fd36", + "version": "MTc1NjMyNzc2NjQ1OTkxODAwMA", + "digest": "sha256:a9f710955302bcb360fae89b727a3aff9bb54dd22f2b12274b07d3974ae1ca8c" + }, + { + "id": "5fb098f6-c20a-5cbb-b5a1-8523d6e592f3", + "version": "MTc1NjMyNzg4MTg4Njc3OTAwMA", + "digest": "sha256:25e8dcf1d37523191c414be3476169c2b65aecae101883b24e2ee66ae27ae196" + }, + { + "id": "5fb0c56a-9ea4-5d82-8499-52dc904dab37", + "version": "MTc1NjMyNzg3OTk1NTM1MzAwMA", + "digest": "sha256:6e6989ec993ed673cb475cab558eb59d5c89cb53488beb3b5aff10ab2c4579d3" + }, + { + "id": "5fb13c43-77f7-5bb2-a36f-f1a99a3a937d", + "version": "MTc1NjMyODA2NzQxMTMzMDAwMA", + "digest": "sha256:9a1cd15deb64491eef2e04d02e0af3972cfd07b3ee9c0b8c2ab7c7b112d3c044" + }, + { + "id": "5fb659be-f3a9-5285-b943-afe9197a8717", + "version": "MTc1NjMyODA3Mjc2NTYyNTAwMA", + "digest": "sha256:7713e4f50114925f02b0f6888b2fe0b0b8924b26f5067c74103200f01c019701" + }, + { + "id": "5fbbdd26-0233-5bbf-a4f4-b8721059b857", + "version": "MTc1NjMyNzc2Mzc2Mjg3NzAwMA", + "digest": "sha256:ecaa9303e6725c765449e388405415d6a60aee2ccc7dd5ccb8fdd0f229e735c3" + }, + { + "id": "5fc16ff6-2653-5046-acf6-98e14b8aa8a9", + "version": "MTc1NjMyNzc5NDc0OTgzNTAwMA", + "digest": "sha256:f2434deee478880865efa98d89549e8f40bfb9669d40d978518f95876397fb78" + }, + { + "id": "5fc73a69-f1a2-5281-9b26-7650d7478513", + "version": "MTc1NjMyNzg3MTUxNDEwMTAwMA", + "digest": "sha256:a9f557ac0478d728065f8e42a21c54cbe8f06284472987c9c84170ec544675fb" + }, + { + "id": "5fd7f8d6-7d70-5e25-b7c3-d418bcfd999c", + "version": "MTc1NjMyNzc5NTI4MTcwMTAwMA", + "digest": "sha256:9bf5eab0a9db9db8103b83d20f8e68562a7bb6ab5776e82b95760f9d997988de" + }, + { + "id": "5fdef541-aadd-516f-8286-d753002a81ea", + "version": "MTc1NjMyNzg3NDkyODE4NjAwMA", + "digest": "sha256:abc23b417440e249ddbc5083baab0fa84abe70ad732e6273f357ffb38897d23d" + }, + { + "id": "5fe20ccc-146c-5a73-ad16-5b1326fea7b4", + "version": "MTc1NjMyNzg1MzIxNjMxNDAwMA", + "digest": "sha256:c1385befa1d4407f3b5580305cd90644d9d3e6137a1b4ee19c26ec93bfc7e71d" + }, + { + "id": "5fe52490-85eb-50b7-9a2b-cd273d6db8e7", + "version": "MTc1NjMyODE2MzExOTcxODAwMA", + "digest": "sha256:f3ab54efbc7d5cd4f54e968b10f1a259722d1eafbba8230163dc153fae66b7f9" + }, + { + "id": "5fefd4e1-60d9-524d-8422-af09f17e7d2a", + "version": "MTc1NjMyNzgyMjg3NTM3MzAwMA", + "digest": "sha256:b2f3231636e66813e475d98b30e330a6de2a7488d2bae5a01ad0c87d731b0169" + }, + { + "id": "601eccd5-ca4a-52a8-abbb-b549d22f56c8", + "version": "MTc1NjMyNzgxODk4MTU5MzAwMA", + "digest": "sha256:7651b07dce5c2556deeeae56da940c61280a3c4407a525d7e5430c1dd9c1a613" + }, + { + "id": "6032f176-d966-5699-90c6-18a15617f455", + "version": "MTc1NjMyNzg2NjI4MTA3NjAwMA", + "digest": "sha256:fade3026d46c215c194448cc4c626cf5a3f434dfcd1636508cb52d86f0b10721" + }, + { + "id": "6033ed39-ecbb-54a3-acd1-2af21d766a73", + "version": "MTc1NjMyNzc5NDY2MDYyMjAwMA", + "digest": "sha256:d8a30ff5695fb2021f8b777dacc87641922b5c47a5eb51f9aadaaebe689249aa" + }, + { + "id": "60485f31-47c6-537d-a175-3d1c143a8fe0", + "version": "MTc1NjMyNzg0MzAyMDYxNDAwMA", + "digest": "sha256:e94fcfb9f7e6302954456db9fd1452fc39f2ede91327a589a77eddee36e4cccb" + }, + { + "id": "60486186-29a2-5c62-8241-b550a380b110", + "version": "MTc1NjMyNzg2OTUwMDg4NDAwMA", + "digest": "sha256:def87f77ca8112edc777b7f4763a8f553d6ce487411edeb8ce542708076ee419" + }, + { + "id": "607418a6-4d98-5af0-aa70-5454e11335e3", + "version": "MTc1NjMyNzg4MjA3NTI1MjAwMA", + "digest": "sha256:b8bef247e3d388d1e8281ebfac39130b11357da77b1b4ef3906d09d9028e5fec" + }, + { + "id": "60761ae1-5459-545b-905a-577b282e3648", + "version": "MTc1NjMyNzc5NjA4OTc5NzAwMA", + "digest": "sha256:05bebb452068672ea9890828231cc356b89103e6801e04390e665a804ba8c450" + }, + { + "id": "608de77c-d85f-58e7-b322-4a5c2049d624", + "version": "MTc1NjMyODA3MjIxNzM1ODAwMA", + "digest": "sha256:28bf707dfa0051acb295fdb6a06a185ce076a034309de9012ed6db424cea85ab" + }, + { + "id": "60a44806-c068-5581-b484-a606c06e354f", + "version": "MTc1NjMyNzc0MjM3MzM0MTAwMA", + "digest": "sha256:00020b1238fb3af6c35c235d026faaa0a2774c42912a00f9035d80a3abf1e6c6" + }, + { + "id": "60c88f16-37de-5ea7-a97d-3a79cb2aa7de", + "version": "MTc1NjMyODE3NzYxNjEzODAwMA", + "digest": "sha256:1bb989b56815d6cad415b63e9b59c82ac37eec6a63f4c0065e38ee7ff86c0a17" + }, + { + "id": "60dae0d9-ab7d-5835-b80c-9f60a245c78d", + "version": "MTc1NjMyNzgxNTYyOTQ4ODAwMA", + "digest": "sha256:7e6d80fe1c0559981ed1d6086ef3f98cd7e504ff133c01a919f311f3cc952f44" + }, + { + "id": "60dfadb4-9e27-5b37-8a5e-19bef387c6df", + "version": "MTc1NjMyNzg3NDg4NzgzNzAwMA", + "digest": "sha256:b0dc1fa270a7237f7bee01b575f20c492dbdc19e65bdf15102ba7816f327bc96" + }, + { + "id": "60eae57c-ec72-52f9-b09a-bc6890b98826", + "version": "MTc1NjMyNzkzMjU2MDE0NDAwMA", + "digest": "sha256:32487a4360eef96016796989208823d4200e792b8c43f472631313ca29f0166c" + }, + { + "id": "60f49a6c-274a-52ca-9fc1-1b899231adbc", + "version": "MTc1NjMyNzgxNTUxNTU4MjAwMA", + "digest": "sha256:d306972b85b2bb799c859c0f3ce32243536b30dc59a2db1902dc4db3b0c3ad6f" + }, + { + "id": "6108ad81-d0df-5562-bf9d-5bd0c01ae9c3", + "version": "MTc1NjMyNzc2NjQ1NjQyNDAwMA", + "digest": "sha256:54f19237b74819c3b689284d6904a9a73af1824a482c9c7ee31f9d4f15f84f30" + }, + { + "id": "61103d8a-b88d-51c5-9e25-cdd32158d96a", + "version": "MTc1NjMyNzgzNTczMzU4NDAwMA", + "digest": "sha256:4f873681387d87dd870f1655e768d15d5ead3b62087145479cf8fbbc16025bf8" + }, + { + "id": "612bec4e-bfae-597b-b0d7-7662b4151b04", + "version": "MTc1NjMyNzc4NzM2MDY0MzAwMA", + "digest": "sha256:860259538bc66b02afc9b9e7daab4cd006a575db9e219be223adf5b63fa43c35" + }, + { + "id": "6146335d-5e1e-5815-b0ca-556aaa11a885", + "version": "MTc1NjMyNzgxNjQ0ODE5ODAwMA", + "digest": "sha256:d25bca3cfd9fb72aba6091557b9a5cd189189a833722a480faf425be11b5dee7" + }, + { + "id": "617a0dca-21c9-5ffb-9e30-08e354e60c17", + "version": "MTc1NjMyODE3NDQzNDc1NjAwMA", + "digest": "sha256:af80e79f4af57d172eb2ade35ee2b5e24e0c4ba97cee33927d398fd770c543f7" + }, + { + "id": "61a0f79a-e1e8-59e1-8b5f-2d4967435528", + "version": "MTc1NjMyNzg1Mzc5NTI1MjAwMA", + "digest": "sha256:d857ab57f83ea3b7cc59128bdba06260948a1cff83e649aca6a5f7188cc76b00" + }, + { + "id": "61c42aa3-153d-575d-8734-7bf6651a636f", + "version": "MTc1NjMyNzc1MDA0MzAyNTAwMA", + "digest": "sha256:5decdbc4013d17dde60da664f2b60eb4f75ab3522162606cd7a22fc5a22e1782" + }, + { + "id": "61d751ea-683c-59c3-a6ac-9dd4a3294603", + "version": "MTc1NjMyODE4MTgzMDgxODAwMA", + "digest": "sha256:08e867a1c091e9f1bf62ddc74031233a4cbb215e50734c19138e2ab18d6a9986" + }, + { + "id": "61ef9825-4040-55a0-951f-0660eddce44d", + "version": "MTc1NjMyODE2ODg5NDkxMzAwMA", + "digest": "sha256:5802b3242eb6d32d8f5be7e35aa0849f2c8b022f9ab8cec5bbbb8ef5b71c4444" + }, + { + "id": "61f624a1-547a-5837-8051-302bc22100f0", + "version": "MTc1NjMyNzgwMjkxMTA5NTAwMA", + "digest": "sha256:e0ad8791383fe680d774484e614ecaa1e65b412fa0af8b12f11f24afbdc9229c" + }, + { + "id": "620d1788-7cf0-58a9-a447-5847852fb7f0", + "version": "MTc1NjMyNzc4Njc5ODYwNDAwMA", + "digest": "sha256:3142abe834d106f9631d6fb859be9af31dd4362b9f0fed8de64445bb003c12bc" + }, + { + "id": "620d43e0-a7cf-5763-9f2f-f5245ea5b8de", + "version": "MTc1NjMyNzc2NjI3NTkwODAwMA", + "digest": "sha256:7a2819451d1bf3a4e38351053cb3d27d5880e80159464da17b6da9e7cda1efff" + }, + { + "id": "6217cb31-bf22-54fe-9e40-1782b8dfad0c", + "version": "MTc1NjMyNzc4NjkzMjM2OTAwMA", + "digest": "sha256:584914bce397d2d32f5a68ac4ec27ba82f80a59e3ce41fadff89e67602fa1462" + }, + { + "id": "621da679-a44d-5ba2-990e-b801a3b4f859", + "version": "MTc1NjMyNzg1NTczNTc5MzAwMA", + "digest": "sha256:5e7d4f73425355a81ae428ca8a4998785032bee51833b21d78bcf2de832a6692" + }, + { + "id": "621e7d3d-795c-5dbb-afdb-3f342489c965", + "version": "MTc1NjMyNzc1NjA4NDkwODAwMA", + "digest": "sha256:6321622d3f2815650b68463819dc692298f96d14583fdfb9df153e4dc84196be" + }, + { + "id": "622944dc-cdf0-58d1-bcb5-553e88f30b06", + "version": "MTc1NjMyNzc1NjA4NjA5MjAwMA", + "digest": "sha256:804142fc2544373eb93159076786023519eb437b16370d49e732f0fc1e537ef7" + }, + { + "id": "6229b939-8c1b-5f08-b019-12fbaa6a3151", + "version": "MTc1NjMyNzc5MDMwODA1NTAwMA", + "digest": "sha256:a02ceb27a61d4bbb71d4e07fa1bcc3a3a90fec7fd28475dea5258a27f90dca49" + }, + { + "id": "6247bf2b-89c2-53e5-ae29-87dc3ee0dc54", + "version": "MTc1NjMyODE3MTExNjk2NzAwMA", + "digest": "sha256:dbf8ae485ac2ea17dba46413e76a9da7c041640db908ae82b79ffbcde83fbc95" + }, + { + "id": "62526fdb-cb34-5dd0-8193-0c05b19e71c6", + "version": "MTc1NjMyNzgwMjk3MzkyMjAwMA", + "digest": "sha256:d675a96235c1bc46e30da3192af620ac126f11d6e829adb8d02d48bb0fbf4669" + }, + { + "id": "625321b3-ca4c-52af-b288-b80823de97a0", + "version": "MTc1NjMyNzgzODgxMDMyODAwMA", + "digest": "sha256:ad2ea3a11230f30f588adaf8438a5fbe1e50038d3c3be8453079eca5dd17f60f" + }, + { + "id": "62568a99-c458-53f2-bfbe-1ac40bcd72c7", + "version": "MTc1NjMyNzc4MjcxODMyMzAwMA", + "digest": "sha256:447c8f4ec8de9fb55b9a7398443214f0f892aabcbbdb6103b7151ecf0bfafbf4" + }, + { + "id": "6275aa76-46f3-59ad-beec-a583f13c82aa", + "version": "MTc1NjMyNzg3NjU4NDQxNDAwMA", + "digest": "sha256:d151dde4a6d2a5e4c3b6ad3122d346432af80bb60a17688682fb35a712b19aef" + }, + { + "id": "6282184c-1f36-5970-9697-888872f5ebb6", + "version": "MTc1NjMyODE4MDQzNTE1NDAwMA", + "digest": "sha256:69f7f504c45f798abb74845a08d1218a1f4574e4f3a18edd10583310f46299d6" + }, + { + "id": "629711e8-0aae-56db-8325-09fd3a2e3f4a", + "version": "MTc1NjMyODE2NTg2OTYzMzAwMA", + "digest": "sha256:313ae28e6522a0c6026b674f6f0b6c7788c915752cd0c7bde1a70fbfd8af2399" + }, + { + "id": "629b38b4-8197-5503-a646-226a68f84fd2", + "version": "MTc1NjMyODE4MjE4NDY4MzAwMA", + "digest": "sha256:73c2ef44afed5084b9183b697d70a19bea6ee63dedbea1923ab8724b81a251a3" + }, + { + "id": "62bf8a46-01f7-530a-b25a-306ec374c412", + "version": "MTc1NjMyNzg1MzgwMTI4MjAwMA", + "digest": "sha256:5b67d34b8405ad28cd3511ee113dfeabf55c15efc945b777bbf8ae43b65a21a0" + }, + { + "id": "62c12e8d-28cb-5a00-8b79-178aa076f00c", + "version": "MTc1NjMyNzc4NzMwMTkyMjAwMA", + "digest": "sha256:ebd4041c1473a93dfcd6fac88f14436085321cc7bba7738499b60883882fa26c" + }, + { + "id": "62d416bd-8bc9-518c-bbbc-7802e5172a1c", + "version": "MTc1NjMyODE4MjA5NDM2MTAwMA", + "digest": "sha256:1d0bdba0282f3dd08aef847a2fce6c66c8ae5ea924a7ce0814b0b242a1203dce" + }, + { + "id": "62dc6291-5594-580d-aca8-152faa55b342", + "version": "MTc1NjMyNzgwMjkwNjI0MzAwMA", + "digest": "sha256:1833bdb401c8006e7841c7438170f81c50ee2ccb04b55451c37210b0211259ec" + }, + { + "id": "62e9c98a-ed05-5ab0-86eb-e78afb544849", + "version": "MTc1NjMyNzc1MzY1OTkxNTAwMA", + "digest": "sha256:a95856f24328fd99e4608db35f8af6687d853c547d28a1e3650c5d63d9f0bfe0" + }, + { + "id": "62f8360e-68e4-59fc-9912-e1ae312e76db", + "version": "MTc1NjMyODE3ODA3NDc4ODAwMA", + "digest": "sha256:56e17fa0164a8a9c7ade9cabafe1010fb33e9549aa1b223e6452417f326fc119" + }, + { + "id": "62fc1cab-914f-5224-ac1b-e1b065409bf6", + "version": "MTc1NjMyODE4MTg4MzM2NzAwMA", + "digest": "sha256:c1de7fb04be958ae5e00a4dacf33e02b1b45dc93199fdffa6d69ff7484280b22" + }, + { + "id": "6308fb34-018a-5f12-b3a6-311b401071d0", + "version": "MTc1NjMyODE4MTg0ODY2OTAwMA", + "digest": "sha256:ff86a734ee1fc049b9cbddbbde0c3947f2a9d74720da92088eca5a7765e2e396" + }, + { + "id": "6320cba2-6259-5084-9068-5478a5506213", + "version": "MTc1NjMyNzc5NjA1MDEyNjAwMA", + "digest": "sha256:90b45c1306fae7bc60ed6ad52624cb3f591c40f3c2bf234f4bdbf555380b0a74" + }, + { + "id": "63509977-82b5-5c65-b5dd-90395b27a6d2", + "version": "MTc1NjMyNzgxNTY0NDUyMDAwMA", + "digest": "sha256:a376e0216b9b1b7dbad104e03b5e1e3370cb2822dad852cb52e59112bd79e023" + }, + { + "id": "636ddb11-283f-5ede-a0d1-687b69507c6a", + "version": "MTc1NjMyODE3Njc4NTY5MTAwMA", + "digest": "sha256:418b18b15cfc1329d3ee63c0701c268acf387d56a17a779b9845d768f63dc26d" + }, + { + "id": "638b7473-5f31-5f6f-a2ea-0168d4616255", + "version": "MTc1NjMyNzgzNjQwMTgwMDAwMA", + "digest": "sha256:15a753e90298c6814468f54731b3fcec5b35c4081d308b407863925eba1bcc6b" + }, + { + "id": "639ffe8a-ff46-5e74-81a1-b964b74420f0", + "version": "MTc1NjMyNzc5NTMwNzA1MzAwMA", + "digest": "sha256:1178adf7f207acb2a1baa4057bf2bf26ae40f63f04d8f4e96f6189f571cd89d6" + }, + { + "id": "63a59b17-f36b-5386-9b96-a8f843389351", + "version": "MTc1NjMyNzc5MDMwODg3MzAwMA", + "digest": "sha256:ae103d879dc7e346d37886a69af496953bff8253e35b423a52dd8945cb232780" + }, + { + "id": "63aa7eeb-0dcb-513f-a3bd-43445c382e36", + "version": "MTc1NjMyODE1NjM0MjI1ODAwMA", + "digest": "sha256:37605df747d68b05b5d72211e16721a6d52de5e1754057d37ac09d1afc1d13c9" + }, + { + "id": "63ad88a0-98d2-5b40-bc28-03671d0f37eb", + "version": "MTc1NjMyODE4MDQwNTQyNDAwMA", + "digest": "sha256:f69cd95ce4425e34af98a06a64a0ca5b7301ce5a6e9eb99778ec199bb3e64a71" + }, + { + "id": "63c597d7-e0e5-57cf-b65e-68350ee932fe", + "version": "MTc1NjMyODE3NjE4NTYyMzAwMA", + "digest": "sha256:cce596318609caae4b1d22570c7b978d9331d6b4ebde1e76fbac1eeaf72c57a9" + }, + { + "id": "63cd0aa5-e492-5b48-886b-2bad27640322", + "version": "MTc1NjMyNzg3MTk3NDgwNDAwMA", + "digest": "sha256:d6f199ab10c524005ca9275bd4a67a2569b25ff938c8e99f6cdde0c20013708b" + }, + { + "id": "63d5fff0-e85b-5a62-8340-71cdb8f6794b", + "version": "MTc1NjMyODE3ODA5NTE4NzAwMA", + "digest": "sha256:bddffaad45804172f6aff1a93c2d2aef3e073193ee8b1b7715191163476ab20c" + }, + { + "id": "63dc3cb1-48bf-578e-8f32-fc653ddd5e1a", + "version": "MTc1NjMyNzc2MzgzNzQ2NzAwMA", + "digest": "sha256:b8e210abc2068d44aae73f59707cdf38bbaf8b24c967237e15206974f8a183c7" + }, + { + "id": "63f70749-b2bc-5c6c-be22-55293e55c890", + "version": "MTc1NjMyNzc0NzcyNTkxMTAwMA", + "digest": "sha256:8704cfa638b8978e5ec293a35bd8d48ac66dacc1865a53cf77f643718cd90f40" + }, + { + "id": "6402c320-8c1a-527b-9873-9517b5fbfefd", + "version": "MTc1NjMyNzc1NzYyNTY5NjAwMA", + "digest": "sha256:f9cb68f31752429556bba477c0d115d43e6e4058bd32b07bf996906eb9f5b80e" + }, + { + "id": "64244880-9a70-56fd-ab31-88b92bae1f94", + "version": "MTc1NjMyNzc0NDQ4NjA2NTAwMA", + "digest": "sha256:37e71eb0809da67ff49866f73f346974120cdf2d9845b07653b67f22ddf27aad" + }, + { + "id": "6430e7f8-ee70-545c-85d0-49a5cf5fcb41", + "version": "MTc1NjMyNzc3MjU4ODE2MTAwMA", + "digest": "sha256:d8df402b111729befc712c81754d46744906534c91d2cefd22a89832305c62f9" + }, + { + "id": "643f5ee5-7504-5646-8285-89d38e3ca180", + "version": "MTc1NjMyNzg3MTYxMDQ3NzAwMA", + "digest": "sha256:6859bb1780da423752fcfb04b67129b01f059c3e9b821d9a54ac686a1fc626ec" + }, + { + "id": "6456ff34-1806-5996-bd98-9b780c37afc0", + "version": "MTc1NjMyODA3NTI4NjA3MTAwMA", + "digest": "sha256:e7780ced73bbb5b2603ce5f15d74a4d75a2000a556c4f37650bebc6be5a329b6" + }, + { + "id": "6475ea5d-4917-5ce0-98d0-585f39199e58", + "version": "MTc1NjMyNzc3OTc1MzY3MDAwMA", + "digest": "sha256:71094b966a4a76ca8b6f135fa1385a2a85361d023bbfa2590426fe1d7e32bd00" + }, + { + "id": "6499756a-2693-51fe-b871-fda9f77a7dc8", + "version": "MTc1NjMyNzc3MDYzNTg0OTAwMA", + "digest": "sha256:d93d3f967203df6ca7ff658518cab6e4ab028eadbde2d204ea02e1a92ecb6dcc" + }, + { + "id": "64b80a8f-c40d-5f3f-9ae9-14fc034c093e", + "version": "MTc1NjMyNzg1MzIzMjA3MjAwMA", + "digest": "sha256:5f56c200370b10b60c6a085d811662b74ff6fc5b76f68446319752257ec36226" + }, + { + "id": "64d1b374-6b78-51f8-9de3-e361353ce15a", + "version": "MTc1NjMyNzc3MDQ1MDIzMDAwMA", + "digest": "sha256:ce5808d78d589ac7dd64736816b8a15d5e7d5d28e07d2bbcb651d383d9b2b774" + }, + { + "id": "64df0fe3-95a4-57c2-a228-28bc3e0ca946", + "version": "MTc1NjMyNzg1MjY3NzY5MzAwMA", + "digest": "sha256:49967816342d9bf7bc851a175a799ce27a039d41905b05182b9df04fea353efb" + }, + { + "id": "64f98625-b5f6-56ff-97b8-e6b162b0bf8d", + "version": "MTc1NjMyNzgzNTIwMzk3MzAwMA", + "digest": "sha256:a393cf04a4b059870ea26d15011ba942d042ee91af95daa2257a63a1f50e22c2" + }, + { + "id": "65040555-7e9b-5d6e-b370-ee8d1a85d431", + "version": "MTc1NjMyNzc0MjM3MTkxMzAwMA", + "digest": "sha256:2548e39a27316208c49fc975a3e2311a86773f1a1c9876adca38bc454a0f40f7" + }, + { + "id": "65097d5f-1968-5d51-be2a-dc0acb4bf20a", + "version": "MTc1NjMyNzc2MzcwNTk1MjAwMA", + "digest": "sha256:56ee32710d2da27ac3ae68cc7c34e354c3c1ac81bbb66b44de02d1aa2ff2c687" + }, + { + "id": "6513b365-bc5a-5106-93ca-3bc7de894aaa", + "version": "MTc1NjMyNzc1NjA2OTE5MTAwMA", + "digest": "sha256:82d1fd79c11387c0afc866b9226fd92b8d48a39cb91414be98bff1142b6f4ad5" + }, + { + "id": "65184f6f-962c-5326-ba5a-59ae630c27a2", + "version": "MTc1NjMyODE2OTAxMTA4NjAwMA", + "digest": "sha256:9c02218417c8c48661749f3d3ecf16a200ea707ef782b4f5fa6c517f03794fdd" + }, + { + "id": "651affce-c48c-5a02-a480-f004750ca7e0", + "version": "MTc1NjMyODE2ODg5NTg5MDAwMA", + "digest": "sha256:fe6c87dba721473ab183009b50804d4d7be3a50f5f34856a89758cfbf95ac972" + }, + { + "id": "65201e60-b345-5dd3-8387-d22c382150a8", + "version": "MTc1NjMyNzgzNjMwODQ4NzAwMA", + "digest": "sha256:04d995f966ac237e6bc85c980ba6e6086d9e73fafc7fb79a8d0be06ddb5e9ac8" + }, + { + "id": "6523e884-7ee5-5a4f-bd30-bf1b296de748", + "version": "MTc1NjMyODE3Nzk5MTY4NDAwMA", + "digest": "sha256:fd778ab26b4d907808e37af49205ce54fe8fa6473a675044444708f1e1321742" + }, + { + "id": "6528aefc-dbaa-54e6-a12c-2faacd6ed43a", + "version": "MTc1NjMyNzgzNjM5MDcwNjAwMA", + "digest": "sha256:0cd8f2ee876a05b58933916edf69e43953b377289ae01dd012a4cb0d0dcf6ad1" + }, + { + "id": "652ad450-dc0f-5034-afae-e5b76aefcc87", + "version": "MTc1NjMyNzc2MjM3NDE4NzAwMA", + "digest": "sha256:d2e4adcd4d9f86e025b6f30e5d54e474dc523580dbc3079a05cc1de281d970ba" + }, + { + "id": "65364779-6cc4-51c8-af67-7f3cf1e36850", + "version": "MTc1NjMyNzgwNDQ0OTY1NDAwMA", + "digest": "sha256:c1cb4fd9f58aff1d2f762163f396e576cfd507bff46aaa312ee3c410f784511b" + }, + { + "id": "653ee845-c326-5029-b231-f2826816bf9a", + "version": "MTc1NjMyNzc3Mjk3NjEzOTAwMA", + "digest": "sha256:1566880a9c292854c6d8f72af9929c521fd63f8e234f9c50b6dd4ebc4ae0d725" + }, + { + "id": "654c3b15-5ede-53ec-a877-61ae044ae10d", + "version": "MTc1NjMyNzc0OTkyMTY4NTAwMA", + "digest": "sha256:f002ad3dc08303d46b125bec77e22113f5ac7c49e1e2cc584c2f41c0cabfab41" + }, + { + "id": "657e5c9b-f591-5433-9a5a-dd8e653b0406", + "version": "MTc1NjMyODA2OTMxMjg2MTAwMA", + "digest": "sha256:9417460912ecdc08ea3567434efe9e1db3a5346cb149c4770f2d447f8aa3c841" + }, + { + "id": "65a1c6b4-bc19-5028-b1dc-ff4592adaee6", + "version": "MTc1NjMyNzc1MzYzOTg5MTAwMA", + "digest": "sha256:c5cb813a63511664a53ab74bd6734015b69500a87de02a29b42ee4f10a3aaabf" + }, + { + "id": "65a50c67-1efa-58a2-9a6a-150bc8b55ed6", + "version": "MTc1NjMyNzc3OTkyMTk0ODAwMA", + "digest": "sha256:41e6390cc32a7cd56a21acf1e73efc3a33519297ca84041b03b1d44817ed6170" + }, + { + "id": "65b2f22c-394f-5d24-bbd2-5d94849dffc0", + "version": "MTc1NjMyODE2NDUzOTY4MzAwMA", + "digest": "sha256:d76258b717ed4ff08a5f061478076bf2fb277734ff8d605b90a828e4e0c4f003" + }, + { + "id": "65bdeb0d-34a6-5175-8fec-c044832dc651", + "version": "MTc1NjMyNzc4NzM2NTQyMjAwMA", + "digest": "sha256:be50a90c35ae0e850795591772d50a34ab842515f2a9dece1706ee13e9e44d9d" + }, + { + "id": "65be3cc2-f283-54b7-b8b2-a0ef9c0f89b4", + "version": "MTc1NjMyNzgzODgzMzY2NzAwMA", + "digest": "sha256:250aa75fed41a57ea404e6bbf26fc720064dfba29607746a4416eed22d1e8379" + }, + { + "id": "65bf141b-dbb6-5a50-be9c-8a622d0a0ca0", + "version": "MTc1NjMyNzg0NzcxMTI4ODAwMA", + "digest": "sha256:1d726728d0e2157560ae224408d41dd5a990516a7678878be2526edd24aad686" + }, + { + "id": "65c7413a-78c8-5a23-8147-75bc532a1bf9", + "version": "MTc1NjMyNzc0NDM5NzcyNDAwMA", + "digest": "sha256:966a1621fe4f9b01d3ea239cedd1148589cd2161f28d26c29071b7eb1ac28b2f" + }, + { + "id": "65f3e11f-0e51-5648-b566-f126924edc6b", + "version": "MTc1NjMyNzc5MDMwODU0MTAwMA", + "digest": "sha256:2dfeecc1f1c2d06f0d5564c006a1dd7350ee825f94258ce9fae8d79771e6d35c" + }, + { + "id": "66093da9-9978-5b83-9510-eabad0e2fd46", + "version": "MTc1NjMyODE4MDQ3Nzg1OTAwMA", + "digest": "sha256:ac180726edf52717c8c3c7a963c54252cc0a5ef04bdd3fa21c6fc681fb2af26a" + }, + { + "id": "660d0b04-258b-5e5f-b144-3c9533fe33fa", + "version": "MTc1NjMyNzc0OTkxNzY0ODAwMA", + "digest": "sha256:f852f29a279bf25a107fda845d52221a0edd6aff03bd6a66b92f4ab2e3f661b8" + }, + { + "id": "660d1fe6-519f-5ed6-a183-89b1f6e26096", + "version": "MTc1NjMyNzc2Nzc0MjIxOTAwMA", + "digest": "sha256:0531ac099102806286bdc8d8470c3241f1943af4336b952a53e7a63414670a8c" + }, + { + "id": "6615150c-3325-57e0-9cdb-5af43d2e807a", + "version": "MTc1NjMyNzc2Mzc3NDE3MTAwMA", + "digest": "sha256:67acb6d54fe2254bca0f1a56c9b2c78e624da43493718ef7c790ef4b71ecce8d" + }, + { + "id": "6621aeee-db92-5872-a2ef-0703eaf35b6d", + "version": "MTc1NjMyODE3Njc3OTEzNzAwMA", + "digest": "sha256:c8093d7b6407cffffc2f2aab0b8aef569345b52c0b17e013e1927ac7c7624bd0" + }, + { + "id": "662240c9-29ee-52f1-855c-a0dfd35640ec", + "version": "MTc1NjMyNzkwODY1ODIxMzAwMA", + "digest": "sha256:8944e69f36e0b8846e6ad960bdfd0aff86441ae725364b3e022dc7d7611dac35" + }, + { + "id": "662d8a88-6146-5fdc-b0bb-f604864d6283", + "version": "MTc1NjMyODA3MDg3MzIyMjAwMA", + "digest": "sha256:733b5e8a5ba46a74fe60a1b241aac16129bd4a7db1517f3dad4d0e971c855a9a" + }, + { + "id": "6633ba13-3bb6-5c93-9b50-19de7261ad0f", + "version": "MTc1NjMyNzc3MDQ4OTQ5OTAwMA", + "digest": "sha256:2721d74bf0ed3e4c078be8b7cdea1d173edaceed5ecf0cbcc224648d3046f930" + }, + { + "id": "66b11c5a-0cdd-52cb-9745-6fdb3dbbea80", + "version": "MTc1NjMyODA3NzQxMTkxODAwMA", + "digest": "sha256:8aa73a4874ed5b6cd3e1e5747d116cb06ba065f6606a70d817b41045f03d1c7b" + }, + { + "id": "66b43ab8-1184-5b6d-8e49-d88d9c2b6e15", + "version": "MTc1NjMyODA2OTMyMjE2ODAwMA", + "digest": "sha256:454fd6609ed9860882a4868e9b2b884565bfd668a01e05266835605e7c08c63b" + }, + { + "id": "66b7a5ac-46f9-50f4-81a8-d7abb84fc5cd", + "version": "MTc1NjMyODE2OTk1MDQ5ODAwMA", + "digest": "sha256:a7f89b662d89db473cfc043f4089fc061cda2acc3e6f11f73163c9d7034f7c01" + }, + { + "id": "66c723ac-22b0-5b95-a5c1-64d93757e725", + "version": "MTc1NjMyNzc1NjkwOTcwNzAwMA", + "digest": "sha256:e03b20ad9d1aa41101181acb0245af50741efd3794cf0ff186c0896bb914ba7b" + }, + { + "id": "66c9b834-9c4f-5de2-9a95-37cd58028462", + "version": "MTc1NjMyNzg3NTUwODQ0NjAwMA", + "digest": "sha256:5ea9cf0b4da15fe2e5c6fcc21481b9c5f5dfd9743bb50e7c84ccb98fb50ef478" + }, + { + "id": "66f523c3-ca03-5277-bc88-7d4fdd7cccb0", + "version": "MTc1NjMyNzc4NzQwMzE2MjAwMA", + "digest": "sha256:67ee5a43b6af05e32d7c52811d8b98ea0dbdf76811ba55f2b03bbd0867e50bb9" + }, + { + "id": "66fd48ba-9b55-50f8-ac71-9035287a5358", + "version": "MTc1NjMyNzc4MTk0MTQyNDAwMA", + "digest": "sha256:a7d401ca6c6ff7811982a9c5f14ca7e0e448a41bfec5aba81dbc5288394ffd01" + }, + { + "id": "6700795e-38ba-5f1f-8c13-631def4dce4a", + "version": "MTc1NjMyODE3MTE1MTA4NTAwMA", + "digest": "sha256:44dc31749afa1a826255a4e77cf96ad96d8130f6ac14c0611c6e8cc00d5612bc" + }, + { + "id": "673186d8-c00e-5b36-afd8-b87b20f70093", + "version": "MTc1NjMyNzgwMzY2MDY4MzAwMA", + "digest": "sha256:5006067698dfffff0ee0ada818af0beb6dd559f4bc9269ee8e775c4192a43ece" + }, + { + "id": "674a0105-19dd-580f-94b3-7ef91c70e043", + "version": "MTc1NjMyNzgzODcxOTY3MzAwMA", + "digest": "sha256:66b86de7c999960a865ee7b0a959009901d18823a52b7521b39b1bb2f650da33" + }, + { + "id": "6767e226-1a9f-5880-bde6-512f4d3c9a9e", + "version": "MTc1NjMyNzc0NTc1OTQ3NjAwMA", + "digest": "sha256:c5d7ddef45752d8c6cc449ba633a3a1373f2712d5f1e3e71569c8f57f83547d9" + }, + { + "id": "677a8693-a730-58f2-84b1-7ffe750f2490", + "version": "MTc1NjMyNzg3MDM1ODcwNDAwMA", + "digest": "sha256:5b4ff7378424c1c7b5ee50560cf249c31fdab9b9cdaebfb91e0d145049ac4f8e" + }, + { + "id": "6781ef7b-28d1-5a31-a6d1-f42b00985d82", + "version": "MTc1NjMyNzc4NzQxNTUxMzAwMA", + "digest": "sha256:4c7ac01cf45dd19a9972c76e5b85c948726f555f067ce3e30073a8199982a61b" + }, + { + "id": "678f8f1f-630f-551f-875e-ec56a6adea1a", + "version": "MTc1NjMyODE4MDQ2Mzg0MDAwMA", + "digest": "sha256:01ccf705d23a4bb0001120e692797952710e013d51c257b5f5845bf48f485977" + }, + { + "id": "679e2184-a47c-5d77-ba6f-ede76828ae0d", + "version": "MTc1NjMyNzc3OTc3NjkwNjAwMA", + "digest": "sha256:f22fabafbc288c65150c7c60157c5944901f19f1ee2ae6787a4657e44727397d" + }, + { + "id": "67a873fc-2aa4-5cf4-b9ed-412c50e9fb2b", + "version": "MTc1NjMyODE1NjM0MjY5NzAwMA", + "digest": "sha256:edb91f3e64e234b2abed855b79d94fe6f13e0f3b22693f2e15166504635cc135" + }, + { + "id": "67c91505-82ef-5ba2-a12e-02d1f436d7a4", + "version": "MTc1NjMyNzgyMjc2OTIwMTAwMA", + "digest": "sha256:9aa2c1b5fc1881e14bf1414765d02915b2c6dbbf25ff6ef78cb251d7cdb0a1de" + }, + { + "id": "68065a6a-2cbd-5c12-b9c6-f36ad716b1eb", + "version": "MTc1NjMyNzc3Mjk4NzI4MzAwMA", + "digest": "sha256:2d8d5b1858983a08c99933c058c1b99f276e8637556bbec0bc7e544da524c4c5" + }, + { + "id": "68066816-e10e-57cc-abc8-ffcc59a47573", + "version": "MTc1NjMyNzc0MjQ2MjcwOTAwMA", + "digest": "sha256:5c3f6654a699298be1371f5c4e46003a5e90569294c52409f02d9f7fb87a0d5f" + }, + { + "id": "6814a326-8852-5231-830b-692430761908", + "version": "MTc1NjMyNzg2NjY2MTYwNTAwMA", + "digest": "sha256:d4b307f3fe15806d276a3a11fc6348d4643e8b9364e8383bf84e41b355cc6f2d" + }, + { + "id": "68170b61-ec83-5a26-bf2c-8c716f5a2a61", + "version": "MTc1NjMyODA3NTk3MDUzOTAwMA", + "digest": "sha256:66adaf6068f87ce64524db733e86dc15c172b1d506da95ba540d2ad56a6cccdd" + }, + { + "id": "6825a8a8-8afa-5e27-8775-5e2c49e8a891", + "version": "MTc1NjMyNzkxMDg5MTQzNjAwMA", + "digest": "sha256:cb36f17e4aa938dacc8e4b76d021a4ed3c30829474e6a58b4fba57a485478c2a" + }, + { + "id": "686089e8-80ca-50f7-8a4d-25a185752ac1", + "version": "MTc1NjMyODE2Njk1ODkzMjAwMA", + "digest": "sha256:3ed2c954de876b12f46a8988b848da849723c0b0ca078f95a2f91e735fafb9b6" + }, + { + "id": "68679066-c839-54e3-914d-0df2bb931b77", + "version": "MTc1NjMyODE4MjEyODg4NDAwMA", + "digest": "sha256:100966e39b784d76dd6f07915c17a331494b24daa6de9664fbcd417907d8b1ca" + }, + { + "id": "686b2ef7-1af6-5e51-ad8f-dd6b0ccb8524", + "version": "MTc1NjMyNzc0MjMzMDU1MTAwMA", + "digest": "sha256:e2fc2c5e718e67546e7298f1e13ce0c0e3885c24e24cf2c7cd03cddea5f878c0" + }, + { + "id": "6877d994-2094-536e-989e-56a64b64960b", + "version": "MTc1NjMyNzg1MzM2ODI4OTAwMA", + "digest": "sha256:ad5ae54ab929461d3e83353c5aec3ba5a98c83934bf23a425256f762fc255328" + }, + { + "id": "68788363-1452-5de4-90b2-60674ee32b81", + "version": "MTc1NjMyNzg1NDM3MzU5MDAwMA", + "digest": "sha256:b3a2096cc363ccb3a4eb5f37ef7fbda8ab8f8e1791f28570a6cfdf6b1315d180" + }, + { + "id": "68793366-f1d5-5f04-9d39-dcaac38681d9", + "version": "MTc1NjMyNzg0MzA3ODc4MTAwMA", + "digest": "sha256:924b2bff0d1ec6921ed435278b88d25ab08a5235674a44c8b184053c2657c16c" + }, + { + "id": "687e27be-687f-5d16-9784-4ed6b2408cf8", + "version": "MTc1NjMyNzgyMjQ0MzE0ODAwMA", + "digest": "sha256:7512f9d87b38303a46e1dd589700c404cdadb000171892a0ebf05f839070f6f1" + }, + { + "id": "6889f1c7-8477-5a50-9f23-b47330b4a9b7", + "version": "MTc1NjMyNzc1MDE4MjgyNDAwMA", + "digest": "sha256:d48506551005efb55dc4e6c092059bf79a6271a76c439837d2fc032a4e4e9614" + }, + { + "id": "688b00da-84a6-500e-baba-4ed1d06816d1", + "version": "MTc1NjMyNzg1MzM2MTAzMjAwMA", + "digest": "sha256:d4d7ce5d4191a72c5456a112d8f8b47ca9109f5b36a88c742eda8d7e9015c1f7" + }, + { + "id": "6897a4fc-b539-5353-8586-13282f5959b7", + "version": "MTc1NjMyNzc4Mjc2NDAzMzAwMA", + "digest": "sha256:dd01c9ca878a0cfa2da9405f224ff08a3438d5d628f9ebafd7c947ba22a748a5" + }, + { + "id": "68b7f5c6-9973-5a37-8675-e55b4f2a51c1", + "version": "MTc1NjMyNzc3Mjk4NjY2MTAwMA", + "digest": "sha256:4680c060ca1516b1c2c62e53a3ddf17faac77616cbf4360980752e72b9fe1718" + }, + { + "id": "68bf0d16-fccc-5199-b22c-b2285dfe0c91", + "version": "MTc1NjMyNzg3NDU1MjA5OTAwMA", + "digest": "sha256:5fcc81848cdd5bc46f61a446e6a42683efa1ab37a5d171c2e1b6f2c108e869df" + }, + { + "id": "690dcfd9-a8b4-5dcc-a27e-3e9797a5f355", + "version": "MTc1NjMyNzc4NzM1NDMwNDAwMA", + "digest": "sha256:ce921095aa4bd863e1414dbe62654e8d3e0dda01e88d38f81b51ea85fc0b96bf" + }, + { + "id": "6920064e-8ffc-559e-b294-1db0186e53ee", + "version": "MTc1NjMyNzc1NDAwNzQxMTAwMA", + "digest": "sha256:31cde31718b9342476cc31e16a0d2c466709e1df43af78a70adc47d1a9707be3" + }, + { + "id": "6929080d-2f88-5d86-8ec2-bfcb1d631f8d", + "version": "MTc1NjMyNzc1NzQ3NzM4ODAwMA", + "digest": "sha256:df9ed17820aca501fcb4fe0bc3c8f9d7e8b730c13d7234357e3e970f1c3d898c" + }, + { + "id": "6935dc47-dad4-50b0-9ad2-abfad6032280", + "version": "MTc1NjMyNzg1NDMwMTY1MDAwMA", + "digest": "sha256:7b25e8e839966c0e424a3ca2ff9d24e2b5a1d521d595c11ffc167a53caec6072" + }, + { + "id": "6936a079-2592-52ac-8047-eac9e07b5f1c", + "version": "MTc1NjMyODE2NDQ5NzczMzAwMA", + "digest": "sha256:14532bd1d8bf6bcc51766ba4dc6d43f13c77b3ee707b7e898fed8a91ef8d7870" + }, + { + "id": "69373ddb-87aa-5d4a-ba51-622577fe8526", + "version": "MTc1NjMyNzgzNTc1NjQ4MTAwMA", + "digest": "sha256:93bd381b3f0ced34d67231d7114772bcfbfa1cd4221957eeeb550fd4f7767603" + }, + { + "id": "69382403-9eed-51fd-9a74-f81639ce6e81", + "version": "MTc1NjMyNzgzNjQwMTE5MzAwMA", + "digest": "sha256:b6837d11da10558db2b6adf41d1c29a3a48fde8826e9af6bd312cb3848d3a883" + }, + { + "id": "69447425-453c-541b-ad21-496e9831dc86", + "version": "MTc1NjMyNzc4NzI4OTMyMjAwMA", + "digest": "sha256:667dab9f3d1bbaee43e275c2be9989895bff3640bc9105aab0f45daccabbe238" + }, + { + "id": "6969d031-b14a-5c02-8ea7-ec15b33307b8", + "version": "MTc1NjMyNzg2NzY2NzI5NjAwMA", + "digest": "sha256:0f995c75c64a832c71d4328348b7011075ee5ee7cbe7107dc3ca2ecdbf7140d2" + }, + { + "id": "696ba569-632c-5d8e-bbc9-c45a4f108b11", + "version": "MTc1NjMyNzgyMjQ0NDA4NzAwMA", + "digest": "sha256:a5d2a78c908e1860e9b552d70d2851c09de89da4b1ebdd565a9dbeafac276566" + }, + { + "id": "697c57e4-defb-5717-bfc2-ffc76e65077d", + "version": "MTc1NjMyODE4MDQ1MDk4MjAwMA", + "digest": "sha256:04e9a7e9a97157996bd10ddec4074326e4b222e18ab0debb8690bde15eacb95c" + }, + { + "id": "69beca50-ced7-558f-9f3f-5b9ea95aecbc", + "version": "MTc1NjMyNzc1NzQ0MjI5MTAwMA", + "digest": "sha256:79c701f85c60870dafc5b1e649ccefd43a5b34d49bbf8e8efe2e90baae204de2" + }, + { + "id": "69cd9cd4-eb26-5c17-8c10-9b0641af921c", + "version": "MTc1NjMyODA2NzM1ODQyNDAwMA", + "digest": "sha256:ed28a2848b3caa6de830541b59758a5678573fe7a02eeaa7d350bd124639ac4b" + }, + { + "id": "6a0e42b2-ecf9-5b9e-9eb4-542505d8dbd5", + "version": "MTc1NjMyODE2NDQ4NjQ0NDAwMA", + "digest": "sha256:e8b71c49ac39c19ee69b04cce3bf1fd5e4cd1d2b315ebd2fb23d240d8c5a63b1" + }, + { + "id": "6a11af4e-3541-57b5-ae63-e3c46d440c2a", + "version": "MTc1NjMyNzc3MDYwOTU0NzAwMA", + "digest": "sha256:da3fa887929f3fd12672a60249e11c01c5a1e96ba625a564178cbf087c338f37" + }, + { + "id": "6a3a9d52-2a3c-53ac-a310-002f6630e57a", + "version": "MTc1NjMyNzc4Njc4MTE4NTAwMA", + "digest": "sha256:44beb7fadc353bc2a0800a14097059be7447214c705f7dda59788b9a8d43cfc3" + }, + { + "id": "6a4f7bf1-9d6b-5e2e-ae37-699e24b09185", + "version": "MTc1NjMyODE3MDAxMDU4NzAwMA", + "digest": "sha256:f5961520090f05e4ec4c1975ff401e770265aa953539360aa62271d809388f1e" + }, + { + "id": "6a50a850-da83-5090-a7cf-3669ee6293f6", + "version": "MTc1NjMyNzc5NTI2OTcxNTAwMA", + "digest": "sha256:a6391265576884079f04275e09e387f7b0b6af4a5cf6ab6bd61218ac1bd86c6d" + }, + { + "id": "6a56f07c-aa08-50b5-b52d-d0e63e7520ff", + "version": "MTc1NjMyNzc0MjUwNDMyODAwMA", + "digest": "sha256:6e81ef810196601d6827d8bba86ff9679afe6615659acc0a5e0fa3a4b721148b" + }, + { + "id": "6a59459c-fb21-5168-86b7-eab274ba1c48", + "version": "MTc1NjMyNzc5MDI4MTUwNjAwMA", + "digest": "sha256:93b6352df3a75c463d89620b2ea772f5acc5eac02da520f3b71c76193045c457" + }, + { + "id": "6a8a3f07-f90d-52ed-b01e-23d69e5437b4", + "version": "MTc1NjMyNzg1Mzc5MzY4NjAwMA", + "digest": "sha256:8971dba5890336f00fe2eef9a0aff70bd50057a6cdad7eb5f9a160c9a33c7564" + }, + { + "id": "6a940993-0598-5271-ab67-a8e2743d517a", + "version": "MTc1NjMyNzgxMjA0MTU2MzAwMA", + "digest": "sha256:78332c5c57ac440b7cd19c9ecc176d3dec9f3ac4ac7d1db2efe7cd5a14c2b168" + }, + { + "id": "6adb5626-3e94-540c-8c56-d0fd9ef00664", + "version": "MTc1NjMyNzg3MDMxNzU0MjAwMA", + "digest": "sha256:d96b89f19414eeeb19854c1ea6bb256cfb54f02102d79a2a3f95eb193a4a6cc8" + }, + { + "id": "6ae41174-1187-5b20-8723-e8a23601ac3d", + "version": "MTc1NjMyODE3NDM3NzM3OTAwMA", + "digest": "sha256:ac896679b7ba70949059e3eb000919a081eddd8bb1fc399b852e0fa895f688e7" + }, + { + "id": "6ae55e62-8c44-507c-8d4a-4bf95a44657c", + "version": "MTc1NjMyNzg3NDg1MDkyNDAwMA", + "digest": "sha256:73333df9d9de3d10f5f9fde35e28f0715ddd7d76c8228e3d3b3281557b9c2697" + }, + { + "id": "6aed949c-b905-5039-b54f-58d0ee2ab802", + "version": "MTc1NjMyNzc1NjA3NDIxMTAwMA", + "digest": "sha256:ce9077063b72332a6aa55c1cf1f025e31ab794095c874b73ecf49645ead0bd41" + }, + { + "id": "6b0359fb-0c43-5d96-aa4e-11f691fb81d5", + "version": "MTc1NjMyNzg2OTM1ODAyNTAwMA", + "digest": "sha256:75e36b7ab729aaf2d9f79c0ba135095fe0a37d44ed0d2b4ae6736b9c6854ef5e" + }, + { + "id": "6b14c2e5-201f-5975-afbd-94280d50536e", + "version": "MTc1NjMyNzg3NDU1MTkyNDAwMA", + "digest": "sha256:5515abe48cb2b2ed345c6e0d145ce40a9822163aa31562e32232c976628fbf82" + }, + { + "id": "6b31e217-bf9c-5987-9e0c-5a362351f1c6", + "version": "MTc1NjMyNzgzNjMxODQ0NjAwMA", + "digest": "sha256:2f7f595ca60838d05d532e9e2391f3d3cb706b408d235a8a5aafaa20cf31a48e" + }, + { + "id": "6b8f3a41-eb07-563a-b52a-cc53d809ed66", + "version": "MTc1NjMyNzg1MjcyMDY3MjAwMA", + "digest": "sha256:689e02cac5feafb4482acf3d0bb6475b9cd84fd639bcb4de8bf0d9e9f769f255" + }, + { + "id": "6b978c46-03b1-5500-b8ac-017629d4a783", + "version": "MTc1NjMyNzczODQ2MDMwODAwMA", + "digest": "sha256:d93160d1c4545ad049484aab83df7b20ade1e7aea264f7d1336683fd226a23ed" + }, + { + "id": "6ba8fbb7-f813-56a5-9d90-7eced765af78", + "version": "MTc1NjMyODE3MTIyMTc2NDAwMA", + "digest": "sha256:d622154baadd9745cd1817abd92058d4983714bce5dd7cf6fd2c46746b015a66" + }, + { + "id": "6bbaa179-3924-5e94-b8da-c11f55513ec8", + "version": "MTc1NjMyNzgzNjI5MDgyOTAwMA", + "digest": "sha256:ed90e419aacdf18e44400a41ca097c94258ad701c4d35dceb671bf3565f8d258" + }, + { + "id": "6bca1a14-e543-577e-a9ec-29f654d426df", + "version": "MTc1NjMyNzc0NDQwMzM2NTAwMA", + "digest": "sha256:6f0fc1a593baab6aafeddb1be8bc507954850ec8ce157a1828ca2f36205b43fb" + }, + { + "id": "6bd25d00-66fd-5b71-8485-207d5cf67b15", + "version": "MTc1NjMyODE2MzQxNzUzMDAwMA", + "digest": "sha256:781ba3970fed2d94e37d3300a52121d0e80931fefc852509eca763d17410fcf6" + }, + { + "id": "6be1fd3d-5f66-5afd-ad29-f1ad06ef60e7", + "version": "MTc1NjMyNzc3MDQyMjM4MDAwMA", + "digest": "sha256:104bcf8ed14bc42c7d156b6bff91d2302ecf1d5b3112dc03ee088799375a0530" + }, + { + "id": "6c0e1134-56c7-533b-a9e3-62ba5e1f9c48", + "version": "MTc1NjMyNzc3OTkzODkxMzAwMA", + "digest": "sha256:2c02ea56e7f6e631f5f52463bd26939373f46b7371d5ac7e36fdf55fc1ccea68" + }, + { + "id": "6c43b93e-6e0c-5ea3-9224-03de6a8fb56e", + "version": "MTc1NjMyNzc4NzIyNjAxMTAwMA", + "digest": "sha256:d32e2ef073ddeae7b00771447ea1baacec5312240551dde92644e2e3818c97d2" + }, + { + "id": "6cba6f4f-7b48-5932-afad-6aed0f5632f9", + "version": "MTc1NjMyNzc1MzcxODU1ODAwMA", + "digest": "sha256:2529c09a1f99742e11fb19fd4e108b47f25e3b4cd099066f39a6cfffb7b19c71" + }, + { + "id": "6cc98488-f6be-59d1-9f5d-3dd793c74272", + "version": "MTc1NjMyNzc2NzcxNzc4NjAwMA", + "digest": "sha256:17a3809c41db1d2eedc331aaa92d6467d6802979eaa3d6379195fa9e1339d5b0" + }, + { + "id": "6ce2b9f7-06f9-5dbb-b393-7127dd5dbc4d", + "version": "MTc1NjMyODE3Njg2NzcyNTAwMA", + "digest": "sha256:033c79609bd0e1979275586f35f1d2483acd599205f1f4904f845f7a4cccb094" + }, + { + "id": "6cf9f0c1-0b0a-5cd3-be00-3e0262d24fb0", + "version": "MTc1NjMyNzg4Njc5MzY5MjAwMA", + "digest": "sha256:21369dfd2fdea2813207a18b004d655e15e8c2b197d99fa3758608fa7e53a489" + }, + { + "id": "6d16f392-0c48-5df8-ae4f-bbe698a9077e", + "version": "MTc1NjMyNzgwNDQxODE0MDAwMA", + "digest": "sha256:76cd99a51739badea0c67eec4e5903aed371275a7b53578f48d2eb2bd4777b9a" + }, + { + "id": "6d1d69e8-9c86-58d1-8569-1f2391b9552b", + "version": "MTc1NjMyNzg0MzEwNjI3NzAwMA", + "digest": "sha256:f2773a3bd6d660933a37bbe04aabbd8b3759b12ab37d4fdb60e674e390de9583" + }, + { + "id": "6d28ee69-7598-5174-aa84-3e64c816a611", + "version": "MTc1NjMyNzc5NjA4MDQ4MDAwMA", + "digest": "sha256:94d88baf5d6c0d39329bd6695ea857d74a04de36b84c86ce1396eb406ea90937" + }, + { + "id": "6d564ca3-6abf-5b46-b8bf-c4b7c8000358", + "version": "MTc1NjMyNzc3OTc4OTIxMzAwMA", + "digest": "sha256:972bf5cddb382229bb15420dc8f8732aacfa1860cef1cab7b548a6a0bab75e40" + }, + { + "id": "6d5bf820-f648-5450-9c24-715dbf4e7972", + "version": "MTc1NjMyNzc3MDQ4OTI0NDAwMA", + "digest": "sha256:6c3662286f753062240bf90779664e124cfd4778dbede8462695cd15de21def0" + }, + { + "id": "6d6a81e2-86d8-5ed3-932a-759ced471970", + "version": "MTc1NjMyODE3NjgxMTAxNjAwMA", + "digest": "sha256:8366b6bb85784a9d2bf261e6cdc68662eaa224810f7dc8b69b2a14db69011638" + }, + { + "id": "6d948ed3-88a0-5480-ad70-25680d22f409", + "version": "MTc1NjMyNzc4NzMyNTMyMTAwMA", + "digest": "sha256:b79af7846bdeb48f5227488af21b189db69865667f0b8dd1109e013bd0bde407" + }, + { + "id": "6db01c26-6dad-5b5c-bb97-c657e56d8a2c", + "version": "MTc1NjMyODE2OTA1NDg4MDAwMA", + "digest": "sha256:133bae67dd160f360114a5eca1f41cf213766c92fa04b6eb6e81e2b088fc3ecc" + }, + { + "id": "6dbff614-9412-5db3-930f-72fe70e22203", + "version": "MTc1NjMyNzc4MTAzODIyMjAwMA", + "digest": "sha256:805057bf8425517242ec47cfddbb6d4a004d1131f95459109d70120c8aabe9b2" + }, + { + "id": "6dc72425-5cba-5f51-a603-195cc90195de", + "version": "MTc1NjMyNzg0MTE5OTMwNzAwMA", + "digest": "sha256:ecaaaec6a7307aad2cc398c2b0597ceb8d8b3685c42057f6806bf81640fcf791" + }, + { + "id": "6dcae9a6-1765-52d2-83d9-a5d88ebbf743", + "version": "MTc1NjMyODE3NjE1MjM5MTAwMA", + "digest": "sha256:989d519e3ebc12195f57122cb6302367d1359e3b9fb79262adf0952f8589585a" + }, + { + "id": "6de1d6fa-d72a-5557-bff9-0123b8bf3a0b", + "version": "MTc1NjMyNzg2OTUxOTAyNjAwMA", + "digest": "sha256:9b7f5cfdace187009f67508cc94f0627a08354948d463258ff65329010348a7f" + }, + { + "id": "6e15872f-c42e-5b69-9f9a-d54e1584eb1f", + "version": "MTc1NjMyNzg1NjExNTI1NTAwMA", + "digest": "sha256:c3b624782fecaafbfb4efed620a4796d246cf992d75feaee97edece1e3a1bab4" + }, + { + "id": "6e181644-754d-5fb0-92ff-f7dbba2b3bb0", + "version": "MTc1NjMyODE3ODA2Nzg2OTAwMA", + "digest": "sha256:aaf8697835e974b86459f3acd74b3dc88c84c4fefd446d80234d394c1686454e" + }, + { + "id": "6e238966-212f-5dfd-af59-35e1f90b25bc", + "version": "MTc1NjMyNzg2OTUwMDY1MjAwMA", + "digest": "sha256:cf064efa42ad590d89d307a749d4e32d9804622f57ae281d314a8692cc2df05d" + }, + { + "id": "6e257763-e5f8-5262-bbbe-bd2dbd55cf95", + "version": "MTc1NjMyNzkyODQ4ODUxNDAwMA", + "digest": "sha256:32b51f21bfafcf1e5a621bfe8dac8bf853590f02f88dde98e0c20906e254a0e3" + }, + { + "id": "6e4525ad-c988-549d-a693-17454f228b6a", + "version": "MTc1NjMyNzkxNzEyOTg3NzAwMA", + "digest": "sha256:4f55f30747eb8a547c9b4533efbf2498532b5a397cd68c3a6e7ca20cc358da9e" + }, + { + "id": "6e561fa6-4824-5047-84ec-c7c30bb106cd", + "version": "MTc1NjMyNzg2NjI0NzM3NDAwMA", + "digest": "sha256:3140f52f2f51f1a679a5450c3596d17659e1e473b0eaf40aeb201f2209b10107" + }, + { + "id": "6e57bd15-40b1-5392-a8e5-da82e50776ee", + "version": "MTc1NjMyNzc1MzY3NTQzMDAwMA", + "digest": "sha256:f54293dd0fee9782601146bc561d7a75a19c8b0c2cb2948cc089a5c298879c9f" + }, + { + "id": "6e5aa594-e2c6-5e28-8aef-b6ae410e85e8", + "version": "MTc1NjMyNzgwOTM3ODc4OTAwMA", + "digest": "sha256:5ce084e055475fc663c11042e15a158f9d1722a4a70decd242a52cbcc224a28c" + }, + { + "id": "6e5d039f-464b-54d5-9b91-2f49bafadfec", + "version": "MTc1NjMyNzg2MDI3OTczNTAwMA", + "digest": "sha256:ee33bca31c505508a96cc9a6b23fd7739a48d4d4337256cd4b14d2dfccd0165e" + }, + { + "id": "6e63a495-3bff-515c-a76a-d60c24083d99", + "version": "MTc1NjMyNzc5NTk3NTM4MzAwMA", + "digest": "sha256:7a5dc812b9082428b5143a34070db490b488efb826fb1a361f2296b86771653d" + }, + { + "id": "6e65a4fe-2d5b-59e0-9306-325da0a33287", + "version": "MTc1NjMyODE2MzExNDg5MTAwMA", + "digest": "sha256:d23b0075fe94a218c85c917949217d82f3f620eee17b94f2aba27216fea9405b" + }, + { + "id": "6e6826fc-fffd-527c-a276-4bf501a335c4", + "version": "MTc1NjMyODE4MTg3MjYxNTAwMA", + "digest": "sha256:5aa4e1e37be49c79666c53e484adae117d6884e7b2048319503dc6c351b408a4" + }, + { + "id": "6e74ed10-ab22-5084-a32c-8fc5099f781b", + "version": "MTc1NjMyNzc0NTY3MTI0MDAwMA", + "digest": "sha256:77c66966810ea5a2222d48b9f32c2563a544c34f2e2a10744f58a07be54dcf28" + }, + { + "id": "6ea0ef5f-973a-57a8-8177-0b63c3666fe2", + "version": "MTc1NjMyNzc1MzYwNzc2NTAwMA", + "digest": "sha256:b756b841e662cb4fffd1e8f5e05b554f3695539f0a37c6549ac2c52c658b29aa" + }, + { + "id": "6ea12ba5-cbe5-5ec2-9fc5-211bb4e20019", + "version": "MTc1NjMyNzc4NjkyMDk4NTAwMA", + "digest": "sha256:e26cd40652c507d037d08c6d7975c711ac5c6be1d6e4747daff08fa6d001e58a" + }, + { + "id": "6eb71eb0-186d-55dc-aa48-98ad9506a941", + "version": "MTc1NjMyNzc3MDQ5Mzc2MzAwMA", + "digest": "sha256:af4e49de595f143f3ca7b581e77674d402c70961bfc1ed6a703ac1b211e1478a" + }, + { + "id": "6ebd9a26-da18-5d24-b978-8b19496e3924", + "version": "MTc1NjMyODE4MTkyOTQ5ODAwMA", + "digest": "sha256:4a006a8f270e48d0aa87f05750c0c19926b7535d831114e38db4ffadec1e1042" + }, + { + "id": "6efa7ccd-320f-5327-b46e-ce68583de060", + "version": "MTc1NjMyNzgxMjY4NzA3NzAwMA", + "digest": "sha256:8e8acbaac2f87c4432b827311550696df82c9662339cbc7601d6df30be09d432" + }, + { + "id": "6f3a97c9-22e9-5a87-9a35-d422848441a8", + "version": "MTc1NjMyNzg2NzU3NTc0NDAwMA", + "digest": "sha256:e380043cd974a72b12fdb24395e5691c9b3403e3290dc0d4eab2e194b3916e8c" + }, + { + "id": "6f3c5ca5-4d90-5bd7-8f91-c624f7c4f8e9", + "version": "MTc1NjMyNzg2MDE4NTQzMzAwMA", + "digest": "sha256:38b3bd5cc82c5a1ce5c3db32dbbada0badcad10d85165735c9bb8ef2a8a25757" + }, + { + "id": "6f4be5c0-01c7-549d-922f-01f930a7795d", + "version": "MTc1NjMyNzg1MjExODY5MzAwMA", + "digest": "sha256:a7324661b283c8754340db9565e16e3f29090a1e30b1b82fcb6cbefb02537ecc" + }, + { + "id": "6f59cab5-7b7a-53c8-825c-6a5c389c435d", + "version": "MTc1NjMyODA2ODEwOTgzNTAwMA", + "digest": "sha256:8308b3679abdb346004117249eb3b42b4cb4b3089e8758e9454d13fd6631797b" + }, + { + "id": "6f842996-9366-5a33-8927-45e7bfc2b268", + "version": "MTc1NjMyNzc1MDEwMDk1NDAwMA", + "digest": "sha256:830503e6baedd3079c7b68dff1583e2ce449d5577f80d1aef2c3acbe937294f8" + }, + { + "id": "6f98e6f9-68eb-5816-8cab-af577dcdc98d", + "version": "MTc1NjMyODE2ODk2MDk0MjAwMA", + "digest": "sha256:f0300486601bc63858e523b48782fe3a9125b84e7cc8e41bfbba6a338324c8ab" + }, + { + "id": "6fb4d590-b547-5738-8c8d-943e14bcaffd", + "version": "MTc1NjMyNzc5NTMwNzAwMDAwMA", + "digest": "sha256:5e06bb3657501db866fce82fb60257ad1cdac210c5b577709819472b3bfbc164" + }, + { + "id": "6fca99b8-f824-50c6-a1a0-0e1acdebf989", + "version": "MTc1NjMyNzc5MDMzODY5OTAwMA", + "digest": "sha256:e5a82cdd2bb1f9601e60237a322221908bd559258fa305265b893d2a4d9789bb" + }, + { + "id": "6fcc515d-a53b-530e-9280-b61cd2aa6a62", + "version": "MTc1NjMyNzg1NTc1MjMxMjAwMA", + "digest": "sha256:1c8c758ec493a72a93f68a8c0982db8640262d06eb04e77927e48fb8b80d7142" + }, + { + "id": "6fddc21d-0c58-5e1f-ba93-dcbe2baf2f76", + "version": "MTc1NjMyNzc0MTc2MjgwNjAwMA", + "digest": "sha256:960bd29517a25bd3620a861b576eb2ffdc4e0816fe7f2ea8b0f768bb90bfd42e" + }, + { + "id": "6fefc0f2-64f8-5b14-89cb-acdac262d728", + "version": "MTc1NjMyODAyMzQwMjI3MzAwMA", + "digest": "sha256:99a7b81ba0541e273c276cb54a06929d09e78e4eda4a7dad84cbb748c9c439d2" + }, + { + "id": "700651ed-eca4-551b-a6ed-51ef719b1546", + "version": "MTc1NjMyNzg1Mjg2Njk2NzAwMA", + "digest": "sha256:075314ba1ab317026dcf230c226256fe82d0240089f140df779f2f67237d141b" + }, + { + "id": "700d6b04-ff1a-54bb-8766-b166a7fc0623", + "version": "MTc1NjMyNzc3MjU4ODgyNjAwMA", + "digest": "sha256:07c3d133f0841ffdfc1e01f6f5942ec3a2b12c5e32aa0b7a6bd3607829e4daf6" + }, + { + "id": "7036949f-6933-52d4-a0cd-5a381e3e7dbd", + "version": "MTc1NjMyNzg2NjI0OTc0NDAwMA", + "digest": "sha256:6fa7359a151cfc16733350100b87564e758d11475d348d8f9ef770072bcdde46" + }, + { + "id": "7052a059-a083-5ecb-86b4-b6d3a09b462d", + "version": "MTc1NjMyNzc4Njk0Mjk5MDAwMA", + "digest": "sha256:00b4d0b7024891058d67be2c509571d3f59d4ce77329f6c8bf54d0e1ec8ee374" + }, + { + "id": "70585686-723f-5637-98ca-447f798937ea", + "version": "MTc1NjMyNzczODQ0NTMxNDAwMA", + "digest": "sha256:489e7b274f28c659d155047f3d97d917a8b5ecb55120b5ee2cc379ca8b732820" + }, + { + "id": "707d94f8-fed8-59cb-b67b-45337b60f1fd", + "version": "MTc1NjMyNzc5NjE1NDk4MDAwMA", + "digest": "sha256:a75dd5e303f1866f66ca82e1dbfea5739190d354d34364e96133b869b3549f08" + }, + { + "id": "70815554-ff05-5251-a4f5-c4c40bd528c9", + "version": "MTc1NjMyNzg2MDI2MzM0NDAwMA", + "digest": "sha256:6d4a83fc176d0aae170b2648bd7a5c08bb73d8be96fc40973794b84595aa58c0" + }, + { + "id": "70a24a20-a424-5196-98cb-ab16de67b030", + "version": "MTc1NjMyNzc3MzI4MjQ0ODAwMA", + "digest": "sha256:a9328381a96196832ceb73a484f37398ada84218eeab2380e969f2fe11eeb386" + }, + { + "id": "70f89540-4109-5325-bd59-2887693ce6fd", + "version": "MTc1NjMyNzc0OTkxOTY1NDAwMA", + "digest": "sha256:f6dccff8453aea2b07fb8ec600f8278ba72bb7d33a6dd540a4e368c17e49f73a" + }, + { + "id": "714ad29a-3176-5f17-a3d5-0a234df904dd", + "version": "MTc1NjMyNzc1MDA2ODg4NTAwMA", + "digest": "sha256:08b94bb95800c68945603e58d23922c05a920337a518b18c381b88bdcee19eed" + }, + { + "id": "715519aa-a37c-5fb1-a832-959b695a6d30", + "version": "MTc1NjMyODE3NDQxNjE5NzAwMA", + "digest": "sha256:135386169b3c9d9a579e5e63bcea07ea00db20264602d285a32f6d664d3effbf" + }, + { + "id": "716301ac-57ea-52bf-a280-e7459e299813", + "version": "MTc1NjMyNzc3NTE2Njc3NzAwMA", + "digest": "sha256:d035170bdb91fdb5ffd7e0de6b73b7c744e16d83a1212b219a0688c399e0b48f" + }, + { + "id": "7173b10f-a33f-5c21-9bb5-84882e231c95", + "version": "MTc1NjMyNzczODQwNzQwOTAwMA", + "digest": "sha256:fc4cf5523ff896406f3f380c6a42dd30d9fab80eebf0ce8986a691aa4aab2eb4" + }, + { + "id": "7174ce64-8c0e-5047-857f-66a7a7d93d3e", + "version": "MTc1NjMyNzc0NzU2MjIzNDAwMA", + "digest": "sha256:5f44c0671c99a1af2432fd3d6513fbdedcb1587a689ae3a7fef689dc3c83e165" + }, + { + "id": "7192464e-de3b-545c-bbbb-8dfe11fd1121", + "version": "MTc1NjMyNzg1MzI1MjYzMDAwMA", + "digest": "sha256:d96bb8dbdacad27e71ebb131e50ff28a15bb7a8ec8faad3e81d9140235775cf2" + }, + { + "id": "719647bd-34e4-5421-9925-340be982b1e1", + "version": "MTc1NjMyODE2NDUyMDg2MjAwMA", + "digest": "sha256:3d6cca992548e19f4666647d8dd1e59fdcc5fe94e26679d95c72f58efd1561bf" + }, + { + "id": "71987ad4-3736-55e6-8aef-a5409bfd9f2a", + "version": "MTc1NjMyNzc2NjQ2MDEyNDAwMA", + "digest": "sha256:6531500db7fb9367089bbaf7ca8a506775eb50be662b764f9ae891c5a58aae50" + }, + { + "id": "719d9147-a4b8-5fed-9c05-370a839d1cd3", + "version": "MTc1NjMyODE4MDQzMjQ0NTAwMA", + "digest": "sha256:13a85c549c9d7c2a972e20ba6cadcd2b417fcdad990b6aadb0ef0e8e2f51a0a8" + }, + { + "id": "71a436a1-93e4-56aa-8739-69ae58a8a93c", + "version": "MTc1NjMyODE3Njg4NDY1MTAwMA", + "digest": "sha256:dca8bd9fa09285d734c7629c53f13172b2d85a7969820b85f6d03c30a22be88a" + }, + { + "id": "71cca92d-041c-58e5-bf02-9c8518ee2a2e", + "version": "MTc1NjMyNzc2MjU3MTM3ODAwMA", + "digest": "sha256:7c237dbb561241ccbb595526cccf88840d2a38513f0dd8177d904ce8443b0a6e" + }, + { + "id": "71e8d939-f892-5253-b968-251f2c46c2a1", + "version": "MTc1NjMyODE3NDQyMTY3NDAwMA", + "digest": "sha256:d96bfd825b6f463bf667bfdca71c9af1d3956421a9a0c2a243d97bf5ba2fba07" + }, + { + "id": "721f8436-a76c-5db0-a89e-832e282661e7", + "version": "MTc1NjMyODE2MzA2MTI2NTAwMA", + "digest": "sha256:154e9171022553c513e59263c314ac4be5161ea264ddb9ef0ebf96705ea252c3" + }, + { + "id": "72226dcc-57bb-502d-be0f-f9ba72f47842", + "version": "MTc1NjMyODE3Nzk4MTM3MDAwMA", + "digest": "sha256:477698bd45eb0f50211036afa1022a01cf75c120bff4e7288b01cbfffafb9619" + }, + { + "id": "72317d67-7c9e-5dfa-a257-ffebd05e2092", + "version": "MTc1NjMyNzg1Mjc3MjMxMDAwMA", + "digest": "sha256:c950877e68c47c504d94b17c432cfced206b2be7ba953f62f2fe9e56478ce62e" + }, + { + "id": "7233d203-df03-5894-80c8-86fab9d324f8", + "version": "MTc1NjMyNzc0MTY1OTQ2MTAwMA", + "digest": "sha256:0d05384787f2a25ade526e0e06546b9e66bb2ba731c844a97534cfe8804dfd42" + }, + { + "id": "7261b007-b126-5b41-9731-548e2eef6ffa", + "version": "MTc1NjMyNzgwNDU3ODY3NTAwMA", + "digest": "sha256:4655275d3876c928a4d9fbdeb7acf6fce818642930cd3b6ce67712bec1464969" + }, + { + "id": "7274b1d7-3b18-5d21-9c71-e23ebc7244a1", + "version": "MTc1NjMyNzgzNjM0NjY4NjAwMA", + "digest": "sha256:7ad0df6b8377f048b2674787974f65e9445a942c45f4229998de4a2e528fb972" + }, + { + "id": "727de6f0-6b73-5041-89a5-aa780e14b217", + "version": "MTc1NjMyNzg4Njc2MzkwOTAwMA", + "digest": "sha256:9e69280d0596a37e25dd08dc53b5ebc5d474721ef5b700e9f2a09e6df06d04f5" + }, + { + "id": "728cc4c6-3ce5-539f-a6e4-636f4bbc0d4b", + "version": "MTc1NjMyNzc0MjI5MTAyNjAwMA", + "digest": "sha256:7c091e852ac2de47d3d865d400f2a9ca1410f3e1757cc24992b053c9099c3bbf" + }, + { + "id": "72b11a06-e978-548a-84db-c061d3e0263b", + "version": "MTc1NjMyNzgxMTk4MDYyMTAwMA", + "digest": "sha256:18f008724abc257ba27ffa657a4550df63353bbe841385fb31af7932bbe14364" + }, + { + "id": "72ca3a60-8075-50a9-9253-738e3786591e", + "version": "MTc1NjMyNzgxNjUwNTc1ODAwMA", + "digest": "sha256:5c23b20b12ce08d4167b2402f77dcb72989cc65ae94a94d43a68f3ea235721fa" + }, + { + "id": "72e99bd9-c277-55e7-b1d3-d0de8b308bd7", + "version": "MTc1NjMyNzg3MjA3NzQ0NzAwMA", + "digest": "sha256:5225b562b40dd8e6f74cae53cbd01d5f630662388811266eeb5ee4d04f084a17" + }, + { + "id": "72eea698-80f2-5306-9fc1-ad4c0f4850a6", + "version": "MTc1NjMyNzg0MzA4MTI1NjAwMA", + "digest": "sha256:9b984f40df1949a347e88e04abe6745b6172f056ce5b033cd242cc0ae94a8729" + }, + { + "id": "73305ed7-ff15-59f1-b68f-d4334daa9fce", + "version": "MTc1NjMyNzc0MjM3NjMzMDAwMA", + "digest": "sha256:952c2be3addd9cf69af7ce8982e9fd16b59a3fb7e3ea2879ba27fd4505074ed5" + }, + { + "id": "73479144-ebd8-5c16-a6a8-cc921e26cdb2", + "version": "MTc1NjMyNzc1NjA4OTY4MTAwMA", + "digest": "sha256:cf8c6e1e998c2fdcefbc7998e1b7e4fa9a2671fd3b1d610579363f5299bf49ec" + }, + { + "id": "734a33de-679b-55af-a165-331b4e16c0e2", + "version": "MTc1NjMyNzg4MjA3MDIwOTAwMA", + "digest": "sha256:2e1eb3674bcda099a16575bf110ac3fe3c9a498f6764be78a94798150b1edd8d" + }, + { + "id": "73647072-843b-54f2-a41f-31433820edd2", + "version": "MTc1NjMyODE2NTg4MzA1MjAwMA", + "digest": "sha256:0f95d80a54e593e0368bb7bfef4946b5c3109ac989e855b0fb0a1e3a9c5b276b" + }, + { + "id": "73690f47-40dc-554a-a4de-f4d711dae757", + "version": "MTc1NjMyNzc3MTQ2NTcyMjAwMA", + "digest": "sha256:444eb91350d2eb35b062e024b4dfb4090b9ee08ec9925073973f32c841360f87" + }, + { + "id": "738a6b36-51f5-58c6-bf20-4566d9e27b43", + "version": "MTc1NjMyNzc5NDgzNzA3OTAwMA", + "digest": "sha256:fdf19b7acf0d9361ba83a779b7e63cd7f8f7d79a0cca29dcfdbf6ffcb4328876" + }, + { + "id": "738c62a4-882f-5525-a876-a38669db1d2a", + "version": "MTc1NjMyODE3MTE1MzY1NjAwMA", + "digest": "sha256:a5f0303b39d45142153c326cc9c5e116307300e89d67dc210f7dc7a4a818a187" + }, + { + "id": "73989175-ab82-5641-b8ea-87e7c2c807a2", + "version": "MTc1NjMyNzgyMjQ2OTg3NTAwMA", + "digest": "sha256:64689e372339831ddd425c81c9607e576c7b812e721046a67883f88a723b6f88" + }, + { + "id": "73a09833-7ef9-555a-ad05-e59d7e8795e6", + "version": "MTc1NjMyNzc2MjM3OTEzNDAwMA", + "digest": "sha256:c466fa8fbe11c7194b376a7b8e792f1e82307b3a3336f61af8d5397f0dc8d646" + }, + { + "id": "73d1daed-2fec-5028-a85a-398643c82a4d", + "version": "MTc1NjMyODA3NDgxNzAxNTAwMA", + "digest": "sha256:ad13847ec8f63df02c2d0b5b1119f5ddfed2897f6c18e274c0ed991feb6f8e5c" + }, + { + "id": "73eb402d-a9da-5b2e-8b59-c91343503423", + "version": "MTc1NjMyODA2NzY4MTI4NTAwMA", + "digest": "sha256:2ccd63f6dcf7015f52ea230f08440005b98fda10d6fe624fb4ac7cc66659a3b7" + }, + { + "id": "73f16937-352f-542a-baa6-ef5554a58cf5", + "version": "MTc1NjMyNzc1NzU2MDE1NDAwMA", + "digest": "sha256:cd186398047eb39a224b306f8ed529380d6f68564ca1b04a5b0ab80b8e46f1c4" + }, + { + "id": "74156c57-4117-5a2a-ac69-11e395eea293", + "version": "MTc1NjMyNzg1MzI1MTUzNDAwMA", + "digest": "sha256:aa90ab8311d9905bdd05910fb9ee2d2c34ffeab583774b93b4e1ec01eea84e05" + }, + { + "id": "741c36e5-f1a6-500c-9f3e-9aa0c56e8960", + "version": "MTc1NjMyNzgzNTIzMTI1ODAwMA", + "digest": "sha256:9f63f5b131c135452468f5b80e086fd184bf269508fc141cdb5553c1c6d55c6d" + }, + { + "id": "742e99ec-4e6c-5fad-a9e7-c21d3d0c3068", + "version": "MTc1NjMyNzc3NTIzNDUxNDAwMA", + "digest": "sha256:a44e018fa4ecc8d78967516ed17ce10f321db8e0e62987ae7fef50947d3f4bab" + }, + { + "id": "7446f2f3-40aa-5367-9ef9-b11e517723ee", + "version": "MTc1NjMyNzg2NjMxMjgyODAwMA", + "digest": "sha256:ab8d1f12002a536a3ecc84697b6ee0356bbe1803f1be9ef6c84a3978505c6bb9" + }, + { + "id": "745e3e2b-7a10-5130-bcb9-19c45a6c0867", + "version": "MTc1NjMyNzgyMjMxNzQyMjAwMA", + "digest": "sha256:2cc7978107df1ab5b20b89604c2593f18084675ad3cbb0ffd6f71ec0c5c51d26" + }, + { + "id": "74721393-0766-553a-882e-5758be42d6d7", + "version": "MTc1NjMyNzgwMzY2MDkxNDAwMA", + "digest": "sha256:7ef978b6bff8507d1cc26ffeaae57a795c7fe5c271b7b2a621b5993b2c76d862" + }, + { + "id": "7477a677-371d-53e8-a85c-b540aebb0c96", + "version": "MTc1NjMyNzg0MzA3NzA4MjAwMA", + "digest": "sha256:d077a1776058e4d80436fccb35f7bd7bcb0d284cd6989f814a7c3fc401670cb9" + }, + { + "id": "7499136b-14b0-5b4c-9d27-dee32c7d480a", + "version": "MTc1NjMyNzg1MjExOTk1MDAwMA", + "digest": "sha256:6653f563dbcf53dc83d1fd7d79eedf7a4d6c77f162ff60a5fd932a692162f68a" + }, + { + "id": "74af3ccc-7565-551b-935f-f8b6b7233bd9", + "version": "MTc1NjMyNzgxODc3NTAwMjAwMA", + "digest": "sha256:a158febbaef941c2943b5a9f92b0c58c9b705e2c72201ead0b8393251a097833" + }, + { + "id": "74b23123-4163-51f1-840f-c9e1bcd3a03f", + "version": "MTc1NjMyNzc2Nzc0MjAzNTAwMA", + "digest": "sha256:0635950cabb8279e2ec65cfbd5b9aabb884271da2bc8f28ab3f9431355bf4a44" + }, + { + "id": "74c9320d-bc58-5ade-9b7c-b4ca89949bef", + "version": "MTc1NjMyODE3MTE3ODQ5NjAwMA", + "digest": "sha256:4c37dc7221025e039ba0de8b35f7d6be9397f4e733fbeea5be230f964e906ae5" + }, + { + "id": "74e559d8-fe0a-5427-b9cd-a90a4b6db3b3", + "version": "MTc1NjMyNzgxNTUxMDMwOTAwMA", + "digest": "sha256:a5a422a646f67e24e8d671373e6fc1243a03ab1102629cead55a6bf2ebbef977" + }, + { + "id": "751ee94e-d026-502c-ae40-d6783f20c2aa", + "version": "MTc1NjMyNzgzNTIwOTM3MjAwMA", + "digest": "sha256:f10ad10aeb4eb7cd0f2606d09e1398c791acfc225f7f29eedcf2ffb863d2d404" + }, + { + "id": "752a090c-d1b4-5c63-a905-fb6341c62bda", + "version": "MTc1NjMyNzc1MzY2MzkxNzAwMA", + "digest": "sha256:dd01714e8f51ab65fab2bc4ddf39595c5b1f9b1910afe0c24841cd874eadd118" + }, + { + "id": "752c3f92-ff7c-568e-8f4a-e1ec5dadb734", + "version": "MTc1NjMyNzc2MjUzNzI3MzAwMA", + "digest": "sha256:3921619c5057c52f52354a148d3247c61ddc31015f991e1d020be2658db4906f" + }, + { + "id": "7547f579-4cb3-5d53-9389-f1187344f8d5", + "version": "MTc1NjMyNzc2NzY4NDEwNTAwMA", + "digest": "sha256:a643194bfb6157f34d814995902817340f28ea932063c96242a28cca56dd0fd8" + }, + { + "id": "754d23bd-226c-5e95-806e-5c8618faf8f9", + "version": "MTc1NjMyODA3NzA4MTg0NjAwMA", + "digest": "sha256:fcb4f605d95ecca85721942ac759b21731840bc6db3ee3d4c96b71ad2995de34" + }, + { + "id": "7551b5ab-da32-5e32-ae60-fb60cdfa5dca", + "version": "MTc1NjMyODE2MzA1NjM5MjAwMA", + "digest": "sha256:ed29338ea601969855d77ec4e6c31041461d10b619721e7aa7ae5eba33ad0ba4" + }, + { + "id": "7562c083-dbf5-516e-b743-3ef8d4b836e0", + "version": "MTc1NjMyNzc2MzgzODgxNjAwMA", + "digest": "sha256:6bc4f920bb80ad72e5ccb3b7bf63737096aeca86e01c68200003fbe26f894771" + }, + { + "id": "758d6342-c8a9-5582-9b38-8776e66d0560", + "version": "MTc1NjMyNzc5NTE1ODE5MDAwMA", + "digest": "sha256:3c30c77eb68d7b4f8a36e12dfaf4b8e101c44053e074b0fa759583911d43e04d" + }, + { + "id": "75bd8a6d-54c2-5cf4-b6f1-2caffac43e18", + "version": "MTc1NjMyNzgxNjQ1MzQ5MzAwMA", + "digest": "sha256:33f438c352a8c155e579747a7896326400f7f6f7d4e7f1a868aaaf7a14a4a72c" + }, + { + "id": "75d2af83-9e38-5d82-804d-a0e764e6bbdb", + "version": "MTc1NjMyODE1NjMyMDAwNzAwMA", + "digest": "sha256:247e07a642551c3b0b45947952b4b8d60827a53404cfae5f0e4b07f4e28062a8" + }, + { + "id": "75dbe2d0-f637-5ea2-bb43-6c72fca7da47", + "version": "MTc1NjMyNzgxNTY0MDk2MzAwMA", + "digest": "sha256:97aa5e56cd561e733666696db8871190856ccb7f30d036c31c30e46f10b947d1" + }, + { + "id": "75e99050-6042-5d60-a6ce-b23080beb33a", + "version": "MTc1NjMyODA3MDYwNDQwNzAwMA", + "digest": "sha256:f5e871f8e633be5c516a7bdb4762d8ccc1ae73a5e9cf977f0c99d975f8ef3629" + }, + { + "id": "75f30f8b-ef7d-536e-8077-8f60d77c5e07", + "version": "MTc1NjMyNzg2MDI4MDI5MDAwMA", + "digest": "sha256:1654d8ea15a7e9f455634fd248955ea7874fb31cadea4d8ea521c78faa610f94" + }, + { + "id": "75f5d062-0e8f-5dda-b4a5-7208a7baad22", + "version": "MTc1NjMyODE3NjEzMDA2OTAwMA", + "digest": "sha256:7c9e19ca998c4b6d0551d48721915638db7ecfa62471216f75b41fd87ab57dfa" + }, + { + "id": "76124ea8-b957-5c92-9bfa-4a7f9de8e144", + "version": "MTc1NjMyNzc4NzA0MDgyMDAwMA", + "digest": "sha256:bd6fe7bd4882679131f5e2a225bb921e82a613537b947771a1f8c9d2c27a580e" + }, + { + "id": "7621bf74-0227-5796-9afa-bc35bc7b8848", + "version": "MTc1NjMyNzg2NjY4MjIwNTAwMA", + "digest": "sha256:5911b14b77ea658010031be13b61d0026d326713b207d1fcd9895b468fd953f2" + }, + { + "id": "76353400-5f4c-5fa1-9057-7f5bef968c26", + "version": "MTc1NjMyNzc5NDY5NDE0NjAwMA", + "digest": "sha256:06be9367853247b2c434083f4b314c66829077ac799da7806ce74cb95ec2cf0b" + }, + { + "id": "764e3716-195e-5267-b703-7bd32c56ab0e", + "version": "MTc1NjMyNzg3MTQ5MTUzNDAwMA", + "digest": "sha256:eb50be235ef8d56678c6971cfdbb763db482bed78e407937352251d65301c9cf" + }, + { + "id": "765e67d8-cc89-5cd5-b33f-35e8e98839b1", + "version": "MTc1NjMyNzc0NzU1ODg3MjAwMA", + "digest": "sha256:2237d17e0183040e3c993559cba4f24951b53a263b2333a083a26e1da8c37b2e" + }, + { + "id": "76714a06-8651-5e03-a835-83047ee8bfa1", + "version": "MTc1NjMyNzgxODk4Njc4MjAwMA", + "digest": "sha256:c82223d413fa8fd24b57e5bcd0ce23bedb9c974525f9e310717ce47a15e86ea0" + }, + { + "id": "767d6bd1-c8d4-509c-ad91-db0e7257e659", + "version": "MTc1NjMyODA3NjkyMTgzMDAwMA", + "digest": "sha256:366df300f490cff8b26c2364c738cb76b5e450479cbf2f0db2193b92b7f2f5ff" + }, + { + "id": "76e9a892-27c1-537c-a4d5-1400c8d582a2", + "version": "MTc1NjMyNzg1MjE5NzkwNDAwMA", + "digest": "sha256:68e35d3a6128d75f2acff87949826b707bbe36bb4f864f604429f4eef795977d" + }, + { + "id": "77366228-a0bc-5577-82dd-8cc46fb49f2c", + "version": "MTc1NjMyNzc0MTY1OTk5NTAwMA", + "digest": "sha256:23cb262284117eb357dd98fdbd4ef287128f030468e69f704ddb6b0602eb635c" + }, + { + "id": "77406251-2d66-55cc-b75c-0acda4a8fa8e", + "version": "MTc1NjMyNzg1MjA5MDU0NTAwMA", + "digest": "sha256:07f7581daa46ceddb0cdc2319a167ba385599e68a2ddf8924f7f1fb789259408" + }, + { + "id": "776b090a-79e6-5376-aa57-57a1e8c78af9", + "version": "MTc1NjMyNzc2MjM4NDUzODAwMA", + "digest": "sha256:a28cad53c38eb9a4e2299f9ed997a764dad3271e9e35dd4bdae3b2c3be91d49a" + }, + { + "id": "7775b9f5-296a-54e7-bf80-07b102872f31", + "version": "MTc1NjMyODE1NjM0NTc0NTAwMA", + "digest": "sha256:ab3774c8af43be8f2bb3147879efbb391acd6ef0010d9e3e29f39c86e72c582f" + }, + { + "id": "7775faef-2d12-5559-b2f1-fb5120b1a9e5", + "version": "MTc1NjMyNzc3MzAyMjY5MjAwMA", + "digest": "sha256:41534e042df85c504a0e016f3ee4fc755acc1949bd4d3c77c1716cc5a5e5ef2d" + }, + { + "id": "779c4e71-d2f8-595e-82a2-36c5c8f5c25a", + "version": "MTc1NjMyNzgxNTQ5MDkzNTAwMA", + "digest": "sha256:bf1941127ffd43c7359453d9d3aa260ed0b3180a14717830e750c4586f0ced32" + }, + { + "id": "77c304e2-860d-5681-9411-6e2678c7bbbd", + "version": "MTc1NjMyNzg4MDQzNzczNDAwMA", + "digest": "sha256:97c9369e1b57be681f508192d578aa936d14f7c5c2b8587834985b1dac9c681a" + }, + { + "id": "7806dc97-616b-5546-95f0-cb50c00a3c6f", + "version": "MTc1NjMyODA2OTIxMzkzNzAwMA", + "digest": "sha256:2aac33d60396d5968dd62590cab1a6dc7848bc06048da1d3bf6710d65aab1900" + }, + { + "id": "7813fcc1-d964-5523-a438-a71b5a3849aa", + "version": "MTc1NjMyNzc3NTE2ODAxNDAwMA", + "digest": "sha256:790911e4b165bb728e77b962002537211868c7a576e2676e5dd9a80fd181516b" + }, + { + "id": "783823ca-0994-5a7d-abb0-126b1d12faf1", + "version": "MTc1NjMyNzg1MzM2MDk2MTAwMA", + "digest": "sha256:0a4a8022ae5aecaba1a91a474c42c4e4ce8dce0a61e216cd977bfc83fe833dd1" + }, + { + "id": "78438308-8a6d-5737-a7be-248c7e172c5c", + "version": "MTc1NjMyNzc3MzAzNzY4MjAwMA", + "digest": "sha256:6e8511a52eb4e0a515ed2d498840ebdfa0c572625835f08b5152a889c8f0ebae" + }, + { + "id": "785ab80d-b186-525e-b2fa-4cf2da974b83", + "version": "MTc1NjMyNzc2MjU3MDQ1NzAwMA", + "digest": "sha256:bd4cb0137fdac7581e71fec356677381fde34e13ca10dbf0a5e9782c8cb0ac2d" + }, + { + "id": "785f0693-99e6-5b47-a7e6-262001745b5c", + "version": "MTc1NjMyNzgzNTY3Nzk4NDAwMA", + "digest": "sha256:ba0282c24df55f0852762ea7161bd251a83e959df77e0333b45f95006109facc" + }, + { + "id": "78806b18-2998-5935-9de4-d6aba5a3cd73", + "version": "MTc1NjMyODA2ODEyMzcyMTAwMA", + "digest": "sha256:54341ed9633641eac32af1af2e73409154240e06b6bf77550e000604b5158c3b" + }, + { + "id": "7887d766-7423-5cbf-a24c-71c15cd65c7a", + "version": "MTc1NjMyODE2Njk4NzM2ODAwMA", + "digest": "sha256:bac4f69ea0cb8271a04f7cc332d0e769c32cc944f157ec65d6c851a3aaa60ccd" + }, + { + "id": "788d3c13-f981-56cb-8814-af848810bd6d", + "version": "MTc1NjMyNzg0NzkxNDU4MTAwMA", + "digest": "sha256:fbb7010a7e42e7d40b9a32c56ad22784a5b16acea57bda77850f11f3f31432a0" + }, + { + "id": "78bc1480-cf9c-53dc-865c-d67f2dd27776", + "version": "MTc1NjMyODE4MTkxMDc1OTAwMA", + "digest": "sha256:181e5de548bd0f9e3256bc1cb3564d42ac8208464053b19f071a08ee05cc5499" + }, + { + "id": "78cbce43-f7eb-5576-8992-fb7e60af67dc", + "version": "MTc1NjMyNzc5NTE1ODI0NDAwMA", + "digest": "sha256:47f761308e6e4172d7d27b99289b752cf808d8118291279cde0eb5ad8371483f" + }, + { + "id": "78d1ec71-a208-56e9-b486-2567370309b4", + "version": "MTc1NjMyNzc3MDQwMDI5NTAwMA", + "digest": "sha256:f70d6fdb532f21e90de49254895ffe231f9c52d79a2d0e8f85717b43fea2d4e8" + }, + { + "id": "78d3dfb0-2fc1-5d76-941a-b2521247beba", + "version": "MTc1NjMyNzg0MTE4NDM0NzAwMA", + "digest": "sha256:5667671c970a5858dad32f827d5f03bc1253bcb55b5a56f9f2a3cf3f7a636099" + }, + { + "id": "790b0eec-965e-5fcd-a9c0-46659a395962", + "version": "MTc1NjMyODE4MTg5MTY3NTAwMA", + "digest": "sha256:cd124cb06c828f5d7c80c5ea6b0bf380365049b1219cdb23dc17b2af243d6d3c" + }, + { + "id": "7913fe74-5c70-5956-80ec-424cd5ec606b", + "version": "MTc1NjMyNzgwNDU3OTgwNTAwMA", + "digest": "sha256:1c1a8174b5cf99cf16818c8ea0e84c9990bba14f25994862425e536f45c907d2" + }, + { + "id": "792a6bde-bfd8-5610-af1b-a74358fca507", + "version": "MTc1NjMyODE3Nzk5Mjc0NDAwMA", + "digest": "sha256:ef2caf23f2656d6d3a4f4702ae53dc06c3041fe03d7f1edf3b8b6f2d40304030" + }, + { + "id": "7957e656-630b-5dde-95f2-1b3394d05d54", + "version": "MTc1NjMyODE2Mjk2NjM0MjAwMA", + "digest": "sha256:bb6084328fca99d87da414d2405dec1817f324e1caba3895daeff596427ad41d" + }, + { + "id": "7969d654-2ec0-5e8c-a071-83a1137a2e58", + "version": "MTc1NjMyODE3NDM0MzU3NjAwMA", + "digest": "sha256:6bca7d5810b03f645ed408de300f3bb696fb533dc171b97eea7e6f9bfc66660c" + }, + { + "id": "798021b1-db7a-593c-99b1-3973fba60c82", + "version": "MTc1NjMyODE2NTg1NjEzOTAwMA", + "digest": "sha256:882eea36b41d1538bd339d6a24fa80dfd2db34a06abaac6934578c00dffde4c6" + }, + { + "id": "79a4bfc8-9440-51ea-842a-ae959db941d9", + "version": "MTc1NjMyNzczODQzMTM5MjAwMA", + "digest": "sha256:388643fc2fd68b91c1fde5c6229bf3331101b72793915465cdc90d80a995c3e1" + }, + { + "id": "79b39eb9-f1b4-52dd-b97e-5d7599bdb05a", + "version": "MTc1NjMyNzc3NDU2MjM0MDAwMA", + "digest": "sha256:8c10930a0bb1ec43e9279692e14fc607772c2f2d15ab7ba3aa8a829f852671c5" + }, + { + "id": "79bc477d-19bc-5a4a-a99f-cc3f68372da6", + "version": "MTc1NjMyNzc2MjM4MDYxMTAwMA", + "digest": "sha256:d8592e6f71187016e12dee2cd9122979f3568dd76c4cdaeb08fb46a73fc3bae0" + }, + { + "id": "79d58b6d-8db1-5e23-af56-9b793c7a00ce", + "version": "MTc1NjMyNzc1Mzk1NzI4MDAwMA", + "digest": "sha256:2fe28168bf708d353474a2e93f0fb80d21130a57e33de6f29016d2530b1d0f71" + }, + { + "id": "79da6a3a-cc40-578c-80db-048c28c54f76", + "version": "MTc1NjMyODE2NTg3Mjk3NzAwMA", + "digest": "sha256:3658791ff87fc5641d59707f1a33e1d40c07502cb0fc166ac3899fd11464907e" + }, + { + "id": "79e80f9f-53a6-5eb6-8fdf-393151096d6f", + "version": "MTc1NjMyNzgyNjI3MTM4MjAwMA", + "digest": "sha256:d35d0b7be6b0a4d0d77c8f38651b45503a3cdcac327d3d9b87afdcca79710d0c" + }, + { + "id": "7a019685-b62f-568c-80ce-06718077a4f2", + "version": "MTc1NjMyNzg0MTEzNTA0ODAwMA", + "digest": "sha256:4019c0940e107af321f7b7cd70943fcc0aeefbce8645fe735d6b9b537a4e499a" + }, + { + "id": "7a049a19-84d0-5321-9078-0eb82f5bf745", + "version": "MTc1NjMyNzgxMTk3NjU2NjAwMA", + "digest": "sha256:2cb75cf51e7ccb37e8fe5547c3bd66eaa1955c2d855d12341f3e2f420a99a86e" + }, + { + "id": "7a077df2-b9f1-589c-958a-615a965ddea2", + "version": "MTc1NjMyNzc3NTIzNDAwMTAwMA", + "digest": "sha256:b4e1ef6e585d6ba914c254285576e2ab33df05dd7b378bb54222084107720494" + }, + { + "id": "7a188de1-f76c-588e-8070-0b1cc4f1fd6e", + "version": "MTc1NjMyODE2Njg5ODEzMjAwMA", + "digest": "sha256:896ce6bf4a0f5930e83d2fae9b9ea0d7a846cf043d5d51da5e029db7743e5472" + }, + { + "id": "7a37d671-264f-5c44-88e6-98979d116cc2", + "version": "MTc1NjMyNzc1Njc4MzE4OTAwMA", + "digest": "sha256:48a4288b653c0f56276e6840c655ba8816aae7ce2e9de6741ffefccb0beccfc1" + }, + { + "id": "7a538abf-079f-5219-9cce-7a28358362b4", + "version": "MTc1NjMyNzc3MjkxNjg4NzAwMA", + "digest": "sha256:b9e4268486d9d52423b66fa97ce04ae7822c323d9fd8712cc7e9e8598a7f7f46" + }, + { + "id": "7a5f8d69-10cc-52a1-a798-c159328e4f62", + "version": "MTc1NjMyODE3MDAwNjQ0OTAwMA", + "digest": "sha256:7edf4727eb7740d3f8688a312055b1fe3d72eb339016bbeb67c5744e5c1cee74" + }, + { + "id": "7a644077-30fe-58a3-bc4b-e1c2dd2e2015", + "version": "MTc1NjMyNzg4MDM2NjQ4MTAwMA", + "digest": "sha256:550bc2abd01c0cad06981e98c11fc751a3c46e82c158280999cbd2d556c51313" + }, + { + "id": "7a6d8c0b-b0b8-558b-9912-6f3f5da7281e", + "version": "MTc1NjMyNzg3MTYyNTEyNTAwMA", + "digest": "sha256:c363966d72167b72ef2c85f59885a655077f1ea837b0b08297e8d4f92761caf5" + }, + { + "id": "7a7191fc-bef7-5c50-97e6-88b505d30c5e", + "version": "MTc1NjMyODAyMzUwNjYzMjAwMA", + "digest": "sha256:4b37b43a7960a08cee1ac9a219485bdf7c7bcbb19f996e0e888045691c5e92b9" + }, + { + "id": "7a7bce91-743b-5bac-8920-6433f55ccd49", + "version": "MTc1NjMyNzgxNTY0NDM0MjAwMA", + "digest": "sha256:ed4b9c38f43883577db2277df22c4aaf4687125ec5aa443fa17945271f923be4" + }, + { + "id": "7a81e03d-5d06-5fc4-8788-288b23242b94", + "version": "MTc1NjMyODA3NzQ0ODg2NjAwMA", + "digest": "sha256:5b35786433ded8d282fdd30df843e08bcee0871ac783a432e5cb7b02ce34d2c6" + }, + { + "id": "7a84a53d-8874-5d54-a296-8e1b97568034", + "version": "MTc1NjMyNzgzNTIwODgwMTAwMA", + "digest": "sha256:ca9a6bc29ecd83ca70cb189aa95f709fabdb5df80c5c1308504869244062bb13" + }, + { + "id": "7aa92e04-72ef-5962-8aa6-b2fba1e537d2", + "version": "MTc1NjMyNzc2NzY1ODMyMzAwMA", + "digest": "sha256:73a519128ad1865624018d473c94f8020be1180b5762f24d9bc247fbc4f3e2e2" + }, + { + "id": "7ab04032-2e89-5410-8a68-ebe53da810b6", + "version": "MTc1NjMyNzg0Nzg0MjAzMDAwMA", + "digest": "sha256:f7753132077a51f33ff64c37d0dc1814fb33107616ad7dfda75632597cd5dd87" + }, + { + "id": "7ab592fe-e31a-50e9-aa85-166d78c537da", + "version": "MTc1NjMyNzg2NzYxMDM3ODAwMA", + "digest": "sha256:73965b9b25cfa5dfbd58defc85ae613e320ecd7089da89ddfe7e1eb21f015531" + }, + { + "id": "7afac16e-7d13-5405-88b6-1ae4246688d4", + "version": "MTc1NjMyODE3MTEzNTQ1ODAwMA", + "digest": "sha256:9a1849f8dc6893d6e94fed920183e478e0092c3b4e02d19a489403f65e4695a7" + }, + { + "id": "7b53a041-aa62-5dd7-9114-85adc50b62f0", + "version": "MTc1NjMyNzc3MjU4ODYzNTAwMA", + "digest": "sha256:b01c745cf9a32982ad013128cb304c99395418da18c3002a72521801f38f19b8" + }, + { + "id": "7b848c46-cc3a-53d6-a571-a7008c909c85", + "version": "MTc1NjMyODE1ODIyMjEwMjAwMA", + "digest": "sha256:9b85e2c6643863e443692e6665d2687327440ab5c218e9fff0ea41eab58ba887" + }, + { + "id": "7b9b411c-4681-5064-81dc-36859251cec1", + "version": "MTc1NjMyNzg4Njc1ODgyNDAwMA", + "digest": "sha256:9f4a603f29b96e320766925a7a2d58525cd9ab41ebdf4364b4cabb63c8e000a0" + }, + { + "id": "7b9c4244-8af3-5d3c-b3fd-531584649932", + "version": "MTc1NjMyNzc5NDc3NTA0NjAwMA", + "digest": "sha256:e87bbb57fa2da442218bf222fbdab97c9e1b6438d981bbd1d1aebf99f01ef257" + }, + { + "id": "7b9e519c-31d0-59d1-ae30-4d0deb0bdc07", + "version": "MTc1NjMyNzg4MjA1Mjc4ODAwMA", + "digest": "sha256:8497881e36351f995e7a3536a8df0f02aab1ade1525c1781ce65b1398975aab1" + }, + { + "id": "7ba5464c-2365-5cd1-83a0-9eab2fe26b0a", + "version": "MTc1NjMyNzc5NDY1ODY4NDAwMA", + "digest": "sha256:d97854cf309a4c210c72751ad0967a1a915f229d440ee6b6601e8efcd3dc3dad" + }, + { + "id": "7bda4941-5447-5faf-97f3-55028996fd29", + "version": "MTc1NjMyNzg4MDM2NTMzNzAwMA", + "digest": "sha256:33f56e9ff54d3330e419a8c0e49948d7a53397ae86a2f753cb079f83ef8efdf6" + }, + { + "id": "7bda6475-047a-586c-bec1-afd91b4879bc", + "version": "MTc1NjMyNzc3MjU4ODA3NzAwMA", + "digest": "sha256:48e9e06407136cb73fce3fd9c2346a4ad70d6319c0d429fdcb9e729fd0f01ca6" + }, + { + "id": "7bea5602-d960-5e0d-b65c-6f951e504c4f", + "version": "MTc1NjMyNzg0MzAwMzc4MjAwMA", + "digest": "sha256:c8a2d79ba4ab0f98cafeee9e16c28e23db05087907589ab6d7203ae04ecd6756" + }, + { + "id": "7bed028d-6407-5132-9884-e4e55d7feb03", + "version": "MTc1NjMyODE4MTgyOTk2MzAwMA", + "digest": "sha256:ad804bb0bc9ea3240c89fb7e5a662355424e44d72c6ec37e9b8da41be33e4873" + }, + { + "id": "7c1188e0-1663-57f1-8554-4a013990b0f0", + "version": "MTc1NjMyNzc3MjUyMTcwNTAwMA", + "digest": "sha256:37ba412fb795899aebe0df804f228d7aa5036587cf136a6b3b601e62b97dd0cd" + }, + { + "id": "7c353ad8-f65c-5c83-9858-465ae2bf010e", + "version": "MTc1NjMyNzc1NjczMDA2MTAwMA", + "digest": "sha256:d1bfcbba413dda786503e87c7a6f78c3b8020c22cb45849243366db9601b1c3d" + }, + { + "id": "7c3d7b80-1188-510c-9981-24af97a62b57", + "version": "MTc1NjMyODE2MzQwOTg3NjAwMA", + "digest": "sha256:d1b010ebc2dfbfc0583c86ba405dba53a995625b0bb4a4802ee45c63e4c09c1d" + }, + { + "id": "7c63cfcd-49f2-51cc-b448-b07f286cfae9", + "version": "MTc1NjMyNzgzNTc2OTMzMjAwMA", + "digest": "sha256:fa8c3f99540a2cf6e27e6e7d5bcb5d9b8418ec921122d935b037e3c2b2b7dfd7" + }, + { + "id": "7c6b0fc1-c53f-5d32-a9eb-52e6602b154c", + "version": "MTc1NjMyNzc1Njg4NzM4MDAwMA", + "digest": "sha256:0ab59f20f05457e658d9eddc1d27d754395e5d4d2b10a4745dc747b0936da5da" + }, + { + "id": "7c72e2d6-674f-5d35-b4e8-66c843e39ac9", + "version": "MTc1NjMyNzg1MzM2MDQwNDAwMA", + "digest": "sha256:85edbe30a2fb08d65b1c43c89a60aa404757bc0d5faa856076e3d6d7af005287" + }, + { + "id": "7cc6d09b-7b1e-5079-8ac1-14fc990ec19f", + "version": "MTc1NjMyNzg1NTY3Mzg2NjAwMA", + "digest": "sha256:ccbb26935b266a4f814addc732951ee6e2f0ec80a68be131f706522555175ce7" + }, + { + "id": "7cc73f65-f0be-59a1-99d7-7e019538aadf", + "version": "MTc1NjMyNzc3MDQzODE0MDAwMA", + "digest": "sha256:55b59924162c3b23ee2881717ce419b113dcaf7125158b26cfb670ab449f1e89" + }, + { + "id": "7ce2e6de-d8c5-5bc6-bd88-c3be1d9b844c", + "version": "MTc1NjMyNzg3NjYxNDI5NjAwMA", + "digest": "sha256:833964ceaa8ee84bbc5b961a8341d5ca277e12f14fc23e942447cbd1995c6563" + }, + { + "id": "7ced3f21-3c2a-59aa-b585-bd4e4ff306c6", + "version": "MTc1NjMyNzgzNTIwOTY0ODAwMA", + "digest": "sha256:d203df1e222b11ffc3600ce964307c3820feee9b60b0469c23bc5ad13e4d8593" + }, + { + "id": "7cee96d6-1360-59eb-aced-3a8d760241aa", + "version": "MTc1NjMyODE1NjM2NDAwMTAwMA", + "digest": "sha256:dc41686f25c00e8c11a504e8c8806be1548e6eb011ae45a6ca2e53927c5aed21" + }, + { + "id": "7cf4fad3-7231-52d7-abc9-8e78cb79cbe5", + "version": "MTc1NjMyODE3NzY1MzU3ODAwMA", + "digest": "sha256:f31073d5440921d9de4eddcf0d01a86cd32cad5ae63a752e2b7afdc63aba0525" + }, + { + "id": "7cfef546-aa12-58bd-802e-d14244a2e8f8", + "version": "MTc1NjMyNzgxODkyMTY4OTAwMA", + "digest": "sha256:7c44da7bdb4f75fcb067c035d4dd929aef37cd9a81d06c91dd32736f1832a918" + }, + { + "id": "7d4835f8-1590-57ea-9a17-a4dd8811e274", + "version": "MTc1NjMyNzg3NTQxMzQ5NTAwMA", + "digest": "sha256:6c3dae6fb886b215298104085f30fecd788fdba5eec79500a8259c198016f64c" + }, + { + "id": "7d5954d4-536d-58e5-a822-fab21b68ca60", + "version": "MTc1NjMyNzg3OTk5ODc5MDAwMA", + "digest": "sha256:2d501bd5359f52970f1228fbc958260b16d89de8f02f0f21e6ee5910c7614bd1" + }, + { + "id": "7da33258-3946-543e-8beb-2986707144f6", + "version": "MTc1NjMyNzc0NDUyNDUwNjAwMA", + "digest": "sha256:84545fb2ae0b3178a89f59d2457a3c44b9d914de926b1a98da851c52f3961149" + }, + { + "id": "7dc14208-7a0f-55e1-8755-3dd4d587d839", + "version": "MTc1NjMyNzg1MzI1OTQxMjAwMA", + "digest": "sha256:b98ce622e1a1893b000a36e517ef3d27be851663b1e68325f25ee33f99dd2b49" + }, + { + "id": "7dc82f82-83f4-5d1a-ab69-c0d8fddb342e", + "version": "MTc1NjMyNzg0MTIzMDc0NjAwMA", + "digest": "sha256:71abf40068fba6cb6505304dd3bd5afe61ac51bbd888cfb1109ad470d46c25cc" + }, + { + "id": "7dca3feb-10b9-511c-a8d2-0dc2305e9d83", + "version": "MTc1NjMyNzkwOTEyNTU4MDAwMA", + "digest": "sha256:bb9846c7be787cf9f34d64bc9c6b04c6e35fa07c9b5a13a8a79472b0b8dc12f9" + }, + { + "id": "7dd1ccbb-efe5-582f-9272-f5dffd271abb", + "version": "MTc1NjMyNzc3NTEzOTExMTAwMA", + "digest": "sha256:0a222ffb7c2c371534352d46611b385b8036689afc71432be4530d5ad29a1970" + }, + { + "id": "7ddb644b-f26d-5c0a-ba4d-86c8886c5060", + "version": "MTc1NjMyODE3NjE0NTU5MTAwMA", + "digest": "sha256:82f7705085cc72fbfa52be3bbe0b8ba2513d85133ecfd734878df01cb17b77b2" + }, + { + "id": "7ddedf1d-ebec-5b62-b320-aef11e1d6b39", + "version": "MTc1NjMyNzg2MDM0OTg1NzAwMA", + "digest": "sha256:ecf327ef2ca704a3e48626ad95c4fdd598f90884e77bb1e09dd2c3cf1df16d68" + }, + { + "id": "7dfa6184-cf20-5fe6-951c-0b22f7d1a2b0", + "version": "MTc1NjMyODE3MDAyNTkyNzAwMA", + "digest": "sha256:06a82b2cfeb4a30a9485a79c3b9aa2dfdae19fa1be3ada21599ccdcb89bfba8e" + }, + { + "id": "7e11c0a9-7661-5b0e-8984-d677f92c7f48", + "version": "MTc1NjMyNzg1Mjk1NzEwMTAwMA", + "digest": "sha256:9df35843acf01e70a5f44cdceca22390c68b4758235b888b87b086e0d9c5fcd4" + }, + { + "id": "7e129222-5b56-5b07-8f3f-6acfd7c86014", + "version": "MTc1NjMyODA2Njc2MDA4MDAwMA", + "digest": "sha256:8fb91e891a3e4a41d9aee403331af97bd0444ef491505913e50415ac8f0648a3" + }, + { + "id": "7e2bfbe8-a476-555b-ab40-192b3b0473b0", + "version": "MTc1NjMyODE3MTE2MTMxNjAwMA", + "digest": "sha256:1d20de4097548e349883133219035037e6d6319aab5f2961a82736c7f8a303a5" + }, + { + "id": "7e42b47a-c64e-5993-bbbe-ee493f4a4572", + "version": "MTc1NjMyNzc2NjM3MzM4MjAwMA", + "digest": "sha256:e75bf4e47d2dd1c30d043fd77cf47645e7e75b2292407339123acc7b69b83c02" + }, + { + "id": "7e4cbf21-9649-5d3b-80a1-2881c0047f43", + "version": "MTc1NjMyODE3NzU1ODYwOTAwMA", + "digest": "sha256:fdc5d13fe2597d9534fb125eb1933e8a6c4acb6eb1cee267dec955b90ef49bb4" + }, + { + "id": "7e5bf893-8a0b-519e-8f31-d114f49b7f69", + "version": "MTc1NjMyODE2ODk0ODk2MDAwMA", + "digest": "sha256:2331b27cf14e9c9c7a128ce5bdda8a3cd71ac5ffbfe102d645866f3cbe3eb8bb" + }, + { + "id": "7e714aae-dbb9-5e84-a084-57517c39f643", + "version": "MTc1NjMyNzc3OTc5NTE1NjAwMA", + "digest": "sha256:b5c849ee0cc77a35a000d231a981cce1878b711df3dff3e76cde99ae4902ecb7" + }, + { + "id": "7e85b0df-dbf9-594c-94af-d8eab1048214", + "version": "MTc1NjMyODA3NzQxNDg5NjAwMA", + "digest": "sha256:6bfd80882a964ba01e63d7838ce5910771ee17709218e54ff27bc30bcb5b50ee" + }, + { + "id": "7e947dcd-da5f-5f2c-b844-54feb98da9ce", + "version": "MTc1NjMyNzc2Nzc4ODg4ODAwMA", + "digest": "sha256:a8188e3db28dbd223d17d1a451917bf6d799e7ba5b00e7e64a0fdd622d0c0b9d" + }, + { + "id": "7eb54a88-35ec-5504-9ca7-8c70226cf959", + "version": "MTc1NjMyODE2ODkxMjkyOTAwMA", + "digest": "sha256:3db8b3479be2f9db2bc93289cd34e514e8afb7b79babb4e75993248bcd351829" + }, + { + "id": "7eed9994-bbf7-5aac-9123-027a0fd44d46", + "version": "MTc1NjMyNzc0NzcwNzI0MDAwMA", + "digest": "sha256:622d8b4387dc5cecdb83ad0452d4b5f9dbc1c9486de1fa36566e4069094cdb89" + }, + { + "id": "7f2800ea-cf67-52d6-881f-b2d653eac068", + "version": "MTc1NjMyODE2NzAzNTg2NTAwMA", + "digest": "sha256:ddfb44ba0ba631746cfba7f974cb6a0cc087b330d7fdb8e3dce94d55f138c46c" + }, + { + "id": "7f2bbb4d-49a9-5e23-88d4-4d11d812de7e", + "version": "MTc1NjMyNzg1NTY5NTg3NTAwMA", + "digest": "sha256:1d1365d758afb009881d3bc7819e8458b86c224b58fb33a517dff9aed9976459" + }, + { + "id": "7f3d46a0-f2b9-5a81-8d4f-21a98b8266f8", + "version": "MTc1NjMyNzc3OTc4NzA3NDAwMA", + "digest": "sha256:a13f7a168e4b9c365336d894df584000b4e4a0c6234c0eab0e51df0bd5d38128" + }, + { + "id": "7f4a3b56-7efd-52ff-8a03-52932cd7c8d5", + "version": "MTc1NjMyNzgwOTIxMDc5MDAwMA", + "digest": "sha256:cc033a43ccf12e200da9b18221131d988b7e6a72559b829a3ab383e15402076a" + }, + { + "id": "7f4b8627-7820-5dc7-b979-4db61d99c317", + "version": "MTc1NjMyODE2Njg5MzU1NTAwMA", + "digest": "sha256:06f55509091c3b79d1a7368a5adb2beb5859095d7ebee2e38c335cc7fe3bde33" + }, + { + "id": "7f4c245f-32f0-52ab-bb03-fb0dea6330f0", + "version": "MTc1NjMyNzgzNjMzNjYxMzAwMA", + "digest": "sha256:426594bfa1ffebe84ac6e42cc7f076e13a536c2800ebb93c2834ea3aa57a7a09" + }, + { + "id": "7f6c3cde-ccfc-54d9-9634-a2b679b0dd46", + "version": "MTc1NjMyNzc5NjA5NTMxMDAwMA", + "digest": "sha256:4b17926417da58a5172fd3b29a5f7780027b1781ac5b25e383c872ad6eb83778" + }, + { + "id": "7f6d9c76-4b74-5efc-b5ed-07f33caab10a", + "version": "MTc1NjMyNzc4NzQxNjE2MTAwMA", + "digest": "sha256:5a8dd333b740e2ad84551dfc5bbcb03d112cce61c7d26f5a8a7c6debdb25a262" + }, + { + "id": "7f7a8362-90c4-5dc7-ab0a-1b3fe911a4c9", + "version": "MTc1NjMyNzczODQ5MjkwODAwMA", + "digest": "sha256:5a60e5774a5eb21535c947c564e3edd50f096d7dc0c5537ed4a43126a400570d" + }, + { + "id": "7f966c73-387e-50cd-be3b-70a7eb87f26c", + "version": "MTc1NjMyNzgxODk4Njk5NTAwMA", + "digest": "sha256:cf8b4c052258c39b93b9355563c905c47d78ed40eb13acd20dc042ac10da5403" + }, + { + "id": "7f976a22-dce9-59b9-92d9-b20acbc47d53", + "version": "MTc1NjMyNzg4NjU4NTAwMzAwMA", + "digest": "sha256:bd905b627df382dc34dc48a62497a2ff287a2f45c24be4dd4a0c437e95838e99" + }, + { + "id": "7f98f46a-01c9-5c43-955d-7fd3e33bd88f", + "version": "MTc1NjMyNzgxNTYyNzk4OTAwMA", + "digest": "sha256:4d20477cf5210362541f7e76a59114f540956e8fcb13498830646257ec47ebe5" + }, + { + "id": "7f9b87f1-6d5b-57b1-a9a4-acac613b84db", + "version": "MTc1NjMyNzg2NjY4NzI4MjAwMA", + "digest": "sha256:ec2dfbedb71c85524f3c0c5dd45022920f416a6bbaa141d201f3861ccf445fae" + }, + { + "id": "7f9ca660-c845-52bd-8176-f0c2a444c9bf", + "version": "MTc1NjMyNzgzNTcyNDQ5MDAwMA", + "digest": "sha256:cc78d9ac3cc630926d6a5544bfca5dabcb695625440ba075e6d77c8b7bd6a050" + }, + { + "id": "7fa0d2ab-3646-5140-8644-d079dc089222", + "version": "MTc1NjMyNzg3MTYyMTE2MzAwMA", + "digest": "sha256:d6818bb1e1c9865d31260d8c0e388af33bd35797c07478a5989c87a1927bc66a" + }, + { + "id": "7fa644cc-d279-5f0e-9159-f29300278c78", + "version": "MTc1NjMyNzc0MjUwNDc2MTAwMA", + "digest": "sha256:09b00bb9de05f9a797b42320e9892467b928875e3089f9ee3d13c86c3e3bd88c" + }, + { + "id": "7faed19d-5267-50ac-83b9-774f931fc755", + "version": "MTc1NjMyNzc0NTczNzEzODAwMA", + "digest": "sha256:3a215c0034825c3ad9adaec1fe9577a23b5075cdd99840237890e9be694ee9f1" + }, + { + "id": "7fafd414-9dea-5740-a29e-e58b086e37d8", + "version": "MTc1NjMyNzg4MjA4MDI1NzAwMA", + "digest": "sha256:f8cbd8727d978d29a7769505a77b700ff98d724773fed498a36492cc445e62b5" + }, + { + "id": "7fb84353-87c9-5f24-89fa-6ea487021f35", + "version": "MTc1NjMyODE4MDQ3NTAyMjAwMA", + "digest": "sha256:618815d60f87b3e10ce1ba24ef47219bf54922af8be3915d5c2d83578f10af49" + }, + { + "id": "7fc6995a-9c64-52b2-84e6-7f068dc3b577", + "version": "MTc1NjMyNzc0MTc2Mjg1NDAwMA", + "digest": "sha256:13f62770fced7c0b351ccca161343daf6365b4c3de071a56c0f8cc9da383bc0d" + }, + { + "id": "7fda0dd0-c736-588d-9f7c-8505755ed12c", + "version": "MTc1NjMyNzg2NjU5NDA3NDAwMA", + "digest": "sha256:a340261a3937d1601303a3d68034e340757a4bd1576c5698e821f312c5dc50c4" + }, + { + "id": "7ff7ee62-9af8-500f-8b29-0c3487f3728f", + "version": "MTc1NjMyNzc5NjExMjQ1NzAwMA", + "digest": "sha256:0ef0b6547a8601f74233f79fc77b5957c9d2237586fb7b388902c4f5a1f5faf7" + }, + { + "id": "80079205-7e9f-56b2-a4a5-bd07a442358b", + "version": "MTc1NjMyODE3NjE2Nzk2NDAwMA", + "digest": "sha256:44449ef5d39a56b028c2d1dbdd4161ccf46366412f182e7885b36e8227b87fe9" + }, + { + "id": "801ef12c-101e-5c55-a672-25e2cd74bdc6", + "version": "MTc1NjMyODE2Njk3NDM2NDAwMA", + "digest": "sha256:e1415e991c457fcde2d13425ef1429a694d9d7ab6278828b737d41f6e8245592" + }, + { + "id": "805fd46c-bbb7-597e-980c-f582960a6724", + "version": "MTc1NjMyNzgzODk2MjUzMzAwMA", + "digest": "sha256:979f95da013e4002a045e37911308e0d43297b5c26a4cfb0c539dfc62a5923ba" + }, + { + "id": "8062a556-dde8-5cfb-b867-3ca86a927694", + "version": "MTc1NjMyNzgxNTY1Nzc3MzAwMA", + "digest": "sha256:5b2775391261a4437e073a24a62ff954ebd6a7f52cc2862b84a698f836d2c673" + }, + { + "id": "8068ff28-6694-51c1-83fe-3ec5a8e4d386", + "version": "MTc1NjMyODE2NDU2NTM5OTAwMA", + "digest": "sha256:78cb3b3b4c4b4e3b2773c197d5cd67f3265363d4e0dada46b2907196ff4f22c2" + }, + { + "id": "806d5255-c79e-5fc4-bad3-896db9684f87", + "version": "MTc1NjMyNzgxMjYyNDU1NTAwMA", + "digest": "sha256:85353c04e37ee00e7b69cea8792a4206746dc75bab324cf39cca8979fc911759" + }, + { + "id": "8079ea30-65a6-5f80-a2ff-b61afc0e9ef5", + "version": "MTc1NjMyNzkyNDI0NTA0ODAwMA", + "digest": "sha256:ebab800ff7f5a646354396be8a4c235abc941f88ccf3804f56e0983d2abbf567" + }, + { + "id": "80853f3d-0f2a-53e8-acfe-ccd13de6c6f4", + "version": "MTc1NjMyNzc4Njc5OTExMzAwMA", + "digest": "sha256:9491abb8238bbc26cee4b73178ae3ff5d9204997f1ef5c95a8a62563863a5b12" + }, + { + "id": "80c53131-ea53-5b14-bf64-b25ab741cad6", + "version": "MTc1NjMyODE4MTg4MzAxMTAwMA", + "digest": "sha256:680bfe4a4940326657e3b8f5e74fcbae6a5c8d31bc88f55b2173c4bc78ffd76f" + }, + { + "id": "80cf2468-5ed7-56bd-b9ef-53719306a175", + "version": "MTc1NjMyODE4MjA5NzIwOTAwMA", + "digest": "sha256:d6fbaa980313b9aa9a3b8b74c08c09bc90b8ae8db46658bcf2f86f03c69ea961" + }, + { + "id": "80dd0f26-dcdf-50ac-8b64-f22c598886ba", + "version": "MTc1NjMyNzc4Mjc2OTc3MjAwMA", + "digest": "sha256:50a9efc3092981bb3b317e0553102ae3529cb0987f33219148ab2548bdab6ea4" + }, + { + "id": "80eb1712-1e9e-5204-845d-1cdabb341374", + "version": "MTc1NjMyNzc4Mjg5MjAyNzAwMA", + "digest": "sha256:f3653cd4d6c1be713484fdc4d5510c6ea5a78dfd3b79c48175ea2b72bd2e26ee" + }, + { + "id": "80f1da6f-6097-5754-bec9-2cc9f17a7ada", + "version": "MTc1NjMyNzg3MjEwOTQ3MzAwMA", + "digest": "sha256:181063d07d5ace6a660fec1b6798f6e60a67d3742f13e4f073cb49fb129c581a" + }, + { + "id": "80f41a7f-44ca-5982-8e31-62e128039885", + "version": "MTc1NjMyNzgyMjg3OTgzMDAwMA", + "digest": "sha256:7464d9a50e2001b7d340dd7cc65506a4d4f74a9d4c2f5878f5c12ac7aeeea801" + }, + { + "id": "80f4299b-0673-5cde-8d8a-8fc1c3260827", + "version": "MTc1NjMyNzc1NjAyMzg2MDAwMA", + "digest": "sha256:f97963d274f5144e70c2a892268b668bbe28bb5bf2bb2dd094629e02281b58fe" + }, + { + "id": "8103858b-1f7b-5d7c-abe0-b7ab7a5d095a", + "version": "MTc1NjMyNzczODQwMzY2NzAwMA", + "digest": "sha256:8e00ef60ce81539c7f8dcfdcdac7bb426529e403b9fdfe55f96284cfd11bc622" + }, + { + "id": "81051715-a576-5cbe-9a45-dd1c441abc5b", + "version": "MTc1NjMyNzc3NTE0MDM5MzAwMA", + "digest": "sha256:2b27ab60e4bf211c2c8131ae58633777fc8ec22b8111ec6cac8677be9113885b" + }, + { + "id": "81a1049e-371f-5c7d-a0f3-605ffdc3b5cd", + "version": "MTc1NjMyNzg1MjgzMTg4NDAwMA", + "digest": "sha256:60f05b50103cab2661bee95c9748c917a5136bffb075543bfdd721ce9d7bb627" + }, + { + "id": "81b0c760-4fa4-51c6-8384-8e038dc2eef2", + "version": "MTc1NjMyNzc5NTI3MDAyNjAwMA", + "digest": "sha256:bac83d682e2982f02ac6b6ab9eb389c6bd95c17697887a3d4c2b59a9186e6845" + }, + { + "id": "81ec6fbe-0c93-5401-a227-4aa87666d24d", + "version": "MTc1NjMyNzgzNTc1ODE4NjAwMA", + "digest": "sha256:4d0b57e890127cbac52255e848933dade99421039265e9a7d1748855427d95e1" + }, + { + "id": "81f9963d-c044-5770-b297-288f0f543fb7", + "version": "MTc1NjMyODE1ODI0ODE5OTAwMA", + "digest": "sha256:f36dd6b887e45369257bda3f3e5c8d8e6413ef08e41d38abb3cdc4846bee43e7" + }, + { + "id": "82033587-54bd-5801-9bdd-392de37c4ca0", + "version": "MTc1NjMyNzgwMjkyMjE4OTAwMA", + "digest": "sha256:e37e3f1fc82e71a084174702d4a5da0518f0b3aafbea7d3cac2be80312b79ed2" + }, + { + "id": "821c89b7-b0cb-59e4-bb10-d67a4f3206cc", + "version": "MTc1NjMyNzgwNDU4NjM1MzAwMA", + "digest": "sha256:804e405842dc80053ddf6aa860edb9b173e956e99408dbc3cf7ae8009db10399" + }, + { + "id": "8227efc0-84f6-5305-9c5c-58bbb693a4de", + "version": "MTc1NjMyODE2OTk3Mjk1MzAwMA", + "digest": "sha256:765543c7a6411ae4cc502899a6fbfbce4dd99dd8262a6aaa6bb0a8a71d655fe6" + }, + { + "id": "82409618-baf2-5a32-b27a-cdc0f0dd341b", + "version": "MTc1NjMyNzgxODg1NjY0MDAwMA", + "digest": "sha256:98f5aab9471911d1b909671506401ac8181346c7efdb1b66d8f4e1c464e2ff91" + }, + { + "id": "824b7fb5-0981-572b-b05c-9d9b1eda3a0f", + "version": "MTc1NjMyNzc0NDUzODc3OTAwMA", + "digest": "sha256:5abc0e385e3e48cfda28f7cf11bdebe8362a9611c2a616be12f4ecc611d59c41" + }, + { + "id": "8257016d-d463-5b58-b4d1-da820a183ff7", + "version": "MTc1NjMyNzgyMjc0MzczOTAwMA", + "digest": "sha256:adebc85ecc755794827b2ab200a422c01904c49065afb410048c91ee9587fe9e" + }, + { + "id": "825ace98-2406-51aa-b016-eca3605e6fa0", + "version": "MTc1NjMyNzg1NTc1MjcyMjAwMA", + "digest": "sha256:c691912297628d4b8e21be6458f5452f9fb5f666ade385bfa6122fa8286bfcf9" + }, + { + "id": "8262c3d0-3ebb-5c78-a41e-f60de0a46225", + "version": "MTc1NjMyODE2NTg2ODcxMzAwMA", + "digest": "sha256:ab953bca554ab9721df5f248862e762ca525eff6ef5a968eb3f610800510218b" + }, + { + "id": "8288761a-8576-50f0-af87-e49ee52f6bc0", + "version": "MTc1NjMyNzg4MDM2NTczNTAwMA", + "digest": "sha256:aa992f742d396b222d0838870c5d39379e79e395e73330c3442d50ecfe803112" + }, + { + "id": "82b30e15-0699-5ac7-a9a0-d7c344ae6a99", + "version": "MTc1NjMyODE3NjE0NTg5NzAwMA", + "digest": "sha256:debb22c7c57c0dd5b0b959d37e5d319464fc6c750a8572d86c4896f45ed2de4b" + }, + { + "id": "82b44b6e-9987-51a9-a822-14c7b2e5d49b", + "version": "MTc1NjMyODE3MTE0MTIxODAwMA", + "digest": "sha256:8ba703893e99d4d4e3bfc51289485f7b355bd0d62f157ab631d39ac694d734c1" + }, + { + "id": "82c16626-abad-50a8-a483-054a49ace158", + "version": "MTc1NjMyNzc3MDM0MTQ4MDAwMA", + "digest": "sha256:abc6931ddf802da5ccbd2e3e5bd99eaa79fa14122f3562af186e8aa4ee8510ab" + }, + { + "id": "82ee6539-e4f2-5ba6-8af0-b670cc71f194", + "version": "MTc1NjMyNzc5NjA0ODYzMTAwMA", + "digest": "sha256:3cca527cdafc4e8610df29fa532d8606ac6ddc1e391633917acfc36c7f52e2dc" + }, + { + "id": "83005d93-508d-5a6c-bde1-7703d4acdfea", + "version": "MTc1NjMyNzg3NDA4MjQwMjAwMA", + "digest": "sha256:1264bd3c556a73fa97010531568c495447abce14daf2025afec48eee29a14cbc" + }, + { + "id": "8337d0e9-9c5f-53d1-a67b-e8fc6186faee", + "version": "MTc1NjMyNzg1NDQ1NTA3ODAwMA", + "digest": "sha256:36ca1bae15b843df74a577a64993970a22abfd72b7f2f0c4d0ad0b36d89a2a2d" + }, + { + "id": "83449727-4317-5db2-b1b0-973b328b9c06", + "version": "MTc1NjMyODE3ODA1MzQ1OTAwMA", + "digest": "sha256:f4add03d3a5f21b0b5202fb7be71bfe5468f6b414a433ce2e9e08f35d8137ee5" + }, + { + "id": "83492e7f-5182-59f8-8bbd-6241c2e9c989", + "version": "MTc1NjMyNzc1NjA2ODkyODAwMA", + "digest": "sha256:4b903e5abfa32bf95223787ba7ace60480524004b48eeb59c7b716ee8418bee7" + }, + { + "id": "834a8b5d-2148-5f57-852e-216169a0740c", + "version": "MTc1NjMyNzkwOTExMzE3NjAwMA", + "digest": "sha256:82974c0832c10e98af21ea81f00e76c8df1b4e54e862fb98c83d9c72ce4591b5" + }, + { + "id": "8356d291-2c22-5feb-a7c8-7c743dd6eb85", + "version": "MTc1NjMyODE4MTkzNzA2MzAwMA", + "digest": "sha256:e2a9a2e127b8cf9b0cdd69554a3f50f0b9394385514f2da35638868ad0c5776d" + }, + { + "id": "838ee201-be6e-5589-ae39-caf4dbac1f1e", + "version": "MTc1NjMyNzc3MDUwODIzNTAwMA", + "digest": "sha256:cf15e784f316aec206c5ff57a43ce3e820ddf84f9a94b499028937ac2fdab666" + }, + { + "id": "83a30d86-d188-58cc-a47d-bc5566189689", + "version": "MTc1NjMyNzc5MDIzNTE3MDAwMA", + "digest": "sha256:c7580b4e7a14c70d9c89d7bd45cec65a5f1e4143aeff4ffd8abc0fd3ea632b8b" + }, + { + "id": "83b43361-d04e-5501-b855-db7ec27db428", + "version": "MTc1NjMyODE2NDU1MDIzMzAwMA", + "digest": "sha256:0ff4cd63c8a136ab4d67c5ec2a552fa93dd526dc330ea65f81a13ee569e32350" + }, + { + "id": "83df58f2-baeb-5434-b74e-64a784249c51", + "version": "MTc1NjMyNzc4NjkzNTkzMDAwMA", + "digest": "sha256:07ecdca2f298884b4824346d4d1afa43aaf44c70761a352cf44d9726b8939afd" + }, + { + "id": "83e74df4-8e8f-5b98-9300-e0e5e27ffeed", + "version": "MTc1NjMyODE3MDAyMDg4ODAwMA", + "digest": "sha256:71d063c51895da9f860fe445f4c6a37daa69118196abc490a99a487383aac0d6" + }, + { + "id": "842b86f8-1694-5dad-9e15-1a6c4313ec13", + "version": "MTc1NjMyODE4MjA2MjY2MjAwMA", + "digest": "sha256:7ab1ae247d2baacb177a7f9c7020ff903a197f2b3e7cb1f08616c35bc7bc708f" + }, + { + "id": "84392ccb-b7f4-59f9-9297-f64c8b3e4ec5", + "version": "MTc1NjMyODE3NzU4MzA5ODAwMA", + "digest": "sha256:7a7fe1dd5875703b0e3f567d71775646171a5f19cd557f427c05e604e02f89a9" + }, + { + "id": "8443e7e1-99a2-5eae-9f35-c797ce99fd6d", + "version": "MTc1NjMyNzg3NDg0NzI2MjAwMA", + "digest": "sha256:07bcd71de900bfaad818497d088bc3dd68811e3cf04ce5a0c68b43a307b2dc6b" + }, + { + "id": "8455e360-8e1f-5151-8c51-e154e0e7e86b", + "version": "MTc1NjMyNzg2NzU3NjAxMDAwMA", + "digest": "sha256:1a7d55923f03b6c63a5166d767b98c3571394455c11f0dbeb7ace655305d3ef7" + }, + { + "id": "8458c26c-9a39-5882-9595-ccb46c9e1432", + "version": "MTc1NjMyODA2NTc4NzkyMjAwMA", + "digest": "sha256:7b7d742d355116d6780a965a2520c04de1a8c9141da139e6e59d90a33b07ee8f" + }, + { + "id": "84604799-ae2d-5985-89e7-9cea74ecb9e3", + "version": "MTc1NjMyNzgxNjQzNzQ5NzAwMA", + "digest": "sha256:b7ae5a7f2bb33699da28fd04768a0b452a44c81798f1435b40ddc13ce07ff274" + }, + { + "id": "846aa3f4-7074-5a18-987f-789801bf529b", + "version": "MTc1NjMyNzg0MTA3NTA2MTAwMA", + "digest": "sha256:d8b19ba13467fac81dfcfa9fd774c593a93533af3ccd3f8f948e0df90595f3da" + }, + { + "id": "8498fb23-ee7b-5f03-bd2e-587f497b4438", + "version": "MTc1NjMyODA2OTg1MTAzNzAwMA", + "digest": "sha256:a6d0369c76a9c8f5dd2c97a243fe51fd2822b8874f95943f05f035794bc88517" + }, + { + "id": "84b3a30d-7924-5936-8e83-8259fe9b2003", + "version": "MTc1NjMyNzc0NDM5ODI0NzAwMA", + "digest": "sha256:e03f928173d94e6e0a6312d2ca5ccd4ff1e03564ef47ed976d5cc414b965ea9b" + }, + { + "id": "84c196ea-a770-5afb-8c40-27bc6a37aae4", + "version": "MTc1NjMyODE4MDQ3MDA0ODAwMA", + "digest": "sha256:540cd446b7b6548573696350f43962c0779f6715c3833aff5127c2472e170525" + }, + { + "id": "84d855fc-fc62-5efa-a943-eb644e3b50a8", + "version": "MTc1NjMyODA3NjU0MTQ3NTAwMA", + "digest": "sha256:67b97fe7fb8a8010adcbe958f781690f046bb95d52526b75bdd3b61ed31a4db0" + }, + { + "id": "84e6d293-bbcf-5fe4-afd3-f1071e5aa3a3", + "version": "MTc1NjMyNzgwNDQ0Njg5OTAwMA", + "digest": "sha256:0aa097fe107fc903bf3697763f388d02bdf597244f805ead6425147ae4840e5d" + }, + { + "id": "84fb01ec-4d13-5bec-8d3a-d20fca6e5bd5", + "version": "MTc1NjMyNzc3MTUyMTY4NTAwMA", + "digest": "sha256:e63188528b5f035519c945141fdae87016d2538ba1bee6bd78272d839ab47d54" + }, + { + "id": "850a2540-3b52-5051-bdee-896b2ccb3cd2", + "version": "MTc1NjMyNzc5MDMzNjg1MzAwMA", + "digest": "sha256:711f63dcd9f9addb6ae1951314b3460e6a7302ff727c6689178263e16afb5c9e" + }, + { + "id": "8517b57f-4e33-5116-8b74-9efa699f1345", + "version": "MTc1NjMyODE3NjIzODY4MDAwMA", + "digest": "sha256:943efbd2be7bf3aee8f01f110b6738a563b2e746962495bbb14b3acd40752e1c" + }, + { + "id": "851d621c-c83d-5015-8e5e-530f8facdbf1", + "version": "MTc1NjMyNzg1MjgzMzQ3ODAwMA", + "digest": "sha256:0113fb5b2512a9338f16b6ad61d8dd646ee507bad9ca625c5c75ea23aeb0c2ae" + }, + { + "id": "852a9cff-1903-5acc-a9af-f12abeed5789", + "version": "MTc1NjMyNzc3MDUwNzI2NTAwMA", + "digest": "sha256:8209f9b11cde3bd12399b92f8dd017dcc318e6cd3da50b448a54e9d516d9a7fc" + }, + { + "id": "852de84b-75b4-5b88-8621-167372841c31", + "version": "MTc1NjMyNzc1NjAyOTQwNTAwMA", + "digest": "sha256:232fd9dd23cdd820af4fc99f945de9f02340cf0df41edd403037c7d71bfeda28" + }, + { + "id": "852ea02c-24e7-5d3d-a350-1a667505e65a", + "version": "MTc1NjMyNzgxNjMzNjIzNDAwMA", + "digest": "sha256:fc365a520db7f772b8cd673bbb28c3ebb63452dba2f22a204f31b4aa8f2025cf" + }, + { + "id": "853c33d8-73b7-53bb-8acb-ab409560b439", + "version": "MTc1NjMyNzc4MDg4OTgxNjAwMA", + "digest": "sha256:b790941238e1f7bcfc7298201bc2d303d045fa4d7c0f2889a8f2ca45eee8a42d" + }, + { + "id": "854086d0-5556-55a1-ade7-d506c42e9ab8", + "version": "MTc1NjMyNzc3Mjk4NjY4ODAwMA", + "digest": "sha256:f2b794472426a4b375c222601895196a6e8a9b7c27cf26b9ca3b61ed217db374" + }, + { + "id": "855b4def-fd0f-5ad3-937e-88f760c0a1e9", + "version": "MTc1NjMyNzg4Njc0Nzc2MDAwMA", + "digest": "sha256:e941bfbd49110095c584a030700ce35272b3a106d83e6af4f4f9453311fed155" + }, + { + "id": "857613af-d767-5ef4-a027-c00c1a30876f", + "version": "MTc1NjMyNzg3MDQyNTM0MDAwMA", + "digest": "sha256:8a30fef7354ccb5635b2d8a8479cd3d97ad2f1e4ce958ee3576d53d785d90013" + }, + { + "id": "857a2f37-27af-5418-b976-7627def6ebd6", + "version": "MTc1NjMyNzc5NDc4MDM5MjAwMA", + "digest": "sha256:4786967dc01d7e896009dd70ae9626cbfa78aae7e21be473c80230b698814a53" + }, + { + "id": "857d12e2-d1c6-5a1d-90ca-25b54f6916d0", + "version": "MTc1NjMyNzgyNjM5OTEyMDAwMA", + "digest": "sha256:4fc60712ad73b71261552d500855d8c4256e494f5799a818273befa63a6b1ad7" + }, + { + "id": "85a1c86e-84af-581b-a1d2-81fd5859a0cc", + "version": "MTc1NjMyNzc3OTc5MzEyODAwMA", + "digest": "sha256:79986efee54af781e250c7cafc1345d770888138b9148d67bf819bc6b56ddee5" + }, + { + "id": "85b6ccff-eb65-542a-9919-05408e3b6658", + "version": "MTc1NjMyNzkxMDg2MDM3MDAwMA", + "digest": "sha256:6ff11efa1f1aaa4e142f86a1c7081f0420fd29dce1bc0b7555c6b450a81d27cd" + }, + { + "id": "85d334ba-7c6a-5b70-b980-6bd81082de03", + "version": "MTc1NjMyODE1ODE1NTc5MzAwMA", + "digest": "sha256:988cd33223052a9e98e39f3a0338381da347f982fbefd3eceb628fc14e8a1e31" + }, + { + "id": "85d43b79-0a8e-5f1a-90b3-63c69aea3dae", + "version": "MTc1NjMyNzc0NTc0Mzg3OTAwMA", + "digest": "sha256:4b7a6c98d021d1fb88d44096b8e7291ff422f965f712fad4bf1593e00abf5951" + }, + { + "id": "85dc491b-8cb9-51fb-b19c-efea029a3192", + "version": "MTc1NjMyNzc5NDY2Mjc1OTAwMA", + "digest": "sha256:9404e47ee7c1fa65cd6c3e1fc460af28d948a52bd7fb404211d1bc40a51602a8" + }, + { + "id": "85f73a72-65ab-5fd8-8ffd-f3dc15af6b00", + "version": "MTc1NjMyNzg3MjA1Mzk3MzAwMA", + "digest": "sha256:f78248afa61d4266633acbf7e2eb1659dbc2388e9998e79f563fb11b698dc3bd" + }, + { + "id": "861af509-3b60-54a9-b979-6b0547efd33c", + "version": "MTc1NjMyODE4MTk0OTM5MDAwMA", + "digest": "sha256:2c18b507eee8a18966118bb450fb51a52417df591747e74acac40a664f10f664" + }, + { + "id": "862497bd-5d2c-5370-b936-6487b815e5ba", + "version": "MTc1NjMyNzc4NzQ2NzAzNTAwMA", + "digest": "sha256:881137f28ff1dc72fbfe9db05e152bac05ac4fcbe3af49cc5b81271eb75f7bbe" + }, + { + "id": "86323dec-fd6b-5e06-bc4f-ec75272714fc", + "version": "MTc1NjMyNzg2NzYzMjIyOTAwMA", + "digest": "sha256:1b1d3988508388a3349a3b8ab17e423d73dff292183cc06c31ceb171c623aee8" + }, + { + "id": "86697cf0-7c3b-5ea0-8298-e145d774922a", + "version": "MTc1NjMyNzgwNDU4ODg5MTAwMA", + "digest": "sha256:d014a7f5c244750f4c26c9363fff6f5d45431c7ddf7ca82aa6290ce8abd36196" + }, + { + "id": "866f2d08-79a8-513f-b6bf-36eb1b06be36", + "version": "MTc1NjMyNzg1Mjg3Mzk1NjAwMA", + "digest": "sha256:2cdeb99adaff0064dd5095d9ef37255f50db4bce7efff92845dac287ceae7bea" + }, + { + "id": "868ed155-e231-5059-9154-67e002920890", + "version": "MTc1NjMyNzc1NzQ4OTU1MjAwMA", + "digest": "sha256:b875c3029c96ce6379c7c3ddf5de6b98064835f526feb4d6567f39c8faacf5ce" + }, + { + "id": "8693c76f-09a7-538e-b521-759026d8b627", + "version": "MTc1NjMyODE2NzAyOTY3MDAwMA", + "digest": "sha256:8001a46007920478413209a19c413a262bdbbd7dc0f8116df105e420cf54aa93" + }, + { + "id": "86b1cb22-1ddb-53cf-a66c-97b5fd696877", + "version": "MTc1NjMyNzgzNTY5NTcwNTAwMA", + "digest": "sha256:54fd3bddba8c439069df1c6412c019164e093b7c5a0f02fe312677ef57f7b388" + }, + { + "id": "86b55b62-c804-5f29-8f6a-2e8983d622ee", + "version": "MTc1NjMyODAyMzg2NDc1MzAwMA", + "digest": "sha256:14eeeddc062dbc91620307538f2cc546167572c5df590617fd9a06030147e074" + }, + { + "id": "86c775be-44c6-570f-97d5-5406cd0508e8", + "version": "MTc1NjMyNzc4MDg2NjMyMjAwMA", + "digest": "sha256:8a22070bae16ecd48e0fda2e3cb4fe25e3bbe9d02e381a14f9e151e0a21724e6" + }, + { + "id": "86c86556-7d53-52b7-813b-b29e84874d7a", + "version": "MTc1NjMyODE3NjI0NTM3MTAwMA", + "digest": "sha256:3666d4b843c226f8f8c0289fb4fc2a341608f05363e35d1237d71c5170f92586" + }, + { + "id": "86dbfa23-9a0e-5be0-9f40-d55ed3d22179", + "version": "MTc1NjMyNzkxMzE1NTgyMjAwMA", + "digest": "sha256:264e2df2eb713fc374430decb624faa26b2298daf7d0ddb05ae6f4fc2c71b484" + }, + { + "id": "86e95080-e992-5fa7-86f6-e2025bbaddd7", + "version": "MTc1NjMyNzgzNTY4NDc1ODAwMA", + "digest": "sha256:8637232fb691b3d48db57041eac0badbd6b251a910e77565471ba7382fcfad0d" + }, + { + "id": "86f535c0-f24e-5e69-893a-1436ec02832d", + "version": "MTc1NjMyNzc3OTc3MTMwMDAwMA", + "digest": "sha256:deef4ffa112c603101174755464e9fad67e6dd5011aa33e216040139d3572803" + }, + { + "id": "86ff3f09-552a-5bd4-b998-2b35e40e7af4", + "version": "MTc1NjMyODE4MDQ2NjAyNDAwMA", + "digest": "sha256:93d38c3eb47bd687ec22f8789970ab501e998d12213e618e2ef3099fe5075379" + }, + { + "id": "8701d3c9-71a7-5a47-9aeb-9c8e2abaa8eb", + "version": "MTc1NjMyNzc0MjMxOTI0OTAwMA", + "digest": "sha256:90f0c5128efc212d7489d9b0a49725b62b224031a8479e85aeaf4cd3a5995467" + }, + { + "id": "8701feca-81fc-5640-9f27-7cac7d1e68f4", + "version": "MTc1NjMyNzgwOTE1NzQ2NzAwMA", + "digest": "sha256:e3470ccf6db851e515fc7626998f62e6b47c954af847642e84a3567a6a56f5aa" + }, + { + "id": "870f385a-b761-50de-8d0a-3d13102923f4", + "version": "MTc1NjMyODE1NjM4MzM5NTAwMA", + "digest": "sha256:c1ccf9109e2c1449a180d5682a4da283fada198253f3a0617149a64a4bc475f0" + }, + { + "id": "871d7331-34a3-5d66-a460-1201179641dc", + "version": "MTc1NjMyNzg2MDMzMDgzMDAwMA", + "digest": "sha256:f330e2885e05884183bb5ce257ef6adf3547ef20a1a69e8f4a5877b0c3ebc4e8" + }, + { + "id": "87272c41-031e-5bdf-bd13-34abaf2c3232", + "version": "MTc1NjMyNzgxMzY0MjA5MzAwMA", + "digest": "sha256:e3f0f349101f37a4ebf6cd8d8e50cb74773fb29bc5a8b83f47d1c4487999eb49" + }, + { + "id": "8734fbf8-eb8e-5241-9c88-426644b14ecf", + "version": "MTc1NjMyNzg3NjYxNDg4MzAwMA", + "digest": "sha256:22927e8b27796c7fbfad1049eb67692bb094d1dc6028a298e715e28ae36c4218" + }, + { + "id": "8763d891-6c48-5f83-aecc-1d8d8e14f93b", + "version": "MTc1NjMyNzkwODUwNzc4OTAwMA", + "digest": "sha256:3b06a7a9c1c7806e9e21655248dbf4c92231ed31c191e827a108f1e3fcdcfd3b" + }, + { + "id": "876da5bd-9183-53e1-bf23-4e029320b718", + "version": "MTc1NjMyNzc1MzYyNjYxNjAwMA", + "digest": "sha256:12d36a40244bc6bb43d60240e06dca1bd2de999706505fc95d5bf093b4e8c2c8" + }, + { + "id": "876ff42f-a593-591a-87f0-e452ac17eff9", + "version": "MTc1NjMyNzc0NzY5MDcwNDAwMA", + "digest": "sha256:d831653fd39de021eeb3925298739b76b672c3b1f735d5896389d6a657eb28cc" + }, + { + "id": "87737b93-c7cf-5c89-9d02-d1962dc02a95", + "version": "MTc1NjMyNzg3Njc2OTg5OTAwMA", + "digest": "sha256:a9bee427064d0c5adf316ce212079b960e7e81620e10c6ba6a55ca3a1b2190db" + }, + { + "id": "8777c89b-d8ab-5a84-a328-f89672551fec", + "version": "MTc1NjMyNzg4Njc4ODM3NTAwMA", + "digest": "sha256:4b0236b9d11c51f1cb9c12507a2e73d76e8e056fec9808ffdebf2c477b216ddd" + }, + { + "id": "877b2f46-b01e-5a08-920d-427c62112776", + "version": "MTc1NjMyNzkwMzYzMDk2OTAwMA", + "digest": "sha256:333b52e7ba169d89c48d40ee187b4d1914acad269781b84690e223df4a786261" + }, + { + "id": "87a2a805-6e91-5844-840c-80fd81f52200", + "version": "MTc1NjMyNzc5NjAxOTkwNjAwMA", + "digest": "sha256:9b5f958dff38a7b4098f1686cd27be8c2bc1330e55abd975bccf0a7abf3eb9c4" + }, + { + "id": "87aa085a-d17b-5637-bafb-698c1d712000", + "version": "MTc1NjMyNzgyNjM4NTc4MjAwMA", + "digest": "sha256:4293ba3d18c8735a792d6baae5fffdee7c72f4ee2948b3c753a50db09d6fc496" + }, + { + "id": "87bbc9d8-6f32-562e-b52a-d279a65ee519", + "version": "MTc1NjMyODE1ODI0NjM3NzAwMA", + "digest": "sha256:fbd8e9a723733bc965974867dbcca1db2b940282c2a4c00899cd9cf933df0b26" + }, + { + "id": "87bdb5ca-0494-5fc9-b95c-fee33f6d8e04", + "version": "MTc1NjMyNzgyMjQ0MjUxNzAwMA", + "digest": "sha256:b6eeb8b5d94737ada001ab207cb2170fb6176ef8ca4a033f51fdfd4e50fab20c" + }, + { + "id": "87e2a41c-04a3-5884-87f2-89ff21ca68ca", + "version": "MTc1NjMyNzg1MjExODA4MzAwMA", + "digest": "sha256:9aefc5c9021d6837fe5276b7ef1ad2f825ec388567afc2889daa867d4c7ff936" + }, + { + "id": "87f89c84-8410-5c99-8385-b160b04bc5df", + "version": "MTc1NjMyNzc5MDI2MzM5MzAwMA", + "digest": "sha256:e335cd920bd5c4c632e722cad8b60889c36a8d66d69f8f775f988a591404f4da" + }, + { + "id": "87fd33e0-f75f-59bf-ac34-1ad9a822a97e", + "version": "MTc1NjMyNzg2NjY5Mjk1NDAwMA", + "digest": "sha256:d1159d6b50d5d87fd9d2623313389f1981126c0b83dd4292d2fcd2e9b9cb0bf0" + }, + { + "id": "880f9767-8d54-514b-9db2-6de9c910d171", + "version": "MTc1NjMyNzc4MTAzMzE4OTAwMA", + "digest": "sha256:37c1a5a5024c05ea15e76dd9a064eee093fa8db8f881d6a2e9508d06ed9b0de3" + }, + { + "id": "88110c9f-e798-5d59-8087-d07282954a59", + "version": "MTc1NjMyNzgwNDQ5NzMwMTAwMA", + "digest": "sha256:787da2df8da1bc13e43e02393c8ce5508304763d374188dbf4c8e5a332d284ae" + }, + { + "id": "882c27cb-32d6-537a-b14d-aba61e0ec162", + "version": "MTc1NjMyNzc4MTk0MDIxNjAwMA", + "digest": "sha256:0392155d386e11dfdcbd083fb6009e664f2bf663755b5019e5b4ebab58f35adc" + }, + { + "id": "883f4333-6fac-5c6b-8731-8a82a0183f9a", + "version": "MTc1NjMyODE4MDQwMzUzNzAwMA", + "digest": "sha256:c3a2692808e2c4e4dc983402fdd191b89493a0dc349022a45e432e628d00d2c2" + }, + { + "id": "88405164-d5ec-5d09-94b6-be3b68d29a42", + "version": "MTc1NjMyNzg3MDMxNjc3MzAwMA", + "digest": "sha256:6d85db12536215f45b5c6e12bf8d7aedfc712d0cdade309e30adefb88bef5fbf" + }, + { + "id": "8840bd23-0a4a-5a52-af1e-e87fc21415ed", + "version": "MTc1NjMyNzc2NzcxNzY2MjAwMA", + "digest": "sha256:41cc83638f612c877a6b5b4ce96939fbee0c16b610aa019c98770fbddbfa5b9b" + }, + { + "id": "884853ee-422e-56d9-a7cb-c95b76be6dd4", + "version": "MTc1NjMyNzc0MjU4NTM5NzAwMA", + "digest": "sha256:6b99729540b82d9941e1e9e4da8768303df9e44130c1702e585536231dd6106f" + }, + { + "id": "884adc7e-5409-5ebe-8622-b4b013862710", + "version": "MTc1NjMyNzg1NDM3MzE3NzAwMA", + "digest": "sha256:e490eec6147f444ef9181d4c8a600109e7b26f11920bdd9fd4df3edaa17d3454" + }, + { + "id": "884cc437-64a6-5a0d-aff6-a210867ae3c9", + "version": "MTc1NjMyNzgxMjAzNzY3NTAwMA", + "digest": "sha256:bb1b86942844551176d65fed174f8fcb3fa4fbf81f662e013b9e76fcea1e0f3d" + }, + { + "id": "885c3319-1968-5d83-bd0b-2a1e6932f67b", + "version": "MTc1NjMyNzgxNjQ1MjMwMjAwMA", + "digest": "sha256:28104311a27b779dcbb9a958691aed359c41e8c2a5d61e574ad2e75fac6b1d4b" + }, + { + "id": "885d4e1f-f585-5ea9-aaea-9fd862cd8b4b", + "version": "MTc1NjMyNzg1NTc2NjY4MzAwMA", + "digest": "sha256:f64d873455fd7caf27865754a82f735b2e44ef719424b2f2db3de2aba334e015" + }, + { + "id": "8873617a-a5d5-5149-8a6e-bd642526bc81", + "version": "MTc1NjMyODE2MzExMDE4NjAwMA", + "digest": "sha256:fb5c519ecc208cbcdeef872c696bafb13aa77174aae73c96e44eb06399ec41c8" + }, + { + "id": "88b4facd-1a78-5db3-b896-581d1ab8f495", + "version": "MTc1NjMyNzgzODg4NzAwMzAwMA", + "digest": "sha256:575dbc7868d391e6dd65d41dd16af3fe842995cfa6fe106b1c0526894544db23" + }, + { + "id": "88ba6e6b-90ae-56f5-ba9d-06a7d33f3eb7", + "version": "MTc1NjMyNzgzNTkxOTU2NjAwMA", + "digest": "sha256:500d954162d9ea936961860a2ccdc867545d450a37048648d34d186723d459e9" + }, + { + "id": "88c70fd2-1d14-5c92-9f36-f0b65ae47cd9", + "version": "MTc1NjMyODE2ODk1MjI3NjAwMA", + "digest": "sha256:cfd5567dd43f9a9acb0f30c8fcb292db915d0fc9408081f0c6add0a398d21c3f" + }, + { + "id": "88dd77e3-0f65-546e-b5fd-91863809dcd9", + "version": "MTc1NjMyODA2NTExMjQxOTAwMA", + "digest": "sha256:b5fe62291f0d9ea56937fca94394d9cfdbebacee18fc56c708ff5466c52ba8c4" + }, + { + "id": "88f6a01c-7881-5d75-9c74-a41cc3514751", + "version": "MTc1NjMyNzc3OTk0NzUzMzAwMA", + "digest": "sha256:d05ff34bac02fac17c5b71a48c931f075292248748f873d9a2cdc9084db448f0" + }, + { + "id": "88f6f1ad-656b-56ef-bcbc-0b4b4a8205ce", + "version": "MTc1NjMyNzgxMjcxMTY2MjAwMA", + "digest": "sha256:a1abc18419f92ab66721da97b55d8931f09aecd3f129c8cf40120112fba4b1d5" + }, + { + "id": "88fb9bfe-0bb8-5d4c-83ee-8a9f5abae1eb", + "version": "MTc1NjMyNzg3NDg0NjQ0NDAwMA", + "digest": "sha256:1620ff894d16211454670fc9d5995357a3b508258aab8ebe3d7b200e6209448c" + }, + { + "id": "8900bb8d-0e46-576d-9dc9-d085d39d9f5f", + "version": "MTc1NjMyNzgwMzUzNjAzNDAwMA", + "digest": "sha256:65c4be979d131ddc68abb107f75486e9a2a62bf315dd12b0c41741c773f3591f" + }, + { + "id": "8934589b-6176-536e-8754-a92124142fa1", + "version": "MTc1NjMyNzc3MTU3Mzc0MDAwMA", + "digest": "sha256:381355ad573759b31ae5ea8f4a57f76daf2af88d0add41c99ccea25919daa59d" + }, + { + "id": "8936c52a-01d3-500f-9a7b-0ca2994afb80", + "version": "MTc1NjMyODE3ODEwNDQ5NjAwMA", + "digest": "sha256:842796d55c91d22bb725995ebd9b1ac441d8ccff3deeb6219242b3a3fa334901" + }, + { + "id": "8948a8c0-9cfb-5d89-809a-890bbcb711bd", + "version": "MTc1NjMyNzg2NjcyMzUxNzAwMA", + "digest": "sha256:a8bfe2c480bcd6fead08c6cb2cf2baf2384cb90a2b48d2499eedcb86877d682d" + }, + { + "id": "894b5947-cebf-5840-8dd8-a10f30f18a9d", + "version": "MTc1NjMyNzc3NTEwNzE5MTAwMA", + "digest": "sha256:caa551fb29bf1c36725ec1e93c59579e5e335c7a9f5c09f84f9332b18006ccfa" + }, + { + "id": "8968aeb0-0f45-5f74-b7fd-ba934d8fe485", + "version": "MTc1NjMyNzg4MjA3NjY0ODAwMA", + "digest": "sha256:4b09124e0f06794c63e8b316df2874ebaf408896dcc6d51a9083aa4ed32bb59e" + }, + { + "id": "8979db7c-eb2c-51bb-8d7e-ab8655572392", + "version": "MTc1NjMyNzc1NzQ1NTEyMzAwMA", + "digest": "sha256:7d2e848acf5d96a57d31fb3e7a22ee849732ed237e80e627f5a132c077e853e1" + }, + { + "id": "898b52e5-5d39-5bce-b42e-f8c504027d94", + "version": "MTc1NjMyNzg4MDQxMjkyNDAwMA", + "digest": "sha256:7055dc7d29ce7340e028eb89fe68e0a0dc1ffdedbe8a2930cfb584743b8d7c59" + }, + { + "id": "8997e973-69b1-5934-89b6-df5575f2edfa", + "version": "MTc1NjMyNzc3OTc4OTczMzAwMA", + "digest": "sha256:7d54a09f2ef850abcd75b74215db87747a3cafd520ff833fa16e91091f73d26b" + }, + { + "id": "89a88514-598a-5ef5-bec2-feb9ad015fd0", + "version": "MTc1NjMyNzg3NDg0MDk5NjAwMA", + "digest": "sha256:2a565990a78ff35a42877076e1290a2a8e869f5be6423dbf6937403ae1caa78f" + }, + { + "id": "89aca432-bb72-592d-bbce-c11fcc094ca7", + "version": "MTc1NjMyODE1NjM0NjE2NjAwMA", + "digest": "sha256:874379342203e9b1d005bbd73599665e5d4bf076f9d8911f1a5d5cab987482c6" + }, + { + "id": "89bfd4fd-781a-5d2e-a9d5-6655ea72e67f", + "version": "MTc1NjMyNzc4Mjg5MjQ1NTAwMA", + "digest": "sha256:de2146d7c887eab8751d121627624a7133f8f778155c498526238a3ac470fc94" + }, + { + "id": "89c63bf3-3419-57df-81c7-3b795ac36af8", + "version": "MTc1NjMyNzg4Njc5MTE1MDAwMA", + "digest": "sha256:8bc58a489ffb6a006bccc01c98f196cae31979a89c7669a51eaec83903d66e4d" + }, + { + "id": "89ce906e-9466-5f35-9a33-b88989afca35", + "version": "MTc1NjMyNzgzNjMxMjEwMTAwMA", + "digest": "sha256:e49f9a23ce28f9fde3b4c15c52f2df35f279058587214e4732c5d1f8446e3844" + }, + { + "id": "89d8a9fd-7740-5abc-8781-0ba0a72d753e", + "version": "MTc1NjMyODE3NzYxMjY5NjAwMA", + "digest": "sha256:6cb3a801e41a24386807f0ad5149dae2a52e966e594e3d0165d213636d9af02e" + }, + { + "id": "89d93b7a-3a1f-5bbb-883a-38018139f14d", + "version": "MTc1NjMyODA3NTc4NDIxNjAwMA", + "digest": "sha256:28b74a0302ab40ff550a0bef71b2b89ac71770f9ac8e3f6ed7f17a13720dff4d" + }, + { + "id": "89e0bdb1-a889-5966-8ef7-521092b51894", + "version": "MTc1NjMyNzg2OTUyMTU4NDAwMA", + "digest": "sha256:6b661d03c545f7f36c84fd696b4d7887fa8c30ec862643fc627ff6aa277dc9d7" + }, + { + "id": "89f35a77-3a8e-5ad8-a9ef-d3c5213aaff5", + "version": "MTc1NjMyODE3MTE5OTI0MTAwMA", + "digest": "sha256:3672da232ee6f3b2b011be375158452773114930e360e7864404554bb4395ec5" + }, + { + "id": "89ff67a1-ffe1-5387-ab96-50c7a8804f23", + "version": "MTc1NjMyNzc1Njk1MDkyODAwMA", + "digest": "sha256:f3afd41fcf0afce06a2f8163ae2d3994e6f032c4a57f186fccae1c0ea7cbf768" + }, + { + "id": "8a157cf3-b674-54af-8fc9-b6072237e0d3", + "version": "MTc1NjMyNzg3NjYzMjI5NTAwMA", + "digest": "sha256:01111ddf0889ef23ff34bf8c3543990b7c95f378ddb8e3cfdb4334711567735e" + }, + { + "id": "8a27ab0e-4051-5693-ac2b-8411efa49dd5", + "version": "MTc1NjMyNzg1NTY3MzcwMDAwMA", + "digest": "sha256:b472409917f202b38c826e015947cf6311c88e470ad1e2dc477d5949c63d47b8" + }, + { + "id": "8a303b19-c087-5673-9bcb-42ca7bf2a5f3", + "version": "MTc1NjMyNzgzNjI3MzIxNzAwMA", + "digest": "sha256:4e5d895badac081c6e4274c811ea0845e43cd84ec933dcf288a0e79173e3946f" + }, + { + "id": "8a50c1a8-f114-55d8-82ed-dced07bd868c", + "version": "MTc1NjMyODA3MTM3NjA2NTAwMA", + "digest": "sha256:81db78319f823bf94ce256d14b735576316bac8ea31d4519d6b9376df7c160d8" + }, + { + "id": "8a5636f9-41b9-5f39-81c8-ee135202961c", + "version": "MTc1NjMyODA3NzQ0ODQ2MDAwMA", + "digest": "sha256:dd5e43945405dd8adc0ed5ceb1cb1b40d2b1c14460a6b51875e86fc1deb95472" + }, + { + "id": "8a70ab49-7581-57d8-865c-59f10c6bea2e", + "version": "MTc1NjMyNzg1NDQ1OTc5OTAwMA", + "digest": "sha256:1edac76a03c9201d0f93b78cdb9a402b49f4a102e51ee2a1cd11348939f6c0ea" + }, + { + "id": "8a851231-1ecd-511e-96c0-39b63176eb44", + "version": "MTc1NjMyNzg4Njc0MjYyNjAwMA", + "digest": "sha256:8c7113eae0cd6a82b6e45347e35b7a8dce536f08768295e1becc447c715aacd8" + }, + { + "id": "8aa8f523-c00f-5f9a-aaed-cd06c60e4e01", + "version": "MTc1NjMyNzgxMjExMTMzNDAwMA", + "digest": "sha256:dab0b4905b50427fdeef0e0a91b0fcc5b6bcb7060b9b72a790fa7ae9bd0b7f7f" + }, + { + "id": "8ab842c1-9a0c-5e00-b405-0504a67c2615", + "version": "MTc1NjMyNzg3NjcxODQ1ODAwMA", + "digest": "sha256:249b04cb33b79797970b57537e8165558037cff8d93e5cf8726737366039eeb9" + }, + { + "id": "8aca906e-d81f-5be2-a8ef-257dd5f0dd99", + "version": "MTc1NjMyNzc0NTc1NDE1MTAwMA", + "digest": "sha256:e94ce6b32ed9d832ae4100a815c657712f03f396c063187ddda8b1fa9bd75b2d" + }, + { + "id": "8ad3d4c3-6c5f-5b43-9b6c-c228f72a97e1", + "version": "MTc1NjMyODA3MjU3MTAwNDAwMA", + "digest": "sha256:0d43c25742813965f18f42476ccae3ac6c4f95667ce3079be6c964abb81303de" + }, + { + "id": "8adc6626-4db4-590d-9429-323a3f52ed0b", + "version": "MTc1NjMyNzc5NTE2ODMwNDAwMA", + "digest": "sha256:604275af006014536d0d0afbe5a3fcff2aa898891c13f0ea81757e920a1b297e" + }, + { + "id": "8adca7f1-c5c9-528d-82f7-76236fc50c68", + "version": "MTc1NjMyNzc5MDI4MDAzODAwMA", + "digest": "sha256:2490bff3b692b67afcb5182cfe62bc2458c9ecc2af233705a58d8e144e0c60b2" + }, + { + "id": "8af4178a-0cd8-5653-9ca6-85a14071c949", + "version": "MTc1NjMyNzgwNDU3ODY4ODAwMA", + "digest": "sha256:8ad34dd3ac111894e5937e5a220030ee4c9d06d6feb1ea76270620a353dd4699" + }, + { + "id": "8b1a17db-a364-50ac-ab88-5bfda7a79028", + "version": "MTc1NjMyNzc5MDI4NTc2MjAwMA", + "digest": "sha256:3b33b0a4b1dbf471bdc5406d3180472c0ca681582819ec53742436a7bb0d5733" + }, + { + "id": "8b33ceb1-e8f4-548f-8ea8-2b267cfbc9c6", + "version": "MTc1NjMyNzc5MDMwNjM0MzAwMA", + "digest": "sha256:6aad7c1db5b1e12b281394032d1994d09c2edb485f685f6e5945493166b9ff45" + }, + { + "id": "8b3cf94d-ac48-5716-a6c1-7d1b7a476215", + "version": "MTc1NjMyODA2NDAzMTYyMDAwMA", + "digest": "sha256:9110bda634ca7cc2f10bdb4d3858dc90e02ef579f5c848c232a829c73d1987bd" + }, + { + "id": "8b492fee-2168-5206-8ceb-6373e263d9f6", + "version": "MTc1NjMyNzgxMTk1MzU0MzAwMA", + "digest": "sha256:28c5f41fe51f9d130043102a36ed6c3181386897efc78d6bb56392bfdabe7e7c" + }, + { + "id": "8b4f8161-d407-59c3-8a00-7c6ce853c207", + "version": "MTc1NjMyODA3MTY2MjMwNzAwMA", + "digest": "sha256:c06fec603dcf8de1b59f29441e4c46ca1fccfc1b4493f5d30ea18d1b696e7cdd" + }, + { + "id": "8b559b06-4e8d-54c5-a7f3-531d16957465", + "version": "MTc1NjMyODAyMzgzMzAzMjAwMA", + "digest": "sha256:14f05eaef065adf98b637965a9645354d86509c77a60bbc4e284964a92c06558" + }, + { + "id": "8b7fad35-9dd8-50c7-9f1b-cd37872d3ddd", + "version": "MTc1NjMyNzg2OTUyMTgyMjAwMA", + "digest": "sha256:1c49ae70f84c9145bbaaa7ab28dd3ee83b6fdee0d49528a914209ffa0161eee5" + }, + { + "id": "8bab974a-fe91-5496-8257-d79fa9499a16", + "version": "MTc1NjMyNzc0NzcwMzY3NTAwMA", + "digest": "sha256:aff094cedb41686bafd869d330008ac571c21fc8a43d40a2bbcfcfa8a3d534a6" + }, + { + "id": "8bb00b0d-778d-5ded-8140-d31cb4a7ec0e", + "version": "MTc1NjMyNzc4MTA0MzU0MjAwMA", + "digest": "sha256:2f8b18628697f01f989cfb2cddce69cc513169e26d5cd5b0e7df91baa20adfa8" + }, + { + "id": "8bca08b6-e8e1-5760-adaf-76b082e569c8", + "version": "MTc1NjMyNzc4NzQwMjM2MDAwMA", + "digest": "sha256:022783d743f764eee6c65502eefeea83007f6730629e432a5d0a7bdd541f5e3e" + }, + { + "id": "8be825c7-094d-5210-addb-65f9d644313a", + "version": "MTc1NjMyNzkxMzE3MTk4NzAwMA", + "digest": "sha256:7cff3307791c9b96ab58452916c9d7c5cd2d30d8f2738deae7e4d91ff816d1f0" + }, + { + "id": "8c0984a0-a7f5-565f-9fb0-ffc03b3219dd", + "version": "MTc1NjMyODA3MjMxMTU0NTAwMA", + "digest": "sha256:8c211f868ecd9163b2a8f734f2591de37c1d5073a130867422ef06a7cf62f4d1" + }, + { + "id": "8c0dd8a0-5e78-556e-8f87-ff6a71281941", + "version": "MTc1NjMyODE2MzA1MzYzOTAwMA", + "digest": "sha256:facf9ff50dd49c1b24e7f5c5137e6975faff61c1f432f67f45c0adf6923294a1" + }, + { + "id": "8c14f101-4762-56b1-971e-7fbc8980c74d", + "version": "MTc1NjMyNzg0Nzc3MjQ4NTAwMA", + "digest": "sha256:9d6a2533f9e57188c7cd79c150e7af91849fcded4018e0735caf4ed601b8c0ac" + }, + { + "id": "8c2150a7-80ba-590b-a698-555e509386f3", + "version": "MTc1NjMyNzkxMDk2NzY1NTAwMA", + "digest": "sha256:172099d2a2adb22e051d5cbe7a2a4ddf16bd0911c30d6216039f4c95a4578d6b" + }, + { + "id": "8c2bab0a-df70-566b-a8fa-56ee800cdfe0", + "version": "MTc1NjMyNzg3MTUxNTUwNzAwMA", + "digest": "sha256:8188699a0416472e3405438228c0d039ad5c7f2252ca651dab280b6351364ecf" + }, + { + "id": "8c2e887f-ff10-55c4-a602-005407134c63", + "version": "MTc1NjMyNzgyMjQ2NDU0NzAwMA", + "digest": "sha256:cef9df07701eb1b8ab585f5ea9b69059e42e47d5833ac214f6288a472021311d" + }, + { + "id": "8c329a21-36a8-5166-8588-32524ce88c6e", + "version": "MTc1NjMyNzg4Njc4NTU4MDAwMA", + "digest": "sha256:3ce3e3704bd2bb013ea8ac04b202e062dfbcc9fc4f19560c04a9838002ed754c" + }, + { + "id": "8c3d86d0-685c-5619-8374-593394cb4b38", + "version": "MTc1NjMyNzc1MzYzOTA3MzAwMA", + "digest": "sha256:5a5ea50d2f6c37f1132f2762672edce6f178f44f533fd89515d360a06e645f21" + }, + { + "id": "8c4779e2-2350-5612-8da6-8fe20948a3ec", + "version": "MTc1NjMyODE3MTA5NjI1MzAwMA", + "digest": "sha256:a68ee724ec2ffe3e261936c228884138bd8cd5104856be1befeb134390b7895b" + }, + { + "id": "8c666339-e40b-54f8-a201-64007028f350", + "version": "MTc1NjMyNzczODQ2NjQ1MjAwMA", + "digest": "sha256:5acb7eca9e7f867eebad7e9453e4a41b3b19dccdb193ffbcf566cfbbdb5b9e93" + }, + { + "id": "8c674c57-da58-5995-a035-fec57181f3ac", + "version": "MTc1NjMyNzgwOTMxMDYwMTAwMA", + "digest": "sha256:cc4cc3e923b2d6fd1281e79edfbbe1f3a174ee8258d03ec911c01410bac3caea" + }, + { + "id": "8c71e60c-a608-5e7b-a820-65dcc665e288", + "version": "MTc1NjMyNzg1NTY5NDc1MzAwMA", + "digest": "sha256:c6695664ce18c86944c077f18e118f8e7552e4487d53d62d4d66ba359e12d68e" + }, + { + "id": "8c7b0ee1-e543-5898-b1cb-e5e98ab54dbc", + "version": "MTc1NjMyODE4MTg1NDU1MTAwMA", + "digest": "sha256:9a0f8f0a7252462faa6eb2fc431431bc5f790e65c952803f9a5d13170b215aee" + }, + { + "id": "8c832c4c-3dca-54f5-98a4-8c6443443b21", + "version": "MTc1NjMyNzg2NzYwOTE5MTAwMA", + "digest": "sha256:4f68a3d4915acac7009fab2295249018c30eb87b0fc24ef0f78ec4222f0dc34b" + }, + { + "id": "8c9ab1fe-17cf-58ab-9bcf-2ece3ff771e5", + "version": "MTc1NjMyNzc3OTczNzYzNzAwMA", + "digest": "sha256:ab911c3f2f10ee6f1ba3d9e2ce4d7af6a71a2f84b87ecfc1f832b93723c26ecd" + }, + { + "id": "8ca0fd8a-5acb-5099-a579-8f12a2d90a94", + "version": "MTc1NjMyODE2NTg3NzU1NDAwMA", + "digest": "sha256:70f5a0ef3026a6a7eb771740bfcf7c3b0faa1b6e9a19acce15203127b3d18d69" + }, + { + "id": "8cc1a1c1-3d06-5b13-8478-42e760a4bcae", + "version": "MTc1NjMyODE2MzA0ODI0MDAwMA", + "digest": "sha256:303e6cc055ac132689fd0f067573134cdb50a9436148ffba18da54a6c783de2b" + }, + { + "id": "8cd005f9-38c4-59f7-b171-b0ca2603052b", + "version": "MTc1NjMyNzg2NjY1OTgyODAwMA", + "digest": "sha256:ba0ecab2e57f807ecd1db30c82f0a842b5cc101172046384fb6f0efb50bb4753" + }, + { + "id": "8cd4e32f-322f-5311-b3e7-f6f792d51df4", + "version": "MTc1NjMyODA2NDkzMDcxNjAwMA", + "digest": "sha256:07cc5711bcac7d1023904b3757452a23ff43983c9f1b630c29be6efd5ca54e2e" + }, + { + "id": "8cfec97c-1b2f-50b2-937f-ae1c1521eb43", + "version": "MTc1NjMyODE3NDM0NTg5OTAwMA", + "digest": "sha256:4b44f9057f7082e934cec4c0c59675c98590bd557c7064d7208ddf3b2fe480bb" + }, + { + "id": "8d02949b-23f7-5e9b-af88-4dead3355226", + "version": "MTc1NjMyODE3Njg1NDQyMTAwMA", + "digest": "sha256:68f0dfa3e48278cee07e7111503118e308a39601ae841ea9be01f50c2bbf63aa" + }, + { + "id": "8d081ef9-c8ad-53cc-94ac-b203695dd866", + "version": "MTc1NjMyNzgwMzY2MjIwODAwMA", + "digest": "sha256:b01d119e066cd0243f4dab9eb42bb71345d6fed3966882953afa28cf443dd555" + }, + { + "id": "8d20a190-5d72-5118-beec-520a5ff2fc9a", + "version": "MTc1NjMyNzc0MjMyNTYzMzAwMA", + "digest": "sha256:73fecf7d09368dc881c220648278b4652af4199f3918e64fc864b5215bb9a4bf" + }, + { + "id": "8d28c9d0-e107-597d-83a2-e46b5a024548", + "version": "MTc1NjMyNzg3NDk0MTk0NTAwMA", + "digest": "sha256:bf7ebb9f633dc6e736cccad263d2fa8d92ebffc13b96a3bdf1a5077c019838cf" + }, + { + "id": "8d3b26c7-b1b4-5917-9047-307f33f4d952", + "version": "MTc1NjMyODE2MzAwMDI1MDAwMA", + "digest": "sha256:e3332dce7e73de3b4e6a276caa7facfb52076bb834a2505eb3a3b51d7134db6c" + }, + { + "id": "8d4b5d93-ec20-5adf-95c5-247e42903b02", + "version": "MTc1NjMyNzg3MTYxNDA3MzAwMA", + "digest": "sha256:5ef764acea9a25564ca6bb678856a84c3d7c07f6748591978d6072cde769ea55" + }, + { + "id": "8d66e84a-c9bc-55c6-a594-e9c75d546c36", + "version": "MTc1NjMyODE2NDUzMzkyNTAwMA", + "digest": "sha256:ba61801b73c7747cdd34f348fe655308265e7ca4fdfe31040173b692cbf0ec87" + }, + { + "id": "8d6e2396-a62f-584f-93bb-32000f0536a1", + "version": "MTc1NjMyNzgzODg4NTU5NjAwMA", + "digest": "sha256:1cac972d325f9a46672e6a124dd60a3dfcada0f0b2001c75d03e827b4a0fcfd4" + }, + { + "id": "8d7bd9c6-e3b4-51b1-bfc5-3ffa4bb27344", + "version": "MTc1NjMyNzgxNjMxNTg2MjAwMA", + "digest": "sha256:a34abd663d436d9406adcece44e5a63882cff473422da4d6f324bb9c81ef8828" + }, + { + "id": "8d86cd3d-57ae-5953-87ec-f6bb98ddddc3", + "version": "MTc1NjMyNzc3MTQ4Njk3NzAwMA", + "digest": "sha256:9d2d74606a2edcc89ee3ad620cec2f97297a1f972fc814c22f1e313ba2377c03" + }, + { + "id": "8db7b957-7c87-5c51-9979-0381a67fe9bc", + "version": "MTc1NjMyNzgzODY1Njg1MzAwMA", + "digest": "sha256:5bf85e4c59f0dc89a1e40f434d399ab293bb4ac26c368a0df74c898248446b48" + }, + { + "id": "8dc39d8d-dfb6-5063-be9f-17f42964c97e", + "version": "MTc1NjMyNzc1NTk2MTM5MDAwMA", + "digest": "sha256:7856278859771dd1ae6cb1b950f3d0102b94830319ccacd1b6ed61cf0e180e38" + }, + { + "id": "8dcf1249-955d-5bfc-839a-fe6ff5f98d3f", + "version": "MTc1NjMyNzg2NjYyNzAwNTAwMA", + "digest": "sha256:d66f378674138e33661af7f90933455781248503e7df55e8abce45baba34fc99" + }, + { + "id": "8ddbea62-3f84-5a3a-a638-519fdb44cece", + "version": "MTc1NjMyNzc3NDU2NTQ1MTAwMA", + "digest": "sha256:3ce36eb6b0d2e85e4cb0d61b678cebecdd56f64fc808c1d5e44451ec0a19e5a3" + }, + { + "id": "8defb6df-c26a-5ebd-ae74-e337f3dcbe89", + "version": "MTc1NjMyNzg1MzM4MTEwMjAwMA", + "digest": "sha256:40bef08a89a61c3f638eea4242d62809b119baed94102f746af922a31830f51d" + }, + { + "id": "8df695ff-b269-593d-bc9f-368423a0eef6", + "version": "MTc1NjMyNzc5NTI3MDE2MTAwMA", + "digest": "sha256:674b456085837903c90be6db3af412215288e7011b24a88591e9b6ed4833f076" + }, + { + "id": "8e156a8a-972e-57d8-a254-7e110d4e2b01", + "version": "MTc1NjMyNzg1NTc3MDkyMzAwMA", + "digest": "sha256:367b94a33e5022678501351f198a79bd9abac399f1889d22eef72a7fb40ece90" + }, + { + "id": "8e30e509-a8a4-58a5-aff6-408abed07911", + "version": "MTc1NjMyNzg3NjU4MjgwODAwMA", + "digest": "sha256:a4ae8c6406d0054dc64ba54eb40df879a2d82a98a0099fd06559a7445991219e" + }, + { + "id": "8e3f8860-07ad-5415-b789-456de88d7af0", + "version": "MTc1NjMyNzc0MjMxODYwMzAwMA", + "digest": "sha256:374079e21ea9c91cc719a69b1ebfb4a3a875a936b35717c41980a35cb41f1002" + }, + { + "id": "8e4a6c0d-25a6-5f9f-89e6-365bf9d57812", + "version": "MTc1NjMyNzgxNjQ5MzA4NTAwMA", + "digest": "sha256:6fef7e914da7a8f8cf76119a11882971df2a553e4f4e135b7e988ea81b648287" + }, + { + "id": "8e68ef95-b627-509d-a36f-c07eb8acb8bd", + "version": "MTc1NjMyNzg2NjMxOTAwOTAwMA", + "digest": "sha256:4961047596e65682f310b2f07c001af861d3765cdc4bb78e4d5b591313e7f8fb" + }, + { + "id": "8e704dcd-79fb-5347-a1ad-21e2131d2b34", + "version": "MTc1NjMyNzc1Mzk3NDc2NTAwMA", + "digest": "sha256:0703e91e948dbbae903f9d665d0d49c56857baa387bdf0c57a073045076b68ef" + }, + { + "id": "8ea7e199-d47b-58c2-8c6f-589c6f5cc9d8", + "version": "MTc1NjMyNzc3MDM3MTgyMjAwMA", + "digest": "sha256:b8f63ae50afaac5536f7ce846d02770559971b70970623119c5b4956099e758b" + }, + { + "id": "8ef1d537-ed48-5e2a-a6dc-a810e3a4518d", + "version": "MTc1NjMyNzgxMzU1MzUxNzAwMA", + "digest": "sha256:7e8a3de5234f707fb2cf362a199c30d75e5c3791b0ea9a35937fa6e97126c9af" + }, + { + "id": "8f00e694-6f93-5b17-b37b-131844e73de8", + "version": "MTc1NjMyNzgzNTY3ODcyMDAwMA", + "digest": "sha256:0b332661bbc627d2e4a236c9f98238a7db4a2cdecf8d3ef2e07b7a4443d289b2" + }, + { + "id": "8f011b96-e660-503a-833c-d78c14333ea9", + "version": "MTc1NjMyNzgxNjUwNjA3MTAwMA", + "digest": "sha256:5d09098ca148d5e75c6d7b500945b9de5b7356f16894ad69f868d7aaa962bc63" + }, + { + "id": "8f091fb6-8258-57a4-a4dc-3fd2e2225e30", + "version": "MTc1NjMyNzc5NTI3MjcwOTAwMA", + "digest": "sha256:97bd15422673f53ae19166b695e334b961877958eb50076f380066b12541a1ad" + }, + { + "id": "8f23005e-2114-5cf6-93a2-2497da88cbb0", + "version": "MTc1NjMyNzc4NjczMjU3ODAwMA", + "digest": "sha256:a176225f3ab2687aa74f7d6c8a613ae13ffa35e08e40052b4f1790e6439ec034" + }, + { + "id": "8f3af148-6ef9-5fff-98a0-b9c142d5a7a7", + "version": "MTc1NjMyODA3MTYyMjE2MDAwMA", + "digest": "sha256:6c2ac9aded1cf115cb5134ce9ffa6c65f74658129c632c029e15ce9624e665a9" + }, + { + "id": "8f7a9098-2941-5a1c-9162-7b8f8b616682", + "version": "MTc1NjMyODA2NjkwODQ2MDAwMA", + "digest": "sha256:4a3fbc81e1df57e6240899d07c05a637178ae83a944c37cb9ae996d1f687b4f0" + }, + { + "id": "8f99d232-a455-502d-a354-005758e67bbe", + "version": "MTc1NjMyNzc5NjAxOTU3MzAwMA", + "digest": "sha256:fa50dabf31987bfbdc38d7ff23692f7665cb8dd17dc6be110538430f2ec88ac2" + }, + { + "id": "8fba4cb2-396d-579e-b8d7-dc56a263b975", + "version": "MTc1NjMyNzc4NzM1OTE0NzAwMA", + "digest": "sha256:6a5e1d91f462915c7b5aa30a84872ff4b59f59ba98181f1098a2daf7459c6eed" + }, + { + "id": "8fcb415d-a7cc-5509-bf88-2e88efa0dd95", + "version": "MTc1NjMyNzc5NDc0ODA4OTAwMA", + "digest": "sha256:4e3aac59623dbd690dd9365ea4f3cbab841acb19b91c76a9215c5f1f6a553d50" + }, + { + "id": "8fcd4c3d-28b9-5938-9e6e-c86df352ba8a", + "version": "MTc1NjMyNzc1MzYzNDU5MjAwMA", + "digest": "sha256:ef59bb073a82003689fc29f7521b6269f851ea90e6b3b5abc4a62b910f667dde" + }, + { + "id": "8fdd8eed-8cc7-5eac-932d-52a122fd1075", + "version": "MTc1NjMyNzc1Njc4Mjc0MDAwMA", + "digest": "sha256:8c963cf024c4cedab48b2d8318232b385ec8c656bbd6c35bfd8066788b3d3f7f" + }, + { + "id": "8fffe317-2a55-56ff-81f8-595a02056995", + "version": "MTc1NjMyNzc2MjU3MTQ3NjAwMA", + "digest": "sha256:681e6681ca19410b233e6e83d4561ab0535aed8ed5e1f15d5452ee15f307b731" + }, + { + "id": "900003c5-8e88-5131-a040-ea12be8fec5c", + "version": "MTc1NjMyODE3ODAyNjk3MTAwMA", + "digest": "sha256:631847f4ad65a338418bd2427ac4665c22b490159e15cbbab30e2ef7926adf05" + }, + { + "id": "9015f08d-42a0-5bae-8229-41f4be9bdf3a", + "version": "MTc1NjMyNzg3Njc3NTg4MjAwMA", + "digest": "sha256:14ec5de2881585273611a0ca752bac674b9ded9b971c0e008f9a26de4f0ad41f" + }, + { + "id": "90186e13-1fee-5f1a-bbed-d420ed027487", + "version": "MTc1NjMyNzc3MDM0OTMzMTAwMA", + "digest": "sha256:c017bdedabece280bb2deaf01397d99e1491d03c14cecc8bbf3de9110da17cb0" + }, + { + "id": "90227660-8f59-50bc-9217-b7cc8a6ce83d", + "version": "MTc1NjMyODE2NDU4Mjk1NzAwMA", + "digest": "sha256:728a6cb7d8f8487eadbad7e409057731a0a96939099a7767bfefe28527c77960" + }, + { + "id": "9025b549-8580-5456-abdc-85c02c2c8241", + "version": "MTc1NjMyNzc0NDQyOTg5NjAwMA", + "digest": "sha256:e7278748531d1fb8a9d86f9e5bdef6986c6641bdc662b7e908de70c670f3b7d9" + }, + { + "id": "9026eb7a-0d99-5181-a744-cde56de72cb6", + "version": "MTc1NjMyNzg4MjA3NTYwNTAwMA", + "digest": "sha256:b305c451e0285f1986f28aeffae30cdc17bb7e2e553b0998e262092f7add647e" + }, + { + "id": "903124ef-73cb-542f-8380-51240f150620", + "version": "MTc1NjMyNzg1MzIxOTg3NzAwMA", + "digest": "sha256:0a8b78a9b52a34f17744ec9c22f39a25838f10faacb56a8f58f06cc889eaf946" + }, + { + "id": "90386a9c-461c-5ba8-b109-d92f473cb355", + "version": "MTc1NjMyNzc4MTA0NDIwMDAwMA", + "digest": "sha256:c8671e6f4d523bceaf1011e2aa1163f2e58460b6700faa68c5877c8813fe1cfe" + }, + { + "id": "90777d67-0272-5f11-b818-603ad1b0b800", + "version": "MTc1NjMyNzc0MTY3MzExNzAwMA", + "digest": "sha256:1344878bed28f3b9e7bbe4a33332447c346df38eb90915a07e116c36c7b1aae4" + }, + { + "id": "908eb0dc-9c67-5e35-b7dc-81534cb4400b", + "version": "MTc1NjMyNzc1MzkwNTg3OTAwMA", + "digest": "sha256:254de8f721cca51c76c8b299563a0bc7e7670597d95b707ae4a219cb29132107" + }, + { + "id": "908ebadb-4b92-5489-89ef-5fa6cbff5153", + "version": "MTc1NjMyODE2OTk5ODkzMjAwMA", + "digest": "sha256:8b7b799f1d9dd1310714351cacea65690e5f94a199772f2c8ab39e66f08a0b46" + }, + { + "id": "9092b4f4-8c2c-59e1-bfdf-2d2535467a77", + "version": "MTc1NjMyNzgxMjY4NzQ4NDAwMA", + "digest": "sha256:c2ef39580a433c9438304e43916fb6205ac233640ebafa8f7f5f186f17f0510d" + }, + { + "id": "90b94dd2-6dbe-581a-9e26-5b16fb463c36", + "version": "MTc1NjMyNzg1Mzc5NDkxOTAwMA", + "digest": "sha256:e2986fbde44cad3bb85b63da20f6281ee6f5907922043210f40a345eddc5d349" + }, + { + "id": "90debf0f-2d33-5855-a913-18224f5b3557", + "version": "MTc1NjMyNzc4Mjg5MzI5ODAwMA", + "digest": "sha256:544f13bec605a6588e44476feceefee13708e833e7286bda8aa0508aa6ccd755" + }, + { + "id": "90f6a31d-9c10-552d-86e6-7facd6f691cc", + "version": "MTc1NjMyODA2OTk4NjYyOTAwMA", + "digest": "sha256:3916b8f38fe05b868af88dc5b0af5de258b17ff1f9467b848397f647943a02df" + }, + { + "id": "9101ccb9-1747-5f26-8353-625f101d1325", + "version": "MTc1NjMyNzc4NjkzOTA5MzAwMA", + "digest": "sha256:6023a5c54d7a012e6dc87d1678b12cc7ea11ff64b209d2ecc5c7feceec8c967c" + }, + { + "id": "911a9c91-8f7a-5646-b4a1-5ec04d93dd0d", + "version": "MTc1NjMyNzgxNjM0NzQ5NjAwMA", + "digest": "sha256:d1619f21e6bc9e2b2ac742bb69425620efe8fd4c24888c92eef7fb5a988ab6c0" + }, + { + "id": "911d6af8-cccc-555b-8bc5-d6f7483cd896", + "version": "MTc1NjMyNzc4NjkyMjYxMjAwMA", + "digest": "sha256:541132801d934cb90bf948d402bd93633e7b5270560db2a02f5d8974fa82e51d" + }, + { + "id": "91292e94-f884-5146-9530-c67024a3a7c7", + "version": "MTc1NjMyNzg3MjAzNzEwMDAwMA", + "digest": "sha256:7b46518fd63a698eced43ff93d72d8e965acb757e74be4354fc0649ada8e22b6" + }, + { + "id": "912d98ab-148e-58a6-98bd-15857dff50e5", + "version": "MTc1NjMyNzg2OTUxOTYwNzAwMA", + "digest": "sha256:49c3bc3124935f3fc63cc64a549f3a4ecb688ed7252c85ddeda2f34a9caa2479" + }, + { + "id": "913b32b8-1e74-5dbc-8364-bd1b9e59b0e4", + "version": "MTc1NjMyNzg3NTQ1NzI5OTAwMA", + "digest": "sha256:4fa56784fc52544e617c5168e359cbee026219e428362c9dab24ff2a7891753c" + }, + { + "id": "9147f594-a505-52fd-acca-dfc53e1e89fd", + "version": "MTc1NjMyNzc2MjM3OTEwMjAwMA", + "digest": "sha256:4526f587fc92414359fea7e29be6e0c558c8f548e2fb3b822f2b7a6a8f7f6f4b" + }, + { + "id": "9149c051-5135-5071-8c05-f9d2a80539fe", + "version": "MTc1NjMyNzg4MDM4MjQ3MjAwMA", + "digest": "sha256:a0fab0c96212444acde251617f20d1dfbf378db65bf434e5f0dd0cdf39ad7f3c" + }, + { + "id": "9157a440-0aaf-5d40-ae7d-5a1043ec7e8b", + "version": "MTc1NjMyNzg1MjA4NTg4MjAwMA", + "digest": "sha256:33baf8c1cf3cb3dffd8a9d1aea82bbe4abd63ad07361cf370fa0b247f47e82f4" + }, + { + "id": "915e7b7e-4a43-5e2f-b136-e5f177508f03", + "version": "MTc1NjMyNzc3MDUxMTMxOTAwMA", + "digest": "sha256:cb6cc7f9be929fc84233dedf6be1055cd7d6d36257ddaf05875db6c5f6363088" + }, + { + "id": "9166754b-ef14-5937-895b-2567c133c9af", + "version": "MTc1NjMyNzg3NDkwMjgxNzAwMA", + "digest": "sha256:1638394ad8bcd123a318ed79cf7afd058b67699afc6cb9649e1b684269bd6595" + }, + { + "id": "916d300a-e95d-5d7d-9913-cec00e7eeb37", + "version": "MTc1NjMyNzgxMzUxMzczNzAwMA", + "digest": "sha256:f070ccfe4641377831e8d915dbfd382628fcd40eea8bdb2bdbbcdbbf86d4e59c" + }, + { + "id": "9183d1f0-2eaf-5bc9-a0bc-b3a38ee847a9", + "version": "MTc1NjMyNzc5NjExMjM0MjAwMA", + "digest": "sha256:7b8950cc228f6b4e533dc8f7a56abea88605b1fe229bb4b0c89e3005975f3427" + }, + { + "id": "91a69fc8-d929-5f4f-b75c-451935718c18", + "version": "MTc1NjMyNzc4MjgyOTkwNTAwMA", + "digest": "sha256:a2c8cd139e31c9140a620088a379c3509d659e455f06a0fe7843467f4b5ae0df" + }, + { + "id": "91ca9ad5-1529-5b56-a970-bf1818f39258", + "version": "MTc1NjMyODAyMzYwNTIzMDAwMA", + "digest": "sha256:78d41a33b16bffe4fbdfe375b45b7651df351a64f0f7d8f97e214a0c65392c6b" + }, + { + "id": "91d758c1-402b-509b-b428-7e8975c79470", + "version": "MTc1NjMyNzc0NDM5Nzk2MTAwMA", + "digest": "sha256:d4758ae4162a03333eab8c9c41ece7d62710d47bd457f0a12fd6e83b3e72a16c" + }, + { + "id": "91dfe328-9cbe-56d0-803e-ff53d7badc54", + "version": "MTc1NjMyNzc1NjA3MzAxMjAwMA", + "digest": "sha256:3ff41827508bfd9cc60c69287c5b26c325ba4cb17ef86e0c211a5592fee17eb3" + }, + { + "id": "91e47736-2f74-5cf7-963a-a26cc7282845", + "version": "MTc1NjMyNzc0OTkyMjMyNDAwMA", + "digest": "sha256:3dc56f01173e2dec448d45ca0358d09a7d16d086a1936a0902b57aca0aa7293b" + }, + { + "id": "92112957-3521-59d9-bf8b-eeecb28f9aec", + "version": "MTc1NjMyNzgxMTk0MTYxMzAwMA", + "digest": "sha256:f04b12af3019851a14e6094d766ac6d486e49c8e64a8f3f4e4c4dc9bdbb1bae4" + }, + { + "id": "92133f42-5886-5416-a621-d6b8e1bcfc48", + "version": "MTc1NjMyNzg0Nzc5NzM5MzAwMA", + "digest": "sha256:459950864ab457815a98034cb8473b19f8fd59761defed9a1b2dbd803279e5aa" + }, + { + "id": "92398639-aac6-51c3-9afb-af6ce393c4a8", + "version": "MTc1NjMyODE3Njg4MDc3NzAwMA", + "digest": "sha256:f075e3ab3620432d576475a22803e3d94a1b79f87de4012881b0cafe7ad55593" + }, + { + "id": "926ee24b-c007-52a5-b364-ff4e7e6042a1", + "version": "MTc1NjMyNzg1MjgzMDM4MTAwMA", + "digest": "sha256:e1a54c57d291d5c187ec7d0d1bba3bb5060bc157607646059861aa4209ea5da8" + }, + { + "id": "9279e1fe-7c69-566b-985b-7b74d090c301", + "version": "MTc1NjMyODE4MDUxMTUzMDAwMA", + "digest": "sha256:ae43242cc7b621967d91efc6b18b693e8f11d6ce20f121de0f9d769ecb2a2690" + }, + { + "id": "92a8b400-74ec-5df4-954b-8c879e98f6e3", + "version": "MTc1NjMyNzc3NDQ1Nzk4ODAwMA", + "digest": "sha256:98aa726f1b70e8de2123cc5770a90ee13de1cb0c7f5dab0bd9c66db90b161787" + }, + { + "id": "92d253e1-d94a-516c-9eea-4cc91d0123b1", + "version": "MTc1NjMyNzgwOTIzMzczNzAwMA", + "digest": "sha256:41ea4cd09f7a94f0ea3f96957b35dccb5dc3a7853fcdb7e16cc8a7df80fb5243" + }, + { + "id": "92ea8595-9dd0-5986-b4df-75d04bdc73f1", + "version": "MTc1NjMyNzc2MzgyNzMzNzAwMA", + "digest": "sha256:31b1e463eec11fd0a6fbce79d994333bc42855b1a07a2a0b68b85ddd50b2a1d8" + }, + { + "id": "92ee5215-096c-5df1-a7cc-068f664ce624", + "version": "MTc1NjMyNzc2MjQ1NDk2NjAwMA", + "digest": "sha256:8a879ca7961a66c5454645436aab1fb7e7804af353500d611ba8aab6256c2168" + }, + { + "id": "92f0f489-01ea-512e-b419-d9e8c2fc34db", + "version": "MTc1NjMyODE1NjM2NzYxMTAwMA", + "digest": "sha256:862b9b000271897afe2bffda44cde7952845372d3cca38c46cf94ccd0c18be6b" + }, + { + "id": "92f349a9-eb80-5bcf-b36d-91e065e3ce5f", + "version": "MTc1NjMyNzkxMjgxNTc0NDAwMA", + "digest": "sha256:a4b88a17f3c6aefce817d450045e2263c5f424475c06bb609e24a71197a94510" + }, + { + "id": "930e1ff6-10c6-5e3f-a93b-90ff622c7ad8", + "version": "MTc1NjMyNzc1NjgzMTY0MjAwMA", + "digest": "sha256:e41d158e24de51771cfbd15f9e20e4d52144da832324e84d56248f21c3f35220" + }, + { + "id": "93138538-5eb9-5d73-b81d-288aadfde952", + "version": "MTc1NjMyODE2NzAyMzYwMzAwMA", + "digest": "sha256:9307049d93c1b6157cecd1ef63ed8167564b80105b3012a012141c6dd4a58a90" + }, + { + "id": "93378b69-2546-5f5c-a0a5-bb534e6c5802", + "version": "MTc1NjMyODE3MTE4NTIyODAwMA", + "digest": "sha256:f91e607087c78d738af1d2a62323e035380c686340edcfba9d4da77a8d08368d" + }, + { + "id": "933f846f-2823-577e-9c91-299d201af09b", + "version": "MTc1NjMyNzc4MTk2NjczMzAwMA", + "digest": "sha256:afd17a1341b4a8d635900add9984efe8fa5fb72b38a6f04633e845d80dfec30c" + }, + { + "id": "934416a0-ee77-5ff1-a3c9-2b930bb9da53", + "version": "MTc1NjMyODE2NDk1NTM1MDAwMA", + "digest": "sha256:7c5e7597242c2336b90e90d16476dfd7d7aea0e98e515335a38720f72176e43b" + }, + { + "id": "9345d1ec-e156-5f0e-ba71-39b299e7ac71", + "version": "MTc1NjMyODE4MTgzODIzNDAwMA", + "digest": "sha256:c8aeffb707bcf21406241438407a941fb05926388fa77df2ed324422ac573709" + }, + { + "id": "935a5533-734b-5f77-8123-579a8f4102dd", + "version": "MTc1NjMyNzc2MjM3ODg4MjAwMA", + "digest": "sha256:80005d2beb37e8280724ba0f678eede3f3b130ef0ba62132a099645ed752bcf1" + }, + { + "id": "9362f465-cdeb-507f-a703-093aaeb7d9f5", + "version": "MTc1NjMyODE4MjA2NjkyNjAwMA", + "digest": "sha256:5340cd0880df72a1db978673001a620487fc885e36938d394b85f015d8bb3cc8" + }, + { + "id": "936ec8ed-535d-52ff-a63f-9636fb44adaa", + "version": "MTc1NjMyNzgzNTIzMTU0NzAwMA", + "digest": "sha256:1dda98d06d01789a6272b5831c2a99118bd64189b5cb406268ec963b4122c1f5" + }, + { + "id": "936facc8-20bd-5d34-a5ee-67aea8e8b6ee", + "version": "MTc1NjMyNzgyNjI1MDU2NDAwMA", + "digest": "sha256:4c698e25da4b0c645f38991216d427dccdf8fa0494ffd5387a7c048f0224806d" + }, + { + "id": "9373c450-421f-574e-b644-56e913ae2cee", + "version": "MTc1NjMyODA2NDI5MzcwNDAwMA", + "digest": "sha256:e3030f877bc9984dcb1010172eac690a9da5109df48988616bf85f29e7c36f95" + }, + { + "id": "937fbaeb-41f4-50b4-9e66-2003325ad4d8", + "version": "MTc1NjMyNzc1MzY1MjU4NjAwMA", + "digest": "sha256:42254474fb61d744515c8424cec2f41beb2dc3fb04d5822a3fabd38f8f1b6756" + }, + { + "id": "9384a922-342e-59e9-9c50-cec9045ef553", + "version": "MTc1NjMyNzg3MDY3NjUzMjAwMA", + "digest": "sha256:78389b21dd85fa7785c89e5b2ba93d81d289841a0f70072e7076ee60f32f510b" + }, + { + "id": "938e86dc-cc94-5b4c-a9f8-2440e29ade0f", + "version": "MTc1NjMyODA3NTg2MTcyMDAwMA", + "digest": "sha256:9dc5f7fb6c06e8f9ce2b262ea6852f9200a42d075be45a3c8617e8099828d0cb" + }, + { + "id": "9395f2ef-98cd-5f1f-a41b-98c04f09d1f3", + "version": "MTc1NjMyODE2MzQwNTIwMTAwMA", + "digest": "sha256:7e1a3a4f463d2c6d9864111b40570c7346c118963f811d823c632d9c7928d132" + }, + { + "id": "9399ec11-c284-5de5-bb4f-15785f7426bd", + "version": "MTc1NjMyNzgxNjM5NTE3NTAwMA", + "digest": "sha256:0fe62d0827b931a606661809fd62a301048a631f94f31f024080cf899c6fa45c" + }, + { + "id": "93b5e256-df1f-513b-8871-d529dc8f4f78", + "version": "MTc1NjMyNzkzMjY2NzE4NTAwMA", + "digest": "sha256:c48d60d9127e617c9a7ade92a17d14c226a36849b291dfb4b8eb85ec5d932450" + }, + { + "id": "93b9f5c2-eb43-5be3-94e5-afd57392d088", + "version": "MTc1NjMyNzc3OTc3NDk2MzAwMA", + "digest": "sha256:c73c402c8bd87ed11a54972609dc095cca6fbd9ea54bbf6e2d551ea843f2f76d" + }, + { + "id": "93c11814-4c8b-5cf7-bcfb-498d74a8c50b", + "version": "MTc1NjMyNzkyODY3MTc3MzAwMA", + "digest": "sha256:ac29412404c3a2d19c16ead3a8faaa1d22c88e03e7a8dc30f0ceab12fc883033" + }, + { + "id": "93f4cd86-7ece-5b8a-8a67-619050f0545c", + "version": "MTc1NjMyNzgwMjk3MTM2NTAwMA", + "digest": "sha256:18d968730fc8a7a0a816ab3d571244a28182c4cd5b9ef8841b7ecb3b9309f9b9" + }, + { + "id": "93f5edab-8202-596b-a859-7d36b1ef9e88", + "version": "MTc1NjMyNzgzNTIyNzU2MzAwMA", + "digest": "sha256:49811d25259d89cfc0f954630defc4e0a2410220484e00888b6d7c2b6de6bd1a" + }, + { + "id": "93fb7bf1-2e93-565d-890f-a4283d00e3b7", + "version": "MTc1NjMyODE1NjMwNjExMjAwMA", + "digest": "sha256:40a29e0d49d939e32db1aa103d6761e045691b622b27b40705ea46fb2a58f2cb" + }, + { + "id": "9429260c-62d0-5f45-9c3f-1be97b0c4b23", + "version": "MTc1NjMyNzgyMjc3NTE3MzAwMA", + "digest": "sha256:2d137d18a5099348950428fd0b6d877158fd0c5671024e666a2aa77355c1564b" + }, + { + "id": "942a192f-3e02-5e52-b451-d220b61f8e7d", + "version": "MTc1NjMyNzc1NTk0ODYxODAwMA", + "digest": "sha256:69cf9291487230b960eaf8bca55ea237fce95250128941d2ece16db6a6032205" + }, + { + "id": "944b6360-4c17-5a45-a175-cf6b9f89fe9c", + "version": "MTc1NjMyNzkyODgyNjE0NTAwMA", + "digest": "sha256:7cdb439b2d2a2d6a4366b0cffae670db5db927098803b1ac2883d34a3a4192cf" + }, + { + "id": "944e9152-2c52-5253-820e-528e445ebd14", + "version": "MTc1NjMyODA3NjU0Nzc2MjAwMA", + "digest": "sha256:a38916586593b0c8c453942908b14b0c77da1a65d0f976ae8ac6069916ffb969" + }, + { + "id": "946040a3-baf9-56ef-8a11-7176c65fc8d8", + "version": "MTc1NjMyODA2ODgwMzgyODAwMA", + "digest": "sha256:400af1881322209828010c3e2d4fa72bbb0ac9ae96a2ddb1ec255abdfdfd7deb" + }, + { + "id": "94887054-370f-5f18-9347-68cbaa5cc10e", + "version": "MTc1NjMyNzg1MzM2Njg3MzAwMA", + "digest": "sha256:78251b9f1500b0ad74ebf8a16afd3da1726baf3923fd2d490450879a6ef46641" + }, + { + "id": "94b9c206-488f-5492-97a5-67f205f47291", + "version": "MTc1NjMyNzg2NjI0ODI3NzAwMA", + "digest": "sha256:e55047f3e0bc608d7fa265a89a383dad2d1873e507f026ea0e055ab5662050ef" + }, + { + "id": "94baae40-df1f-5b59-8960-37054891c2f7", + "version": "MTc1NjMyNzg0MTExOTUxMDAwMA", + "digest": "sha256:f25c3b4b343393d93585f2ca1e8296df54e9067063e241afc07d940d49aa11dd" + }, + { + "id": "94ee67b9-e53b-526b-90c3-f92d436affca", + "version": "MTc1NjMyODA2NzY5Njg0NDAwMA", + "digest": "sha256:9254c46aa89f40786febd2e742c36e16e5d0bff1a0791bc9bc620d4f76a915d3" + }, + { + "id": "94f5fbd4-5a26-58e5-8913-9ef46f4cfabe", + "version": "MTc1NjMyODE2Njk3NzM3ODAwMA", + "digest": "sha256:e3a824d2d31a9ac15b157e2cbd2a3ba56950ba864e2759ed1e45fcc6c5a52d43" + }, + { + "id": "94f64253-27fc-5798-b4be-6fa8746e2ab0", + "version": "MTc1NjMyNzc4NzQ5NzY0ODAwMA", + "digest": "sha256:38c442b83842930a4e0245eef8d34e8c70dcbb2c516cea3b4846eda10c3126e3" + }, + { + "id": "94f969f6-7017-54c4-84ee-3cdde3ebf313", + "version": "MTc1NjMyNzc0MjUzMDI4OTAwMA", + "digest": "sha256:74a5154f27279e7a17c74b3985d9926478e766c76254a60fdb55082e0dbde282" + }, + { + "id": "94fe2155-4575-5fd8-916f-af63e2a72ffe", + "version": "MTc1NjMyNzc1Mzk3NTE1MzAwMA", + "digest": "sha256:3970f6fed3d9bd0826cdbb9c057b85db0544b6aa8eeb16f2b74d09790999642a" + }, + { + "id": "95123017-f9ba-5dd3-8ab3-27dadfde1f00", + "version": "MTc1NjMyNzg1MzgwMTI1NDAwMA", + "digest": "sha256:f6d517e20401c193b2877b7642dfa8df6908f2b202b19a477fed9007fcca0fd1" + }, + { + "id": "951e0f75-ce68-5a16-9254-77dd5b078ab8", + "version": "MTc1NjMyNzc1NzU5NDA5MzAwMA", + "digest": "sha256:de295b2d4f09827adf61e89d6d4e80aeec229908c064dd5ba28dde2703ebe073" + }, + { + "id": "953c12a9-28d0-5334-a798-c886de27a3b2", + "version": "MTc1NjMyODE2NzAyMjcyMTAwMA", + "digest": "sha256:37a0e092759c468ba43fc02bed7a73ac61572d321cce50ee6a2a7a37d5cac6f2" + }, + { + "id": "9547b0a3-0e00-5f9d-96de-bebf9993cdc8", + "version": "MTc1NjMyNzc3MTUwNTA1NjAwMA", + "digest": "sha256:49a78cf7050c2a8ae33173545f876b0d171851b76c308acb86a37b34542778a7" + }, + { + "id": "954e1264-d552-5308-971b-088efa310082", + "version": "MTc1NjMyNzc4MjgyNzY0MjAwMA", + "digest": "sha256:95573c121b8b7ccccae7085daf0dca260930eda7f3445424b184c9c6ce54c6a1" + }, + { + "id": "957cdfca-bc90-59e0-ba84-df32cbf878fe", + "version": "MTc1NjMyODA2OTU1NDM0MjAwMA", + "digest": "sha256:0d7e457c4084dca94074a36de7b95956cc45df771f01548426f03431322ffe18" + }, + { + "id": "958306b2-e68e-5644-b1c2-1a51de12b2ae", + "version": "MTc1NjMyODA3MjE0Mzc4MzAwMA", + "digest": "sha256:099c242007d30857bf1da24bb25372eef1621e889aa6378afa980a376a4ddea5" + }, + { + "id": "9599e1d6-ce05-53d4-88db-803a67096102", + "version": "MTc1NjMyNzc1Njk5MTY2NDAwMA", + "digest": "sha256:a7b67fdde57ee5d1c5b98c2a1d4117786305bb4944c986595fe4e96d168bc973" + }, + { + "id": "95a3ee28-c7cf-54ba-a6af-9b3c0f5b46ca", + "version": "MTc1NjMyNzkxMjgzODQ4OTAwMA", + "digest": "sha256:645b2dc4e54747e4b5c9a3d21393a97c1a92312edf4a2d959b0f6ebca3ccd3b9" + }, + { + "id": "95c9fce5-0c1d-5607-8335-a6c558c83550", + "version": "MTc1NjMyODE3NjIzMjg0NTAwMA", + "digest": "sha256:8220603a014421394082d7de2aeaa5ded7e1a5a28d722893a7f2528af387b0ff" + }, + { + "id": "95cbb21f-9820-5ea7-83d5-821c5c38fab6", + "version": "MTc1NjMyNzkwNTA3MzkyMjAwMA", + "digest": "sha256:f34637ce0746eee38ec15d7183d3e379e26c1b43fd5d4be996b63814fae6cd35" + }, + { + "id": "95de6cd8-3e7e-5b8b-bdfc-1a73c51963d3", + "version": "MTc1NjMyNzc5NDc0NzUwNDAwMA", + "digest": "sha256:c4a4ef95ff51aa84eb1af6f4dcf263745efa4f941ea751d987ff0c99e8f295fb" + }, + { + "id": "96079c89-64cd-5447-a10f-c5656533acdf", + "version": "MTc1NjMyNzc0NzcyNjY2MzAwMA", + "digest": "sha256:67f31e7c9f36b2fb4f83c2a37596f071ffed37c0b85ab202634c6a13275e8209" + }, + { + "id": "9608f407-67d4-50b8-b29a-92d71faf3a1c", + "version": "MTc1NjMyNzc0MTY1OTk3OTAwMA", + "digest": "sha256:8accc9a9e46e68dbcf6244da3ce82c8f6681ade87fd552c24f619df2c8d699aa" + }, + { + "id": "961bbfac-b56e-5e15-ad6d-7e471571e6b0", + "version": "MTc1NjMyODAyMzU5MDIxMzAwMA", + "digest": "sha256:ef7147acc520751fda740b0c829ce24e9c0764014dda16c4395d859df20c60a6" + }, + { + "id": "961faa3d-0922-56ed-a5c4-765fb2fd0c6e", + "version": "MTc1NjMyNzg2OTUwMTc2NTAwMA", + "digest": "sha256:38bcc60a2291972a4b4022bc84b4434b4a766a9eb91ebced974f91fca4c7b7cf" + }, + { + "id": "96477859-3351-569f-a262-73e340fc5eda", + "version": "MTc1NjMyNzc4NjkzNDg4NTAwMA", + "digest": "sha256:ae06c032ac5f42c53179b285654c8c22e3fcac79b733eb70074e601e66014b6a" + }, + { + "id": "965d81a0-5802-5926-9695-1311ff180863", + "version": "MTc1NjMyODE4MDM5NDc4NzAwMA", + "digest": "sha256:12f072ad1ba6b37c4359911cecef1396366d997af34b545bc26ddd5c06777568" + }, + { + "id": "96635328-8905-53fa-bb76-781694630ea4", + "version": "MTc1NjMyNzg1MjY2OTYwNDAwMA", + "digest": "sha256:84c827bd13ade57b98409419484c70dfe43fc7d8246859c04c07ed6bf012b916" + }, + { + "id": "966c1286-fc63-5132-864b-b1e9dba6b6da", + "version": "MTc1NjMyNzgzNTIzMjAwOTAwMA", + "digest": "sha256:cfdb1fef1614289ff04836d492c36f344a3c20492364004050ce055cbfe94684" + }, + { + "id": "96747b9d-fc4f-5921-a009-641014b7345b", + "version": "MTc1NjMyNzgzNjQwMzU1NTAwMA", + "digest": "sha256:477264817981126c724e7effcb06ca829016755451b0851a107210305ea402b2" + }, + { + "id": "967692c4-6d91-5d37-9626-4cca8c499972", + "version": "MTc1NjMyNzc0NzY5MTUxMjAwMA", + "digest": "sha256:b466cf59c3fed535e6d677f71c14d6e5a8406085d6cc43621db64265a5bea932" + }, + { + "id": "969c479f-bfd3-5b0c-b1ad-e9cd433da2cc", + "version": "MTc1NjMyODE3ODAxMTIwNjAwMA", + "digest": "sha256:f627d2b610f9d6f10bfe0bf0bc249269748c82c018d6b588d4f55e56251967c1" + }, + { + "id": "96a7b0cc-6f6d-5c13-9d6d-df12955dcdee", + "version": "MTc1NjMyNzc1NTk2MDA1MjAwMA", + "digest": "sha256:b990dab9b2c82ddf2d17a2cfc01aa5bf5c06a7176ffceb5cf68abcef0a5bb6dd" + }, + { + "id": "96c2cb0d-8c2d-561e-8a4a-9a185e88e0d9", + "version": "MTc1NjMyNzg4MDI4OTAzNTAwMA", + "digest": "sha256:f186694101a73ac4bd64b2e2f9425a6d79ae3f1810ce5e8c25e1915f4438166f" + }, + { + "id": "96c509d0-087d-55d3-9c82-78f7ae140afe", + "version": "MTc1NjMyODE2Njk2OTIzMjAwMA", + "digest": "sha256:b2564c3dba1cc064dfd4a3383a069dc48dd04a1433c6e636ec292157d5ac0e09" + }, + { + "id": "96dca7bb-b91f-5e03-a4ce-2eda02a698c2", + "version": "MTc1NjMyNzc3MTU3MjQwMDAwMA", + "digest": "sha256:5c3a4de8521bd30940367ebb6d239db515f6cf2d698f3f0ae73603cd57ba2b51" + }, + { + "id": "96f171fd-dd13-5f66-b15c-8d0a84a6c118", + "version": "MTc1NjMyODE2NTgyMzY0NjAwMA", + "digest": "sha256:4d1fb5c63ae65508aec763d5f0e912a15ea3d17b3b13a6d7da0161dfce2d3843" + }, + { + "id": "97062a06-b069-5b48-b17e-93bd5486fe87", + "version": "MTc1NjMyNzczODQyMDg2NTAwMA", + "digest": "sha256:b4306167b657c93057e9c627e74114c3a6273a010482adfe0e4440fc26dc96d1" + }, + { + "id": "974c5943-acff-5390-be65-c8a48d294717", + "version": "MTc1NjMyNzc2MjQ1MDc3MjAwMA", + "digest": "sha256:a5eef6413d2ba70ac2550389e0a167bbb4e38ccc6f3d76c18d3210416d1d58d8" + }, + { + "id": "974e9ac1-3299-53a0-85b2-9933ae39ee37", + "version": "MTc1NjMyNzc5NjA0NTIwMDAwMA", + "digest": "sha256:f197d4a8a163fab8689c506a269f93dc229bbde30980609516e2dc587629a21d" + }, + { + "id": "974ee9fa-ccfa-5b00-be49-63a233401015", + "version": "MTc1NjMyNzc3MDM5Mzk3NDAwMA", + "digest": "sha256:53dc6ad2fbfb8d3a65590b75a578195a22df0e7005059c7022c30d69ac4624ba" + }, + { + "id": "97546ae4-91c6-58b2-a8f8-2ef51a431bee", + "version": "MTc1NjMyODE2NTg0MDI1MjAwMA", + "digest": "sha256:8c40c68fe53fe36bd5185c19ee3a666dd81a01723c8490491b39d9c978f51218" + }, + { + "id": "976fdfdb-03b0-5af9-8393-f95119007eb3", + "version": "MTc1NjMyNzg1MjcyMzc5OTAwMA", + "digest": "sha256:7324a1e53895851bdbb71444dc164a4a17aa01879ffaa349131a57a2c9e86b15" + }, + { + "id": "97794bf1-6bde-50c4-9f06-f9a132789b74", + "version": "MTc1NjMyNzc3MDM4MzkxMjAwMA", + "digest": "sha256:f8c588c444164ea5c90390cbc3e1d7c58e64c85c17fdc35aabb5ac93f6b1cba5" + }, + { + "id": "97987c8c-66b8-531a-a2df-7619009b5abb", + "version": "MTc1NjMyNzc5NDgzNDY4ODAwMA", + "digest": "sha256:ba76f1f21c7371385da7c8062470244501f75192936f0c50df61a956b61b7620" + }, + { + "id": "97ade1f8-9d36-507f-8256-5fd2f9ff4c6f", + "version": "MTc1NjMyODE3NDQwMDk2ODAwMA", + "digest": "sha256:e5341c3556ddc8a2e070eb1c4eb96730a80698f16ec6332e83352fa9fa5defa4" + }, + { + "id": "97b471de-7f94-550e-b985-49f7d82674d0", + "version": "MTc1NjMyNzgzNTcwNzM0ODAwMA", + "digest": "sha256:a69d4be4e4facc3593b5e4886fcf5fe6e6ed32c9712a285dfef2fee092e593dd" + }, + { + "id": "97e14b43-1d56-5c70-b3a9-b75e9a523cc4", + "version": "MTc1NjMyNzg4Njc0ODE3ODAwMA", + "digest": "sha256:dce487131c150a74a5ac6de90b7439861cc7bf58b2ade61fe15e3476617d53bf" + }, + { + "id": "97e76d54-3278-5ea7-9105-9cc67a74730a", + "version": "MTc1NjMyODA3MDYxMTc0NzAwMA", + "digest": "sha256:8e664d714b124543e56924e77d6e5e8333ebf72dd1a41047c3a6b76e50f1564d" + }, + { + "id": "97fbfba2-86dc-5583-ba5b-219b2d338930", + "version": "MTc1NjMyNzgyMjMxNzM0ODAwMA", + "digest": "sha256:09d4979a820f094f2e0ebd4b58a495621a2850a991b2c39c345c9013fa744ea6" + }, + { + "id": "980db0c0-1c71-5463-8ba7-6befbc5d44fb", + "version": "MTc1NjMyNzc3MjUyMjIwODAwMA", + "digest": "sha256:6c2bcb009cfd4f68a258f539f95ef3041ad116fdd6fac145e94806550a9d6d5e" + }, + { + "id": "981d755b-6fb0-56a4-916f-763ee3301a48", + "version": "MTc1NjMyODE3ODExMzc1NzAwMA", + "digest": "sha256:5ea1284e9eece83e8750f4ea63eacbfade43d5e50cc3cb61a84c5e7660c8fd12" + }, + { + "id": "98207bde-0e95-56c5-a439-eb641a603c35", + "version": "MTc1NjMyODE3NDQzMzA4NTAwMA", + "digest": "sha256:eab8c683c09a0590b0343b8fa0308925185ff685551d62a9a60763fba7030a13" + }, + { + "id": "982e98d7-c497-5e94-8419-86891af119b1", + "version": "MTc1NjMyODE2ODkwMzg5ODAwMA", + "digest": "sha256:273a67585a43343d7ea3290afdadfe5f6ccaa6f2f38e8bcd9996650459a76076" + }, + { + "id": "9838458e-4950-5410-b264-0af72d1b0ed8", + "version": "MTc1NjMyNzg3NjY5NTQ5NTAwMA", + "digest": "sha256:4fc2f53c91a7eddcc82c2166b4bf3e18cc5be5c130b52455b760e86ab9e9abf6" + }, + { + "id": "983c9064-d423-551b-9674-751379b6e5df", + "version": "MTc1NjMyNzgzODk1NjcxNzAwMA", + "digest": "sha256:aab1fc977c0be61fb50f0e3f3a8b2e80bae1c01fa224a3e9ac0579d9493eb481" + }, + { + "id": "983ef804-2ed9-594c-ac23-1f881717383b", + "version": "MTc1NjMyODA3NTI5MjY0OTAwMA", + "digest": "sha256:c2ca37f74a92628afbc73373ae6c8cf36400b0e4c3667ed3826ad7ed7c030847" + }, + { + "id": "984e778b-12d2-5c0c-8477-bca30a440de2", + "version": "MTc1NjMyNzc0NzY5Nzg0MjAwMA", + "digest": "sha256:203a6ad1bc6f19ed42e636d23b19965ef03a090520c2db83f77e8c6c67036fb5" + }, + { + "id": "9852d4f8-4db9-5262-9c5a-c1bd6694bb91", + "version": "MTc1NjMyODE2MzM5MTgxNzAwMA", + "digest": "sha256:8f97bc7d20f485c78dcadce05bf19f8a8e33042d3cc46907b5f9aeb8d0c5f87d" + }, + { + "id": "98606e12-6e52-5e0c-bbcd-7ee6db9291eb", + "version": "MTc1NjMyNzg3NDU2OTAzMzAwMA", + "digest": "sha256:3792d13e6af82c92ea6bcaf5f8d43641aa3294b79822249445269183cf656bba" + }, + { + "id": "98653b08-c82a-5724-9e8d-3ec474b6c238", + "version": "MTc1NjMyODA3NTk4NTY0ODAwMA", + "digest": "sha256:21ccb432830337abb9e82fb6c0ed649d4585c821d595a144e64643d321c6ea9f" + }, + { + "id": "9874ce46-6052-5131-b7c1-76b9734b731f", + "version": "MTc1NjMyNzc1MzYzNTkxMzAwMA", + "digest": "sha256:d22365a5e4ba760502e631cfb40d1065fc93c9598ad2aaeb004b36444c549bbb" + }, + { + "id": "9890492b-b21f-5d47-b8df-e01949e343dd", + "version": "MTc1NjMyNzc3MjYxMTUxNDAwMA", + "digest": "sha256:9760df5b5b44f4378f964ce014ad6eb860cda12f25f937e27fcc8a44545d560f" + }, + { + "id": "98928b79-40d6-5c30-a46e-1e8483a30bf3", + "version": "MTc1NjMyODE2Njk4MDQ4NzAwMA", + "digest": "sha256:cd2ef64e9f966f09da5ee03c7ece41c5bf4775f7dd2a1a8529059d95f20c3a3d" + }, + { + "id": "98bdff1b-3d35-588c-9435-37eb0c69d5cf", + "version": "MTc1NjMyNzc0NzA5NzUyMTAwMA", + "digest": "sha256:4e0f24b58a3e3531c016af916ea12913a4eecd73e3ebb67c02ca21e8542bf358" + }, + { + "id": "98c2cf14-d61c-5a85-a613-6412d8ba347e", + "version": "MTc1NjMyODE2ODk5ODUzNzAwMA", + "digest": "sha256:e909dcdf22f447b0bd80d079c7fe207341cac925a7a7173ebc148fe5ff5d731b" + }, + { + "id": "98e966ed-cdd1-533e-adba-110ed858e029", + "version": "MTc1NjMyNzc4NzQxNjU1NTAwMA", + "digest": "sha256:281ca3bda0a48576f1de172857a666beebd968effb7221a5c4f23f7eec38a55b" + }, + { + "id": "98f34033-d1b7-555c-8be8-d806d3fc829f", + "version": "MTc1NjMyODE3MTE3MjE0MDAwMA", + "digest": "sha256:b64038fb33def930503e676082ce2f9d1144e200ea8818c7e215a9dba21b2651" + }, + { + "id": "99000a74-7fd7-5107-878b-9b7c0ad78ad8", + "version": "MTc1NjMyODE3NzYyNzU3MzAwMA", + "digest": "sha256:ba5fd5555e6b5ec400bdb3c8e4eb96a469012787b0acb9bc64f4c66a0ebe94ce" + }, + { + "id": "9903ab59-d245-5830-87ef-801f44e6c025", + "version": "MTc1NjMyODE2Mjk3ODg1MjAwMA", + "digest": "sha256:5859fc5d5edab933ef1dbe5522de8cb897e4d23b8a6d6c3bc92f590e3aa38132" + }, + { + "id": "9917d4fe-13f6-5456-8078-2ba777f88f6c", + "version": "MTc1NjMyODE2NzAzNDEwMDAwMA", + "digest": "sha256:261f6f12a405a35c923d51bed9e167afa4e34f7bb345c6267460b2e5addaeabd" + }, + { + "id": "992e9e51-ad42-57c4-a677-e6f7c8f88119", + "version": "MTc1NjMyNzg1NTY5Mzk0MzAwMA", + "digest": "sha256:dd3cee5f1254dd3a8836d4c314b50a7ba00fa5dddeca40d38e86358f5ad045b8" + }, + { + "id": "993278a4-8365-5245-afd2-604a5708f637", + "version": "MTc1NjMyNzc3NDQ0MDM0NTAwMA", + "digest": "sha256:75c5a76d92ae3c3c8d78bf4040477163bcac6621b438e76779cd0c9f8767d71d" + }, + { + "id": "99593a75-4f06-51ad-a730-065ebd53decf", + "version": "MTc1NjMyNzg4Njc1MzE3NjAwMA", + "digest": "sha256:f08c2ec659147f789dcb6ede15891ba4068eeea6020d10918897b9aa105c421a" + }, + { + "id": "9963c402-7a64-504b-9f87-dd101547d131", + "version": "MTc1NjMyNzg1MzgwNjMzMjAwMA", + "digest": "sha256:0cede1d7a39bc2a08d925e203030e27ad817e5818646f48652e0dfc9b8049832" + }, + { + "id": "9979ef65-57c5-5af3-82ef-4598c3cd0d1e", + "version": "MTc1NjMyNzgwNDU4MzkzNzAwMA", + "digest": "sha256:bec5ab0eeb28190ce674a4023d17683229b155a249a2a6af39dc029715b456c7" + }, + { + "id": "99a3b433-e423-5087-bdec-fa466a37d89c", + "version": "MTc1NjMyNzc4NzMwMjAyNjAwMA", + "digest": "sha256:ba64c444773ec83fcee2e7cc117d0f705d1575ae12afa0a543141b1506eb3768" + }, + { + "id": "99a7ae40-b118-520c-87f6-7ac659e9cf78", + "version": "MTc1NjMyNzc1Mzc2ODIyOTAwMA", + "digest": "sha256:64a645d29398a1ab38d3cd14c5572c3255aeb7b676b92cb58c6816f4ce763011" + }, + { + "id": "99b60bfd-5472-59b4-aa9f-8baf420a4c1b", + "version": "MTc1NjMyODE2OTk5NTAzMTAwMA", + "digest": "sha256:0da549612b5437bcdea2165fd15b414b119cb23f0366f66fadb214033abc3ed3" + }, + { + "id": "99c89c51-0be6-54df-a850-d0b359e600a3", + "version": "MTc1NjMyODE2OTAwNzExNDAwMA", + "digest": "sha256:7fbc8282a05aaef61db3f5e73484dbcc5e160fb16182461298d8b607311c3714" + }, + { + "id": "99e94ae2-bfd8-577d-99be-ff7ac7a9604a", + "version": "MTc1NjMyNzgyNjM5ODQzMDAwMA", + "digest": "sha256:3fe72a874fed739d1e9b4af23c384bf581dbd3374f2877bc89aa4136ea40fc9e" + }, + { + "id": "99fd47a7-d1b5-5b39-b616-410f74208c38", + "version": "MTc1NjMyODE4MDQ5NDEzNzAwMA", + "digest": "sha256:8b4fe50f1642de9bb47d7f21a57528c0204ce7ca22e638c828508fd0d62fe217" + }, + { + "id": "9a06c062-cdd4-5ef8-8448-8f7dac0f354c", + "version": "MTc1NjMyODE2OTA0Mjk2MjAwMA", + "digest": "sha256:ff2239ab4bf991f1cac30673ed0c1a6f6fd2a7b3ecdec5671af507cfd3ef397a" + }, + { + "id": "9a0dd87a-f8b9-578f-af24-c37e41840f14", + "version": "MTc1NjMyODA3NzgyMjEyMDAwMA", + "digest": "sha256:8b050fa82974eabfc3fa98725590f6acfcd0b3a531d696f698a6d3f8301798b5" + }, + { + "id": "9a19e32e-dc0d-55e8-aa00-005352b9b3a1", + "version": "MTc1NjMyNzc3OTkxNzIzNzAwMA", + "digest": "sha256:afd976c4c918ec258f52567e20b8b1b84bb3bc300257d256b35f5d63d5b3db62" + }, + { + "id": "9a2f7772-f1a6-5c03-8031-59f94ee30b9a", + "version": "MTc1NjMyNzkxMjU1NDY2NjAwMA", + "digest": "sha256:5678415fd6dbc44ed953d6b4b33c96a8fe8dee84facf1316f0fb436ea1343559" + }, + { + "id": "9a3b94f2-7634-554b-8579-07650c5110a8", + "version": "MTc1NjMyNzc1NzQ4NjM5NjAwMA", + "digest": "sha256:3f31023c22c5e2073ca6446b06ee90f624b32e872251ea3bf19e63a15fd8918b" + }, + { + "id": "9a4ec56c-200c-50d1-a544-fd191c5898a6", + "version": "MTc1NjMyODE3NDQ0NzM2NTAwMA", + "digest": "sha256:94642002dee333ef3f794aa72573626be650fd77e242ee749a2097555274c32a" + }, + { + "id": "9a5c6c9f-f919-5d3c-9237-79089c88a3a5", + "version": "MTc1NjMyNzkwOTA4NDE5NjAwMA", + "digest": "sha256:c0d95b10ab4d8f0827eb55906d9ae8ab591270936fc6ca2fcfdaeca2a46bdce9" + }, + { + "id": "9a7db596-03a3-5cf0-a8e6-179d5fe83a86", + "version": "MTc1NjMyNzc5MDIzNTkzMzAwMA", + "digest": "sha256:a3fd1050acb805c7378c6320386f31d84191bc8a12bff3a2321f998b72be9609" + }, + { + "id": "9a9c5a6d-53b2-5dda-9409-90b1ea8ec182", + "version": "MTc1NjMyNzg3NTUwODk1NDAwMA", + "digest": "sha256:8e5de17e5348ce2a124ab55443e53f54859d98ec9de1fee7192085c97846cf64" + }, + { + "id": "9aa09ca8-00d1-5d0c-bfb3-dddf244b8dab", + "version": "MTc1NjMyODA3NzE0Mjc2OTAwMA", + "digest": "sha256:7809ab84c9478cce69484f4d8e58af0e31f850f73f78866af478f5d97503999c" + }, + { + "id": "9aa2d2eb-758c-5542-adf2-69dd88c83099", + "version": "MTc1NjMyNzgwMzY2MDk0MDAwMA", + "digest": "sha256:e7457a412a350528897529ccfd25b1c4fbf7c9c9942d7b5f9ff10bb6bb2be500" + }, + { + "id": "9aa6ca6b-77b3-5a11-883f-43bcd122d1c0", + "version": "MTc1NjMyODE2NDk2ODIzNDAwMA", + "digest": "sha256:f6d8eabbaa3ca1c321b033596ee56f58ae97564f08aa6e6002eb8ad66f7c117d" + }, + { + "id": "9ab9b013-0b0d-5b3a-ab15-24bab6563d40", + "version": "MTc1NjMyNzc3MDU1MDM0NzAwMA", + "digest": "sha256:913211300dd0b01d7ffd2e1010e18ddfd690056809c826e563d7f55385740491" + }, + { + "id": "9ac794fc-267c-5a17-bb89-e3034280b2e2", + "version": "MTc1NjMyNzgxNTY0NTcwMTAwMA", + "digest": "sha256:ba9f7365d1c4174d87bfdc515178426d4f8fe8e31d911f83d6d667505084a792" + }, + { + "id": "9acc7e65-0a6b-523e-bc02-7ddeacb1c1dc", + "version": "MTc1NjMyODE3Njc4MjAzNDAwMA", + "digest": "sha256:b492e3c7c00833d6bd1feb00eb808679bd3654e008bb2a15b3b4271e2f997c05" + }, + { + "id": "9af49e64-3729-5402-b44e-6676a4468550", + "version": "MTc1NjMyNzc3MDU3MDgyMjAwMA", + "digest": "sha256:7aa337d4c07197a7d1b9f147ec70e87533a6b84b1613e622530897e35103b449" + }, + { + "id": "9b145ec2-8d3e-59af-91e9-5e5366677db0", + "version": "MTc1NjMyNzc2MjM4NDE3NDAwMA", + "digest": "sha256:f6d21c0c1a3612677e0a2280dd3126aa0d423a09543fd84e97660fdbf091e9f1" + }, + { + "id": "9b1c4a7f-b73d-55c2-8b84-73ee6f13e862", + "version": "MTc1NjMyODA3MzI0MTQ4MTAwMA", + "digest": "sha256:b99e1640de0e7db392e59e384318da956980744beb0104b8520593781f9de3d6" + }, + { + "id": "9b268392-958a-556c-b378-0ef4611996cc", + "version": "MTc1NjMyNzg2NjYzNTQ0NDAwMA", + "digest": "sha256:2913c5cb3cc2910fb8b0b62105fec0b4b16f7c45972163c182d22a85c4740ce9" + }, + { + "id": "9b3869bf-def3-5d46-963e-aabc5fbe1948", + "version": "MTc1NjMyODE4MjExNDIyNzAwMA", + "digest": "sha256:f86b9cad6dae94a82d9d4dd88a1be53a9f35d966252ccf2fdc8ffbe84cbe2dff" + }, + { + "id": "9b444db5-fbe9-5836-a529-b53766592961", + "version": "MTc1NjMyODE3NzY2NDYyODAwMA", + "digest": "sha256:110b327f22284383eaa07c8972ac09b83c4839fd911183b7df87e41e32eab812" + }, + { + "id": "9b467fd8-ae80-50dc-a689-ced640c460b9", + "version": "MTc1NjMyNzgyNjI3MDc0MDAwMA", + "digest": "sha256:0adb3d44afcc0a00c1a2a1fb7377a0e6db3ff0193434977bd1c6f5f9f32947de" + }, + { + "id": "9b7c3c24-a2ec-5369-b5da-764e4a87e22c", + "version": "MTc1NjMyNzgzNTcyNDIxNTAwMA", + "digest": "sha256:5157247d76050e6a2d7410d1429bd87ee54b2b7a9a43324335237f37cf1814ac" + }, + { + "id": "9b8dbcb3-cd47-50af-9010-ff1f26e4509a", + "version": "MTc1NjMyODE1NjMxMzIzMjAwMA", + "digest": "sha256:bd24db7c5a79ca61ba72f1d7bb402ea9a6045da3e730c29a99605160f4c9b0b7" + }, + { + "id": "9b95c87d-5f5a-5c77-8d12-a394ccebfd5d", + "version": "MTc1NjMyNzg1MzcwMTA0MTAwMA", + "digest": "sha256:75b945e2336063994c2e0757846ec311029bc482b6cfd909e1a8d48017e8a426" + }, + { + "id": "9bae7440-a52b-5813-a345-f4e297723a56", + "version": "MTc1NjMyNzgwNDQ2MTgwNDAwMA", + "digest": "sha256:6fb01c255e9f907cc07fb1373444626eae6fd8ef9154292124c4e1e36e7a6762" + }, + { + "id": "9bb5df61-3c52-5356-9a16-8620ecfc1862", + "version": "MTc1NjMyNzc4MjgyODUzMzAwMA", + "digest": "sha256:1cb3a283cfa15f1b549457f24b34389bfcc3742d34176496cd6b44c82f4eb742" + }, + { + "id": "9bc3e6de-5299-5009-9054-193bc7f51b8f", + "version": "MTc1NjMyNzc2Mzc2Njg3NzAwMA", + "digest": "sha256:006740c32f705246e8917fa229d0827911a01cb919b713c96989370269e66e32" + }, + { + "id": "9be544fe-1016-5e59-9101-810a4f04298c", + "version": "MTc1NjMyNzg3MDQyMDExNjAwMA", + "digest": "sha256:9f22900c0a3c1d67f7f34321642c75b6536df792b836237f789495fde7d5a0da" + }, + { + "id": "9bf23af5-8d17-5014-a2fd-683b598010c1", + "version": "MTc1NjMyNzc5NTMyNTQzNjAwMA", + "digest": "sha256:b231a4d802f9656dcb1d19e0459349f2ac0be87da96177c3b424f9a43e518e93" + }, + { + "id": "9bf4f33d-d313-5f17-ad24-7899ee55a9d5", + "version": "MTc1NjMyODE1ODIwOTYxMTAwMA", + "digest": "sha256:c6b0c015660302cc264b4f438e1dc7e21372fbf203f1bbae4114500cb1735120" + }, + { + "id": "9bf79345-fdc9-5661-a666-68d1ca6ade79", + "version": "MTc1NjMyODE3MTE1MzE0ODAwMA", + "digest": "sha256:4121b3bd0426050ed57de86c6ed0e8e3e7c2a8b22931581951a6437f901883aa" + }, + { + "id": "9c112a9b-20d4-5ed2-a799-8d73dcc82df8", + "version": "MTc1NjMyNzc3OTkzODY2MDAwMA", + "digest": "sha256:846ff7c770b84f8c3604029a2e9d93f0e9be990a26455f61142ee78e567c6eca" + }, + { + "id": "9c1a94d0-dae7-5d78-813a-c964b227eefa", + "version": "MTc1NjMyNzc5MDI1ODYzMTAwMA", + "digest": "sha256:f7f688a9a64cb241a340e4b87f95201d62b3886b2308c3441381a7f2fda87968" + }, + { + "id": "9c2682ff-aca2-581c-8220-3790fb5230d0", + "version": "MTc1NjMyNzgzNTkxOTk1MDAwMA", + "digest": "sha256:bfe5c0a123e011c18c3ed273168e3a1ada9a1f28c58fbb59177f9d9d978b813d" + }, + { + "id": "9c291df7-edd6-584e-80f8-8a62edd141a7", + "version": "MTc1NjMyNzgxMzQ3NDkyMTAwMA", + "digest": "sha256:15ad37c574699921e2ab68d87aadd0d74f5a8f7cd0d9995372bb2074d1924760" + }, + { + "id": "9c413091-553e-51f7-96f0-f999efeb4761", + "version": "MTc1NjMyNzg2NjYyNjg2ODAwMA", + "digest": "sha256:48178237885dc5ca3d8a0ac88d90dfa1b51b8787c8f1d054c549be374fc2c880" + }, + { + "id": "9c5098dc-77b1-5aa1-ad40-53908e39b8c7", + "version": "MTc1NjMyNzg4MDM4NDEwNzAwMA", + "digest": "sha256:e74abcb736459ae2b227af7812ee068b25fcf157dd92ba3451b670bd1b372219" + }, + { + "id": "9c51a838-2e4d-544b-ba56-7369e325c57f", + "version": "MTc1NjMyODA3MDExNDg4OTAwMA", + "digest": "sha256:b75a637b81428f3d4ba02648bea1572115a28870648eef0e564c43983b382990" + }, + { + "id": "9c5ddabe-635c-5700-816e-c2b8faf6425e", + "version": "MTc1NjMyNzg2MDE4NTc0NjAwMA", + "digest": "sha256:038904ce658044fd37cfead36b657d05f606d52ea4f67fbcf8cec3e114943c5b" + }, + { + "id": "9c63e99e-cfa5-513e-843a-5b88ba340434", + "version": "MTc1NjMyODA3MjgyNjU0NTAwMA", + "digest": "sha256:505cf3531666044663fb295b94fee7bc5da8f2970ab64af185d3643ba6d8a977" + }, + { + "id": "9c65af94-ebb3-568c-9cd0-ea578589d6c9", + "version": "MTc1NjMyNzg1NTczMDM3NDAwMA", + "digest": "sha256:d0d07188e4455438b5a51c87c36e78c2d4e9f774e326fdc02ecd929df84eb888" + }, + { + "id": "9c66defd-c707-5e09-9107-816e50239afd", + "version": "MTc1NjMyNzg2NzUyMDYzMDAwMA", + "digest": "sha256:f362d9bab18e5589ceba674e6e3e622b029c5b9e3fa07f205c2328b2dee09c37" + }, + { + "id": "9ca105c3-036a-538a-a07e-4df8e59b36c5", + "version": "MTc1NjMyNzc4MTAzNDAyMTAwMA", + "digest": "sha256:007ab23ffd162a5dddebd599bc120642be98857a949d34210317f175ebe119ef" + }, + { + "id": "9cc2af94-cd67-569c-bc2e-7b2c495404c8", + "version": "MTc1NjMyNzc1NjE1NDA4NjAwMA", + "digest": "sha256:60f69981e4ed2b01159c468880a7c969807816eaaeac173085c8faadd70d9302" + }, + { + "id": "9cc57a84-e34f-50b8-8da3-aedf656ea939", + "version": "MTc1NjMyODE3NzU3NDgwNjAwMA", + "digest": "sha256:8d6df2745fdfb1e603e569eebbd4d54cd9093c3316bde61dce49a297a16a348c" + }, + { + "id": "9cff3b00-7c1d-5db2-82c1-04ff106f80e0", + "version": "MTc1NjMyNzc3NDU0Njk2NjAwMA", + "digest": "sha256:3150e0b9ca5981df9ab2cc9848ac842469a8a1ccb8793cb2c87736b0b6312252" + }, + { + "id": "9d1abf4e-c187-5b5f-9f91-2195a3e26016", + "version": "MTc1NjMyNzc0NDQ2ODY5MzAwMA", + "digest": "sha256:d74f70b52006d5750b5cd947f517e67adaa57b3008c28f938316abf12e15aa77" + }, + { + "id": "9d1b0185-048f-51db-85be-bb3967a40843", + "version": "MTc1NjMyODE3NDM2MjE2MjAwMA", + "digest": "sha256:034513214c216a53ed80b04b0129455f4eaf2b304bd8890ba50be2fe0a180101" + }, + { + "id": "9d2ff53a-fdac-55c7-956e-5fae2bfff47e", + "version": "MTc1NjMyNzc0NDM5NTkwNzAwMA", + "digest": "sha256:af79ec9a3f2805206e3e04d4ee7ae4c69b7489eca0ab87fa9accb9f385356201" + }, + { + "id": "9d40fb9d-ab5c-5b92-bde8-c31f5fe2a93f", + "version": "MTc1NjMyNzc3OTkzMzM1NDAwMA", + "digest": "sha256:108902365abaa7a88b5a075197096cdc1af2a0f7297f461f93930b1c156984f4" + }, + { + "id": "9d56854e-f47b-553c-9f22-3eacd113a6ae", + "version": "MTc1NjMyNzc0NzY0NzA3NDAwMA", + "digest": "sha256:bb7e7aa133dc077e020894225aaaf5176eb20928fcb98c1845bda102e18661c3" + }, + { + "id": "9d576515-9fa1-5ba4-b1a6-5ef80c089b53", + "version": "MTc1NjMyNzg3NjY3NzM4NTAwMA", + "digest": "sha256:0412b76f02435e8834c1aab929561a184dbf244e6fb7fa38cca1351dff4083ad" + }, + { + "id": "9d77e042-1425-5077-a8d2-f9e8a1381bc8", + "version": "MTc1NjMyNzc2MzcxODU0NDAwMA", + "digest": "sha256:fe7d576d507faed3e0ee3eb0d120d4cf3a8ef19f908e6cceed1f0b45742d692c" + }, + { + "id": "9d7d1c64-c199-54ec-beea-0ff390005a09", + "version": "MTc1NjMyNzgxMjA5MDc0NDAwMA", + "digest": "sha256:fb709cd71796bb77985fcf4f0709260e0908f3251e25e72c3d2237041e79a5e3" + }, + { + "id": "9d8ec7a3-3351-511d-80a4-23ac588a85af", + "version": "MTc1NjMyNzg2NzYzMTgzODAwMA", + "digest": "sha256:a27255d49ddfa32e40e82cf79e71f6219333fa39607a36327f3ed82b290cbc58" + }, + { + "id": "9d9dcc61-b97a-5af9-afe7-12c120d16b52", + "version": "MTc1NjMyNzczODQzMzAxNzAwMA", + "digest": "sha256:e51ff163edf53f48e1c968f01142898bfedfcc5e71b9191890ba96144f317f65" + }, + { + "id": "9d9eaf3e-0162-5b2c-b9c2-59bde4f5874b", + "version": "MTc1NjMyNzkwNDcyNDM2NjAwMA", + "digest": "sha256:08b69b88aaf0fc4b6951f516e2f53ed79522415567c307923d1bcf5d602922f7" + }, + { + "id": "9da1e210-0e5d-50b7-8f0f-a09d2d839bda", + "version": "MTc1NjMyODI1OTI4MDMzODAwMA", + "digest": "sha256:746814a8429976bed0f553424e8fd14afc781bf94a86e68f0781087dce02304a" + }, + { + "id": "9daaf192-3e63-53ae-b52b-3560d59cf013", + "version": "MTc1NjMyNzc0MTc2MTU0MjAwMA", + "digest": "sha256:d1d137e39b726a57b36958b850286e8d448e5e3402c041658d7ed608509c1bf8" + }, + { + "id": "9dc9ea45-a220-58af-83dd-10e64e27e2c9", + "version": "MTc1NjMyODA2NzM1NzcyOTAwMA", + "digest": "sha256:81a40c23c85d2044fd6734e60511adbfe6c56cee0a30d9d6d6849ac2066727bd" + }, + { + "id": "9dcbfb4c-b7aa-5d50-8d4c-1e6919206d8c", + "version": "MTc1NjMyNzgxODk4MTY5OTAwMA", + "digest": "sha256:2227c80001e7cff3af6170b8ed3958bae774fd661568aef45cd30e095aedce0b" + }, + { + "id": "9dfbb1ea-42e2-52d5-9c84-314c8a105603", + "version": "MTc1NjMyNzc1Njg4NzUzNjAwMA", + "digest": "sha256:d2ec36172db4c7d66314acb5e6dcaf79f8eabc626cddc7b194c0b8f955d02b88" + }, + { + "id": "9e11e396-a5ac-5adc-9cd7-425bed35a8ed", + "version": "MTc1NjMyNzgwNDU4NjI3ODAwMA", + "digest": "sha256:8c93f288204d20e06431e236718199dc439ad77effcb5882eabe90eac41d096d" + }, + { + "id": "9e31cafc-687d-58b3-ab39-d1e99535b983", + "version": "MTc1NjMyNzg4MTkwMDY2NTAwMA", + "digest": "sha256:c0bd2aa7a9365b81d927252e91ea1b26e2f760c26c18162e2669296f3c89c41f" + }, + { + "id": "9e415479-ce80-53b2-9b4f-fd4a828392ea", + "version": "MTc1NjMyNzg3MTk4MDY1MTAwMA", + "digest": "sha256:e7cd8489e4c06cdc5409e6469262048c2843e6227ef1ac042918c1a2d4b8f8c3" + }, + { + "id": "9e4787e4-9b69-590b-a90f-4d4d984b0ea2", + "version": "MTc1NjMyNzg1NDQ1NTg3OTAwMA", + "digest": "sha256:36c698d711f2534a2c4fc83edad559e4973f8770323d74d3bf2f656daedf503f" + }, + { + "id": "9e9367c1-d165-5268-9dc0-f8119df909c0", + "version": "MTc1NjMyNzc0NzcwMzcyNDAwMA", + "digest": "sha256:90dce4be2c1f120e067625362a74b8e7fd4a1eb0803bd733bcb225cf97933b18" + }, + { + "id": "9e982eec-4793-5ed3-bcd9-52d7d0098726", + "version": "MTc1NjMyNzc2Nzc0MzA0MjAwMA", + "digest": "sha256:ddbc8ee0cd50b3b487c4e0ee93d9b4f1923edf2328258f724ab2de4d0ad20559" + }, + { + "id": "9eaba466-8bbe-5bee-b72e-2c69d9bed1ce", + "version": "MTc1NjMyNzgxNTUxMDc0MzAwMA", + "digest": "sha256:42b756c8841262824ca13db6e470549767a31c913c3f93796733c6432cce9b17" + }, + { + "id": "9f085586-3f79-56ea-a80f-3378da865d04", + "version": "MTc1NjMyNzg0Nzc0MzI0NDAwMA", + "digest": "sha256:e1692ad91661c3dfd33ad285e14c234d9f72cfd0ec7e90a201b6c05cea45ee86" + }, + { + "id": "9f1563a5-01ca-5d9c-b1e0-9604a31d0108", + "version": "MTc1NjMyODE3NzU5ODU3OTAwMA", + "digest": "sha256:c6e8cf6e633934c0410fa81acc9070425ada744b6598417cfe98727050d24dbb" + }, + { + "id": "9f1d5d1c-1b93-53c2-ba52-d6e671fc7cf3", + "version": "MTc1NjMyNzc3MDUyNTU5ODAwMA", + "digest": "sha256:134fdb32a7da18020900f505bdcd96939399613db96f3079899a8c39970bd476" + }, + { + "id": "9f23765b-4acb-567b-94c5-01c95fbaec98", + "version": "MTc1NjMyODE3NDQwNTUyNjAwMA", + "digest": "sha256:9d2daa1e28200de9ede8b43d64dd8766f2e1210a86a8807c5c1663456b3ccc3c" + }, + { + "id": "9f338ac0-f6e2-5d9c-bfa1-6f1fa3489a20", + "version": "MTc1NjMyNzgxNjM5NTYwNTAwMA", + "digest": "sha256:82cf8fc37adac6627a9c60a4ec2bd9acb7f4350f3816b5f3b5b2d36e30143f7c" + }, + { + "id": "9f5ec194-84a3-5fff-9ad6-1210ff51d6e5", + "version": "MTc1NjMyNzc4NzA0MDgzMDAwMA", + "digest": "sha256:e7379245c9ef1f62aa121506f8ee9b581be5e5e1cf6b05721bf66715f60bfaaa" + }, + { + "id": "9f7d9306-7be3-50a7-a011-72b8b4b8ddb9", + "version": "MTc1NjMyODE4MDQ3NTM5OTAwMA", + "digest": "sha256:c70f496f546cdb29fb79a56fe9cf95e749eaa4e2a07ab7227ec3089b6379e09e" + }, + { + "id": "9f85805d-c9a1-54c9-a46b-be9b3def111e", + "version": "MTc1NjMyODE3Njg0NzExMjAwMA", + "digest": "sha256:536311154cd184339d2e5ea79cef126acdc3b6d0bb4bc85d2e31672ec0cac4ea" + }, + { + "id": "9fa8b2e9-3fc0-5f7f-af5c-94962eabbdc6", + "version": "MTc1NjMyNzgxMzUxMzkzMzAwMA", + "digest": "sha256:97c29abe34da84fd41ca64ca2a380d38e14e314289b279a7eec8511e12c9c94b" + }, + { + "id": "9fbbf24f-ff44-5a48-a495-b1aee96217c0", + "version": "MTc1NjMyNzg0Nzk0NDYwOTAwMA", + "digest": "sha256:caebb56342a2f3d1ce65120df4391338b1fe21135d4b2292b51fd97171388ba4" + }, + { + "id": "9fbfd850-7306-5cb8-a56b-48e7ef6fbf8c", + "version": "MTc1NjMyNzc4MTkyODMwNTAwMA", + "digest": "sha256:ea6b4c73ff4e17b22acffa44f57d50e0eba05725ea4fdfa2441529eda59256bc" + }, + { + "id": "9fc260c0-70f5-5dd1-ba49-a62fd15d68db", + "version": "MTc1NjMyNzkyODgxMzI1ODAwMA", + "digest": "sha256:c0e7fe8a1baa32979d5b8df61fd783b565c97aa9f18503426d62c72dbaef28a4" + }, + { + "id": "9fc46a0e-ba72-5842-bfc8-47f7767add46", + "version": "MTc1NjMyNzc3OTc4ODg4MDAwMA", + "digest": "sha256:27b3757878858b0de5540fef50f8eb7d24cb477453904cf80ca8c279f613933b" + }, + { + "id": "9fd380ae-fad4-51e5-aceb-99c4a2f08749", + "version": "MTc1NjMyNzc3MDYwOTE2MDAwMA", + "digest": "sha256:46bf0f716c69c15720cbbb2aab389eb28565c967e66ba120314f3b115281ecbc" + }, + { + "id": "9fea7d75-da5f-53c8-9df2-2138278e0c33", + "version": "MTc1NjMyNzc0MTY5Nzk1NzAwMA", + "digest": "sha256:50ea289cd41faefc33d3c419b5e69f2a462bb6cbd4c6203368522de1b0fdcde5" + }, + { + "id": "9feb87f5-139d-58da-a06f-5e52cc599422", + "version": "MTc1NjMyNzgxMjA0MTUwMDAwMA", + "digest": "sha256:b8c3d7a559700f32710f3121a1b471374bd4d9617897585781217b9311f88fa2" + }, + { + "id": "a00b1c08-69f4-5ec6-9669-5a0624560758", + "version": "MTc1NjMyODAyMzg1OTYyNTAwMA", + "digest": "sha256:2e9e360af6c827591e3232842bcd3cdc4b1d825447d39358eab97e2771679cf6" + }, + { + "id": "a00f467b-2f56-5667-9f73-12179c4c10a1", + "version": "MTc1NjMyNzgxMjY4MTY0OTAwMA", + "digest": "sha256:33c95dadb55779c6df0260b3e0271a3678a5f87458286ab6dfcbd4bc2f19bdc5" + }, + { + "id": "a01815be-cdc7-5d65-9723-fad56bfb7fd3", + "version": "MTc1NjMyODE1NjM1OTkyNTAwMA", + "digest": "sha256:e5aabaec462cf5bf4dc221b81cab07ad96e2d8bffad43b1e91a89f74548af972" + }, + { + "id": "a01b28fa-9559-509e-b1c5-5ef903c6d08e", + "version": "MTc1NjMyODE1NjM5MDQwNzAwMA", + "digest": "sha256:15bfbd8ad70ad41608f18ef3629a3e9d687e125c2a36084c854538132333e7dc" + }, + { + "id": "a02cafe5-d3ee-5acd-881f-adbd6315b47f", + "version": "MTc1NjMyNzc1NjkwOTcxODAwMA", + "digest": "sha256:3c4add3546b917de110bf0356512be19561371d13045da1af2c88b23d870834b" + }, + { + "id": "a03eff59-9231-58d7-bf6a-c22a4a8077f5", + "version": "MTc1NjMyNzk0NDc5NzQ3MjAwMA", + "digest": "sha256:03dca8d49419dc0f3b1a27686168c40362b86c947cda8a45132c564393e26f46" + }, + { + "id": "a03f911f-6a4d-5232-a51e-ebf605a85c7f", + "version": "MTc1NjMyNzc1MzU1ODkyOTAwMA", + "digest": "sha256:45858a3089b2439d2fcd4214ba57b8a6361f396a5d99a716b3c6044e31be4135" + }, + { + "id": "a04e56ba-2387-568c-b281-ce666cb824cd", + "version": "MTc1NjMyODA2NTY2OTE0MzAwMA", + "digest": "sha256:f348857a740ea68b25ff6346b5aaa70ba6719f1c5439fbefa9c93a7bd386f99d" + }, + { + "id": "a089abf1-d105-5c9b-8565-c2b9a97703f3", + "version": "MTc1NjMyNzgyMjgyODgyODAwMA", + "digest": "sha256:4c8c6c29d462fe0a83a5a1aa761ebb7bb67d2302ad94e163f7cec24d15984f8e" + }, + { + "id": "a0939880-556f-552b-bf58-39a58c7308d3", + "version": "MTc1NjMyNzg2MDI3OTk3MzAwMA", + "digest": "sha256:be36967884034d7e326a25e3ae64307f27c9be938a5ecfd0c0a6df742110da02" + }, + { + "id": "a09de9d1-481a-55ed-9682-5f6311607771", + "version": "MTc1NjMyNzc3NTE5MzIwMDAwMA", + "digest": "sha256:1ecf8b8ef731203d915dcaee986f8f4c98fdd164e8f1f3dd45ea3de528e615a5" + }, + { + "id": "a0d0e9ef-afcb-5df7-9f8c-50f8b5947aff", + "version": "MTc1NjMyNzg3NDUzNTQ4MDAwMA", + "digest": "sha256:5f67a0d380b8d77eb6c10bbfd8fd03b0cdf1d35c07b33831124eabb3077be552" + }, + { + "id": "a0e0af88-9959-5888-85c3-5c45a59b6549", + "version": "MTc1NjMyODE3NjgzMjQ5MTAwMA", + "digest": "sha256:8c0e3b7ac1050e2025d2941d433e3dd5e96a5e8f2a61b4250ac52480aa34797d" + }, + { + "id": "a0eb38bd-5509-58a3-a5fa-a2566ce1516c", + "version": "MTc1NjMyNzgwNDU3ODE4MjAwMA", + "digest": "sha256:406b179770847963d9befcd40682cafb73a85c07b1d0d2debb9bbdf6daa89f18" + }, + { + "id": "a0ec09fa-dd09-5264-b2d9-654d6a62161b", + "version": "MTc1NjMyODE2Njg5MTc0NzAwMA", + "digest": "sha256:4dfd587b5f223dbc625e8b430e7043b9ab8b9b8e2251944c8e9de3c5d35700c7" + }, + { + "id": "a0ff7b2b-8bc6-574d-a3fa-a1ede36171b1", + "version": "MTc1NjMyNzg1MjExODk0NTAwMA", + "digest": "sha256:b428135b5f9a3edbb4a7e0f1fdd0b368b3bb5570c5193918e04c118e69737950" + }, + { + "id": "a1226f1e-5f6b-517c-985e-5a8bd3ba33cc", + "version": "MTc1NjMyNzc4NzM1OTIwMjAwMA", + "digest": "sha256:366291050764ddf0e9729318c07df81158dd28fec5531603e2a80ffc32616a13" + }, + { + "id": "a1229c9d-e5a2-51e1-9e29-77d3634ea84c", + "version": "MTc1NjMyNzc0MjM3NjE4NzAwMA", + "digest": "sha256:eeddf15fae5f7efe2d8a1a50f1e69783351ff7709f9cdf1796872067dbbd50d6" + }, + { + "id": "a123c7ea-83cd-5375-b93f-0f3a2c961f47", + "version": "MTc1NjMyNzc3MDU4ODM0NzAwMA", + "digest": "sha256:b77e393f1b824b61cebc8fbbca29fdad598e930c545c09629627ca2ef7476843" + }, + { + "id": "a1463aab-3369-5d97-b1d8-f8e739a4c2cd", + "version": "MTc1NjMyNzg1MjEzNDI3MDAwMA", + "digest": "sha256:cf3ec23393625670bdda90680872f85bbee3491540ae0f5b37854a03642f78cd" + }, + { + "id": "a1481737-2402-5824-9280-07f28ca87068", + "version": "MTc1NjMyNzgyMjc1Njk2MzAwMA", + "digest": "sha256:c77f4fde41f6a0c037b755323ad4a13b3346299cea1fefe17d03949c0fbe297a" + }, + { + "id": "a16e689e-e948-51b0-bda8-e5c73b8afce5", + "version": "MTc1NjMyNzg1MzgwNjEwODAwMA", + "digest": "sha256:bb11717147604cdfe87d1b80938391f83ffd8fb1c22a6a0f57e788b174f50a22" + }, + { + "id": "a1859dd8-1305-55da-b5ab-a6386dc06f6c", + "version": "MTc1NjMyNzg0Nzg2NzkzMTAwMA", + "digest": "sha256:28b6e8a1f49725313c172de774f550502749698098ec86f3662b0a6ab337ae0d" + }, + { + "id": "a18dda91-3f17-5495-8a82-dd3adb65a294", + "version": "MTc1NjMyNzc0MjM2MTY1MTAwMA", + "digest": "sha256:e1a6797dd83f39a6c803a4f1bd670e19c77cdfe2198286f36b6a2bd9cc769739" + }, + { + "id": "a1928fef-08f2-50bb-b1c3-938444b75c81", + "version": "MTc1NjMyODE2NTg1MTQzMjAwMA", + "digest": "sha256:c7cea032472d2e958e6851e37f4c17ba5ea9a5bdbc29dd1b0cf145c71c53df83" + }, + { + "id": "a1bac6fd-87ec-5279-99dc-698561861b1c", + "version": "MTc1NjMyODE2NDUwMjY5NjAwMA", + "digest": "sha256:c7cd66a25fa25ce8bb50b566438060f26215fa84a5786b96218bdc2dda401788" + }, + { + "id": "a1c6e1c7-3715-5860-991b-9ef867cb36ff", + "version": "MTc1NjMyNzgzMjY3MzMxOTAwMA", + "digest": "sha256:39578df46ad93c58e1f90fb6fc79a1c73c8c8e0f4a5d99f45d51c6cb6b2b7acd" + }, + { + "id": "a1def8e4-843f-5d30-a527-a4604f8eb0cd", + "version": "MTc1NjMyODA3MzAwNDg3MDAwMA", + "digest": "sha256:f28cc84b6118cd073193c90facadf0b94ba296bf92eadb71f514510df31bb2b9" + }, + { + "id": "a1fafbb3-22b8-5239-a87d-490455c7eba9", + "version": "MTc1NjMyNzkwMzczODcxODAwMA", + "digest": "sha256:aca2605f1100ad1ddc68d9f8aed03aa5644be790e7fd47e580c5c30fb9758c42" + }, + { + "id": "a1fdcd6f-a6ad-59ba-83bc-a29cdb358225", + "version": "MTc1NjMyNzgwMzUxMDEwNzAwMA", + "digest": "sha256:55722a738f70e9083870491f565c6a145dbd2ca9a3dc03a340edb9a723c63516" + }, + { + "id": "a200feb5-085f-56c1-9abb-7dea8200d423", + "version": "MTc1NjMyNzkwNTAxMDU1ODAwMA", + "digest": "sha256:32ae6a5d5ad1354ba2087285fbe0c7fbe769bd5fece677dcda831ce12181b9dd" + }, + { + "id": "a20615f0-9dd5-5071-9220-1279f5c83bf1", + "version": "MTc1NjMyNzg0Nzg0MjQzNTAwMA", + "digest": "sha256:c69e803c62a1e75127aca59f0a4f1f619d177392cf6e575e70135e1b85cf7933" + }, + { + "id": "a267f995-c192-50d8-b8f9-9e17af7a288a", + "version": "MTc1NjMyNzg4MTk0Nzg2NTAwMA", + "digest": "sha256:4d0551b5ae2437d3dd79aaf6d5c0565de5e49bec8b79208f4f488a0f727c8c0e" + }, + { + "id": "a284ab3f-31d4-582d-a2f5-6eb8bb1ad781", + "version": "MTc1NjMyODE3NDMzMzYxMTAwMA", + "digest": "sha256:2ab25fcf2fe99029bd57bd98e40e49c1a1962804144ad2cf631b0e4dfb9c19a0" + }, + { + "id": "a293b68d-3926-5aa5-9476-405506a2e9b0", + "version": "MTc1NjMyNzc4NzQ2NDIyMDAwMA", + "digest": "sha256:1ff65cdc80c4d8e1b2fbaacf69e925fd1ea2272be154cabc95bcdc90e0cfe62e" + }, + { + "id": "a296c0cf-6671-5ee9-9f3b-f8e5400aaf3e", + "version": "MTc1NjMyNzg3NDkyNzYyODAwMA", + "digest": "sha256:6a65cb2efdd9ff068c053e732a20bd7c624e24a05ca8dbba74917aa5affef9db" + }, + { + "id": "a2bcb1f6-45c1-553b-b4f0-db12b67d083a", + "version": "MTc1NjMyNzc3MDM5OTIyMDAwMA", + "digest": "sha256:13a48daf502eed58b8d194544a377fa758978f0ddf228a6f3aee5fda4bcb4e39" + }, + { + "id": "a2bef1b6-a0a1-53b2-a7f1-436a27bbfb8b", + "version": "MTc1NjMyODE3NjE3NTE1OTAwMA", + "digest": "sha256:8a92da4ec4fda9f20f533ede6e850ca3397efa77b5470c7471486be17dd341e9" + }, + { + "id": "a2cc706b-683a-5286-a014-d5b618064cbf", + "version": "MTc1NjMyNzg0MzA4MDM2ODAwMA", + "digest": "sha256:142a7f124c3551cf6f921c1c928427dfec2e2368ed0444756184712e7a868a93" + }, + { + "id": "a302bc6f-bddf-50e0-984c-5a5a711f96de", + "version": "MTc1NjMyNzc3MDYwMTU0ODAwMA", + "digest": "sha256:26ecdf8937d6a924cbc7ca77d70d0eea431af220cd7ca81b9e0c6313467bf1df" + }, + { + "id": "a305865c-6e4f-570d-9a0c-5a390f1c7318", + "version": "MTc1NjMyNzc1NzY5NTE1NTAwMA", + "digest": "sha256:2d27713628f1ec555e8b1c60c130eab7cae804568b166725f55c4e59c1a5f698" + }, + { + "id": "a30752a3-c777-59c7-89f9-7b4ad40fb790", + "version": "MTc1NjMyODE4MDM5NzI4NjAwMA", + "digest": "sha256:d996389ba8e13b3217bfcaee0bc22215f009b4180f6fdfddb339f9461a677f41" + }, + { + "id": "a33f2773-de17-539a-9872-0eaed1a1535e", + "version": "MTc1NjMyODA3NzU3OTQzMzAwMA", + "digest": "sha256:81f6182290fdee6d621673b97d6bde1c193f95cf06b5143ee70d41e840da2442" + }, + { + "id": "a35d25be-74bc-5207-9957-9f381917ccb8", + "version": "MTc1NjMyNzc0NDQ4ODIzNTAwMA", + "digest": "sha256:8da43775286262e2c32a667d40be13adea3d3e9a7b428e83130ef662ea68fd16" + }, + { + "id": "a36f9c4c-ae77-532d-ba9f-28b6a4ea2f5a", + "version": "MTc1NjMyNzg3MTUwNDI4MDAwMA", + "digest": "sha256:2ff7cc61b8fce6f876ff582e942803a2cf5f5dacd8fbb7e0084a7ba97915d154" + }, + { + "id": "a3756f30-f79e-57be-ba94-db9d65bf3d88", + "version": "MTc1NjMyNzg2MDMzNjI0NTAwMA", + "digest": "sha256:a6e4facd2fbbe99b8578649ea6420b371d64e3d1253e0aa6037d8882b747d134" + }, + { + "id": "a37bcc16-16ba-5f6e-95bb-54396fce20a4", + "version": "MTc1NjMyNzg0MzA1NTAwNDAwMA", + "digest": "sha256:bea88c9ce44f0f79cc3d56537c1428c569b2d7c7a50b589f08a464473c70e63b" + }, + { + "id": "a37c1d47-673e-50ce-8810-1e51f8f0574a", + "version": "MTc1NjMyNzg3NDk0MDUyMjAwMA", + "digest": "sha256:411f407d3868df6cacd52948a5ea03dc6386ccf3171ad4ad4148225f3c07cc90" + }, + { + "id": "a3b46274-e8db-5845-a2ec-47d7a83964be", + "version": "MTc1NjMyNzc5NTk3MDgyMDAwMA", + "digest": "sha256:119e78d09a31f816ad8694a6ea16110dd2d80a0898ad17058ebcee05060ac6c6" + }, + { + "id": "a3c03e2e-3944-57e8-9080-6267f1bce358", + "version": "MTc1NjMyNzc5NjExMDcyOTAwMA", + "digest": "sha256:47f60ce7d1e7f8bcaaa57af7c39b9e13bea60b4bcb59aa6dd49bc790ba6c7d1d" + }, + { + "id": "a3ee760e-7a34-5614-97d6-bc27152e6e32", + "version": "MTc1NjMyNzg0MzAxNDkwNTAwMA", + "digest": "sha256:9c29bba68125c04e300cb0e82cb7e477888d3f526596ab3bd72110beffa33404" + }, + { + "id": "a42aa1d1-0f7d-586c-aef7-64d56bfb6ccd", + "version": "MTc1NjMyNzc2NjMxOTkyMzAwMA", + "digest": "sha256:e7db40733285ffebb4e1fd8cec8d24cf3e1f23a6e0219b6b9a44dd2c220794b3" + }, + { + "id": "a42e61d8-af1a-596d-a7ad-cf9843f97864", + "version": "MTc1NjMyODA3MDE4MTYyMzAwMA", + "digest": "sha256:bc18309100a53d6ea9e54fbf4f749d5545956048f8473dcc17262b750f4516d8" + }, + { + "id": "a447c7d9-d4c4-579c-86f4-0016fe37fefd", + "version": "MTc1NjMyNzg3NTUwODY1MjAwMA", + "digest": "sha256:9255db57178723674cfe6f5ec6eb2629839ef4f66d9cb6e557e94d865321960e" + }, + { + "id": "a47a96b4-8994-57ba-81b7-b73d8d8c757f", + "version": "MTc1NjMyODE3NDQyMDYxOTAwMA", + "digest": "sha256:1475d4b9d81f41bcea7904aa62529467970a43b2748e563f95c4dbf479e45f33" + }, + { + "id": "a485c658-1f75-5779-8d62-cca85d5e851d", + "version": "MTc1NjMyNzg1MzcwMDk0MTAwMA", + "digest": "sha256:8f65185e4963152bc70b222cfe0d7110504a062cfa112a4fb7fc2ef0f4ed1268" + }, + { + "id": "a486f9fd-273c-51cf-9aae-a21ed11d42dd", + "version": "MTc1NjMyNzc4MTAzOTYyNjAwMA", + "digest": "sha256:a1f5cdbc24670dc71dcb6a10918a3b91654ca57194ec71fb45779d536b695cff" + }, + { + "id": "a49712d4-3c6d-506d-9340-13b07d8e8b3d", + "version": "MTc1NjMyNzg0MzA1Mzc1MjAwMA", + "digest": "sha256:d2217958abb7284e0751dc97c4c7270b2e6147d5b89b9d88655cbe9ae62e1108" + }, + { + "id": "a4bcc15b-1c1c-56fd-8721-58ef0f5e7f5b", + "version": "MTc1NjMyNzg3NTQ2MzA4MjAwMA", + "digest": "sha256:e1ad620f2f56656ded76b37621e83f3c3a41aa00d5895a5ec0e7bdcdd141780e" + }, + { + "id": "a4bde125-641f-5ec4-8e39-7be39435cc1f", + "version": "MTc1NjMyNzg4MDAyNDAyNTAwMA", + "digest": "sha256:e0ed19cd6d7dfde100c32f918cf17d6fa856f4f5ccf2b41b65783f37c85708bd" + }, + { + "id": "a4c64ec2-fb60-5b86-a3ed-d6e71aed92c6", + "version": "MTc1NjMyNzg1MzIxNTYwMTAwMA", + "digest": "sha256:46e537e0f11f34e413f29439bcd4308db6a91ac8b7f9ce49c13d87ae38d0142c" + }, + { + "id": "a4ceebba-df08-551c-9945-28e40a42a36e", + "version": "MTc1NjMyNzg2OTU0NzI5NDAwMA", + "digest": "sha256:109b796f50df2e2954511c66cef1d18482cccf777a655f21e76b9b732e165169" + }, + { + "id": "a4dcb6f9-57d7-5d46-a6e7-260d47e7d921", + "version": "MTc1NjMyODE3NjIwMTMwNDAwMA", + "digest": "sha256:42ebbb2907c60b73c89255477e3c4891d7e2175da2c962fba3b2ddfa2454331b" + }, + { + "id": "a4f5e82b-3b40-51a6-8a96-9e8a2638bac6", + "version": "MTc1NjMyNzg1MjA2NTMyODAwMA", + "digest": "sha256:2b605e8948ba4421b159c826e0a10eb1bb5924e1c34648e7161fe3b0ff7c07b5" + }, + { + "id": "a4f730c9-f1c4-5fb5-8bea-eb11fee26472", + "version": "MTc1NjMyNzg2NjI0ODAyODAwMA", + "digest": "sha256:24bda66f89812a48d0707e560ed23e21a5685d931821d56b962941a7e29c48fe" + }, + { + "id": "a5298236-d355-5616-9c51-d9bb427ad5f8", + "version": "MTc1NjMyNzgwMzUyMDcyMTAwMA", + "digest": "sha256:f053cfbf39a23979e8de961e8e0690c071663be67ba7c9bf614881e8f5f139f0" + }, + { + "id": "a52d2ec6-f855-5454-a69c-a0c881496b59", + "version": "MTc1NjMyODE1NjQxNjY4MjAwMA", + "digest": "sha256:bc825ce9ee8b24a55d69a81ca7f016489ef78fcf19a4c5167c465d259f4f7f8b" + }, + { + "id": "a5344627-b593-59ad-a41c-e11c426577b5", + "version": "MTc1NjMyODE2NDQ5MTYzNTAwMA", + "digest": "sha256:a770e8d67360e7ef8c645b66579aa4ef9e2d84257bb4a6d49bed6e6c8cabace5" + }, + { + "id": "a53abd45-2227-5d20-ac51-4ed97b987c10", + "version": "MTc1NjMyNzgzNTY4NDAwNjAwMA", + "digest": "sha256:0371fc8bf2bab025daeb8193292242e01a654b5549d9ac73dfc5bae61759002c" + }, + { + "id": "a56abe88-1b80-5757-88df-1f2c1ec3f66f", + "version": "MTc1NjMyNzg3Njc3MzQ3NDAwMA", + "digest": "sha256:7a52fde750306ebc3ecd7dc8c89751156be64ec5e958858dd0a7cf1fcb4b689d" + }, + { + "id": "a5921acd-38ab-5ca4-bb21-179516fe38c6", + "version": "MTc1NjMyNzgxMzY4MDc4OTAwMA", + "digest": "sha256:c63887b023dffec6c94a6c4f3a0fa3d96481c920d35fffebdd1022c601cc3423" + }, + { + "id": "a59e93ae-cd6c-51a3-aaa3-1bd85b544361", + "version": "MTc1NjMyNzgyNjU3Njc4MTAwMA", + "digest": "sha256:53e216492a56ccfc47ef49fa43ff685ec13494f394e7d653b7aac140e60dcf51" + }, + { + "id": "a60b20ba-7086-569f-988f-7ce82d07d9dc", + "version": "MTc1NjMyODA3Njk2MzMzODAwMA", + "digest": "sha256:2aa1705631c91c9997026bc17e343ffb50c076c2a2a7580688fd89b74f4676b6" + }, + { + "id": "a61984fd-6bf5-53a4-9097-0cba91207bd9", + "version": "MTc1NjMyNzc0NDUyNTA3ODAwMA", + "digest": "sha256:bbf226ed688db64dc07531ce0a9b83769bc0eb9aad01d46c1627d51327cfe469" + }, + { + "id": "a62cc116-3e93-5bbc-b977-278051b3ff97", + "version": "MTc1NjMyNzkzNTgyNTgxODAwMA", + "digest": "sha256:eb3c6cb839b62d26fc944a41094e4900f71c0f5e539dbb1ce0b36ec1bbf852b1" + }, + { + "id": "a6673c54-3008-5e29-a532-70136a2e371c", + "version": "MTc1NjMyNzc3MzAzOTM4MTAwMA", + "digest": "sha256:d9e3fc5b40a843131579e3dd7febc061bb48316fd8ce611eb0b5cb886f877dd9" + }, + { + "id": "a684cefc-a72e-54c3-9ab5-88caa3a0c9be", + "version": "MTc1NjMyNzg3MjEwMzI0MzAwMA", + "digest": "sha256:3d23fe28313a95ab02f484c7c590e42b7a4e2b4a123ccd5a1baa4ab0f18074b5" + }, + { + "id": "a6bf93be-3496-5373-997c-a322287c46fb", + "version": "MTc1NjMyNzc0MTY3NTc0OTAwMA", + "digest": "sha256:58a4ab968ce9a574557cc45ee879c718fc9c365c2dce9121f4b6a7abf7ab490e" + }, + { + "id": "a6c43b19-0b1f-5400-a39a-f9c2c6183a4c", + "version": "MTc1NjMyNzgxMjA0MDM0NDAwMA", + "digest": "sha256:ae2b28e65096769fd446934e30d1aeca71915cf7b39f14e56a16ac4f2a41b188" + }, + { + "id": "a6d3ec3a-c231-5beb-95a2-78df80ab9291", + "version": "MTc1NjMyNzc1Mzc2MTk2NjAwMA", + "digest": "sha256:bb42c7b0de46727bc0d0c338dcc046e31d8aebb37d3c6080057cc2fdd56bc4d8" + }, + { + "id": "a6d53689-43b1-59b1-bf96-b9fc9569c984", + "version": "MTc1NjMyNzg0MTEwNzgzMzAwMA", + "digest": "sha256:54c64e362ee7b6f00c9b89f2ec58a82e190c8a7ab94f097e73c0bf6c4e831b82" + }, + { + "id": "a6eff71b-e84a-598b-8d1e-7281f7645fea", + "version": "MTc1NjMyODE1ODE4NzY1MjAwMA", + "digest": "sha256:bc4ad817296f91659335ac7a3ddbfa4e81bf6c4948aab4b1af7ab0fb0c7229d5" + }, + { + "id": "a6f0da89-2641-5181-9232-3930091facb9", + "version": "MTc1NjMyNzg0MzA3ODE1MTAwMA", + "digest": "sha256:7fbdad9ad0416d9b974905e44600b558c963124ac9c31844703d699d5a3b37d9" + }, + { + "id": "a718d71c-293b-575b-928d-ab152227e32f", + "version": "MTc1NjMyODE3Njc5NDM4NjAwMA", + "digest": "sha256:fcf24df851c3f70648353f9729a8b182db678d12b7ae49913116c4fc568b1589" + }, + { + "id": "a728fd30-bdcd-5e18-9177-507f2998f895", + "version": "MTc1NjMyNzc1MzgwNTE3MzAwMA", + "digest": "sha256:63566632f8c35de31063a75daf739917120a0ac317298aa368ecd52bacad6954" + }, + { + "id": "a750e8a1-436b-5d82-993f-4b768331b308", + "version": "MTc1NjMyNzgzNjM4OTQyMDAwMA", + "digest": "sha256:49c4f186389de45acfcef63c78c1e36293d6b996c9869b1f8b46ac45dd441b12" + }, + { + "id": "a75ca0af-9554-565a-a4fe-4a04b9f88d8d", + "version": "MTc1NjMyNzgyNjQyMTUzNjAwMA", + "digest": "sha256:ef8b2c2ea66ea022a1f552a9cb3f963504e98802fa1d1b3e2af768eae0fafbd3" + }, + { + "id": "a767fa46-7113-5138-b000-7ca19adb1f9b", + "version": "MTc1NjMyNzc3MDQxMjg0MTAwMA", + "digest": "sha256:3efa0d3547d53de4ed389606aa092b69c5c91a6aacd5f44dd83a19fd5717a5bb" + }, + { + "id": "a7b07408-519e-56ae-8ad4-8f756505a439", + "version": "MTc1NjMyODA3NDk1MjEzMTAwMA", + "digest": "sha256:745508f9db1ef68544f09ddedd59889fd8eb2bf02e2902cec2f906e979fab451" + }, + { + "id": "a7b6c66b-f709-5396-9402-f5aebc247b32", + "version": "MTc1NjMyODE1NjM5NTYxMjAwMA", + "digest": "sha256:d5a112b4553c168fa4803abb8040e700de8411dd45674c312a5e6c005236fee3" + }, + { + "id": "a7c8c342-19a8-5385-89d9-58c47209f692", + "version": "MTc1NjMyNzc3MjU2NTg3NzAwMA", + "digest": "sha256:ade30cf32c2cc8466c4e38a4dd4a38bdef6e9b2144ea313104f849b7ff4af6c1" + }, + { + "id": "a82e3dbc-cabe-5b34-a682-318f1afe9446", + "version": "MTc1NjMyODE2MzA1ODcxNzAwMA", + "digest": "sha256:96f6bd71625d9f4882d905f24ba41c6c141701f4f9c6efbfb0481c3c5f11cf09" + }, + { + "id": "a87afc02-e78f-5f94-84aa-2023497f79b0", + "version": "MTc1NjMyODE1NjI5ODA1MjAwMA", + "digest": "sha256:e8de3fb16d57d37510e844ca595972a209042859d28ab86c820ebfc560b25cc9" + }, + { + "id": "a87e3391-c143-5c39-991b-a21bda88c10d", + "version": "MTc1NjMyODE2MzA2ODg2NDAwMA", + "digest": "sha256:15cc9930f4afd136384ddf9e033c826c3609bee2fd99c53d0afd9598551d1637" + }, + { + "id": "a890c488-7b62-55f4-80ce-4d5dd6255e8d", + "version": "MTc1NjMyODE1NjM3NDkwMjAwMA", + "digest": "sha256:9e4e157fe428cbf34094b5cb29069a7246eab549076fb90b2d1d486e0c94288a" + }, + { + "id": "a8a1b476-8e02-51d4-a8f2-b19dd2a000d3", + "version": "MTc1NjMyNzc3MDYwODcxMDAwMA", + "digest": "sha256:09f958924bda4b6bbadf9b6c35ed0633f7ef867b60b88a3c7c304b9fa145ebf6" + }, + { + "id": "a8a3ff09-6c9c-57f7-9ac7-99319d04fd25", + "version": "MTc1NjMyNzczODM4NzI5NzAwMA", + "digest": "sha256:75ba50958a3a500bfe4c51c538f715da5984757151ac67ee9a18806f7e9b44b6" + }, + { + "id": "a8c2cd35-5b27-5b3a-abc7-f2213be10448", + "version": "MTc1NjMyNzc1Mzk1NzA1MDAwMA", + "digest": "sha256:4d058935532191a8fbb861ad8b28a4540d57c05b5749d738b31f1a483044b476" + }, + { + "id": "a8c9da41-21f9-5496-8eed-45dce819ac36", + "version": "MTc1NjMyNzgxNTUxNTk1MzAwMA", + "digest": "sha256:39d7019ad3b51633fe00ce405ab7cc0324647b2c7d09c4ede9b5ff60aa224d0a" + }, + { + "id": "a8d80979-88bf-5f1f-869c-edbdf04ae1b0", + "version": "MTc1NjMyNzc3MDYzMTUwODAwMA", + "digest": "sha256:a461a25053c2c33668d93e0f16a5eda96fcc87e71d3a7e0ba2ce60d6cdda7c77" + }, + { + "id": "a8dc451c-7b4c-5706-989b-5c98fcb35bd8", + "version": "MTc1NjMyNzgxNjMxODUzNDAwMA", + "digest": "sha256:27e14a34f51e58e7585f660a64e9826ca28d8c04be45bde9f3038425bd74db4c" + }, + { + "id": "a8e20f20-4d96-50ba-8115-c096e644a48c", + "version": "MTc1NjMyNzc3Mjk2MTY1ODAwMA", + "digest": "sha256:1d77e953486bd83477cae5f5621927ff2466988eab5938b7c72ff7e16638d9d8" + }, + { + "id": "a8ea6741-27bc-593d-afed-b72f29232a6f", + "version": "MTc1NjMyODE2OTAxNTU0MzAwMA", + "digest": "sha256:f8375412e839914a290fd83a59d479683810319727e037e3831e0314cd860a45" + }, + { + "id": "a935c6a4-4f5e-5c94-be33-a7b1f12f3ceb", + "version": "MTc1NjMyNzc1MzYxOTAxMTAwMA", + "digest": "sha256:d4d2bb6003d6b333a82d46828d38316e67f9bba1af1496d4ccdb0a51995c3bd7" + }, + { + "id": "a95c60a9-90fc-57c1-b744-3d8e819b03a9", + "version": "MTc1NjMyODE2NDUyMjk1NjAwMA", + "digest": "sha256:d44a75e73d39ed40b60e38d25294055d9dd48c47e87ac5f2293fffb0cd3c9dda" + }, + { + "id": "a97d80b8-1347-574b-9a80-d224cd352d3d", + "version": "MTc1NjMyODE1ODE3MjgyNTAwMA", + "digest": "sha256:8d51dbb0ed8b64d178d0b849b7b1bd657c42c6490e5917017510f322125823db" + }, + { + "id": "a991a507-e14e-50e4-b3cf-eb232be55cdd", + "version": "MTc1NjMyNzc0MjUyODA3NzAwMA", + "digest": "sha256:9614f9de791c00ef7db001007b8f350cbe0d358bfbfa47ae682d91f3f954a557" + }, + { + "id": "a9c49412-bb28-5eb5-bffd-347086e4b958", + "version": "MTc1NjMyNzc1Njg2NTMzNjAwMA", + "digest": "sha256:4d8081992e1e6e34b1ccbeec8197783765fc93a8efea0ed1c93c6f67e6a97873" + }, + { + "id": "a9da300b-b644-510a-baf5-6361d45e1351", + "version": "MTc1NjMyNzc2NjQ0NDg3MjAwMA", + "digest": "sha256:9d842ae83d47f0736f671696d4b857dfc168b2701c738a25fc808da7987249e2" + }, + { + "id": "a9e335bb-ab06-50a6-90ef-3141d7ee963a", + "version": "MTc1NjMyNzc5MDI4NTM2OTAwMA", + "digest": "sha256:1115215facaca5a6a5a1661a2befa9e359aa79edc603dbaed5df2fea51c18b60" + }, + { + "id": "a9ed66b0-5c86-5b13-9373-0a1b385e47cc", + "version": "MTc1NjMyNzkwODU2NTI2NzAwMA", + "digest": "sha256:b7f6b68701bf585b747fcfefc188cdb4f96d79c6eaf8bb7f373153494d6bfab1" + }, + { + "id": "aa1f11ed-745f-5788-9667-7142505536a3", + "version": "MTc1NjMyNzgxMTkwNTM1NDAwMA", + "digest": "sha256:90674d73eefd48853153dadf6d48e2e8b18bb6e66c6300ce321b1e93cef4441e" + }, + { + "id": "aa3cefe0-8356-565d-886d-1939a68f1c18", + "version": "MTc1NjMyNzkwNDY1MTEyODAwMA", + "digest": "sha256:6b144ed27aa2f8160c6726bca1261bb41519c993dd762ae87c8562d802c34226" + }, + { + "id": "aa3cf6c0-3a54-5c1c-8b64-6365bbcb4a7f", + "version": "MTc1NjMyNzgwMzU5OTc4NTAwMA", + "digest": "sha256:7ca2e306d43bfaaaa508cb2ff4ea10fc586f33242c017a2a74d8ed479e190e9d" + }, + { + "id": "aa591514-7844-5584-9f84-e08dbd0fc1ba", + "version": "MTc1NjMyODE2NzAwODE0OTAwMA", + "digest": "sha256:58e2ff64697e2fb4e91b5077223df574d33860d10d036aa334e2e69b737bc5e5" + }, + { + "id": "aa66820b-78b3-50c3-9904-42951e272ffa", + "version": "MTc1NjMyNzg0Nzg2MjMwMDAwMA", + "digest": "sha256:dabbb8cf100a664c7db4135f683e9663881db5c52f34f2c72f366894a7ea6c55" + }, + { + "id": "aa8403ba-2803-595e-a232-10f27c0b670f", + "version": "MTc1NjMyNzc0MjMxOTQ4ODAwMA", + "digest": "sha256:afbd1cba05a9ceafc1a94f83b4cbd1f03dc592cf213f4d12e722729a27e2b24f" + }, + { + "id": "aa87829d-8668-5147-8349-d5b4ed97b28c", + "version": "MTc1NjMyNzc5NTM2MjEzNjAwMA", + "digest": "sha256:94b7a9f551fd932956aaba7e5d6df4b6f96bcd0adb5211be46b40cb7b7a4cd07" + }, + { + "id": "aa9cbae1-8b60-526d-a6d6-0138b22400a0", + "version": "MTc1NjMyNzc1Njg1MzUwNTAwMA", + "digest": "sha256:7f0e4170deb8b693afe8b01044248d6daebdbf598190764ceeb38aacb1d567ed" + }, + { + "id": "aaa433e6-7254-58d9-b11a-6fd08103a8e1", + "version": "MTc1NjMyODE1NjM3MTc2MjAwMA", + "digest": "sha256:0edc9d29a1021fca73f6cec7f7671876095a007413442fa4618de6448006e3cb" + }, + { + "id": "aabcec4a-355f-5450-9926-55f15adb493d", + "version": "MTc1NjMyNzgxMzU4Mjg4NDAwMA", + "digest": "sha256:d821edf9beeec2a61703913016e9e7d088eb7e893cf8f5449d0729398ba379db" + }, + { + "id": "aac6831a-8451-53a1-a3fb-96340f44bcff", + "version": "MTc1NjMyNzc0OTk4MTcyNjAwMA", + "digest": "sha256:0b1ba356a148a5f52517dd1701b4c8b8b42c2d219c09bbf5fcd9d448cfdead83" + }, + { + "id": "aacc3acd-34a8-5f1c-9085-023cd1da2c63", + "version": "MTc1NjMyNzc0NTc4MjM1MjAwMA", + "digest": "sha256:1f67f5acb85283a8027d455e26e9f41e54c592815b470129ef2e7da2f6422589" + }, + { + "id": "aafbafea-5507-5188-b3d4-9d50a1f07cc1", + "version": "MTc1NjMyODE3NjgwOTI3MjAwMA", + "digest": "sha256:e4066aeee117af248c1491f2a7ab46fd54cd20aa6577bc5c7ab98b1d8cd8c124" + }, + { + "id": "ab19b1d6-ee06-521f-b7fe-1c37f0edd7b0", + "version": "MTc1NjMyNzc2NjQ0NDYwMDAwMA", + "digest": "sha256:5b3bfac8abd075ff8c8fb8892baa73fa01d56c5ea19c77c23e3d509e226200f3" + }, + { + "id": "ab1df31e-0ff0-5e58-8726-832dc5d0406f", + "version": "MTc1NjMyODE2NTg0MDY0MDAwMA", + "digest": "sha256:61ebad704c12ee8ace3e15edb46013544968b952f14cd5954e32a0d6fe7ed9f1" + }, + { + "id": "ab2aeb71-3cf8-5214-96f0-784e43010f9f", + "version": "MTc1NjMyNzc1NzQ5NDgwOTAwMA", + "digest": "sha256:88cc899ca790ea9129fadcec75981d5868ed55638d1be2f8cd481a55143562f5" + }, + { + "id": "ab2ee90b-9a6c-581c-921b-3afd232ea216", + "version": "MTc1NjMyNzc1Mzk2NTk3NzAwMA", + "digest": "sha256:690a57339eb513de367c8dbbb87085ee1a9e0cc00a3ad4a726e36390dc7fd018" + }, + { + "id": "ab5ce4db-0304-58da-87d3-ed7d685e9f9e", + "version": "MTc1NjMyODE3ODA0NDM2NjAwMA", + "digest": "sha256:eba35a53e580c8f9517867e09a762c463a9fc7c157589c66c31e6874008b6644" + }, + { + "id": "ab743129-c68e-5fd4-8774-2fc878012229", + "version": "MTc1NjMyODA3NDY2NTAwNDAwMA", + "digest": "sha256:c3fda630888bfbc43380c72a2081277c7062d4d48449f7ad17570276f9a3c478" + }, + { + "id": "ab7725f3-5364-5268-8f19-06221c6f5c6d", + "version": "MTc1NjMyNzg2NzY2OTIwOTAwMA", + "digest": "sha256:ff37b1a46e0a81ab0e5397ef6de3235030eb35d9347d7e72c41888c71231325b" + }, + { + "id": "aba5a122-6544-50bc-9c2b-837a6e63e433", + "version": "MTc1NjMyODA2ODExODMyMjAwMA", + "digest": "sha256:f67dc9a65630a3ddc16097d848d22f1e72a99d1b7dc2742fd6fac6a543cb2ba7" + }, + { + "id": "abadc2ba-0af8-5d8d-ab49-1645289a3cd5", + "version": "MTc1NjMyNzg1MjA4ODkxODAwMA", + "digest": "sha256:28ab034d466154daf72aface78a8784a15f6a7f754d42eb49d3bee3a29e7cb13" + }, + { + "id": "abb5c4fe-4c97-5350-9fdd-88173f7d4e60", + "version": "MTc1NjMyNzc3MDQ5MDgyNDAwMA", + "digest": "sha256:86a03ca3886a3c618f704b1725e1eabb43e641a11c400f579540d3214a55c386" + }, + { + "id": "abb5c74f-200a-5087-8f5c-7cd5009c4c23", + "version": "MTc1NjMyNzgxMjg3MDk3ODAwMA", + "digest": "sha256:c27fc948eb38de88201e8f9e0284fab2425c87577909a16e4f32b1dc760d8b70" + }, + { + "id": "abbf5225-58a3-5ab9-87ef-e4030784b8f7", + "version": "MTc1NjMyNzgyMjI5Nzc3NzAwMA", + "digest": "sha256:976431199f8810928d930f92702b1bbaf3ea7e74754dadec9139205db77cc719" + }, + { + "id": "abec4223-1c14-5bc7-8315-fe081d6101fa", + "version": "MTc1NjMyNzgwNDU3Mjg5NDAwMA", + "digest": "sha256:58834ea27c45778b597ae7228b86edd3d61cc4179591a2575f82181e9b821eff" + }, + { + "id": "ac16f3b9-10f8-5b07-9ff9-6133a006e435", + "version": "MTc1NjMyNzg1NDQ0ODA2NDAwMA", + "digest": "sha256:6cb00bfb2281c6212d4ade2040235bf50e1dc620d1921d7dab7b65f35307ad57" + }, + { + "id": "ac278282-95b4-5f9a-9aa6-9c49d1ce30e0", + "version": "MTc1NjMyNzc2Mzg4OTE2MzAwMA", + "digest": "sha256:6d938f343a140e495b7edc130c39b28b021de0cf6efe693cc7d5d508e99ce97b" + }, + { + "id": "ac2a6f31-8495-5113-9ccd-4fd95fb31329", + "version": "MTc1NjMyODE3NjE1NjAzMzAwMA", + "digest": "sha256:b5b3b28ba40cf399c77bd8cb7e1fbec0425cd03a283ccb3dc7acd225d315c5a9" + }, + { + "id": "ac46286e-e4e6-557b-a5b1-724572897807", + "version": "MTc1NjMyNzgxODc3NTQ3ODAwMA", + "digest": "sha256:b771d4fffd55ab1afb0ab749e4298b0cedaf229638ec7a860e7e0fffa8de08b5" + }, + { + "id": "ac5498b6-19ef-57d0-aa0c-ff84b504e2e3", + "version": "MTc1NjMyNzgzNTIwODg3NTAwMA", + "digest": "sha256:b2ae1ed0c68a62c164e1f9b99f65e589b0d9c4521826b28987a7afb3711d8c60" + }, + { + "id": "ac74bd3b-d08f-58e5-85ef-6dacd0783d8b", + "version": "MTc1NjMyNzgxNjM5NDQzMDAwMA", + "digest": "sha256:382aa9720038261a50a13e47370730313e5c652f0ca56e14fb8e8a91bd48a0cc" + }, + { + "id": "ac74fd6a-122d-5de5-8ad9-fc50575df539", + "version": "MTc1NjMyODA2NTE5MTkzODAwMA", + "digest": "sha256:6bd1b0edfaaf2a55b6823ccc64df2a61a62f6e2566927e28e2a4bcf417bc9642" + }, + { + "id": "ac87ddb6-0e2f-5bca-b077-964661c60a9b", + "version": "MTc1NjMyNzc4NjkzNTIyMDAwMA", + "digest": "sha256:8234e35bf137b2fbe47676972e1bf42c45f497e11823a652dd17f1f0ee0fbfc2" + }, + { + "id": "ac8ebe1c-fd0f-5641-93aa-7fd94f674140", + "version": "MTc1NjMyODE2NDkwMDk2NDAwMA", + "digest": "sha256:e7b7c901646503294ee7e0cc05672ff3bf14422594422abaea255ecc8a6e8b6f" + }, + { + "id": "ac93e2a0-85e3-5f1b-b78f-9e787c09abae", + "version": "MTc1NjMyNzg4Njc4MjI4NDAwMA", + "digest": "sha256:cf4722af908fc8f6c112c36ae158a0194136297ebf9885dca26a5229a6154cb0" + }, + { + "id": "acb10795-ee1d-5556-aeee-2b87462e205a", + "version": "MTc1NjMyNzg1MzM3NDgyODAwMA", + "digest": "sha256:026c126e0cff0d4e04f7d029291b6759ecf402c729a8a404f3234c43f827994b" + }, + { + "id": "acbf6253-c5f5-5904-9205-2a48b8b6fc79", + "version": "MTc1NjMyNzg2OTQ5NTU5NzAwMA", + "digest": "sha256:7058df2eb1edc20f5f5c7d1d1e08dbea8f077fd952b17fb474252f12e6f73156" + }, + { + "id": "acc1f9d5-f8ad-5d0a-a573-5c5bd9fe733d", + "version": "MTc1NjMyNzgzNjMxNTkzMzAwMA", + "digest": "sha256:198d5f5b9abf1c5763724f15e61707e7f9faacaf1b5762f94c2232acfc417315" + }, + { + "id": "acc3f1ed-2a5f-5b94-a7ee-b461bea5a66b", + "version": "MTc1NjMyNzc0NTA3NzMxODAwMA", + "digest": "sha256:564536b4ea2fee3205ecd352512dc61676f8286a15095b62c3ffe12c2eb68c19" + }, + { + "id": "acc98f5f-c0ee-5bf8-8a7a-a549a7fcb644", + "version": "MTc1NjMyNzc4MDE1MDc0NDAwMA", + "digest": "sha256:8098af7b4beba6e91fcc4bd95f896a378f7a582a972c2088b781ea12cf0f484c" + }, + { + "id": "accb818e-27b5-5a63-a98c-7f2f0c723448", + "version": "MTc1NjMyNzc3MjUwNDQ5NzAwMA", + "digest": "sha256:aa0df5ce96506ed684a5fdff072f2e1d5743565355b3ba79509bca892469a815" + }, + { + "id": "ace7e11c-f14f-5045-ae8b-50a1208fea11", + "version": "MTc1NjMyNzg3MDY3NjA5MTAwMA", + "digest": "sha256:40bda17e78ba30b05679a61eabfab583af28fc32562d150f0e3eb5a87169d35f" + }, + { + "id": "acf8690d-8726-5caf-8841-d156741f10f4", + "version": "MTc1NjMyODE2NjkwNTc1NTAwMA", + "digest": "sha256:a30cdff91016345ff90be99c2e9e61fb4d35efeffca024d8476271511f06ceb3" + }, + { + "id": "ad085061-f31d-53b5-b9a8-b54ad0688109", + "version": "MTc1NjMyNzc3MjYzMDIxOTAwMA", + "digest": "sha256:8a1dfe4476630992d63c9d18c0a2c5ada9babe05aa6749d7ba07935e32966e07" + }, + { + "id": "ad08511a-cd41-5d38-9738-5444ae5c2349", + "version": "MTc1NjMyNzg2OTUxOTgwNDAwMA", + "digest": "sha256:e42aeb9e1a3b366cdd13d4f6320a2ce0c04064ef7b6685bcdee93f52e4a93ced" + }, + { + "id": "ad0dd27e-d68a-5e8b-93ef-4a5fc95246b3", + "version": "MTc1NjMyNzgwNDU3OTU4MjAwMA", + "digest": "sha256:fcea12229ab76c189920cda7ccd491959b1ccebf3392edcaf8cc0a4fa80c2cc9" + }, + { + "id": "ad179aba-17c3-57c0-8f33-e51780fa6927", + "version": "MTc1NjMyNzc3NTI1NTI1NTAwMA", + "digest": "sha256:c488eaf65a357f5760d97b8b16d23ed0bfa85fbde1408566bda9136c7314fda3" + }, + { + "id": "ad25a881-97af-5457-ae6e-d041321048d7", + "version": "MTc1NjMyODE2Njk2MDEzMjAwMA", + "digest": "sha256:eec800f40d3e5b5ef69d05ec7942702ca5e907e402a185f0db8e1b7993d02a6d" + }, + { + "id": "ad2e4e2b-3a51-5fec-b832-176278d0e489", + "version": "MTc1NjMyODA3Njg4ODY2MDAwMA", + "digest": "sha256:35488ab3fdbd4aabc9900bde43a55924a4b99ad0b801b3babe675958c03db9ad" + }, + { + "id": "ad3078f3-e275-5890-ac04-bafb7c9a8f92", + "version": "MTc1NjMyNzg4MjA1NDUzNDAwMA", + "digest": "sha256:ec41619403a2a7fc887dfb0b0f943ecabb00cffe23cddf4a79719f1d95f87db1" + }, + { + "id": "ad3e1221-5747-5e7d-9da4-68c8f4d6fc9e", + "version": "MTc1NjMyNzg0NzkxODA4OTAwMA", + "digest": "sha256:bc835c9f0ba6c049aafe7977fa0914c9f701d7c59709cbf97979f2077b222ffe" + }, + { + "id": "ad54f3ce-671f-53a3-99e9-69fcbd9777e8", + "version": "MTc1NjMyNzc1NDA5MjA4MzAwMA", + "digest": "sha256:3b5ef82d032149318821c4719b51a65b48bee0320a7a82ef747985ff19d1d441" + }, + { + "id": "ad5a690a-b2f8-58e7-a557-f271ad7d73f6", + "version": "MTc1NjMyNzc0NTY3MTU5MDAwMA", + "digest": "sha256:5e1fc1fcb1017756633e5480837519fd1fa62caa85c8075c3140d7e6b058a6f7" + }, + { + "id": "ad64e3bf-92b5-53b7-90b5-3c366e19b5f1", + "version": "MTc1NjMyNzg0MTIzNjEyNTAwMA", + "digest": "sha256:2eb7b53d55298600251b346fd688cc6b82825470068f6ebbe61981f3c9df17a8" + }, + { + "id": "ad87cc76-725f-5f97-8f8b-ab45c2abc8c7", + "version": "MTc1NjMyNzg3OTkyNzg5MjAwMA", + "digest": "sha256:8b902a3c6a0839bb01aa63752361c41b57ecbbb3ff8cc1578f4c0d500e98b7a9" + }, + { + "id": "ada75f79-52bf-5f99-9ef8-5e1bff3ecfd5", + "version": "MTc1NjMyODE2Mjk5NDAxOTAwMA", + "digest": "sha256:d85793420c1bf04aadf74ed4072dcaa270dd257065255aba7f23d2a2a5f47707" + }, + { + "id": "adabff75-81ee-578b-bae0-351de014c729", + "version": "MTc1NjMyODE4MjA4ODgzNzAwMA", + "digest": "sha256:0d8dd21b75c6c5d633b9afe7962cd367c4b1812942d3d290a1c56c0caa290fa3" + }, + { + "id": "add4d1a9-b365-5be8-be62-b74f2bce4ae3", + "version": "MTc1NjMyNzg3NDU1MjA0ODAwMA", + "digest": "sha256:9ac971071d0a3d0c93559cedba4bd2c69dbb11760cdd116280e2096c1f8b1ebc" + }, + { + "id": "ade185db-3933-5ae7-96f0-f732427c40d9", + "version": "MTc1NjMyNzg0MzA2MjMxODAwMA", + "digest": "sha256:51750baa61952caf34cba1badc4378c5f4cfc94e3e8a742f9d135591fd1b0838" + }, + { + "id": "ae2d9646-1d46-5657-8eec-5b727283e08f", + "version": "MTc1NjMyNzc1MzYzNjkyODAwMA", + "digest": "sha256:bca704cd1b7e51400ff01fba9d6dcc9a85b379215e43428cfabad4546d884bff" + }, + { + "id": "ae3bafcb-20c4-51f5-9bef-d2867ff7c78e", + "version": "MTc1NjMyNzg1MzEwNjQwNzAwMA", + "digest": "sha256:b62a67b5365af0c2c07ac48efbcf3a9a2c4803e3fa12b115ab444adb6507bcf0" + }, + { + "id": "ae5c8e97-c48b-5502-9124-0a59abcf374b", + "version": "MTc1NjMyODE2NDU2MzAyNjAwMA", + "digest": "sha256:6208e25add897121490bcb9d2415a32eee92d127829a4f6ca73e26f0bcd7e790" + }, + { + "id": "ae741a22-3f0c-53d3-bb7e-d1efce7f479a", + "version": "MTc1NjMyNzc3MjUwNDIyNjAwMA", + "digest": "sha256:807189732581facf487b8308e40cf502829ee15ddf68a0d2fdcec52684dd92e3" + }, + { + "id": "ae792d4b-cab2-5a5a-af2d-4a69bb8934b3", + "version": "MTc1NjMyNzc0MjM1NzkxNDAwMA", + "digest": "sha256:7a959af77e7bc7d5665bbd0a4904bc3cc58722f21b78241e3aa59fc899661d17" + }, + { + "id": "ae9a25f0-928e-5dd8-ae5c-6c44362b4f92", + "version": "MTc1NjMyNzg3MTUxNTYxNjAwMA", + "digest": "sha256:5220d451d93cff33f4531733263cf1bafee3905d16006321dbae1103f51a4a38" + }, + { + "id": "aea7a9b2-9ce9-5213-bc8a-98ecbfab9809", + "version": "MTc1NjMyODA2NTEyNDY0NzAwMA", + "digest": "sha256:1cd91351bf14b279a919c0c0b3ddedd3a6d83996994a129e6b8fe8cd11ce48e5" + }, + { + "id": "aea92cea-a740-581c-b521-38f3194bfa6c", + "version": "MTc1NjMyNzc2Nzc0MjU3MjAwMA", + "digest": "sha256:9241845f7809cc57930b7ebfe003ac49fd507557ce29ebdc204b86c47385fb96" + }, + { + "id": "aeb4b5ea-79f9-5f60-b23b-6426e81aa037", + "version": "MTc1NjMyNzg3NDU1NDQzMDAwMA", + "digest": "sha256:a10a00ce0d45a0f8bd7289d6c2ebc3a372e6aa7ad452adb2672c1250b75c0af4" + }, + { + "id": "aefc8ff4-6d3c-54a1-a40a-b5c7724b13c5", + "version": "MTc1NjMyNzc4NzQxNjY1MzAwMA", + "digest": "sha256:1b097549ecfbca0a2215d76d90fbe4fe567981896dc58394c48ceb621e71883b" + }, + { + "id": "af19089e-e8c7-50d4-9a0c-aa0e760e5e0f", + "version": "MTc1NjMyNzg4Njc5NzI1NzAwMA", + "digest": "sha256:3164daeee448e31a4db66ada439c074a1452cda74acddb5177ef465854c49ed0" + }, + { + "id": "af209ea0-ec14-5e40-a08a-2817008cbc23", + "version": "MTc1NjMyNzg4NjcyNjc1OTAwMA", + "digest": "sha256:cba09a34c7315619f9d489b938a6404886cecd5ab9c2c9468776395f68986ce2" + }, + { + "id": "af216c2e-1ed7-59a9-9d86-648282fda7db", + "version": "MTc1NjMyODE2OTAzNjc4NjAwMA", + "digest": "sha256:40e2642526e58ed7d0021222b3a79bef401a731adbf5172ef4404f53d8dab567" + }, + { + "id": "af3a009a-ba42-5d3d-8555-be971640b432", + "version": "MTc1NjMyNzgwOTMxNDkxOTAwMA", + "digest": "sha256:c3aeb81c406e01fb10cdba9e639658ec6a6d164af403b2c9ec333a6a6600de6b" + }, + { + "id": "af49cb67-0c99-5da6-94e9-97df20bfbb3a", + "version": "MTc1NjMyODE3NzU0NDI5MTAwMA", + "digest": "sha256:15b9c97d9985a3c7a0df77b9e3dfd3ba1c850219a4d7b3920a4bb925da2b6ad7" + }, + { + "id": "af4d300d-44a7-5b06-8fff-f40fafb32dea", + "version": "MTc1NjMyNzc1NzUzNTMwNDAwMA", + "digest": "sha256:5e9550ef326a05354b5fd15e070051696a4c519483e77bb6b923f9bcb6f5f057" + }, + { + "id": "af4df7c6-7f67-5270-b1ff-1abd1e1390c3", + "version": "MTc1NjMyNzc0MjM3MzU3NDAwMA", + "digest": "sha256:86d1f7bd58a8a867a5df2af775db5bb8023c61b2e7f7a2b8abb478fd37b50822" + }, + { + "id": "af5a9923-eb39-561f-932f-91c3f89d7511", + "version": "MTc1NjMyNzc3MDQ2MDE4MjAwMA", + "digest": "sha256:8a48024da6577974256d055c75a409ed7bbf01a55ac82f9040a961cd871956b5" + }, + { + "id": "af6bedf5-0671-59bb-a2d3-e67a70e5d58b", + "version": "MTc1NjMyNzc3MDYzNDEwNDAwMA", + "digest": "sha256:d9b9c5b14ee89fefd2f563bafb5e25d5af2ddc96f6f79f0f8077b58232ef1e76" + }, + { + "id": "af715c1a-b406-5f01-aeb1-5ba7a31f8420", + "version": "MTc1NjMyNzc2MjM3Nzc1MzAwMA", + "digest": "sha256:2da577a9b0a32557566563c513a8ab6da55b0cd70286c2a44a5a4dc27e09687c" + }, + { + "id": "af91508d-dece-5db5-8572-978f6eeae13a", + "version": "MTc1NjMyODE4MTg2ODEzMjAwMA", + "digest": "sha256:ffff50f12e06f5f788a4fa24bc4964f5fb94f326c621e7da9cb1d976a1bb6111" + }, + { + "id": "af972668-3f88-560b-b89d-6f98b5e377b8", + "version": "MTc1NjMyNzc1NzM3NDAyOTAwMA", + "digest": "sha256:e8a77b2eb33aa4dc1bdf1e7a7e56f5015e9041e25ed9debea11670dc79e49018" + }, + { + "id": "afa5b542-442e-5e10-b0b8-b96f227a16c4", + "version": "MTc1NjMyNzc0MjM3MzExODAwMA", + "digest": "sha256:10784e268067ae70b380941b06635da888f2ac5dcb575691ac004f0735f4fb47" + }, + { + "id": "afb47c63-52e9-5c8c-a3c4-7b23ed474541", + "version": "MTc1NjMyNzg3NjYxNDc5MTAwMA", + "digest": "sha256:54766d6ff7d61893212f79c20b4e6e19feff8526b6fa8401e35ccd39d6f443f5" + }, + { + "id": "afb727af-fd2f-5904-9ff5-76285f5484d5", + "version": "MTc1NjMyNzgyMjI5NjYxMjAwMA", + "digest": "sha256:ad6d623efac5add823fb1237ead1c11a0251f1b506d837f404ad11693d103248" + }, + { + "id": "afd93d50-d677-5dbe-b4e0-20ecfba66c9b", + "version": "MTc1NjMyNzc5NTM3ODk3MDAwMA", + "digest": "sha256:4dd07278bfbf05143bf1425e34f518aa8ffac76b21785cd54a6cd34e5dd9e504" + }, + { + "id": "afe8d9d2-3d54-5a8e-b199-f266a3808da6", + "version": "MTc1NjMyNzc4NjkzNTAwNTAwMA", + "digest": "sha256:f5a811722dfa34587af9451fbe4a43e8cea9a706834bf176f09c563d2a955a7e" + }, + { + "id": "afff04b5-18ee-54ac-9b6a-ce78ac6ee728", + "version": "MTc1NjMyNzkxMjgzMjUwOTAwMA", + "digest": "sha256:78b612fe16ffbd0a132a586272533328c573bfb220682ed4da8eae5c0a2e25d3" + }, + { + "id": "b0005bf9-918b-5857-ad4f-48b87dc68d75", + "version": "MTc1NjMyNzgzNjI1NDEyMTAwMA", + "digest": "sha256:5fbf5abf90aded5ce90c1fbcf9ee926febd57795aaddc54cee912c7b47067dff" + }, + { + "id": "b022404f-6956-5532-829b-f665f59f6631", + "version": "MTc1NjMyODE2NDkxMjEyNDAwMA", + "digest": "sha256:89d457de898ceeb0b07eb06efae9cd189da521a2546430e1afcacd560f2f9b6a" + }, + { + "id": "b0268dbb-cebf-5148-9f16-2b9976ecbc29", + "version": "MTc1NjMyODE3MTE5MzcxNzAwMA", + "digest": "sha256:a8566aa80e1efda91ad8c5f46eb92e17f7f7b601ca44cea8cecd1165741c1f77" + }, + { + "id": "b027ba87-94ea-52a7-9878-8fd4330ac3de", + "version": "MTc1NjMyNzc3OTc0MjQ3NjAwMA", + "digest": "sha256:02d740c57cc786394b8f06f76d079d31d578a52b24f988d218cd3395d3859326" + }, + { + "id": "b0302d9d-9b5f-52c8-b4bb-57af39e745f9", + "version": "MTc1NjMyNzgxODkzODE4MTAwMA", + "digest": "sha256:7c89f92c418efaab458170ec7db1ea4ba8c9a6a332f7a1d2c9a5a91935616f19" + }, + { + "id": "b0346e4d-d33b-57e9-87d1-109ba90fe7e8", + "version": "MTc1NjMyODA2NzYwMzQ5MzAwMA", + "digest": "sha256:da886d35204b3bd9552adef895866d389d64f6058c6e3fa6ffd33244b3e17cdd" + }, + { + "id": "b0370eb8-c37c-5c0b-bcdf-2c63ca3ea34b", + "version": "MTc1NjMyODE2MzQyOTM2MzAwMA", + "digest": "sha256:46001c633e8ff15ef63fc6aa4bff00e5719368c7a571f3292fbcdaf7420ea7c7" + }, + { + "id": "b044c2bd-28c4-5e4c-9512-f5b7df474638", + "version": "MTc1NjMyNzgwNDU3OTYyODAwMA", + "digest": "sha256:b7653fc89e6ee29958127fa3b0ed9c5dd8e1b656ae2693aad07ed52e47379a6b" + }, + { + "id": "b0484521-44b8-527e-88fc-2e99447ef9b4", + "version": "MTc1NjMyNzg2OTQ0MTM1MTAwMA", + "digest": "sha256:b5070d54e179da6b30f80648628f336b83f8cdc8842ec0186c922eba36c3b6e7" + }, + { + "id": "b0604316-7201-504c-96e7-d5d565e3d8cb", + "version": "MTc1NjMyODE4MjE2MDg3NzAwMA", + "digest": "sha256:0b88c7fdf11c803245d323ef3f2e561fa40c2119dd82f8ec8d4a7b88832d514f" + }, + { + "id": "b06a0592-c868-5eac-93cc-3440dfe446a2", + "version": "MTc1NjMyODI1OTI2NTk3MjAwMA", + "digest": "sha256:4addb1e13d66f498cc98db6b90117d20afcc28186f8e2b2a8a3ceb4548aeab73" + }, + { + "id": "b08e5e66-35ad-5c8b-9c0b-36234b131d4d", + "version": "MTc1NjMyNzg1MzI1OTI3MDAwMA", + "digest": "sha256:7828d5edf142f0160e8c45c8461f8e09fe2f16d1a23c438c20b486e22a2661f7" + }, + { + "id": "b094eda6-e1b3-58a6-a922-42a0ccb4cae5", + "version": "MTc1NjMyNzc5NTE1NjU5NzAwMA", + "digest": "sha256:3d548c44466d611ce4a4bddf58522b3bc93110c412034b651175914afb94d53a" + }, + { + "id": "b098bb8d-536b-5291-a540-75f2611693bc", + "version": "MTc1NjMyODE2Njk2OTMxNDAwMA", + "digest": "sha256:26702fd938dcefe23278b195ad7a17f53daae88c0a928e2d7d216d46538ba1e1" + }, + { + "id": "b0a897ff-91b4-5a8e-91de-c86e7199fd05", + "version": "MTc1NjMyODE1ODE3NDk1NjAwMA", + "digest": "sha256:9eb80c62c6248db354defa293328e2fb536990155c720a19d0ec3dfe7dfb2af0" + }, + { + "id": "b0cf7181-ab66-5242-b37c-081152de5ed9", + "version": "MTc1NjMyNzg4NjczODIzODAwMA", + "digest": "sha256:0e3424ba09e522205ef116d99cb3c07175a42897f66707d33243ba1818b344ef" + }, + { + "id": "b12ed3be-fb91-5288-8faa-46698378bf83", + "version": "MTc1NjMyODE3ODAxMzc3NzAwMA", + "digest": "sha256:5c7bf8f9629451d680100f00e51f35ac3bb536deca260b3f62f38f4533a52ae2" + }, + { + "id": "b13397fb-acbd-50fb-898c-f4ac34541da6", + "version": "MTc1NjMyNzc0NDQzMjgzNjAwMA", + "digest": "sha256:60a1b32eec04a6c6200891245d33e33c13fc326a79936f512558a20b12b1ba95" + }, + { + "id": "b13fed22-691d-5337-a067-b30cfaf8de05", + "version": "MTc1NjMyNzc3Mjk5MzA5MTAwMA", + "digest": "sha256:63181e68e665f8b734f5a0882d805499c3e2a9666443a0798431ce1db37a6d37" + }, + { + "id": "b151a78c-8bdd-5681-8cbb-b489e4212801", + "version": "MTc1NjMyODE4MDQ0MDU0NDAwMA", + "digest": "sha256:f4e11e2c0f02d3a9bb7d8d26ad14c363d85d0fe5c87710dcc4adfb964a9dc5cb" + }, + { + "id": "b154b7d8-41cd-5462-8fe7-3c888355e950", + "version": "MTc1NjMyNzg3NTQ0NDE1MDAwMA", + "digest": "sha256:35ede97e357b8f74789c3b21199a02cfa0d98f29d3d27b9f97b3475eeb8d79e5" + }, + { + "id": "b1830d3b-5014-5e0a-b8c6-c9a7d040386b", + "version": "MTc1NjMyNzg3MjAwNjA0MzAwMA", + "digest": "sha256:57db4b6f600384e900fe344bcea6c3a7babadb7554c4f0553ef502ef3728d854" + }, + { + "id": "b1921740-ffb6-5e84-97f7-5118424a1ec5", + "version": "MTc1NjMyNzc1NTk2NTY3ODAwMA", + "digest": "sha256:e197ffea89443c6d658a697df24f8b72d62b13ded160c6fb37f2fe552e568138" + }, + { + "id": "b1959109-e307-5ad9-a30a-f32c3ba8cbe0", + "version": "MTc1NjMyNzc4Njk5ODYzMjAwMA", + "digest": "sha256:3f800349498710f857a1e3c545693733e43036893a60fb467fc0e56aa2f63463" + }, + { + "id": "b19982f9-08d0-5d0d-b680-f5fb19b03b60", + "version": "MTc1NjMyNzc1NzU4NzUwMDAwMA", + "digest": "sha256:582f7af2b780f8cd0473f09fccbe80352847444cb69c483b635d881454306090" + }, + { + "id": "b19e37aa-f894-562c-a1e5-dd0de1178808", + "version": "MTc1NjMyNzc4MjgwOTAyMDAwMA", + "digest": "sha256:4b2efe488ed63c8e2910b0aced3c99f93cea2d296c6511dfed0154a0961df3e5" + }, + { + "id": "b1a187dd-64ee-505f-b0c4-a0215947f1b5", + "version": "MTc1NjMyNzg3MTYyNjQxOTAwMA", + "digest": "sha256:d3b2db6d71444573d7fcefd6a9891f1cd4bf55bb239d72a09f5540c63c6785a4" + }, + { + "id": "b1b792ce-ce3d-5233-a806-d5ae09a0b3d4", + "version": "MTc1NjMyNzgzODkzODkzODAwMA", + "digest": "sha256:f17698abdbe3de51738d460ce4766332b1ff0666cb984acecce7261ff3b4b4c5" + }, + { + "id": "b1d40b15-d480-57eb-bc9b-835290c0f64c", + "version": "MTc1NjMyODE2MzAzNzA5MTAwMA", + "digest": "sha256:c261b249cd8c1d845f7b43ef4c4cdae184ef9bf74692a70e4857d20059237e5e" + }, + { + "id": "b2003b77-79fe-5c1f-94c9-5f391fd80c17", + "version": "MTc1NjMyNzg3NjY3NDk0NjAwMA", + "digest": "sha256:7b2b06dc2bdfa055be92635b3ab6628c34eb7ce881847097a8ddf4983ccf066a" + }, + { + "id": "b2055702-1ddd-5e04-8dc5-8498c47273f2", + "version": "MTc1NjMyNzg2NjY5MzcwNTAwMA", + "digest": "sha256:9153ec03572e4c74f3e52022101462e6a0feb7e689c626edf06de1597133eb10" + }, + { + "id": "b229af37-8c29-50f9-818f-31cad8b613d6", + "version": "MTc1NjMyNzg2NjY5MjgwODAwMA", + "digest": "sha256:e08c740d3481851572e3f7a358527fadd7d0c3598ff356ea315cfdb3e70204b6" + }, + { + "id": "b237dffc-e4a9-5caa-908b-1805d3fca674", + "version": "MTc1NjMyNzg1MjgzMDIxMzAwMA", + "digest": "sha256:f968aa2c854107acd8b34661700243740b82f396c0977f16116385892419e6a4" + }, + { + "id": "b24202f5-b4cc-58d0-b8b3-d5357b0ef8ee", + "version": "MTc1NjMyODA2OTU2NjY2NDAwMA", + "digest": "sha256:d7327aefc1c3646e6802e59632d98900c516bf950259ab6f6b48034d808dd34e" + }, + { + "id": "b2447759-d636-579d-a7e8-f915c50631a9", + "version": "MTc1NjMyNzc2MjM4NTc5NzAwMA", + "digest": "sha256:cf1ece174972e95ab071b06f1ca6646ba2bfc736851b51b34263bf4663832754" + }, + { + "id": "b2520865-6f8b-588a-bfdb-12c648a3ab87", + "version": "MTc1NjMyODE3MTE3MDI1MzAwMA", + "digest": "sha256:94f78ceb1d23791cf969a1191b7bb327b559ea6b62897c91958a728ed6da695e" + }, + { + "id": "b25db4c2-d91d-57be-8394-0ad6bbfb5a76", + "version": "MTc1NjMyODA3NzI2MTE4MjAwMA", + "digest": "sha256:76fe4a659d24c61982a39e1ccdbb9c221245734f8006a9baa30735210e21ca94" + }, + { + "id": "b26f50eb-d413-544d-b7c1-615e0eaf1945", + "version": "MTc1NjMyNzc1Njc4MjYzMjAwMA", + "digest": "sha256:d604680dacfadc9ca6fc3a41fd7db769fa8d12dc137adebf2e40c97cf3da4d64" + }, + { + "id": "b29bacea-7259-50ea-b09e-1907bea55675", + "version": "MTc1NjMyNzg4MDM2NTk2MDAwMA", + "digest": "sha256:44f971d06f66dba2379f4991643b59d4f2ddbb45291a64c6ebb77e1f6b5587f0" + }, + { + "id": "b2a45551-9bbd-53cd-a0d7-76ba44fc047f", + "version": "MTc1NjMyNzgxMjczODI5MjAwMA", + "digest": "sha256:0e1f9267db29d83426c23ad7b4d3e0cfbf6c9d4fb5fc3c78ff5c8572cc7f6673" + }, + { + "id": "b2ae1cf8-387d-5532-8401-5615f58a0acc", + "version": "MTc1NjMyODE4MDM5ODg4NDAwMA", + "digest": "sha256:608083abeba5b3d0a99babf053dc3602791ba785ec07e83970d74ee2db5a1e99" + }, + { + "id": "b2c51d80-3e72-54ec-bcaa-56997534d1b6", + "version": "MTc1NjMyNzkwODUyODU3ODAwMA", + "digest": "sha256:c25939376227bb6aa84bff642315d9bb4d89a767004581b42162156a16873397" + }, + { + "id": "b2cf3256-fb70-5441-abb6-de6ac39c0849", + "version": "MTc1NjMyNzc3MDI4MzIwMjAwMA", + "digest": "sha256:9766b9bbb08d14242be82c607b2d099e8dc07a152dd7db065ac847e597fa761b" + }, + { + "id": "b2d871b8-0869-5617-890a-c8c041c115e1", + "version": "MTc1NjMyODE4MjE2NTkxODAwMA", + "digest": "sha256:49f3ba0fe3911b0c4b66b60ef380151199506b07dbeac80ff1a3b65928a0a35a" + }, + { + "id": "b2ed499d-b220-5c1b-a0a7-d93ba1889edb", + "version": "MTc1NjMyNzczODQ5NzcxNDAwMA", + "digest": "sha256:ed815acd82af3f4e25eca8e03d13d51f61312ae5a898e5cc74a711fcc16ca92a" + }, + { + "id": "b2ef0bde-d9e1-58a4-a167-0daedf41d571", + "version": "MTc1NjMyNzc4MTk3Mjg5NjAwMA", + "digest": "sha256:03e9641394b2b7719ea4f469f55fe4c510f49a3d9b841aca3b87a6f016cbff7a" + }, + { + "id": "b2f03718-e1b1-5300-bdce-0626e6315e6c", + "version": "MTc1NjMyODE2MzM4Njk4ODAwMA", + "digest": "sha256:27d54e8c12dbaa81c6cd34e84d8e6f4a511a58dab437acc1d674bb4b131a81b2" + }, + { + "id": "b30213c2-964a-55a2-9a6a-cb6f96a058eb", + "version": "MTc1NjMyNzg2OTQ5MTAzNDAwMA", + "digest": "sha256:2a1ce0bb76494248a2490ca3dce2b0dc034108248308e264ea12006596fa14d9" + }, + { + "id": "b3088140-7433-5e66-8487-d3c28c6e76e8", + "version": "MTc1NjMyNzgxMzY0Njg5MjAwMA", + "digest": "sha256:738361ec8695b772950860e815dc4f7f593654c41bd30fda181ea5bcedd5d2ee" + }, + { + "id": "b330c64d-4d15-5ed6-83b4-887771c465c0", + "version": "MTc1NjMyNzc5NTMyOTYwMDAwMA", + "digest": "sha256:0ee438e76358367b44cb843683379d841963ae62cc41f72f5e9c40dd3f69a670" + }, + { + "id": "b3483c57-ba70-5571-8d03-661722d1739e", + "version": "MTc1NjMyNzg3NTQ0Mzk2MTAwMA", + "digest": "sha256:4c6889af65fc66fe8a14fe62b872efd6ccdade445a7e0466bf52565ba63f34b7" + }, + { + "id": "b35620f1-27a3-52a6-94a5-3a0b5e7c2ab6", + "version": "MTc1NjMyNzc1Mzc1NTc2MzAwMA", + "digest": "sha256:1417b04c6603a224f40b001bed9ad530be44ff02b45c4cb954dac3258d608210" + }, + { + "id": "b3562818-f83d-584e-95b6-08b40174f2a0", + "version": "MTc1NjMyNzg2MDI1MTIxMzAwMA", + "digest": "sha256:d85e3103242029f2d6ae2b2b91c265f2f0f3ef224c3bb863e001e986088c1f02" + }, + { + "id": "b35a9809-d33b-5fd6-b4f9-ade07eb8f221", + "version": "MTc1NjMyNzc3MDUxMDU5MzAwMA", + "digest": "sha256:8726959036f88b89db73da2aa94dccbc6edf031a4000f8a6ac9b1066e9c49704" + }, + { + "id": "b36d8e6e-44c2-5f2a-af7c-52cbf9f9d398", + "version": "MTc1NjMyODE3Njg5ODkwMjAwMA", + "digest": "sha256:fe66dd47fc64cf3e77b87d91646e94ff05f20877689822f95950e787204b70be" + }, + { + "id": "b36e4641-ba00-576f-b82a-5383e892df4e", + "version": "MTc1NjMyNzc3MzAyMzk1MzAwMA", + "digest": "sha256:6dac5555ca32e5da66727c21aa43a52e268a356fd3ae57be8d7e50a3fa3d5e0f" + }, + { + "id": "b3833737-fdd8-52e4-b9a9-45e8cfed140f", + "version": "MTc1NjMyNzg1MzI2MDA0NDAwMA", + "digest": "sha256:f098241d7371eb4038f2415b4b88045fb016907668bcf56d2f2c248c15fe546f" + }, + { + "id": "b3964ac0-102e-5c70-9fc1-3d3f0fabee0f", + "version": "MTc1NjMyODA2OTc0NjI5OTAwMA", + "digest": "sha256:89992bd7cd8df57a295d7e858270144db75ab74b810aa730c54f2c5e3356e52a" + }, + { + "id": "b3985f59-feca-57a8-8068-54db0140e29b", + "version": "MTc1NjMyNzg1MzIxMDU0OTAwMA", + "digest": "sha256:e154f4b8a80d0f9a6a364222ad16bef673c2969902f40a189ac531ef1505ebb7" + }, + { + "id": "b39f5916-36d7-51d7-bd5d-7ab848776f47", + "version": "MTc1NjMyODE2MzM4NDkwMDAwMA", + "digest": "sha256:d6878f4c85557622323563c3186912035587f7f2f61f3500014e6e5878ef7e05" + }, + { + "id": "b3a8ded3-18a8-5937-af08-57b2230701e7", + "version": "MTc1NjMyODE2MzM5ODM3MjAwMA", + "digest": "sha256:513cbbb05011d4fba6a31b121d4722b3f2a0ffe6c5936c2158f1542b939af39b" + }, + { + "id": "b3c12eab-d4d7-5aa8-846c-c7cd2908d922", + "version": "MTc1NjMyNzg4NzEzMzA0MjAwMA", + "digest": "sha256:8f4bb26125162d8a3a5e60f69e5ffc266ff2620c03f5f0279df8da658a0b4978" + }, + { + "id": "b3cfcd2b-8c16-5cfc-89f3-a30cf4e3e2da", + "version": "MTc1NjMyODA2NTgwMjQwMzAwMA", + "digest": "sha256:c371e78b74faecbb8748e5cd86a9b2b37953fbc5f73ed0bf929268e4e2087848" + }, + { + "id": "b3e6a409-22e7-5244-9e64-e17cf0257eb9", + "version": "MTc1NjMyNzgwOTI2MzU1MjAwMA", + "digest": "sha256:541b850a88037b1a256906aec2f51c7fd447ef2232cffff2e9151787bc5a49f7" + }, + { + "id": "b40ba07d-ea15-5af1-b8d4-b7ce3f175ae5", + "version": "MTc1NjMyNzc0OTkxNzc0OTAwMA", + "digest": "sha256:c473946f53165ec4b221b34f38ff51c1961a2fba7478e307a0ede5d5bc3bebba" + }, + { + "id": "b42dced2-f1d2-516b-a33d-e90641522efd", + "version": "MTc1NjMyNzg2NjI0NzU4NjAwMA", + "digest": "sha256:6fe0bbfc853c8d5b35691e2db1b608020668471fb622ae3d3a3d16e0c4db35fa" + }, + { + "id": "b43fbd94-59fa-5f55-87ff-3fae2d2abbce", + "version": "MTc1NjMyODE2OTAyNzA0NDAwMA", + "digest": "sha256:26198bd5ca38cff5b5b59b34dadf7efd198efcc264eb08fd31d42deb647b11f9" + }, + { + "id": "b441e873-2a28-570b-8afc-9fbb3f48a054", + "version": "MTc1NjMyNzg4Njc3MTM3MjAwMA", + "digest": "sha256:eac2825f7b06a347b413e3a2f865ae67108149b366952dee792ec8c86afe2d53" + }, + { + "id": "b44c72e5-32d7-5b21-96c4-a84cd59d4f0d", + "version": "MTc1NjMyODE3NzYwMjc4MDAwMA", + "digest": "sha256:d0b95284447cee15c71ab9ec30cd37c9c8baab9c246df94db7c063d90387bd43" + }, + { + "id": "b450b3ba-17fc-5a3e-a052-c71377895b78", + "version": "MTc1NjMyNzg2OTQ5NTQ0NjAwMA", + "digest": "sha256:0a32c4da586d251eaf97b14b39dcfe3664c2e46c432946c1b520e6c10736c8d2" + }, + { + "id": "b4514398-ce5d-5306-b7d7-e9276be4d724", + "version": "MTc1NjMyODE2ODk2NjgxNjAwMA", + "digest": "sha256:ab7910b14b740b5c05e1b4db91ec2a328cf1017a6952e51a4c5643c707a10d37" + }, + { + "id": "b47af909-9696-5b25-b10f-d030ad2bf5c2", + "version": "MTc1NjMyNzg3NjU4Mzg3ODAwMA", + "digest": "sha256:11bd0929a0ed9f1e0536259cd688079793db9dbeb12ce1140ab1787ccf88187d" + }, + { + "id": "b48b5292-f88e-5d6d-941b-16d1c43800d3", + "version": "MTc1NjMyNzg2MDI1OTM0OTAwMA", + "digest": "sha256:2f6573c5152b0035b34ca9dda43f8e7c7ec1dc72237be1cbf614d6d583125d2a" + }, + { + "id": "b49bf9a6-e35a-52d4-8744-db44f8655f0b", + "version": "MTc1NjMyNzc3OTkzNDA5NjAwMA", + "digest": "sha256:a10d195920a349950fb0f05e7a8a7a6e675788f51ff738693004b0914e6106a9" + }, + { + "id": "b4a7a518-f54a-5dba-bb7b-05f19b5e148b", + "version": "MTc1NjMyNzkwNDY4NDk4ODAwMA", + "digest": "sha256:ea714e4034a3c12f2da96d2e3ed2661907fc6e893b9b8104f6b98c1f1cd654c5" + }, + { + "id": "b4d26814-4ef0-53b8-b0d4-9f801e7c82bd", + "version": "MTc1NjMyODA2Njc4NjAwMzAwMA", + "digest": "sha256:03ae7417521082cd55241e1a64ca88c4ad26b26a72d21a2b0186826d41887410" + }, + { + "id": "b4d3ee74-a47c-5019-883b-cf17fa1636a7", + "version": "MTc1NjMyODA3NTg3NTA4ODAwMA", + "digest": "sha256:360055944501d8651d6219c6fcd29387b8733a5c38c805ee1d3d7ca19dac7669" + }, + { + "id": "b4f6d925-ee24-53e2-9e6e-53d8308ab9d0", + "version": "MTc1NjMyNzc0OTkxMjI2ODAwMA", + "digest": "sha256:91382dd809c95dc88222edf256b3f142d8e833a58afa98efaa79d0fccab633b0" + }, + { + "id": "b4f7c500-10c9-5809-8777-1ce203a1c9f5", + "version": "MTc1NjMyODE3ODEwOTM1NjAwMA", + "digest": "sha256:79666fb7af0e90beecaab25cf583e4502d1ce4533a03e914c8f9686b8d1b0fee" + }, + { + "id": "b4fe43da-521e-5e5c-93b7-c300cb1553e1", + "version": "MTc1NjMyNzgwOTIyNzUxODAwMA", + "digest": "sha256:f645880f79dd182dfb601dc73dd022f167fb841eb37a28f7dd6db8b41d591097" + }, + { + "id": "b4ff8a63-be33-5e0e-a41f-5582b6d8bfe2", + "version": "MTc1NjMyODE2NzAzNTcwODAwMA", + "digest": "sha256:bcaa1c06e6e123b5a76618cccd5f6b02e49d40c6fba46af58c5af1fde5b27aa4" + }, + { + "id": "b5189989-8045-52d5-ac70-75e622e15863", + "version": "MTc1NjMyNzczODM4Njc0NTAwMA", + "digest": "sha256:033b2051f9b0ce30c6bfa502fb69d16a355d78dd7768fcd0c2443f66b7d6a4da" + }, + { + "id": "b51c3c8b-0dcd-5cfb-b34c-d6c8704f3f7b", + "version": "MTc1NjMyODE2MzM2MTQxMjAwMA", + "digest": "sha256:f583f60b5aa581829b0c73c02c99d978f93e4255f51a25748b89a279d98be91f" + }, + { + "id": "b54da19a-01b7-5212-8fde-4f4bdc59bf98", + "version": "MTc1NjMyNzc4NzM1Nzk3NTAwMA", + "digest": "sha256:28e148f8f82f617ee3d1792e1046ce9d6e11f9403aa18b64db298e2293425072" + }, + { + "id": "b550fa4a-859d-5f08-ace6-0e4f28cfc216", + "version": "MTc1NjMyODA3MzQ2MjkwNzAwMA", + "digest": "sha256:52b227da05783a839ae871ceb60250c8ca16eaadd8e062cf158a146ff35255ea" + }, + { + "id": "b5994d9e-f8f7-505e-984c-7f3104c29a26", + "version": "MTc1NjMyODE4MTkxMTY4OTAwMA", + "digest": "sha256:7069aa0a725ecbfbdff5231536f31f2da35014995e6a3f3a96b55aa8cb32d1f8" + }, + { + "id": "b5b94f22-3c32-5217-b75b-a248d686f805", + "version": "MTc1NjMyNzg4MDAyMzg4MDAwMA", + "digest": "sha256:2fdb00e543d1da50221d56113113539bfaecab849dfa7ee154efa184aa58a1e2" + }, + { + "id": "b5f590b5-e73f-5a9c-82f2-49a1b416833f", + "version": "MTc1NjMyNzg1NTc5NzM1MDAwMA", + "digest": "sha256:1a1884f0775233440090ad05b30591f5e5ab851550b8e551ada9816da5c4ac19" + }, + { + "id": "b5fa816e-d29a-55eb-83a8-7044a6a6c30b", + "version": "MTc1NjMyNzc4NzQ4NjM2MzAwMA", + "digest": "sha256:950d1ef5f370fee2ec11b11ea544adef109cf4093cecdbe54eca253dcc1df3b4" + }, + { + "id": "b5fe6646-a75e-52a0-a233-abdc1296a98f", + "version": "MTc1NjMyNzgxNTY1MTAzODAwMA", + "digest": "sha256:5d0e3cc9db2ce354a5963d55784d38f26a69415e29b4b4c07a58a8a8cf079dd6" + }, + { + "id": "b608e4db-b0fa-518f-9177-45291bb2c414", + "version": "MTc1NjMyNzgyNjU3NjUzODAwMA", + "digest": "sha256:928ed8b4e0abc5164851eb5d59d3f8219f7e7d6decfb274f07d7a493a64341ea" + }, + { + "id": "b6161d7f-727c-5e88-8ef4-bd88cdcbfcc1", + "version": "MTc1NjMyNzg1NTYzNjEwNTAwMA", + "digest": "sha256:a7d96b2d35f5597ca736eb4a7b47053cfdb24cffbdaff0ab638cadd93f5a948b" + }, + { + "id": "b61a6084-ffb6-5dcc-a2aa-d82dffec58c9", + "version": "MTc1NjMyODA3NjkxNjgzNjAwMA", + "digest": "sha256:750647df534477c1b3458e7627bef43f9f231edd00e13ebd8fc0af858f8aa7fe" + }, + { + "id": "b6225a16-0bea-53ce-bc92-99250f48bf49", + "version": "MTc1NjMyNzgyNjM4NTQ2NDAwMA", + "digest": "sha256:2b698a05ea3019b5330edbe61b41c4e642524515feca07575f44cb424cd40477" + }, + { + "id": "b627668c-8f34-5dc1-91aa-b28d4c941642", + "version": "MTc1NjMyNzgwOTI3Mjc1NDAwMA", + "digest": "sha256:04cfd557e02e219d44c06c6a48273c00b7a55c05068f8c2fc42f2fbd77daf8e2" + }, + { + "id": "b627e6d3-9896-583d-9d11-19021a8a4a10", + "version": "MTc1NjMyNzc4MTk0MzE5MDAwMA", + "digest": "sha256:7cb432b88ad7f3e5ea56e82708aca2c5736d171eaacb6a65d7ca8d533ea6e3f7" + }, + { + "id": "b63433de-0d19-5548-bdbc-fe2ad9fad158", + "version": "MTc1NjMyNzc0MjQwNDg4NTAwMA", + "digest": "sha256:584575d2effc688b0d9de65500d869d97e7d31f475ee913898e33b17c62c0b58" + }, + { + "id": "b63546ec-7a6e-5eee-b795-f32322a9edd1", + "version": "MTc1NjMyODA3NDY2NTcyNTAwMA", + "digest": "sha256:1f2d409e1cdb5ad059d5364d1acd14ec16141edae6b8ebdb4084c274a797b447" + }, + { + "id": "b63e7b37-9edc-5a77-be82-3191cf8f9706", + "version": "MTc1NjMyNzg3OTk0OTQzMjAwMA", + "digest": "sha256:f77861976393d6d67af3b7d271be9f97d14d21c261f4580062b6591871fe3855" + }, + { + "id": "b647bdc2-60b0-5235-87b8-4c99239d2880", + "version": "MTc1NjMyNzgzNTIwOTM1MjAwMA", + "digest": "sha256:952eda42267e02ec42840e25d83119edb15bea015aebcb5c1b6eb649839afc43" + }, + { + "id": "b64cd8a7-4e08-500a-9bd5-95c82678ed69", + "version": "MTc1NjMyODE3ODA0ODgwNDAwMA", + "digest": "sha256:c4cd4a248accf5248f58b95bb95f4591be084806816c0edb2c6151e52563b0ec" + }, + { + "id": "b65504d2-9f0c-5431-8c9c-613ad2e3e3fc", + "version": "MTc1NjMyNzgxNjQ0ODQyODAwMA", + "digest": "sha256:b6ab600489f928f2a728583a7506686bff4b852d9cb5a7a83326fa6890240285" + }, + { + "id": "b66cd3ca-8c20-5b40-aa53-956581c7daf9", + "version": "MTc1NjMyNzgxNjM5MzkzNDAwMA", + "digest": "sha256:1f69b59f7ebdd40510a33a2e0f814bda2561d53197c764e2a89b4f081c972a9a" + }, + { + "id": "b68b3046-d084-59c4-955b-0cbc7d62f3bc", + "version": "MTc1NjMyNzc0NTc0MTg2MjAwMA", + "digest": "sha256:238aa6f79b3f37b128a96ad5fb65cfebf63dd6bd2cfc569e0df3c21a33724d48" + }, + { + "id": "b691ea92-d6ab-51fb-a924-f473a46d3a20", + "version": "MTc1NjMyNzg1MjEyNjEzNzAwMA", + "digest": "sha256:6436d79cdb116a55386d796fbdec51158823e854a571f1a4265b47408d7b1fb3" + }, + { + "id": "b698d31a-e048-5637-b87b-29914e425a7a", + "version": "MTc1NjMyNzczODQ5NjM0NzAwMA", + "digest": "sha256:86de6422e3f8d553ed6bc4398802fab7b8986d2b6f4567606b26c0c02101efd2" + }, + { + "id": "b6ae217d-6418-5e5c-aa33-c6d295f0fae1", + "version": "MTc1NjMyNzg2NzYzMjU3OTAwMA", + "digest": "sha256:f05b91a7c6d87f054e04742ecabf209b2dba3d16279eab333df84e5a22bd369a" + }, + { + "id": "b6ca9e2a-bb4f-5d59-bb7a-3893460279ae", + "version": "MTc1NjMyODE2NDUyMDI1MTAwMA", + "digest": "sha256:b14a747b9467612788f3488487cd8cc1bc3685a047674cc66515fdf9be4d433b" + }, + { + "id": "b6f09897-86b4-5ae9-a1ae-64880cf53b23", + "version": "MTc1NjMyNzc0NTc0MTg2MzAwMA", + "digest": "sha256:fbfe76445e93c190687b3e8e22d2b6a8f7d96f04f7c256b29838155c084f01d0" + }, + { + "id": "b7171ca5-1dfc-5f98-9485-fd970f22575e", + "version": "MTc1NjMyNzgxMjY2MzA5OTAwMA", + "digest": "sha256:516072eb3ce95de3bc1904e1c9ddca867ef90209bc21576cd32a3edfb7742f1d" + }, + { + "id": "b7401a7f-491c-5c88-b1b8-9228cc0aa891", + "version": "MTc1NjMyNzc0NTY2NDUxMjAwMA", + "digest": "sha256:d5a2553ce39923f13b2d824da8435c8f63f53eae6c4186218d3e0f20020a8f6c" + }, + { + "id": "b755198f-8192-535a-8cf4-cc11a0195bd8", + "version": "MTc1NjMyODE2OTA0NzQ5NDAwMA", + "digest": "sha256:f7c64fb499aad2e7784951b78004633e0c20a46fe59b079a3454fd59e9479051" + }, + { + "id": "b7583e42-2a4f-5661-91f6-0684920cd326", + "version": "MTc1NjMyNzg3NjY1ODUxNzAwMA", + "digest": "sha256:f8034710c6aef9384e72439934c5e903c367bb81198a30174c550ecb829ff410" + }, + { + "id": "b79465b5-0c67-5fc4-97f1-cf4ef5499340", + "version": "MTc1NjMyODE1NjM5OTM5MDAwMA", + "digest": "sha256:bd28036817a55edf0bff0a0aabc0295d42b319a61df3a5e40eb3de371281e684" + }, + { + "id": "b7a9dae8-51d6-5b98-b54d-540857015706", + "version": "MTc1NjMyNzc3MzI4MzMwNTAwMA", + "digest": "sha256:c49c33e1a5a7cebe8fc88ee10c10c9ffa1ff209a77c7318b22fae2398845b767" + }, + { + "id": "b7aa2d3a-7c4e-541c-bddc-f48d05656c01", + "version": "MTc1NjMyNzc0OTkyMzI3ODAwMA", + "digest": "sha256:4426da6ec5ebf392377e9db77c0ee9b696069a18c9250ba60d8310aa15b382b7" + }, + { + "id": "b7df9c69-4451-5fc9-8e31-a77ab665cb24", + "version": "MTc1NjMyNzc3MDU3MDM2NDAwMA", + "digest": "sha256:12fc3a1713e6ed1d1f2f0b33f845c12626b6b4f012f89b5480a8879d0820dde1" + }, + { + "id": "b7e0fcba-aa93-5dcf-880b-fc633c9a0ace", + "version": "MTc1NjMyNzg0MzAzMTg1OTAwMA", + "digest": "sha256:c0455ce77d718dbf6f5f92082ef19601193ffafbbcbf945a530d8b45278b59ae" + }, + { + "id": "b7fe800b-a16b-5dfc-8394-c72a5c506ae3", + "version": "MTc1NjMyODA3Njk2MjQ1NTAwMA", + "digest": "sha256:4b37b17808d7b3af679ebbad12bfa4d6255f13bb116f0f426df998c1d71056ef" + }, + { + "id": "b80106ce-3a1e-5fe5-b746-9d034eaa6976", + "version": "MTc1NjMyNzkwODY5MjQ2NzAwMA", + "digest": "sha256:8867efdffa20ae2f6792257189cd220ab97ed13e68ba7870e40ba9d705173f11" + }, + { + "id": "b81238ef-f12a-578f-ad62-29570f2b4fc4", + "version": "MTc1NjMyODE2MzAzMTg1NDAwMA", + "digest": "sha256:705f9ff5392c4b87060088ccb351aba0126466220fac09d2fa50a51d5610ea46" + }, + { + "id": "b82d25c0-9034-5506-8545-cefa98563ec2", + "version": "MTc1NjMyNzc3OTc4NzQ1NjAwMA", + "digest": "sha256:acf609e592ac7a32b41c0235b09ee917b787c2394b58d26d7e501c08b08c4e24" + }, + { + "id": "b849bb1a-7a35-5568-93b1-b32891ff0ce5", + "version": "MTc1NjMyNzc3MTUxNjU2OTAwMA", + "digest": "sha256:e274a45b013a35cfa2aa4a3cfc6d943cde5c468f9e3260dd4a33f6dcb284534f" + }, + { + "id": "b853d2ac-39e8-5fb3-9a1c-05561bbe6881", + "version": "MTc1NjMyNzczODM5MDkwNDAwMA", + "digest": "sha256:5c870de3397e45d9d5249376018b52ad3e16355410b6db0276d07d44de917c49" + }, + { + "id": "b8600c48-c544-5d40-8ae2-0c937c2cd468", + "version": "MTc1NjMyNzgwNDU3OTE1NTAwMA", + "digest": "sha256:6d9bb194e007a97ad6ad16431a91533f02f4cd7c29158ed7ec8500a0bfa64fcb" + }, + { + "id": "b86741e8-51e8-5995-881a-d2897f7bb8e3", + "version": "MTc1NjMyODE3ODAxOTk1NzAwMA", + "digest": "sha256:d99660c3057d6cc5f68fd9e00f63bf698a2052cc4ff943ca16ab6f915de1fa30" + }, + { + "id": "b87838fd-ac92-5224-a14c-d2975ddd0155", + "version": "MTc1NjMyNzc3MTQ2ODgwMTAwMA", + "digest": "sha256:1e11925ecfcf69fb5a181253ebe3422ba04b38f7edcbeb3e11b789b7b9c9fc24" + }, + { + "id": "b87ee801-ecce-546f-a190-a406e0dd7129", + "version": "MTc1NjMyNzc3NDU0NzAxNTAwMA", + "digest": "sha256:db407faa8c6dd75dd55b574b6702cc48e609da1f4cb655cb18dfd54366cff1d6" + }, + { + "id": "b8855fc9-9881-5515-b363-09b4e97b3d46", + "version": "MTc1NjMyNzgzNTIzMTI0OTAwMA", + "digest": "sha256:a607e0e43756f057f30da31b8ac1104fe8e53635aa77e64e646f2b0292994f8d" + }, + { + "id": "b88be9a4-6e1b-5606-a01e-f827bea4bbcb", + "version": "MTc1NjMyNzg4Njc3MTU5NDAwMA", + "digest": "sha256:ee8e7a3cabf0014c069a76a6ad360571c02448be1c684d044463a81e6c763e9f" + }, + { + "id": "b8b260ac-db9e-5f1a-a47f-3bc170a6a8a9", + "version": "MTc1NjMyODE3NjIyNzE0NTAwMA", + "digest": "sha256:33f71be0d4c1efb31a6fa6c57bbf9151d499978e06a7827480eb0e15d6f7cafe" + }, + { + "id": "b8e8d041-6ea3-584e-a5a3-31d143c2f5b0", + "version": "MTc1NjMyNzc1MDAyMzM2NDAwMA", + "digest": "sha256:74c6a609a980d04e49d4ae42848b43091934bd8769bdad80e02de158e2cc4fa6" + }, + { + "id": "b907890f-5264-590d-b485-d0f1bb80380c", + "version": "MTc1NjMyNzgwOTM4MjMwNDAwMA", + "digest": "sha256:bcef34fc24cc3d53e8cc5ddef331398a56c91f42493104cd9518024fc07e5380" + }, + { + "id": "b927ebb6-110d-59f8-97d5-30ef10f07a19", + "version": "MTc1NjMyNzc4NzM0MjQ1ODAwMA", + "digest": "sha256:1717ba2b36ce03737c1b79765dc1d64aafe08c12a168673d908745dfe49b62c2" + }, + { + "id": "b93444b9-6191-55fe-83b8-225b65251931", + "version": "MTc1NjMyNzc2MjUzNjc5NTAwMA", + "digest": "sha256:a4b5ba578426d9c63ec7f44411ca6eba44f151a5d6a5c86e665ab306b7c7b27c" + }, + { + "id": "b938224f-8288-5fcf-a27a-62d243370438", + "version": "MTc1NjMyNzczODQ0MTI0NjAwMA", + "digest": "sha256:0fd0d3679cb22d806f14a3614a5a2357f5204791cc454c84c367d55622ce0d71" + }, + { + "id": "b977859f-90dc-5928-8bb3-5d9b5333341c", + "version": "MTc1NjMyNzc0NDUxMDkxNTAwMA", + "digest": "sha256:be438a36106536a1d1f185fe58cfe0ddc14253f0486ba4264ccfd25662e640aa" + }, + { + "id": "b98a7660-348d-5f82-8470-d27a520b7d4e", + "version": "MTc1NjMyNzc3NDUxODk5ODAwMA", + "digest": "sha256:6e50057511522d4d154cf2b79d4207e314aa82ce649c1ec74bd1f221159d656c" + }, + { + "id": "b9901bcd-64ef-5ad1-ac9a-b42fa8148907", + "version": "MTc1NjMyNzc5NTkyNTkwOTAwMA", + "digest": "sha256:ccb7ed7552fb6f556271c55e4b0bc46d2eee72fcdaf9c5fa79b78165e9bc36fb" + }, + { + "id": "b9a829cf-b4fd-5c3a-8a0e-094275208f96", + "version": "MTc1NjMyODE2Mjk3Mjg3MTAwMA", + "digest": "sha256:2b0c15633238141fd0e74bda48239c46fe2de332b58336179346cfc9b0364a30" + }, + { + "id": "b9c0a9e6-d3bb-508e-8552-ab68999abfb7", + "version": "MTc1NjMyNzgyNjU3NjQ1MzAwMA", + "digest": "sha256:a94584961d18ca07ea75e3f86160609ec615bc90dd51136c02c2532e84429ba5" + }, + { + "id": "b9db810d-273b-548b-9e84-ce000670464a", + "version": "MTc1NjMyNzg2OTU0Mjg5ODAwMA", + "digest": "sha256:229bce5bd748b26886dd98222ae580ab93e0d02f5ca5cea1c539092679a1c5eb" + }, + { + "id": "b9e3a009-c719-5907-84f4-a1eef5614a65", + "version": "MTc1NjMyODE1ODI0NzU5MzAwMA", + "digest": "sha256:d751d8198ac45a081ee436a1b1af347fe8c31dee5b28d080e76de89a140c72e8" + }, + { + "id": "b9ed8960-615a-56d5-9d04-fe7573b330c2", + "version": "MTc1NjMyNzg2OTQ5NzA5MjAwMA", + "digest": "sha256:11e42af7ad77b3c143d95dc91a3629bbf1af66f0240ef7cb179a09d4c9dc6410" + }, + { + "id": "b9f5a328-193d-5433-a597-072eb0367151", + "version": "MTc1NjMyNzc4MTA0MTYwMDAwMA", + "digest": "sha256:88b03d1f364e8b79729d3c2c8c8d70146868a4bdd1cc7448cec07f3fc4d30c7c" + }, + { + "id": "ba1f2245-3614-59c5-9ab2-c588de191e18", + "version": "MTc1NjMyODE2NzAyNzAzMjAwMA", + "digest": "sha256:db5fcdfb4d76240947344090e085d009d5ee365c0ff27f58ee9c7db63af49937" + }, + { + "id": "ba2027ff-6cae-5420-be61-100341daa1e0", + "version": "MTc1NjMyNzc4MjA5MjQ4NDAwMA", + "digest": "sha256:686a3dea9474ed91bcd23c7540369ddeb2b5429edb05f298e59c90b8441de1b7" + }, + { + "id": "ba2068a8-47b0-53a0-96da-641d255fae4a", + "version": "MTc1NjMyODE3MDAzNjc2OTAwMA", + "digest": "sha256:c7599884c6154c58a74ddb3f039691704440f4c47cccfa8cbd85a8b8a0537c3b" + }, + { + "id": "ba4369bb-9e09-5f6b-b3c6-60e69a007768", + "version": "MTc1NjMyNzc3MDU0OTAyNjAwMA", + "digest": "sha256:3bb1e0ffc0f912a43d7dd28e1f2e631b781fd754e6b8424c4e39a340fa7bcf0c" + }, + { + "id": "ba601615-05b5-5237-a938-c5675583c74a", + "version": "MTc1NjMyNzgwNDU3OTcxNDAwMA", + "digest": "sha256:de6788fd81aca55dc79a459e310c09b2cd2825e04b4060a8053d70cb2ca01081" + }, + { + "id": "ba6194cc-4c07-5e7e-9bc7-934a08fd18ba", + "version": "MTc1NjMyNzc0MTc2MDk4MTAwMA", + "digest": "sha256:010060c31c513ad73546f11eaa2ea8d73303bb5f4231ff1a8f5583447a0df4dd" + }, + { + "id": "ba6304b7-6641-5a1e-9f41-150f2570382a", + "version": "MTc1NjMyNzgzNTIwOTM2MDAwMA", + "digest": "sha256:566c17eb98238dab2ed35f387364ba9988b6194ce73bc43d532ff201b33ef9fe" + }, + { + "id": "baa1a0dd-04a9-5f24-be37-9cfeb0d082d8", + "version": "MTc1NjMyNzc4NzI1Nzk4NzAwMA", + "digest": "sha256:3c64a0399292a0090b84d2213c7585ecd4c24019bf7de0e8453be1e4689911a4" + }, + { + "id": "baa565d8-1bc1-5070-b407-e4d6e9130978", + "version": "MTc1NjMyNzgyMjMxNDgxODAwMA", + "digest": "sha256:3555a8b0b469cdb0df03f30d6b0fb59b6cba85467934f0509d1e756d6fcda684" + }, + { + "id": "baa607b5-67a8-5131-b8ce-675664c7874e", + "version": "MTc1NjMyODA2Njk4OTkwNzAwMA", + "digest": "sha256:b33520a34ab4543454e90984d77e027d9a6662fb878e539beef4bf57ac9ecf23" + }, + { + "id": "baa8fd85-b63c-5fdf-9ee8-463346e4e11d", + "version": "MTc1NjMyNzg3NDk0MDg1NTAwMA", + "digest": "sha256:c42d367f2e5a055b2ef29b44286da4024e60e42ff9323f45a94999c722e1e6ec" + }, + { + "id": "baafae7f-02a7-5379-85b2-43936213176e", + "version": "MTc1NjMyNzgxMjg1NzMyMDAwMA", + "digest": "sha256:9a4a2df0612f504b1870c9df9c0a98cd3436a8d54b7dcf317f377b93896a076e" + }, + { + "id": "bab963aa-4aee-59c0-b301-347d5d01501c", + "version": "MTc1NjMyNzc3NTIzNTYwOTAwMA", + "digest": "sha256:bcb05aa0c5cf9316536dca80dbce656ae8c296c38dd7af8b770c084b7999caba" + }, + { + "id": "bab9b0f6-b643-5e5c-9d2e-a100135efbe3", + "version": "MTc1NjMyNzc2MjM4NDM3MzAwMA", + "digest": "sha256:b216a55ee99bf9d86f6940b2bd0be4f65fb4f16364fcd48db74d15ef9fe1b0c3" + }, + { + "id": "bad4ee43-ad8f-5d2e-a13a-528a8a61694f", + "version": "MTc1NjMyODE1NjM2ODYzNTAwMA", + "digest": "sha256:15726704c61341826800e85bcbd7fc49037dd0e73b0d70a1aeff56595f8017f0" + }, + { + "id": "bad76fdc-7c2c-5d6b-8948-8328abfa56a1", + "version": "MTc1NjMyNzgyNjU3ODU0NDAwMA", + "digest": "sha256:ffb10e34a2d35196df0fa991146949ada7b667aa55cf6b46141ee60c13d7ead6" + }, + { + "id": "badb4629-8736-5a4c-826e-1fc7012a96b1", + "version": "MTc1NjMyNzc3NDQ0Mjg5NjAwMA", + "digest": "sha256:7091330e55ec54c0be05c8909eddae84849d4b07a0b46792d1368284781078f1" + }, + { + "id": "badcafb7-4de7-5871-b31e-5585b96f6818", + "version": "MTc1NjMyNzgwMzA1NzU0NjAwMA", + "digest": "sha256:543fdddf7e191b0c464a720dc0d0d8a8044e80bbf3179a14ca3c328ef73a3f78" + }, + { + "id": "baf1c751-938b-5842-8f78-6b93c644c89a", + "version": "MTc1NjMyNzg0Nzc3NjIxNzAwMA", + "digest": "sha256:941ce5d46cadef418f03b90431c2079b9851a3c46e9b67f255b4a759534612ba" + }, + { + "id": "baf99de9-f412-5182-ad75-0b090068cbc4", + "version": "MTc1NjMyNzgzODk1NjgyMTAwMA", + "digest": "sha256:52edf3f8f15f9c19989aa6c525625e3d2cc71cee7a72c19c66b7d3f749f03329" + }, + { + "id": "bb04900f-da7f-5618-98e0-ea91fad9563b", + "version": "MTc1NjMyODA2NTUyODYxNDAwMA", + "digest": "sha256:9c1398b153fe4849f6f9add123c2edc2ebd158c6e1d3c384d19bb45e8d0d384a" + }, + { + "id": "bb0b12b3-e75e-5809-b8db-6e90c1dd47e4", + "version": "MTc1NjMyNzc5NjExMDc3MTAwMA", + "digest": "sha256:6042aecb2822b99065f96b4546896b44f73187bce423dff9e461612dcb99c3a8" + }, + { + "id": "bb0e00ef-795f-53f0-8fb9-202c66c28b89", + "version": "MTc1NjMyNzc1MzYwODk4OTAwMA", + "digest": "sha256:483a540f41c139434b985d0583e56c2e8457a7ec8ad27ddfb3c360d516a8f6d6" + }, + { + "id": "bb1f46b4-3253-5c52-95bd-2e36a6547cc4", + "version": "MTc1NjMyNzc2MjQ1NTUwMjAwMA", + "digest": "sha256:d488008a10ff0cc07e05c9de48ad50976d6b47fd13fe4ef3f974666115f1cf8f" + }, + { + "id": "bb4e3b94-ab40-5484-851b-264513a5bd69", + "version": "MTc1NjMyNzc1NjE1ODEzOTAwMA", + "digest": "sha256:b19597ffedbd3c6744f358567020d5cdba1a477ba007e55656c046500d8ef899" + }, + { + "id": "bb5e4585-b0af-523d-864b-9a9dd69bfd64", + "version": "MTc1NjMyNzg1MzA2Mzc5ODAwMA", + "digest": "sha256:55871109824fcf8672830e9b696a65f8cbee6c23b9087ec1831140cca35183e2" + }, + { + "id": "bb69775b-58e6-51d4-99f6-041fd17d9e81", + "version": "MTc1NjMyODE3NjE5MTA3NTAwMA", + "digest": "sha256:539c90b63974c4262da8332498b4467da4307534096080b07199b490f10b9ef7" + }, + { + "id": "bb70fffd-9559-517c-8a46-6db6f9b3778f", + "version": "MTc1NjMyNzgwMjkyNDkzMDAwMA", + "digest": "sha256:f01891f2d8def25c5f059c4cbe8c2b9f0333eabecbda38163681a73e68cf3267" + }, + { + "id": "bb8cec7e-5b75-5ef9-986c-0b14c2d288db", + "version": "MTc1NjMyNzg0MTIxMDI3NDAwMA", + "digest": "sha256:8e4b0994347da477a3d280454021f6fd803540d81f543abe275a0d69da09d586" + }, + { + "id": "bb984f33-4df1-588e-8808-651df892bf08", + "version": "MTc1NjMyODE2NDk1OTg4OTAwMA", + "digest": "sha256:efff1dd40c67c6fb3d9933214c5459b7b82de49c221389a22a66dd027ba5758f" + }, + { + "id": "bba85702-9994-5c4d-85cc-15b771166714", + "version": "MTc1NjMyNzg3NTU1MDQyMDAwMA", + "digest": "sha256:652e14282cca2f6eb8a5f00def123f909d2ebcdcaeeead6dd9002787e503fe3f" + }, + { + "id": "bbae2e83-cd97-5a5f-855e-c072f7a92743", + "version": "MTc1NjMyNzg3NjgzMjExNTAwMA", + "digest": "sha256:83e9d5c6af9a3fd809191bfddc5b6a7c2eb39fd01bc9e843f19e39e4c49c67a2" + }, + { + "id": "bbb96242-16c5-55c9-ab49-27947e9b77e7", + "version": "MTc1NjMyNzkzMjY0NjEyODAwMA", + "digest": "sha256:4386c3531648dde424bb31fff8e2d2e7a403221115462f7e19b781d3c538c528" + }, + { + "id": "bbb9aab8-2d89-5524-bce5-991cacf9483d", + "version": "MTc1NjMyNzc1Njg1MTgxNTAwMA", + "digest": "sha256:1a82ed92018ed8402b4f3e99e0e8fa6ab909611e6b346bbec0f93d376916ca5a" + }, + { + "id": "bbcaa9f7-9d51-5007-92d5-3c5fabe529fa", + "version": "MTc1NjMyODE3NzY0MzIyNzAwMA", + "digest": "sha256:b4a17533420b8c096612de0d79e58a2f3bd2fc96fc6cc0594d94de0bbccc02d1" + }, + { + "id": "bbe5875f-acdd-5d28-9718-738ab9eb9dd5", + "version": "MTc1NjMyNzc2MzcwNjE1MzAwMA", + "digest": "sha256:0e84e20b206d7da4977b661a6d2cba1cf08e3b24cf1246a4641fab2e81af3584" + }, + { + "id": "bbf40310-9ff8-5e9b-ba5b-4ccbeb7d3d26", + "version": "MTc1NjMyNzgzNjM1NzQxMjAwMA", + "digest": "sha256:bd04b47b4055e3b6ca7a8ffb32fded98bb7ebe27700eb169fc4537b6f47d3575" + }, + { + "id": "bc07f742-3ff9-5d9d-bebc-4d5cdb13e413", + "version": "MTc1NjMyNzg1MzM3NzI4MDAwMA", + "digest": "sha256:7a6a70518aea5b05573d644bfbd01b414c50c439b97aae1718e2e1771e17e544" + }, + { + "id": "bc15c60a-72cf-50c4-88ad-918063c1d892", + "version": "MTc1NjMyNzgzNTIyNjExNDAwMA", + "digest": "sha256:e15fd92a8dae1597b8c5c89dab0ed82f76e5ed7b8a7617f3c461cc69ffe769f8" + }, + { + "id": "bc352098-6aa4-53a9-b5d7-9055606c084d", + "version": "MTc1NjMyNzg2MDI1NzQxNTAwMA", + "digest": "sha256:91aad2ae1b92acb9271ef7f2ad6dd3f90ca9613c2a76acf0ee0e2e00cf14672e" + }, + { + "id": "bc4e340c-da81-58dc-9684-5ce45914c7f4", + "version": "MTc1NjMyNzc5NjEzMTc0OTAwMA", + "digest": "sha256:7f6e222e531bde5c620a7dd362e10d047ab9a6ec7fbc4e923c3d5af40fbf9231" + }, + { + "id": "bc51eb24-b2d4-5c17-9522-fa8c78551226", + "version": "MTc1NjMyNzc2NjIxMTIwNjAwMA", + "digest": "sha256:8125f75256d4fec43014f5cb0e05cbd532593b4403f72f463901a0d3ad9e8af2" + }, + { + "id": "bc6b1cd4-2183-515e-b81c-470db1f9af67", + "version": "MTc1NjMyNzg4MjA1NDA2ODAwMA", + "digest": "sha256:3cd44fc31d04406acafc501e943d2f5fcf575b8c710bf627679fbd40e2be607b" + }, + { + "id": "bc77e8f2-6e3a-5888-95a1-1b07af2926a9", + "version": "MTc1NjMyNzc5NDY0OTQ5NDAwMA", + "digest": "sha256:29b98a3bb7e2e7ffe0813e21ada2e08181e33a1a96917a29bae74efcc4b21d62" + }, + { + "id": "bca2326a-8469-5a27-939d-9f765331af6c", + "version": "MTc1NjMyNzgzODcxOTAwOTAwMA", + "digest": "sha256:47b7f9c75bc9eca78e692545ca4031a106fbb814a61e4989aa6dab4a1d1ba693" + }, + { + "id": "bca3285b-2258-5aae-aae2-770bf0999f9b", + "version": "MTc1NjMyNzkwNzI0NjMyMzAwMA", + "digest": "sha256:d12523ab7e08ad1eb299cad062be6e551e91446b7d50eb34e02a0e223ffc5e4b" + }, + { + "id": "bcbb720e-099c-53d0-b99b-1c56b134ef4b", + "version": "MTc1NjMyNzg1NTc3NzYxMjAwMA", + "digest": "sha256:f408c4390aad5be5c11839e15d7db8a6e958933b36abe54c46a573b08737b128" + }, + { + "id": "bcd1e544-133a-5f72-b3f2-65b06cde44fa", + "version": "MTc1NjMyNzgzODc3NjAwMjAwMA", + "digest": "sha256:0209d2f62d0e5108412496057fec79264b9e86bad71ba6946c68ea96685512a3" + }, + { + "id": "bcdd21a1-ce9b-547b-9d60-756c7f354fac", + "version": "MTc1NjMyNzg1NDQwNzMwMzAwMA", + "digest": "sha256:5a5f39e9297a718e47bc4c6d460468e034770ae750e330b79461d036d2ebaf85" + }, + { + "id": "bcf07969-fa11-54f9-b80c-5ea3d73e5098", + "version": "MTc1NjMyNzk0MDUzODkxNzAwMA", + "digest": "sha256:797612d6f61a878031a2fa2da8c56baa3e71d41b7557a39c5192cf1afc52d1f3" + }, + { + "id": "bd14958e-3ace-5b88-8773-a5f1b39a1d9d", + "version": "MTc1NjMyNzgyMjQ2NDYwOTAwMA", + "digest": "sha256:12ec53cafc0c359b888a73f66dab75076d07c90fa2847605985c66a41487a371" + }, + { + "id": "bd231a41-898b-533d-8e50-cd76c8563081", + "version": "MTc1NjMyNzc0MjI5MjAyNTAwMA", + "digest": "sha256:90ef1dea4739bd2472a2456db166cabe2faa0d54dd231b8d0942cf6e0e850b74" + }, + { + "id": "bd287747-278a-54bb-83d0-58d4f1dfe50e", + "version": "MTc1NjMyNzc3NTEzMDUyODAwMA", + "digest": "sha256:2bc1cbeb404320131373e83e9ffacd6f3a5ed456956bb77fa146b5955d60b2cc" + }, + { + "id": "bd3976ed-750c-5e9b-ae49-d4bcb6c50068", + "version": "MTc1NjMyODAyMzcxOTQ2NDAwMA", + "digest": "sha256:e393be3141e988794e29084378d769fc7e2deb865d3eef5452ab809d69f5b21c" + }, + { + "id": "bd43baaf-5ba6-5366-b3b6-867ff4da86c1", + "version": "MTc1NjMyNzgwMjk5Njk2MTAwMA", + "digest": "sha256:ace1bdefe2186f2959ef224b1042129b12e1ae92ab8a8cf41093d8cc6c49ec47" + }, + { + "id": "bd58af07-bfc3-567c-962c-23944e0f5dc2", + "version": "MTc1NjMyODE4MjEyODA1ODAwMA", + "digest": "sha256:00b583b2aa4f5fc2a0b66d63e060f7256d243d548e911e74704d55303968ab3d" + }, + { + "id": "bd8597bb-85ef-5368-8736-7ded17258854", + "version": "MTc1NjMyODA2Njk4NzI2MTAwMA", + "digest": "sha256:f4019c42367e7a1ceabb559ff00e71d368cbac2b5dd1fe2ac12adbd57c806f92" + }, + { + "id": "bdb2a7d8-6222-5af4-80cd-2c7d819122b7", + "version": "MTc1NjMyNzg1MzM1MzIwOTAwMA", + "digest": "sha256:12bac36990ec9bea6d461cdbc51e0a5c2b92e28a304d411efb13d6bde3206739" + }, + { + "id": "bdb3e9ce-26c1-5930-b9f3-175489a9a915", + "version": "MTc1NjMyODE1NjM1ODQxNjAwMA", + "digest": "sha256:0977bdbebfaf380e0792aa8a38ec8751e20704d9369d35535c0be133fd290328" + }, + { + "id": "bdc8839e-f475-554b-a858-63952512de49", + "version": "MTc1NjMyNzkwOTA0NDA2NzAwMA", + "digest": "sha256:57855e86e906194fd886150c5f8c5bf403319273c7bbba593ff96ca763224d7d" + }, + { + "id": "bdcbc09a-9a63-545b-b8c0-70b1e6909e9b", + "version": "MTc1NjMyNzc3MDY0NDUzMzAwMA", + "digest": "sha256:78d13cf853e422d00c4e8ec56e23c48bc23fb86060232e08c183e9dbd882ed0d" + }, + { + "id": "bdcc4e40-1213-5d2f-9e17-756d7063eb57", + "version": "MTc1NjMyODE2NDk0ODk1MzAwMA", + "digest": "sha256:010271ee4a60fa49bbfd7dc193d9d1effd789829255b1efd6265b47414e087dd" + }, + { + "id": "bdd5e1d1-610c-5616-86df-5c4f0bcfc25d", + "version": "MTc1NjMyNzc0NzYyODUzMjAwMA", + "digest": "sha256:a6add08fc7f1113c21bc651b397b208b46a6c42cf19d5695fb43bee83ce31352" + }, + { + "id": "bdf32e53-f293-5809-8191-9850dba7f7c7", + "version": "MTc1NjMyODA3NTY2OTY2NjAwMA", + "digest": "sha256:7c949295931e30d4e3738d92478ca004a773e16cff23eb2d387261f0fc26eee2" + }, + { + "id": "bdf85430-06d3-5b48-b531-e6f4f5671b4b", + "version": "MTc1NjMyNzc2MzcwOTQyMjAwMA", + "digest": "sha256:eb8ccd3f8ee5ee20af97f7d9f5ed080b79e986443b79225cd76aef4a73c97324" + }, + { + "id": "bdf8af7a-3f5c-54e0-a48a-bd88505bf31d", + "version": "MTc1NjMyNzkwMzQ3MzQ4MzAwMA", + "digest": "sha256:055deb36c35f69f87f28b3a11fcaaef178be30d69c67408dd513d1d7cacc4c46" + }, + { + "id": "bdfadd6b-02ac-5750-aea3-f7f44eafff63", + "version": "MTc1NjMyNzc1NzM4MDQ3MDAwMA", + "digest": "sha256:2fd3eb7dd00b95dd0d699dd4c30f118d7f3faa8109f8cdd802f4b414522c1205" + }, + { + "id": "be30bacc-eca7-56f4-a8cb-6f9baf80a121", + "version": "MTc1NjMyNzc5NTE1ODk3ODAwMA", + "digest": "sha256:5143667e343d91cd4f0c18ecd4971cac0a7bb0246cf7f5543364eab48dd5d4d8" + }, + { + "id": "be4aa5a9-b1f2-5942-87e0-f052ad754338", + "version": "MTc1NjMyNzkyOTE0MDQxNDAwMA", + "digest": "sha256:0395bf2a4216b818a61f03ff514310c912265c05838ec18735fee5715b6d9b36" + }, + { + "id": "be5201f3-8149-5748-b92b-b028c8e5c840", + "version": "MTc1NjMyODE2NTkwNDMwOTAwMA", + "digest": "sha256:230f340e349fae91155b999d0605379f6d2c147d5d0bb7e2edf5a8764cc5573d" + }, + { + "id": "be5c9a1c-9d23-5e80-8f37-b79cef482a28", + "version": "MTc1NjMyNzc1Mzc2MTU0NjAwMA", + "digest": "sha256:20aac872f8f28e635bb97c9ef4b9ecbaf3ef3b4e9ed918b150e489085ce96f09" + }, + { + "id": "be7eb30e-e529-50ed-b4ce-f176675797ae", + "version": "MTc1NjMyODE3Nzk4NjM1OTAwMA", + "digest": "sha256:29638fff361411491d1abddc4f1b4dbf40f353a6911df1fd3ac810b54a6e59ec" + }, + { + "id": "be97a143-04b4-5178-a91e-fb9ab99ec517", + "version": "MTc1NjMyNzkwNDg5MjMwMTAwMA", + "digest": "sha256:a8320ecff1fdbc2ef3f79dab8f235099337a6886d53ef045c2ea76f5ef68ebd0" + }, + { + "id": "beacb2e4-bf1f-5b2c-831a-e57ac1322e89", + "version": "MTc1NjMyNzc5NTE2MzM0MDAwMA", + "digest": "sha256:3ab0266f9a4807fd2b6bfc8d6a7bfec8b2891983cf87d00ee7d811bd5428c048" + }, + { + "id": "bebcca0d-2be4-5928-8044-cb9ef7abbc23", + "version": "MTc1NjMyODE2OTk0NzY0OTAwMA", + "digest": "sha256:3208a492d7c6b2a73440dc920f0ab5fbb82a93d50a8ed83d1dda6b8eb7e5e366" + }, + { + "id": "bec4ce0a-3f7e-583e-9f40-d02ada9bd065", + "version": "MTc1NjMyODE3NjgyNjU0NjAwMA", + "digest": "sha256:efa8c305cdeeb378199d802ef87d816fdfbf1089183345e54350f6e989747edd" + }, + { + "id": "bece6bc1-e304-54e5-b1ca-d39c62b32f36", + "version": "MTc1NjMyODE2NzAxNjU4ODAwMA", + "digest": "sha256:181294c4007d094903d556c8c2603d5f0ae1139233dfb64dc5c7231b35ea1ff0" + }, + { + "id": "befa07ce-fcee-5541-8822-15e24dc9ee46", + "version": "MTc1NjMyODE1ODIyNjYxOTAwMA", + "digest": "sha256:263731092c5dea15ecbb86a6e98f4311f988e7718b3e55954f554737f9cc03f6" + }, + { + "id": "bf0a25c4-a1f3-5424-acda-dcaad7269f41", + "version": "MTc1NjMyODE1ODIxNDU2OTAwMA", + "digest": "sha256:b90419e15bc45a505604feb3926463f74f38c8394ce6add0cb89f7c849794ef6" + }, + { + "id": "bf361ec9-374b-5bfb-bdc0-12956a1a08dd", + "version": "MTc1NjMyNzg3MDY3Njg4ODAwMA", + "digest": "sha256:9050abaf2f95172e4bc9414664b8f973cb6b88661f8f507b08e1aeea49848aa7" + }, + { + "id": "bf4d932e-f2a4-570c-9c20-36b827e6709a", + "version": "MTc1NjMyODE1NjM5NTIzMDAwMA", + "digest": "sha256:8d8899d4d425ae553dc3fdc8d2e4b8f9b7eb752c2e1c8d102cd602307d382fd1" + }, + { + "id": "bf6dca39-8674-53a6-9618-cf7b9f258ac0", + "version": "MTc1NjMyNzc0MjM3MzMwNzAwMA", + "digest": "sha256:c0904e3eaddd803df714d768f1764efc33b1eda2379183f8f0494260d4abcdce" + }, + { + "id": "bf853dd1-6baf-582a-8c0a-fe4266db2e62", + "version": "MTc1NjMyNzgwMzU4MDMzNzAwMA", + "digest": "sha256:8b27b1b69ea74a10882b752706dae53cd11c72deeb4326de3395edf9877233b2" + }, + { + "id": "bf86d1c5-f320-5e8b-a746-5110927539fc", + "version": "MTc1NjMyNzgxNTQ4OTIwMjAwMA", + "digest": "sha256:4f8b8cc9103cb552a33049e93807ace22d9a99fc4bc99ff1fad96b05aed936f1" + }, + { + "id": "bf8c274b-1054-5e79-a10f-bd7d83aa2f4d", + "version": "MTc1NjMyNzc1NjkxMDQ1OTAwMA", + "digest": "sha256:fd74e8b2af0000f27488159408bbd5843ae9f48e297f4f4befeb2dfc728dc050" + }, + { + "id": "bfa4e3a5-f983-50db-9c3a-f8425494398a", + "version": "MTc1NjMyNzg1NDQ1NjUzMDAwMA", + "digest": "sha256:c6a177c6bbfbcf3889d037eb6f2de37ce397cfa21a34e95e44b2198a9fb7331e" + }, + { + "id": "bfefc070-edb0-5c20-b2ee-0b94df7ff3a8", + "version": "MTc1NjMyODA3NTg4MDY4NDAwMA", + "digest": "sha256:4c30a6341efee154e21f35de2241de774037883e06722a0980729539081bc720" + }, + { + "id": "bff1ed75-002d-518b-bf86-c80290425ec9", + "version": "MTc1NjMyNzc2NjQ3MjU3MDAwMA", + "digest": "sha256:a28d8816fc0ba7f1b842a63746537ec36e2d45895ec29fe76f3f863208ff50d7" + }, + { + "id": "bff2b586-3c3c-59fb-87d9-d53968a3a2e1", + "version": "MTc1NjMyNzc5MDMwODYxMDAwMA", + "digest": "sha256:6e700397a37354fac8a2904a8f9fbd1aac4f3966d8ea55b50efc249589b99273" + }, + { + "id": "bffa660e-99b3-5f7f-a3e9-d2e3d926d42c", + "version": "MTc1NjMyNzg3MTU3MjYxMjAwMA", + "digest": "sha256:0b78c1362b506e17d357b4371a5d1a6669d0152d3223030a2361026ad1188512" + }, + { + "id": "c012f31a-3f67-5d87-9d6c-c26be15cb631", + "version": "MTc1NjMyNzc3MTUxNjEzNTAwMA", + "digest": "sha256:2f5b3d8760187e8533b4cd230bb5a811db035817145db5a31cd8fd2b75045d82" + }, + { + "id": "c0181574-35da-532c-b43a-df892ba73ca0", + "version": "MTc1NjMyNzgzODg2NjcyODAwMA", + "digest": "sha256:31f145b3258bde526d9f621deab39136a14cae7308e83d8af7cd109dc372f83d" + }, + { + "id": "c0498129-ea80-5d72-a007-0ddf0fdf0619", + "version": "MTc1NjMyNzc2NjQyMzM3MzAwMA", + "digest": "sha256:104223baf2932e03893cd0691daabc28b1ae34afea61b4cc4b9f24b468c95fd9" + }, + { + "id": "c04acd3c-67f1-56aa-8cd2-92acf36da301", + "version": "MTc1NjMyNzkxMjUwMTc4MjAwMA", + "digest": "sha256:b718750cc35b40bbfe6ad4cbf4e36a0a7cbe3953b89ffb76d853188b0943e0f4" + }, + { + "id": "c05d9127-5ca9-56d2-af51-cd8b5789db3d", + "version": "MTc1NjMyODE2ODkwNjMzODAwMA", + "digest": "sha256:35b702bdc87f9b6d4b56eb82ac8e5b4017fd243017b971aa1a2158fe5db828b1" + }, + { + "id": "c071933d-ad76-51d4-8884-b7b2192b3756", + "version": "MTc1NjMyNzg1MzgwNjM0NDAwMA", + "digest": "sha256:bcff8b0c58a5c065ecbbac558fe9e1d7757c1f886839134bc456a09e874470bb" + }, + { + "id": "c073a106-40ef-5dae-8f72-eb03900f73d6", + "version": "MTc1NjMyNzc3NDUxOTg0NjAwMA", + "digest": "sha256:c026f1780c45e8a44117fa2b3926c484345908e885e743d33c0dd6599b883fd1" + }, + { + "id": "c0741919-38b4-5431-b220-8244f06365dd", + "version": "MTc1NjMyNzgzNjMyMDMwMTAwMA", + "digest": "sha256:d9ca72f31bc6e0011fad2db5ee8f681635f9a485c24a6980328b2d2ffaf16b9f" + }, + { + "id": "c07c976f-190c-5066-9d08-f21389b4a865", + "version": "MTc1NjMyNzgxODk5MDc1NTAwMA", + "digest": "sha256:abdb8d36de890cd5b6be54ddb551a023487e4fa566b61f475b30ae70970d79b9" + }, + { + "id": "c086faac-8f26-517e-8dda-56ba2a5e676a", + "version": "MTc1NjMyNzc3MTUyMDY4OTAwMA", + "digest": "sha256:57d5593873324e2bdd56329b8eae3486586ca307861130f44da7d7f5c232e83e" + }, + { + "id": "c0947700-ecf7-517a-bd2f-3089d33e4ba9", + "version": "MTc1NjMyNzc5NTk1NTA5ODAwMA", + "digest": "sha256:8801dd8dd5555bfc70d2e9fa66d6a71fe614ed3acc6f12be2a75dee5a12ca2ed" + }, + { + "id": "c0c1d657-9118-5d25-a579-66d747f7c457", + "version": "MTc1NjMyNzc4NzA2MTgwMTAwMA", + "digest": "sha256:e83e26c5af0235b315d50022f82883d24ae2aa76d7c75d75463eee916da04231" + }, + { + "id": "c0f0506a-31f6-5ca7-80e2-61db3129196e", + "version": "MTc1NjMyNzgzODk1NjU0OTAwMA", + "digest": "sha256:cd6c25581a597b7ae98bed7a5e528a9b99b5efbc018265a61d8c40d42b70f8f2" + }, + { + "id": "c0faf0d6-3fe7-5487-97f3-d435644b0371", + "version": "MTc1NjMyNzgwOTIxNTY4MTAwMA", + "digest": "sha256:febbdc382999b1b2c67b499cec92316c7135315815748b288264322984635a16" + }, + { + "id": "c1077ff7-1220-5b1e-a319-a2fecff42d81", + "version": "MTc1NjMyNzkxMzA1MDg0OTAwMA", + "digest": "sha256:e4151d73c1ede5141b9477c7b3f22fa1402f09565334643bec787727bd8d6dc0" + }, + { + "id": "c125cefb-5364-5127-b3f8-3699e13b631a", + "version": "MTc1NjMyODE3NjEzMjA2NzAwMA", + "digest": "sha256:e695bdfd47146ce70a5533c12b4c166e3ce8a5cd6ab904a658dfca9e38b570a4" + }, + { + "id": "c1276238-16bc-573e-a493-3b04674d9616", + "version": "MTc1NjMyNzgyMjMxMTMyMzAwMA", + "digest": "sha256:3beda02eea3a406b72742d1c6bb87e6dee3bb431fd002ecac102a2b54686d5ae" + }, + { + "id": "c12af0f8-0950-5a5f-880e-367c5525f5b1", + "version": "MTc1NjMyNzc4MTk4MTQ3MDAwMA", + "digest": "sha256:1b92cc10f6cd4b823aa4515c69d4ea33de40ab58869cdbe434cf9f323383a688" + }, + { + "id": "c16dfd6d-6403-5134-8584-2d2b929e864a", + "version": "MTc1NjMyNzg1NjExNTc2NjAwMA", + "digest": "sha256:83e461f8e2717c7ad47cdb50df066c96311e755997104fa7c00166b7a8bbb611" + }, + { + "id": "c17cf019-f8b1-5760-8194-9b3a1a9a6e82", + "version": "MTc1NjMyODE3MDAzMDMyNDAwMA", + "digest": "sha256:bd9be9554e5a0e956a85fb731b4c11e4621e3d1c322563f12a5341f0bf0bcd4c" + }, + { + "id": "c18192a4-9985-56de-af24-f6f7bca8ef0f", + "version": "MTc1NjMyODE3MTExMTcxNzAwMA", + "digest": "sha256:d3c692f4c64c107c19c50cf4a1a356cc65110179108f53566d6376e577272b0e" + }, + { + "id": "c183b75c-4ede-5f94-b90d-7f46584a07da", + "version": "MTc1NjMyODE2OTk2NzA0NjAwMA", + "digest": "sha256:06f5cbed2122d134c13def61d0d30a772afdb5d2495eb1c06e8c3a3f853ac156" + }, + { + "id": "c18c1971-5e89-568c-97e7-ac5001219d49", + "version": "MTc1NjMyNzg3NDg4NDkwNjAwMA", + "digest": "sha256:a79d163c02de6e909df193e31309714caf25c061ee7bfa58c7fa336efb96d582" + }, + { + "id": "c18d5f35-4dc9-581b-9a49-bc0b2762fc41", + "version": "MTc1NjMyNzg4NjU4NTQyNzAwMA", + "digest": "sha256:1a3399595fcef2dedfcdf6e6f7e7edddbac7083646ffa8120c88b624440c8e9c" + }, + { + "id": "c18f4ea9-2bd9-5d4a-8887-b6c56e6470b1", + "version": "MTc1NjMyNzc3MTUxNjA2ODAwMA", + "digest": "sha256:6a00555983b0b3ab19b30747a27a023e6d854f0fd33eb2196adbdaa278154095" + }, + { + "id": "c1a65d51-2287-50a5-b41f-2b78f8a8d082", + "version": "MTc1NjMyODE2NDkwNTA2ODAwMA", + "digest": "sha256:bf55f9003b5b68f4c6077590e2f15af956be46496d20535acb76a1d4126785e9" + }, + { + "id": "c1b01f3c-c3e0-5486-b71c-6f3789b48d51", + "version": "MTc1NjMyNzc5NTI3NDgwODAwMA", + "digest": "sha256:ae7eec9dd3a22d56343ad75bfe6edd0132def9370f7b20076c15b35eb9234d2e" + }, + { + "id": "c1e0e2ca-2315-59e1-9083-e31a04607ba9", + "version": "MTc1NjMyNzc5NTI3NDkwODAwMA", + "digest": "sha256:3d2c516bd8845ab914c7df2d585ca936126372f812a215f0b693b12344ac0a37" + }, + { + "id": "c1f9dd93-60b6-5c2c-b016-75e9aaf9b7c6", + "version": "MTc1NjMyODA3MjgyNTU1MjAwMA", + "digest": "sha256:c8cc6abbb4364477f5d596b5f7accab4fa7b2bbb2d27cabf657ce66d32ee7bf4" + }, + { + "id": "c2004fdd-ce66-53e1-b916-9913ef941db4", + "version": "MTc1NjMyODE1NjM1NTMyMzAwMA", + "digest": "sha256:d87f73b50652a320ed872bd6e255f02511530fea44db6fc7f17100aa0adc91fa" + }, + { + "id": "c20077d3-c741-5910-890c-eba29a202f03", + "version": "MTc1NjMyNzg1MjEyNTkzODAwMA", + "digest": "sha256:4e15dbaaaeee11301616c02a3dfc9c679e8fb81a189dc82d0fa75d584abe631a" + }, + { + "id": "c234867f-1031-5a90-877f-df81b1465b09", + "version": "MTc1NjMyNzg3NDg5OTg2NjAwMA", + "digest": "sha256:f937c77a76d3a0ddd13aa0c68dcd41df447640a90f911554dba57a36f0484f96" + }, + { + "id": "c255d519-4537-597d-812e-e12de52388c4", + "version": "MTc1NjMyODE3MTE5MDY1NzAwMA", + "digest": "sha256:cb6b0f3942f33990f1d7b0aa9fd54d260485537ab3d5c7184da393203928ddeb" + }, + { + "id": "c2930c9e-853e-5621-986b-d79e59464a81", + "version": "MTc1NjMyNzc2MzcxNDk4MTAwMA", + "digest": "sha256:825444cae9bcb757e2a2c7d38103d31dc1d7ab16639de56bbb0531c051d3d658" + }, + { + "id": "c2e6cdee-b544-54c9-8e48-dd3e2d608b42", + "version": "MTc1NjMyNzgxODk3NzYxMjAwMA", + "digest": "sha256:5495f2ccc3d2511aeae04334a39fe329f93995d560045618bbb9695215cb9863" + }, + { + "id": "c310c5f8-afd3-5040-8230-9cbcbea3548a", + "version": "MTc1NjMyNzc3NDUxODU1NDAwMA", + "digest": "sha256:b07a47c4c2d69d9ad4e40a2900ba7a5afeca34a48ee5372ea9c95eb8c9659b6b" + }, + { + "id": "c31124ed-91fa-5534-8947-0344918bf6b5", + "version": "MTc1NjMyNzc0MjQ1NTY1NDAwMA", + "digest": "sha256:c8e074e0f30d9955da11a0d2446e0a6299ba3ad3bf3b7170382ea689b093a3e6" + }, + { + "id": "c31608ce-2623-5f31-9672-70f6d6ca0bdc", + "version": "MTc1NjMyNzc1Njc5MzczMTAwMA", + "digest": "sha256:47fc6652b5b1b7ef0385fdc0d5b5375e9e0af533f5a9a4c9317f01db467eb480" + }, + { + "id": "c31a9163-dd57-5fd5-8f28-e101ed07d291", + "version": "MTc1NjMyNzc0MTY0MjAxMjAwMA", + "digest": "sha256:2e9e8ae7a9a7c1e91e3919d1557651c28306354f5b575faa7d1175190a0d9aad" + }, + { + "id": "c3273245-6671-518d-8e0f-4e7c41290131", + "version": "MTc1NjMyNzg0MTE4MzcxMjAwMA", + "digest": "sha256:00c29f13a4ecc2cbcdc7bef3cd9378c91ad058296d5c8285958684af26ef5862" + }, + { + "id": "c341ce2c-628f-5b19-950d-14524e378c2e", + "version": "MTc1NjMyNzc3MDU1MTE5NDAwMA", + "digest": "sha256:bbda6a1d0859ed1d8ef22c58921dc7d457232c8ddd41b5c10b872654af2688a1" + }, + { + "id": "c35da736-1027-5f54-9299-25958b0f196c", + "version": "MTc1NjMyNzc4Mjc1MzAzNTAwMA", + "digest": "sha256:9ae2682dab79c60831ed6ab94e0f5bb7f86b5e2e76fad288dd34caf3ae0937c3" + }, + { + "id": "c35fd63b-7967-5c7d-948f-4ff564e8e7b9", + "version": "MTc1NjMyODE3NDM1NDY2ODAwMA", + "digest": "sha256:340e311bc4abae1513a627e5756d706944028111b410f8e1bc949ca5180e2499" + }, + { + "id": "c37f80a7-ad40-5160-9d06-890b4b73a192", + "version": "MTc1NjMyNzgzODkzODUwNzAwMA", + "digest": "sha256:815d413471f277e8bd27a5c941b862e43b07b3f1eea14721f6b8cd42d2deb1eb" + }, + { + "id": "c3aad187-dad9-5f4b-acf7-b5658ba6450b", + "version": "MTc1NjMyNzc0NzE1NjU1MDAwMA", + "digest": "sha256:f3fae97a97c1645a2a373b4bfb2f7e2c43318b4c98d63743f3a99d4c29059d85" + }, + { + "id": "c3b22040-87dc-59f1-b9bf-c235efa91018", + "version": "MTc1NjMyNzg3NDkzMDI1MjAwMA", + "digest": "sha256:334ff06c45217b132b42bdbbe0fb9e3347da0f5798ac5d5dec7a2507a940daa1" + }, + { + "id": "c3b9e78b-ec8d-5d02-9b98-c36c7d5466cb", + "version": "MTc1NjMyNzc1NzUzNjExOTAwMA", + "digest": "sha256:0534ad5f70a250b77da2dc35a5162330dd6867717ca8b45b48f5909fd198b30a" + }, + { + "id": "c3d365d6-527b-50c1-8d2b-c4b7bd8d7c2e", + "version": "MTc1NjMyNzg0MTIzNTEyOTAwMA", + "digest": "sha256:e8b69d5dc3d2badf1b204dec50bd57e20064d7019e137cd39f737d44d98555d3" + }, + { + "id": "c3eb296c-8dd9-5790-9026-8346f0c7e0e9", + "version": "MTc1NjMyODA2NDk3MjIyMTAwMA", + "digest": "sha256:5d78da7de40d17cf4c253be4a75dbebd1c76a005f605afe57800103b340dc3f2" + }, + { + "id": "c3ef7fec-0628-5c26-99d6-9600fce2a966", + "version": "MTc1NjMyODE2NDUxOTk4NTAwMA", + "digest": "sha256:d79bb38b50866d29d1f6f8c737d2773dcdf0dc4f259b5243f61791892dda2c57" + }, + { + "id": "c3fdd91f-4081-53be-a665-3e8f12984574", + "version": "MTc1NjMyODE2NDkwMjA3MDAwMA", + "digest": "sha256:8c58584d170843e76b3256d2824221882a47fa917813dc8a0a598564c0dfb954" + }, + { + "id": "c40c3b71-0526-5e99-a729-ee4868a72520", + "version": "MTc1NjMyNzgwMzUyMDc0MjAwMA", + "digest": "sha256:3b399a286e73b3100f7697316564cbe2fffda521a4e8347d7262810496662bfa" + }, + { + "id": "c415c8b1-0e47-5ad8-9827-8b9de9cfebee", + "version": "MTc1NjMyNzc0OTk4MDE4ODAwMA", + "digest": "sha256:8e4f9617e896afcddac268abd3d18f19d9800661a477b2f1157398532ec2db76" + }, + { + "id": "c4329f61-8c8b-562e-9d61-caf81ed32930", + "version": "MTc1NjMyNzkwNDgwOTM5NzAwMA", + "digest": "sha256:75952cca4638b608da1efa61448367db288f60bb3ffb1a2f29805c7610fc00a3" + }, + { + "id": "c43b574e-0a8e-59f4-8dc5-6cd925d26102", + "version": "MTc1NjMyODA3NDUxNzIzMzAwMA", + "digest": "sha256:3a562b2b178354330ed5fbe41805664b58adf0e1ed4dae3eaee0077725087c70" + }, + { + "id": "c46489ea-5361-5fe7-a3c7-f399f48e47f0", + "version": "MTc1NjMyNzc4NjkzNTU1OTAwMA", + "digest": "sha256:3e3583c31f84f2c4f0ffde57612d24a146579a8a206e38d63b64cbcbcf49e1df" + }, + { + "id": "c4653d5a-0d73-5be6-8bf4-1560c3b41569", + "version": "MTc1NjMyODE3NjE5MTM4MzAwMA", + "digest": "sha256:04eb8c567d64fc2c5233698604b2e42df52d522d58168d102416f85af200a852" + }, + { + "id": "c46f6589-7c56-5c16-8d84-5317b62fcdba", + "version": "MTc1NjMyODE3NjIxOTk0MjAwMA", + "digest": "sha256:fd9656a1c84f469bfe25caac06d04ecd22b29f8f7e8e31138728f3e887c81634" + }, + { + "id": "c498e368-d81f-5c69-8c0e-b2a23e36e407", + "version": "MTc1NjMyODE3MDAxOTk1OTAwMA", + "digest": "sha256:691b8a810742aa39a84a745f674bca36ab0dad6119952f78915aa183aac87698" + }, + { + "id": "c49cadca-0a36-5215-b10d-d49991d6d926", + "version": "MTc1NjMyNzc3MDUyMzg4NDAwMA", + "digest": "sha256:4511059e6f1f3ba322664b580683044374ca7025b775d54320538703b9f856ce" + }, + { + "id": "c4b37999-f227-55c4-8260-af86100bc2b1", + "version": "MTc1NjMyODAyMzkyMjA5MzAwMA", + "digest": "sha256:8d4422874cc47edc33e6dea75c6ad9508e4c69d488e6269f6c8a4b9786c53a99" + }, + { + "id": "c4b9c554-aad9-5c4a-b5f1-a96cdd211744", + "version": "MTc1NjMyNzc1MDA0MzAzMDAwMA", + "digest": "sha256:bb5310299f5c9a92d7c9ff6213dabb2c02540b611764f4e8c6f8488c77c1178e" + }, + { + "id": "c4e2f4cc-b30a-5fc4-a78e-a3379399661d", + "version": "MTc1NjMyNzc4MTk5ODE2ODAwMA", + "digest": "sha256:0e796e4e04eb2f2cb00511b0a724d9dd8984c56f95439e0600ee726550100cf9" + }, + { + "id": "c4fdc66e-adbe-5fbd-ba77-fac3da187c35", + "version": "MTc1NjMyNzc1MDA1MjgyNjAwMA", + "digest": "sha256:26ef9c90455f89b4700a937d52dc0cb18964b5b84792163da1d954522cdcc059" + }, + { + "id": "c52c0d65-c452-5e1c-8d5b-6bab7fe41e2a", + "version": "MTc1NjMyNzc0MjM1NjQxNzAwMA", + "digest": "sha256:25601c2a2f6cd9fbef9bdb3fdf43898ab28fce7ca190d0576a3f87808839cbba" + }, + { + "id": "c52fde6a-b8a7-5532-bc66-f425260ee698", + "version": "MTc1NjMyNzgwNDU4NDQ5NDAwMA", + "digest": "sha256:09621c18b63e939958fd5caab66cbe055565917ba648fa69b05f5b2439d138df" + }, + { + "id": "c5538dc5-d1e6-5484-bfef-bb963bb470c6", + "version": "MTc1NjMyNzkwMzY0MzIwMDAwMA", + "digest": "sha256:422ac6fb7d85d54359bbf5e2324e2ceb523569ea792960f504e2348be06e15d5" + }, + { + "id": "c557390e-3c92-50b7-a7d9-af53907e3899", + "version": "MTc1NjMyNzk0MDY4MzUxMTAwMA", + "digest": "sha256:ff1d11b104cf0ade3e4f84e8602c9dd46898fb4a7172b6d2f54f111bae0f3316" + }, + { + "id": "c55b3fc4-6e4e-553c-861a-08243e19379f", + "version": "MTc1NjMyNzgzNTY5NjcxMzAwMA", + "digest": "sha256:f43cfee927cecfd287b9c9fcb8e1da73abbb4c079bb9e96196fe5d92bac684d6" + }, + { + "id": "c5669c5e-2d27-50ae-9e4d-411f21453fe8", + "version": "MTc1NjMyNzg3MjAyMjY2MzAwMA", + "digest": "sha256:932b789385a4e03797f189653b2c116ba211b5a70c8171ac19b9f3ca781fd3d3" + }, + { + "id": "c56af6e0-dbaa-5370-96a2-85642450d191", + "version": "MTc1NjMyNzg3MTYyMTA0MDAwMA", + "digest": "sha256:dba68197d3d4832fb59f966191a387ab29900d940c1dd20a3583527d6b2d8980" + }, + { + "id": "c575dc3f-05af-5d7f-8bb6-4cf716970874", + "version": "MTc1NjMyODA2OTc4MDk1MjAwMA", + "digest": "sha256:1452ca5b251334d7c3e663dca50f54036be66fa527cae2b3242bd22cc30a2275" + }, + { + "id": "c59bca51-e64a-5bcd-9f65-5e98909f5446", + "version": "MTc1NjMyNzc5NjExMjcxMjAwMA", + "digest": "sha256:9942d5c691635118f479429f6a072ba90d6553e2e9504639834d5c5fd6ac1954" + }, + { + "id": "c5a0fa0f-0e3b-5457-a894-d3b9483dd641", + "version": "MTc1NjMyNzgxMjczMjU3NDAwMA", + "digest": "sha256:89561d6d1e0b5f308d8a7c53729fd0da5382285047edb506a8c8f6890f2332a5" + }, + { + "id": "c5a10d6f-975b-518c-a59c-6755504bd2b3", + "version": "MTc1NjMyNzc1MzUyMTAxMzAwMA", + "digest": "sha256:6ed5d145d2b3de71b047d111ac5fd6a845886ee745597d7604546687849d04b1" + }, + { + "id": "c5a3650d-8df3-5a94-8e83-33f6de9023ba", + "version": "MTc1NjMyODE3NDMyOTkxODAwMA", + "digest": "sha256:595f0e457b674b2de325aa29b7d46f4771d75d6e497479984968928b324a296b" + }, + { + "id": "c5ab455a-326c-5e10-a6ae-d642e4486e0f", + "version": "MTc1NjMyODE1NjMxMDAzMzAwMA", + "digest": "sha256:340a37687f7c8e49a56c942fa60666c10f645b28bcf53e4a41d9d3e9a8c88e0e" + }, + { + "id": "c5bc0e1f-fc92-518c-b706-acfabfb93a0b", + "version": "MTc1NjMyNzg3MDQyMDE1MzAwMA", + "digest": "sha256:4bf424a1c6d7fd97d2046b47d6318f817399c92cda9a952ca06bb774fb57151e" + }, + { + "id": "c5bfe9cd-05d9-58c0-9c9a-8287afc8deb6", + "version": "MTc1NjMyNzgwMzU1MjkxNjAwMA", + "digest": "sha256:b33bf6148fd0181dec44b40c219fbb9ea30bb8b9e1ceb00316a020e65ed8f037" + }, + { + "id": "c5c2a6ce-c9cd-5e0c-883e-5deb959304a1", + "version": "MTc1NjMyODE3NzYwMzUyMjAwMA", + "digest": "sha256:90cf027b766302a2e63e083470505476eac3d0f53b69167054b58d642347fc66" + }, + { + "id": "c5c7bc9d-55b9-5787-97db-a0255c3f08ef", + "version": "MTc1NjMyNzc5NDcxMzU4NjAwMA", + "digest": "sha256:0e8b14632af361bdebbca4157cc7fc49d0e9a7013e2fe574fac22d9b8847132c" + }, + { + "id": "c5c97271-971e-582f-9b28-a61cb66a18ea", + "version": "MTc1NjMyNzg4MTkxNDc2MDAwMA", + "digest": "sha256:bc8a0de91c1dbfe08041bdced4000e3e2ed56dc9727c5c8a54b7b5e5dab732c8" + }, + { + "id": "c5d1f5bd-7743-5229-aead-a8b81d579074", + "version": "MTc1NjMyODE2NTgxNzc0NTAwMA", + "digest": "sha256:a762318766fe06bdedfdd5d66d45bebf37913aac76a12fe74506f575b7ad1b5b" + }, + { + "id": "c5e11587-8951-5edd-9818-e69e31a39704", + "version": "MTc1NjMyNzc3NDQ1ODQ1NDAwMA", + "digest": "sha256:1f1c3def5e7b5e0947461fbfc88da1255060c152e06a1f1be3a9c18713191de2" + }, + { + "id": "c5eae783-5a95-5c7f-bba4-ec624f2795ce", + "version": "MTc1NjMyNzc0OTkwMjE2MjAwMA", + "digest": "sha256:479ccf606a6896ef9c8b010f70be604200f53de7572d21de314d5af382c74902" + }, + { + "id": "c60d5340-3fa9-5c44-bf2f-cffcac550582", + "version": "MTc1NjMyNzg2NjY4Nzc1NjAwMA", + "digest": "sha256:7c62d17b5ea6cf5cb468772553fb2d52a7fbf455b99d88dae408e3d59c84cefa" + }, + { + "id": "c625efa1-0aa0-5212-9144-033d1acb3b7f", + "version": "MTc1NjMyNzc1Mzk1NzE0NDAwMA", + "digest": "sha256:4cf9ddfadc2ac0948d125f499560a2bab5722f9b255abc4d24f76a71c17d409d" + }, + { + "id": "c6323807-ccdd-5eb7-bd9c-cf2379760912", + "version": "MTc1NjMyNzc2NzgyNzY3MjAwMA", + "digest": "sha256:c0a329ddc3c73adb7dde809e13644d33ca23faf58334dd4b86152d373ffdf110" + }, + { + "id": "c63dc08c-296b-5b4d-80ba-cb1c1fbf7d23", + "version": "MTc1NjMyNzc3MTQ4MjcwMTAwMA", + "digest": "sha256:e667db412bef6da4fa8fb49e1b12ec84d4968677680b5a7524bd88ecaa796328" + }, + { + "id": "c64be954-f81f-5ff0-af99-349a381f438a", + "version": "MTc1NjMyNzg2MDM4MjYxNjAwMA", + "digest": "sha256:c76cd191c557bdb89198bc145f68900ff5208553d8367b5c5fa9a82f4ee51d7f" + }, + { + "id": "c66f5804-6b9f-592d-819d-e689f3bec97f", + "version": "MTc1NjMyNzc3MDQ0Nzg3MjAwMA", + "digest": "sha256:ea4697f1fda24c110e8e75b3e5a61ad04471a94a829dbb59702250c530ea26a4" + }, + { + "id": "c6884932-8a0d-5741-b795-fbfc6244cd90", + "version": "MTc1NjMyNzc5NjA0ODEzNzAwMA", + "digest": "sha256:162b219c82445c7ad55a48f93e5befc14810ba6d9348d7962b4400c5630c4ce9" + }, + { + "id": "c68859aa-c928-5355-9713-2a447394a1fa", + "version": "MTc1NjMyODE3NjIzOTUyOTAwMA", + "digest": "sha256:f1515e35dd229619cc4bd106d73406605c7dc57d875a09efa1b875a51bb87177" + }, + { + "id": "c689289d-e937-51b9-907d-ad925ca405f6", + "version": "MTc1NjMyNzc1Mzc2NzIwMjAwMA", + "digest": "sha256:272b5930023096180fe7338ca6ed02a5342d6426b899a369db325f1ee3067f80" + }, + { + "id": "c697d756-97d5-53b0-9d5e-3cf1a47d0c70", + "version": "MTc1NjMyNzgzNjMxMjY5NzAwMA", + "digest": "sha256:09937c6fd0a2ad59b4ffe318fd26a5dffc6d7adfc5c54540f03f05de786ef54e" + }, + { + "id": "c6a45a92-039a-566c-9ff8-a6f68a4859de", + "version": "MTc1NjMyNzc4NzM1NzQ5MzAwMA", + "digest": "sha256:c78193dbe2f89cf8ae362e7cdfd9e0bf175dab8ff72aa2266720c536f482f3b6" + }, + { + "id": "c6cc8274-8885-5f77-b9cc-d42d35c45533", + "version": "MTc1NjMyNzgzODgwMDIyNjAwMA", + "digest": "sha256:e4595aa1cda3459d33c9b4a21d7f33b51fca2db62941b3a36db80058f8d96565" + }, + { + "id": "c6dce121-c4a9-5c20-aa66-91dbab427cb6", + "version": "MTc1NjMyODA2Nzc3ODM1OTAwMA", + "digest": "sha256:06177c4d88ef4acc39e0788399796d60598abde83baa3f63390296e7433086a9" + }, + { + "id": "c6e366f0-5775-5596-b1ac-690425abd0d1", + "version": "MTc1NjMyNzc5NTIwNDA4MDAwMA", + "digest": "sha256:b2f3da6b3058c9f5bc5969dbcf084c288b70cefbaa1f54861347fffa4a78781f" + }, + { + "id": "c7094646-b911-513f-a19c-488a9028fb7e", + "version": "MTc1NjMyODE2NDkxNzUwNDAwMA", + "digest": "sha256:4ed373c71f158495a3fdfe49ed0f2e8e74c615117ac3b165f1e9077a0f183660" + }, + { + "id": "c70b7a0f-6aa3-5fab-aaf5-a7c344bef905", + "version": "MTc1NjMyNzgwOTMxNTE5NDAwMA", + "digest": "sha256:999673c8fb7bd68b14d584455f03e4b7d8007fd0d30ae3bf9598088816d6bf3b" + }, + { + "id": "c71e3035-a336-5891-8369-681744eca390", + "version": "MTc1NjMyNzkxMDkzMDQ1OTAwMA", + "digest": "sha256:aa553653f2dc29da817ed0941601d0b13e777f9a11cc77a0bc1c341511614093" + }, + { + "id": "c732f44b-b19c-53ac-b3ac-dc283c9a6480", + "version": "MTc1NjMyNzc1Njk5NDM1MzAwMA", + "digest": "sha256:dea46c66cd44fa95fc96c865ef7d429a2c706e1e62d0b0a409cc64b2c7a01602" + }, + { + "id": "c76d49ea-a483-5ea8-b893-33a77bcba12c", + "version": "MTc1NjMyNzc1Njg4Nzc5NjAwMA", + "digest": "sha256:b514ca59241ba84b3451164837dd5e8c4147e7e0268a323013693de67256c1da" + }, + { + "id": "c76de3dc-b615-58e5-90b4-431cd8365a62", + "version": "MTc1NjMyNzc1Njk0OTkzNjAwMA", + "digest": "sha256:bb82c50835c935b5439495ee93f129e09e5c308260b70ec3fe96ad84477b5f07" + }, + { + "id": "c77a843c-0144-53b3-97e9-cd0b7efea4b2", + "version": "MTc1NjMyNzg3NjcwNzU0ODAwMA", + "digest": "sha256:5c15e61d4cc1b92abfa2c9c3f7d3d4b879bb1aaee1801438cfb0b8a012bdd962" + }, + { + "id": "c77c9ef8-f0d7-54a5-9975-c2a4ec14a976", + "version": "MTc1NjMyNzc3MDYwNzAxMzAwMA", + "digest": "sha256:cc3bc74d6978fba30ba44c35b36c5e3e0df2b25197b557cfdebf9b1642e8b413" + }, + { + "id": "c77ff612-0b7c-5d7d-8825-a5c4f635f7b4", + "version": "MTc1NjMyNzc5NTIwOTEzODAwMA", + "digest": "sha256:1cacc09a05b462f839ff6758af2eb2da075fbc8bf873058d2c69e7c49dcc8547" + }, + { + "id": "c799b330-ade2-5422-bcc0-4519261b71a7", + "version": "MTc1NjMyNzgyMjQ0NDAxMDAwMA", + "digest": "sha256:3962cb93dd1db3b96d526309e865477fdc103fb0408682ceb7853cfc8b6abde6" + }, + { + "id": "c7a057ba-bfd9-50b5-86f4-51c337a6a250", + "version": "MTc1NjMyODE1NjM0Mzk1MjAwMA", + "digest": "sha256:86af4a8b33faab8054adec9639e37ea85f966ed89e15856cb173f6e9f83e56af" + }, + { + "id": "c7b24454-81a0-55ba-a690-1b65a902be27", + "version": "MTc1NjMyNzc3Mjg3MTE5NDAwMA", + "digest": "sha256:b13bf743348af3853478cc445e72762d97ee0579a9d0b944e74c00a26736ca89" + }, + { + "id": "c7bd91e5-574f-5355-8259-dc9ada74cddb", + "version": "MTc1NjMyNzc3MDM5NDI2OTAwMA", + "digest": "sha256:8c96edd38aebcf7c8c1062aa359a0b088bd8c8c2c3ad5196752a77388447a457" + }, + { + "id": "c7f53bdc-ee52-5407-9c8c-0df89772e75b", + "version": "MTc1NjMyNzkwODkyNTI5OTAwMA", + "digest": "sha256:fabd7a553d039f57d394cc52de44f41155c1eeb4c5cf7af261358f17770502f8" + }, + { + "id": "c81a5ee7-b217-5eaa-a7b2-90c61b6c04b7", + "version": "MTc1NjMyNzc3MDU0Njc3MDAwMA", + "digest": "sha256:962fa168ccfc0356899d99ab8f7da02626ef0f6b7670f9e63d83c03f8a956cd6" + }, + { + "id": "c84e2b5f-72d1-5fd1-98ee-c3c928863868", + "version": "MTc1NjMyNzgwOTIzMjc5MjAwMA", + "digest": "sha256:bbcc40a8ad475e98d0e570172522b9cd004d766bb67f97f1eb0c26aa1fb436c3" + }, + { + "id": "c85d4d97-66c7-5e12-9acc-90488c2af32a", + "version": "MTc1NjMyNzg3NDk1ODg1NDAwMA", + "digest": "sha256:9e477a13228111ac6583fb5cef3d1a38a4075bf186a308ab9a1d027125ad7233" + }, + { + "id": "c85eaa98-e819-58eb-a354-dd7df12b2199", + "version": "MTc1NjMyODE4MTg5OTgwMTAwMA", + "digest": "sha256:f4c0e8f2efd4a5ec6ede093e490ac2b1d28c3c44b0a8d0c09d040f82d4c28c5a" + }, + { + "id": "c86575ac-d401-5c4a-8d46-01c77e467368", + "version": "MTc1NjMyNzgxNjM5NTIyOTAwMA", + "digest": "sha256:324c02a8c8893f324eb147f37ff318b599054ea72d0fdad82e678ab4dfa5baab" + }, + { + "id": "c898aa7e-0ea2-520a-8c5b-6aee8af12956", + "version": "MTc1NjMyNzc2MjM3ODg2NzAwMA", + "digest": "sha256:26be3a378c2ac85a2fa4ea1dc103f888d3007ac8aa5a378b32ba5f5d8a2c3256" + }, + { + "id": "c89f2246-fb15-5165-ae08-b9f8b61dd18a", + "version": "MTc1NjMyODE3NDM5NjIxNTAwMA", + "digest": "sha256:901bdfde78c36f6809b982abad6783a2911a37c0d43c085b6db0c725fcb1cca3" + }, + { + "id": "c8a3a6ec-c4c2-5c62-8d1c-48cdbca7d8b1", + "version": "MTc1NjMyNzc0MjMyNDgxMjAwMA", + "digest": "sha256:0ea3c4264464aaf3a618b4b14b6757ed23c2677769b40949133d9448ace742b8" + }, + { + "id": "c8adb315-15be-517d-bb6f-6263123adf07", + "version": "MTc1NjMyNzg1NTY3NDE3MzAwMA", + "digest": "sha256:ad46ed2ff70d769c05afda6fb2d7ceae3076c1ceb6276a765c80f50b6ff84b53" + }, + { + "id": "c8e6a0b7-fa8d-5d17-9dc4-e01aac87ed0d", + "version": "MTc1NjMyODE2Mjk5OTg5MDAwMA", + "digest": "sha256:5143a1be9482a1b2e98233850e4c4c2040e6bcdd60a1641026646263c50c66d4" + }, + { + "id": "c91c072b-5b2e-50bd-912e-c37c99c6cea0", + "version": "MTc1NjMyODE1NjM5MDU2MDAwMA", + "digest": "sha256:1977506562b2eea1c272284aec390a5735fb681c4145e3cd1582e65a1423c86e" + }, + { + "id": "c935715e-e092-5a67-8755-1b8f7a13d033", + "version": "MTc1NjMyODE2NDQ4OTE2ODAwMA", + "digest": "sha256:8a0126bd0eb912353c9a090dbd3326c3cc640ad8be33bb43d62dbc1d1e1c3da8" + }, + { + "id": "c95831be-b72d-5b87-a5f4-c6d9f7aad220", + "version": "MTc1NjMyODE1ODE5ODU1MDAwMA", + "digest": "sha256:75ea4a5d030dbbf4f1f1163011b67fc57cb9a07d193b02f0c4c379370a764953" + }, + { + "id": "c95ff242-f0e0-5686-b0d2-7a2c3d10c7ad", + "version": "MTc1NjMyNzgzODY3MjU1ODAwMA", + "digest": "sha256:580b979471b024778ab02e4d6be5b0e6900f52b96dabd409827374a236d0a18c" + }, + { + "id": "c9632a3e-0220-5943-aaaf-eac891adf138", + "version": "MTc1NjMyODE4MTkyMjk1NTAwMA", + "digest": "sha256:30091abf18d85b73f4b11da2fe0d8ff46cc655e2ea7258bbe81df39ded0f3cf0" + }, + { + "id": "c968457f-f675-592e-a626-6502b788988e", + "version": "MTc1NjMyNzg2NjY5NDA2OTAwMA", + "digest": "sha256:38c605b99e5605791ffded2c0b6e8550d7977e4aeceaa8cca646f35b02c3f426" + }, + { + "id": "c97404fd-2c18-5157-b254-2dea95d4471d", + "version": "MTc1NjMyODA3NjA2NDE3ODAwMA", + "digest": "sha256:cbe61502adb8d092a37f5fc3d082b5164052eda436792b69c2263813f4110ca7" + }, + { + "id": "c987a9d0-8a19-565b-8063-93b3bee49767", + "version": "MTc1NjMyNzg3MTYyMTE4NzAwMA", + "digest": "sha256:fc0e76bcf6c6d5cfbe3415251457050e8f4136684881e2cdfed36f7429185f81" + }, + { + "id": "c991581d-cbca-589e-adac-0deab92b0524", + "version": "MTc1NjMyNzgzNjMxOTAyODAwMA", + "digest": "sha256:5fab97bca9b6710dc2e4687fcbc2f887e174e9c83c7572f3f5a78e647bfb5c2c" + }, + { + "id": "c9b5cc6e-9c63-57e7-837c-76be7cd07d24", + "version": "MTc1NjMyNzgyMjg2NDkyMzAwMA", + "digest": "sha256:78a67b401c17910d9202506ce7c14c5635f81f1b2c9248d75ca5b7261fce120b" + }, + { + "id": "c9f80e71-c128-58a4-9d26-3b6e66ebf2ff", + "version": "MTc1NjMyNzg2NzYzMTYyMDAwMA", + "digest": "sha256:1f5adae1929389a783156682271468e8b64e840105952183cd28ba1441a19c38" + }, + { + "id": "c9fe609e-a287-50d9-9b1a-e777990f311c", + "version": "MTc1NjMyODA3NjA3Njc5MTAwMA", + "digest": "sha256:bcde9fd3be77a81785c49b19261b6d406fc1686664af7d2176995bc3eea60efc" + }, + { + "id": "ca054a5a-6bcd-51db-a867-f3a71b6972dc", + "version": "MTc1NjMyODE2NjkyNTA4NjAwMA", + "digest": "sha256:6e524ce266bcf8506e872410ffd6eddcec45d0919a60dc72a9b715032e9d6103" + }, + { + "id": "ca1a13b8-2400-56fc-8a0b-05bfb69f0004", + "version": "MTc1NjMyNzc4MjgyODExOTAwMA", + "digest": "sha256:6ee6aec39956db9912ca0f18d6cef406be2e4dfe745e7ffe95182e38b8baf317" + }, + { + "id": "ca579f4f-e8d3-5005-a611-c786c4d12028", + "version": "MTc1NjMyNzgyNDI0MDA2ODAwMA", + "digest": "sha256:413eaa7f726502c7370eaf0ce50f911b97c87815cd953a1e78ae8bae179f3fcc" + }, + { + "id": "ca7fcf64-5268-540e-9933-12c3c4e7b4a9", + "version": "MTc1NjMyNzgyMjg4MTQ1ODAwMA", + "digest": "sha256:5b048d1287606304ef89937e2f715d67bf761216f6824b88a7ef7b929afe2aa7" + }, + { + "id": "ca9d013a-416e-50c4-b684-24f377d39325", + "version": "MTc1NjMyNzc3NTEwMjY4ODAwMA", + "digest": "sha256:aa4307899daf6a49307dbd8533502eb7b5346c233236612027ef475fe97351f4" + }, + { + "id": "cac5e3a8-196a-54aa-99e9-c5a22a390701", + "version": "MTc1NjMyNzg3NjUyNjIyMDAwMA", + "digest": "sha256:cc5f2cad5c82c5bb4a8c6ee5816e42b13d5734212412584a833e47f57c44e22c" + }, + { + "id": "cac8a623-ac98-50ad-8995-f7a391d14263", + "version": "MTc1NjMyODE2Mjk4ODY4NjAwMA", + "digest": "sha256:1a962597704514213755cf43fe330c0690a66b8db788c3e585a87c5f2fbc91b0" + }, + { + "id": "caeffc88-961a-5807-b26d-99fdccba4c95", + "version": "MTc1NjMyNzg1MjA3MDM3MzAwMA", + "digest": "sha256:a478ae4133149a16b36d4c9ff07fa99785fe1c2b01ba350b1f244fb81e95692b" + }, + { + "id": "cafd66ab-24bf-5633-aec6-c32edd6f2afc", + "version": "MTc1NjMyNzg3NTUwODY4ODAwMA", + "digest": "sha256:b6c66c11336e0c47267547bf167ad34d424812f3c702624460db60cf65a6b234" + }, + { + "id": "cb0c31ee-a675-50c2-944c-0e4935692e97", + "version": "MTc1NjMyNzc3MDU0ODMyMjAwMA", + "digest": "sha256:d447d312a5279fae3f73b43b8be533a56f074506af78622451271b5663fc0665" + }, + { + "id": "cb25bd85-5fc5-55d9-acf0-d66b2616297e", + "version": "MTc1NjMyNzc4MjczOTIwNTAwMA", + "digest": "sha256:48cd4cc301ae21f009de6bf0ca449a7d77488d3c170aa5e89674ba3509cd6bf1" + }, + { + "id": "cb340d55-3ce7-5167-8275-b51880eea477", + "version": "MTc1NjMyNzc4MTAzODM2NDAwMA", + "digest": "sha256:c8bef7051ece36da1a48bcb74568453f2facee488f709c8b257f61075a095178" + }, + { + "id": "cb66eaf8-214a-5cfa-9885-58592cafc6ec", + "version": "MTc1NjMyODE4MDQ5NjEzNjAwMA", + "digest": "sha256:eca434786a5e960f1d032e3bb3b8be9e5432eedb444dab4b2d8bcdea394cff95" + }, + { + "id": "cb6eeb86-a2e2-50f5-aa3b-0295ece8c5b6", + "version": "MTc1NjMyNzc0MTc0NjIzMzAwMA", + "digest": "sha256:a39c41108697920ded8875e98d71b3640028a00d6a7b2a7dac180eac82114722" + }, + { + "id": "cb832398-3b77-596d-aa36-b84d8615c962", + "version": "MTc1NjMyNzgxMTk4MTUwODAwMA", + "digest": "sha256:24c8a61711acfaac24e15ee2f0463e8f1162dba6d31f5f64fc820975a00cbf79" + }, + { + "id": "cb8d88ab-28d3-5bde-97e0-265771ca9c16", + "version": "MTc1NjMyNzc0NzY5MTkxMjAwMA", + "digest": "sha256:115558fba177ef8c3b2f297a2902ee67da68792dbc389268954ea9cf5ca7fb7e" + }, + { + "id": "cbb92386-05ce-54b8-b483-1333934d3372", + "version": "MTc1NjMyNzgwOTQyMTYwMDAwMA", + "digest": "sha256:8e34a364dc81746bddf03768e1620f20cb1307e3bdd2632c1738d3b6f94a14ca" + }, + { + "id": "cbbacb3b-9a0d-5eec-b190-5dc355a0a5d2", + "version": "MTc1NjMyNzc2MzcyNDgyNTAwMA", + "digest": "sha256:784a47ae745c888dc980b256a1736968b05911ce134cb8077050ef9ccef232ac" + }, + { + "id": "cbd5117c-b302-5db2-a28f-fe91b231f9b2", + "version": "MTc1NjMyNzc0NzcwODg2NDAwMA", + "digest": "sha256:b00ced584e41ceb9114bbdbc41f1530af679f18d374ca5240d7f4ed5cb3f86c0" + }, + { + "id": "cbf742b4-78d8-58c3-9e47-f4505cdbe8d5", + "version": "MTc1NjMyODA2NjUwMjk2NzAwMA", + "digest": "sha256:de095988cee4f4e8fbc4c34e6d23333b3509f0341617a323bb7b3d26f2e91716" + }, + { + "id": "cbf8a9ad-c777-5ccf-aaa6-ca4a634a80ff", + "version": "MTc1NjMyNzg3Njc4MTAwMDAwMA", + "digest": "sha256:d8e83254cb645f62746eaf80bcf60f5ba110ed83e559df2c9a56b0be8a040b26" + }, + { + "id": "cc08f715-dd40-5cc2-90d0-7caf4ee02fa6", + "version": "MTc1NjMyNzg0MTA3MzQ0NzAwMA", + "digest": "sha256:0175e18d4bc2490da2046105d5e0e9db42f72b7dacbdf7b17233ea1899aba71e" + }, + { + "id": "cc28049b-c3fe-57b0-9d8f-442460e509ed", + "version": "MTc1NjMyNzg4MDQxNjM0OTAwMA", + "digest": "sha256:73d3eead84567d7d4599e06505131fc32355f207d9aaaa9d9d83be67fe2a18a1" + }, + { + "id": "cc66e42d-fd42-572a-8915-0067793c9c2f", + "version": "MTc1NjMyNzc3MDYwMzc3MjAwMA", + "digest": "sha256:b4af8a43dc0a2e529e5aacdc7c398c0ffd6aa9be5fb0a101b6a660c9a9c16b20" + }, + { + "id": "cc7d7961-d5f4-55e3-b301-ea27480996e0", + "version": "MTc1NjMyNzg0MTEyMzIyNzAwMA", + "digest": "sha256:ab0f6a3eacf41f9506c843100088918296461fece51eed5d64541d9233c8a4fc" + }, + { + "id": "ccae5c06-8083-5b37-a770-5144a78e5f1f", + "version": "MTc1NjMyNzc5NjA0ODU5NzAwMA", + "digest": "sha256:3f0f2e34be3f7ec319ad6a1277580c2964f2aad0a3a6fe26268aeefd13892c04" + }, + { + "id": "ccb5108d-277e-5439-8cb3-420bce663783", + "version": "MTc1NjMyODE3ODA3OTI4NDAwMA", + "digest": "sha256:177f0ba2c14d1d62d1e20dcbcb3d27097f02ea319da41c6f3c499ce0455e01f8" + }, + { + "id": "ccc6c368-4d19-5c78-b188-7cf62f26c112", + "version": "MTc1NjMyODE4MDUwNjkyMTAwMA", + "digest": "sha256:b25c82616ec2de66d39ab67967c7ec8cc6508b70cbed394345c73176053ed0fc" + }, + { + "id": "cccb876b-d1b1-52a7-8818-6cdd5d8091b1", + "version": "MTc1NjMyODE1ODIwMzYwMzAwMA", + "digest": "sha256:36c4cb0820899fd701d83ce3f43f84dfcda08dc7181d9e13362884e376c1262e" + }, + { + "id": "cccee188-ad4c-58ee-861d-535a2f49a0ae", + "version": "MTc1NjMyNzg1MzIxODA0NDAwMA", + "digest": "sha256:3c1d8c8f3a32a2c7fa9b01d9952b52f6723c8283ca0693cd287082d2c1b96832" + }, + { + "id": "ccfcfba3-8375-52e0-898e-19ed8238e325", + "version": "MTc1NjMyNzc1NDA5MjA1MDAwMA", + "digest": "sha256:069d1007f63160740f9f9bbebefe68891ce69e0766924b1b0f08a8222da14ae7" + }, + { + "id": "cd1be0c6-db3f-5227-81cc-71b3a0584063", + "version": "MTc1NjMyNzc0NzY5NjcwNzAwMA", + "digest": "sha256:17f7b2ac3ba1da15f421486e8afb45279bd80e7c5701675fd0a10940f93f8011" + }, + { + "id": "cd1e5855-31fe-5a95-b9bd-10d96ab22938", + "version": "MTc1NjMyNzg2NjYwNjM2MzAwMA", + "digest": "sha256:3cada396fc1cf08b87ab5e29caf734e172c14117abd92f7fb7706b44ab68c7e4" + }, + { + "id": "cd3dcb7b-d279-59e1-84aa-c428ca3c2706", + "version": "MTc1NjMyNzg2Njc0MTE3OTAwMA", + "digest": "sha256:1850a75dea6455e2d1799f1a647a8e338a8bad610f98ad6dd563c23cdad6348a" + }, + { + "id": "cd76e8fa-8971-54f8-8ac0-2c4cddd41ddd", + "version": "MTc1NjMyNzc5MDMwNzE0NzAwMA", + "digest": "sha256:ef962335ff676554b374a66e57ae9ed1e05c2e7ffef13e76734c8e159669af93" + }, + { + "id": "cd8d515c-3af1-5e62-8cfa-0e53184d1170", + "version": "MTc1NjMyODE2MzAxNTM5NjAwMA", + "digest": "sha256:93be729c5b1c025df1389813e3668bb500069ac79c70af340a7b0ef1e1afc48b" + }, + { + "id": "cd934330-7811-55d2-9a8f-5fa7755abfc6", + "version": "MTc1NjMyNzg2OTUwMTczODAwMA", + "digest": "sha256:609cb68133aacedd4177fb4997e98fa21becedc0e316e85b332edcc3a05b22fb" + }, + { + "id": "cdd56d44-bfad-52ed-9746-9d1dec21f56a", + "version": "MTc1NjMyNzc4NzM1Nzc4ODAwMA", + "digest": "sha256:1c1d708f5c5f5ac3abf7f0abc1e23d81e0624dd62d03cda0370f276115d5f905" + }, + { + "id": "cdfd5dc8-af57-5a84-ad41-d6b43cc7b64f", + "version": "MTc1NjMyNzc3MDQwMDQyNDAwMA", + "digest": "sha256:2f5a69c3963f7eafe52c5b08939931962fdcfadbb49375af3a85d529667ccf2e" + }, + { + "id": "ce034379-dace-5bb9-8b7f-c84ccb665c6c", + "version": "MTc1NjMyNzc4MTk4MDAzNjAwMA", + "digest": "sha256:639392cff4d27002fc65e0da72bfb0ecc5e3fd11a43adf9fa1ed7e7be308faae" + }, + { + "id": "ce07f1dc-6361-512e-ad4e-8758a2bae529", + "version": "MTc1NjMyNzg0Nzg2ODYyNDAwMA", + "digest": "sha256:46c12da63f4526b9d92ef2efa1e56335b37ab76121ab5bf89de39a626a8f0926" + }, + { + "id": "ce1b244b-ef5e-5bc4-bd01-8e73f90564b0", + "version": "MTc1NjMyODA2NTY0MzQ2NzAwMA", + "digest": "sha256:ecfcffaa05c92b4f474da62947c29ce1607feee6aa3dd33aba00c9f3749157c8" + }, + { + "id": "ce55754f-e40e-5622-a623-8ff4128697db", + "version": "MTc1NjMyNzc3NDU0Njg4MzAwMA", + "digest": "sha256:8e9b25d987821a025efdc10d668102e8754e28d92d32deb710d4bd54e67a22e6" + }, + { + "id": "ce55ba9d-766e-5ccc-8e2c-c120f4ed3bad", + "version": "MTc1NjMyNzc1NzQ0ODM0NzAwMA", + "digest": "sha256:944a9139b807716c5b25a738e4762543d7d6aefe3b3a96cebcaa2a1648d63842" + }, + { + "id": "ce8d313b-33f5-526f-8c8c-a76a488b6912", + "version": "MTc1NjMyNzkwNDg3OTIxMTAwMA", + "digest": "sha256:08f3dd9125e70d8c530fc4bfd6fd90a570a51e99eb594d05e9b024d2c1093a90" + }, + { + "id": "ce9ac74c-7ddf-5b3a-ac84-0780f2ca7f0c", + "version": "MTc1NjMyNzg1NTgwOTk1NTAwMA", + "digest": "sha256:6fd4656d7112f3244c7ffe2a9715b87fa15fc177f891516b049aa43de6e02d5e" + }, + { + "id": "ce9e27ac-78dd-5795-a9a4-ae28f206ede0", + "version": "MTc1NjMyNzg3MTY1NDkwNzAwMA", + "digest": "sha256:f82472ab93e5ae8e0464cd58ecce7ebb60cba251ba2ce019fa44714c858ec398" + }, + { + "id": "cea66e99-10c1-5265-a100-c1f8fe9d8d5a", + "version": "MTc1NjMyODA2NjAxOTE5NzAwMA", + "digest": "sha256:e52d9d628f46fa72b9bc5c5bce39927f7f13d6fefaf4201ace5843397536c12d" + }, + { + "id": "cedbc637-1105-57e7-b5b5-8dff316a21da", + "version": "MTc1NjMyNzgzNjI1MzQ0ODAwMA", + "digest": "sha256:625fb7d0751d85157a3f89e0f3a36725d55258de13ec6f221668392cd1267c43" + }, + { + "id": "cee639cf-ec34-5faa-8a3b-a31d94ba81e3", + "version": "MTc1NjMyNzc0MjMyNTAxMTAwMA", + "digest": "sha256:a6cc350bb8b9402cee270899e5d3fabed4e97005f7475c3c6406b3bfa4a17f26" + }, + { + "id": "cee840b0-1fa8-545a-889e-160e6d1bc9c7", + "version": "MTc1NjMyODE3NDM4MjY2MTAwMA", + "digest": "sha256:6fd3e29158975ccdafa4ae96999cbdd1ddfcae1e4ee4d5aaa53c46c813976027" + }, + { + "id": "cf01754f-e6f0-5952-8d41-d311be81ead8", + "version": "MTc1NjMyODE3ODAyMzQwMzAwMA", + "digest": "sha256:785c8ca94233a394612a5f76b4006728badd99d18b6125a92a10098454d58473" + }, + { + "id": "cf0284c8-fcd8-5822-aaef-a6ad43018a09", + "version": "MTc1NjMyNzc3MDQzODI2MTAwMA", + "digest": "sha256:fb896f76ad4fffc2c169a31c45fd81fd6dddeb45e3ac38b23ac63e5dd6c08af7" + }, + { + "id": "cf0d49a9-e460-5b79-87fc-5c828e9daed9", + "version": "MTc1NjMyODE2OTAxOTQ0NTAwMA", + "digest": "sha256:2ee373d93a9dcd225ea78dc782e31549d5b29416d4c4424c7bda56aa9b8dd877" + }, + { + "id": "cf0fe7a8-5130-53a5-8437-f6ebfaffd110", + "version": "MTc1NjMyNzc1MzY2ODc2MTAwMA", + "digest": "sha256:50b7975f8ac1c14a4c7a39af356e30cd66cf65b01032bd4c5e06b583648d111e" + }, + { + "id": "cf1e9c2d-7784-5695-84c1-018c160901a5", + "version": "MTc1NjMyNzgxMjA3MzQ5NzAwMA", + "digest": "sha256:967b1d533f3e13bc57a9aa2a1ad530798602610ca63390d191e702eabee02a2b" + }, + { + "id": "cf2fad28-a7bd-50d2-9e89-871803a5b8d4", + "version": "MTc1NjMyNzc2MzcyNTE1OTAwMA", + "digest": "sha256:1ce5763b583273efa1bbed7c4e005b4ef514b8eb1951a57c9439836cc877bd10" + }, + { + "id": "cf3db83b-115a-5b76-bf39-b4c32c7e481c", + "version": "MTc1NjMyNzg0NzgzNTU4MDAwMA", + "digest": "sha256:089f109973ee596764aac9099c984edb7071a1483afff3b1643cfbe96cdbb567" + }, + { + "id": "cf428801-b548-5020-9814-85d5ca9209af", + "version": "MTc1NjMyNzc3MTU1MDE1MjAwMA", + "digest": "sha256:88adf1078841abd860859c20dc8720d6592a93b225b85146aea04de075a1e0c5" + }, + { + "id": "cf43cbc1-b34a-5943-9df3-87a10c7e9173", + "version": "MTc1NjMyNzc4NjkzNTQ2NTAwMA", + "digest": "sha256:a3c2e85dc0d67d8d9bac206a5ecebd7f8c2e3b8246ac2a9d34e6bc07aea4fb12" + }, + { + "id": "cf4e8351-c913-5363-8001-92559f143c5e", + "version": "MTc1NjMyNzc3NDYyODcxNTAwMA", + "digest": "sha256:ac20c10613ddb499e300521df2d93b14d977e8d9121184a85cfe946c2e26fe2e" + }, + { + "id": "cf5f54c4-1d97-5d2b-aca4-b003a565e85f", + "version": "MTc1NjMyNzc1Njk1MDEyNDAwMA", + "digest": "sha256:cba8f8152b3b9d737a76173cd7c31bef1922290fbaeb6ce0ec3283246a4618a9" + }, + { + "id": "cf614a0a-a008-5375-a8b4-5c2456a8e0c0", + "version": "MTc1NjMyNzc3NTE0MDQ1MDAwMA", + "digest": "sha256:6a12c47019b52235c71748c416fcd805428415f9d5d16c84cfc3c4463e20b948" + }, + { + "id": "cf685029-7231-5cb2-a395-836a5f1886e8", + "version": "MTc1NjMyODE2OTk3ODUwNjAwMA", + "digest": "sha256:b4459465eacd8ef40492a23d873e3b9fd0d1227aeb79f347aae9a56e1de29398" + }, + { + "id": "cf9319eb-6ce1-5c53-ac3a-fe1bb2c50997", + "version": "MTc1NjMyNzczODQ3NzQxMzAwMA", + "digest": "sha256:f960d4b96def15d4681a3b8633d1c66b8bdf3edde8135188dec51ebdb38d4342" + }, + { + "id": "cfb07052-2b7f-51e0-9d1e-0b1e07bcbe15", + "version": "MTc1NjMyNzkwNTAyMDg0MzAwMA", + "digest": "sha256:740b8960ff7e1c767bbd8a6f6f9283e3f7007ba84cd0c0c83d7f937dd09ae1a8" + }, + { + "id": "cfda17d3-f882-5b9b-ad96-ff9bd8fb15a3", + "version": "MTc1NjMyNzg3MTkxNjI3MDAwMA", + "digest": "sha256:857894cb0771e785a87196976cda873d6d52f2149f5e73ca03d535a0e43db11b" + }, + { + "id": "cff80196-ad71-52cf-a6ae-5abb2a159838", + "version": "MTc1NjMyNzc4MTk2NjA2NTAwMA", + "digest": "sha256:6641df92dc179a8611634fb79a5634e0bd52a5504831e64c400dff905c3c79cb" + }, + { + "id": "d002095a-5fdf-57b7-8198-505644ea9fdf", + "version": "MTc1NjMyNzg3NDkzOTA2NzAwMA", + "digest": "sha256:d2046df9c839de10a8f5a36fd6a2c67f3b365663c2a9136bedb0ca5a6aa5f670" + }, + { + "id": "d061b048-d4a5-5d1c-870a-7ea46f9b7d0c", + "version": "MTc1NjMyNzc4MTk5ODQzODAwMA", + "digest": "sha256:d7be8bef727d30fa11cb055648efb482c4f108f19213d1105dfb168d2a76e470" + }, + { + "id": "d0663349-25dd-586f-8916-8f4286dcce49", + "version": "MTc1NjMyNzkyODUzMzY1MTAwMA", + "digest": "sha256:295ea73dac11968f73756dcfa8e668503d0db5d6229360820712d2d488200fe0" + }, + { + "id": "d06b006a-ec4a-5401-b2fe-5c1d45e0300f", + "version": "MTc1NjMyNzc2Nzc3MTE3MDAwMA", + "digest": "sha256:127b4dfcafc525d2a5cd9baf1cdcadadb6483a6d944d5b53c2a976a80abbf6e2" + }, + { + "id": "d084cf3d-fc23-5c72-b2e1-12070a4c83f6", + "version": "MTc1NjMyNzc3MjU4ODA2MDAwMA", + "digest": "sha256:29bc1f78c0d485dcb52d9e812a52eb26d2a01e45cb5666ecdc627c350df81463" + }, + { + "id": "d085e314-981a-5a3d-8fb8-685b67f2d36a", + "version": "MTc1NjMyNzc1MDAzNzkxNDAwMA", + "digest": "sha256:d1ca1a184b5f3802df6ef404e77c29485ce217cd80ca0cf747d4813b2a8f2398" + }, + { + "id": "d0866ae4-110e-5696-aa56-16379b0384a0", + "version": "MTc1NjMyNzc4MjAxNDA3NTAwMA", + "digest": "sha256:466b7620fda11b7bb19062dce9eb1b3b63be2febafa8ade8b0fcd6db7b700e84" + }, + { + "id": "d0936f91-9e1b-52d0-be9a-98acc51217e1", + "version": "MTc1NjMyNzc2NjQyMDk4ODAwMA", + "digest": "sha256:96814781677ca6a88e76e9c605e171155b3e2ade9b1f9cde82c24e3b4807edc2" + }, + { + "id": "d0937a19-7e19-52a0-ae06-027938ae0982", + "version": "MTc1NjMyNzc1MDEwMzMxNDAwMA", + "digest": "sha256:e165b6937f7e6452572bf6bf52a01df1efe5496a60c371300f9dfa734ac540d6" + }, + { + "id": "d0b948eb-15cd-517d-b43b-0dfd1a350c94", + "version": "MTc1NjMyODE3NDQ0MzYxMDAwMA", + "digest": "sha256:ccb99082306e73fed243c8ebdc9322a86d55eb8edaf558738c7880c48e27b721" + }, + { + "id": "d0c945c7-1c8b-52c9-ab3b-51d21c231531", + "version": "MTc1NjMyNzc3MzAzNjYwNTAwMA", + "digest": "sha256:cd1376bbf01f917b0a99537b98df4a7bff6e39b1843f4dc72b9c694e312e341d" + }, + { + "id": "d10f6795-4c19-5177-93fa-1c36026628c3", + "version": "MTc1NjMyNzgxNTY0NDU1MDAwMA", + "digest": "sha256:f7d8560a1213c3d626fa4a233dd03b4f025d659acd56a4596670070804699463" + }, + { + "id": "d11602a0-9ac3-59a2-9d60-cc5245c4b4f1", + "version": "MTc1NjMyNzc1NjA4OTY4NzAwMA", + "digest": "sha256:c64ebce2db37612f4a4eb32f31a5bbc9c58f364e4d605a81ffa90784bc34ebf7" + }, + { + "id": "d133e53e-ded1-5c2b-8450-5b0ffe227399", + "version": "MTc1NjMyNzc1MzcyNDMxNDAwMA", + "digest": "sha256:966629d52b7a41107c70e81819dfda6a668f2349bc956b6b8963b84c9dbf0c5c" + }, + { + "id": "d139c1c9-7945-5b32-ba2a-23704321ce70", + "version": "MTc1NjMyNzg1NTY5NDE4MjAwMA", + "digest": "sha256:3004eb23a1621ca277cbeef22e9f188e82549773c20b4efdbd628360b3c42ec2" + }, + { + "id": "d158281c-70b3-5010-8fa8-b039ef4f89a3", + "version": "MTc1NjMyODE2OTk1NjAyMjAwMA", + "digest": "sha256:4c67426fe4a26fddb5b5bb1acd118beeff885cebcce21853f74c53c919496ff4" + }, + { + "id": "d169c219-d281-597e-b1c6-d4ae12d2b66f", + "version": "MTc1NjMyNzc1NjA5MjI0MTAwMA", + "digest": "sha256:cfdca77b97f36a9829e1f9ab062ff7fc7e1e35fb6f997db04edcc07defbab7d8" + }, + { + "id": "d174fabd-81c1-51e0-b8f7-952e49728020", + "version": "MTc1NjMyNzgyNjQwMTk3MTAwMA", + "digest": "sha256:ec0d73cead4ef6496672d636ecec38bac5d4807ea219b8a50b2133b3a262eed9" + }, + { + "id": "d17dd130-c907-5c89-b71c-ce5933658380", + "version": "MTc1NjMyNzgyNjU3Nzc1MzAwMA", + "digest": "sha256:430633a03f83848126bb88c31e41594d6d650af805bb1ca46142f6e417d9df15" + }, + { + "id": "d19640f9-b0c9-5a8b-a12d-8fff531c73ba", + "version": "MTc1NjMyNzg1NDQ1NzExMzAwMA", + "digest": "sha256:a56a75acf2f1e797d7faafa9d3739a052ed4dc627a7852014225154805cb0aa4" + }, + { + "id": "d19971b0-7816-52a0-b8b1-8d4ab65e3e28", + "version": "MTc1NjMyNzg1MjcyNTM4NDAwMA", + "digest": "sha256:3588d191245360e74f6c7dbe4c50e21a33f4b0783b56ff19582a27890af045d4" + }, + { + "id": "d1cb018f-cba0-52f2-a0b8-cd5ea56638e7", + "version": "MTc1NjMyNzkwODM5OTYwMTAwMA", + "digest": "sha256:aabff878e618919d7cb7cc1da8597f282398e119be26627d5cef3e0eb38de284" + }, + { + "id": "d1e3d6c0-f3c9-5c63-8675-d6d2819847ec", + "version": "MTc1NjMyNzgyMjgzNDYyMjAwMA", + "digest": "sha256:8dcb3269536a79814213bd3cb6a0d7037d4b44f6c408b047caa2d81bd90903dd" + }, + { + "id": "d1fa1f27-ef5f-5ee6-bad0-b6847dbb4389", + "version": "MTc1NjMyNzgxMzU4MjAwMjAwMA", + "digest": "sha256:dc5839b28e7c5f485a6e8da5a5150d3fccdd95dbd18ac272a6a3f46605c23db1" + }, + { + "id": "d20c003f-1111-5ac0-b35b-0f10da19c44d", + "version": "MTc1NjMyNzc4MjcxNzkxNjAwMA", + "digest": "sha256:e9589bd93fd70c58bcc70ed131d505d19e8865865fab78f217fa264a09e49c14" + }, + { + "id": "d2104335-6aef-53f9-8cbc-1c95a5a8649a", + "version": "MTc1NjMyNzg0MTEzNjYzODAwMA", + "digest": "sha256:0f91593c459170d4d0bced3fd0e89cfdaebbdcc51f76923f1cec9a74f1bf0311" + }, + { + "id": "d2225495-3e62-57af-b495-e83df6af1d96", + "version": "MTc1NjMyNzg2MDI4MTkzNzAwMA", + "digest": "sha256:8fba847dcc774b15685603a0c84e9380804e68615a4f3afdc5d802e4467c12b9" + }, + { + "id": "d2376a12-1ee7-575e-8279-00cbc31000c9", + "version": "MTc1NjMyNzg1MjgzMDM3NDAwMA", + "digest": "sha256:24433fd7be4768b68c6705257487a3ed06fb82ce26111be33b4d2668ea9c2625" + }, + { + "id": "d23c5c02-a403-5160-9e2e-cbb3118077cb", + "version": "MTc1NjMyNzc1MzYzNjY1MTAwMA", + "digest": "sha256:48381e3a782604ebf4b5bf90511751fe59baf2bc8e91c8f97f399d391e6c782e" + }, + { + "id": "d24cb307-ffb2-5b4c-a285-ffeafac12cd9", + "version": "MTc1NjMyNzkzNTc0OTk2MDAwMA", + "digest": "sha256:01443af26828f1024a58fb4add31c3777be595683e40a3eff3119d417251f220" + }, + { + "id": "d24ce490-a560-56ed-89d6-3e7b5d18272a", + "version": "MTc1NjMyODA2OTk0MzUyMDAwMA", + "digest": "sha256:9de62190e79cc9468e97c4dcd1fbf1c0aea25b8583e9a8e239873a1fa70192d6" + }, + { + "id": "d29f96f5-88e1-5fc7-babb-57de0aff560d", + "version": "MTc1NjMyNzc1NzQ1OTI0MTAwMA", + "digest": "sha256:275e0b94cdaecd4935bb79a4ae8af4d7250d4d69745a5be5fb3b21618f4fa143" + }, + { + "id": "d2d6785a-dcad-5447-b38b-d43eb97e3ad8", + "version": "MTc1NjMyODE4MjEwNTQwNjAwMA", + "digest": "sha256:20ea2ffe94319d39473acb220a024d4d17d891c381ecca09e0b779b413331bcd" + }, + { + "id": "d31bf4be-9f5f-508f-920d-ad8e9cbd4004", + "version": "MTc1NjMyNzc1Mzk5OTYwNDAwMA", + "digest": "sha256:facbba4109eb1ece2c4923b5542a0d04d65ed38bf2ee8f3fdfbb927db40de2c8" + }, + { + "id": "d342b625-47b7-58b9-a873-190269a3f0dd", + "version": "MTc1NjMyODE2Njk1ODU2MTAwMA", + "digest": "sha256:535495d80a2c04209aeccbbabdf07df90ce33784b3f648d077f31e3b913e3a8c" + }, + { + "id": "d3466261-2100-50b7-9f79-dc4305e4511a", + "version": "MTc1NjMyNzc5NjA0MzI1MjAwMA", + "digest": "sha256:e449f90269a29339bd2686f52346358679e2bba99ed6f8002e291391a9f2f7c5" + }, + { + "id": "d35c0127-c637-57ff-8f7f-aa2a2077bbe7", + "version": "MTc1NjMyNzc4NjkzNTAxMjAwMA", + "digest": "sha256:83953cabddb57b5f6ddb678780c1f646b39885eacc44bcc831b75fd98c434889" + }, + { + "id": "d36f0d6c-e8a8-5bf7-8716-33cb62bd1364", + "version": "MTc1NjMyNzc0NzcyNjk2MjAwMA", + "digest": "sha256:cfd8f95c6ef26f31b9ee72898339f830b62a8fc035652d858b09566e0dd9d560" + }, + { + "id": "d3882a58-75f9-5c33-823a-d8920d445701", + "version": "MTc1NjMyNzc5NDgzNjcxMTAwMA", + "digest": "sha256:6fd020470265fa64e99e0edbd62858d03c985798b9ae5776bad2de7dc70a2178" + }, + { + "id": "d38b67be-5670-563e-ad90-e6abb1c924a5", + "version": "MTc1NjMyNzc0MjUwNjkyMDAwMA", + "digest": "sha256:c47358a568c65a5b90ba47635c3157c68f91c263bb348b180ecf51bbbbc8cd0f" + }, + { + "id": "d3aa2496-fd61-5615-a9b7-20c6425353a2", + "version": "MTc1NjMyNzc5NTE1OTQ2NjAwMA", + "digest": "sha256:dda2bcc481430918d1d3040a2f5c2673d3487df5844345408be7adeb9e7a74f9" + }, + { + "id": "d3dea22b-e746-5dba-b970-7b7c37ba6fe6", + "version": "MTc1NjMyNzc5NTE5MDA3NjAwMA", + "digest": "sha256:11d0539c55d5647fb010fbd50505eb52e7b1f629bb6dc91aabd0e901aa573b67" + }, + { + "id": "d3f1a4ea-b9ff-5186-992d-9275f501b86f", + "version": "MTc1NjMyNzc1NjA3MzQ3NjAwMA", + "digest": "sha256:89ca11dfd3be6646f84519ea997dfcb5771ed223746ccadfae1450553d4c7a51" + }, + { + "id": "d40ee4e6-ca25-5f8e-81d1-3db2bcc008f0", + "version": "MTc1NjMyODE2NjkyNjU2OTAwMA", + "digest": "sha256:ce685d9755c30b92527c0896a0ca830ae5a74fcd74c507817b606ecb59ad950a" + }, + { + "id": "d414d41b-0ee0-5d8b-b776-b852b7a90c5c", + "version": "MTc1NjMyNzc2MjQ0ODA2MjAwMA", + "digest": "sha256:155a30116570649f2415491a8605aea51d64f5fc6189a22c7af975b52f8e2e43" + }, + { + "id": "d41f7fe5-5eae-5cf1-93f5-e3e41cba735c", + "version": "MTc1NjMyNzc0NTY5NDg1MzAwMA", + "digest": "sha256:d30ed0cfcc1096a0c8ee77ecd5bd3ee8d99021a8c706b7e69784100d1e35a66e" + }, + { + "id": "d42a725e-e6be-5e45-bb70-667212fd4c6d", + "version": "MTc1NjMyODE2MzA3MDEwODAwMA", + "digest": "sha256:11b474d3bc9795b20ffe7552c354322e35ab0ba54be68619f2c797ac56b60c02" + }, + { + "id": "d45a43b3-23e3-58e2-aabb-3cb859259792", + "version": "MTc1NjMyNzc0NzY5MjA2MjAwMA", + "digest": "sha256:41cd0e4bf96affdd35f0a9c7a12a10827bcfd31f008aade21b5478c4c6b9e4d1" + }, + { + "id": "d474ac13-dfd6-55f9-b465-837211037dee", + "version": "MTc1NjMyNzgwNDU4ODk1MjAwMA", + "digest": "sha256:d07e78896c4317b52a437b5332ddb5e8ac0b06ee5b3c34eb90465c615bf05e77" + }, + { + "id": "d47e9c3d-59d9-5574-a96e-61a41a8c1f55", + "version": "MTc1NjMyNzgxMTkwNDk2NjAwMA", + "digest": "sha256:6c309bdfc45ed031090539216b36dcd21eabc69a2973eedb45c93879c29530a8" + }, + { + "id": "d481715f-fb13-5328-9ecd-486b2fbdc935", + "version": "MTc1NjMyNzc4MTAzODEwMTAwMA", + "digest": "sha256:80685e75db2d54351ca118ca5a5aaf745342973148598d37a97f67623571e0c9" + }, + { + "id": "d486b1a9-352b-5d6c-8aa8-4e09ae86168c", + "version": "MTc1NjMyNzg1MzM3NTk5MTAwMA", + "digest": "sha256:22b2e3aa2850c944d58d8687f797e8d3a87a33ecc352c3821214110cc6593a09" + }, + { + "id": "d4a61a5b-5058-566e-b25d-9ad7910c3bdb", + "version": "MTc1NjMyNzgwOTI5OTcyODAwMA", + "digest": "sha256:66f8fd41bc09efbc3d6adf183d4a3b42746db18b1732511491cab9ae213bd778" + }, + { + "id": "d4d183fa-3362-5a88-9199-251e02ec676d", + "version": "MTc1NjMyNzg2NjMxMDY3OTAwMA", + "digest": "sha256:9bad66468d81748d9995e2082debac6bc62c9018671edf755dbbf361bb048ae4" + }, + { + "id": "d4de3e69-4efe-54c2-b54e-f34e5b3076d9", + "version": "MTc1NjMyNzc4MTg5OTgxMDAwMA", + "digest": "sha256:7321eecc4a36073294dc1e440486a5fe1b116e91c34da0db78f58d67df5ec0e2" + }, + { + "id": "d4f9ed03-debf-5bf1-b8e0-0e5d2d69ae58", + "version": "MTc1NjMyNzg0MTIzNTExNTAwMA", + "digest": "sha256:d2ab7add05bf35d4d501162a0074176a5441a0a5e2eb4f3d7a8de44e9d0bc7fd" + }, + { + "id": "d4fe6dd5-43a1-5fb7-935f-db787a10ac50", + "version": "MTc1NjMyNzg1MzY4ODY1MDAwMA", + "digest": "sha256:641bc7ab9fe98c72ad93d7d91786bb21b560cb1eda7eff1756aa9789f2615dc8" + }, + { + "id": "d503106b-51ee-5f35-a05a-c2703d153b7a", + "version": "MTc1NjMyNzc4NzM0MTg1NDAwMA", + "digest": "sha256:fd64e8067565b9321bf40aaf5af4aef452844c469498537a004b5ddf0598a8bd" + }, + { + "id": "d523c7e8-b561-5dab-ac1a-1e132d53552f", + "version": "MTc1NjMyNzc3MTUxNTA3MjAwMA", + "digest": "sha256:023ac5bc6dc2bd88a965f27873031e1a7e2021a677b0f36f9016b55390b7ddd4" + }, + { + "id": "d535f07b-6848-57f7-be43-75ffbb813720", + "version": "MTc1NjMyNzkwNDk3NTE0NjAwMA", + "digest": "sha256:c0c8936d4af168f2d9e332e11171a5c8a3fe0e7ff4ccdee9f1d552859a26bf83" + }, + { + "id": "d549a3bc-a1f3-560c-beb2-68ed8476ec54", + "version": "MTc1NjMyNzgzNjI1MjU4MDAwMA", + "digest": "sha256:85862dccbd847f450c7a1d53c0d535fabdf04681a4bc768abcc5da4b84ebb831" + }, + { + "id": "d554abf4-b898-52f5-a4a2-812ad7abe353", + "version": "MTc1NjMyODE2MzAzNDg5NzAwMA", + "digest": "sha256:72caef86f4418c18c5d5989d01763baa23be37bb2e952fb1a61decf4c6604fec" + }, + { + "id": "d55925d8-8a88-501d-99e3-1c1ac769d289", + "version": "MTc1NjMyNzg1MjA4NjEzMzAwMA", + "digest": "sha256:61e21bdeab082513b515f50760fd916127b6bd7f070b87f6db8e09eac6ad2e00" + }, + { + "id": "d55e02a6-7a1f-5346-9632-3becb4d32527", + "version": "MTc1NjMyNzg4NjU4MDI1MzAwMA", + "digest": "sha256:d5efc59a4f4200cb89ed0727e7f7b2d13edf4cf05ae03597eeb2391a2239c8e2" + }, + { + "id": "d57347a9-435a-5acd-8e85-f828a4807eec", + "version": "MTc1NjMyNzc1MzYxODk0OTAwMA", + "digest": "sha256:08f4fd492133cd37033735f61d8a39b2e56502d88381faabda92893a7d04f67a" + }, + { + "id": "d5b8315d-722b-5414-8385-cd27db05c840", + "version": "MTc1NjMyNzc4Njk0NTAwNTAwMA", + "digest": "sha256:c4eb23e3249581fc19d2e1667244ad09c2720d7844416bf59cef3f73aa59f02a" + }, + { + "id": "d5bb5ff7-5a2a-5496-bf63-8a1a5b3b3a91", + "version": "MTc1NjMyNzg3NjU1ODM2NjAwMA", + "digest": "sha256:4280550e44f2d37ef6457f85b0cfdf5cbc1e750ec706cd2b2fca58b8967088f3" + }, + { + "id": "d5bee712-f851-5a8c-b7de-13c94c29034a", + "version": "MTc1NjMyODE3NjE5OTA2MzAwMA", + "digest": "sha256:77e3d3bf8c5315d005e0573fdfaeb8fd1e488eb06cca9f6009926c718e856eca" + }, + { + "id": "d5ce9e86-7c4a-505f-a989-78aa68d2f384", + "version": "MTc1NjMyNzgzODkzODMzMDAwMA", + "digest": "sha256:b6bbe8a5fd0eda23b05d7cb02acc14ac9650d7f2466b353961c55c46d2f10a25" + }, + { + "id": "d5d9948f-2eda-55fe-998a-cb0608d9a65a", + "version": "MTc1NjMyNzc5NTM3ODM0NjAwMA", + "digest": "sha256:eda4430a2f602e7fcb6ef07c78e9b21dea23daeaaccfc311fd1a953ad8da0bf7" + }, + { + "id": "d609e76a-c536-5a25-989c-922b6239ac51", + "version": "MTc1NjMyODE4MjE1MTI5NjAwMA", + "digest": "sha256:452c9e969b87641b87daa877bda7118c0ab9dd3147aec655ddcd309e7ca86a2a" + }, + { + "id": "d610ebce-bc48-5a7e-9905-d88535e79848", + "version": "MTc1NjMyODA2OTQ2NTk3OTAwMA", + "digest": "sha256:b54360e2da4354e5eb9163cee54210bb1b639ce34ca6a9cf0bb3474a4b8e61ae" + }, + { + "id": "d6146e03-523d-55c8-a26f-4ee6f8e63441", + "version": "MTc1NjMyNzc4NzA2MzE3OTAwMA", + "digest": "sha256:673b39e1aec6a0b4f09514b2334bd00e393e324da130bf2b6bb462c0f88597ad" + }, + { + "id": "d6161d4b-79b7-5161-ae09-6fca87dfcb37", + "version": "MTc1NjMyNzc4MTk0MTY1NzAwMA", + "digest": "sha256:f48fdb98be581193960ea75d4ce9fb225ea23c744bd5691c7897e027109413b5" + }, + { + "id": "d6214bbf-e560-5cc0-8ada-d63a187ea546", + "version": "MTc1NjMyNzc5NDc3NTIyOTAwMA", + "digest": "sha256:0ea4ce3f2cd8e58a935431f44b3c054fe35d781e1c9f0c8a9c29ee46404e5786" + }, + { + "id": "d62d32c9-2510-52c7-8c52-402285c36c95", + "version": "MTc1NjMyODE3MTA4Nzc4MzAwMA", + "digest": "sha256:281047c3be542bc212d94dc892e1d571299069825162ca922e305e8d61b5a8f5" + }, + { + "id": "d63fe321-4c32-5c9b-8e97-3bb25933b517", + "version": "MTc1NjMyNzc5NDY2MTM4OTAwMA", + "digest": "sha256:5dfacc624c2cf4d13f4764dfc7b275fc407bf97c9d1aae5a0776e4c37f3466fe" + }, + { + "id": "d657e9a2-9ffe-50d2-a7c1-72d32fe47de2", + "version": "MTc1NjMyNzg4NjU0MTI1NjAwMA", + "digest": "sha256:984f5aa2c01af4ec7c3c76272b4de5f90ce9027f931076abfb2cfd84a6ee1f15" + }, + { + "id": "d65f634e-a367-5ce2-a82f-61d88f848e2b", + "version": "MTc1NjMyNzc3MjUyMzEwNjAwMA", + "digest": "sha256:f09ce14f61e05850fa63d9d35915b58e479cfd9f5383162c62ec0a0c8f13878e" + }, + { + "id": "d6793963-82b4-5f68-988a-3f5d1cbb47cc", + "version": "MTc1NjMyODE3ODA0MTI4OTAwMA", + "digest": "sha256:4f165f2d844c43c9e87e229a3520976339b1f74ebd7a63b49d691e681588988d" + }, + { + "id": "d697598e-2750-5b4b-a071-f1c917e552e8", + "version": "MTc1NjMyODE3NjE4ODQ3MjAwMA", + "digest": "sha256:0cdd9a92844f47db02d7689ee78748bc7b637966f640a15204d9b47e4237390a" + }, + { + "id": "d6a834b8-5876-57c7-8c25-a4ff06a12945", + "version": "MTc1NjMyNzc0NDUyNDQ4NDAwMA", + "digest": "sha256:13f5e9a9b052e0306cb983dfdfa031066063fe5b5e847697e600fbe3b19a40eb" + }, + { + "id": "d6b2f04e-fe85-565d-be45-0e13604aa8de", + "version": "MTc1NjMyODE4MDQwOTMzMzAwMA", + "digest": "sha256:7c545c6e3f958054a814c1be49202b42e9f453ae54721b5c7706a284f2dd0cd2" + }, + { + "id": "d6cbe0cd-642c-50c0-80cc-565460a7e46d", + "version": "MTc1NjMyNzgxMjY4MTU5NjAwMA", + "digest": "sha256:742f7e9ffb1185a2c56f6325cf5d517e038ee36a8881358c1dc65cdecc62fd5f" + }, + { + "id": "d6cdcff5-4247-5244-8fbf-901ada737964", + "version": "MTc1NjMyODA3MDE3NTAxNzAwMA", + "digest": "sha256:641fd6c7727abedf220b855660c128b65e797191a47e73776f44e986d3919050" + }, + { + "id": "d6d0ebe1-8d3c-5fa5-9da1-3701212af650", + "version": "MTc1NjMyODE2NTgyNDUzODAwMA", + "digest": "sha256:5510f5de9654be2cd629214cab37247c1a47bf6a2b43822725f8dfa631b8a0bf" + }, + { + "id": "d7070f04-d756-5f6e-80c5-e83990e45912", + "version": "MTc1NjMyNzgxNjM5NDUzNzAwMA", + "digest": "sha256:59d703ec425d1b4e3454ade5506dbc3f469b09727ebd150ed556ec5bc834d532" + }, + { + "id": "d70db90e-353f-5778-8e63-df72d95e2ab2", + "version": "MTc1NjMyODE3ODAwNDA3NjAwMA", + "digest": "sha256:46530c7d78eb45ab396e2e19459a0b8d6e30c14a3491ef72bb5aafc1688900bb" + }, + { + "id": "d7121832-3681-52a1-85e5-2f98a6ad5559", + "version": "MTc1NjMyNzgyMjgxMDc1MzAwMA", + "digest": "sha256:64811e73b4b9b7b4fc9c3cfcc588d69d77d06ec690982abd11072f9f79f0ac8a" + }, + { + "id": "d720f3f3-87c6-530f-977b-6309a9d9fa46", + "version": "MTc1NjMyNzgzNjMwOTk4MjAwMA", + "digest": "sha256:bf29264b64edc1cd9873abb1ce682692550475e51a59a28d5f807608bd8172cb" + }, + { + "id": "d7227563-0075-5780-a20d-6ce590b05ea8", + "version": "MTc1NjMyNzgzNTIzMjAyMTAwMA", + "digest": "sha256:3ebd12349290734f865d155337438a6a58ad45d3e1b496133a1933247aab7500" + }, + { + "id": "d73a68e0-3abb-5353-8186-5aa21b5090a2", + "version": "MTc1NjMyNzg1MzIwOTU5NzAwMA", + "digest": "sha256:38b2fd7e2f78065a08ff360c51a0ed6d35ecce6f77f454ce45374ee741e5551c" + }, + { + "id": "d740ec6e-0f69-54a7-abd4-5900215e46a9", + "version": "MTc1NjMyNzg1MjEyNzA4MDAwMA", + "digest": "sha256:6ef45e80cf2684a0ec3b3c4dd306f4204561b05f602b3be3274a13c253b015cb" + }, + { + "id": "d74860e2-6d4f-5c18-8100-eb31e752e95f", + "version": "MTc1NjMyODE3NzY1MzE2MzAwMA", + "digest": "sha256:68cf6c98de7e1a054a32f34160184dde681234e5172cc3a78ac813e6aa169f74" + }, + { + "id": "d74edb12-1c2b-5b6a-87af-14c7df2ec751", + "version": "MTc1NjMyNzc3MDQ1MDkwNTAwMA", + "digest": "sha256:ab3ce16c50464612c259141e447cea48701f0fe5d55a15fe4940c120ab403367" + }, + { + "id": "d75f6026-fd89-5c6b-8186-88b9784d2f48", + "version": "MTc1NjMyNzc3NTEwNzUwMDAwMA", + "digest": "sha256:ef03eb0df1f56d2a0478937dd0ee09c90c3567bce9fecdb20d298eb64cfaa3d8" + }, + { + "id": "d7804d6c-804e-5dbd-abc1-1fce07078657", + "version": "MTc1NjMyNzg3NTUwOTkyODAwMA", + "digest": "sha256:78a0024be6565ea1860e9675b8ffc2fd7b30c1b9824d7d06717c0973d6902f28" + }, + { + "id": "d79ecdb1-838b-52d7-b2d3-fcb2d473f810", + "version": "MTc1NjMyNzg3Njc3NjI1OTAwMA", + "digest": "sha256:f4b54444d625265b61034916fe8397eba7cfee68003c3aaa4063051e16465679" + }, + { + "id": "d7b7bc67-f034-5d52-b2ce-7e8631b34e0e", + "version": "MTc1NjMyODE4MjE1MzY0MzAwMA", + "digest": "sha256:22234eeb66e30e7c6483164061f5ad9763aae1090f21a9a0337661cbdaf66c4c" + }, + { + "id": "d7cc7dd0-c0fc-5c1e-b757-d82ea97efa9a", + "version": "MTc1NjMyODA2OTQ2MzE4MjAwMA", + "digest": "sha256:281e268b4261ddbfbcfc17f8bc9510e1215aacf9b10cc58b7cce6f46dcce0e42" + }, + { + "id": "d7d6127c-0985-5bf1-9b64-98ba4812c2fc", + "version": "MTc1NjMyNzc0NTc1OTc0NDAwMA", + "digest": "sha256:40905cff123ea5387e247a47be5027299b4da8485abc7a257ca4d2edd67489ab" + }, + { + "id": "d7df51d8-8e71-527f-b461-a44bade345d7", + "version": "MTc1NjMyNzc0NDk3MDY2NTAwMA", + "digest": "sha256:754713e5ea2e7ed789f44c8271d1228d29b5d11692a218c4c6a9b5735acccc46" + }, + { + "id": "d7e88b1f-9083-58cd-9e23-ddaf3c210061", + "version": "MTc1NjMyNzg1NDQ1NzU1MDAwMA", + "digest": "sha256:4f801bb50fcdcbece0f76809eb0a9f426f2df8e0b09477494ca829b2d1f89496" + }, + { + "id": "d7eb8bd2-7341-50fc-be9f-b40180912b69", + "version": "MTc1NjMyODE2NDU1NzE2MTAwMA", + "digest": "sha256:a2061342653d20b2dfffc72a57e1f61699dc2f08cdb59ad8fa149a91b0dac5d5" + }, + { + "id": "d809d975-22bd-5f85-989a-af6da012cf15", + "version": "MTc1NjMyODE4MjExMzU0OTAwMA", + "digest": "sha256:936f501959561b09fb5283f1cae2bbe785a654013cd6f884201219f2f094dc16" + }, + { + "id": "d80e3632-4086-561d-baad-30d3cb0808da", + "version": "MTc1NjMyNzc3MzAzODAxNjAwMA", + "digest": "sha256:7c224c1f04263a2683ab1aba6363d5a11415696b9cf6283475f90a2f97c9bbad" + }, + { + "id": "d8192b0e-9181-5788-b333-fbada77ae70a", + "version": "MTc1NjMyNzc5NTE2MzQzODAwMA", + "digest": "sha256:6367016abe43f9464ef2affcb6c93bfd1293c2330b7ff8f474710c52f52d1d37" + }, + { + "id": "d81d9453-cf77-509d-b870-90ee1c12c7f7", + "version": "MTc1NjMyNzc0MjI5MjU0ODAwMA", + "digest": "sha256:c53f46e7935a4f02ef84506ea8452dcfb928bc71c7477cba9ee8cdc6851c3dd7" + }, + { + "id": "d82f0424-8a25-5ae8-b33d-7928d0f945d5", + "version": "MTc1NjMyNzc3MDM2MDY0NjAwMA", + "digest": "sha256:25640f4124f4c128f98cafd53a16b1561136631cca0f063aee8659b98dde87c8" + }, + { + "id": "d8382ae5-e6ce-59e5-a9e4-1d29ccfb9478", + "version": "MTc1NjMyNzgzODk2MjEyMTAwMA", + "digest": "sha256:72ae6f86c18af24c2dae118ae090fd461e514b76bb54b891f618b00d03a6a35e" + }, + { + "id": "d8472168-aa10-57d9-b2db-44401f431a9d", + "version": "MTc1NjMyODE1NjMyNTU3MDAwMA", + "digest": "sha256:86955611dc4eb50dbfcbb2cd1bf2806b78287c30ffa42f864f8a705ee750313b" + }, + { + "id": "d85b2a4f-9a5c-59aa-b188-0816e71307c9", + "version": "MTc1NjMyNzgzNTc4MDg5NzAwMA", + "digest": "sha256:483abf551e0cbab4a2c48f87be240ae01f2ffc179719ce9089752fe9816ec459" + }, + { + "id": "d85de5c2-112f-5cd1-9a15-7612afffaca3", + "version": "MTc1NjMyNzc2MjM4NDQ1MTAwMA", + "digest": "sha256:8b157c8b70a9496a9a9432fb5247b1e2d22b24566a51ff2b28e8d361b984a47c" + }, + { + "id": "d862a961-91eb-5410-8dac-4c6b081644b7", + "version": "MTc1NjMyODE4MTg3NDA4NTAwMA", + "digest": "sha256:f214f64e0e0facb096fd9fe7d2bbd5229435c7c9b2f4166ed238aec8c6b7e074" + }, + { + "id": "d86b5286-a37d-55ac-9e8b-06878966e087", + "version": "MTc1NjMyNzc5NDc0ODIzNDAwMA", + "digest": "sha256:50c9f746bb61120d977bcd6b6224da07636e151b4a00e5eff53b85180dfe5520" + }, + { + "id": "d87a093f-fdbf-5637-af56-538763393e0b", + "version": "MTc1NjMyNzg1MjE3MDk2MjAwMA", + "digest": "sha256:bf656ba7cd33fc97174420f1b12a02a5e82dfa4252c0108218d57cd41023999b" + }, + { + "id": "d88fc4d3-2a5f-5d58-b3b8-42538ca18003", + "version": "MTc1NjMyNzg3OTk1OTc4MDAwMA", + "digest": "sha256:47e12023bc40ff8ac6c30fb9e26759097b1fa374f4fcb9140fb82d55baefe779" + }, + { + "id": "d8a51381-1468-5260-922a-0bc8e8c8e269", + "version": "MTc1NjMyNzg4MjA3NTIzNjAwMA", + "digest": "sha256:e1282208102681a7e4758dca45ca3cf2e415f6c27da5d1374a5d43904c067724" + }, + { + "id": "d8d7c2b7-d345-5de9-8ff1-66f3f4141e2c", + "version": "MTc1NjMyNzg3MDMzNDAzNzAwMA", + "digest": "sha256:de37921a8897d8ed56706f4baa3ca840049e9da1fd87f544f92cfae8b15813de" + }, + { + "id": "d8e8847e-436d-55e4-ba19-479ec526e9f6", + "version": "MTc1NjMyNzg1NDUxMTA4OTAwMA", + "digest": "sha256:56a7e3a23d1d4ff9b9881b60200f6f97d73b8c560e16435566051a2ee751a49f" + }, + { + "id": "d9190153-d22f-5aff-a4ba-b4e1b6058ff5", + "version": "MTc1NjMyNzg3MDM5MDQ1OTAwMA", + "digest": "sha256:21b86839640aae0681980559d240a78ebe0ef15c32613f3c23d8710a604b8b06" + }, + { + "id": "d92b9fef-7c93-5ee7-ab7d-9b1fce3cfa46", + "version": "MTc1NjMyNzgyMjczNTc1MzAwMA", + "digest": "sha256:40018063f0ddee1a926f74ab37e48e48144aa6a1b0fbf7c1371b8e06e6e128a1" + }, + { + "id": "d9477bf8-3217-5fb5-8452-55d13e6d30fe", + "version": "MTc1NjMyODA3MTIxNjkwMjAwMA", + "digest": "sha256:5d63548e40f1815bfa1d5828b678994dc55d4df334ea159b5b1683d2b722bd42" + }, + { + "id": "d94eef0b-17fc-522b-95c4-f6dbc3b85d8b", + "version": "MTc1NjMyNzc5NjE0OTQzMjAwMA", + "digest": "sha256:feb2d4aa7da7f109c337e26783795eb3fd013e27b8aa5c4cdc252ddae8152aa7" + }, + { + "id": "d9562a01-8a65-5e73-9bd6-7d1e5cbd38df", + "version": "MTc1NjMyNzgwMzU5OTAzNTAwMA", + "digest": "sha256:f15d3eec0f79ec1bebd53c7c65757d70ae5b6ae0877100bf9f21420edb548562" + }, + { + "id": "d9666208-1547-51e6-963b-bbeef6301169", + "version": "MTc1NjMyNzg3NDgyMDk4NTAwMA", + "digest": "sha256:0ae09a8abdc71bab5828bc09dfe87882ca8abafd8f301d6ce0f545ace6294bfa" + }, + { + "id": "d9675c3e-7326-5a88-bc2e-caaa331976c5", + "version": "MTc1NjMyNzg3MTU5ODYwODAwMA", + "digest": "sha256:70db3676193cc118036804a907745a373d1e680d8765a2dea89009abfe09c4b4" + }, + { + "id": "d9694127-b04e-580d-b56f-f11791cc8227", + "version": "MTc1NjMyNzg4MDMyNDgzNTAwMA", + "digest": "sha256:469721331ef28e34ad7635053d6a53032a76f93317a938abef417ace57b6b260" + }, + { + "id": "d9b9c0d2-8d85-5d9b-b876-4c7fc07d4c5f", + "version": "MTc1NjMyNzg1MjEyODM3OTAwMA", + "digest": "sha256:fc49d6ac4fa92e717c4b3416d5758d7139f3093e3ed7dce3b14b0b979253ee83" + }, + { + "id": "d9c0e13e-1084-5bdf-a97e-b531d1913925", + "version": "MTc1NjMyNzkwOTAzODkyODAwMA", + "digest": "sha256:1196508d221a8d3ce98abc76a651184b3b2e8f4bd7ae1be49515eabbce2415b7" + }, + { + "id": "d9cae7ea-77db-5df6-b3e7-0d34b963a5ac", + "version": "MTc1NjMyNzc1NjA5MDI1NzAwMA", + "digest": "sha256:05e4e6f00640c14cfe4d83243e369492cbe1d6b1e43d7e420f58965b030df2e9" + }, + { + "id": "d9e2cbfd-19d0-55dd-b2bd-ec63c0503720", + "version": "MTc1NjMyNzc3MDMzNzQzMzAwMA", + "digest": "sha256:0bc1dd3fe1bf3339e149861075ae51fc39a0b429bf21b50ec62c0fa95ffc97f5" + }, + { + "id": "d9e425c0-5ae7-5978-9792-80903a475397", + "version": "MTc1NjMyODE3NDMyMzI5MTAwMA", + "digest": "sha256:6d3d493599e697c75f1d7d171207c74f0439ec0ac0ac1437994fba4bb90938fc" + }, + { + "id": "da01ae4c-0599-5e0e-840a-8bf8acf15f74", + "version": "MTc1NjMyODE3ODAyOTM4MjAwMA", + "digest": "sha256:b2974f561d14d7ab402f762c9719305b8ee9fe5ab5d859f3a4e47a2f7b6f0ba4" + }, + { + "id": "da0dd3c5-0d9b-5e28-97bc-e01e012eb93e", + "version": "MTc1NjMyNzgzNjIyODY3MzAwMA", + "digest": "sha256:67d872ff75317f3b4bdc6d2c9738b96ed9a14d5a930faa2c1dcbe87e16a7c1e8" + }, + { + "id": "da1a129e-704f-562d-9b30-8d92e3476daf", + "version": "MTc1NjMyODE3NDQzNjkxNjAwMA", + "digest": "sha256:200b59e6c557cb5da9f5134a774bed5bc8bf4c27b2f4dbb7e1f5ce0964194c8f" + }, + { + "id": "da1ba618-cf21-5a27-8c39-31a5dfb19fa3", + "version": "MTc1NjMyNzg1MjY3OTY0NTAwMA", + "digest": "sha256:561cd4f87733b4abf0923a28ef593f198f2b2a7e67ff577ec52c919c5f33ba88" + }, + { + "id": "da1f5eaa-938d-5147-a812-fc5b5a227408", + "version": "MTc1NjMyNzc5NTI3Mjk5MzAwMA", + "digest": "sha256:5545c3e2118d30175105c114441470413cb8d8f5decde336e5796cd0554a4c43" + }, + { + "id": "da3f3c78-6392-5af7-afed-f2eebc33b90b", + "version": "MTc1NjMyNzc5NTM0MzcyNTAwMA", + "digest": "sha256:12b7a46b80cf87b14fb866b4b7d0787ce66cefa6a1521498ff54f38fa5006b91" + }, + { + "id": "da400b6d-2f1f-566b-aac8-378ceaa45ade", + "version": "MTc1NjMyODE2Njg4ODM0MTAwMA", + "digest": "sha256:0229fa6676615b050b5e00b5da359915f744c278991f041dba10091a01c5ce29" + }, + { + "id": "da437a11-2f89-5483-8a34-e0d5170d4a1e", + "version": "MTc1NjMyODE2MzAyMzA0MjAwMA", + "digest": "sha256:ed249f1ab04c6d62e5bfb8677d23991e8d991340112ff7274d176d48611d75d8" + }, + { + "id": "da714576-b4d3-5ac2-8851-9d6d94f656ec", + "version": "MTc1NjMyODE2MzQyNzk1MzAwMA", + "digest": "sha256:19e7309075556f017f948f01e657d16438071d8f0d69882fb23a679b6c9c0ba2" + }, + { + "id": "da7418b1-5fb1-58dd-b405-b420335cb2b4", + "version": "MTc1NjMyNzg2NzYzMjEwNTAwMA", + "digest": "sha256:9fbc71521e6c1266327a8d66d76d2fca3bc3d3021cce561b296642b559aa81be" + }, + { + "id": "da7bf745-f942-53fc-8e6b-7bfbe9b15d33", + "version": "MTc1NjMyNzc3NTE2ODE2MzAwMA", + "digest": "sha256:3344925b47b0e5dc70491c22b5355afebe6a777188ca17366128178a7b4d0a0f" + }, + { + "id": "dac2f7b4-8ce2-54d6-8b01-a45ba88c1a19", + "version": "MTc1NjMyNzc1Mzk2NjE4MTAwMA", + "digest": "sha256:88582db89cef163b91f9d502fbe1ff5be55c7f35e010135229d789efb2ff7a02" + }, + { + "id": "dae53a9d-6451-5505-9259-cc0fa89cecef", + "version": "MTc1NjMyODA2NjcxMjgwMDAwMA", + "digest": "sha256:c9d3dbef70e147bf71fcbb2c02049d529783e1f117a10392d92c1856f4604a19" + }, + { + "id": "daeeeeed-460d-5181-a113-8959648f4ca0", + "version": "MTc1NjMyNzc3MDY1MDIyNTAwMA", + "digest": "sha256:a5e591113b1c241dbd37d94d2851b6ca2172b8974e3f7aa344f6328b5c2f9bcb" + }, + { + "id": "db070108-0d63-5e80-8183-3eb07ac0444e", + "version": "MTc1NjMyODA3NzQ5MjE5NzAwMA", + "digest": "sha256:bde9bf6d1bc16b58ec78cfa0cc9102b3d837e5debb9ecd9969f5366313ceefd0" + }, + { + "id": "db0e4478-2e9d-5db8-896b-6c7714e86f9a", + "version": "MTc1NjMyNzc4Njk5NDI5NTAwMA", + "digest": "sha256:3dc63a6aec5c384b5406b6aedb75dedfa21371bafb147ab807924d9b05e8ebc1" + }, + { + "id": "db215da7-b3f7-5eab-a23b-a3260a7975c3", + "version": "MTc1NjMyNzc1MzYyNjEzNTAwMA", + "digest": "sha256:b27eafbd4306c565ad79e040fed6be8c30e5ebc7b876921d319528f3cec842c5" + }, + { + "id": "db30906e-1238-5dae-a52b-7edc5962edf6", + "version": "MTc1NjMyNzg3MTk4ODU1NDAwMA", + "digest": "sha256:2117993b8a59fd163271721b9bd50a7ee66eb9b6a4c9aa68371c92bfc1a0f19a" + }, + { + "id": "db31a204-6b8f-56c8-abf3-1bf91e76a6a6", + "version": "MTc1NjMyODE2NzAwODcxODAwMA", + "digest": "sha256:1a26ae44b8063534148e01e7a8ae2468793ee60f90dc9df0d50c69108a339f0a" + }, + { + "id": "db561772-b860-5691-a1f1-281d306aba63", + "version": "MTc1NjMyODE3NjkwOTA3NDAwMA", + "digest": "sha256:3c15f98c8e8c8a65620ab7e052189673671c077a781b46249086d1612e8bcadb" + }, + { + "id": "db7172e7-59da-5f52-b541-dbbbc24ec639", + "version": "MTc1NjMyODE2NDUzOTQ3NTAwMA", + "digest": "sha256:879da132883acd361d83b5d918a51575c2e59986ff22fb9153da0dd753c0cccd" + }, + { + "id": "db73e538-6f6a-5a87-8b8e-8a7d2b25e610", + "version": "MTc1NjMyODE3MTExNTU5NDAwMA", + "digest": "sha256:4453d65ae9eefe901b6333a1088f839cd924f96e76af351f8f56dcc919293ee1" + }, + { + "id": "db75e145-129b-54db-aee1-f58ce3a641bf", + "version": "MTc1NjMyNzkwODQzMzI3NzAwMA", + "digest": "sha256:9517f87f0aa45e268d1bca35e1b1383dbcb0765d2b964c0f0811884d4d479e66" + }, + { + "id": "db789c19-2c2b-54fc-b77c-a9942a473ae8", + "version": "MTc1NjMyNzkwMzQyNzUzNzAwMA", + "digest": "sha256:0746ef84c3c358debadf6dc3b9ec3e8fad886cc4367d7cdb2a74a1a314def275" + }, + { + "id": "dba5c438-776c-5261-beda-e62473c8a002", + "version": "MTc1NjMyNzc5NjEwNDc4MDAwMA", + "digest": "sha256:ee1c68b2fda9b79554b118cccb7234016cc2db2b69478b7c2082f69cdde73599" + }, + { + "id": "dbdada87-1d2e-5150-91db-7da14f5da834", + "version": "MTc1NjMyNzg4MDM4MTUwODAwMA", + "digest": "sha256:48816512bbdef2144acf56d45946fad5abb67309dd17602ac32cf04526c329a2" + }, + { + "id": "dbe2edba-f70c-5b54-ba08-649db9293126", + "version": "MTc1NjMyNzg1MzIwODkzMDAwMA", + "digest": "sha256:fc21aaa8f9064ddd384ea152907db72a1c9e9bc6f52b62f06b748f5e185ab0c2" + }, + { + "id": "dc067d36-0162-541b-8620-f7bc7483344e", + "version": "MTc1NjMyODE3NDM1MTI0OTAwMA", + "digest": "sha256:06ec873b2c1d12b608429e7110ea38cceba76ea3a30183197ddc66cfcb1a8845" + }, + { + "id": "dc18af5e-0bfe-583b-8557-0445c251adfa", + "version": "MTc1NjMyNzc4Njk0NDE4OTAwMA", + "digest": "sha256:fd64ef2670b35495984460f104e5aa0929021d619ce90840881f19df8fac3bfc" + }, + { + "id": "dc2801fb-977a-5859-b8a8-2f269b364296", + "version": "MTc1NjMyNzc0OTkxODU2NTAwMA", + "digest": "sha256:536cd56a095b95d5119b8c2a910f152b122676798c9b2b9f25b6d37cbaeb3856" + }, + { + "id": "dc3f45bb-0557-59b8-aa0c-7bb772c483a7", + "version": "MTc1NjMyODE1NjQxNzE0NjAwMA", + "digest": "sha256:92c3cde4c671b656aff111365c892b6f18a14b00bebff47cab2733125c0b7489" + }, + { + "id": "dc588bb0-13a8-502b-81f1-e959cddd3a65", + "version": "MTc1NjMyNzgyMjQ0MTQ1NzAwMA", + "digest": "sha256:f06b2285a980e20ecf9cd23d5804e5ef2bed374d96dcec26546d2d7b80c7f412" + }, + { + "id": "dc5afd8b-0912-5a04-859b-925887b51d6c", + "version": "MTc1NjMyNzg3OTk4MDM0MDAwMA", + "digest": "sha256:09602855a89594dc09a32c49e954ff465043a78ce2936dacc56081b05d21d937" + }, + { + "id": "dc70042a-cdcb-5bac-ac06-4a6cad542a80", + "version": "MTc1NjMyNzc4MTk5NjQ4MTAwMA", + "digest": "sha256:f3b25b185a667ee8f317f430559ab8673ddb374325884f7bd9a68f8582ad1ea7" + }, + { + "id": "dc7e5ebc-4759-53d0-9ad4-3b1789a18cd5", + "version": "MTc1NjMyNzg1MjY3OTc0MDAwMA", + "digest": "sha256:aaa9a184f73c2766d697ce3d134bd5b20cb9d816318436a757f00e19987fb232" + }, + { + "id": "dcad1934-54a4-5559-bb55-b3e921fca6e2", + "version": "MTc1NjMyNzg0MTEyNzYwOTAwMA", + "digest": "sha256:e665b4503e357f73f09e520ab85bfcb370ca44f4e35d8442095daec2e74bb366" + }, + { + "id": "dcb3f207-7cfa-52b7-9ef3-5a16709a62f2", + "version": "MTc1NjMyNzc1NzUzMzEwNjAwMA", + "digest": "sha256:4695ddf71d7463167912176061f130851e1a63ba7ebb82131391ae67bf094146" + }, + { + "id": "dcda9643-985e-5d8b-ac87-07df29811fb7", + "version": "MTc1NjMyODE3Njg5MjE1ODAwMA", + "digest": "sha256:889585a028879a30f0851be53a8694542a4b437f3ea5ff4910038a899ed9cd29" + }, + { + "id": "dce828f4-5584-5c51-8535-1098cc019ca2", + "version": "MTc1NjMyNzc1NDAyMjMyMDAwMA", + "digest": "sha256:1b326d8e837172aec197fb94c417b0606347d47155bfa78f2b25bfc03661c1b2" + }, + { + "id": "dceebd6f-bc20-507e-a4e1-34c37694d5a2", + "version": "MTc1NjMyNzc5NDY5MTQ3NDAwMA", + "digest": "sha256:3924c553b648f795eb0ca2c5928b730264027f5628e86bd0554f7cc92bff04a1" + }, + { + "id": "dd2237ae-953f-5899-973c-2dbb130f68db", + "version": "MTc1NjMyNzc4MTk5ODEzMzAwMA", + "digest": "sha256:ce6b56e8b8a0a58039bd9e9103b9853cb67bebc1241ec9abe88a9f4a242ae856" + }, + { + "id": "dd5682a1-f8e3-55ae-8b3a-d29edd170fde", + "version": "MTc1NjMyNzg1MzgxMjkyODAwMA", + "digest": "sha256:4d71aa7620b97a4652e69d3c295c12cb624cd5f4c5f40bb301d48246e17e1cc4" + }, + { + "id": "dd5a6e54-c366-5051-9e8e-f614cf9b6d6f", + "version": "MTc1NjMyNzgxODkzNDMzMjAwMA", + "digest": "sha256:417e69cb058fa4f1efa42b44f3fdfb4480185502601a0232411d7415225b7881" + }, + { + "id": "dd8d9916-4055-52a2-85d7-ac335eccde97", + "version": "MTc1NjMyNzg3NDkzMDU5MzAwMA", + "digest": "sha256:50f162fa072dbe16f6339bc011c78276fa39ce10085586e74ff66a0837e51a2c" + }, + { + "id": "dd9226e4-c429-5460-ae34-1701db4e0bf9", + "version": "MTc1NjMyNzc0MTc0NDg0MzAwMA", + "digest": "sha256:934c94e3eee853d9d853d125b46535196e50f419c4e239cb9580c99de531b1f5" + }, + { + "id": "dd9ec43d-7777-54ab-9298-250eeddc1709", + "version": "MTc1NjMyNzg2MDI2MzI1MzAwMA", + "digest": "sha256:9f49ca3853d09e873d0fce57c31856272fa65fbbfb8e1e8f6f4efe585631f6b6" + }, + { + "id": "ddccf1f9-59fb-5732-b345-629df01d5cf8", + "version": "MTc1NjMyODE3Njc4NzA3NTAwMA", + "digest": "sha256:e1903036190572acb97bb3682c7e99e5d412f5693fb2925da591f0c185ddf396" + }, + { + "id": "de0dc301-cfb3-5f6c-8f12-2c396f978765", + "version": "MTc1NjMyNzgxOTA0MzcwMjAwMA", + "digest": "sha256:1d7bcfc7b4ac0d37819fd849ed66aa63e41a53df598ef37a5ed5c470b3d39bb9" + }, + { + "id": "de3736e2-13f5-50b6-9c46-e3c4afd8aa5e", + "version": "MTc1NjMyNzc4NzA0MDMzNzAwMA", + "digest": "sha256:b83fc0fb9f62880eaa0e273d45d98abd8ad4696340d2f28e6b4c6433f90ab3bf" + }, + { + "id": "de42659c-d9e4-517c-9e8f-98d815159530", + "version": "MTc1NjMyNzc0MjQ1NjQ2NzAwMA", + "digest": "sha256:b8219953fe0d8d3861ab1086e73a47f3c03e5d5105ed7bb4ea9c3204ec64fbc7" + }, + { + "id": "de4887bd-ea34-5bbc-b000-ec54736e7db4", + "version": "MTc1NjMyNzc0MjQzMjQwNjAwMA", + "digest": "sha256:bae727b9545f1dec04a2f739e2ebb199511c19aa467be6e4f364369cc395e89e" + }, + { + "id": "de5d7ec1-feaa-535d-bf8d-b7f845a11f2d", + "version": "MTc1NjMyNzg1Mzc5NTA1MjAwMA", + "digest": "sha256:f26c7fe552a9bcbbff7e3013c177923355b0584fbf1d1b80df98f9f43eeedadc" + }, + { + "id": "de5f6be7-bb0b-5edf-994c-0cec078dad2f", + "version": "MTc1NjMyODE1NjM4MzIwNzAwMA", + "digest": "sha256:2326809afeb81b750aba2ea98b6adc0f691a190ab160e16cd82945f3c9873858" + }, + { + "id": "de6c9fd9-e8e6-5b29-a058-2952a6e375f5", + "version": "MTc1NjMyNzg4NzEzNDgyMzAwMA", + "digest": "sha256:c2d8aff1608fdc5808ebf5cbcce14853d4af46ad9eddda6e37f84441888b05c2" + }, + { + "id": "de72667e-5375-5f94-abfc-3f17a1eef268", + "version": "MTc1NjMyNzgzNTE5OTQxMjAwMA", + "digest": "sha256:41b580f157645e82be0fea10e22d1179aca1637a3f1683a7afa9a0eadfade3a4" + }, + { + "id": "de76a56b-0f74-5a36-8f0e-31562107e3ff", + "version": "MTc1NjMyNzg1MzI1NTkyMzAwMA", + "digest": "sha256:8fe9b9c6142b2eac1c638f057cb433968aa8ce9c487ea1d6e473490913ab9634" + }, + { + "id": "de85b7af-1f6f-59fd-bb22-921ce3a8e146", + "version": "MTc1NjMyODA2NTQwMDg3NTAwMA", + "digest": "sha256:a5a7516a40358431095256dfe6774627cbb4308983e653e9bfb11cdb17df87eb" + }, + { + "id": "de9e3c25-b36d-533a-af72-033183da4531", + "version": "MTc1NjMyNzc2NzY2MzA3NTAwMA", + "digest": "sha256:4dfae85db392d5ce8b385e6b6587e67ea55538ac3a5f4d5f8e86293960b3988a" + }, + { + "id": "deb6e5b1-090f-5919-a9c0-a22d425c3079", + "version": "MTc1NjMyNzc3MjUyMTg5MDAwMA", + "digest": "sha256:69c34f47c795ec20e1dd5e84e310fd0b588a262deccbcf108d214a81e5acfce7" + }, + { + "id": "deb86969-ed59-53bb-97ea-2e167972c3cc", + "version": "MTc1NjMyNzgyMjc1NzUzMDAwMA", + "digest": "sha256:d83af12d4941c31a4a55bba88b770db1d69cf2ab70074a3c85fff7756da73e54" + }, + { + "id": "deca53ec-1d3c-531e-87d8-4347da7678e8", + "version": "MTc1NjMyODE4MjEwNzcyOTAwMA", + "digest": "sha256:9db6193bcafec80ecb0c92403dc629fce588f01307d6f35a0021a52b4ebea2c1" + }, + { + "id": "decd5dcb-874a-55fb-95d5-5b80e2002f6a", + "version": "MTc1NjMyODE2MzEwNDEzMDAwMA", + "digest": "sha256:8898d68415bb754d1b2d048616e1c2aa65b4661348cb19d4e2ea39f737783d36" + }, + { + "id": "dee4cf45-c8b5-5d81-a942-fd6b6c11235b", + "version": "MTc1NjMyNzkwODQ5NDYxMjAwMA", + "digest": "sha256:a6afcbcb68e4e9a9e68279863b42f888d40a990be7a99f9caa631a165844e0f6" + }, + { + "id": "df011371-a620-5630-a1cb-9e6a9863331d", + "version": "MTc1NjMyNzc1MDAyMzI3NTAwMA", + "digest": "sha256:eba35bcff06d195eea0cc1a9904aa84b5c6f6eecce822983bc7fdedb10dae5b5" + }, + { + "id": "df08aa00-d61d-5687-9456-823530a57da2", + "version": "MTc1NjMyNzg2NjI0NzUxODAwMA", + "digest": "sha256:2656c7e8cc608bd4f78df6c55561804167a3118ea52c64316640f250a0aabb2c" + }, + { + "id": "df096f0f-1b45-5bb7-988b-9cace0c3f29a", + "version": "MTc1NjMyNzc3NTE0MzM2MTAwMA", + "digest": "sha256:bb69466252c34cda4c03137f9753d0219f29ff26f3877fb3c45fb89360d86827" + }, + { + "id": "df0f1858-e64e-5cf8-a9be-e98e203a895d", + "version": "MTc1NjMyNzg1MzgwNjgyNDAwMA", + "digest": "sha256:1d74db22f5db93bca461824ed94e903d04cfcf9dda44a1f282547ebb24a6ab1b" + }, + { + "id": "df17b789-83a6-5b16-bcf9-709fcbee941d", + "version": "MTc1NjMyNzc0NzY0NzM2MzAwMA", + "digest": "sha256:5f50d61f4ec7fddb7b435f60a7ed021bec80496a2893eb23e424a41e04f5c07b" + }, + { + "id": "df26a6e8-5c85-56c8-8d31-13f159753d81", + "version": "MTc1NjMyNzc5MDMzODY4MDAwMA", + "digest": "sha256:d74362c990bd916218582ae5414783d582194aae0ba1a9d064268bf36cda0e65" + }, + { + "id": "df402c8b-d78c-5a1d-88d1-75ef28ad13a2", + "version": "MTc1NjMyNzc4MDg5MDI0OTAwMA", + "digest": "sha256:64a80a82a6ff5f259a1b7879181c54b9d12afc7d661d9d23a9bd5a37443a8379" + }, + { + "id": "df4d458f-b963-55c6-a90b-6b2a54201b68", + "version": "MTc1NjMyNzg2NjYyNDE0ODAwMA", + "digest": "sha256:7cf58eabe7c8674d15f0c88b2031f9fa95a27abf276cbd3c74cb83fd700d30a4" + }, + { + "id": "df67a8a1-7a99-5456-bb70-d9db57cbc738", + "version": "MTc1NjMyNzg1MzIxNTMxMDAwMA", + "digest": "sha256:a64a6dc640ce7ed22112c70347aed98c98903e6f6a73eb225bbcf02ce5e1b8c6" + }, + { + "id": "df745cbc-058e-5155-a618-271654e46242", + "version": "MTc1NjMyNzg2OTUxOTg1NDAwMA", + "digest": "sha256:3e5160fdd52e4a64bf96c5a7cd1a3cda3faefcf56156d15b1fcfd174dcc2977b" + }, + { + "id": "df74c0dc-e899-5d4f-94e3-82f4459dabdc", + "version": "MTc1NjMyNzg3OTk3OTY5OTAwMA", + "digest": "sha256:1b80e817b5eab89859d9e1d4a5f4e2ebee1c660c60ce1bafa27296eaabae3fdf" + }, + { + "id": "df8d02ab-4138-5c39-a1e1-06e655951f6e", + "version": "MTc1NjMyNzgwNDU4NjU1NTAwMA", + "digest": "sha256:8e63bf700377deac77ffd4626f495a5b95678ae28f0b6abdf3b7cd4ca9bd2243" + }, + { + "id": "dfa88657-ab3a-509b-857a-3b6299343fb9", + "version": "MTc1NjMyNzgwMzY2MDc5ODAwMA", + "digest": "sha256:121fa399dedeecdcea7609db95c4c3890de79a1d15c5fec8b72d37ef2c6b6f50" + }, + { + "id": "dfb39ede-0495-5e4c-84cb-f9121e1833be", + "version": "MTc1NjMyNzg3MTUzMjY5MDAwMA", + "digest": "sha256:fef9a6e9a5b708243a78b1ee2e1d34c52c3fe658ce96241308cfc52106ef26c6" + }, + { + "id": "dfdc2d52-f7c9-5be1-8a24-241c452c3a7f", + "version": "MTc1NjMyNzczODQ0ODM3NTAwMA", + "digest": "sha256:4893c477d804af892efc32c440a13a95f790a5d5bdef88071a2f728f2a797cb8" + }, + { + "id": "dffc93ab-2430-5783-b897-24acd47fae7c", + "version": "MTc1NjMyODA2ODkxMTY2OTAwMA", + "digest": "sha256:5873c6bba179b506e6827b5ce076c02867f303cedcbc24f856294e3a5ab5872d" + }, + { + "id": "e0066b80-858d-51a5-a0ef-8532262bf17b", + "version": "MTc1NjMyNzc0NTA1MTc3MTAwMA", + "digest": "sha256:06fa827fa5afe653cd49c7c593c40bdce2319d71355da1c2f36cb9cae243576d" + }, + { + "id": "e00bd20c-aeb7-5678-962b-6d462186871d", + "version": "MTc1NjMyNzc0NDUzMTY3NzAwMA", + "digest": "sha256:f9d8accce4cd86e82539f15e0f6565e3f6370387cb809c0f3878de0cec918ab1" + }, + { + "id": "e028b196-ac8e-5892-8ccd-ac41a57c70f7", + "version": "MTc1NjMyNzkyNDI1ODU2NzAwMA", + "digest": "sha256:c0f95d53b49111b31cacc05568f52ca2c9049a98f75f1cbbc5e76147b3be7aad" + }, + { + "id": "e0434ef8-8961-50eb-801c-bf0afab065d3", + "version": "MTc1NjMyODE1NjQwNzMwMzAwMA", + "digest": "sha256:be0493f3d24d7065b7d1d742a9827dfea7439ba6645ee5624e4254d1b4c40407" + }, + { + "id": "e04f9a82-e00d-5540-9406-6476b4b413f9", + "version": "MTc1NjMyNzg3NjcxNjI3NzAwMA", + "digest": "sha256:6d8cb8600ca1e60474b15fc74a13a1636bfe9b5b53fb6dc3af35bee513a39368" + }, + { + "id": "e06ea17e-4432-5ccc-8e93-4dbb3e54acfe", + "version": "MTc1NjMyNzgwOTMwMDI3MjAwMA", + "digest": "sha256:02f622455e09d110912cc3ba23db266c25b4af5daa6b0bf823dcd3ec6c162b0d" + }, + { + "id": "e0996762-c1a7-5da5-91d8-67bcb6d3d2ae", + "version": "MTc1NjMyODE2MzEyODE3MTAwMA", + "digest": "sha256:c2869b33597380e7fa4e05b3a59fb26deeb45c017265ef1cfb7b851067c61fba" + }, + { + "id": "e09cb264-3e24-547e-9b35-c3d2ac79d8a8", + "version": "MTc1NjMyNzc3NDU0NDc2OTAwMA", + "digest": "sha256:05ca70afb49a2a1a30bdda9ce4ef170b8578dbd4274e6ef0f87c28dc48d98495" + }, + { + "id": "e09ce3c2-ab5b-5613-8d8e-5d5637bfbedc", + "version": "MTc1NjMyNzg3MjA1MzY0ODAwMA", + "digest": "sha256:e96fb9c47b28f1c56174928fb92a02d982dd3bf20ae36fd9551964c576aeadbf" + }, + { + "id": "e0a206c7-8705-5ec9-971c-55782680e306", + "version": "MTc1NjMyNzc0MjM1Njc0NzAwMA", + "digest": "sha256:15fca34b85bca2903c70c7e1511f03277463bf5f135da4169b06274b52daff87" + }, + { + "id": "e0a8229d-3846-5a1b-a821-b509215c7b9d", + "version": "MTc1NjMyNzg1MzgwMjI2OTAwMA", + "digest": "sha256:28c469c6f0b5d6fb23daf65952046246cb3bbbd529792ab82468b74e1cbd2f8a" + }, + { + "id": "e0b774dc-1d17-5a59-b146-c73ebc8c53d9", + "version": "MTc1NjMyODE2MzQzMjY1NjAwMA", + "digest": "sha256:34d84313a787537cd2bbd017a05a9b7af16b19e6c5ebe40a7c2bf29ed3e4a8ab" + }, + { + "id": "e0c0cb6c-c7dc-54a7-b5db-b68e4c747b72", + "version": "MTc1NjMyNzg2MDI1OTMyMTAwMA", + "digest": "sha256:8b3705ab0f61ecdc90a841637535774a288f3e3b891c5e9a19bb8b7db2dbf8bb" + }, + { + "id": "e0cc898d-12d9-5e8e-a2ea-436bf1a94691", + "version": "MTc1NjMyNzg2MDM2Mjg1MDAwMA", + "digest": "sha256:7ef54ec13d91248289327f35f992f3fd43be8b35d329da24991a6e675b79a36c" + }, + { + "id": "e0f3ee57-19da-52df-828d-26dbe3004546", + "version": "MTc1NjMyODE4MTg2MzAyNTAwMA", + "digest": "sha256:9e895956792b60563a67951dc37879572b17b7f12ad15911784ac9f91a979e83" + }, + { + "id": "e0fd30f6-fba6-56c8-a8a3-d41c8a24a759", + "version": "MTc1NjMyNzg3NDU1MjIxMTAwMA", + "digest": "sha256:b9d9c96c31f993234839c6bd61e317c68db41be37bd913f7a60cf31a0c2acfa0" + }, + { + "id": "e1115a20-3ef9-5643-ad7e-96e53711621f", + "version": "MTc1NjMyNzgzODg4MjYwOTAwMA", + "digest": "sha256:222a87f96b2ba97fa30d7a81631b8689d54c3edf1f21a3740d719e14ecd95c33" + }, + { + "id": "e1638c43-46fc-550f-b994-fb2cb30fd0e7", + "version": "MTc1NjMyNzgyMjg3NTUzNDAwMA", + "digest": "sha256:8827acc0dcd31d9c5e421c491a4bb9d592f2736bbacbecc50eabb237e4684cb7" + }, + { + "id": "e16d2adc-6e69-5b8f-a866-4a2886e65a1b", + "version": "MTc1NjMyNzgxODc3NDk0ODAwMA", + "digest": "sha256:d39f31675cd3a2b3a59dce9e210a57b5281e6c16b90a2f96cda56e2532c16bff" + }, + { + "id": "e17026b8-c778-53ce-9ad7-21d8e14504d3", + "version": "MTc1NjMyNzc4NjkzNTI0MjAwMA", + "digest": "sha256:63ad58ab9b55cf84a0fb3f77749160aea985dec2be3f1b71e82387fbf6dff10a" + }, + { + "id": "e173e00c-ed27-562c-aad9-96468b679f6d", + "version": "MTc1NjMyNzgxODk3NjI3NTAwMA", + "digest": "sha256:3405c4f63fb99b6cb86edf547374459de93fa85542ca2820d7bcc7b0daaff215" + }, + { + "id": "e196d046-3c01-53cd-9613-93ad4906c750", + "version": "MTc1NjMyNzgzNTY4NDM2MjAwMA", + "digest": "sha256:38a1fb889dba568872e82d80a19650ac72d261b7b04d06330b7cc67d1023c27e" + }, + { + "id": "e19e208e-0088-5434-8c77-e0e6a8490027", + "version": "MTc1NjMyNzg3NDU1MjY1NjAwMA", + "digest": "sha256:f1ddf25a88ac97c6ea55ca1c35abf4ace055a0c1ed80ea6070ee73460a49c4dc" + }, + { + "id": "e19e95b7-d4fd-5666-ad98-0d9ece599dd7", + "version": "MTc1NjMyNzc4MDg2NjM5MjAwMA", + "digest": "sha256:24875a198738a8766909756263565611a5c77ee264f5a42cf000236b7d895731" + }, + { + "id": "e1b38f78-3cc0-5787-9446-a900e1405aeb", + "version": "MTc1NjMyNzc2MjM2MzM1ODAwMA", + "digest": "sha256:08e43277f7707397559cefa27e7edb02fc89dd2b663d99f62414380e67cfecd8" + }, + { + "id": "e1d4abe4-e710-5deb-988e-dd1d07894a8a", + "version": "MTc1NjMyNzc4NjkzNTg0NjAwMA", + "digest": "sha256:87124a42097d7a6bce53750f7e7611103cb3482f0b8ebb14aef2ec4d0679e9c6" + }, + { + "id": "e1da029d-884a-55ee-b9a5-d6487197683b", + "version": "MTc1NjMyNzc3MDM2MTI2MTAwMA", + "digest": "sha256:0ead9a4481e8e30e1dad67fd328a012eca21217e0aa1dcc9db97f535d65e49ea" + }, + { + "id": "e1de88cd-e248-587d-8f70-e4426237e708", + "version": "MTc1NjMyNzc3MjU4ODE3ODAwMA", + "digest": "sha256:28ac565ccc2c6731c81acc2b65188b1592dcc7d4a6a37608016f4334ee41abaa" + }, + { + "id": "e1e50f3a-2fd6-527c-9e5a-3f39a953f589", + "version": "MTc1NjMyNzg1MzI2MTk4NTAwMA", + "digest": "sha256:28a502d255a2d903fd7e4e02d838d6e4290bbff8669ce8d477b7d131d18b9f88" + }, + { + "id": "e219bf23-0fbe-59b5-931d-36c05e6d26e6", + "version": "MTc1NjMyNzc4NjkzNjA2NjAwMA", + "digest": "sha256:86e3f2ee880bc1bc482acd273b8e4f383ae4a83841baf1960642aa9fec7e874e" + }, + { + "id": "e22dadee-f894-5bbc-abd4-f918a984f5f0", + "version": "MTc1NjMyNzgxMTk0MzExNjAwMA", + "digest": "sha256:91cdde6f5acd8c9599537dd26a69006a5f5331084a2337d71af7cf23379f106e" + }, + { + "id": "e268710c-ba7c-5d18-8b4a-a3baf1e991f5", + "version": "MTc1NjMyODE2ODkxMjc3MDAwMA", + "digest": "sha256:2cb4651f43c6b784e652aab2dafd690bf215d301af11d6e70fc9d64f6d64d210" + }, + { + "id": "e26ea38c-16d9-58cf-ad4c-aee7ba08001b", + "version": "MTc1NjMyNzg4MDQxNjg2ODAwMA", + "digest": "sha256:615bdb3c32ca868b241e95a1f73ab9b85725cfc0814b5a750dade79698842f8a" + }, + { + "id": "e279f5f5-d211-5736-a98c-c3f7b0cba267", + "version": "MTc1NjMyODE2Njk5MDAzNzAwMA", + "digest": "sha256:cbd930ce2b02e2fc85188067ca92dee1843a5e7a16c0252d66d7c5a34518b14d" + }, + { + "id": "e285b740-e872-58f2-8a1b-be50a6f46bce", + "version": "MTc1NjMyODE3NjgzMDMxMTAwMA", + "digest": "sha256:81dd7bb31deb74941097b59c6f0a7586de5a2f868e2f8b65b6165f99292fa3a4" + }, + { + "id": "e2957f04-1f42-5b7b-8d21-37d7c2d18b95", + "version": "MTc1NjMyODE4MDQ2MDc3MTAwMA", + "digest": "sha256:df7d6f51033d38a73f3a2a4eb7fee70cdba9c92317db3d908e700858dd69d319" + }, + { + "id": "e2a2fbf5-34ba-5f4e-bbf7-bc6d2d1446ab", + "version": "MTc1NjMyNzc4MDgwNDA3MTAwMA", + "digest": "sha256:9c4d84fd32da3946ee2c3ff02fe247ea9d477557ee74bb8cc5f52efa60c4ac7b" + }, + { + "id": "e2b422ae-76ac-5b1a-ad1e-81e07917d1fd", + "version": "MTc1NjMyODE3NjE2NTg2NTAwMA", + "digest": "sha256:fafe70c7862c3592c93ae1438647f04273dcb3360e27b8d338fcec57657d5ec9" + }, + { + "id": "e2b92a84-135a-5a28-8301-89ad1bb2e401", + "version": "MTc1NjMyNzgwMjk2ODY5OTAwMA", + "digest": "sha256:4840b2ad6134b7ad022a23343e1d97736f29b5ec8f8ec0c3ea027dbc1def837a" + }, + { + "id": "e2d6c539-c077-537f-afdd-e0d0d47a2b53", + "version": "MTc1NjMyNzg1NDM3MzI2MDAwMA", + "digest": "sha256:3067004aaab427e683cf5747c19af2185aedb1cd6070a2c310d4d469b9904566" + }, + { + "id": "e2dd0f08-b0c5-57cc-9fcf-5dd9ebfc94ff", + "version": "MTc1NjMyODA2OTc1MDc4MTAwMA", + "digest": "sha256:b849de179f4e8036bdec39cb1556faa8f77286fb91420ea08435c88e9a54e788" + }, + { + "id": "e2e2363f-09d3-5b08-9709-7125bd3de45a", + "version": "MTc1NjMyNzg3NTQ2Mzk3NDAwMA", + "digest": "sha256:7dd28480cee298b4db4daf5f01e448d251a8cd9c3733b7f2c71fd16686c03aff" + }, + { + "id": "e2e5d475-4f28-5b65-9f98-3fa516a95e6a", + "version": "MTc1NjMyNzc1MDA4NTE0MDAwMA", + "digest": "sha256:3a5c45b390f2d74247399c4a7177d06a68595f787ce34e6a12a55f0f7f5522db" + }, + { + "id": "e3068880-ea50-5ba1-80f5-3363d14cdaf9", + "version": "MTc1NjMyNzg1MzM1NjQyMDAwMA", + "digest": "sha256:a7df391b4c8c446c0930f841a989db4fcedbe99090577f1885bd0965e16afe64" + }, + { + "id": "e3117391-431f-5201-b0c1-239ef352c1d9", + "version": "MTc1NjMyNzg1MzIwOTI3NjAwMA", + "digest": "sha256:d9269eee7805122d845bd8707e649e81a367fb9f41e561945c6fca4d85dc241a" + }, + { + "id": "e325efc5-ae53-57f8-b362-96081f143958", + "version": "MTc1NjMyNzk0NjM3NDkxMzAwMA", + "digest": "sha256:253bcba131bc2fd9a01646d9f94976568590427dce1cfd5bb2ce2a1f04af6048" + }, + { + "id": "e330bcc8-fb1a-50e6-8b75-76b181b98d17", + "version": "MTc1NjMyNzgwNDU4Njg5NjAwMA", + "digest": "sha256:77f64550618858ad9a0bcb462cb9c80a31d947a1c7c284a2c2e0be2ade2b93f2" + }, + { + "id": "e367ff31-bc05-5f3a-a178-f0e9f154f39c", + "version": "MTc1NjMyNzc3NTE5MzY3NDAwMA", + "digest": "sha256:9bf4c6a9b2defd995500cabc5fa33a4592ee28b1cd2b29a10a77e0a786564989" + }, + { + "id": "e36aa956-665e-5160-b369-59fa1d5f1c16", + "version": "MTc1NjMyNzg1NDQ0MjcxODAwMA", + "digest": "sha256:31af7a138020a1ca7a140e890dc48f5c4005e5bbdb5ddfa0fcdf084443d26e07" + }, + { + "id": "e36d1ab7-f24b-5ae6-9e39-5c381cd0e5bd", + "version": "MTc1NjMyNzg2NjEzMjE3MjAwMA", + "digest": "sha256:6dab0c29e5eca7c7fe091895ddfc0128192f6b779720a1f9c57aa1cd97b28872" + }, + { + "id": "e3893f35-73bf-5810-ab2f-8e509cbdda86", + "version": "MTc1NjMyNzczODYxNjQwNTAwMA", + "digest": "sha256:cf2c581d7fff9cdab89cdfc79340efaea2b70192584d9226d791a344e6a4758a" + }, + { + "id": "e3a58ba1-c878-5eeb-8541-1be0017e5a6c", + "version": "MTc1NjMyNzg1MjY0NTY2NjAwMA", + "digest": "sha256:e7f9408f4fdc641ef62fa2190fe3253203d3b04e7daf2a34dbf667f9990e1f20" + }, + { + "id": "e3b2883c-ce58-5e99-a754-9da531d89a31", + "version": "MTc1NjMyNzgxMjY4MjgyMDAwMA", + "digest": "sha256:b62e50e69af1586cddd0ad0cd9d11950de739b181f9161713b12070bf09261a0" + }, + { + "id": "e3bdf844-6e4f-576f-a8a9-a6b5c4b7f9c7", + "version": "MTc1NjMyNzkyODc4NDM5NTAwMA", + "digest": "sha256:8abe4f5996aefbdb2357dd9d3f9c1bbd7f3e95b8663cac6716825acc150aa729" + }, + { + "id": "e3c0d72f-cbe9-58ef-8e23-1b01552d26bf", + "version": "MTc1NjMyNzczODQ1NjAwNDAwMA", + "digest": "sha256:3fc29ea7efebbb0ef3f3448a4a9ed5c3acbe4b22a81ed768921567a9067cdb3a" + }, + { + "id": "e3c972ae-e6d4-5d13-a557-58d28bf7c574", + "version": "MTc1NjMyNzg4NjU0MzAxNDAwMA", + "digest": "sha256:3ef4f4cadb176594b5baa76946438efd6f28355f403b311f33ec9e8d020a58c9" + }, + { + "id": "e3d92280-d3d2-5eb1-9286-e84059e471ce", + "version": "MTc1NjMyNzc3MDQ1NTU1MzAwMA", + "digest": "sha256:7bb131dfcbe7ceb838e0b55a082e1668fcc976dc4956b4fee11ae6ed2c152b03" + }, + { + "id": "e3f69764-815f-53b6-9bc0-91242bf34c43", + "version": "MTc1NjMyNzgyNjI4MzI2ODAwMA", + "digest": "sha256:ddc845a05ee7a738de046835603fd681e6c35bc2d6bd9bb03a8aa7a621c42db6" + }, + { + "id": "e42cc735-b85f-5bed-a1bd-5170639fa69a", + "version": "MTc1NjMyODA2NzQxNjIxMDAwMA", + "digest": "sha256:504adad7d43923e70cc7f43b647e809e594b406c03eeee1000b8f2703e153ef6" + }, + { + "id": "e45864bf-18d0-5c7d-90bb-1d5708d3181c", + "version": "MTc1NjMyNzgwMzYzNzc0MDAwMA", + "digest": "sha256:68eb34263124e18a4f4cd1a9faf5b69a65daf464827789467f90f92ae3f44959" + }, + { + "id": "e4760083-e76e-5a2e-b705-979edc50e978", + "version": "MTc1NjMyNzg4MDAxNzc2OTAwMA", + "digest": "sha256:881bc5af154eb0c970a8e3ee91e7d9dda8c553248f6c96618ee375ee7a9ae7fe" + }, + { + "id": "e478134d-9818-5d3f-bfe2-2c0a0b12016a", + "version": "MTc1NjMyNzg2NjE4MDk4NTAwMA", + "digest": "sha256:9649c14f0ef328b00077b00cb0cd4cb2883bf90f282410b5c665905acc2792f1" + }, + { + "id": "e484e6cb-9821-5d5e-906a-52f1b8c3f3b1", + "version": "MTc1NjMyNzc3NDUxODUzMzAwMA", + "digest": "sha256:b921659303f1840d562d0b1e8631206af96834383012276339bdd0507fc36013" + }, + { + "id": "e49721e9-4232-5537-8e72-14a9e9bfb699", + "version": "MTc1NjMyNzg1NTgyNjM4NzAwMA", + "digest": "sha256:513b419f772431252ebe84c32e0abc0f1bff764b647b895bd03ac6a284e4938f" + }, + { + "id": "e49f3d58-6caf-5cca-b2bd-556bdfa315b7", + "version": "MTc1NjMyODE3MTE4MjA3NjAwMA", + "digest": "sha256:dc1a9333d48b623d8d11efe1a212942bd402fd328c7592505e28589964d85b43" + }, + { + "id": "e4a0f740-2ca9-5193-918f-177fbda43168", + "version": "MTc1NjMyODE4MjE3MDk2MDAwMA", + "digest": "sha256:49b4abc81b74c943db70a2aab10a03589c3384712cd09f11807c7f4c72842d5d" + }, + { + "id": "e4a52ea9-4788-5897-b1a7-ff0fa2d7d6be", + "version": "MTc1NjMyNzc1MzQ3NTg2NTAwMA", + "digest": "sha256:ca37a527260613cf195a60654df59ce943739e9cd4761f4a505a6544ae383e3a" + }, + { + "id": "e4a904e2-9f50-5e6a-bfca-8406a47c5b38", + "version": "MTc1NjMyODE3NDQyODI5MjAwMA", + "digest": "sha256:94bdc08cb2c6f1c0ce9b299bd359cbdba86d87e35c38f41bee9daea7e747496e" + }, + { + "id": "e4b482c7-9b02-5c4b-a3bf-5a9a86e94d40", + "version": "MTc1NjMyNzc2Mzc4NTc3NzAwMA", + "digest": "sha256:47629c5bb941818a351caccbf072b5d669fb2edc0bf89cf6ee1d6e88e1aa7863" + }, + { + "id": "e4e34924-3be4-5a38-821c-d04c74ede0b7", + "version": "MTc1NjMyNzkzMjU0OTMxOTAwMA", + "digest": "sha256:6c52b3b1356e3e39dce4676bd61a951315b5aa8f543e8d8f85bac3fca506f0a4" + }, + { + "id": "e4e9bdb8-5947-5124-b6ff-4a8d07688fc7", + "version": "MTc1NjMyNzc2NjQwNjU3ODAwMA", + "digest": "sha256:dafcce73aab13c14f8712d0736657b30715a6c606d329787bbb650a0f41d1f2e" + }, + { + "id": "e4ffef6c-b736-5b12-92d2-9026e2f811b6", + "version": "MTc1NjMyNzc2NzcyMDkwMzAwMA", + "digest": "sha256:5b08e597d6408a90c2bad2f1ce6d8ecb1f05713f6cfb523f5f7752d5ea8edaad" + }, + { + "id": "e508a0b0-2111-5f15-84ae-08f2c853c89e", + "version": "MTc1NjMyNzg1NDU1MDQ0MzAwMA", + "digest": "sha256:58e16b3936aad0bb98106223eac525887425653ddecb3d2b66f60d9c6edfbaa2" + }, + { + "id": "e50c74e6-0897-5d1c-9310-cce09e035422", + "version": "MTc1NjMyODE3NDM2MjE3OTAwMA", + "digest": "sha256:d7c411b434052c4ddd7508cf1f087a95d8bfcb02dd858883acd2bdf7fd18f925" + }, + { + "id": "e51a5d9c-ff07-5568-a209-e2138e24cc3f", + "version": "MTc1NjMyODA2NjAwNjcwNDAwMA", + "digest": "sha256:774ddee01587e921bd7966f3e18f65ace826cb992b3236cfec77f5d94f1c5372" + }, + { + "id": "e5304738-13ad-5d13-b677-160c057f9455", + "version": "MTc1NjMyNzg1MzgwMDIxNDAwMA", + "digest": "sha256:8d1689b45c88b4216d12c4e0d22886703303515de885702a91c57c4ef0144f9e" + }, + { + "id": "e53f200a-3723-57f6-8967-e6b76951b0a3", + "version": "MTc1NjMyNzg1NTcwMzQ2OTAwMA", + "digest": "sha256:f517484487b8102945bd696e1946118eefd606f8153344710cde17f583b662bb" + }, + { + "id": "e5425099-8a89-548e-b7a1-f82492c85175", + "version": "MTc1NjMyODE3NDQyOTIxMDAwMA", + "digest": "sha256:871f0303f3e107ccb86c4769ee03071611f089fc8d5347d33ae9c2b52bc0daef" + }, + { + "id": "e545cdef-ede3-5472-b36a-cf1b5c88c93f", + "version": "MTc1NjMyODE3NDM3MzAwMjAwMA", + "digest": "sha256:ced26fb93800f26ef5e0312e87dbcb6980109426e733a8e06fdbd732125758e9" + }, + { + "id": "e572689d-11db-5f77-a5a9-9d4274a653b3", + "version": "MTc1NjMyODE2Njk1NzM0NDAwMA", + "digest": "sha256:542b4fa41436239c12d6beb73efd7d289bedbbd602b9fe53aaf6cb9d6019a618" + }, + { + "id": "e58c0da8-0ec8-5809-9154-acfe4c67971f", + "version": "MTc1NjMyNzczODQyNDgyMDAwMA", + "digest": "sha256:3d7ca61d5e5203720b0c127f58e8bce6bda263e15dc6aaabe04e2eaa85a770b5" + }, + { + "id": "e596fb38-f8cd-5444-a0f0-e2e1e15beda9", + "version": "MTc1NjMyNzc1NjA4NDQzODAwMA", + "digest": "sha256:6912db2abe0de1e7976774dc7f152d860f5fce50bcc459eb2a6d2a5cbdc90d2f" + }, + { + "id": "e5c439f4-edc4-5be3-92b6-062fb635e68e", + "version": "MTc1NjMyNzc2MjM3OTUxNjAwMA", + "digest": "sha256:f3dda21656f503153cb0ef3cfe67f1a29e6bfbac4e8ad41f5fa004c5724db5b5" + }, + { + "id": "e5eb869d-91e1-5f15-8c20-3bb1f546d082", + "version": "MTc1NjMyNzc5MDI2OTM5NTAwMA", + "digest": "sha256:0cfc976c436111db383282aacd1d2e4f78fca5c75c643ab888076e1bddfd65ac" + }, + { + "id": "e602f4a7-4d5d-5837-9833-a686f8218ce3", + "version": "MTc1NjMyNzgxMjczMzAwNTAwMA", + "digest": "sha256:adeb48638da126b4e93c7ce3a16839c3d210f8f2aa8b5c3432bde6fdf0a16eda" + }, + { + "id": "e63b498b-fac6-5e94-97d6-5ac98074a467", + "version": "MTc1NjMyNzgxNTY0NDY0MTAwMA", + "digest": "sha256:cf297800e48f9a9a4595ce574689e14f0b67d90547fb239aa8b300250d258551" + }, + { + "id": "e64d0433-8269-5841-a1a1-8b48703986bf", + "version": "MTc1NjMyNzg3NDE0ODI5OTAwMA", + "digest": "sha256:2eb03bb8ee31149dfd1eb54b69da9d1f1412092adbd618b7373bf5b425be5fdf" + }, + { + "id": "e654d61a-8bcd-5771-ad3f-78bd49a2149a", + "version": "MTc1NjMyNzgzMjY1MTc1MTAwMA", + "digest": "sha256:3058a95c6413cec78b6694e460ce4eaaf9cbfcb67be7c6368fbf99d058980092" + }, + { + "id": "e6586511-8c49-5078-9e22-ea0dd5d28505", + "version": "MTc1NjMyNzg1NTcxMjUxNjAwMA", + "digest": "sha256:748a4c3d812672099aad0141e6031ba9573db54f98dc85a73404456e971822aa" + }, + { + "id": "e67fe547-1d39-5fcb-85e4-308d97f2aaf9", + "version": "MTc1NjMyNzg2NjYzMDI5NDAwMA", + "digest": "sha256:4f82953ac95648abbb6b758fb4e9786cee9c86ef70118ca9ab6bd11ef014a99e" + }, + { + "id": "e6863ddb-5bd6-529b-940e-a8b01c758213", + "version": "MTc1NjMyODA3Mjg4NzA2OTAwMA", + "digest": "sha256:4fb0435b47d4db4e6ac022caf36528d8d93a5549e73883ddcaa3d3e20f7ed8e0" + }, + { + "id": "e68b4410-b551-5530-98d5-92ed4a0874b1", + "version": "MTc1NjMyNzkwNDkxMDY0NTAwMA", + "digest": "sha256:de18648e484af28985b30532be39eb667dee7ef9fdd5b37bccbdc0798eca1597" + }, + { + "id": "e696d4d9-08a9-5e83-b30f-44ebe3c63ffb", + "version": "MTc1NjMyNzg1MzIxNjEyMjAwMA", + "digest": "sha256:0dc28a15eb293b55554d22393c9e3b1762716959247e6493928a802d8d4f380b" + }, + { + "id": "e69f4271-6962-5f9c-9724-284a15c3b1fd", + "version": "MTc1NjMyNzc1MzYxMjk0OTAwMA", + "digest": "sha256:c3f82077dd39e4c5fbe8774219171ea07842ebf9487e1c6cb287884a3095da33" + }, + { + "id": "e6a0dafc-b3fd-554a-b13e-b664da5c3d3f", + "version": "MTc1NjMyODA3MjE0MzUyMTAwMA", + "digest": "sha256:4feab0e0e4b24649edeb0f1b1b3b35663a32228c40ea365f7e7a4cdf8b12d410" + }, + { + "id": "e6b12f37-7a5b-539e-acbc-51418e0ee4f9", + "version": "MTc1NjMyNzc5NjAxNzMyMzAwMA", + "digest": "sha256:d88f87793736f83afd18941f1968488054d6d34d4bbe0c10bed2e7173ff04d16" + }, + { + "id": "e6b28788-483f-57a3-b9ad-edebd4e687b8", + "version": "MTc1NjMyODA2ODg4MjExNzAwMA", + "digest": "sha256:1058f559cb80c51e68cb74a939f24992561c050ec0eb3d46d3fac506ebfac9ec" + }, + { + "id": "e6cdfe79-6e6e-57f3-8ff8-0cc1d33457ff", + "version": "MTc1NjMyODE2ODk0MDAxODAwMA", + "digest": "sha256:9f108eb64d62286af3ac23f521c230c7ad557355e8a40eedb4d3028cccdeeb3b" + }, + { + "id": "e6e241b9-46c3-596b-9c04-36be3cea1d90", + "version": "MTc1NjMyNzc4NjkzNTY4NjAwMA", + "digest": "sha256:7b2742513136e5c4f996940bdcd74a63a3698c1906f71eb933d191f54e69dd47" + }, + { + "id": "e6e8dd03-1c37-5e94-8e88-2ec49253b22c", + "version": "MTc1NjMyNzczODUwNjUxODAwMA", + "digest": "sha256:2f12c055ad21a7de145e9800d74a1380bb05219e9d9ae4fdeed56e8dcc419864" + }, + { + "id": "e6f9a328-8c5e-5eac-b098-e25ff089bada", + "version": "MTc1NjMyNzg1MjA2NjU5MjAwMA", + "digest": "sha256:9ca1bedfb902c8fb63384cfa6c9e30ac7f2765ea5bfa66a0c6e3b06127f2a6a4" + }, + { + "id": "e70fc55d-eec3-5756-aaaf-2b7b953dbfe7", + "version": "MTc1NjMyNzgzNTE5ODc1MDAwMA", + "digest": "sha256:9882c6571b9d3e9bdf3b4d56f94b9d3ca29f2243b1e11e822e3528c148e7dcaf" + }, + { + "id": "e7202bfb-0206-53e9-b709-9a597498accb", + "version": "MTc1NjMyODE4MTg4ODIwOTAwMA", + "digest": "sha256:4424fb8244e88df996414b48b734532facf880297fbdf86f6c996731fdc35de8" + }, + { + "id": "e7262240-d12d-5b62-988b-fc01bfce093e", + "version": "MTc1NjMyODE3NjE0MTg4NTAwMA", + "digest": "sha256:4591c88aae59ae608fe77c374c3ed30e1f3f99e1804fe07488819c9197d0666d" + }, + { + "id": "e7279809-1756-58f6-b5c3-93cadccf24e6", + "version": "MTc1NjMyNzkyNDI5OTU4ODAwMA", + "digest": "sha256:aa35aedba4aaed34e5a2d1dd3abe0cf3d71b8c1141a47a074df0994beef221df" + }, + { + "id": "e727bf6a-443b-5bcc-a2e4-4dcb16ca4bc7", + "version": "MTc1NjMyNzc3NDU0Njg4MjAwMA", + "digest": "sha256:4c5f00a692d53511b1aefdb9cfcaf8e48f0577c23bda5f9dcb38f84985a5431b" + }, + { + "id": "e734a7a0-8a1d-5e08-99d4-1123f9447ac0", + "version": "MTc1NjMyNzc3OTkzMzgyNDAwMA", + "digest": "sha256:e79f6e64b34e87ae5f6a3fbc81b0b5ef0e13f40a7f482703be899115b24968fa" + }, + { + "id": "e7482802-ab9f-500f-b5c2-7f7b7d60aae2", + "version": "MTc1NjMyODE3NzYzNTI5MjAwMA", + "digest": "sha256:111725380a094d093739ca15a87fa49924a235c72e0b0b371853a1f48e46e4c4" + }, + { + "id": "e760b94b-b462-5607-a7fe-4fa2caeed678", + "version": "MTc1NjMyODE4MTg4MTE2NjAwMA", + "digest": "sha256:e8c4ce3e7736c2c7cd1364abb5e5f79eba479e77ad6ca5d0c2b0f1573b6b7fda" + }, + { + "id": "e7782947-f43c-5386-b414-043a0bf0d8ec", + "version": "MTc1NjMyNzc3MzI4Mjk1MTAwMA", + "digest": "sha256:8dde1cff27573754532ab28540e7a4a16a33521faf398678826c74073003ef0e" + }, + { + "id": "e79f1686-2041-543c-814e-b830fc7f8464", + "version": "MTc1NjMyODA3NzcwOTAxNjAwMA", + "digest": "sha256:d4930547e50628e29b3ac29e0da44a3ee3157547e9cbd7a9ad8fb8ab9413b43a" + }, + { + "id": "e7acc963-5a87-5239-977f-88f09933456e", + "version": "MTc1NjMyNzg0MzA1NDYwMDAwMA", + "digest": "sha256:86dbe8ab6090a2b9730ab4e20379e30fc9b930021eb4f1ab228a0ecac37b3127" + }, + { + "id": "e7d1aacd-ebd1-50c3-a237-da08325c1100", + "version": "MTc1NjMyNzg3NDg3NjgyMjAwMA", + "digest": "sha256:67a2bfa7e5a1a90d0a207c15fd9f172de3917df5096b5f0439d4190afedb4cad" + }, + { + "id": "e7d6165e-9efb-52e0-a6af-84e2bcfad388", + "version": "MTc1NjMyNzc4MDE1MDUwMzAwMA", + "digest": "sha256:dd73b328e78499e30a7da971a2da323abe0cec10114b12558fa99f0d8f50840a" + }, + { + "id": "e7dc63d2-1b57-5ef7-b044-9e97a6a91843", + "version": "MTc1NjMyODE2ODk4MzI4MDAwMA", + "digest": "sha256:237014e68105ff6102d505d39320c73974bb1b748057ecfd8e9c67a0fc229c38" + }, + { + "id": "e7efa911-6895-5ad0-8444-986b384f2a7d", + "version": "MTc1NjMyNzc1MzU4MzU4MjAwMA", + "digest": "sha256:5ce4c9f2433c2924c8435c41e1c650616ffcbde9213a58d09dbf76b9c618b435" + }, + { + "id": "e7efb851-7a46-5042-8859-8ef4f1ff747b", + "version": "MTc1NjMyODE2NDk3NjcyODAwMA", + "digest": "sha256:0548450c248af31aa88faf6c5d6c1e043b74a5bf85656724034ccd7d55e4e1df" + }, + { + "id": "e7fc0bd6-f85f-5fe2-92c0-29e690b9fc49", + "version": "MTc1NjMyNzg3NTU1MzA1MjAwMA", + "digest": "sha256:78c954d72f6f90a3dbdb8b235ddb05fc17742a97389b912c32e8cdcd8de57ea9" + }, + { + "id": "e8118f1a-77d6-53ca-bce8-c235436569ce", + "version": "MTc1NjMyODE4MjEwMzQxNzAwMA", + "digest": "sha256:537ea70f3a40fc0675da1e3636bdb3960bc3bbc4ee94e6705ef246d5add7f16e" + }, + { + "id": "e81ca225-b196-52ed-93af-ff8da5309f17", + "version": "MTc1NjMyNzc4Mjg5MzI1MTAwMA", + "digest": "sha256:6800002e71f371520fe821ce48d1cdd4b052835dac8a777d71ed69a05f77297c" + }, + { + "id": "e82b4990-66bd-5bdb-8a68-24ea302a2dfb", + "version": "MTc1NjMyNzgzNjI1Mzg1MzAwMA", + "digest": "sha256:7a22bf92f18e176ec2fe8bdc689bb4bad6e0e79740e4d65fc50d0f51f04b34d0" + }, + { + "id": "e83fedf9-95c3-5507-b676-b0a07eb7c185", + "version": "MTc1NjMyNzc0NzY5MjUyNTAwMA", + "digest": "sha256:e2da31537c3d89b87c32b356e41bfb842abaf3ad2182d4909d1ecbd92a57a962" + }, + { + "id": "e8442af9-2c60-5a16-9b16-17049cd248a2", + "version": "MTc1NjMyODE3MDAzMTg1NzAwMA", + "digest": "sha256:b618bb6b53b188cb67c06b7d38422b5d846b13c6e42242128b83c1f79720ecab" + }, + { + "id": "e84aa9ce-3da4-5d06-bcfe-fd792e5f8192", + "version": "MTc1NjMyNzg0Nzc5Njc5MzAwMA", + "digest": "sha256:5d2cb4c589c53c05a66ca06a1a68fc0c85340e838ac7b58352f088524feaaebd" + }, + { + "id": "e858abd2-22bb-5d1b-b420-87f85f591d96", + "version": "MTc1NjMyNzc5NTE1NjA0MDAwMA", + "digest": "sha256:109727c7c45ec967b9e48da8b8940982d67dd71b7bb5f0ece7013512fefde795" + }, + { + "id": "e85de9fa-8cb9-5f40-97fc-394ede8d28e8", + "version": "MTc1NjMyNzc0MjQzMzczMTAwMA", + "digest": "sha256:39d11755ee6b3b58c65e72631fbdaf0142892248f84bfec68cf3a550cc2db659" + }, + { + "id": "e8605c2b-13fc-5839-8eb4-1d0e9945c5cd", + "version": "MTc1NjMyNzc3MDU4ODM1MDAwMA", + "digest": "sha256:b8c20efeecadd5d7ab560cfa7869bc9a17426df1edf17a8902a7da5bb74a2843" + }, + { + "id": "e878da17-ffa4-54fc-a994-369c5af43c03", + "version": "MTc1NjMyNzkxMjY0NTI2MjAwMA", + "digest": "sha256:6a3f2800c36973eb41b81dc97eaab7f5fc915f94e7de42f936d80bfea9f9ec78" + }, + { + "id": "e87cc066-340b-5ee5-820d-84ee104d07d2", + "version": "MTc1NjMyNzc2NzcxOTg3ODAwMA", + "digest": "sha256:d44568c88fd48fec6b4e3554d920b7913ec00d27264fa0c8e676bf6260e2f59a" + }, + { + "id": "e8820ac5-b99a-522a-bfc0-63ca85db99a0", + "version": "MTc1NjMyNzg3MTYyNTEyMDAwMA", + "digest": "sha256:8d4e665807b28076a4cb2ba6942f49dc93b64256023a5dcfb49bca31a8db7b8b" + }, + { + "id": "e8a3ce93-0655-58cb-b8df-6cc54da77d98", + "version": "MTc1NjMyNzc3MDQ0OTgxNDAwMA", + "digest": "sha256:8deedfec227a254dc65f6cdda03cc0a43dd8408b3301423fcfc57e2c68ce04ec" + }, + { + "id": "e8b33cdf-8ba1-507d-9ed0-b99edfb2f16a", + "version": "MTc1NjMyNzc3MzAzNzczMzAwMA", + "digest": "sha256:7184c2049ec51f9493dc65686a8c1f9a8e426466eb7056e06e640ad215ab2c06" + }, + { + "id": "e8dc165c-572f-5a53-a3b7-6bfd25db26b0", + "version": "MTc1NjMyODE3ODA4ODQzNjAwMA", + "digest": "sha256:393101aafef751195877820ee99107c37d5102dc0482e2bce4d35934538a27e7" + }, + { + "id": "e8dfa841-c904-5a31-b6e4-b716e5394802", + "version": "MTc1NjMyNzc3MDQ5Mjk0MDAwMA", + "digest": "sha256:1e1fdbc10dcc20420f1c5f2050a6242abada7e6deaf49e158692c96c612cb045" + }, + { + "id": "e8f3b420-2fcc-564d-bdfb-66684ab75ef9", + "version": "MTc1NjMyNzc0NzcyNzMyNjAwMA", + "digest": "sha256:ae44e2610ab28568a6acedbf9ce8ff52716cd713cba7ee0409e022a8ee6e42f4" + }, + { + "id": "e9034c52-957e-5319-8ec9-723ddd147211", + "version": "MTc1NjMyNzgzNjE4NjE3MzAwMA", + "digest": "sha256:5e5e09c75b6339442546e5204a0f4035f9aea7ca0d35a4d8d954616d6dd91136" + }, + { + "id": "e91201b3-92ac-5745-98f8-3c275d7e8726", + "version": "MTc1NjMyNzc1NjgxNzk5ODAwMA", + "digest": "sha256:2c14b78f37bdaff888fe478cc7f3de8da77f6c93d614e19c49ef3c682de50ae2" + }, + { + "id": "e9301999-3b21-5f36-ad71-878b5ecd4df3", + "version": "MTc1NjMyODE3NzU1MTc3ODAwMA", + "digest": "sha256:68cf9583b22a0fb2eb625e1ccfa2a7eed721506b2510cab792c440c213c82265" + }, + { + "id": "e947c1ed-8241-5878-b1d9-dd6a15ba2ffb", + "version": "MTc1NjMyNzg3MTU2OTQwMjAwMA", + "digest": "sha256:701a7d4762b462b9e9f0edba53ed6c7887b92ae8105e744e007f25f34649651d" + }, + { + "id": "e959416f-d15f-5661-bca7-62dced925c66", + "version": "MTc1NjMyNzg1NTgxNTIwNjAwMA", + "digest": "sha256:d123c0e1a4d30e02c8b918ba20be92a60c285e9d5f3fd650c3314d74584f4649" + }, + { + "id": "e96acf56-4913-5d9a-a659-b29de2059320", + "version": "MTc1NjMyODE2OTk3NjczMjAwMA", + "digest": "sha256:d9c93ef53f5a46dc3ef30688a46d2d794737195f9bf2c031aa552089e094822b" + }, + { + "id": "e99569c6-4335-597e-88b8-8322ee2ff25d", + "version": "MTc1NjMyODE4MDQ0NzA2MzAwMA", + "digest": "sha256:c3fda9ffb34ea0fa98667c13ee4c2f5928716d1f1de2d675b3af32fa955fdbef" + }, + { + "id": "e99f4d70-36da-531e-a967-6aa9e185b801", + "version": "MTc1NjMyNzc1NjkwNzY4MDAwMA", + "digest": "sha256:c3409ac2ae08fb74d328586555de957c0bf77ab3434fa9b45ca04132e2b5eb92" + }, + { + "id": "e9ae4733-8ee8-52bb-b24e-74f175fff998", + "version": "MTc1NjMyODE4MjE0MTgxNDAwMA", + "digest": "sha256:211b14364590d077f3fbe20a0c15277a9ff27bdced22f3eb726f08f5a7645613" + }, + { + "id": "e9b35b4c-0d7f-5673-8f04-32cdbbb3aa16", + "version": "MTc1NjMyNzg3NDU1MjcxNzAwMA", + "digest": "sha256:e0ab3aa44abc7cdff41e9cab090f250b97b3342fe85de75d054b45fb7927bfbb" + }, + { + "id": "e9c1feae-b66d-54bf-9957-f4298f5c7183", + "version": "MTc1NjMyODE3MTEzNjgyNDAwMA", + "digest": "sha256:0afb3d214800c34868ccb925a53acb343d24024e00ad84b19a65765c86d52425" + }, + { + "id": "e9c2dfda-bf3b-5697-a60d-54f0d040f846", + "version": "MTc1NjMyNzg4MDMxNzIwMDAwMA", + "digest": "sha256:877fa730ec3050c4dd401461404a51160285140ee719baeb1136ae66755d0168" + }, + { + "id": "e9cdec82-a4bc-56cd-8293-46ccafaa42c0", + "version": "MTc1NjMyNzc1NjA2ODY5MDAwMA", + "digest": "sha256:56112befb79ed3374666342b27cdbb6e0d64a72d1cb5c1840504ac6cd44f19be" + }, + { + "id": "e9dab656-70c5-5212-8f1e-1527e862e820", + "version": "MTc1NjMyNzg3MDQyNjY0NjAwMA", + "digest": "sha256:7060d6e57febb13948d5a80f8c294044740e3ee92f31799fc684e4870d7d8d3f" + }, + { + "id": "e9f9c45e-c109-59e8-b5db-fe1fd54f18e0", + "version": "MTc1NjMyODE3MDAwMDk5MDAwMA", + "digest": "sha256:aa13b966780c9618f8e5d5c4007ddeec99b54859060f98fd73387aed9fd08b61" + }, + { + "id": "e9fa055b-2225-5da6-9605-140629e2d350", + "version": "MTc1NjMyNzc4MTk3OTk5MDAwMA", + "digest": "sha256:eda86fc1c2717bcf638bcfc630c1cd7f29a835bf0075b33fe347348a530741f5" + }, + { + "id": "e9ff64f0-0de0-574c-8f54-cef2a45ee06c", + "version": "MTc1NjMyNzg3MjAwNTcxNjAwMA", + "digest": "sha256:5f5ae63d1dbcf5b2d402892b40da39fd586313e267e26b13ab0be2498c3fd56a" + }, + { + "id": "ea0b854f-351e-5341-a753-b5cf24758960", + "version": "MTc1NjMyNzg2OTQ5Njk0NTAwMA", + "digest": "sha256:28f206f2b5cd0e0df7eb719119cf1ec9df6d208c5c5420192d5b22643ea5e4c3" + }, + { + "id": "ea313d9f-92d8-503c-94b0-f4c42d0a044c", + "version": "MTc1NjMyNzkyODgyMTk2NTAwMA", + "digest": "sha256:0a43839e2920c8db036e7ae4c35253ec0fc15a2bfc56607dbdae9d556f1f7c90" + }, + { + "id": "ea325e92-ffd7-5a66-8470-ceb4ff28b7b3", + "version": "MTc1NjMyNzgyNjQwNjYwMTAwMA", + "digest": "sha256:f620682dacc0c02a8f8c0a521f0167f0bb8290b06e9b25f0871bafa70e365d09" + }, + { + "id": "ea982344-7d78-5c02-9935-b525ed02e224", + "version": "MTc1NjMyNzc0NDUyNDM4NzAwMA", + "digest": "sha256:c314c938b4fce7f6562ca81b779cf20adef0f77d4c0d39cb84e2f9963c387fc0" + }, + { + "id": "eab333bf-c711-521d-a5ed-51c4dc2900d0", + "version": "MTc1NjMyNzc1MzYwNzcwMzAwMA", + "digest": "sha256:c9caf130dd0199eb84872f2bba18db0da291caf7a76b71f7574075a99bd6c22d" + }, + { + "id": "ead43cd4-0f65-51b8-9b7b-bc01bcd594a1", + "version": "MTc1NjMyNzgxNjM5NTI0NjAwMA", + "digest": "sha256:395ffb39f02ab140b25ef234abf8d76f64cd28831e4848a13969c7970d803639" + }, + { + "id": "eae4365c-0812-5f69-87d0-2d197f51a174", + "version": "MTc1NjMyNzc4MTkzODQ4MzAwMA", + "digest": "sha256:2edf68192fffd70ddc43dc755903bdf35058d7ac4814aaa9435f845a4ba5d95f" + }, + { + "id": "eae9f607-f86b-5b7a-a6da-30af0aaaf66e", + "version": "MTc1NjMyODE3NjgzODA5NDAwMA", + "digest": "sha256:03a69d22ce80de53f7eeaff12f52bb478bc26924f0c34ff17489167fce4bd7b2" + }, + { + "id": "eaebf157-b429-5e29-8cd2-ad1551d2edae", + "version": "MTc1NjMyNzc3MDY1MDQwMDAwMA", + "digest": "sha256:819d1e1cac36d305a12e854415d9625d151ab9e26890c5cbef6750ffb019389a" + }, + { + "id": "eaed3b5c-e196-58e5-942f-fde5a984efbc", + "version": "MTc1NjMyODA3Njg5NTk4NjAwMA", + "digest": "sha256:a00ff59d7a0d512431fb6b11de58936bfe886f9b932c8d57cede4636e3ffb2f1" + }, + { + "id": "eafc394e-0e08-59d2-a786-9ac0176be7c3", + "version": "MTc1NjMyNzc4NzQ2NDQ0MDAwMA", + "digest": "sha256:e2c0e2fcd4170dc34fb42894ac1fa6227fbe0cbfd45d14b70ec7c3d933079327" + }, + { + "id": "eb0231a3-a9e2-5137-895b-9fabb5e6b718", + "version": "MTc1NjMyNzgwOTIzMjM1OTAwMA", + "digest": "sha256:84e160602000cb31c96c88469b28c066f2da50a7f482304055f04b35075ebc8c" + }, + { + "id": "eb15f02f-1352-532b-9eb5-90dd7be9ae06", + "version": "MTc1NjMyNzg3Njc3NTEwMDAwMA", + "digest": "sha256:0f6380629ebe19be2d2c2754bff45b2dafeea788377adda781ec762670d76389" + }, + { + "id": "eb1cf03d-359d-518c-90d7-de0896ec6747", + "version": "MTc1NjMyNzc4MTAzOTk1NzAwMA", + "digest": "sha256:0219ecb5e7486a3b49cb0da4f2b2620f1a38955854f04a6b9821320c108b1714" + }, + { + "id": "eb454c99-a5fd-539f-ae25-af6ccc8c1711", + "version": "MTc1NjMyNzgwOTIxMTEyNjAwMA", + "digest": "sha256:eea3f9708f3997d0979ffce6f2b988cf904172f4bfd5e2d5da05d99027e21695" + }, + { + "id": "eb4962c5-70b3-57cc-9ffb-7cb122a1ad4a", + "version": "MTc1NjMyNzgxMzUzMTc0NjAwMA", + "digest": "sha256:46a469dbef5e3bd3218f978dd3056c9601c419e93c712c74ffcf464ad367690a" + }, + { + "id": "eb5042ce-8284-5e10-a132-b835f2f4ebb3", + "version": "MTc1NjMyODE4MTkxNzE5MzAwMA", + "digest": "sha256:531bd6daf1037a32b3dafb00ae7285266be8d0b12e67b9299ff0b271acc45917" + }, + { + "id": "eb7a75d6-47d9-513e-8c13-98ddfd94fb1a", + "version": "MTc1NjMyODE2ODk5NzkzMTAwMA", + "digest": "sha256:dbf46e3c978d192a7feef7e7f95833fd97e437a3c34711b1bea7928acc40b24c" + }, + { + "id": "eb88deb4-05d9-5222-a1e1-08d0ebd15c85", + "version": "MTc1NjMyNzg3MjA1NTIzNDAwMA", + "digest": "sha256:f35cdd0334f6bd757436651ce759688ee06525ab34c758f7a495970350f767ea" + }, + { + "id": "eb9e7605-9e91-5187-9d42-1ccb9df81696", + "version": "MTc1NjMyNzg1MzA1NjY0NTAwMA", + "digest": "sha256:83a8d5569553cc8ac2a95d51991c214286e90289b14f9cbdfbc9564c07491976" + }, + { + "id": "ebe62594-ee18-59ed-a743-18a3d122dab2", + "version": "MTc1NjMyODA3MzczODgwOTAwMA", + "digest": "sha256:d2bb0ea2c9df68202bdaf3f2f2d23a20dd8a8f8a50690d7c756765716be03afd" + }, + { + "id": "ebe79c6a-f71a-5496-9b34-e69fd29e7a2e", + "version": "MTc1NjMyODE3NzYzMTA5MDAwMA", + "digest": "sha256:2ba5f8e0bc55bdfcb03ba094ba44cd26f95efb11f5e599c7328b784b66b48095" + }, + { + "id": "ec0ff28a-d522-5ffe-9794-c3afbcbca553", + "version": "MTc1NjMyNzc0MjI5MTEwNTAwMA", + "digest": "sha256:1675a53eabcaa6cbe6020189e35cf0657b00bda9e2a69cb2a259dddcd4ec0121" + }, + { + "id": "ec296792-5801-552a-8193-50d7e0438b8a", + "version": "MTc1NjMyNzc3NTI1NzIzMjAwMA", + "digest": "sha256:8dd2e2cfd199d9f1228bdd3567dfd6f941d9e5af803cb27115d7c809254dff00" + }, + { + "id": "ec34a2e5-38ec-5122-b0fe-1b3bb47562af", + "version": "MTc1NjMyNzkwMzY5NDk3MDAwMA", + "digest": "sha256:5f592ea56acb19a84aa671e2137e25808776d92ff345f0051f6eaf965ed96537" + }, + { + "id": "ec38bf96-b9e6-5da6-a48f-98ff112d1124", + "version": "MTc1NjMyNzgxMzUzNTEzNzAwMA", + "digest": "sha256:c7bab3e16c0c2fd9017ff142637ae9a8220be54f1e1b0605ddb9851dcb3047d1" + }, + { + "id": "ec3988e6-430e-5b1f-9a68-220837984a10", + "version": "MTc1NjMyNzc1NTk2MTAwMjAwMA", + "digest": "sha256:de6f4d73107f51e0d76a7b053d32e038b98127df39229bcdbe9177f3a70a8c91" + }, + { + "id": "ec39ddc6-072f-53ee-b7a5-c4f906856100", + "version": "MTc1NjMyNzg1MjcyNDMyNTAwMA", + "digest": "sha256:a7f8e065b4476dba7f70b171dde974cbb89b1fc46b4c7c4450a0cc8016b2bf49" + }, + { + "id": "ec5a5a7d-1db6-5167-96c8-c0700aa09b0a", + "version": "MTc1NjMyNzc3MjYzMDYwMDAwMA", + "digest": "sha256:86f5796224604e35ed9083fa86a2eb9d2c6465aea84b0e516ac15e1a58b3281a" + }, + { + "id": "ec74f8f9-0d3f-5b99-925b-e75ba5d592be", + "version": "MTc1NjMyNzc5NDc0ODM3MDAwMA", + "digest": "sha256:1054e02a1e74f1470294b3840b2af74b575530e252002e9cf4b9adbac1623107" + }, + { + "id": "ec7565f4-785f-573f-be84-990edc7365ce", + "version": "MTc1NjMyNzgyMjgyODgxMzAwMA", + "digest": "sha256:470f399b14739345318d4b60f990cc099cc17be38589365d575028763d4e6f11" + }, + { + "id": "ecb1e72e-dee6-5cb2-9232-0bc96308da2b", + "version": "MTc1NjMyODE2ODkzNjQ4MzAwMA", + "digest": "sha256:f630946ea5cb6b6997a4f8d2f91ad6dce3460e62529bc566e4cecf702934af7c" + }, + { + "id": "ecc2fa54-045d-5810-8d2b-bc3bb266d78c", + "version": "MTc1NjMyNzc0NTc1OTAzODAwMA", + "digest": "sha256:b9367c7aa25dfbb3c79c64aaa3e40977526c45336bddc351445f8372c50c3c0d" + }, + { + "id": "ecc564ca-f342-54ce-bb1a-f6cb82257a5a", + "version": "MTc1NjMyNzg1NTgzMDc3MTAwMA", + "digest": "sha256:fb946c5ad45377b694b7e05bca1fe704775ad6a8a8552cb7e00201dffa609cd2" + }, + { + "id": "ecc6f643-447e-5baf-a0d6-ad41361bf233", + "version": "MTc1NjMyNzk0MjI2ODExMzAwMA", + "digest": "sha256:01c26cef28fe1390b7e24dde1f7bdd49d36347febc57903d04798b369ba94912" + }, + { + "id": "ecdda9f2-51c0-5d72-a132-8f87763f8811", + "version": "MTc1NjMyNzg4MjAyNDU4NDAwMA", + "digest": "sha256:5b87aa7ded3b37aa36936345f1060836f24ad749a09aeab69b232bef7683e672" + }, + { + "id": "ecf809b5-fcba-5161-b90c-008cc12502a7", + "version": "MTc1NjMyNzkzNTg5NDA1MDAwMA", + "digest": "sha256:dec10c2807791d922faabfa079ded5be16f41e7b15a6136da5b2c6fafba847c9" + }, + { + "id": "ed0c5fbf-d393-5c3d-9606-aeaca48efb97", + "version": "MTc1NjMyNzczODUwMjM5NTAwMA", + "digest": "sha256:8c8f617b2b669d2524ae00dd3461b8640204fd1f766c8a1057da5d9909fc823c" + }, + { + "id": "ed3c1359-772a-54e0-8e94-820d24cd16c1", + "version": "MTc1NjMyNzc0NTA3NTg2MzAwMA", + "digest": "sha256:9ad14a13977261046b133acd2aa9746fdb28034c1d88bd6f7382110e274402f8" + }, + { + "id": "ed40f583-edf7-58c1-8563-c3cf99fb471c", + "version": "MTc1NjMyNzg0MzA1NDk0OTAwMA", + "digest": "sha256:507b0b06c0158b68084be32c92dfa519bd91d7bf03591b649ab413aa9d07ca20" + }, + { + "id": "ed43e89d-df01-542b-92e6-cd1551bc15b0", + "version": "MTc1NjMyNzgxNjM3OTQzNzAwMA", + "digest": "sha256:2e3569d5548ff11bb950c57bcbaae404e93f2862d3bbb132e6be0e9a2c387c75" + }, + { + "id": "ed4421f7-1bdf-5ae7-a1db-588395726b74", + "version": "MTc1NjMyNzc0OTkyMzM5NjAwMA", + "digest": "sha256:9a174b8dd8a8e304aa53a3a41bce86d1a01e58c07e95e86b519b612d6e7ec0bb" + }, + { + "id": "ed601623-6552-5ddc-a0a4-ccb18e3dbdf3", + "version": "MTc1NjMyNzc5NTIwMzk3NzAwMA", + "digest": "sha256:91d84489c14d16111272dfbe4ba2fc1c598794538590a866c5e48d7fe33258fc" + }, + { + "id": "ed6a3011-eda0-5ee5-b00e-6cc44f2fb620", + "version": "MTc1NjMyNzc0NzcyNzI3ODAwMA", + "digest": "sha256:5150cb2f475b45671d30579b67404cc5714bec4e96ef6a463c79bd875f35c9d8" + }, + { + "id": "ed8cc055-65df-5aa1-856c-ddfefd82c890", + "version": "MTc1NjMyNzg1MzIxNTEyODAwMA", + "digest": "sha256:7ca11b9f93c3eacf3e7bf50421881f01c1b7d78c89362e923e196affd4832467" + }, + { + "id": "ed9fd8ce-9928-53ad-bf0b-de1c2b83da56", + "version": "MTc1NjMyODE2NTgzMTI1MDAwMA", + "digest": "sha256:54c457a9682647628e9fa2a3b8f7fc5f9d8d5495694dd59dba34ca19e7971dc4" + }, + { + "id": "eddbaff2-690a-5edd-9884-9221d58c614b", + "version": "MTc1NjMyNzc5MDMwMDg2ODAwMA", + "digest": "sha256:a78b5d2b8764b549744bb71dadc65bfc3d572b2e8e063d51c8d5a4a4ce053813" + }, + { + "id": "edebdc96-7043-5bc2-bec2-6b0207f9145d", + "version": "MTc1NjMyNzgwOTMxMDUzMDAwMA", + "digest": "sha256:f44e9cd3c05cbc66c10b32a6eba1dd1739b0b0844ea897505c0c6db1d67719a2" + }, + { + "id": "edec0773-5dc5-54e7-8bce-6d04f5675cf8", + "version": "MTc1NjMyNzg3NjYxMzA2MzAwMA", + "digest": "sha256:25507c29d96a7105be4c809441dd6b69982c7473ba1368b32dcd8b765854c2df" + }, + { + "id": "edf92fbe-ab0f-5be1-bba4-caa9d0a89a16", + "version": "MTc1NjMyNzc3MDMyNjMyNTAwMA", + "digest": "sha256:2ae39e2223f1a58eeeaa2f8c5880adc1461e5eb8ed40fb3f7a33ea722600b6ca" + }, + { + "id": "ee4078fa-eac5-5085-833c-8060e8b180e4", + "version": "MTc1NjMyODE2MzA0MDEwMzAwMA", + "digest": "sha256:11756860c281906c24e39de007f099e3a28ca9f22254a79bf96590a82434aa48" + }, + { + "id": "ee5f54e4-afcd-59ef-a9b4-0cb7bf3b943c", + "version": "MTc1NjMyODE2NDU3NTY1MzAwMA", + "digest": "sha256:5cff25cbdd6c157c46a7fda6b92841b4d2454d28329a3b3179886d3b4e8faa9f" + }, + { + "id": "ee6a0ef7-87e0-59d7-a778-d2843418d3d2", + "version": "MTc1NjMyNzc3MDQxNDk4OTAwMA", + "digest": "sha256:e7bc1093f8b2a70cd1a0022c7b39efa2edcc47415b72fc1ec38f37bf17eaa96c" + }, + { + "id": "ee9e779b-ceb6-5be4-8472-181670ec0f94", + "version": "MTc1NjMyNzkxMjQ5OTA4MTAwMA", + "digest": "sha256:9fcdb8b32359f113776dd8b422df7f4be5257229efcb573aec64f9f52cfd7857" + }, + { + "id": "eea3a8cb-1ab6-5cdb-b0c5-0d38dd0348aa", + "version": "MTc1NjMyNzgyNjU3MTQ5NzAwMA", + "digest": "sha256:d976546ef09fe8c1baa141829b1370e39a0edd20ca4331f23a0214eca21e6ba1" + }, + { + "id": "eeae265f-4370-5b87-ad7f-30de84d85ccb", + "version": "MTc1NjMyNzg3MDQyMDAxMzAwMA", + "digest": "sha256:e9102fdf27f8ef80179f0c44276ccfbb851a10598f00fa3cfd4ff4d4d494055e" + }, + { + "id": "eeb3bf9b-1a94-558f-8009-6c7f862611b6", + "version": "MTc1NjMyNzg1MzIxNTQzMTAwMA", + "digest": "sha256:23e2b0d97fbfdcdb790e6471e99cc45752e5aacaa930a62a7c1e6c09f0971370" + }, + { + "id": "eec6b1ac-d065-52ae-aaf9-1b3afff7ea56", + "version": "MTc1NjMyNzc3MzAzODI4MjAwMA", + "digest": "sha256:423c475f82b7a94111db5b6804c99b172d56c77fa0f8cf0d76fc3bd4832308cb" + }, + { + "id": "eecf34e9-694f-5df3-bef7-e07eab628c7f", + "version": "MTc1NjMyNzg3NDE0OTMzOTAwMA", + "digest": "sha256:4adda73411e9168a858bd60aaf3d0170fdcfd6e72088d984eb74673124032dd1" + }, + { + "id": "eed86752-7dd6-57ad-bf04-864639580545", + "version": "MTc1NjMyNzg2OTUyMTYyMzAwMA", + "digest": "sha256:2567b4d95bf7ca2d9ea27529cc31b7bd09d259e933f90f06f7ade4e0da5a0082" + }, + { + "id": "eed9c8af-e396-5fdb-9340-583bdaec6654", + "version": "MTc1NjMyNzc1NzUzNjYwOTAwMA", + "digest": "sha256:b8a96753d3d4291c22ed614bc816cc2d53c53bcce2d974f109519ae9d6ecffaf" + }, + { + "id": "eee48291-0b12-50e4-90f4-cbd943d04278", + "version": "MTc1NjMyODE3MTIxNjI3NDAwMA", + "digest": "sha256:f10a58f94f9b1c38d5e239808c504da17564a0ff406890bb1cf0647ebac0da4f" + }, + { + "id": "eef8c1ee-2829-54b7-89f1-eee317e4b43e", + "version": "MTc1NjMyNzgxMzUzNTA0MjAwMA", + "digest": "sha256:c8f490242486a4aab90c955a5bf6cb4f5bacc1dc43721b30c69a9e584fc27224" + }, + { + "id": "ef0d3f64-e212-5c34-bd30-e51ca0adbe86", + "version": "MTc1NjMyODE3NjIzNDc3OTAwMA", + "digest": "sha256:83b360a384db4592184fae49a39b50ec63f08b3e53035335cedf3ee794d12c38" + }, + { + "id": "ef1417ab-6819-5d6e-ad3b-7e2fccddd3a1", + "version": "MTc1NjMyODA3MTIxMjc2ODAwMA", + "digest": "sha256:c25b456d7bc29892f1ece3cff30dfe3eff586a5475ebad0f21ba3c6037c0793e" + }, + { + "id": "ef2537bb-a299-5c48-89d0-fff98f23915b", + "version": "MTc1NjMyNzc3MjUxNjc2MDAwMA", + "digest": "sha256:f1f8be37270bd3e52cc566fbc3541a961dc14980c0971fbfac4998c6761070f9" + }, + { + "id": "ef495f26-db64-51fc-8f1a-fd60de9080bf", + "version": "MTc1NjMyNzg1NTY3MzM0NzAwMA", + "digest": "sha256:0f38602b19b630523c373d229dbc2539235956befbd93d9745d5ab20322f7e69" + }, + { + "id": "ef560f6d-2ea9-5113-aacd-b5ae8890340b", + "version": "MTc1NjMyNzc3MjYzMzEyNzAwMA", + "digest": "sha256:703a8e3565476c3e76eb27d0103bc69a7e4d0d8bf5ae7df12dce516663dac4b3" + }, + { + "id": "ef566100-782a-5be1-b5a3-5a98428d19db", + "version": "MTc1NjMyNzg0MTIzNTEwOTAwMA", + "digest": "sha256:a133b3f935e6e5b5704b16ed2f9fbecec8fe238b2ac5d5abe74b7aee83e013a7" + }, + { + "id": "ef7d4152-179b-57bd-ac25-c10341beb05e", + "version": "MTc1NjMyODE3Njg0ODczODAwMA", + "digest": "sha256:3159bbd841860bcca80df35810f1a264e6964d65ad8c5b2941bd8ddcc06178b8" + }, + { + "id": "ef853d6d-a11c-5222-821d-eb54c0309856", + "version": "MTc1NjMyNzg3OTk0NjI2NjAwMA", + "digest": "sha256:5787f7321a7c81d6e05297713a923ee3f533c0b294a96549fb2e20ed6b925f91" + }, + { + "id": "ef920e29-aa56-59a5-a692-57f5c96f871d", + "version": "MTc1NjMyODA2ODg2OTg0MjAwMA", + "digest": "sha256:26c5cb0df2f05499b5cd1460c57a21a9747420ce5f850bb8edea45972df04af1" + }, + { + "id": "efa587df-3334-5188-b842-8911302ccb1e", + "version": "MTc1NjMyNzgxMzUzMTI1OTAwMA", + "digest": "sha256:a9d90e88403eb6ba13cbecdf51698c2736ec33200047578f2ec58af4fefdee0d" + }, + { + "id": "efb5a7f8-a5e2-5c91-8df6-c3398b816e46", + "version": "MTc1NjMyNzg2NjI0ODAyMDAwMA", + "digest": "sha256:120393264c801b12d91810e4ff4e2232bb9748560b5bc36f1b665d527e9f1c80" + }, + { + "id": "efb5f711-2129-5af6-87af-bfeed62fef61", + "version": "MTc1NjMyNzc1MzY1MzQ4MzAwMA", + "digest": "sha256:428bed2d02d48bff368a0bbf9fab656f443daea69e090b0688ac345316404003" + }, + { + "id": "efc27c5c-d616-5ed1-9b72-c9e92ea8f20a", + "version": "MTc1NjMyNzc5NDY2MTk3MTAwMA", + "digest": "sha256:90a4f3c15da913e18db2e30189e92e7ea7de54ff7d74d418186db39d18bf150a" + }, + { + "id": "efda8017-a301-5dbe-ad50-9f12577e0aaa", + "version": "MTc1NjMyNzgyMjQ2NDEyNTAwMA", + "digest": "sha256:92840f0a25252c40d68ded698774d5037bc1d73c4e2bf0745bff6a907e0780c0" + }, + { + "id": "efe313d2-40ed-531a-b285-9cda34635b6c", + "version": "MTc1NjMyNzgzNTIzMTk2ODAwMA", + "digest": "sha256:657a1aa0fb3a3bf03cd8deb4a28392f20d97d1d138e44d29414238a9b30a5daa" + }, + { + "id": "efec5710-92ff-55d7-aa27-a93813ee3a1d", + "version": "MTc1NjMyNzkxNzE5Nzc4NzAwMA", + "digest": "sha256:2d6b2349271e0322b456ab3e2e2506eb10380bce1f287474f723bc1a88186397" + }, + { + "id": "eff0ade3-18b9-5e36-97f1-70cfa3b40d5c", + "version": "MTc1NjMyNzczODQ1MDUyMTAwMA", + "digest": "sha256:74d52ab9004eea4474711b8018fd79d6a7b2ad06c79dca3007883319e30edd67" + }, + { + "id": "f006947d-7c0f-5c07-b5f0-64685f871ef6", + "version": "MTc1NjMyNzc4MTk5ODIzODAwMA", + "digest": "sha256:3d9ab095f23ff50b6ad06a8fbefb35629bdf63b8370ba3d4cb8ec3dfa58dd051" + }, + { + "id": "f0551077-daca-514d-a8bc-67abf79e7933", + "version": "MTc1NjMyODE4MjEzNTUzMjAwMA", + "digest": "sha256:14843ce90ff9c2e11802e5b9e356aecd313b9f75d2331cec236cab051abd8b54" + }, + { + "id": "f0582d9d-fcaf-5585-93af-e52d61ecec90", + "version": "MTc1NjMyNzczODQ3MTY2MjAwMA", + "digest": "sha256:9bf11861835d5e9222ec128ab8a9070493a4891198bc290dfe77bbc43dde27cf" + }, + { + "id": "f06eca56-a24a-53a3-b4f2-0efbc34632dc", + "version": "MTc1NjMyNzc1MzYyNjMyNDAwMA", + "digest": "sha256:7c9e952bd5fb66fc4a522a43b59df3a8f9f6b6e8a06d839f66bfdc36eb5722d2" + }, + { + "id": "f06f4a85-b0cd-51c1-a9b1-71c8ebf00b61", + "version": "MTc1NjMyNzg3MTU0NDkxODAwMA", + "digest": "sha256:0b9b9889bce5700caaf1f3f3415718db41deac18e5f480ca9b506b9a658c489e" + }, + { + "id": "f084e179-a3e6-5ddd-bb24-8e4543cbf891", + "version": "MTc1NjMyNzc1NTk5MzM3ODAwMA", + "digest": "sha256:5be002430514d2cfaa27bef21be8f81f8e411f1ebc0ecb30b19a5a5882e6c8eb" + }, + { + "id": "f0ba5f98-f3cc-549a-a5da-006597d868a7", + "version": "MTc1NjMyNzc4Njk0NDQ3NTAwMA", + "digest": "sha256:2bb160b18f9f1a28905d40d9a4463076553928e9d6677c2648ea732738f60912" + }, + { + "id": "f0cd461f-3541-582b-ac81-68063a3056e7", + "version": "MTc1NjMyNzg0ODA1MDc3NzAwMA", + "digest": "sha256:125ed9ba40ba654687f1f32c65fbb0653bfbe59815b901185389d3fc5d30e0a6" + }, + { + "id": "f0e66899-69b3-5d3c-98fa-a37e88e9edbb", + "version": "MTc1NjMyNzg1NTczMzg5MDAwMA", + "digest": "sha256:7d391ea4a54d768c4bfd18e71da44b70a355a9609181f9c69e58e84b81729ccf" + }, + { + "id": "f0f9c16b-d7ea-548c-865f-6624762b447e", + "version": "MTc1NjMyNzc2NzY5MTQwMTAwMA", + "digest": "sha256:e3cc0452b8910877bfb1df68b87d914878abea606659f60d90cde55adaa9b5b6" + }, + { + "id": "f0fd2fa0-b509-5b30-870b-f02509eb5a07", + "version": "MTc1NjMyNzc3MDMzNzc2ODAwMA", + "digest": "sha256:16fdc3801a8a7125cd86f36c2cc2ff292f707405716c29b848438a54d1bb6916" + }, + { + "id": "f13a68e7-5218-513b-999b-39e61324886a", + "version": "MTc1NjMyNzgwOTI5OTUzODAwMA", + "digest": "sha256:70f31390ed5158d9570d5fa257548323418bc7aae826f7a3994aab6595533e70" + }, + { + "id": "f14aa3a5-c20a-5eaa-a8c4-d7569265ceca", + "version": "MTc1NjMyODE2Njk3Mjc5MjAwMA", + "digest": "sha256:ee52177580f1ad5bcec65e8b5a8e88053bb168859cf82da394c4c924ef202b17" + }, + { + "id": "f15126ac-dee8-5fab-ac24-248ed804dad6", + "version": "MTc1NjMyNzg1MjYzMDY2MDAwMA", + "digest": "sha256:82e32fd8f479c1a344116aafd0e819ee0202212d3dff6a25eb5f04a1f64212e1" + }, + { + "id": "f17118c0-3906-5917-98a4-2f6e85879dc9", + "version": "MTc1NjMyNzc2Nzc5MzQzMDAwMA", + "digest": "sha256:be5d9e2114324e9284d0b504429afe1177f723b289ffd2cbb4371950efdec0df" + }, + { + "id": "f1735662-0dab-5fa3-b006-d3842da866d2", + "version": "MTc1NjMyNzg1NDQyMDE4ODAwMA", + "digest": "sha256:2b9ca36be53d5c45a4288e0e8bd3b927fce633b8b21d1dfcf9775b4d572f65c4" + }, + { + "id": "f19d1c35-382b-53f1-bb63-9bf7855b1f14", + "version": "MTc1NjMyNzc5NTMyOTc2NzAwMA", + "digest": "sha256:028093326068ada438feaddde8e4ddf590e2619fe183563eb5098b3440ab0887" + }, + { + "id": "f1a8aefe-a321-57f4-a209-c096538b4331", + "version": "MTc1NjMyNzg2OTUyMTU4NTAwMA", + "digest": "sha256:9c4cd54b707074a287d435ad450c934f67019f7707dfc2c3c7b704030b69a7bd" + }, + { + "id": "f1b33737-19ad-56e2-b788-ad25786507cf", + "version": "MTc1NjMyNzg2NzYxMDA3NDAwMA", + "digest": "sha256:bf7959d71c356de6af9e428079c4a532c674305fbbec9e7c37080c521672ea4e" + }, + { + "id": "f1dabe19-9c1f-5368-85ed-d8b8d9c9b4fe", + "version": "MTc1NjMyNzc1MzkxMDAxOTAwMA", + "digest": "sha256:60f622589bfd85979c83eaf21a3743809c569871e2f6a61c1c6d50b5a51db59b" + }, + { + "id": "f20575c9-5afd-5278-8605-216d9e6d51a0", + "version": "MTc1NjMyNzg1NDQ2MTAxMzAwMA", + "digest": "sha256:3c1a24a3e5ab793e5f842f03eadf858ce82d046153e41db92cbad04b2f9e4ed3" + }, + { + "id": "f205f032-9848-52c2-af6c-04093c7477e2", + "version": "MTc1NjMyNzg1NDQ1NjQ0ODAwMA", + "digest": "sha256:f22affb396ef5c0a50c8a579b4c54bcbd53c31b6c143eb18f02c203571c29c8c" + }, + { + "id": "f20bd687-b37e-5fe5-b6bb-b8e265280926", + "version": "MTc1NjMyODE3ODA2OTkwOTAwMA", + "digest": "sha256:02bddf64a6907e23b363da8c6875673402dd3c766e177b723f50495278b6c37c" + }, + { + "id": "f2448e38-ef57-5114-875f-8db959147407", + "version": "MTc1NjMyODE1NjM4MzQ2MTAwMA", + "digest": "sha256:43255aa73965767a66ae27dfcdde2a62276717c55bd87e57cb8121aff9e561a7" + }, + { + "id": "f24edaab-6328-5f28-9a9a-169e2f9885ba", + "version": "MTc1NjMyNzc1Mzk1MjU2OTAwMA", + "digest": "sha256:84d0a8e0bafccd5fabb8634e553066a67220ce68d8a4a21e3181c2fffc49cfa4" + }, + { + "id": "f2a1274e-d666-5c2b-91ae-bca1f360a494", + "version": "MTc1NjMyNzg2NjI0OTY5MzAwMA", + "digest": "sha256:88968086aee90ea0aadba233ca0d0b63bf767708e9def8e86796e97effeea668" + }, + { + "id": "f2abf713-37f3-52b1-8b83-2f7173023fac", + "version": "MTc1NjMyNzc2NjI4NDI1MDAwMA", + "digest": "sha256:d819a93b1e84b18c8d659af9c46d94a8041a458b4147601b59ae6b4950d338f4" + }, + { + "id": "f2e94a1f-2e92-5580-8373-d626a5f7d618", + "version": "MTc1NjMyNzg1MjYyOTQ0MjAwMA", + "digest": "sha256:441f9a2936cbc4c14351b75629dfaaf1d03025554bfe539e02a8576e11dcccc9" + }, + { + "id": "f2ecf446-eadd-55ab-a0dc-9fc1897edc51", + "version": "MTc1NjMyNzc2NzczNDcyMzAwMA", + "digest": "sha256:a0bec01a30abd1d1e334cd40ee6d2a3a30ba7f55d9ae8b9d06fb61237b371864" + }, + { + "id": "f30849a9-8a41-5403-809e-212fea65a1b7", + "version": "MTc1NjMyODE2NDkyODIxNzAwMA", + "digest": "sha256:ed9aa8aaaff5025d49b7389b3e512a7ff32d88a34c9c97aa8a7695908a492090" + }, + { + "id": "f316e865-5c30-5ca5-a3af-f9155fa0ee3f", + "version": "MTc1NjMyNzgwMzYwMjIwOTAwMA", + "digest": "sha256:d4e506ebdb645cd93867149c7289140e27508394b8205603750c60ddb24bf3a2" + }, + { + "id": "f32d74a8-1d32-55b7-8c37-cecc5d8a1b60", + "version": "MTc1NjMyODA2NjY3MzQyOTAwMA", + "digest": "sha256:fdf266ec5b0fae7659e1c153384d92d4b8fdd7efc954cfb7083cc378d7b4cb76" + }, + { + "id": "f333382c-77f4-573d-b61b-753ba25a16fb", + "version": "MTc1NjMyNzgwOTMxMDMxMTAwMA", + "digest": "sha256:ec987c553980e06d1b7099d93a66bf1acf639d7906cd8a0539a133ec68ce2fe2" + }, + { + "id": "f335c0ab-5a4f-5910-b727-63b50de07bef", + "version": "MTc1NjMyNzc3MTU3MzA3ODAwMA", + "digest": "sha256:51a2d7ab55f7d8d455400b19b4ad0cf7e777c9ea928058822f08b5afd527dbe9" + }, + { + "id": "f34ae608-ef37-5330-a72d-2066db42a048", + "version": "MTc1NjMyNzk0MDY4MzY0MzAwMA", + "digest": "sha256:c817a2697e59d7c67dc3becbbb4f9b3a1fc6676c4de8442a1f8a1e71cf018f90" + }, + { + "id": "f34ce25e-6b99-5f92-ae47-56d82fd16194", + "version": "MTc1NjMyNzgzNTIzMTM5ODAwMA", + "digest": "sha256:38bb121f378a2c7b43fef56d32a190e26ed055fdefeb362573f7a5fc8c349173" + }, + { + "id": "f351bc8e-134b-5657-8847-1f9d21adf126", + "version": "MTc1NjMyNzc3MDU3MTE1OTAwMA", + "digest": "sha256:a68ec4169426fdbd117ae78fd122e9f0c588ea333c2596c02890e0dbfff3982d" + }, + { + "id": "f3acece5-7f04-5c14-9b35-24f48e3b8e5b", + "version": "MTc1NjMyNzkyNDIzNDY2MDAwMA", + "digest": "sha256:079765b2e47b87b83782ebbd241d1b66da3cd81f41d090a0c2db44b59336fb5d" + }, + { + "id": "f3c95da8-cbf8-5dbd-a587-b6aa8496ae5b", + "version": "MTc1NjMyNzc4MjA3NjAzNjAwMA", + "digest": "sha256:0bc23ffb68c572558738e262774796c15e96b66f44c9eb1c2c3c03fe80a2a6ad" + }, + { + "id": "f3cc4dad-a01f-5af9-a20f-c658e5a9f4d2", + "version": "MTc1NjMyNzc0NzcwMTk4MTAwMA", + "digest": "sha256:57b1393f3fbd8e75b4e5a41e8ac76a6e63dd6f3b20f51ce502966b961310566d" + }, + { + "id": "f3f94ac4-4a83-528e-9cb6-96f852433b38", + "version": "MTc1NjMyNzc3MDQ5MzYyMjAwMA", + "digest": "sha256:06d8e4b17f4d6f7f91254695f4863abf4f8c941d471c9d56e303ebadf504791d" + }, + { + "id": "f4073022-bc7d-5eef-bc3c-1b45fc3ef5ad", + "version": "MTc1NjMyODE3NjgwNDU0NjAwMA", + "digest": "sha256:690276b4fa1fce7c8f1bf75a76cba59ee84cbe8a641f7fa3f510d9bf972c3b80" + }, + { + "id": "f42598c5-dca1-5312-87e5-f739b35717e9", + "version": "MTc1NjMyNzc4NjkxMTc1NjAwMA", + "digest": "sha256:f4b7ec5a9aa0d7d8388e6a7957f19ee4c766ffce3a1d0d13780b510902f41b19" + }, + { + "id": "f43086d4-c24a-52c0-a7f6-a6bdff9d2583", + "version": "MTc1NjMyODE3MTIwNDk2MjAwMA", + "digest": "sha256:a7ad6eda0c804308ebe240d96eb1ad7a69906d163940ba89b49818497fb5ef25" + }, + { + "id": "f433abe2-7357-5d41-817e-04c484e54bdd", + "version": "MTc1NjMyNzg2MDM4NTE2NDAwMA", + "digest": "sha256:affb80ad50db79096967132bd4bb4ef1752a99cab44b8a9fd6162fa32af5611d" + }, + { + "id": "f45b68ee-425c-5d8b-a846-c14ba2b94932", + "version": "MTc1NjMyNzg1MzM3NjMzMzAwMA", + "digest": "sha256:a8909f5a7a9695a02e8e9bade526c7db955d1d6effd751364afcd538eb3b51f5" + }, + { + "id": "f462caeb-6acb-53b0-a9df-273aa135c7cd", + "version": "MTc1NjMyODE4MTkxNjE5NTAwMA", + "digest": "sha256:94c3a9612974404990cde5c9e172c2590043447de3a55c0a83ba63ec67d326c3" + }, + { + "id": "f476b512-31eb-5420-905a-22c09b85bcac", + "version": "MTc1NjMyNzgxMjA0MDY0ODAwMA", + "digest": "sha256:6d489e33cca94f9dbc495d30cfffa5b3d45ab2ae16187bf738d0357552abfcd7" + }, + { + "id": "f4a05675-2e5f-550b-b6b5-4896d1dab784", + "version": "MTc1NjMyNzgxNjQzNzc1NzAwMA", + "digest": "sha256:e6fb28238374c8f3e8852d12fc75216f99caf5dd00cb0283cbd22bae8a713e9e" + }, + { + "id": "f4b5d8a7-ab29-5bec-b167-ffc3874ae82d", + "version": "MTc1NjMyNzgxNTUyMDgxNzAwMA", + "digest": "sha256:3eed76a84445b04454e689211eb6253e41c80a4321acd2470861f58c2fd17976" + }, + { + "id": "f4e14d61-a5ba-5a20-96b9-e43fb82778af", + "version": "MTc1NjMyODE2NTgzNjEzMzAwMA", + "digest": "sha256:6fec05e8de7ff1de725e6a2c6bec352aeb3074e8259bb637638dbfa2c586aa2f" + }, + { + "id": "f511c638-3084-5690-9d52-0c37f4d2fefc", + "version": "MTc1NjMyNzg3NjYzMzYxOTAwMA", + "digest": "sha256:d41c507c13611101c29d10d6d1252237e7a3f778f0b1820e8a5ac248f801bbf0" + }, + { + "id": "f5140aa1-7647-541b-bc16-2f01e5c93dbf", + "version": "MTc1NjMyNzgyMjgxMDExMjAwMA", + "digest": "sha256:625422dae3d778b76219a8768c86c74db94e2ba9fa77259f2f8f9ef6f291972c" + }, + { + "id": "f51f6420-48aa-57c6-9193-624e1ddef609", + "version": "MTc1NjMyNzg1NDQwNTAwODAwMA", + "digest": "sha256:4fb84d7f6a0e2a26ccf5cfaac23fcbc84fa1c7b124f66b79d1a197d73dadfe26" + }, + { + "id": "f54316fe-ba1b-5e3d-b522-203013d42988", + "version": "MTc1NjMyODE3NjIxNzEzMDAwMA", + "digest": "sha256:e20c4e5b9ecc254d6ede8735add1ded356eeb05a1bb282ee97c160c970acff28" + }, + { + "id": "f54fc630-c89e-5ec1-9f5c-142635118f5b", + "version": "MTc1NjMyNzc0NzY2Mjg2NDAwMA", + "digest": "sha256:3ec995f40fc2d358c376f5f8a02a27d17b21c366f0dc02924cc5ea75a34c7ee4" + }, + { + "id": "f566abe4-a307-55ad-96ed-03b9cd41efd2", + "version": "MTc1NjMyNzc0NTc2NjMwOTAwMA", + "digest": "sha256:84f67b53a15a89a05781cec56478d8b7973c1a9a134c62ebc5b6155f41e0c372" + }, + { + "id": "f584d228-8d0c-50bb-ac51-39bffd2b32cf", + "version": "MTc1NjMyNzgyNjU3ODc1NDAwMA", + "digest": "sha256:7c08f8913bb6763098ba2f60789263fcb8d65923f952df7b9caa6f5a46c38b08" + }, + { + "id": "f5871cf1-918f-595c-8444-90b07f3f4480", + "version": "MTc1NjMyODE3Njg0MjYxOTAwMA", + "digest": "sha256:9712dbc501a47cd025e70d15671c46b32b53a9f6b775a918ef7cd2a605243284" + }, + { + "id": "f5911542-1ecd-59f9-9db1-d5cee2cf77e1", + "version": "MTc1NjMyODE4MTg0MjY5OTAwMA", + "digest": "sha256:c828cda86e1ea06a532eae282fa6fcfa07084c5f82f800a488d578d6ae0eb608" + }, + { + "id": "f5a67679-e4c0-5210-a7cf-67a7df825194", + "version": "MTc1NjMyNzc3OTc4OTcyOTAwMA", + "digest": "sha256:fece05868ba204ff037495dc3efc1b2ed6176fb3f46deeff4c28ec4a073b7649" + }, + { + "id": "f5c29eeb-9b15-5c4b-b1aa-0e89ee515d20", + "version": "MTc1NjMyNzg3NTUxMDE0OTAwMA", + "digest": "sha256:6e9bde04dfbe12688f7db877f67851e37752297ce661d0cd4a7553bbafeb23e1" + }, + { + "id": "f600a6bb-6337-5db6-8726-a51c9fdb1831", + "version": "MTc1NjMyNzc5NTM2MjgwNDAwMA", + "digest": "sha256:2525a5f4eb412bac47d3c207a1651c451973cf34238156825569faee81638adc" + }, + { + "id": "f60233d4-c056-5670-aaa3-ead6f47e7fe7", + "version": "MTc1NjMyNzgzNTY4NDMwMTAwMA", + "digest": "sha256:50631756d6feefecf8015497957f9f953902a05851eb33b033f93b6fb7c35052" + }, + { + "id": "f606d309-5428-5977-b2bf-1296d9c247f7", + "version": "MTc1NjMyNzgwMzY2MDM3NjAwMA", + "digest": "sha256:1a4d591717201b6d4c7d13868c995b40c5055460b268dca93d4f2a01c0c340ad" + }, + { + "id": "f617e79b-ea03-5465-9793-e5315ac18bf8", + "version": "MTc1NjMyNzg3MDQyMDU2OTAwMA", + "digest": "sha256:a183697a93022a57bd0b8ab7ece3700c7cc6a5a1a208305cd6ef78ed86cf021b" + }, + { + "id": "f61ae719-c112-51f6-b881-fd34a2d21b7f", + "version": "MTc1NjMyNzc3MTQ3NzE3MTAwMA", + "digest": "sha256:dfdbef029e77ec03df6c4493be772dcea32e52722173f0fd37b6cc6059f4920d" + }, + { + "id": "f61f7ece-fe7d-5088-905d-005ca7ede73c", + "version": "MTc1NjMyNzc1NzUzMzAxMDAwMA", + "digest": "sha256:80028eeb40f3ffea1cca11da68afa8d7b986896fcecafb32cb1b3356a92951bd" + }, + { + "id": "f62940c1-42db-527c-90c3-e9c43015a330", + "version": "MTc1NjMyODE3NzY2MDEzMDAwMA", + "digest": "sha256:e23c5812ae4864e7539154e63ff18459fb58a72181ba492777066cd54ec1c15c" + }, + { + "id": "f6352c91-cc62-5746-b3fe-e15f11e0a0fa", + "version": "MTc1NjMyNzc1Njc4ODkzMDAwMA", + "digest": "sha256:7cc8ef67595306866e3681b69c75005d086b44ad42ef80ca7c5b28b5b0a4bcdb" + }, + { + "id": "f643363b-1faf-55cb-b28b-f48db5d622b1", + "version": "MTc1NjMyODE4MjE2MjcwOTAwMA", + "digest": "sha256:05078c987b05138f827af893037db91cb409ac65c71d8e03caf5a92255d8f6b3" + }, + { + "id": "f662bdbd-97c8-5c72-a804-7ff90dfdddc3", + "version": "MTc1NjMyNzgzNTc2ODc5NTAwMA", + "digest": "sha256:5940f66d743cc7f8e460e06422a88147f2337f403dd726d5e9696d1683aa64e1" + }, + { + "id": "f676892d-c4cb-5781-a264-64cca57ff1c2", + "version": "MTc1NjMyODA2OTc3MjMyMzAwMA", + "digest": "sha256:6c7d5252000df963c61145003dea1e0209dc151e489214319159064685edc598" + }, + { + "id": "f687f2f0-79dd-580b-9e75-3b14ab4be8cc", + "version": "MTc1NjMyNzc0MjMxOTg0NDAwMA", + "digest": "sha256:bd8ae4c9404294f4d909adfd2f3b40dcb0c428ec667906d648b5018f8ae0e3a4" + }, + { + "id": "f69c8690-37b1-5c17-bf8f-752c822575b7", + "version": "MTc1NjMyNzgyNjI3MTM4MDAwMA", + "digest": "sha256:399aacbd552c449ce295a39b307b8717e321e98a5e20f0e04f1375bcc0be547c" + }, + { + "id": "f6bf3181-8694-58ff-a944-6e2e25105175", + "version": "MTc1NjMyNzc5NTMwMDk1NzAwMA", + "digest": "sha256:958092f99596c3318cfc83361fc655b68c3aa016ec6f5dab33248d6b7fcf9522" + }, + { + "id": "f6e872b4-aa2b-534f-aac3-3919ba6f1f8a", + "version": "MTc1NjMyNzgyMjQ0NTY1MTAwMA", + "digest": "sha256:10163a045d9392a38673d960d8e25c2b3ddd7206ce5785b011a116c68757f975" + }, + { + "id": "f6fc573c-df41-5b4a-92e9-ed7633a91fa5", + "version": "MTc1NjMyNzgzNTIwOTAxODAwMA", + "digest": "sha256:9c9824410ec07d0550627a99593b114cefa6e8a749a87fee3574d9ad1bba5088" + }, + { + "id": "f70058ec-7f78-5df3-b9c4-aa1a7b149a17", + "version": "MTc1NjMyNzgzNjMyMzIwNDAwMA", + "digest": "sha256:39a565699161fe9793a2f771ade7ed228ec061450a9325dee476f5462fdd88de" + }, + { + "id": "f7010e6d-ed35-512d-b43f-11daf90ff14f", + "version": "MTc1NjMyNzkxMDk3MzM4NjAwMA", + "digest": "sha256:17769c0a2e5df03e8e23cfac3a71d423fe74c1b9197c68da812c5d0ca8226724" + }, + { + "id": "f7055ea6-afda-5799-94c9-9f96a4b02cb5", + "version": "MTc1NjMyNzg1NTc1NzQ5MzAwMA", + "digest": "sha256:e6cbf7562385a6adda66a33ae532fafdeeba01606c732b23871710519c6400ba" + }, + { + "id": "f7159545-8236-5524-a52a-44444ddd7951", + "version": "MTc1NjMyNzc5NTMzMDEwMTAwMA", + "digest": "sha256:e1424e4540a28b6935e15aa1b35077feec17a211d3a51f4f038f1270a17c79c5" + }, + { + "id": "f72deba2-0509-56d6-9782-5f7a5dae9725", + "version": "MTc1NjMyNzc5NjExMTQwNjAwMA", + "digest": "sha256:6380b2808d7f882628b270b20fd6ba79f5fb6a617f07fb7696ebb61843041b5d" + }, + { + "id": "f73d5029-3c9e-56af-a9e5-9055e0cc48bb", + "version": "MTc1NjMyNzc5NjExMDUwNTAwMA", + "digest": "sha256:b01413ef7ac70d8170064986eb1d24445889bd451d6da88ac5ac347b5173f808" + }, + { + "id": "f76a0bf1-9232-5686-bc29-f81019c7516a", + "version": "MTc1NjMyNzg1Mzc5NDg3MTAwMA", + "digest": "sha256:51e94cf6f8d223bad4da94f0e2559dc3bb06d1515caf719a0469b8e980765e44" + }, + { + "id": "f76b2ac2-d00a-55e5-b6c6-f9c30c489718", + "version": "MTc1NjMyNzc4NzM0MTUwNDAwMA", + "digest": "sha256:4b66e4c399ab90e1f304646b1fef6026e391e98757bf99fd542dd9ba003eef7a" + }, + { + "id": "f76cc908-fc04-5317-af4e-62dc9186549c", + "version": "MTc1NjMyODE2MzM3MDc0MzAwMA", + "digest": "sha256:8138923f4179e0e29c1b56474d3455b1d8742fc1a0b1cb056420bf1b945596cf" + }, + { + "id": "f7715ef1-90bb-575d-861c-2d2ca8bdde97", + "version": "MTc1NjMyNzc4NzI1MzIwMDAwMA", + "digest": "sha256:ab8a5ce9f84593802efed7ffbe2d93681f936c09311a20722ebe2d1ad0ac462f" + }, + { + "id": "f77c9ec7-0bb1-5a58-9420-8d9c09adac4b", + "version": "MTc1NjMyNzc1Mzk1Mjc5NjAwMA", + "digest": "sha256:ba80d0b79cb7e93deab926e1423f72165624f5983c1fa57ef0d5542d0870b5f9" + }, + { + "id": "f7a3302b-cfee-5b13-8bfd-9cb67c1a3ddb", + "version": "MTc1NjMyNzc3MTQ4NjY2ODAwMA", + "digest": "sha256:b0b8e407576be36d930333d31efa6901e238e50a2113b3d0b57664da3d836b09" + }, + { + "id": "f7a5c0c8-c173-5d02-aec9-aa99b4f2f5fd", + "version": "MTc1NjMyODE3NjEyMDY0MjAwMA", + "digest": "sha256:cc621fc5e65cd09f22d679c1e3e9368130c7294e90a6cb843c0e1a60821f93e8" + }, + { + "id": "f7c25f98-890b-50ac-973e-60eefa52c614", + "version": "MTc1NjMyNzg3NjU0Mzg3MzAwMA", + "digest": "sha256:0fd8846735bd4bf9475f3f920eca96274fbb4b8dd88585f126b347678a8559bf" + }, + { + "id": "f7dd9116-80bc-59f0-8c2e-94195d7b2cb7", + "version": "MTc1NjMyODA3MTYyMjQ4MTAwMA", + "digest": "sha256:74ec727a7fb6e180bf2968986bc2b1a3e04595327bfd2b8207cc00309e0be260" + }, + { + "id": "f7ed34bc-df2d-55f9-a98f-c7f7917a7d88", + "version": "MTc1NjMyODE2NTg1ODIzMTAwMA", + "digest": "sha256:c655b9c9e68e48f1cec05958e59eacd383aaa8082dbd4b2d709db7c7d10a09b8" + }, + { + "id": "f80184c9-6ca5-520f-8f21-645dc928d01f", + "version": "MTc1NjMyNzc1NzQ3OTc2MDAwMA", + "digest": "sha256:0c3fad77183b478193de9111357a656aada7bd3ac058653021ff0c1ddc2bb350" + }, + { + "id": "f81a0e72-0c23-5cda-9b2d-761f9c10022f", + "version": "MTc1NjMyNzgyMjgxMDUxNDAwMA", + "digest": "sha256:fdcbe049efd2cc147d2db9ea971d509c37165d76e392fa5f56a8bdf9c6d06df2" + }, + { + "id": "f837bb4b-2264-5793-a19e-15888e996300", + "version": "MTc1NjMyNzc0MjUwNDk1NzAwMA", + "digest": "sha256:e6a7604fd684de024462e35f259cb61ce4d10ec3ba90220bf0eff92665651f22" + }, + { + "id": "f8479fad-8bdd-52bf-8f4b-a1fe9a67f6bd", + "version": "MTc1NjMyODE1ODE4MzM0NjAwMA", + "digest": "sha256:df0eb9c02796a422b976408ea77ef150a6a2e817c549072d3c4192aee0e5050a" + }, + { + "id": "f849b438-b5b7-57b4-a483-c4651bf1d0b4", + "version": "MTc1NjMyODE2ODk5NDcyNTAwMA", + "digest": "sha256:ef531b4c7876c68eee7ae05d8a40123883939cd8ad755f6d7f96c7fd1fe8ace2" + }, + { + "id": "f88a8725-8ac2-5d3b-abb8-ed581e889e95", + "version": "MTc1NjMyODA3NzU4NTQzMzAwMA", + "digest": "sha256:1718356adbb5c4d2944c9b7591cd33e4cee140189e949359e46d499c6db9624f" + }, + { + "id": "f892f3ea-040c-54f1-9a43-e1d882b21200", + "version": "MTc1NjMyODE2MzA3NjQzNDAwMA", + "digest": "sha256:6e139360da7a79be69eedde07ebb955285a32700ea2835247ed424caefc06f66" + }, + { + "id": "f8a14418-404a-55c3-bd23-04cbfee35a17", + "version": "MTc1NjMyNzczODA0ODAzMzAwMA", + "digest": "sha256:86cfd5d39cfa9341dff0d6b85640e7ab9b80125244cbefb0adc538e55741660b" + }, + { + "id": "f8c4c373-66ec-55c8-8bd2-2a4eac5db702", + "version": "MTc1NjMyNzc0MTc0NDIwNDAwMA", + "digest": "sha256:4ea5e223f38311f17b3bb5271ea78df30596a8a778948d360d3970689097a1a4" + }, + { + "id": "f8cee0dc-c773-5eb7-a161-cc0b5b76bfc2", + "version": "MTc1NjMyNzc4Mjg2MTQ1NDAwMA", + "digest": "sha256:7354645f2e8032ee9e63951005378cdd02e53d16686c8a1df8ab7f340d41c13a" + }, + { + "id": "f909a680-1447-57ac-8113-c8c4fdf31961", + "version": "MTc1NjMyNzg2MDMzNjk1NTAwMA", + "digest": "sha256:4d399141843d8b3f5b64f074606333d45ee6b376d474c04db999458bbadf80b1" + }, + { + "id": "f90f693d-b548-57a9-b7ee-23b91d2f3b30", + "version": "MTc1NjMyNzc4MjgyODEyNDAwMA", + "digest": "sha256:6e67de51352b67956c4ba8a8db9bf3d8c84b2affe9a1d4673ded7128ec0e8fa6" + }, + { + "id": "f93678c7-e55a-5fd2-9a2b-cf816007539c", + "version": "MTc1NjMyODA3NzU3OTkyNTAwMA", + "digest": "sha256:f989162f9185e3bda70ef305b51e9960dd7157c919f5ed8e6b9a549237b3c673" + }, + { + "id": "f94c9e10-60c5-5e51-bd46-5c2b2f12643a", + "version": "MTc1NjMyNzc3OTY4NDY2MTAwMA", + "digest": "sha256:7617158dbd0888efea6e97667b4639348634327b631e526d9d7cd72cfbfe4394" + }, + { + "id": "f95e4f8c-514d-548b-8b46-90f13eecdb2a", + "version": "MTc1NjMyNzc3MDYwNjk5MDAwMA", + "digest": "sha256:c12c09be030c8fd686d1a55a3e6a79c02566d256714c848823a89af9acf83275" + }, + { + "id": "f967fd35-29a5-59e6-a5a8-9609800d0fa7", + "version": "MTc1NjMyNzg0MTEzOTc5NDAwMA", + "digest": "sha256:51e62e2b55f0b2b6f6277f89be1d88c97a1027e3e1c9fedc394afc6043c5230d" + }, + { + "id": "f970a293-f434-5702-bcac-7003b057e131", + "version": "MTc1NjMyNzgxNjUwNDk4MjAwMA", + "digest": "sha256:3f5c4a941aff3915b46c2f8913d2f1dd27f5627d665cc9bb470eaf3fe4baf609" + }, + { + "id": "f97b01b5-6ad1-52d2-bcbc-0ff2f0048333", + "version": "MTc1NjMyNzgwMzU1OTA4ODAwMA", + "digest": "sha256:96e3d2c2b5b2ec95e46bec010707fb5b72616e0377b966df7ab594ecc2374fe4" + }, + { + "id": "f981a0b2-44a5-525a-9f28-31029bb86006", + "version": "MTc1NjMyODE3Njg2MDI1ODAwMA", + "digest": "sha256:8d7e4a9dd3cc60972bcbf484db4826e686305811d244b6db156203b9b0071bf2" + }, + { + "id": "f9ae2ff0-6cb1-5912-92b3-ae58d598cc6b", + "version": "MTc1NjMyODE3NDM2NzM0NzAwMA", + "digest": "sha256:1469e2e386923a7d8c3064bde50458022ab4bcf3c461e7f6ad647d00f87a1b94" + }, + { + "id": "f9bccce4-a375-549a-8b32-ac4e7f9cf593", + "version": "MTc1NjMyNzgyMjQ2ODY5MjAwMA", + "digest": "sha256:79139864d6e70b479333a61d571cf5cbf1a760c81a8e753af243c06969b18fa3" + }, + { + "id": "f9c19ca4-2565-5dd8-83af-734b6b93a91c", + "version": "MTc1NjMyODE3NjEzNDY0NjAwMA", + "digest": "sha256:96089bac4d8605db7ffd2a682d1a124d6b95516bc66ff615b1658cb1dd00101d" + }, + { + "id": "f9c3d1ac-3fce-5213-aaee-f3405e2ad2b6", + "version": "MTc1NjMyODE3NDM5MzA3MzAwMA", + "digest": "sha256:9c8f7e5aed063447502ed03116ff0c76cbf70e4f61a148c19a0892f95d9064fb" + }, + { + "id": "f9cfc518-03a6-5396-96d9-4eb52c8117d3", + "version": "MTc1NjMyNzg4MDM2MjYwMjAwMA", + "digest": "sha256:cecdac55147483672979d25507c50995da37dce6e3a8efcc43a9d69a849bbfbe" + }, + { + "id": "f9dbf60e-98e4-5aee-a964-a2c708106e65", + "version": "MTc1NjMyNzc1NzM2ODcwOTAwMA", + "digest": "sha256:4100107ba6d1d7e5ab6ef38e29422826b51af1f9069dcc7401f910fc1cae2dd3" + }, + { + "id": "f9e75017-e36f-5d03-b901-d6e07bce6454", + "version": "MTc1NjMyNzgxNTUxNTM1MDAwMA", + "digest": "sha256:3d1e3f4a89c53954ddb1fdc9afefb5230326afe33a003706416f66df8f1968ad" + }, + { + "id": "fa0cca25-a9fc-570d-83b9-ad43f112ea6a", + "version": "MTc1NjMyODE3ODA0NDcyOTAwMA", + "digest": "sha256:d7e5e240a4bd56b46478f6d2b804cb4c5472aa5ec903a2408988373ba0ef12ff" + }, + { + "id": "fa1bf1c2-6ffb-5c1a-8b38-072e21cf3f56", + "version": "MTc1NjMyNzczODQxMjk2ODAwMA", + "digest": "sha256:1a8d19c6949fe3432378a52c18f7d03af7220409f4133057700b7a5e4c4c34ae" + }, + { + "id": "fa1cebde-d2f4-546e-b78c-efbe53fbac64", + "version": "MTc1NjMyNzc2NzY4NTg5NTAwMA", + "digest": "sha256:e891f1ae2717f25e1cbc1fa5505af91cdc86a7a62218955af6d825ca75731a85" + }, + { + "id": "fa211a15-d3f4-57b0-a5fa-85ebb4f4d1e6", + "version": "MTc1NjMyODE3MTExMzIwNzAwMA", + "digest": "sha256:b381806212a1a3424f7ce6805f1d9e2e22c8da86d311d5b427043d8221567ca8" + }, + { + "id": "fa27418d-9526-507f-8ca9-2a3ed9ac5e5d", + "version": "MTc1NjMyNzc5NDc3NDg1NTAwMA", + "digest": "sha256:133520b62367f6bb6ead081c27fbbdf951e6104ba791af74ce41423ece84f398" + }, + { + "id": "fa361ae0-bb40-5b81-ae83-60536f1ac3d5", + "version": "MTc1NjMyNzg3NjYxMjUwNDAwMA", + "digest": "sha256:83557a5eb437e015d14e072f2a02497b762c97ab47d53374f03f9ea0e98342ac" + }, + { + "id": "fa3d2924-ab40-5dd6-baa4-72475aecee29", + "version": "MTc1NjMyNzg1MjcyMTY4OTAwMA", + "digest": "sha256:22d9910534d61fe2f68adb7c02e95e5b0f536258efacc110e53ecd077275ee05" + }, + { + "id": "fa4a3c97-2f45-5c62-a6dc-f12986d7e3bf", + "version": "MTc1NjMyNzc4MjczNDEwMjAwMA", + "digest": "sha256:e812db7d33b4d47f90be3bebe9adc0d79d956348c3f4e0713ebb10003ef04b4c" + }, + { + "id": "fa4a5bfc-9230-5c6d-9244-5e6f521d133a", + "version": "MTc1NjMyODA3NzU4OTQ4NDAwMA", + "digest": "sha256:823a4af6770116209f067dcd2acfda2f665b49623b6e42e09560bffa80527985" + }, + { + "id": "fa4a63b8-f092-594f-9735-3ba4d6024f58", + "version": "MTc1NjMyNzc1Mzk5OTQwMzAwMA", + "digest": "sha256:f877001e281a0a94e2a557cf88efe6ad908a49bcdc83408fd5565ec228a122b2" + }, + { + "id": "fa515660-d89c-52d0-99bb-e56f52fa8f5a", + "version": "MTc1NjMyNzgwMzcyMTgyNDAwMA", + "digest": "sha256:df5dbe95921399a225e658a75bec376a07184624c37162d417be3a74d4ea5850" + }, + { + "id": "fa5be13d-b8dd-5b8c-a14e-8f2ce0b06bd4", + "version": "MTc1NjMyODE3NzU2NjA4NDAwMA", + "digest": "sha256:21373c8c10d5936d252247518b5fc60fe75cff10ee399540820dced89afa471b" + }, + { + "id": "fa79c946-6f63-5597-aff4-07ecd3e19d40", + "version": "MTc1NjMyNzg4NjU5NTI1MTAwMA", + "digest": "sha256:71e8e6bc238f0b79809c1058e04266082411e374d97c3e0adede66eb7379eee9" + }, + { + "id": "fa7a5a12-5bc3-5742-a5ae-0ca99088cea6", + "version": "MTc1NjMyODE2MzQyMjUwNjAwMA", + "digest": "sha256:984e35cf741b924add487e5ccb4d0e872ccfe96311251a3d0f34b4a8cb0d38f6" + }, + { + "id": "fa8a119c-18a1-5f1d-af96-5cdd41cac248", + "version": "MTc1NjMyODE2ODk2MDc1NTAwMA", + "digest": "sha256:962f2537d51f85e403f103ff7cd63444d5a82cb3d0e069c9b805a26265649f21" + }, + { + "id": "fa9108c1-ccc1-51c7-87c2-6d7807e67220", + "version": "MTc1NjMyNzg4MDM3NzUwNzAwMA", + "digest": "sha256:3cf7f3ff63a57eb98f86bc7e4f912f71a683f95370872f421bf9215223038165" + }, + { + "id": "fa91f496-c566-5174-827a-7a7c95773507", + "version": "MTc1NjMyNzgwOTMxMDI2MjAwMA", + "digest": "sha256:6dcf641e365f3af4e145e2700a2a92eb11f8af32b0a672511d5e0100b931bab3" + }, + { + "id": "fa9e5dbf-0f05-5eb9-8ddc-03bb82d757ec", + "version": "MTc1NjMyNzc0MjU2MTExMTAwMA", + "digest": "sha256:190befcaa4ae37fee459ecd45c9c3c535c4844ea3737367233ea3554f2f3ff19" + }, + { + "id": "faa58cdd-77ca-54d5-a5c5-b20eada21e0c", + "version": "MTc1NjMyNzc4MjgxNTIyMjAwMA", + "digest": "sha256:f125491116cde684966841192c49c5285e59182700fdf8205a223da5a751bee1" + }, + { + "id": "fab237b0-1e4a-511a-a49a-7aef083cfe0f", + "version": "MTc1NjMyNzc3OTkzNTgzNTAwMA", + "digest": "sha256:26c2507e30ef4de97439b385e9eb6512df6dda6bf56cf1450ed4a1b0e4547b2c" + }, + { + "id": "fab62884-d1ba-55d7-a8a0-81d7bf4255dd", + "version": "MTc1NjMyNzg2OTQ5MTAyMTAwMA", + "digest": "sha256:eb54b64b1ed5e4356842f8f1e75b8ae24fceda1ced0dff78259c504b199ae0fd" + }, + { + "id": "fab80fea-2d09-5afc-a2fd-cc99cd8146d9", + "version": "MTc1NjMyNzc3MjYzMDYwODAwMA", + "digest": "sha256:b9a84ea048143bdb07d52b6d7bbee66a5c36c5abdd7cd5dab2e055d50487198d" + }, + { + "id": "faf8f569-9410-580e-9f15-aac3644a64df", + "version": "MTc1NjMyNzc1NzUzNjQyOTAwMA", + "digest": "sha256:1866c758c3060c6abdf4872b86b775b524d0175db242a3a7ffa55263f17a1968" + }, + { + "id": "fb18a76e-cb34-559a-8836-800b348ec4f6", + "version": "MTc1NjMyODA2ODkxNjEwMDAwMA", + "digest": "sha256:e88ecd91dff44550f57e42d167d3a7a2f911dee732aa2954a313b4de13cc8fed" + }, + { + "id": "fb2093d5-0572-5dd5-904a-2ad8a3afc8da", + "version": "MTc1NjMyNzc3MDM5NDk0MjAwMA", + "digest": "sha256:40db1278d0feb186838a9421050423717809cebb85a47ef45f6e6e667f364193" + }, + { + "id": "fb3e33a0-bf72-5ef6-a456-18383741ac96", + "version": "MTc1NjMyNzg3NDU2NjkyMjAwMA", + "digest": "sha256:64ee8231be6e8816ef7203b225aa352c3306db532c72e338ae563a872e287e86" + }, + { + "id": "fb406fc2-c9d0-5a6d-b565-0aebdf79886f", + "version": "MTc1NjMyODE2MzM0ODA2NTAwMA", + "digest": "sha256:f2f7ddd9fe08f79041bf938949e0427ecfd1aa15580f0c042928056e115624e4" + }, + { + "id": "fb43d06d-18ee-5ac9-b167-ccc773eda93d", + "version": "MTc1NjMyNzg3OTk4MTI0NjAwMA", + "digest": "sha256:2baf713c5ea73c8c8ba686589f3957b29c2fc1296a554c30a0f412475a4f9bd2" + }, + { + "id": "fb4a6756-5a95-5da4-aaf1-de832e187c2a", + "version": "MTc1NjMyNzg3NTQ5OTMyMjAwMA", + "digest": "sha256:200517be535ba7a75567700efed67db09808e4e4fd57552f4e1e22a0318311da" + }, + { + "id": "fb4cd2ad-188c-5d7a-901c-df8905ba2ce5", + "version": "MTc1NjMyODE2MzM3NDE5MDAwMA", + "digest": "sha256:fbac1b274554dada1c89323e397939a75205810219ff2d5ff942f1c9af43fdaf" + }, + { + "id": "fb63640f-7edc-5848-9e4c-a599643ce3ff", + "version": "MTc1NjMyNzkxMjY3MzYxMjAwMA", + "digest": "sha256:83a9c42889b7482e8cb82618b454800e87ec5358da533afb357e8436beea3bf9" + }, + { + "id": "fb758d61-e5ed-5dcf-8eec-e7cddf4cbfa9", + "version": "MTc1NjMyODE2NDk2MDMxMjAwMA", + "digest": "sha256:ad65305a5118e8ecdf57814ed749b42237c00866787e99ad1dc8db274e41c8ab" + }, + { + "id": "fb85c513-7e60-5acb-b808-b36a05fccbe3", + "version": "MTc1NjMyODE3NjI1MDI2NjAwMA", + "digest": "sha256:5f3c8eacdfa92f5cb51a87788b41c5724ccb5b564f996046eef190371520be81" + }, + { + "id": "fb8acd07-6d4f-5952-907d-030c0d683e41", + "version": "MTc1NjMyNzg1NTc2NzQ3NzAwMA", + "digest": "sha256:0c6da0293ebd7e3fe2c20e50fa2a1a00a6a22c3b239c92424c4075989bdbd245" + }, + { + "id": "fb93232c-40c4-5558-906c-f7492b19f355", + "version": "MTc1NjMyNzg3NTUzMDk4MTAwMA", + "digest": "sha256:fc372d1f65bb57bc7a0b6e1bb911d95a7af4c8d6efc2f9cf9503aeb7718e96c3" + }, + { + "id": "fbb8e4e8-2462-5c2c-a46f-960943cbd35c", + "version": "MTc1NjMyNzg1MjcwNzkzMjAwMA", + "digest": "sha256:23b1f972afe6114cf79e2575d9559b1a3418fe80799e9bdfec18ac3a16d448c0" + }, + { + "id": "fbcb26fb-7c3c-5e3a-8c8e-d9ae6c2de6ee", + "version": "MTc1NjMyNzgxMTkxNjg2MTAwMA", + "digest": "sha256:f2b4364b18ca9994949c0f8bdb1a6b5ca30ae3db68df693db0a9bbce1ab40d57" + }, + { + "id": "fbf57962-414d-5598-b920-8ad92c7edf52", + "version": "MTc1NjMyNzc0MTY3ODAwNTAwMA", + "digest": "sha256:a24c5865200315a9585474603c411bb1b81c0b2e2081d0835da7d495baa787a8" + }, + { + "id": "fc208699-e5bf-52a2-b098-06a8ca1ad32b", + "version": "MTc1NjMyODE2NTg0OTE0MTAwMA", + "digest": "sha256:c2a86b46b2f84a9988ae494cd0286ebe2f5ac0d45a84704b6a977fb403cd0228" + }, + { + "id": "fc33e948-c7d3-5857-9ebc-2d84ee769cd4", + "version": "MTc1NjMyODE4MTkyMDIxMjAwMA", + "digest": "sha256:e300d0bd961e971112d0e611ea59c146989ec18f518bd89cd4e493c48e72ef8d" + }, + { + "id": "fc47a0b6-011e-55ce-a9e8-1c25b463e2a6", + "version": "MTc1NjMyODA3MTI0NjU2MzAwMA", + "digest": "sha256:86582c66ee12b06213ee71e39a60b3e38cd8adbfbefaa364d4c38e359acddbcc" + }, + { + "id": "fc5a3bac-8dfe-5eb9-9ee4-1fdf25ba25ea", + "version": "MTc1NjMyNzgyMjgwNjIyNTAwMA", + "digest": "sha256:ff7e188eb66985df1668ae4189cb5d2fc5875a2479c0c18cfd2ca38205be07d6" + }, + { + "id": "fc804379-1898-5bb4-a9f0-93bee0f1447f", + "version": "MTc1NjMyODE3ODA2MzM1NDAwMA", + "digest": "sha256:89a05784c13354b605483076ea6c529867c2f860fd04e989b43da8f527427d9d" + }, + { + "id": "fc8a34e6-b5ed-5071-9dcc-b8d1b26b1f27", + "version": "MTc1NjMyNzc4MDg2NjMzOTAwMA", + "digest": "sha256:3475df245144e73a60443a5205e688957d896955a63295d6431d63011aa7cb94" + }, + { + "id": "fc92bb9a-e345-588c-9b2d-b5529867377a", + "version": "MTc1NjMyODE3NDM4NTEwMjAwMA", + "digest": "sha256:dd010d0ed61563ffac2002aff7d703c7b09b9c03b9c21422dcdb6abb4905057f" + }, + { + "id": "fc9c3bf6-988e-5167-844e-7fa9ede8ea0f", + "version": "MTc1NjMyODE1ODIxNjA4NjAwMA", + "digest": "sha256:b214ead0efc6d355e1f78450409bc6227d74316d887b593af47edfd3af135567" + }, + { + "id": "fcae4076-5ab2-5cc0-97cf-52d03d996113", + "version": "MTc1NjMyNzg4NjczNzQ5MzAwMA", + "digest": "sha256:94d02e5c46306d4dbe25492a66c6927110c75aab9749bffe170bb87cc0ac76f5" + }, + { + "id": "fcde1de5-3abb-5090-b01c-7f7031ea207f", + "version": "MTc1NjMyNzc0NDUyNDk1ODAwMA", + "digest": "sha256:044803f1b78aff3b11de7aa773a74ca645e9fced33825f79034e4e1d934041cd" + }, + { + "id": "fcf82c08-f716-50af-bc90-bc1dbc992209", + "version": "MTc1NjMyNzc3MTUxMTI1ODAwMA", + "digest": "sha256:b266ecb5ceeeb2848173666bd5682529bae034cb197677ae977480fc3181b004" + }, + { + "id": "fcfda7a6-12e9-5178-a6ee-58f92365ebf5", + "version": "MTc1NjMyNzg1MzgwNTg1NDAwMA", + "digest": "sha256:757b71ed7caa0a60b93eabc370d03c74f42c4ec1af46a9704f4704ea34b2b580" + }, + { + "id": "fd072b83-963d-5dcb-9d96-f3dabc03ede2", + "version": "MTc1NjMyNzgwMjk4OTcyNTAwMA", + "digest": "sha256:67038890d573f05ef60363e33b4dd538840e88486dcc578ddecea2a68f08a9f2" + }, + { + "id": "fd0c7fb6-5084-5613-97f5-741f593d0e3e", + "version": "MTc1NjMyNzg2MDM4NDk0NDAwMA", + "digest": "sha256:d5472a1088dbe4b6e3ad042245ecfb11592d0722a94d65f244135b80b186817d" + }, + { + "id": "fd18c9e0-3f1f-52e3-92c1-0c8bf29dcc43", + "version": "MTc1NjMyNzg3NjgyODEzODAwMA", + "digest": "sha256:87251a0d83154ba52415bda10be1231d5985ebd46ad96000a7bebfb4987f5039" + }, + { + "id": "fd2a1175-375e-5f3d-b526-69b060dfbc6c", + "version": "MTc1NjMyNzc4Njk0NDMwNzAwMA", + "digest": "sha256:83aa3ce339960a26ffef4c561b8ccd05338125feabd7ec4d75e2a02cdec9963d" + }, + { + "id": "fd308c83-dcd1-55e7-b8f3-547decf5413e", + "version": "MTc1NjMyNzgyMjczOTQ4ODAwMA", + "digest": "sha256:08026403d62a87431fb3868a9008bcb518a138b9c9a8181933aa372f5b398bc6" + }, + { + "id": "fd4314b0-5a4f-5a1a-b846-4dc3cf33c2f0", + "version": "MTc1NjMyODE3ODA5Nzc4ODAwMA", + "digest": "sha256:f571769646f03d468e0e4f96fb80201d0e3f2d473a55fbb33d3243d47bc51fdd" + }, + { + "id": "fd4455dc-5d34-54d3-a7cb-bd16dd1654e5", + "version": "MTc1NjMyODE2NTg5Mjc1NzAwMA", + "digest": "sha256:4adc2baba5c9aab71abf922ee405396707a610c470fb18e0ccaabd44417e8962" + }, + { + "id": "fd4762fb-62fc-5d69-a0b0-4e12e52b27d1", + "version": "MTc1NjMyNzgxODk2Mzg2NzAwMA", + "digest": "sha256:790b42bb82fa7de4394948053ae7927b70bf55a91697fe0a7d620c623f16891f" + }, + { + "id": "fd5bdf80-b6ed-535c-b7a9-ab08a71e1efb", + "version": "MTc1NjMyNzg3MjAxMTk3MTAwMA", + "digest": "sha256:0e622f46bb246f872f73eda59792d728463c93d7c18d5a78211e259de4d66179" + }, + { + "id": "fd5ea1eb-8603-51ff-9a6f-9cf1c5e78e36", + "version": "MTc1NjMyNzg2MDI2MzMyMTAwMA", + "digest": "sha256:0a312933936c7a5ecdadbf32e0b9bc8d0384458303e40c16ceb716d7c71fd266" + }, + { + "id": "fd60a624-94bb-5f6c-bc59-c2a2e42f08cb", + "version": "MTc1NjMyNzgxODc4MDY4MjAwMA", + "digest": "sha256:3cfb54766e32eeb0e39120a4cb1db52c120c57380f2c652bc69b604a2c8fcf74" + }, + { + "id": "fd96269d-86f6-51eb-be12-373222b06e69", + "version": "MTc1NjMyNzg2MDM2MjcyNzAwMA", + "digest": "sha256:4270b8636b9672c36310772b17b2ab0347f50e50a3e0b0e7e0c4bb57f5ad0502" + }, + { + "id": "fd97f49b-980e-5eb6-a3ec-29a042fc8a32", + "version": "MTc1NjMyNzgxMjg3Mzc0NzAwMA", + "digest": "sha256:7b7ca16c1c0347fe5c22d5652abb40f1505773af61d37eeca8ab91e744714dd9" + }, + { + "id": "fdb58eea-8f94-55ca-9d25-da51cb8deb56", + "version": "MTc1NjMyNzgzODc1Mzg4OTAwMA", + "digest": "sha256:8d9b1eae99f8691e65385dfe9a2607531a0c6ef8c61d0829185ecf9775e3bab9" + }, + { + "id": "fdcbd93e-1f89-58dc-ac59-5730ab9c607c", + "version": "MTc1NjMyODE3Njg3ODMyNDAwMA", + "digest": "sha256:1bc4d4c2a93978fde873030d1970bf28bac5cd3be13919ffd5171f337ec286d2" + }, + { + "id": "fe081288-e04e-5fa0-9152-fd5a5ab35abf", + "version": "MTc1NjMyNzg2NzU3NjA5MDAwMA", + "digest": "sha256:fd54f5cede2f746aa25bba7a6d1aa13ef4563f7cc41759099a12dd235582215c" + }, + { + "id": "fe3aafe7-8882-584f-aae8-e6612e9faf29", + "version": "MTc1NjMyODE4MjA2NjkzNDAwMA", + "digest": "sha256:4fc4d97dabb20b7f777143434efe41e8152adcf5a6aebdbbe1adc31ce26af135" + }, + { + "id": "fe5770a0-38ef-5551-b7c5-6a6a5ba6de8e", + "version": "MTc1NjMyODE2ODkyMzQ4NjAwMA", + "digest": "sha256:43b517190c879f4e0a5e8cec582d2307d3ac34d2028a41717da8cc6b209b0540" + }, + { + "id": "feb3dba9-10a9-52df-91fa-7c9b3a8c7e01", + "version": "MTc1NjMyNzg4MjA3MTYyMzAwMA", + "digest": "sha256:d436c213f92c3a2cc20200230fe156750ac6b86f3542d61904679a54ec635396" + }, + { + "id": "fed59630-4ec2-5cfc-9a0f-cfc3935da0c6", + "version": "MTc1NjMyNzc1NjgzMjA0MjAwMA", + "digest": "sha256:045509a1fe64ca0f6060213da5cf00d9f7374d43e018278962788db72ac318e1" + }, + { + "id": "fee3a2da-1f0a-5a62-abd4-754d53e85be5", + "version": "MTc1NjMyNzc0NzU2NzAyNDAwMA", + "digest": "sha256:9506ccba232d02933d2a1c43e7520e340fa10ac0b8744e2927b62b3c909a500f" + }, + { + "id": "fee42dc7-3665-5d80-8fc8-3010ba0965d0", + "version": "MTc1NjMyNzg2MDM0Mjc1NTAwMA", + "digest": "sha256:2ac9a580441462389af4fd46b0694bc0718181cfb5f2cef0383dc03f4f5a11a6" + }, + { + "id": "fef0b632-3295-537e-95d3-dab853496d85", + "version": "MTc1NjMyNzc4Mjc1MDYyNjAwMA", + "digest": "sha256:a3374c15af7150277934e174f40121ea1e1983b25aeea5e492a5dcb7d03d8e73" + }, + { + "id": "fefaf476-be2c-549b-b7b6-c87bb138c8cb", + "version": "MTc1NjMyNzc0NDUwNDY3ODAwMA", + "digest": "sha256:4e17fc8fd9eae418056cc1fbb37290b59809ab6c53ec29515e5518a672ea4850" + }, + { + "id": "ff065056-8c55-5943-ba09-583f40fbcf24", + "version": "MTc1NjMyODE2NDk3NDE4ODAwMA", + "digest": "sha256:d839c2c4dca5f75c0429816f37812b945b10dcb8bd101e720f30ca088e8f9277" + }, + { + "id": "ff13e0f2-8c96-5299-8417-93348cef5962", + "version": "MTc1NjMyNzc0NzUzNzQxOTAwMA", + "digest": "sha256:055ec9c2d1c16ccc7e4dfbac9c843594148349f127a8664c5f37926297c984de" + }, + { + "id": "ff1f7554-6a6c-5059-a322-bd7de74beeea", + "version": "MTc1NjMyODE1ODIzMjEwNDAwMA", + "digest": "sha256:230327fa2df94cc4b61b936bd42d1ff50486f9cf1cd6eb8a00c2fc274ae8781c" + }, + { + "id": "ff22ed6a-11dd-55e7-a3a6-480368d24cd6", + "version": "MTc1NjMyNzg1MzE2MjI3ODAwMA", + "digest": "sha256:25b679f5fda6473e649ebaf587ef33bf1fd3b1edce463a83a5bd4e511a94b542" + }, + { + "id": "ff264c96-a597-50c3-8fd2-ef4b8da6f77a", + "version": "MTc1NjMyNzc3MjU4ODQxOTAwMA", + "digest": "sha256:a62765abdff82880938744bb53a641a272b24e3c84464d6b06dae19d3eb586ce" + }, + { + "id": "ff3cc9fd-f620-5503-a722-fa9de464b2d6", + "version": "MTc1NjMyNzgzNTY4MDY2MTAwMA", + "digest": "sha256:7bbf203ee5ba1302253f58f0a64c34e5236b3eccbcf7bb51cac520f45099f74d" + }, + { + "id": "ff4c88fe-ce5f-54ec-9cc6-4060fe4e72f2", + "version": "MTc1NjMyODE2NTg0MjUxNzAwMA", + "digest": "sha256:58ca9ced3ead18cee2853791b7f5c86f2797aba5510d83da9b93b150ddf766f1" + }, + { + "id": "ff4f06aa-c963-5e65-b1cc-220469b4ba01", + "version": "MTc1NjMyODA3NzI2NjQ1NTAwMA", + "digest": "sha256:bf9ea206364ebda816b2fe826deca61d832144ba29c651cafe352de849eab2b7" + }, + { + "id": "ff5d5a63-2a77-5255-b8b4-38a4a1c27cf9", + "version": "MTc1NjMyNzc5NTk3Nzk4NDAwMA", + "digest": "sha256:b85bb1bd0647d8a5de3e4e0e5f56a13f3b8b36d73448b280b98117a9854e75cf" + }, + { + "id": "ff5e8894-fbd7-54c8-b1d1-0b05003a474f", + "version": "MTc1NjMyNzg1NDQ2MTk5NzAwMA", + "digest": "sha256:6a289add57450a39ccecf7040c61e023cdd9192e485cc33f5e546f130f8518fa" + }, + { + "id": "ff8075a2-d30d-56c6-b617-3d985780e990", + "version": "MTc1NjMyNzc1MzU3NTU5NzAwMA", + "digest": "sha256:e7c3b00008c29f505bee6bb23d2dfc3d3173e28c01a78b6d2d60a1a5d4a3645a" + }, + { + "id": "ffaa5a31-e61b-5447-92a4-f173b4d995c5", + "version": "MTc1NjMyNzc4MjgyNzY5ODAwMA", + "digest": "sha256:47b9f632229a822d8bc243e76b9e31838502930514b56713fa4fd5cc604dc62c" + }, + { + "id": "ffb77a17-4776-5f37-918f-f5a5065c9bb0", + "version": "MTc1NjMyODE2NjkwNDA2OTAwMA", + "digest": "sha256:72d9ffeb35ac668011c14e22718ba35a41205e4a7e7697df314e62a61f8a8dfb" + }, + { + "id": "ffc851d6-2f3b-5279-9035-d3bd23762b37", + "version": "MTc1NjMyNzkxMjY2Mzc5NzAwMA", + "digest": "sha256:5a9b7197ade79f12689fa88b7e34913b74bb2a5c27e8ff099125a93b78d734f7" + } + ], + "Patient": [ + { + "id": "00125c1e-de24-5aff-b7bf-c038ee1008c0", + "version": "MTc1NjMyODE4ODAzMzk3ODAwMA", + "digest": "sha256:4c6d1bf6676330331f0e6b6e16911f2d8c64d7106e4668eed19ac28362c386be" + }, + { + "id": "003995da-9270-52d8-9f02-b7764c8016bf", + "version": "MTc1NjMyODE4NTQxOTE3MTAwMA", + "digest": "sha256:9b092458aa83d387f1a8736ab33dd97b1411c62d5ff3ebec6c2811cd8e8a8006" + }, + { + "id": "0081fd9a-e326-5ea8-86fa-b40b7745b11e", + "version": "MTc1NjMyODE5OTAxOTMzMDAwMA", + "digest": "sha256:5574f046d12ac8d9bba99d6a0511aa1bb7d854146750939d307a4700c7031a49" + }, + { + "id": "00b33589-090e-5978-9693-a241d357207f", + "version": "MTc1NjMyODE5MzYwNTYxOTAwMA", + "digest": "sha256:2fd74f8516c8ecd18973fb4c6c88507c69294bfcf7bec08bc319214edec75c96" + }, + { + "id": "0156422d-950a-5b33-a0f3-b8a02e66a77c", + "version": "MTc1NjMyODE4MzAwMTczODAwMA", + "digest": "sha256:d5f2a47b1f63c29c2b67225d2edbae349dcb11bc87d99e2d5aff1568a7211af5" + }, + { + "id": "0158b46a-d574-5e8e-9f20-3cec1e662981", + "version": "MTc1NjMyODE4ODE2MDI5MTAwMA", + "digest": "sha256:9696a1a05c16c3617f2b50425fe6a9b9c07762d328db771962a583ed6a4cd4a0" + }, + { + "id": "01649ab0-33dd-50cf-88e1-b33b4a8f42f9", + "version": "MTc1NjMyODE4ODMxOTIyNjAwMA", + "digest": "sha256:5d1418ff156f1a5e8c43a12cd18631c4ae77923b351a41f5d33ecab5fcee35c2" + }, + { + "id": "0219f84c-7f44-5535-b650-076ff405e474", + "version": "MTc1NjMyODE4ODI3MjI4MzAwMA", + "digest": "sha256:06fb5729d7e433c82693cff0f1173adea86966ec54578df6bc3f8119c7e2f299" + }, + { + "id": "029f891d-8404-5616-ac1b-af012cf87438", + "version": "MTc1NjMyODE4MzA2MDkxNzAwMA", + "digest": "sha256:06b5d9783aeab73aa49f87669a4b4ad640e06aa7ef0c283bcb8e6508bb0229ca" + }, + { + "id": "02afafb7-b98b-59eb-acac-c985abfd7d97", + "version": "MTc1NjMyODE5NjQ1Mjk5NDAwMA", + "digest": "sha256:dde681fbaae08b9b6425fbec178a2d8c07d1b280047500463fb210ee89a48dee" + }, + { + "id": "02cf409c-ca33-56e6-a748-a34526ba480c", + "version": "MTc1NjMyODE4NTg0NzMzMDAwMA", + "digest": "sha256:0d7f4ddc10da0d46bb5d1b7424d9a5da3085d048c551b2123fb4666df9d20302" + }, + { + "id": "02f00b49-39d1-54e7-a0df-03cd699fba51", + "version": "MTc1NjMyODE4NDYzNDMxMjAwMA", + "digest": "sha256:327b51287bf795a8035986cda9af9cec35ada3bafed52dccab645914943838d3" + }, + { + "id": "0320303e-612e-507e-be2d-d56a755eb975", + "version": "MTc1NjMyODE4NTQ1ODM3NTAwMA", + "digest": "sha256:f0203dcfe135c5c85452375583f908bcf68d65593e9d40064d5166b43adc53e8" + }, + { + "id": "03229f73-7655-546a-844f-8266e86321a0", + "version": "MTc1NjMyODE5MDIwMjY4ODAwMA", + "digest": "sha256:db9a538b6acbcce141ac992a30c61b1c05efa202059921459854cba24a8f2538" + }, + { + "id": "03404be1-54c9-5146-88ac-cacb377ce946", + "version": "MTc1NjMyODE4NTQ0MTUwMTAwMA", + "digest": "sha256:2d508bb1f598bd2540ed3c32e0fb8a1f6e4a38ac919aa2d90348a66b2dbcbb06" + }, + { + "id": "03731fa6-db5d-55a3-a8b2-db8fa9c4d615", + "version": "MTc1NjMyODE5NjQ0MDk5MzAwMA", + "digest": "sha256:ac304059f55811497807877239f5f5406c9f3d0c7d3c5f95bcc534a4627bd9e4" + }, + { + "id": "0386de81-ed64-5adf-b082-d3393e7f9853", + "version": "MTc1NjMyODE4NDUzNzQzNTAwMA", + "digest": "sha256:f9b4c4b8ac8c23fa4b6d0cfe43f7b8d2eed06ed6b37a10273357b655f1111350" + }, + { + "id": "038fb03d-8861-54c5-a93e-35a0a4b35403", + "version": "MTc1NjMyODIwNzgxMzg1MTAwMA", + "digest": "sha256:07cc6354af869fe76e88709b4758056d39bab4fb8807daef34b2a364ed4d2736" + }, + { + "id": "0399f474-22be-5f07-8b87-7e565b5d0a1a", + "version": "MTc1NjMyODE4ODMzMzQyMDAwMA", + "digest": "sha256:f128f9ff46c1fc9147b0900009fa7b04116b24885d79cfb33f152888f28acd2e" + }, + { + "id": "04012f28-e7c1-5d11-9ec0-84498aebb1cf", + "version": "MTc1NjMyODE4NzA4Mjg3MzAwMA", + "digest": "sha256:391e7291545aa556f26d7ddee9f88d4215bf58ef2e3cdcb24959f4939bd7eba1" + }, + { + "id": "044a3a28-7335-5863-a6fd-c4f8e36a79a2", + "version": "MTc1NjMyODE4ODE3Njk1ODAwMA", + "digest": "sha256:551fb535600e5f75109ca26f15382152282780a94618de3a08d83ae2fe040c96" + }, + { + "id": "0459b56f-22fd-5d21-8387-86538d2dde6b", + "version": "MTc1NjMyODE4NTg3OTQyMDAwMA", + "digest": "sha256:6cbdbdeca4949209be57eea743f04f7b5d9a94f59c39b791ecacc3f5aefafb22" + }, + { + "id": "055e6167-a565-5ea1-bcb4-24c78501e731", + "version": "MTc1NjMyODE4MjE1NDc5NzAwMA", + "digest": "sha256:7f15f1eeeef97cad1f0f4b6e7ceb7dd6b743ae6468a08e79c267149929f46ef9" + }, + { + "id": "057753a0-29b6-53bd-9e6b-699d34843157", + "version": "MTc1NjMyODE4MzA3OTQ2OTAwMA", + "digest": "sha256:6dfbc53dc6abf4ac1d2ea7f9b2d057f68e807aa31e852203d43194cc21f07b4e" + }, + { + "id": "0619e2fe-d72d-5cde-8512-76f45401b657", + "version": "MTc1NjMyODE4NTk1NTExMzAwMA", + "digest": "sha256:edec4c0a975499b6775dfe8839b4afeab39c62145ac7fa51eaca215466fc2798" + }, + { + "id": "0651faf3-5803-5127-8dd7-19e7bd9b9e2f", + "version": "MTc1NjMyODE4MjE5MDc4NzAwMA", + "digest": "sha256:8d58f3cd72482e33bb92ea48b723fdc928670486e0926ce648911895ff3c4802" + }, + { + "id": "06927f1f-04b7-5784-bdc7-5d3cca83adcf", + "version": "MTc1NjMyODE4NTk2NzM0OTAwMA", + "digest": "sha256:50ee13fd0759a9457978d7b6d5f5f9a3a4642be97c75c1e3dca56afcc7cfad98" + }, + { + "id": "069e3e62-f3cf-5978-ad17-4b472d91b4c3", + "version": "MTc1NjMyODIwNjk2MjI5NzAwMA", + "digest": "sha256:679b2b61cd712f8f0b73b43db39347740beb78a783d33849e55acd1b1324387e" + }, + { + "id": "072ca280-206e-525b-afed-7a77dc757767", + "version": "MTc1NjMyODE4NzkwOTU4MDAwMA", + "digest": "sha256:7fc7a0ddb996a0dc31342736badd2e3c305953a3aa8069e905e3f5eb5315ea85" + }, + { + "id": "074edb03-e0f6-5c43-9b32-7a7ecf8e4e93", + "version": "MTc1NjMyODIwMTE4MDE0NDAwMA", + "digest": "sha256:48b982fcc01227e5911e9e60befaf0f2f51a95a36be0d94e0730a9d07c08f58e" + }, + { + "id": "07673486-8b83-5191-af6a-93306f2c5775", + "version": "MTc1NjMyODE4NzkyMjQ0MjAwMA", + "digest": "sha256:74fadcab5839c5560f4bdb2d94d0d7f569071434185795527f7d977567eae180" + }, + { + "id": "07690ff1-647f-5f2a-865f-811e180187bf", + "version": "MTc1NjMyODE5NjMzODI3MDAwMA", + "digest": "sha256:15c17bf5fb5b1cedbd2868a089f6e19848017dd730f2525c406a5f22d4c8c995" + }, + { + "id": "0774f2b8-9db4-5623-88de-845b1bd299e5", + "version": "MTc1NjMyODE5MzU1MzcwNjAwMA", + "digest": "sha256:f533f279e2b81be182be11120e158ffac6bd4855e110617b5a8402f43f60a91e" + }, + { + "id": "07849d24-eb27-5942-80a9-1b2fbf7d55ea", + "version": "MTc1NjMyODE4MjEwNDEyNjAwMA", + "digest": "sha256:23091633b67a00d7c8dac2e18c5e5ba4820fcbd38efa2eb310da96dcba1d6709" + }, + { + "id": "07e72347-82de-5b47-812b-caf891baa816", + "version": "MTc1NjMyODE5NjQ0MDQyMjAwMA", + "digest": "sha256:95465525867d53621e323bd4be13d34483e337818fdd973cb5919c5972945526" + }, + { + "id": "089c72d8-98d1-5293-92dc-23f5f5f7f757", + "version": "MTc1NjMyODE4NTQxMzI3MjAwMA", + "digest": "sha256:1f76ef7560eaff4d5fbe60865ed5e6aebd3aad8536354c79885f468cd9f7abdd" + }, + { + "id": "08bc88ae-84f8-5cb5-9dd8-9fc1a385320d", + "version": "MTc1NjMyODE4NTc1NjgyNzAwMA", + "digest": "sha256:afe541c0e243d1c8437e81371dc764b7a9165a22dedf64e82b46338c0667d45b" + }, + { + "id": "08c5b25e-860a-567c-b292-b388d4d34482", + "version": "MTc1NjMyODE4MjgyMTc2NDAwMA", + "digest": "sha256:08c43bca62f5a95c6724c084d11756562f6e2805d552619cfe7bfc0cb694d0a4" + }, + { + "id": "08caaade-f531-5440-b30a-81191ba4bad3", + "version": "MTc1NjMyODE4NzEwODc3MDAwMA", + "digest": "sha256:eaace44588cc9480ece55f9996f35c64dbbb71d0a270b420feb5205f7693c016" + }, + { + "id": "0920d2ae-dfeb-58ae-a0a5-740431ac202e", + "version": "MTc1NjMyODE4NTMxNDM2MzAwMA", + "digest": "sha256:6d944858ddc62ba541adc6961297037360bc57aeff15d74a9c488cada95d4229" + }, + { + "id": "09235298-7256-5459-ae73-4363e6f1cce3", + "version": "MTc1NjMyODE4NTkwMjcxMTAwMA", + "digest": "sha256:ba3d31578c8d4e5aa729e717d7e1217c42682f496fb1a41f9a144abafdff886f" + }, + { + "id": "0954ecd4-7230-50c7-935b-fbfb5f51d1b7", + "version": "MTc1NjMyODE4NzA2NTYzNzAwMA", + "digest": "sha256:9c981405f5fc5a202c0f8e68737f5054fd98e11bf04ff395df980e3082616028" + }, + { + "id": "096e7cf9-d2d0-51bf-afc7-e5c7c54e0561", + "version": "MTc1NjMyODE5OTAzNzM0NTAwMA", + "digest": "sha256:f841c14289f99be22f64f99dc9b71244eac36709c543b3b7abc3d4f0f1a0972c" + }, + { + "id": "09ac7003-221a-50c4-94a9-c987de69fee4", + "version": "MTc1NjMyODE5MzQ0NTAyMDAwMA", + "digest": "sha256:e10cb0b432f081f753d0ba20ac1ba691539652893d3f8b1141e4978c6ff611a1" + }, + { + "id": "09ed4066-e474-5667-8809-13ac719281bd", + "version": "MTc1NjMyODE5MzU1NjUzMjAwMA", + "digest": "sha256:0e35465106e43f2833f56c8f1ff8d5e0d222b1a1d369a2c52c7c064c117a93ab" + }, + { + "id": "0a4ca255-8dcd-55f9-97ad-ed53d3a7b131", + "version": "MTc1NjMyODIwNDM2MTk3NTAwMA", + "digest": "sha256:374e97a3881e6406c5c9e77c2605596e42fdd322c2c063f67f25decfbf5bcff0" + }, + { + "id": "0abb291c-63e1-51be-9bf8-32542b8e526f", + "version": "MTc1NjMyODIwNTQ2NTU5MzAwMA", + "digest": "sha256:e11e120b1543f38dd8272bb79dd935ec0a9730d172292982a605078c3a094b67" + }, + { + "id": "0b049643-076d-5d7c-bd1f-f3cd16406c45", + "version": "MTc1NjMyODE4NTg0NzEyNTAwMA", + "digest": "sha256:c1bdbc58f7f783dd5dd7408b07a743b55df89b03acad9e6827d62f30e3ec1991" + }, + { + "id": "0b62a0b9-19e4-5866-906a-2a1b112ca135", + "version": "MTc1NjMyODE4NTUzNzE3MjAwMA", + "digest": "sha256:692d14415f7df8b1f5f76669c0ae87fc6a17c60e6e802e99b2c4e27db383642f" + }, + { + "id": "0b88bcac-5da6-52d5-9d59-8d97f4b5cd01", + "version": "MTc1NjMyODE4MTkzNjgxMTAwMA", + "digest": "sha256:ae0766751543026da7d4e366b30517e26d91fddea70c3fd10b46a14aa4a183d7" + }, + { + "id": "0bbe8a1f-14e9-5a4b-b4a4-91f6ee8a2b58", + "version": "MTc1NjMyODE5NjQyOTEyMjAwMA", + "digest": "sha256:46f8170a11879e1959277bae94d636b216bb4dc45e93fb44aacf304cd1a23fb6" + }, + { + "id": "0bc5348e-db39-5a74-a7e7-0809a8c6e21f", + "version": "MTc1NjMyODE4NTQyMzk0NTAwMA", + "digest": "sha256:b2a33e2ca3a5734f6002f44b5c5a37bda3e241e9f429dc3eff3e2379ef7e7252" + }, + { + "id": "0bcddb9f-b808-504c-b7fa-f5080dde7c58", + "version": "MTc1NjMyODIwMTI5ODA1MDAwMA", + "digest": "sha256:6f725b485c37b2f39041407ffc1e29ad8ce620ba32ad0bd6c86a61060dd6826e" + }, + { + "id": "0bfafc91-b1f4-58ee-a328-86ce242cf637", + "version": "MTc1NjMyODE4MzA0NDU4NjAwMA", + "digest": "sha256:7582fafd0950191db5c4c572d4b4bff75ae6bbc8c1d137eab3a33ad737b6ff10" + }, + { + "id": "0c7995a5-7343-502d-a0c3-b165ebebf1c2", + "version": "MTc1NjMyODE5MzU3NTg3MjAwMA", + "digest": "sha256:14d654edbfdd124e669b5e94d5d00e2afbe7dd4596d3df470d873d91aa8ccb2e" + }, + { + "id": "0cbe763a-0bbb-5ddb-91fb-e8552f1dd313", + "version": "MTc1NjMyODIwMTIzNzU5MjAwMA", + "digest": "sha256:5ac102688c33c15220afd80541ecfd0f964bc615c0e0b5fc4f951fca769387f7" + }, + { + "id": "0d663402-056e-5f5e-859b-a2b95116e701", + "version": "MTc1NjMyODE4NTQ2NTk0MTAwMA", + "digest": "sha256:6f4825d085f495f351efd0efabdd7c096162606a3279ab627833f3978d2b531e" + }, + { + "id": "0d889ad8-17c3-5ba9-81be-46cf525b0d7d", + "version": "MTc1NjMyODE4NTM1OTY2NDAwMA", + "digest": "sha256:9d5c2724d81315f44c3c41d1dcc722042939b0d51ddc3fa5fe52711a0679b272" + }, + { + "id": "0e0326d6-8f42-597f-9a50-d97813992858", + "version": "MTc1NjMyODE4NTkxNzg0ODAwMA", + "digest": "sha256:b80a9c5a245e1d65dc4de76ac2f1ab2c77eace14cc5933fc334ad5f1fe4f4f8a" + }, + { + "id": "0e8b1a1a-3177-59ad-8715-4321dc04d001", + "version": "MTc1NjMyODE5MDA3MzQ4MDAwMA", + "digest": "sha256:46c1bb7a65e91e8e84db124897d9e65a97a3a5cd9f5c3778b836ef465229a7f3" + }, + { + "id": "0e8bea08-f403-5c0a-8b50-d111fbdcafeb", + "version": "MTc1NjMyODE5OTA4MzQ5MTAwMA", + "digest": "sha256:49d913a6a0c4452c9952f17589808860b6f5ca2812756078bf5a37fe12d0ee59" + }, + { + "id": "0eb732a1-fd4c-5b64-8092-6eb7c0b54683", + "version": "MTc1NjMyODE5MDEzNzE3MDAwMA", + "digest": "sha256:c41a1fe225890be4ed1d15bc9670a4462ed458c59e31acc8b9e2be4bbe1a695b" + }, + { + "id": "0ef3dd28-7e2c-5f64-8733-662fdfa4b3de", + "version": "MTc1NjMyODIwNDQ0Mzc2MDAwMA", + "digest": "sha256:6fd3714d4a9797da58b82a32b74a9da936f05d85afa42e69fd35ce1f3dfe5941" + }, + { + "id": "0f1f962b-fde1-565d-a619-4ea2ea32cd18", + "version": "MTc1NjMyODE4NTc4NjQ3NTAwMA", + "digest": "sha256:c77ee482c8b1745902ca18893fb228e2c350780126a5d2611d8cf079222f5811" + }, + { + "id": "0f487aa9-30c8-56a0-947b-b9402940a028", + "version": "MTc1NjMyODE4ODAzMTgzNTAwMA", + "digest": "sha256:3b73c2c04dcb86afafd6d63f803ddfd4b9c77b3e752e119acdb9994919a141d3" + }, + { + "id": "0f9cc75a-63f2-5b8f-b8b9-84ea93decf25", + "version": "MTc1NjMyODE4MzE0NjM0NjAwMA", + "digest": "sha256:e0cc8601bd1d363674457b66048a2ba81da7857459aa4ccb90afc673999d5922" + }, + { + "id": "0fbed31f-2e37-59b7-a096-0093758b5481", + "version": "MTc1NjMyODE4Mjg4MzczNzAwMA", + "digest": "sha256:d4ea779523413a42929df4e1029dc9de435c37970d650107a43fb986fa9f9503" + }, + { + "id": "10092ee4-52f1-59dd-9964-30aa931664c8", + "version": "MTc1NjMyODE5MDIxNjkwODAwMA", + "digest": "sha256:d2e3a1b21dab5705796a7e461db98b51876e41bce0458474e8678d638f45add0" + }, + { + "id": "10d2f928-8f9c-5bb1-8cfe-0dced7d9211a", + "version": "MTc1NjMyODIwNDQ0Njg4NzAwMA", + "digest": "sha256:a960fc4aa13ad2a5dd25a58a2035c01f0e8271908baa275a74f1af00789c73be" + }, + { + "id": "11ddb5e6-84f2-5918-9934-5eb59cbf5199", + "version": "MTc1NjMyODIwNTQxMzcxNTAwMA", + "digest": "sha256:e43b0620c10d318ec2e17e0598fd5d9c1e117bea9e8ccad374ddd8c99e7fa651" + }, + { + "id": "11f845d0-ffd0-5fb0-befc-0a267b2cbae6", + "version": "MTc1NjMyODE5NjE5NDM3MDAwMA", + "digest": "sha256:1640396d8d9fd54470c4b0077ced4b84aaa35d80172731a5344aada9f9755a1d" + }, + { + "id": "1223f470-4d79-5008-9563-4cd09dab2dd2", + "version": "MTc1NjMyODE4Nzk2MzczMjAwMA", + "digest": "sha256:1af2770bce4902ccda726215e71d0ad216a6cac5533cd74761710ee9d562b164" + }, + { + "id": "122f5a73-2865-537a-adcc-120611e33382", + "version": "MTc1NjMyODE4MzIxNTI5MTAwMA", + "digest": "sha256:c53e1e98c227bc6351624410cac48115fb4246cb4c67f0b872a3b73bc66cdc9d" + }, + { + "id": "127b31c7-bca4-5a09-ae5d-c2a592ce84c4", + "version": "MTc1NjMyODE5OTEwODQxMjAwMA", + "digest": "sha256:0d81d489aebf72f218d71031089fb9358074eb1cfd8788c510b841679857b25f" + }, + { + "id": "134f818e-6507-5300-be07-a474d3de25b5", + "version": "MTc1NjMyODE5NjY4NTk2MTAwMA", + "digest": "sha256:12a91e554de77fbe10eceb782dcf53f3cbd9d78a753edbc0167348ca6c7609a3" + }, + { + "id": "1377ce8e-0388-5277-a003-8954ddd8cc07", + "version": "MTc1NjMyODE4NTU2MTYwOTAwMA", + "digest": "sha256:e608bc0cbf79bab301ddfa0d70b8a18f11ff0ff6dcc823afe4f1c2dab1ba867f" + }, + { + "id": "1393240c-0123-509a-8270-c81cac714e04", + "version": "MTc1NjMyODE4MzE5MTE0NTAwMA", + "digest": "sha256:0f3b5c495c05fe2efadd5212398e098fe4d5809974aeff976544da60e75470c6" + }, + { + "id": "13dcb7a5-5d7e-5a7f-9257-bef71a777940", + "version": "MTc1NjMyODE4ODE4NzU5MDAwMA", + "digest": "sha256:3d6e0e3454840806baf8bb719a99d579598e45a0b61c0733b3cfeb597fe47b83" + }, + { + "id": "1431f6fc-752f-54d7-af86-bef7c167d3ce", + "version": "MTc1NjMyODE5MDEwMTYyMTAwMA", + "digest": "sha256:0427b1cc04583094fed0d95c037b16c9556c8137093cfdd07d702a6bebd2b779" + }, + { + "id": "14421398-51dd-50df-b920-f4b56078435c", + "version": "MTc1NjMyODE4ODA0OTcwODAwMA", + "digest": "sha256:b2b4285774fc8b5cc111daacc024218ed3ff74b2a35277b279a3a0e7836dd4a2" + }, + { + "id": "14753861-6743-514f-946a-cc8cb1bac9f3", + "version": "MTc1NjMyODE4Mjg1NTE4NTAwMA", + "digest": "sha256:3a148c9503d413dd305a55a9871f641fd4c5a18918cb2f705397ab323908525d" + }, + { + "id": "14afa87d-1515-51c8-af91-db1187507da2", + "version": "MTc1NjMyODE5Njc3NDQwNDAwMA", + "digest": "sha256:71eba390ee5f617af580cc7bf5f8a63b8c4e599c75db15a530cc579d0cc6b9aa" + }, + { + "id": "14be2e58-923b-50b6-8670-bffbecf7ac74", + "version": "MTc1NjMyODE4MjA5MTQ5OTAwMA", + "digest": "sha256:b830505bb7bf8858268084821890a78b77b849d58ea85fb28758ff176b515aef" + }, + { + "id": "14faf8c0-a10b-5a34-9896-9a840555c5a0", + "version": "MTc1NjMyODE5NjIzODMxODAwMA", + "digest": "sha256:7fb85d19df80af6e5c8fd264fbb8369fb876fad90d61f071e5e9b041bef946fe" + }, + { + "id": "150bd9cc-a1e1-567d-96a9-43a338924f31", + "version": "MTc1NjMyODE4NDU1NzEwNjAwMA", + "digest": "sha256:04ae5a31ac334ebdea2d96220de2c484dac4909d056d4dc9326075ff897a5889" + }, + { + "id": "151aa8a6-29b9-5c24-8f55-02bbec63e14a", + "version": "MTc1NjMyODE4MjE0NDczMjAwMA", + "digest": "sha256:8337eea8ce345e48e39cd9f6deb9f21b0714f8c16e88812dff8ae89df6589c15" + }, + { + "id": "15223278-2dac-5a56-be4a-9e5ae57c5265", + "version": "MTc1NjMyODIwNTQwNzkzNzAwMA", + "digest": "sha256:7f62fcd9700fa010626b70dbf5b0638d8d517e6e58d9f05889b6a53b8c0c4788" + }, + { + "id": "156de792-45ba-5a4c-9451-4761aaa5d013", + "version": "MTc1NjMyODE4NDYwNzE2NTAwMA", + "digest": "sha256:722c3b201cd7b576459628e7dda328a445d1c78d89a8e85d6aee8eb64f873857" + }, + { + "id": "15a1389d-3bbf-5105-a228-a77ab47d26b4", + "version": "MTc1NjMyODE4MzE4MDE5MDAwMA", + "digest": "sha256:51d6ac7fdb989fd8b2aa8b1d861c02ed2173a525b3e3f207503c565b3def4949" + }, + { + "id": "15fe3571-9335-5827-9185-6ef52e667a26", + "version": "MTc1NjMyODE5OTAwNjIyMjAwMA", + "digest": "sha256:57b2a3888f673c150dd8e86d163637bd1024fdf2de9a0af79b22c01ab96c1651" + }, + { + "id": "160310a8-ab96-53ea-82a5-67eb9a47860e", + "version": "MTc1NjMyODE4NDU1NjA5MTAwMA", + "digest": "sha256:d1aa97caf28ba6c1770fa4b93e2c7c1b7695ee4d6bb4f0e1ea0a2883d0fe4b92" + }, + { + "id": "1607a323-6a4c-51e1-81b6-67aba88a59dd", + "version": "MTc1NjMyODE4MTk3OTYwMDAwMA", + "digest": "sha256:7a0b1c1eef35389116811949b1297b818ec1199097820c3381cfb9089d1d9b96" + }, + { + "id": "16592f8e-947e-57cd-90a4-d645837e3446", + "version": "MTc1NjMyODIwMzMwNDMzMDAwMA", + "digest": "sha256:69dcad8e9ba1f86f3bb4f37da72de28d12409bdfaa62a30b0f9857141f15e4ee" + }, + { + "id": "16698876-6447-593b-b9df-6fb18d00062f", + "version": "MTc1NjMyODE4NzEzMDEzOTAwMA", + "digest": "sha256:625db3c1b45d2843d1f76a731d970362297ca41368b756207062b43059db4b52" + }, + { + "id": "167e7d61-c223-51be-8d3e-9d9358086430", + "version": "MTc1NjMyODE4NTg0MTUwMjAwMA", + "digest": "sha256:722fe4f80355847740a47ea165dfb36798b3de140cf6cdeee35d1c5f5464736e" + }, + { + "id": "16975be2-33b4-55e6-92e4-cf0f5085053f", + "version": "MTc1NjMyODE5NjQ1ODkzMTAwMA", + "digest": "sha256:2d17a01dc025fad87739026b2ff17c66ed262d210835d362557015981d5a9175" + }, + { + "id": "169a162a-e37b-5b8f-baee-ad7cf7fa94e4", + "version": "MTc1NjMyODE4MjkxNjU0NjAwMA", + "digest": "sha256:843a1dc83ae872f2c401db6d7f6b887360334018afaba7efa46f6905a0cfc5fa" + }, + { + "id": "16d6337f-bffa-5bd2-8ec3-7fa3ef9005c8", + "version": "MTc1NjMyODE5NjQ3Njc2NjAwMA", + "digest": "sha256:68f2cab360fb10726fbeebbc65419812cc5f5190045e8ebd84061fc0fd5b5661" + }, + { + "id": "16e12d69-3200-5208-857d-b6006b81ccd6", + "version": "MTc1NjMyODE4MzA5MTMwMDAwMA", + "digest": "sha256:d4113a85059ba73e2fc6c64dd84f9acd3a55c6b58276fc6b1a4d70e43743872a" + }, + { + "id": "182941ae-8734-588a-ba6b-08fd6a753e03", + "version": "MTc1NjMyODE4ODM1MjM1NzAwMA", + "digest": "sha256:336fb20666afbaf9bcacba444f23503dd401976cb8995a9daace368a796a7564" + } + ], + "ResearchStudy": [ + { + "id": "638f6162-e000-5167-8216-962d86b74a98", + "version": "MTc1NjMyNzczOTk2NTk0NDAwMA", + "digest": "sha256:f7b7cc199961b6bb6f52a61f2f1068053af623cd52eb98180e2ddaf331a689ca" + } + ], + "Specimen": [ + { + "id": "000283ae-6d76-5909-808b-9a86aad05c4b", + "version": "MTc1NjMyODEwNTMwODEwNDAwMA", + "digest": "sha256:88c0f37a4a98e477203a143c20b3fe8bf1856d0bb41e173ed7a6dcecc3314f4f" + }, + { + "id": "000df82c-8c67-5af2-921c-d47159dc4ff9", + "version": "MTc1NjMyODAxMDQ1ODgwMTAwMA", + "digest": "sha256:4d0ae70a93b2d9f6cf43962c96fabe64beed80ac2b9d02aa01801c54059c02dd" + }, + { + "id": "001288e6-89d2-5824-8a74-9b6023a46a5e", + "version": "MTc1NjMyODEwOTE5NjA3MjAwMA", + "digest": "sha256:fa815acb3a7c9060b031194ad8f1b67b136c2cea087df83fa894da8e563ec4f9" + }, + { + "id": "00168977-650c-5f79-90cd-36b06ec537d4", + "version": "MTc1NjMyODAyNjgxMzE3OTAwMA", + "digest": "sha256:232fd79cff5bcb55b526cfe90ef350ffc4dadad47531aae00b0dd16ba66976e4" + }, + { + "id": "0025bcc9-aab9-5518-8960-7d302318f4b1", + "version": "MTc1NjMyODAxMTYwMDM3NjAwMA", + "digest": "sha256:e2860660c8c4688d64360dd74f2e9a61200d5c85c399e4ee9cf795ea9cc034dc" + }, + { + "id": "00322a9e-3dda-516b-9e66-0b8164277c53", + "version": "MTc1NjMyODA3NDE1NTY0MDAwMA", + "digest": "sha256:765f6dc9fc7005bfa0404c7fd7f1e8f9f2597fb885c0c739499a0e09930a2ef6" + }, + { + "id": "005a081e-80b6-5531-9042-faf49f639bf1", + "version": "MTc1NjMyODA4OTM0MTA5MTAwMA", + "digest": "sha256:5b3b3749153bb4848550cd85a8dab3b12c42406a5dceb56675cb8418073fb4d0" + }, + { + "id": "00694907-81a4-5ba3-ace9-1ab4143e602d", + "version": "MTc1NjMyODAzNTg5MTQ2NzAwMA", + "digest": "sha256:e1376a8e1c94e6fbb77d1051a251dfd98801fd402c11368b518b15e97ea9decd" + }, + { + "id": "00744a49-89bb-529a-a5c7-7c47cd5c80d0", + "version": "MTc1NjMyNzk5ODIwMjI0OTAwMA", + "digest": "sha256:4536dda7834a28ea1f47a93ef3ddc6a309062a3a57bb794ede7367ba8eda5d49" + }, + { + "id": "00822db9-3156-57bb-9743-838249436c77", + "version": "MTc1NjMyODAwNDAxNTI4NzAwMA", + "digest": "sha256:f5360782323bcc629af7878d302b618dcac33a181df50c36bb925b6e079a6981" + }, + { + "id": "009955bf-ba53-5468-8200-f29a46b11f11", + "version": "MTc1NjMyODAyMDU0NzU5NjAwMA", + "digest": "sha256:181f7e91866fb252412379d70338121a67ba7e82be40b9df874906906687068c" + }, + { + "id": "00ab44de-9fe1-532e-bc99-f67dc7d00c94", + "version": "MTc1NjMyNzk4ODc0NzQ2MzAwMA", + "digest": "sha256:b9eb5873d33016e41addfa8c61f208dd9d15e2778cc1e2ea8142c09fe9580f92" + }, + { + "id": "00b1f73a-01cc-5e41-8f67-d21a41338b58", + "version": "MTc1NjMyODA3MjU3MTY1MzAwMA", + "digest": "sha256:21c3bd142ed98318df46d819d8584ddc7f8d657625a9246ae2ffe602da20a227" + }, + { + "id": "00d2b0cb-9ad1-5a28-b51b-2e2abbd7c7da", + "version": "MTc1NjMyODEwNTQwNjM2MjAwMA", + "digest": "sha256:016ab7fd8df5cc4f643b8b67b186e71c13b16e0df4ead8f155f2cd24e9497903" + }, + { + "id": "00f83fe4-2393-5584-9a79-6edc76737d23", + "version": "MTc1NjMyODEzMTA4Nzc3OTAwMA", + "digest": "sha256:be5185a0b5fbb64e312e28f21d74d94fd3ada06ad9a28d71a20598e2fb140516" + }, + { + "id": "0103ade5-4f88-5104-bce1-fcb82ed8cfd2", + "version": "MTc1NjMyODAyMjE3MTU3OTAwMA", + "digest": "sha256:2b496462fd73b7a0a3186a95080bf0128196733ac8d8705b99695d50f8f0dbc9" + }, + { + "id": "01400b92-68af-5b72-adae-e738868b0f74", + "version": "MTc1NjMyNzk4ODc1MDIwMDAwMA", + "digest": "sha256:a4630be8ff9109e540b473a30285da6ebf88b87830261bcb30aa16ec26b8eaa1" + }, + { + "id": "01555e72-1ad3-54fb-8ec7-23a299a5b1ee", + "version": "MTc1NjMyODEzOTM1MDE1MjAwMA", + "digest": "sha256:27b4cc727055d6ed039266c910d6c1bbcc696aaad140d3a54dda7c11e5fc6018" + }, + { + "id": "015766d5-3858-5183-8d66-784d4a4581fe", + "version": "MTc1NjMyODE2ODgzNzE5MjAwMA", + "digest": "sha256:a189b291debf2d90ba2f818a016cb10b67ef154eb83e3f12217fd49d2d47a44b" + }, + { + "id": "0167453c-547e-57bd-bc77-33a9f753ecfa", + "version": "MTc1NjMyODA5NDQxNDczNDAwMA", + "digest": "sha256:40e2b2fa4825688a1740fbb03bcf9be2bfc7c235acde6036a3225f6ec0a97f3e" + }, + { + "id": "01906680-0486-52ee-a2e9-b6b26cb199e1", + "version": "MTc1NjMyODE1OTEwMTQzODAwMA", + "digest": "sha256:5432d697d0011d656e90aab47cd0f7729aa7005f4ab3ba6932700af978f88629" + }, + { + "id": "01920375-27ee-5462-9da0-62ceaf1157e4", + "version": "MTc1NjMyODAxOTQwNTEwODAwMA", + "digest": "sha256:d7b41d487eb32656c7ce21fc0a2e851c089adc2461b5dac6db7117195b89c1b2" + }, + { + "id": "0193f893-69ba-5a73-9c31-2c1044d08f82", + "version": "MTc1NjMyODA0NDg2MjkzMTAwMA", + "digest": "sha256:d7a042f9cbdedbc3c608c1ce46e8381895f4598b1fc538ea17dc16dd0abf1016" + }, + { + "id": "01d38979-d35b-54ed-b0b9-59a616c03588", + "version": "MTc1NjMyODEwMjM1OTYzNjAwMA", + "digest": "sha256:46785cbe635187eef226243c6ccb875629b9f5f9ae6e77255cade520aa0d0d2b" + }, + { + "id": "020670a3-e996-51c8-b38d-894b4c0a4b93", + "version": "MTc1NjMyODAxNTI1MTIyOTAwMA", + "digest": "sha256:a27aa81f69fd48445c187133dff907a125780e5e0d265ebfc7a4bd8e7dab1a76" + }, + { + "id": "0210dc46-6ae0-5171-8e7d-c472361f7e8e", + "version": "MTc1NjMyODEwMjM5NDcwOTAwMA", + "digest": "sha256:7f19832415effee069eda24e772a6a93e9118b0ac042af70d136f710ab73feed" + }, + { + "id": "022e0ca2-0e86-5d86-828b-813130a4cf13", + "version": "MTc1NjMyODAyMjEyNTYzMDAwMA", + "digest": "sha256:d36913a89e375cad3461194ff78ceda913af9a97be16a4ce95fd0a20eb78cf75" + }, + { + "id": "023a4696-f570-5e22-8eba-14ffd36574b4", + "version": "MTc1NjMyODA1MjY4MDkyODAwMA", + "digest": "sha256:906e74627faf41e35e79099098261e5fe89665c06aeda185de04cf7efca6cd65" + }, + { + "id": "023f899d-e3f9-524f-aa08-b5fd9fc49933", + "version": "MTc1NjMyODE3MDg5Mjk2MzAwMA", + "digest": "sha256:34d9b1cab0157ad8a509af299dc42bafc9f3e509f0fa3fa12d9227829372e1af" + }, + { + "id": "023fd913-0c70-574b-a41f-2cf78968d0f6", + "version": "MTc1NjMyODAzNDc1NDIwMjAwMA", + "digest": "sha256:adfa3457fa0ae236299ab1d0265aec59d503bc05351bf3e2370baa8e0eda6fb5" + }, + { + "id": "025b5fa5-3423-5347-808a-3ebacffb7bd1", + "version": "MTc1NjMyODAxODg3NTI5NzAwMA", + "digest": "sha256:4d34c04b326cad9e6aea202e7bfc45789b01fe6fb3b488e77bac1bf933f3f42c" + }, + { + "id": "026ead48-066a-5b9a-b824-d15dd4485fcf", + "version": "MTc1NjMyODA1NTY4MTU1MDAwMA", + "digest": "sha256:56681be8dcf14e50fb7aa90720f21cae480a8247cb157db28e334e8f855f704c" + }, + { + "id": "02704efc-6761-55b1-8e35-2cfcfd591033", + "version": "MTc1NjMyODA2Njk0NzI3MTAwMA", + "digest": "sha256:e30a26d26041a193070d87501d465f4005e704c1f07801bcb9026b27a05d6eb0" + }, + { + "id": "02b832b6-45c4-53cf-a76f-682243f05900", + "version": "MTc1NjMyODA2MjU1OTg2NTAwMA", + "digest": "sha256:c06f41a4fb9f93884d28078b04dbefdbdc8f30063f0cbf7df572d1d5427febda" + }, + { + "id": "02e78ed0-2dc2-547a-b255-e551585a7628", + "version": "MTc1NjMyODE3MDg1MzQ2MDAwMA", + "digest": "sha256:f13b7f8abf65d3534acdeb43a068f5b357157e6f09df651a5dd90b500e92c777" + }, + { + "id": "02ee2068-dfcf-5001-b216-448c2e4f87bb", + "version": "MTc1NjMyODAxMzg1MTQ4ODAwMA", + "digest": "sha256:9d6d1a7a5880d6adaea51a509bc734ca76f942d94248829065ec7f3dca4742fd" + }, + { + "id": "03486c83-3558-5d80-9772-86929be38fda", + "version": "MTc1NjMyODAyMzQ3OTQ2MjAwMA", + "digest": "sha256:9e4b265a20a5f1205fdc45ad7aa7ff895c2826d175e8c5c91a801f14ca30dd9f" + }, + { + "id": "03629589-325a-501b-8593-3679f333c67e", + "version": "MTc1NjMyODA0NTgyNDc5NzAwMA", + "digest": "sha256:e718db2dadfe5c96c00fe4e566394b8dd4e40875b24e7965525c6f643f5403dc" + }, + { + "id": "0377543f-1f26-51e2-b552-739960ac5a57", + "version": "MTc1NjMyODAxNTQ5OTA1MjAwMA", + "digest": "sha256:94f9c3d4ca6ec9c884cc11df0630e6c4508b199443805087a154176250a53f92" + }, + { + "id": "0377f715-6552-5ce1-a6c5-64072e49746d", + "version": "MTc1NjMyODAzOTcxOTMzODAwMA", + "digest": "sha256:f28e9f687a7d7ff303096a9ed22aab25325b917fa423967ed341679d3d2e8ace" + }, + { + "id": "0378c8bf-82c1-54ad-a05a-acc5e4aa0bf8", + "version": "MTc1NjMyODE1NjQyNzkxMTAwMA", + "digest": "sha256:6b53a82987da350a4a7bb7199c696ab2d167fe6df03a5eeca951a25237ef8113" + }, + { + "id": "037bc467-3975-5992-86f6-3f88c23b3336", + "version": "MTc1NjMyODAyMzQwNDU3NzAwMA", + "digest": "sha256:4bad31a4aab9ab71246e30e3d3575c0cf8a3083e74b7e67200e609f5512691d4" + }, + { + "id": "038639bf-e5d1-5de9-b35e-a6a28986c4bc", + "version": "MTc1NjMyODA5NDQyNTA5OTAwMA", + "digest": "sha256:481a3a2cc91f79123256b91fdf0a614da53095cda32b8ef9b2e01f86c89767f4" + }, + { + "id": "03ba27d1-b432-5d0c-9899-a7178aee5fea", + "version": "MTc1NjMyODA2NjkzODE4NTAwMA", + "digest": "sha256:32b6a5e922110264dadabd575ae6857b4d5de792fc6a0335ae8d6a5d4058a520" + }, + { + "id": "03bb8138-079b-569c-add8-9fc18bfeb706", + "version": "MTc1NjMyODAyNzgzOTUyMDAwMA", + "digest": "sha256:f2b31c343931f8c8fd4792b90a69c57ab984637fdbde45255456609ce0d1ac10" + }, + { + "id": "03cdf661-29ce-5122-bcc1-47e29b81f1bf", + "version": "MTc1NjMyODA3MzIwMTIzNjAwMA", + "digest": "sha256:956867887530d9dda143ba872809974f462b7fb6e0bbd1a2dbfa8ae475a4772d" + }, + { + "id": "03ec2a2f-9495-549f-8cb6-ba1a7a6db1f6", + "version": "MTc1NjMyODA0MTg2MDM1NjAwMA", + "digest": "sha256:60cd0d332b9969155a1a7bceebf9ab38fdcc4ac77b6e47c466f3fc2c12019be6" + }, + { + "id": "03f5d4a9-147b-50ec-9660-5e42d357113d", + "version": "MTc1NjMyODA5NzcyNjQ1ODAwMA", + "digest": "sha256:ca82d8de5478cfbb66586e164c35cccef64a152faaa81cdec1c4afe228400347" + }, + { + "id": "04006804-9ea5-599f-a8fb-d61d3665aff2", + "version": "MTc1NjMyODE2MjIxMDQzMjAwMA", + "digest": "sha256:848994660b9d0305ee0e62a681ab51d15eed0e74266bc10f5371fdb778bc6271" + }, + { + "id": "04079f9a-4abc-56a2-9a15-12e0d870d8e8", + "version": "MTc1NjMyODAxOTM5NTIzMjAwMA", + "digest": "sha256:a2c5ade1a66503d0e158f3a5cbcf72c3cea1b22684a9b78fe22410ea22cc9d2c" + }, + { + "id": "0407be07-bfc4-5e16-b6b0-7138122054ee", + "version": "MTc1NjMyNzk5NTcwMzE1MDAwMA", + "digest": "sha256:bd7bc8df21bd84aa33464754be2ae197927e4dc042913668b70306fb04089b93" + }, + { + "id": "041593cb-0a38-51c0-a30c-febd30b6c863", + "version": "MTc1NjMyODA3MjU2NjAzNDAwMA", + "digest": "sha256:1349ce8eb3845c2949c648e17692c4bd6e97e668a6a9409ae30073f3bc383e79" + }, + { + "id": "0415ea70-8eac-5131-8e4b-da0b34791434", + "version": "MTc1NjMyODEyNDQ0NzM2ODAwMA", + "digest": "sha256:aa62c87335e6c6816b86523f6c988fbedd6de0dbcfbf3a88d7c49837c7f0733a" + }, + { + "id": "0437915d-1249-5592-83d8-7b8bc6a52b3a", + "version": "MTc1NjMyODEyNjQ5NDc3ODAwMA", + "digest": "sha256:ca6e8e3f66fa4f7bcdb4795ad237b86fd1512c843ab1237e43bcb596dd48efeb" + }, + { + "id": "045a7936-62ff-5a15-ad45-3c293166321d", + "version": "MTc1NjMyODEyMjIxMDAzODAwMA", + "digest": "sha256:634a59f454e65ab0e01013651051586f5aa288633274c8951f9db6a7fbaf969d" + }, + { + "id": "046b4e93-1fe3-5880-98d6-b2286a7aa0fd", + "version": "MTc1NjMyODEzNzY3Nzg1ODAwMA", + "digest": "sha256:51abea445ec8658551299a9f98d7706f7abf1f39d33cc49177767fd20782899a" + }, + { + "id": "0479b192-a2f2-5db8-baaa-830fa08cde3d", + "version": "MTc1NjMyODAyNzQ3ODQyMjAwMA", + "digest": "sha256:d85cf77da2e6d49044e4db30393408ad5d678c9c0f0693d3230c8708d1056300" + }, + { + "id": "049eb2fc-ab0c-537e-8c5f-c5df87c71f9d", + "version": "MTc1NjMyODE3MDg1NDAwNzAwMA", + "digest": "sha256:75abad9baf5e149d9332b53d135e23b3034515218273e8020a1c2a7272e4c8c7" + }, + { + "id": "04a3e2bf-475a-5219-a3b8-fb14061f275b", + "version": "MTc1NjMyODA3NDE4MTcwOTAwMA", + "digest": "sha256:88fd8aaccc244a9c7899756dbe79a25bbb2784ff1906d89c868a8fa1d44e18ae" + }, + { + "id": "04b67d2a-f3c8-5a63-bca5-7fe48698b5fc", + "version": "MTc1NjMyODEwMjIxODc3OTAwMA", + "digest": "sha256:d86292bfc321cf319906dee5edffd2683e9671a929d18ead05053f07f7bf40cf" + }, + { + "id": "04d64748-e9a9-50b2-83da-c62980bb6658", + "version": "MTc1NjMyNzk4ODY4MzEyODAwMA", + "digest": "sha256:af47395cf7504136e6cc15e5fce1c662d56c1882784445cac49f53cc188090b7" + }, + { + "id": "04e2d724-a65f-5431-8402-bd5d8ce99dbc", + "version": "MTc1NjMyODE1NjQwMDk1NzAwMA", + "digest": "sha256:d96f71b5c56857434aed4bf38b2764b0e103f96c3bd9873ec2347c21b5b97d59" + }, + { + "id": "05091dfd-3a4d-570a-926c-2fb54604d6f4", + "version": "MTc1NjMyODAyNzgyNDM5MzAwMA", + "digest": "sha256:97f48fc581382f6807a48b17289219209b96c3247a9d073352589e6d0e58e6d9" + }, + { + "id": "0511c356-75fc-5d44-8779-a3f209b5985b", + "version": "MTc1NjMyODAzOTY4MDc5NDAwMA", + "digest": "sha256:03826654875e4d9b550017c4b1ecefd98e28c54739bf2f70b10651d9ed5e8c6d" + }, + { + "id": "05592b55-7b4a-55c9-9b75-0e02524306aa", + "version": "MTc1NjMyODAyNzM5ODY0MDAwMA", + "digest": "sha256:a09b380421ecde98afbb099b8931ff33ffe8aa44405acb2faeb46a0d148774e3" + }, + { + "id": "05594b4d-4b89-5701-8f19-d8f10dd6dfc0", + "version": "MTc1NjMyODAxMTU2MzE1NDAwMA", + "digest": "sha256:aec1bb8651b9b00af2ae88296bc59a875903356599f3008729155168088754be" + }, + { + "id": "0561da4a-7de5-53a7-9f72-f4d6ff24b0b7", + "version": "MTc1NjMyODE1OTA0MzA4NDAwMA", + "digest": "sha256:1447f6701f2cd140914dc3309dfc89d7e3d202ab1fe89bbedc659c807062e547" + }, + { + "id": "05690333-88a9-51fd-b337-101b46adab4f", + "version": "MTc1NjMyODAyMTI5NjYwNDAwMA", + "digest": "sha256:244d0a1a0d7d061b175a551707205e1880ac6faf417e66ee7f93f9ebd8604628" + }, + { + "id": "058737a2-2364-577c-947a-33a4b8eddc9e", + "version": "MTc1NjMyODE0NzQ5MTg5NzAwMA", + "digest": "sha256:6bd66f969041178dcf321b80d28cee46b0272086ee95fdcf1830f0a6eb579acc" + }, + { + "id": "05a55e37-cc80-54ce-a113-1feecea1de26", + "version": "MTc1NjMyODAxOTQ0NjEzMDAwMA", + "digest": "sha256:fe14a7213e4711cadff3736a8bae21b8ba83d38f41d3990bef7d9f7b98ee2e2b" + }, + { + "id": "05a89218-46f6-5e28-b806-4c273e840206", + "version": "MTc1NjMyODE3MDkzODc1MjAwMA", + "digest": "sha256:0f9a4698721b80349ea4b924554e95fbf3c723d30c082d833e1055e551cc0cb7" + }, + { + "id": "05a8ae73-1e11-5244-b213-d330b1434d27", + "version": "MTc1NjMyODAyMzQxMDY2MTAwMA", + "digest": "sha256:a7fb349ff6f2f237f67b28e7897a043a4dc3bea55b09451b91a6623a1aefd2f2" + }, + { + "id": "05af4b5c-3b54-512b-a419-67dd04c0bec5", + "version": "MTc1NjMyODAxOTQwMTM1NDAwMA", + "digest": "sha256:17d52df73bb9a05ebf71ad9e969a51ef87298d072e2559b289108cbe7df0d556" + }, + { + "id": "05cce54d-f07f-53d0-99c5-519cd3255dea", + "version": "MTc1NjMyODEzNzA5MDAzNDAwMA", + "digest": "sha256:ff06a122285a079793d8a4711f649bc86a828109c3869ac99726510e396f3780" + }, + { + "id": "05e2cc70-395a-59c1-b3fc-787f1ac051be", + "version": "MTc1NjMyODAxMTU1NzkwMjAwMA", + "digest": "sha256:ff20b67fa7f01df2257271b79e16255862b33adb701dad107acbd250f0f56f12" + }, + { + "id": "05eeb4aa-0aeb-56dd-a192-32293df566f0", + "version": "MTc1NjMyNzk5MDM3MDI4NDAwMA", + "digest": "sha256:dadb740960db6d5c7ff29b10593097b4ecf9bf3bb5780d614907e32eff5d1ef2" + }, + { + "id": "0604d485-2605-54d2-b9b0-771a3a822c34", + "version": "MTc1NjMyODAxOTM4OTc3NzAwMA", + "digest": "sha256:b9349827cfe93fbe99d434f354ecbfbd53d3993ff9c6f72ff63c3b5df230042f" + }, + { + "id": "0606dbfc-cea4-511e-b992-aae3a08d9a54", + "version": "MTc1NjMyODEwOTE0NDM4MTAwMA", + "digest": "sha256:4a1e6bfeee15b3282146ff7bd1962ed8a9a2c87a46d54358d9078c8dce948e1f" + }, + { + "id": "064b7a1c-2308-5fb1-a3cb-3ff978667b8f", + "version": "MTc1NjMyODAyMTMwMzgyMDAwMA", + "digest": "sha256:ea5d6a79fdd7a4792922f550032e5edf81c66b12f362546654183694522c1350" + }, + { + "id": "0665d9ca-e732-5e02-a7c2-0f049d0c3bf4", + "version": "MTc1NjMyODAyMjA3NjA4OTAwMA", + "digest": "sha256:e8adcc79ff99246a1486b6b725b61cf94c8e0acc4987ca82944aa8edf41aa897" + }, + { + "id": "06823f44-af04-51b5-9c86-8a5666d90411", + "version": "MTc1NjMyODA1MjUxNDc3MDAwMA", + "digest": "sha256:ab0b7b38d9a7ae77dcd44e77d4f1bd4748c1d9c3708075de0e299dd90f8ae2dd" + }, + { + "id": "06960117-30f7-5da0-9397-f26dd4426df6", + "version": "MTc1NjMyODE2MzMyMTk2MDAwMA", + "digest": "sha256:eef41f022ddaeb5d5fb07ebcf3b8b7ccbc095e854d1702ae12c73bcd162b38a1" + }, + { + "id": "06b8b5d2-9acf-50df-9ef8-216f6c46ef68", + "version": "MTc1NjMyODA3MjU2ODY1MjAwMA", + "digest": "sha256:24d3d79b7e7937c94f5b91078c6196abef6160c0d9834729c0e99006d9905b04" + }, + { + "id": "06d0b3ac-1c9c-5899-9fc9-9c917811ee94", + "version": "MTc1NjMyODAxNTUzMDczNjAwMA", + "digest": "sha256:561e3ccd67fee62f2f78e51c792a7b0ac81b54a73cea3c676c9b7c66f4e9074a" + }, + { + "id": "06d3b425-6e2a-518a-a107-eb9bb8550b53", + "version": "MTc1NjMyODAxNjQ2NTUwNTAwMA", + "digest": "sha256:f3e66a0ffb7b2c4f99908c0a7159e7d0b35b34169a6c48384b93c44e148dcfc0" + }, + { + "id": "06de3d22-1200-51a2-9f97-727405f704cf", + "version": "MTc1NjMyODA3NDMzMzUzNzAwMA", + "digest": "sha256:214026b8c728be5a7bdb0d114738a0ec39b57e4b766eda69569d81d4cca6173b" + }, + { + "id": "06fcf47d-7850-54d0-afcd-cdcd8ec22d56", + "version": "MTc1NjMyODE2Nzc0OTczNjAwMA", + "digest": "sha256:8fca9fb6a1c63f2bb0d86656c2825bcc3d77167616187f7f33d873ed51de6433" + }, + { + "id": "06fd1c4d-4579-574d-8023-e3cd976394d9", + "version": "MTc1NjMyODAyNTg4MjYxMjAwMA", + "digest": "sha256:844ea4cbc0c03aa9ab08598ff14a187b58ff923ce3726cf40199a7184d6c942e" + }, + { + "id": "07086cbd-cc40-5e07-b873-2079bc37fb6d", + "version": "MTc1NjMyODEzOTIzNzk5NTAwMA", + "digest": "sha256:b2159a370a0d617a22935fb7b54703f9df216482ed3a6facd8cabe1b35ddcb4a" + }, + { + "id": "07141c86-82da-5ded-be56-e6e24b1363d3", + "version": "MTc1NjMyODA5ODY4NTAyMTAwMA", + "digest": "sha256:8020e6a0dbc79d64845e246c8b49539a1b5663064788adbb17ffa2d1b338acc1" + }, + { + "id": "071507ba-c118-5ea0-ac1d-55f70ab602b6", + "version": "MTc1NjMyODEzNjA2OTI2MTAwMA", + "digest": "sha256:341ac49f74296e759ddcd1c6de9409d62ed9685d90d927f1418f6211dc587497" + }, + { + "id": "07297e67-b726-5c53-9148-514c660b6090", + "version": "MTc1NjMyODAwNjE3MzIyNjAwMA", + "digest": "sha256:791355efdb06cb489873b244b2486fa33c2143019f54e3b9eeb75efc244924e9" + }, + { + "id": "072f9632-dcd2-562b-acb2-7179047ede7f", + "version": "MTc1NjMyNzk5NTM1OTI2NDAwMA", + "digest": "sha256:a46f23606481f38f9ffe2578621cd9cc7d4a89d9db15c009c7de9ef80dffc116" + }, + { + "id": "074bbf79-cba8-5380-ad22-792b1ce68009", + "version": "MTc1NjMyODE2Njc3NTY5MjAwMA", + "digest": "sha256:a6d751139134e27ac55067ebd7f17d6f256e748b5aea5152015f518ab95e7736" + }, + { + "id": "0757e2da-0bbe-553b-af7b-2e6b97bafa0e", + "version": "MTc1NjMyODAxMTExMDA3MTAwMA", + "digest": "sha256:6beee2777d5db697e19c0617f0304183517f443a80a99230c2eef8f466a8b626" + }, + { + "id": "075ecb3b-2c97-54d2-a2e4-ca362cb5e77e", + "version": "MTc1NjMyODA0MTg0Nzg4NDAwMA", + "digest": "sha256:6ff37ab41e7f20346be1e895c6f7f992b26c24ca9f02363c22800efd19dfd956" + }, + { + "id": "0777ee37-471c-5679-b20f-baed06a98a8e", + "version": "MTc1NjMyNzk5NTY0NzA1MDAwMA", + "digest": "sha256:8af9532b4427e6badb3983baf942b1ba30719a73e8515b8c68285488c9a73af9" + }, + { + "id": "0797a2fd-e640-5316-bdca-8a0df0aae88d", + "version": "MTc1NjMyODE2ODkyMTQ5MTAwMA", + "digest": "sha256:00aef20d6dfd7b333926a91298de237a9b2ab4853db2acad1db9931836cc7712" + }, + { + "id": "07989435-eea2-53c4-bd32-0a89ebfc4680", + "version": "MTc1NjMyODA2Mjc2MTA4NDAwMA", + "digest": "sha256:6b60906b3b759f4dae796d9a2a7f17972f4bc48c2e34b9b8f65af0b661af43cb" + }, + { + "id": "07a0709b-a185-59a8-b347-e2913b0c602d", + "version": "MTc1NjMyODA5Nzc3NzQxMjAwMA", + "digest": "sha256:6ecba39e0dd6d706cc1ab14522db2e6986a747f7bab578b2eb0a05e813084a55" + }, + { + "id": "07b83b16-2282-5a5a-9a81-b7447906f3ac", + "version": "MTc1NjMyODE2ODgzMTg3MzAwMA", + "digest": "sha256:280ad8661bf5036456443c4b90019ca112b821c70b468061aedf011e13c75089" + }, + { + "id": "07cca6a8-573b-53d8-9b6f-c5753e9162dc", + "version": "MTc1NjMyODE1OTA1ODkyODAwMA", + "digest": "sha256:288e7f6deec393c5749ece26916d21345f7b9567d4328c04498834e9445665b4" + }, + { + "id": "07dce4e5-991d-541c-8d1b-08b7b89bd228", + "version": "MTc1NjMyODA1NDE4ODMxNjAwMA", + "digest": "sha256:7f605d0f599e98782232f103ee2efe0c394719c2a8ff494b8837c6c3c9ff0567" + }, + { + "id": "07df6016-33ac-5eae-970c-3803902ae0b1", + "version": "MTc1NjMyODAxOTM5MDY3NDAwMA", + "digest": "sha256:8dcb66736cc97a70e516aeb04168256c946a1c45f8edeb19ddff70736e9d785a" + }, + { + "id": "07f2acb0-f8b1-527e-b2d7-487f584f7723", + "version": "MTc1NjMyODEzNzM4NDUyODAwMA", + "digest": "sha256:29d3ab56c90dc980f9e760d0b21bf82ccbdb3bc3eaf6f2151761ed11c72878e4" + }, + { + "id": "0805f806-c81b-552a-b420-5c172d025751", + "version": "MTc1NjMyNzk5NzA3Mzg5NjAwMA", + "digest": "sha256:599aecc774080e8988d6c67661f4bc52b1b139a563bcda08c6287ea4b662de9c" + }, + { + "id": "081d0c31-f615-52ac-911e-4960078f6493", + "version": "MTc1NjMyODEzMTA3MjIzNTAwMA", + "digest": "sha256:b3960daf97ffb45a7b6bdbcd9aa12a2f45455f4c1ca994f6c57ebac08b129f16" + }, + { + "id": "08334093-6637-5986-87f1-00850277050a", + "version": "MTc1NjMyODA1MTM1OTYzNDAwMA", + "digest": "sha256:19b1714518f7ca65047f0fe93b2d073f75d749ffcb432d2bc852906b9ba21bd9" + }, + { + "id": "0837f2e2-c66c-5151-9dfa-051c0cce2d1d", + "version": "MTc1NjMyODEyMjMxNzE4MDAwMA", + "digest": "sha256:80357901548a45b9101e8657e2e84811166576f4d84a64a7021a223bc27c88a3" + }, + { + "id": "08469042-fb5d-5c70-923c-01be14c0a333", + "version": "MTc1NjMyODA3MjQxNzIzNTAwMA", + "digest": "sha256:99c14bd40c06437291c43916390bdcf8fb252605f15867463974b0d1b1b38883" + }, + { + "id": "0857a7d5-3cf7-5e7c-8fce-0b044622a020", + "version": "MTc1NjMyODAzNDU5NDg1NjAwMA", + "digest": "sha256:2aa5c9b4cc2fed0c8a89d6afccb299c89cc1732e3e5ec21783feef7992dca97b" + }, + { + "id": "086bc113-6e02-5618-9fdc-da1ebd81d879", + "version": "MTc1NjMyNzk5NTY4MDE1MTAwMA", + "digest": "sha256:fb7f23f9f77adb8dda8327e400829ddf8a4a865faec2026909dcd66357056178" + }, + { + "id": "087dfa91-adb5-51ed-9b76-193370dd66cf", + "version": "MTc1NjMyODAxNjUyNjg1MTAwMA", + "digest": "sha256:13af4ffe7e914edea098b77d9efb39533acaa22667574624a6d99dfcc4d766d3" + }, + { + "id": "08d3772c-30aa-5522-8fac-320aa2724076", + "version": "MTc1NjMyODAwNTA5ODE4MTAwMA", + "digest": "sha256:5ee76d250932afd9aa9506f811306db483e01242540414099ace39e8ece20f4c" + }, + { + "id": "08d7bf69-dbc5-536b-9cc7-2f032253cfbd", + "version": "MTc1NjMyODA0MjU2OTA4MDAwMA", + "digest": "sha256:f37f329dfeb551e5dd229689a846bc289ef572edcf1c8f01d9e433c5e2a37438" + }, + { + "id": "08e9bbe8-d407-53ea-a6b8-2d00ff983cd9", + "version": "MTc1NjMyODAxMTg5MDQ4ODAwMA", + "digest": "sha256:99673e57b442358d33c4f6df4a1e5adf6bf44ebb6918ad743cd81277582694a8" + }, + { + "id": "08f0e24e-768c-5772-8e41-143942a30a7b", + "version": "MTc1NjMyNzk5MDQyMjQ4ODAwMA", + "digest": "sha256:be3e34151a01257535fd0d924318b01d0ee1735edc4269153e72d9ae200b0746" + }, + { + "id": "08f81a3b-d64c-55ab-9e90-335c002e0ca1", + "version": "MTc1NjMyODE0MTY1Mzk1NzAwMA", + "digest": "sha256:8143c1ac5622aca87f51f83fe8feb7955e8a91e30f66b1d49e1c00431f254a27" + }, + { + "id": "0912dae9-f6b5-5bd2-971c-98baf235cffc", + "version": "MTc1NjMyNzk5Njc2MDUyODAwMA", + "digest": "sha256:592548e65689698414302f33df3d0bb77091b49e4989e94add68b6ba0c054287" + }, + { + "id": "0918eda8-4dfc-5e7a-b743-3cf8f8e16964", + "version": "MTc1NjMyODE3NDc5MTM5MzAwMA", + "digest": "sha256:46fb480afd3ad193af3d41801aa49441d15629bc8f4e5f8a24d6df2141cea83a" + }, + { + "id": "093587c2-8172-5a07-a0f2-fc0ab275d4f8", + "version": "MTc1NjMyODA3MjYyOTgzODAwMA", + "digest": "sha256:1e2db7e31951be7abce764b739ccac7de710a58eed0d410cb35948dc7c51c918" + }, + { + "id": "093f5b2b-26a2-5ef1-9ab7-4180c85edbd0", + "version": "MTc1NjMyODEzNzM1NTY4MjAwMA", + "digest": "sha256:d23a24758add363524d0676193738960ff7949a8a9b74719e5183d583ba4e961" + }, + { + "id": "0945fcc8-d4c3-5a36-a8a2-1bf9c46ec84e", + "version": "MTc1NjMyODEyMjMwNjg4OTAwMA", + "digest": "sha256:baa24d5e83201114a9b063dd4bf03b9b3f2f1ac141fe82a5130d28ce964ecdbd" + }, + { + "id": "0954e3e9-d5fa-55fd-836c-36ddc16f8ef5", + "version": "MTc1NjMyODAwNjE1MTk0MDAwMA", + "digest": "sha256:e7f1830dd576697bbe22ad9645207fc1634abc1562d45fac520c2aa37b2828cf" + }, + { + "id": "0976bc0f-8b35-5966-baf9-aa70013c0cac", + "version": "MTc1NjMyODAzOTczODIxNjAwMA", + "digest": "sha256:e62a60165918962e7f448988120da0f0dca2b61c368101b992dbd381827f2eb1" + }, + { + "id": "09a3e951-d6ba-5984-8c88-1e3b30f77e16", + "version": "MTc1NjMyODE0MTQxMjg1ODAwMA", + "digest": "sha256:c6a9f86e66119d75fdb68b8e4afb2b7a3a7389627658fc183c926c3ea5a2db56" + }, + { + "id": "09bf0bbb-aabb-5692-90fb-337b376a4e6a", + "version": "MTc1NjMyODA1NTgyODExNDAwMA", + "digest": "sha256:7666fcaa9ba8e7b9f5f05f834d52aeed4c60fc91f3beeec8e931598eed1c95bb" + }, + { + "id": "09cc2ae0-f54a-5286-874f-280fe45d8a8e", + "version": "MTc1NjMyODAyNzM1NjExODAwMA", + "digest": "sha256:de3cdf151c8818d1bc8177562ca90f17fcd3675418b388ad2f4f27eca8004a15" + }, + { + "id": "09df7687-8c39-507b-a29f-72f23def88b5", + "version": "MTc1NjMyODA3NDEyNDc4MDAwMA", + "digest": "sha256:f79207d0e5cad5f5493541ccc3a12fae29390e8c0c15bc813120a4e14627063e" + }, + { + "id": "09e15191-fd44-5b15-b3f1-000116072cc8", + "version": "MTc1NjMyODAwOTA2NzYyOTAwMA", + "digest": "sha256:236aa819210edcefe79ce03516c6136293356eca5d978a2e390717687618a29f" + }, + { + "id": "0a0bb170-bcd8-5a61-97db-ccadd8ba935b", + "version": "MTc1NjMyODA3MjU2NjQzNTAwMA", + "digest": "sha256:d2696737bca27d2753cb70132da2300c583832b1cf04eb1fd70e986a3c07742a" + }, + { + "id": "0a24c009-84f5-5329-9382-bfde946cb923", + "version": "MTc1NjMyODAzNTk4MTg4NDAwMA", + "digest": "sha256:701f470f7e5202d259df9cc1a198fb45d5b468c636800d1908ed654525ee619b" + }, + { + "id": "0a2e87a9-81de-503c-8767-88e908391437", + "version": "MTc1NjMyODExMzUyMzYwNTAwMA", + "digest": "sha256:fb4357263639433665ea84c4c6f9f8bf7879b5b19bfa83df030269bc4bde3664" + }, + { + "id": "0a5b2443-23c1-595b-9afd-36eb34d95779", + "version": "MTc1NjMyODAyMzQ2OTUyMDAwMA", + "digest": "sha256:351caabe398974b72acd93164751b96655d9209bc3292f7622826f0fee5968d1" + }, + { + "id": "0a63cbf9-f123-53ca-99dc-0cf02ebba701", + "version": "MTc1NjMyNzk5MDQzMzU0MTAwMA", + "digest": "sha256:2d763fad18fa91ce58a2dce24432eeab03dc320ca72d8a424fdf8b1588b75d87" + }, + { + "id": "0acb09cc-a31d-5f0c-b9bf-d2a4925c3630", + "version": "MTc1NjMyODExOTc2MzQzNDAwMA", + "digest": "sha256:20f4255341b5e2d0e8793a6124e732e11dd0e62a79714fa980f868043fcde1fe" + }, + { + "id": "0acdde1a-8936-5314-aed0-b765be67f165", + "version": "MTc1NjMyODAxNjUwMTY1NDAwMA", + "digest": "sha256:76f4e01496559d77596e01cb8143d9df03114766981eab86dce34005cd1fd330" + }, + { + "id": "0adcd73a-c4ac-5f32-ac25-a96a17e39f9f", + "version": "MTc1NjMyODAwNjE3MTQ4NTAwMA", + "digest": "sha256:04b601d79a5731199504af965160b2e744b5a33632052e33382f862a03bb46bb" + }, + { + "id": "0aee8210-8b10-5d63-bfff-90e86e4d827c", + "version": "MTc1NjMyODE0NzY2NTc3NDAwMA", + "digest": "sha256:5a05df71172ac1773ac62f88684c59b26afcdad7f04cc0b5fe962497b5fd13b4" + }, + { + "id": "0afda498-6391-5184-91d7-0bf2f53d5578", + "version": "MTc1NjMyODAzNjkwNTc5MzAwMA", + "digest": "sha256:bd5c3d3fe2832ac184b862662065a362dce89f256cd332485b1cd1b1bc656388" + }, + { + "id": "0b04aa16-2a3e-527c-bac0-ebde6e89c5d9", + "version": "MTc1NjMyODEwOTI0MjUwNjAwMA", + "digest": "sha256:27fa4a9615400eae014634f7e6db65ba6fcc412e9424c16dd44cbbc123eece02" + }, + { + "id": "0b1c4629-c339-5508-8e1a-c4678499193e", + "version": "MTc1NjMyODAyMjA4MDExMDAwMA", + "digest": "sha256:7eb69a90f9fdb7cc783e0b15eea072436d8a4e203e5a31a16ec5f89732d2f7d6" + }, + { + "id": "0b4ac72a-0a9b-5241-a3b8-e17c5926c7b6", + "version": "MTc1NjMyODAxNjQ5Mzc0NTAwMA", + "digest": "sha256:b2f1089ad7047d4c907d2690a5bddc6df374116be77c761992e2c0395af5a22e" + }, + { + "id": "0b56a590-2214-55ff-beda-c975fbd17f76", + "version": "MTc1NjMyODE1NjQ1OTg4NjAwMA", + "digest": "sha256:05e469ea8755156edac9ebb95ec00b08e2a97db609009460a3c9e1e203b844b3" + }, + { + "id": "0b5d47ec-6d3e-525e-8932-2e914fcdce7e", + "version": "MTc1NjMyODA1NTYzNjgyNTAwMA", + "digest": "sha256:1b2dd66b3456bc6fc8aee5bfb223ca52718db31d1f053bbd6f203716a4f00747" + }, + { + "id": "0b5f47bf-41c5-51e1-9201-c9ed4e5ac576", + "version": "MTc1NjMyODAwNDAzNTE0ODAwMA", + "digest": "sha256:5967e1001d7a51799129cc3eefe8cc97434afcf6fb633f4de95b97aae4961f48" + }, + { + "id": "0b6a7387-9e73-5957-b22b-3300ea3e8920", + "version": "MTc1NjMyODEyMjIwNDE4MTAwMA", + "digest": "sha256:e7d03eb45f0a6c8e49861f1b4d22701322b620158c8fa021534b3886f8740b9f" + }, + { + "id": "0b7318ec-6103-5220-83bd-2c9f1be89b21", + "version": "MTc1NjMyODAyNTkwMzk2ODAwMA", + "digest": "sha256:f97bdf8dd9930f95458ab6fe0b2c707863a255ef124e8bccd7bbf2e5721fe4a8" + }, + { + "id": "0b7e758b-369b-554d-98ac-74517f64b125", + "version": "MTc1NjMyODAxMTU4MTczMzAwMA", + "digest": "sha256:8f6b525ddc77bbef2d797155d6ec7032f8a070488b23da085f2a4aa6a5eaa89c" + }, + { + "id": "0b91f2ae-e3d0-579a-bb91-3aa4b53efe5a", + "version": "MTc1NjMyODE1NTIwNzc0NzAwMA", + "digest": "sha256:5fc9abe430f1eeed2000e32519669c387d77e7e4afe2db5033944c6530a5bc76" + }, + { + "id": "0bab4047-7b59-53c0-9bdd-aaa2a976a566", + "version": "MTc1NjMyODAyNTc2Mzk3MDAwMA", + "digest": "sha256:e24c449aa98eaf396b8aba1674e4e0b418b132a4f78878a775d4ba17c29a1cfa" + }, + { + "id": "0bc38ba7-9cdd-5b6c-8150-f97a1b7007c0", + "version": "MTc1NjMyNzk5NzA4NjMwNTAwMA", + "digest": "sha256:a6c31578c4c349d2f839b017abed93eee479b3a96a9fafc466e4a892950bdb1b" + }, + { + "id": "0be71703-ee5e-5e95-8da5-8cb1a3563096", + "version": "MTc1NjMyNzk5OTMyMDI5NjAwMA", + "digest": "sha256:e3ccf3f23d1113c54503e3eba63bb4da7821103290cfb997b18640b74b3a74a7" + }, + { + "id": "0bea70de-89e0-5dd0-b229-3f97082f610d", + "version": "MTc1NjMyODE2NjM4ODYyNDAwMA", + "digest": "sha256:32001e0598f25f592655b3ed44a885497fcb6ef7198faa1456c861c2d0faee91" + }, + { + "id": "0c02690d-77c3-5e1e-ab63-bc1df4311a64", + "version": "MTc1NjMyODA4Nzc3NzE3NjAwMA", + "digest": "sha256:46931f9e4d19ac242f04fe9e177fa50f59afb335ff03f051902f5302b086a3d5" + }, + { + "id": "0c12db0e-64a0-532b-8ef9-7d07f5a2fcc5", + "version": "MTc1NjMyODA3NDI5OTc0OTAwMA", + "digest": "sha256:0a44895d94b0e75d8c8e995f366fe764e1b5b8ba6604c4b4018d0e89146502bd" + }, + { + "id": "0c496835-4bbb-55ef-9184-e6ae195cf314", + "version": "MTc1NjMyODE2ODg3NDM1MzAwMA", + "digest": "sha256:605d1790732f1c28110d833c2a42e85b6646377a2bfe126fe0c6ce8c181fd801" + }, + { + "id": "0c618711-58fe-5309-bb4f-f5ecddb9ec50", + "version": "MTc1NjMyODAwMzk0NjczOTAwMA", + "digest": "sha256:5b9c53673c44dabdda250595b61cb41cf52c14932fb63b09411c855aef618c20" + }, + { + "id": "0c7db79d-30c4-58be-acb4-5bf93ff128f9", + "version": "MTc1NjMyODAxMTA1MjA5ODAwMA", + "digest": "sha256:d1243d395c386e90761ab211dcba4eca70d7639bd86aa2ff5897c1f45bfd82c9" + }, + { + "id": "0c8f2aa3-2586-5ef4-b918-e9631444afe6", + "version": "MTc1NjMyODAyMjE3NjQ1ODAwMA", + "digest": "sha256:508ba2d34682f576bf168245f6372b56f82939e4f0d4a511cc46c2261fdbb99e" + }, + { + "id": "0c91542f-2872-558d-b097-bb336029c354", + "version": "MTc1NjMyODE0NzUwMDMwNjAwMA", + "digest": "sha256:c48b4ed083e26eaf343894c137f5553327372899b803123a9f30b6b297fb7c21" + }, + { + "id": "0ca4511c-b77a-590e-9b72-a16f8e187fd7", + "version": "MTc1NjMyODAyNTc2NzY4MDAwMA", + "digest": "sha256:c8ef17f4eb8c2c5204f97ece9de5964850f7664dbaf3b2c36afd124a221b9d51" + }, + { + "id": "0cd3edf0-639a-57fb-908b-edd284743ff0", + "version": "MTc1NjMyODAzODg1MzUyMDAwMA", + "digest": "sha256:7a06dcb57d5ff97721d9aceace74cfe72619dda63b6eec638edd5e60a7753392" + }, + { + "id": "0cde0dd4-fede-5039-b081-2bb1b334c0ae", + "version": "MTc1NjMyODAxOTI1OTA0NDAwMA", + "digest": "sha256:aacaaecebbefce54a3883873e75f634ebfd7644b30db0687e364203e9a9aa4c4" + }, + { + "id": "0cdf54cd-a869-5f44-aab3-bf1e3c0f4dd6", + "version": "MTc1NjMyODA2Mjc3MzQ3NjAwMA", + "digest": "sha256:e35b534b0822fd16e607789dab38fc638101c8602e23a7754bc80106e23adfd8" + }, + { + "id": "0ce684ab-4e91-58df-af33-619225335a3b", + "version": "MTc1NjMyNzk4ODY5OTMxNDAwMA", + "digest": "sha256:91df99999fd0fd90cff8b5bd0c781b48e61a7caec5b447886766ee24012f14cc" + }, + { + "id": "0cfaabbb-a2f4-53ab-94f5-c3f67e1af894", + "version": "MTc1NjMyODA1MTMwMjY5ODAwMA", + "digest": "sha256:7d71ef5dc6b5864829174f194370a3b7b7498cbfaa7f59bc3842efbb663bce84" + }, + { + "id": "0d0584d5-9fe3-5281-b541-f3273a9724cf", + "version": "MTc1NjMyODEzODE1MDIxMzAwMA", + "digest": "sha256:0d0f0adc5719afdf99f18c0e4683edf03b8d664953d291801e130bf97de078e6" + }, + { + "id": "0d10b16a-53fa-5048-a083-47ce57f4e4da", + "version": "MTc1NjMyODAzNjg4ODc2NjAwMA", + "digest": "sha256:11ac0375681413a4e965e1b2b3064a936f3269115b5c530c6655b5c8baf5c70a" + }, + { + "id": "0d18f761-b171-5799-b16d-23c48d095324", + "version": "MTc1NjMyODAyNTkyMzI3MjAwMA", + "digest": "sha256:b4557fa0bc1f5674b61f5aa6b814a75e57d228df76d14e6cc034b7c9c39c1021" + }, + { + "id": "0d1bf479-a980-5016-aa46-9ed9bec19b7e", + "version": "MTc1NjMyODAwNDAxMDE4MTAwMA", + "digest": "sha256:bc475cd8e218bccf9bca47fdd94b5a10c08625d1b6512f831689f4c87de5213a" + }, + { + "id": "0d2521c1-1ea0-546f-a9f7-bb740e26a8ef", + "version": "MTc1NjMyODAwNjIwODg3NDAwMA", + "digest": "sha256:99dcb08874301064be3c3fda9da8a06fcfb18b344d184e3f1f88715514759233" + }, + { + "id": "0d2f19b2-1bb0-5f16-a771-953cad24f3c2", + "version": "MTc1NjMyODAxNTIyOTM3MjAwMA", + "digest": "sha256:b84d8c083de54edd685646c43d0ec5a01d21e3b6cf38fd258f99c8173a093721" + }, + { + "id": "0d3f63a5-dd47-50ee-8b7f-4643901c564e", + "version": "MTc1NjMyODExOTE4Mjc4MTAwMA", + "digest": "sha256:a998dcdde6e436f378791422a212c474a21c1fb786b8ee40ca804efa568aeeb8" + }, + { + "id": "0d40b254-7ecf-5bff-87ec-ae81bba8ccd2", + "version": "MTc1NjMyODEzNjA4Nzc1MDAwMA", + "digest": "sha256:0f772b6dc75d67db08406f87dce33e0c4d494fa457a0f0e896837e0c315d1399" + }, + { + "id": "0d703072-97a1-535a-9f82-e1baee22a6a8", + "version": "MTc1NjMyODAyMjE1MTc3MzAwMA", + "digest": "sha256:48273b33963ce325a43cf5e119c291b3097fe0587ed86cd36fe34bb5bcb58fc9" + }, + { + "id": "0d903029-7dd2-53df-995a-87204da956e0", + "version": "MTc1NjMyODAzODY4MzI5OTAwMA", + "digest": "sha256:741f72d51701321a23cabf762cf3d8a59e851b1baeb30d3d5da6df507dd13dab" + }, + { + "id": "0da18d7a-8afd-5dc4-bda8-fc910a2b8658", + "version": "MTc1NjMyODExOTIyNDIxMjAwMA", + "digest": "sha256:ec431f7a697807f40f51bc819e3405092192fd280672f0c5c0a93ac99754b92d" + }, + { + "id": "0dc11727-31b6-535f-aa15-8bf0c5c59cef", + "version": "MTc1NjMyODAxMzc4MzcyNjAwMA", + "digest": "sha256:49b5dc03cfe8d57a54e88c323560918afdeff0914f08585c260d32f8f31cdee2" + }, + { + "id": "0dc5a8f2-dae8-501e-835a-46f9c05fb8fc", + "version": "MTc1NjMyODAxNjU2MDkyMjAwMA", + "digest": "sha256:f549c0e3257e8fb7f4ee007c760476d178f056a56b5ec990c7444c6173134c07" + }, + { + "id": "0dcd548a-e135-5212-84e7-f087aa43e88f", + "version": "MTc1NjMyNzk5OTMzMzQ2NDAwMA", + "digest": "sha256:9dfa8b2d3fc85a688428b3c256711d9d5a4deba95c47329883011b2d73a7ddf3" + }, + { + "id": "0dea33a0-f8d0-5cd7-95d3-f80f2499fcdc", + "version": "MTc1NjMyODAwMzk2MTk3ODAwMA", + "digest": "sha256:73c69aefe030c1e3752304dbb0b35daeeec99188b647e92c0a3fa97e4802f283" + }, + { + "id": "0e02a5e2-c306-5532-9c11-fecc40feaabc", + "version": "MTc1NjMyODA1MTM3MTI1MDAwMA", + "digest": "sha256:9f3e3c71b97d575e373bae316e22a41c4d1a1e5091d3b53ea5e11050f62d72f5" + }, + { + "id": "0e12288a-2902-5825-b33f-7963e269381f", + "version": "MTc1NjMyODAzMDUzNTIyMDAwMA", + "digest": "sha256:ce63a5c7dd350da9adad970a1093172970c2921a8aa520fdc71b8dbf75e2077c" + }, + { + "id": "0e18e9f5-91c4-5c5a-9539-d5144b2afccd", + "version": "MTc1NjMyODA5NzcwODI1MjAwMA", + "digest": "sha256:36ed3faf316b9edb6dd172bd79e9100c60c18f429cffe78e928599c97a3c6979" + }, + { + "id": "0e2a5f41-d5e9-5439-a3e4-4fdede80b514", + "version": "MTc1NjMyODE2ODgwNDE2MDAwMA", + "digest": "sha256:f98c8f69cc0dcb233242d77cd80fcabd07e25ef486d0c08ffd14b9b75602720c" + }, + { + "id": "0e70d9f4-9c6b-5bac-a530-93d0e78975cd", + "version": "MTc1NjMyODA1NTY5OTc5MzAwMA", + "digest": "sha256:d5084b0381f7a6d8ad4a9adff8fdec3e63525b35081e8d9ce9d17d8dfd6be3d8" + }, + { + "id": "0e8d2843-babd-50cf-be3e-0313a8621306", + "version": "MTc1NjMyODAxNTIwMjk3MDAwMA", + "digest": "sha256:691eea7f3e2ae1bfee6019de6b9762684fbc15f252ae7e36e49c845940384746" + }, + { + "id": "0eae2e9d-1ab3-5227-bdf6-5e97612df586", + "version": "MTc1NjMyODEyNjY1MjQzNDAwMA", + "digest": "sha256:10691b5c7a0054975cc0f253db4f5e32493de64fb5d8e090cbce291e35336abf" + }, + { + "id": "0ed09720-224c-5daf-90dd-63a1033d1649", + "version": "MTc1NjMyODAxODc5OTI1MjAwMA", + "digest": "sha256:5fade6e53798c3d5c4dfc5e6a639e36c13e5e17b749f1f291c48e31f0b68f887" + }, + { + "id": "0ed20564-ea11-51ca-bb5a-0b59fd87ffad", + "version": "MTc1NjMyODAxOTQzMTA4MTAwMA", + "digest": "sha256:80ac73042a3dd8567d8339296f9072a44e47292aea0b9df5f419e40fbdacfe49" + }, + { + "id": "0edbc881-4225-5202-9c14-cff654a6001a", + "version": "MTc1NjMyODEzNzY2OTAxMzAwMA", + "digest": "sha256:877de2592da5fb8b5f1f53ef60b63aa45949eadd5ee8ad7a4f9aa22e8f41bbf6" + }, + { + "id": "0f109bb9-b7a0-5a6f-acfd-66cc1c8651e3", + "version": "MTc1NjMyODEzNzY1NzIyMjAwMA", + "digest": "sha256:3c54deff41075f384c4832dcf2c949f06a786ccea7bf45e07286f2f0595ab0e6" + }, + { + "id": "0f163bda-4440-51bf-9cde-bf47ae4fd3e6", + "version": "MTc1NjMyODEwODc3NzQ0NTAwMA", + "digest": "sha256:fc24ae0586b62f97feeaced2b3dfafae3ca5016dc31f441673fc192e8c2ed9f1" + }, + { + "id": "0f16afe0-2a95-57fa-a6d0-c68fe127ec63", + "version": "MTc1NjMyODE3MDQyNTc3MDAwMA", + "digest": "sha256:41c0dc40cc7266ed7b7d5e5403bd2b3a55a570e46acc87cae64917acd68c8188" + }, + { + "id": "0f39a0e9-1771-57fe-b5ef-f866a2399a57", + "version": "MTc1NjMyNzk5NjY5MTM5NzAwMA", + "digest": "sha256:b0dd83a8a7ad4a3092ede5684b5bed26402324e8213e408529ea841990e07bac" + }, + { + "id": "0f4f574b-9894-5dcd-b79c-d6b4f53124b6", + "version": "MTc1NjMyODAxMTU4ODA1NDAwMA", + "digest": "sha256:7586af9b29c6e3156a3e183a03e195475103e60ed203e395f448df6e10a82163" + }, + { + "id": "0f599353-9109-5c80-8486-873f9449a23a", + "version": "MTc1NjMyODA5NDMyNjE5MDAwMA", + "digest": "sha256:ea4d48361897a10eb2ed38bbbb2e28cb4b52f23bcce7dcb37d924efe742c79db" + }, + { + "id": "0f5d674d-03ba-5e13-83b6-b4bc9c665e14", + "version": "MTc1NjMyODA1NDEyNTY1MDAwMA", + "digest": "sha256:c50edd5a616c2a0a459e25abd88cd5c01f1f17e4bf41cdcdcb922497e8e8b046" + }, + { + "id": "0f5f69c2-bfab-5003-bd0c-85c898524892", + "version": "MTc1NjMyODA5Nzc0OTA2NTAwMA", + "digest": "sha256:428504b35a9e2800d47a3a3c02b7e294c2b8552549fbe0137474382822b083f2" + }, + { + "id": "0f5f73b3-6ee5-599f-aa99-ad3003a3b9de", + "version": "MTc1NjMyODExOTgwOTEwNDAwMA", + "digest": "sha256:256a0aafa9e8e13feb42d1f466d5389a8317791f9bef4a0658b591d7c8f231d1" + }, + { + "id": "0f63f801-9fe5-5aa6-8801-7db19b18d0da", + "version": "MTc1NjMyODA1NTgwMTQ2MDAwMA", + "digest": "sha256:84db9e057313fd9d735d59d5765ca6218aaeae96dcd2900e0b4acba28209d325" + }, + { + "id": "0f68e676-1b1b-5095-919d-6e1cc1cfbddc", + "version": "MTc1NjMyODEwOTI3MzY2MTAwMA", + "digest": "sha256:b94a91fa9995c4e78e5cd4cfe1189c19a16200089310a8cc4de63cba178d2e13" + }, + { + "id": "0f7086d9-0110-5e65-a4d2-1c3880226b2b", + "version": "MTc1NjMyODEwNTMzNTA3NTAwMA", + "digest": "sha256:694f72b929ec5652cf186ffa379376ece0e040a8ac33da5ef3cd895b11900e4c" + }, + { + "id": "0fedea61-8897-5ff3-91ff-21f98278964a", + "version": "MTc1NjMyODAyNzgxNTU0MDAwMA", + "digest": "sha256:c2db1a7aeebe34ad969f92bd23b65e1c491cbf8ef1c0ecc3f4f7c3645bd7e242" + }, + { + "id": "0ff5dc1b-b398-58a8-ac66-c776e98688fc", + "version": "MTc1NjMyODAxMTA5MzU2MzAwMA", + "digest": "sha256:6852f0f4aceb159e1db2de96af459870dc42f08f84962a4678f0375f12cf5f5b" + }, + { + "id": "10004d80-eab5-554a-8398-6f492a56f2c8", + "version": "MTc1NjMyODEwNTQwNzY0NTAwMA", + "digest": "sha256:473ee4da12e56fb960f8c6b4891c39af54de9f5821cf2a97ce32d32dbf05a889" + }, + { + "id": "10015ac4-3aac-53a4-a0ee-01af1f36032c", + "version": "MTc1NjMyODE3MDg4NzkwOTAwMA", + "digest": "sha256:2bbb1558122b4a5e7003bfeeaac6ec4c2e20620e8c3a1687f0c5b8700b5136ba" + }, + { + "id": "1004e05c-34d3-5d39-8f83-f2b8d8e63a86", + "version": "MTc1NjMyODA1MjUwOTU3ODAwMA", + "digest": "sha256:90f62af11a38095d708024df1a4c82ef047c78b157287a221b43bcd729d45356" + }, + { + "id": "1028c00d-7bf8-5806-bfdd-33f2ec076685", + "version": "MTc1NjMyODAyNjgzNTA1MzAwMA", + "digest": "sha256:06297a8601e09a1977574b788ab57971a5b59d2eea17b96bfbf997709c24f9aa" + }, + { + "id": "105d0899-93bd-5d7f-a8b4-1e87b2f6dead", + "version": "MTc1NjMyODEyMjI0OTIwODAwMA", + "digest": "sha256:58ef2fdb0aae7cd1dd7c3daf6cdf9e71cf798b8b834d03eda5de0de389315350" + }, + { + "id": "1097e8be-0db3-5a8e-80ed-10398c5d877f", + "version": "MTc1NjMyODEzNzAxNjM1MjAwMA", + "digest": "sha256:bc6adb227703c3c5144f887b7a57f50745a4602fcfb13d70e26eaa4b43904f6d" + }, + { + "id": "10ca087b-2768-5434-ba5a-5408b35c53a1", + "version": "MTc1NjMyODE2NjQzNjEwNzAwMA", + "digest": "sha256:5c79b53d9ac115af8cc8a211f36eff8024cd7180650fea70f519dc5876d1f2c5" + }, + { + "id": "10ca3051-6cd3-5928-ae23-ceca75743435", + "version": "MTc1NjMyODAyNTc2NzU1NTAwMA", + "digest": "sha256:b1138d5a4ba5c1e46f4d258fb237a6a163ceca29a4f951730f27148f571e41c7" + }, + { + "id": "10cca0a6-65e4-5783-83d0-e902fa1dbcc7", + "version": "MTc1NjMyODAyNTYxNzIxMjAwMA", + "digest": "sha256:cdce51583ff2df47f72d562886621cad9c3ef50bfaa50fcc52add5beac346062" + }, + { + "id": "10d9820e-712b-504c-9f0b-92bae56b4343", + "version": "MTc1NjMyODA3MzAxMjYzNjAwMA", + "digest": "sha256:3eb1fd0c1693246fb9f2261b5ab9588135b2251bb8afbdf7700614222e538a68" + }, + { + "id": "10e8d6b8-bc3c-5bf1-b450-35d0eb8b2177", + "version": "MTc1NjMyODEwMTE0NDA5MzAwMA", + "digest": "sha256:787a62a87c6094c68c2aa5c42e01718212247d87c75b8f55a4cc404a201718c4" + }, + { + "id": "110a7f0f-9002-5b5a-9d3b-837ceafef418", + "version": "MTc1NjMyNzk4ODY2NjA2NDAwMA", + "digest": "sha256:61d611a14b4068e5a85be7c2164250ec945d6fcabdcdf83d8c6ff0167ffaef37" + }, + { + "id": "111b6bcc-c561-5604-b58d-170774cad506", + "version": "MTc1NjMyODAxOTI4MDEwOTAwMA", + "digest": "sha256:5c94e163d9ff762b5c35ce3edea896d716ce2d42656acfa1766366b566548885" + }, + { + "id": "1129ca59-0f46-5bef-9931-4927eb1f6983", + "version": "MTc1NjMyNzk5NTMyNjQ3MzAwMA", + "digest": "sha256:d810e1bba380b978969a3bab2f34cdf6ba1ec9289e6a36195e0adb7257ab7d4d" + }, + { + "id": "112b073d-5e4b-5ebd-9cd2-44b7fbd31bdc", + "version": "MTc1NjMyODE2MDUwMjkwMDAwMA", + "digest": "sha256:58ed9801e4573b379f2563cf160fa43acd035807346b108c5ea78b98c229b756" + }, + { + "id": "113714c3-0e15-5961-a2f1-d7fbfcd09143", + "version": "MTc1NjMyODAxNjU1Nzg4OTAwMA", + "digest": "sha256:b4229b2b1fd82a37f500969f771f48ef9b0bc6241fad648ee7eabfa53fbd37ea" + }, + { + "id": "1137b434-973c-5ad9-9850-fe3eb7cbc8f8", + "version": "MTc1NjMyODA5Nzc5MTA1MDAwMA", + "digest": "sha256:20cb257e305f27fcf10a323482acbdcfc7c0051e21cffd04536b4bfbc2f2fd37" + }, + { + "id": "1154d6d9-b4bc-5aae-8690-8d981648b09e", + "version": "MTc1NjMyODE0NzgxMDU2MjAwMA", + "digest": "sha256:38f809cb0f939ac52e03a7a049e0a97d5582354aab740d4d0744c28e8307f1b6" + }, + { + "id": "1155b906-dd5f-5b3b-aa5a-0d6cea80b5f6", + "version": "MTc1NjMyODEyNjUxNjExODAwMA", + "digest": "sha256:3b8fd71fd10aaa0befd94283af002d46f49c698af2ebf390ffa277d2a41cea47" + }, + { + "id": "116013f6-92f3-570d-a873-8380633169a3", + "version": "MTc1NjMyODA4ODUyMDA4MDAwMA", + "digest": "sha256:24d16ff761b45c958c1d03c26514014efef7f830b2b8ae971883542896063675" + }, + { + "id": "117091ab-56ea-5b58-91e6-b74ec82433bf", + "version": "MTc1NjMyODE1NjM2ODI3NzAwMA", + "digest": "sha256:c873bb139aad5652b8e2ca1283741ee7a8d1986f47a71245ee9a8825391d2b26" + }, + { + "id": "117cd9c3-b2b5-548c-93f0-37fec171b2bd", + "version": "MTc1NjMyODExOTIwMzI5ODAwMA", + "digest": "sha256:f99978ad662b4e69fd1a80d8a77c6ab7a59ae77b2ae28f61103190ff80f0ee6a" + }, + { + "id": "1184deb6-e21d-54e9-b25b-5f0870ab4c00", + "version": "MTc1NjMyODA2NzE1NzI1ODAwMA", + "digest": "sha256:3b07df75dfe315fe952aa3d773649fbec8b468a37f9ff14561e96e8203930136" + }, + { + "id": "11922ac1-75ad-5dfa-9eb9-eb37d7643fe9", + "version": "MTc1NjMyODAyNTc2NDgwNDAwMA", + "digest": "sha256:8f80ba79c89d0a367b6bcb40e2a7f5f30345043be5f8fb23ff8ad55027a60214" + }, + { + "id": "11b75f5b-bfa8-5f1b-84ba-c63ac0612070", + "version": "MTc1NjMyODAyMzM5MDA4MzAwMA", + "digest": "sha256:e026eef0993415b3f14a963c073810e6e60a784622a655595898973c344b8430" + }, + { + "id": "11e0bdd5-3ad7-5f1a-8217-29b5cfcb9d91", + "version": "MTc1NjMyNzk5NTMxNTAzNTAwMA", + "digest": "sha256:20f68a4b73834b4788db35f5fd819e10c3dd6202e1ddbc87d05bccdbfee6fb02" + }, + { + "id": "11e8754f-5ffa-5ca7-9fb4-1a2ebf760d28", + "version": "MTc1NjMyODEyNDUwNjE4OTAwMA", + "digest": "sha256:c6ec3d2c38f4f395fef823d444737370febbe61209bb1500842b9b4cc860d536" + }, + { + "id": "120069da-3dbf-5dcc-9227-b2f6cc491a79", + "version": "MTc1NjMyODAzODgyNzU0NzAwMA", + "digest": "sha256:7b0cd8c5ddb65a4826d162fd4437b3356b172bbe235477115b19fd49b9558bb6" + }, + { + "id": "12072b3e-0dd5-5d28-9358-c5aefe177876", + "version": "MTc1NjMyODAxNTUxOTg5MTAwMA", + "digest": "sha256:8d23805b79688162fe5827d0ae3585366202b4e93c5d8eb53ed4a747854481dc" + }, + { + "id": "121e2aa1-55db-5c81-9291-56c51f1a34c2", + "version": "MTc1NjMyODE1OTE5OTY3ODAwMA", + "digest": "sha256:5c474b3adf9bdcf6aa51d099d35b8409f38ea138ee417991658fcf4e61118be5" + }, + { + "id": "122dc295-bafe-50dd-bb86-85b78dcc8df9", + "version": "MTc1NjMyODEwNTQxNjAxNzAwMA", + "digest": "sha256:6935a01d807a6c4fedb14b024ff14f1635d1f799016c3c9d398cf2e9f53f6db6" + }, + { + "id": "123b4360-860a-5205-9562-b75952fd51d2", + "version": "MTc1NjMyODAyNzQ3NDg2MjAwMA", + "digest": "sha256:648754769c36ad541cd0ad65e6280c9ab2426f60aab6d2222c251ad7ceaa0018" + }, + { + "id": "124a4077-1cb4-5103-a98c-4150f79c2725", + "version": "MTc1NjMyODEzOTIyNjg0MDAwMA", + "digest": "sha256:6ca33eb5ab8544e9ff435c1b0b21b1c49559127e6bea0210f615b06b14594781" + }, + { + "id": "124f7e0f-982d-57e1-9b97-5e43912f86ea", + "version": "MTc1NjMyODEwMTAxNjQ5MDAwMA", + "digest": "sha256:4ef67dc5dcf6327eeb5976208531b17ea3fd09b7d25abfc59149792a7ca77637" + }, + { + "id": "1253bea8-b816-54b7-9edd-cab636447c73", + "version": "MTc1NjMyODA5NDM4MjM2MTAwMA", + "digest": "sha256:bbd4413eac8fe1415a87f564a2784700fadf235c5951bef88faf00e14b6bce64" + }, + { + "id": "1255d61e-88a1-5096-99a2-92c8dfb140f9", + "version": "MTc1NjMyODAyNjgzOTYzNTAwMA", + "digest": "sha256:458687d2450692ed8ffd899a383f08649cbe537be8f0d5812a454e3640d40ec1" + }, + { + "id": "12756901-7eda-5a71-91a5-1b5cf89b5e1b", + "version": "MTc1NjMyNzk5OTM2MjQyNzAwMA", + "digest": "sha256:26d5309f090bf16be86eab78c08614ee86e0c30de90636581f0ad3dbca898edb" + }, + { + "id": "128224b5-02d0-51e0-87f6-1075e94aff52", + "version": "MTc1NjMyODAxMTEwNTMwMjAwMA", + "digest": "sha256:a53f8609912d46ceab8cd1627dec632233a8ebb55b8ab924f3ee3dc91b29facf" + }, + { + "id": "12b12b59-2866-54de-a47a-6394ada37fdc", + "version": "MTc1NjMyODA2NzA2MTQ5MTAwMA", + "digest": "sha256:7afe8539a7b46d568396ed46a0c96a0d8155cac6b6734170d3fafa5e69708525" + }, + { + "id": "12d0fac2-e7b5-5ad3-91f8-b6a0bd553f69", + "version": "MTc1NjMyODAyNTg5Mzc2NDAwMA", + "digest": "sha256:11c8b8815a7b905f0629caaec01f4113926f240b499b14c0368bcdc589da429c" + }, + { + "id": "12ddae53-ce4c-5ea7-a731-a8806d1858b2", + "version": "MTc1NjMyODA1NTczODg0MDAwMA", + "digest": "sha256:7db2b18d7eaa02262e7786326ca9eeffb4bf5bc82adb63bf7d31a32ddc6aa688" + }, + { + "id": "12efd75f-4bde-523b-a81a-1bf2b769ff81", + "version": "MTc1NjMyODExOTE0NDExMDAwMA", + "digest": "sha256:90ba8f46b6bf0519a8a0e38c5f63b1d9bb5f296b40452315a5066a3fac71590c" + }, + { + "id": "12f1dc2d-f870-5760-a3b3-0446e8836c30", + "version": "MTc1NjMyODAxNjU4MTQ2NDAwMA", + "digest": "sha256:917aa021896ea9cbc12710a8775037db57aa8896a4cadb365ab999590ced90f4" + }, + { + "id": "12fdb44d-2843-5798-8548-bbedd7d5312c", + "version": "MTc1NjMyODA3NDIyOTIzNDAwMA", + "digest": "sha256:b46d1c753f241f0105e67048f1ca4876151b8849fa8e20b2ef3d3162d706e500" + }, + { + "id": "13125043-8510-5f84-96a6-4468f64dda4e", + "version": "MTc1NjMyODAyNzg2MjI4MTAwMA", + "digest": "sha256:5c6dfdf609e37b13c30e79741b3b32c34e02c1ddab4da160d3e6e5c0b2168c52" + }, + { + "id": "131cb2ee-badb-5ab7-a22c-ae46b365a5e8", + "version": "MTc1NjMyODA1MjYxNTA2NjAwMA", + "digest": "sha256:fc0d1d26db081e0cefdbccdc0570fb2ec8b134921d6a219d48146e817ac35e7d" + }, + { + "id": "13464517-cded-5438-b792-36642fb6cb34", + "version": "MTc1NjMyODE3NDk4NDA0MDAwMA", + "digest": "sha256:9ad3fdea9099762ea74b09c04561800ce7e4820570508f552047fbd2cbc1caf6" + }, + { + "id": "13479376-2592-5c21-b6a3-dc418b821230", + "version": "MTc1NjMyODE3MDQ3NTYyODAwMA", + "digest": "sha256:9b2e8d352d290d347219ff4d360a4b8ecf5a795382a5554e9af8cfa279c4d319" + }, + { + "id": "138b0fe3-13d4-580e-bac5-affe8133553f", + "version": "MTc1NjMyODAzODY5NDUzNTAwMA", + "digest": "sha256:42f96dfbfed17a5e4196f5290a2da24dda7eede94b32aa52bed1972af80d61e7" + }, + { + "id": "138d7304-7c47-5391-a76c-64f90b7200c1", + "version": "MTc1NjMyODA5NzAzNDM1MjAwMA", + "digest": "sha256:df66f565f621acab1760b28aaa93182b0411c69578bed77e6debddbd918686b3" + }, + { + "id": "13990c1b-ee42-57b0-b4ac-483beab1c404", + "version": "MTc1NjMyODAxMDQ2NDk3NjAwMA", + "digest": "sha256:e9386810daf816487c62625dace9dae7558a256ede9f543f80b8fe2db7c2190b" + }, + { + "id": "139cfee3-bfac-5058-88d9-fb697f24e32a", + "version": "MTc1NjMyNzk5NjY4NTQxMDAwMA", + "digest": "sha256:6c8ef1cb7095bc189f11dab3e84b176909a4ca150229f0353976e5b4b34ad419" + }, + { + "id": "13a469bf-a490-5510-b58d-2ef39a3a9dd8", + "version": "MTc1NjMyODAyNTYzODUxNDAwMA", + "digest": "sha256:8381fe24d73cd48738a56c7fb8dd2db83adf2fe51958304ffef540ff374583f7" + }, + { + "id": "13b25918-f6f2-52d4-bc00-ef100869e0b9", + "version": "MTc1NjMyNzk5OTI3MDg2ODAwMA", + "digest": "sha256:942ad24c83f8089dc9a432841394c352a8206d09509a42cd58cbcf2d1402add3" + }, + { + "id": "1413a650-76c6-5896-a3d7-12eadb094795", + "version": "MTc1NjMyODA3NDMxMDQ1MzAwMA", + "digest": "sha256:30c15b76d616c1f356a2c191f19ba9b7084f3d4f576ac72b66c25df41ed9a0a0" + }, + { + "id": "1420bbed-2a38-5f52-9c2a-b9bca02196ab", + "version": "MTc1NjMyODAxODgxMzIyODAwMA", + "digest": "sha256:a264c9b630b887e8e170e843f6ea5195dfa536d1dc861b958dc541a6f2f3d151" + }, + { + "id": "142dc1a8-3f34-5598-be69-4b2ae583edd1", + "version": "MTc1NjMyODAyNzQ1OTg4MjAwMA", + "digest": "sha256:97513b7be76eff228737ee2e713b5f5207fd8c29bbfdd9adc1e8797486f125dc" + }, + { + "id": "145056dc-d102-5108-bb72-e1485acb159e", + "version": "MTc1NjMyODA3MjU2NjUxMzAwMA", + "digest": "sha256:9403b980505c48fdbc3ff3faa67df7d73e592790670460398ab2ffe8cd8becc0" + }, + { + "id": "14511a8b-9bae-5356-91b5-27303c90bb1c", + "version": "MTc1NjMyODA2Njk4MDc3ODAwMA", + "digest": "sha256:271c7e021cf2b0eb51bec713e8b99a6d5d6e6c80bbd5663f45d119b789b9ffa0" + }, + { + "id": "147a3aca-d692-5fa8-9522-31223102c6f7", + "version": "MTc1NjMyODA1NTgzMDA1OTAwMA", + "digest": "sha256:021b4d5e9d397621f1ddb08c9b9b1c4474b13f57f02479dd086328859858003a" + }, + { + "id": "148c4175-adb3-5560-a346-605cf447e1ad", + "version": "MTc1NjMyODExMzU0MjU4ODAwMA", + "digest": "sha256:3a7ae53eaa8d36bfce881bd5d24880f62bc1ca5d0857f6a1b1c77c072fa6de03" + }, + { + "id": "14965813-67b9-55a2-b9b3-28ad3b1735fa", + "version": "MTc1NjMyODA1MTI0OTQ1NjAwMA", + "digest": "sha256:961569172666cff0901f15e75243c31b6baf1aa40ffcbbf0ed2937dac4cfb520" + }, + { + "id": "14cf1ea8-d31b-5ee8-9b97-3dfbdc7e9564", + "version": "MTc1NjMyNzk5MDM5NTE1NzAwMA", + "digest": "sha256:95d10cc2e6976438f945d4e418d0154e99b29a86e57d09a5eeee44d72104d494" + }, + { + "id": "14d317e1-25f9-55d2-bc75-ec3367e0e077", + "version": "MTc1NjMyODA2NjkyMDE0NTAwMA", + "digest": "sha256:840eb84b132f3e6557c3b3c2881f929cf2b7bdae7e4ed27191649d759ef3e96b" + }, + { + "id": "14dc6c0f-10d0-5257-bd14-51ae9ea22341", + "version": "MTc1NjMyODA3Mjk4MjEzNjAwMA", + "digest": "sha256:9aa2353115449b8a64e30cbe5b0eeb68cd944188829dc5d7757ae46877712d26" + }, + { + "id": "14f77a10-0195-5ff9-9a17-27ae484a2132", + "version": "MTc1NjMyODAzNjg5OTA4NDAwMA", + "digest": "sha256:dbf406b9905b2666d128ad6915e5b22f35a987185b9abc9ccbc624b62c467de3" + }, + { + "id": "1535b999-2088-507f-a563-277e5c43d4de", + "version": "MTc1NjMyODAxMDQ0NzQwNDAwMA", + "digest": "sha256:6069daf429bc5f2df2ebeea29e7a8f9870952199900dfa8386e4db2986dfbb8e" + }, + { + "id": "154d8cf0-1c60-5514-8df0-25f4cc0ae292", + "version": "MTc1NjMyODA0MjQ3OTcwMzAwMA", + "digest": "sha256:fa0832d644ffa477dd889378dabdd726adcc92ab10d65e37b7c527952cf55fb7" + }, + { + "id": "155461f9-29ba-5b8b-9ad7-67e59a7c6a18", + "version": "MTc1NjMyODEyNDQyMTQ5MzAwMA", + "digest": "sha256:64a6374a342df03326867b89976cd9c2c5580a1a66879e958cf709b10233844a" + }, + { + "id": "1554ac1f-adc9-5ace-9c75-58f737c1fed2", + "version": "MTc1NjMyODAzODc3MzU4ODAwMA", + "digest": "sha256:8154f272184b4865418813c13f220ec24f500d8581b903fd3ffc632266542bb3" + }, + { + "id": "155adb55-2983-5aff-8928-0191192aa5ff", + "version": "MTc1NjMyODA2Njk1MjQ2MTAwMA", + "digest": "sha256:fc1b2181aa7e1bea8fcb69b84771c85ce7f4d1f5b7f7cdd44dc5052f24a8f546" + }, + { + "id": "156fc646-d47a-5c21-b99b-e53b226d51a2", + "version": "MTc1NjMyODAxOTMxMDM3NTAwMA", + "digest": "sha256:4006f8039ac17402a30ef5149b8384431692b0749e9bab22361d0fd7d8afef95" + }, + { + "id": "15772997-87f9-5702-a419-bcdb77affcfb", + "version": "MTc1NjMyODA3MzIwNDk5MDAwMA", + "digest": "sha256:eec0ab56a6272c26b99dec1731ae16d297669c656e766151d77e4ebcd88ae2b3" + }, + { + "id": "158c8d73-dd83-5692-8e28-97fec0df22a9", + "version": "MTc1NjMyNzk5OTM1ODkyMjAwMA", + "digest": "sha256:ff316d8a81b05c4bc6c687a4e0d3c0c434d91441c14f9448a5abceabe3d282f7" + }, + { + "id": "15f0ebfa-c777-58b1-b873-8f96b60e818e", + "version": "MTc1NjMyODA5NzY0NTc1MDAwMA", + "digest": "sha256:ccf3ccaac0b3233000addae37c76ef3a1ce0c1fb12b0e22b5b730749b1c0cf1c" + }, + { + "id": "16599b1d-eded-5726-8305-3d44c50b6be4", + "version": "MTc1NjMyODAxNTQ1OTk2MjAwMA", + "digest": "sha256:b01fb4669ff7ef62d9490b2b309268176342b915d6919986529f6e82eb6d3720" + }, + { + "id": "166bada9-e2ce-51c6-8565-b3e95ff8ff74", + "version": "MTc1NjMyODE3NTAwNDcxNDAwMA", + "digest": "sha256:379cf4f11dfee9f6c60d999a1a9277dd212822e2105de3c78f7fdb93be85f6f9" + }, + { + "id": "16786aff-43fc-5b3a-b20f-14025457a5b8", + "version": "MTc1NjMyNzk5Njc1MzY1NTAwMA", + "digest": "sha256:71526cc414d8def7030f351919361a6c863f83ffbf5d41a66025524077925ad5" + }, + { + "id": "16a2bc80-52f8-5ee5-89e0-385be211bc63", + "version": "MTc1NjMyODA3MzIwNDIzMzAwMA", + "digest": "sha256:56d7cbecd0ed5ad844bfa3fa0a17eee9d447019937e00baba132df4846e60bb5" + }, + { + "id": "16a6d03a-f333-52e8-adc5-d0185b47b5ef", + "version": "MTc1NjMyODAxNTI3ODg0ODAwMA", + "digest": "sha256:b3352b78c0a096b73d078b98015d23097892d6d683584c320fd230c9f2143413" + }, + { + "id": "16be128a-8afa-5ae8-aa00-186527e66d95", + "version": "MTc1NjMyODE1NDU0MTE4NjAwMA", + "digest": "sha256:33121f1c2ca70b2ca552acd2640776b9a3d3a0d0ba37d36817d53c08b4bd1434" + }, + { + "id": "16c3207e-b0bf-5945-ae28-71b9951e8649", + "version": "MTc1NjMyODAzMDgwMTY1MzAwMA", + "digest": "sha256:229db5148aa5c0c55ef81e008c81157aeb0f8446ad5af4b599899d2de8ba5f23" + }, + { + "id": "16c808c2-6539-5427-80d2-96a4141cb8f4", + "version": "MTc1NjMyODA5NDMzMTE2OTAwMA", + "digest": "sha256:dafc8e4726776ea14796b574787550a244568b09ae2b8c50838e39f5b0bf83b8" + }, + { + "id": "16ca5b6e-b9db-5978-b1ea-31d937bf447c", + "version": "MTc1NjMyODAxMTA5MDEyMTAwMA", + "digest": "sha256:ed98e213e07bc4094ae989d435602db9b197971ad86523a6050ec38619d29e53" + }, + { + "id": "16df0078-d0a7-53f5-a136-30be6afd54f4", + "version": "MTc1NjMyODAyMjExODgyMzAwMA", + "digest": "sha256:6d4df54ae458ee132ee8ccb519eb90cbcfd5dd85b3c394f241fe5aa142046549" + }, + { + "id": "16df0939-743f-5dd6-8ac3-65d89d412ec0", + "version": "MTc1NjMyODA5NDM0NTcwMjAwMA", + "digest": "sha256:ef50306a91665df9af0d953b1683dc2bbfd5f8743b9fbd6bfe030e81d25e0b80" + }, + { + "id": "16ecea18-e99b-5883-b704-e2ab747cd5c5", + "version": "MTc1NjMyODE1NjUyOTQ1MjAwMA", + "digest": "sha256:b6bfce8bc97e96cfd84cebeca552042a14a7c6c759a005113fc79fe6afb8392b" + }, + { + "id": "16f4f6c6-4fb0-5165-b531-430aa9525151", + "version": "MTc1NjMyODA2MjcyNTQzNjAwMA", + "digest": "sha256:ade10ed7d79a692f93bbf0516c1d05e5b45da85337b240dc2fe782b0d29bfc3b" + }, + { + "id": "16f6a16e-34af-570f-ac4a-2edfea0db897", + "version": "MTc1NjMyODAxMTU3OTIzMzAwMA", + "digest": "sha256:45f83179cd83db2141a8a11da7a2c9cfe1df349202b378c8de39760a85a1773c" + }, + { + "id": "16f97d14-877f-5de2-98aa-bd5f7fbe9e59", + "version": "MTc1NjMyODAwNTA4NTIyMTAwMA", + "digest": "sha256:0555560eff9b53cad94ba734c2c0bda1ea44206409ab21ff822be071026d079b" + }, + { + "id": "17073bd2-4ac2-51c8-9abf-f35ed5aaa73f", + "version": "MTc1NjMyODA2MjEzMTIzNDAwMA", + "digest": "sha256:77b0853239e8e26ed15057c8f9790cb9563926f0bffbff41f91954a2e7ea496a" + }, + { + "id": "1712256b-de5e-5a7c-938a-f91bca4d59bf", + "version": "MTc1NjMyODAzODg3MDA2NjAwMA", + "digest": "sha256:89a4cfe3b092a855b5cef1fc4b0e1fd42389282f61a06a53583a4d8e31c8c073" + }, + { + "id": "171293e4-47a2-5c9f-96ee-be5d50242358", + "version": "MTc1NjMyODAwNjIzMzA1NjAwMA", + "digest": "sha256:212af715e54fc9f3f7e61f4bb71e7934489965fd18ef67735ee725086ccd01a1" + }, + { + "id": "171c3b49-3082-5a3d-8f91-b4f451a9bbc2", + "version": "MTc1NjMyODE3NDgxNzQ2NjAwMA", + "digest": "sha256:ecb2bdeba9cc362aaaa31e0980fe5e57e289e90f21e6673e594474f4c39b087d" + }, + { + "id": "174d74bb-0871-5d2e-b503-388f3f4e6912", + "version": "MTc1NjMyNzk5NTI3MjIzMzAwMA", + "digest": "sha256:737b3c6062c735da2f1e3e3f6fdc15966c724e7432419b9fa3e338c6de175189" + }, + { + "id": "174e992c-8222-549d-a597-9a6689d27536", + "version": "MTc1NjMyODEzNzEwNzY0NTAwMA", + "digest": "sha256:42b98a1cab9d291942a5c550eaafcfb1bafafa923c7ec12b7e6c0d0ea51c97cb" + }, + { + "id": "179429c9-bb2e-5545-b235-aa17fa1f3978", + "version": "MTc1NjMyNzk5Njc2OTMwNzAwMA", + "digest": "sha256:a834b3ab023186f528b280ef90cac72a847796e54b082ab50b86d87c5f6bbc6f" + }, + { + "id": "17948274-9622-5244-86d4-e7c48e820259", + "version": "MTc1NjMyODE2MzI5MDM4ODAwMA", + "digest": "sha256:8f0c1a2703bc16198c6d2c5e3aab33cdbbab5cf412848341c12544f16fded771" + }, + { + "id": "17a3d700-6f42-5d0a-9f3e-39e271cb3081", + "version": "MTc1NjMyODEzODEyMzk1MzAwMA", + "digest": "sha256:8dc58072e812bb68964b05ce8e3e0c0d1cf85c0678d6d41994e0aa7d2215dbe5" + }, + { + "id": "17b1d25e-e080-56e8-b235-d35466d19a83", + "version": "MTc1NjMyODExMzU2ODcwMDAwMA", + "digest": "sha256:e64d75674451ea969ec1857ef477c38d80b7f4ddaaa27354346ef92dd8a15370" + }, + { + "id": "17c36aa0-5774-53ff-bb56-0cbbd47e3f12", + "version": "MTc1NjMyODEzNjIxOTM0NjAwMA", + "digest": "sha256:4fdedb31e6f05e21a26b1aaf744c34ee9db4c469528843495b64f27ed82c4753" + }, + { + "id": "17c5b063-4bed-56dd-8289-f520bfa90c04", + "version": "MTc1NjMyNzk5NzA5OTA3MzAwMA", + "digest": "sha256:68380365c9c89b659bb82b15670f265d89182039a922e75ea3d9becc3b69849c" + }, + { + "id": "182fc6b0-b558-58f1-ab77-bee821e1a98f", + "version": "MTc1NjMyNzk5MDQwNjQ0NjAwMA", + "digest": "sha256:dd59f9f54193880b1214c005dbbc059e80ea36b8b5a2f0dbcbf79680bc8e2019" + }, + { + "id": "1846d656-b0ad-55f4-a0eb-382aa2908d29", + "version": "MTc1NjMyODAyNzQxNjM1NjAwMA", + "digest": "sha256:3e42568c5a897fe21df6c45cf6cc6d080231349003ae60aab00b49534af2e244" + }, + { + "id": "186a4ec5-23d9-5805-97a4-5b9e471acc12", + "version": "MTc1NjMyODEzOTI0ODg5MzAwMA", + "digest": "sha256:2b3189f38c7a7f70804a38eb6e60afd006e84680807fe45b3da102c1d689ee92" + }, + { + "id": "1878db9e-e7a1-5c7e-8f90-3d2075758ade", + "version": "MTc1NjMyODA5Njk5MzAxODAwMA", + "digest": "sha256:3d55d7dc574c424044084f021a32a05e0b8c308ace18fb615c0c462b768fdc64" + }, + { + "id": "18bd0768-0235-565e-8c29-7fdf69008293", + "version": "MTc1NjMyODEyMDM3ODQ5NDAwMA", + "digest": "sha256:85d724af469e0622d017b9bcfdc3d561655acb186b1846ac5739a4decccd4012" + }, + { + "id": "18cb0032-839e-5721-a970-677c2711aa37", + "version": "MTc1NjMyODAyNzc1MzY0MDAwMA", + "digest": "sha256:0eec4f58e85688adcfc9177073a6aff6d930bddb7a798d37abe417ecb5b17935" + }, + { + "id": "18d3001d-c3f8-5059-96cf-cf5a59466a17", + "version": "MTc1NjMyNzk5ODEzODk3NjAwMA", + "digest": "sha256:036f0d26a5a6fc4e7fb99fa8bd10f6845089e6ef1b85f4868262bdba7c743d87" + }, + { + "id": "18e124b4-d490-51aa-88b9-ef9d482ba10c", + "version": "MTc1NjMyODAyNzM5OTQzMDAwMA", + "digest": "sha256:b176baeb0de431e051c163460a3bff85a14a9be37f6be4bd9fafd5bd225987bf" + }, + { + "id": "18e60dae-896a-5861-b1f0-ddacfd2c0299", + "version": "MTc1NjMyODE3NTAwNjgyODAwMA", + "digest": "sha256:676f44c066be1c7ba073193439ec2e2d4015898da55d6ad920971e585449179d" + }, + { + "id": "1903722a-719a-5ee6-8ca3-9382abf6ea92", + "version": "MTc1NjMyODAwNDA1Njc3MjAwMA", + "digest": "sha256:85995485a529c47305834528b8957d220242c7ff0a94d67d01d67ddce2f7ff6c" + }, + { + "id": "190e594b-a15e-56e2-891a-4dc780b3b4e3", + "version": "MTc1NjMyODA1MTMyODc0OTAwMA", + "digest": "sha256:0b4613a3f67f612b42489516a99245313a6b20cb99253fd4bf18a1ad27cd7f07" + }, + { + "id": "192244f1-17d3-5ea2-a9e7-3ff2871175ac", + "version": "MTc1NjMyODAxMDQ3OTc5NzAwMA", + "digest": "sha256:61438178e4de50adbb8a089e04303fe959c38b81c53c1cfe04f9c244b074fee7" + }, + { + "id": "19294700-f962-5694-9a3d-307c6a313ee3", + "version": "MTc1NjMyODEzNzQ4NzgzODAwMA", + "digest": "sha256:361033c086334c271ebccd745f34f5c4850d206b21225a03773fff78b3a9d46f" + }, + { + "id": "192e4416-6d7c-592b-a751-c30c63ba92ae", + "version": "MTc1NjMyODExOTc2MDU4NTAwMA", + "digest": "sha256:7de326a462353884e6aad801751544d2d82e2c6fdb92f71c077b26bd2d5fc74e" + }, + { + "id": "196514df-a631-510c-95c4-c245f88a3b2e", + "version": "MTc1NjMyODE1NDY2Njc0MzAwMA", + "digest": "sha256:fb634a83b762a7c2f3bb3b7b33356502455151c90e1bbc44963aa27bdd6c6982" + }, + { + "id": "197884a7-b4b5-51d3-94ad-63ad6ab2be98", + "version": "MTc1NjMyODE1NTEwNDMyNzAwMA", + "digest": "sha256:c863171f5c1368294c6ef55499153023b148c0f754920d37863e9a6da27ded91" + }, + { + "id": "1999a502-2fb8-5e85-b19d-6ecb5560ee1c", + "version": "MTc1NjMyODEwMjMxMzQ4NTAwMA", + "digest": "sha256:388c71199061b82acba3d7d15a0f77543731eb7b2303feed4781c446923bbdbf" + }, + { + "id": "19ba4a70-e76b-5ff4-9f80-e2ac5baba839", + "version": "MTc1NjMyODE0MTQyNDEwNDAwMA", + "digest": "sha256:9e7a20ffd464c12609d82264a10c7e86e5572338c2587a9442a52c37d5b48234" + }, + { + "id": "19c7b213-d5c9-5d23-9387-d0b0481da475", + "version": "MTc1NjMyNzk5NzE0NDM1NzAwMA", + "digest": "sha256:d76ebe4bab4a98ff45dad98f450edeb833effbefb0a581cde86ce1fbfd3aed27" + }, + { + "id": "19d39b16-8429-5c8c-88a3-d8721451fe25", + "version": "MTc1NjMyODA2MjEwMTE3MzAwMA", + "digest": "sha256:b53db49fab2a9d7ce220ab14bc4a946d413e9981bcc6a76bea2c4d87de439629" + }, + { + "id": "19da1221-6cc7-5b75-a424-1f52d5594c81", + "version": "MTc1NjMyODA0NDc4MDUzMzAwMA", + "digest": "sha256:c8f463a67888e82ce41c07888d0afae788cc958be691c2658738d6c88a71b331" + }, + { + "id": "1a078e8a-060f-5946-9cd0-faae32ed99f4", + "version": "MTc1NjMyODAxMTA4MTcwMDAwMA", + "digest": "sha256:d39acb6d310585a01b76a276bcd4186aec2c3c5f26b6fd2f0ab5f1047fef8066" + }, + { + "id": "1a0fb102-245a-5b6e-ad53-1c20f767a1ca", + "version": "MTc1NjMyODE2ODk2NDYyOTAwMA", + "digest": "sha256:40715bb190da60e94eff424c46f0128a444a3d9f81acd9ce590d8f4f896c1839" + }, + { + "id": "1a10dd05-0d7e-583d-9fed-8ed303b748e8", + "version": "MTc1NjMyODAxNTUyNTgxOTAwMA", + "digest": "sha256:cf40d14addc3c05d541683ac180c920dce00ac9594c9aafe25d5bf84634fa2f1" + }, + { + "id": "1a157505-608c-5032-bab5-0773436b208e", + "version": "MTc1NjMyODE1NTE4Njk4MzAwMA", + "digest": "sha256:04a00e555f2ba72a325a595eb83e96d29e3f3a29aca815657800f27a160f2fb5" + }, + { + "id": "1a17350f-0def-5070-9612-48e0e88d00f6", + "version": "MTc1NjMyODEwOTE3NTE3NjAwMA", + "digest": "sha256:fae01a99b8f144e8509fede603785093bb98222c35c29a75f918f512d6c22367" + }, + { + "id": "1a3c5096-f064-5a38-9d8e-5c2f8211ff5a", + "version": "MTc1NjMyODA3MjQyMzExMDAwMA", + "digest": "sha256:45a7bed80c808c6bd9b221d50604e886dfbe0883d23671f2137afcf344844cae" + }, + { + "id": "1a42f275-7e92-57d1-b9a3-3f9fd7106bcf", + "version": "MTc1NjMyODEzODI4NzA4ODAwMA", + "digest": "sha256:a0e4908bf84e796a2272261c7663ad240fee0a4c715ea6f56e74b275d8b90d86" + }, + { + "id": "1a62a3c9-e612-57b5-a30c-d5f5bf886017", + "version": "MTc1NjMyNzk5MDQxNDcxMjAwMA", + "digest": "sha256:abb49113e656b25bfcfdb171a6b254f984c5b1535122cb2e44ed7231d5db56d9" + }, + { + "id": "1a700d84-2d1d-5057-aa07-37eb68241296", + "version": "MTc1NjMyODA5Njk3NTU4NzAwMA", + "digest": "sha256:9061e1a544863c21e5e91be675292923d5b8562e3da5f6f1e5fd639ea25a613e" + }, + { + "id": "1a726471-6b21-5d48-b373-b1c71f0edafe", + "version": "MTc1NjMyODAzMDc3NDYxMDAwMA", + "digest": "sha256:2c7115d3fdc995023eb4baa3656392fd33078b6cc719f663b1657e0a68ba289f" + }, + { + "id": "1a8d4c85-a14b-5535-99fa-1e3961da0e92", + "version": "MTc1NjMyODE3NDk5MzkyNDAwMA", + "digest": "sha256:a0810bad4bfc39e99a641d310c55468cc4929b50cc00011726f44a1a6beb47e6" + }, + { + "id": "1a9e2e37-619e-519b-aca9-7ae01b1ea662", + "version": "MTc1NjMyODAwODk5NTQ0NjAwMA", + "digest": "sha256:f902ed8a9b493710957501be4734887499dded55c8d7f93c863b8cae5a8dce33" + }, + { + "id": "1acfa240-b696-599a-a5df-3398818a77fe", + "version": "MTc1NjMyODEwMjE0MDQwNjAwMA", + "digest": "sha256:148711620f3e56390aba2b5e7c0fd6ee3cb5824955c36f1d903b1dc4f5e2b360" + }, + { + "id": "1ad06129-b705-54a6-8b4b-0c19f74f853a", + "version": "MTc1NjMyODAxOTI2ODg4NDAwMA", + "digest": "sha256:a98ed1d3dc496c56a872addd9715d7e271bb44b2682cf62419e971fe926aee7f" + }, + { + "id": "1afcf2e0-d3b3-5265-8a0d-aa7ac5ab787b", + "version": "MTc1NjMyODA4ODUxMDI3ODAwMA", + "digest": "sha256:74a6a898515a01fe57fbbd83dbe3a8f67b2571cc912a2a3c6c8678a2b4e1db02" + }, + { + "id": "1afda2d3-8f82-52d3-8cad-83d65f4a37df", + "version": "MTc1NjMyODA1NDA2NDA3NDAwMA", + "digest": "sha256:976e1c0cd921c6065b45e17091d19ca569786e7256e41425e514a8feb769974e" + }, + { + "id": "1b032e6a-4308-5b88-acb0-ee3210db7110", + "version": "MTc1NjMyODAxODc5NjgwNDAwMA", + "digest": "sha256:5d368db380d653bf53b09ce27ad65bc81107380794b557166c1334703b065e9c" + }, + { + "id": "1b038243-9a7b-5a97-8b4e-cbc2e930ee39", + "version": "MTc1NjMyODAyNjgyMTkzODAwMA", + "digest": "sha256:5c6245d5a89e4b55b124e74dba0821e023ebc00dd6f54ef31ca8f32c6d813a31" + }, + { + "id": "1b13919c-e905-5a9f-b60a-a5999888f6a4", + "version": "MTc1NjMyODAxMTU3NjAyNDAwMA", + "digest": "sha256:1e0bb543d65edd3357a7f0d6b7ddd6bcebba5ec2669f9c9548bdc601c0c8c106" + }, + { + "id": "1b86a75f-8032-5042-964e-3d1c64986ed2", + "version": "MTc1NjMyODEwMDk2NjUyNjAwMA", + "digest": "sha256:6e48b4d462f4c70d8c6c240810ffa61ac8c07e9fd538ee60fe8e74d35ff72fd9" + }, + { + "id": "1c14f6d7-a44a-595b-992d-13190b85c92a", + "version": "MTc1NjMyODA4OTI5Mzg0NTAwMA", + "digest": "sha256:9397b850ae630afe603ca840aeca2f5dbfd2a40449cd180ba3497fd6521d2fc0" + }, + { + "id": "1c3d3f38-8826-515b-8a23-e53a725b20aa", + "version": "MTc1NjMyODAyNjg4MjM5NTAwMA", + "digest": "sha256:59f1243fcb61fdf2a202c29601eaee8e477ccebbd9002b11829b29cd81928f19" + }, + { + "id": "1c56f5a8-54a0-52f8-b90b-3d9090d6eb64", + "version": "MTc1NjMyODEwODgyOTM5NDAwMA", + "digest": "sha256:da8b7e9705f6b9e7848335d03715371198bf9f21db14195e34a7a93bcc2ac379" + }, + { + "id": "1c796172-2ca3-5207-a7ec-5801f5c8e545", + "version": "MTc1NjMyODEwODg1MDI3MjAwMA", + "digest": "sha256:35f20854f7a4d698804d367507e880c2119d45c0df5e9b21b373c31a673ad966" + }, + { + "id": "1c828280-90c2-5dd5-9971-a2ae204764f5", + "version": "MTc1NjMyODE2MzIxNjk2NjAwMA", + "digest": "sha256:4376bd3d31cdb0f544ce461a9848d1d36dbc3d23ae481b31519faf88aacbd457" + }, + { + "id": "1c94200d-3381-5963-87cf-a308a90b1157", + "version": "MTc1NjMyODA5NDUwODEzMDAwMA", + "digest": "sha256:3f497be4ebc2428e4450b4033c8eaec6e485266d7698485a27edca1fc70b4ff2" + }, + { + "id": "1cb49879-13a1-5269-9687-01099781c40b", + "version": "MTc1NjMyODAzODc1NjQ3MjAwMA", + "digest": "sha256:5dcc0c928a5291c60989d2274b48ce1a6d415e145ea5f95ab558b304ebe07baa" + }, + { + "id": "1ccadd2d-c9b3-5c6e-af18-f9db4644e28c", + "version": "MTc1NjMyODE2MDUzNDQ1NDAwMA", + "digest": "sha256:fcef88894261f67199ca608548ccf1d6fbcbdabfcce9a87cfccd45b209d4f0b0" + }, + { + "id": "1ced94b8-6846-5f3d-abb9-f5f35a599656", + "version": "MTc1NjMyODE2ODg1MDExMzAwMA", + "digest": "sha256:09161552c42865ee41a121500ddc7228b0d22752809da8ff15d9e30a6bafc028" + }, + { + "id": "1cf07f21-22f5-577d-9f41-44b6e82b6cf0", + "version": "MTc1NjMyODAzODcxNTI3NzAwMA", + "digest": "sha256:72e0e7b0d6503c6ba37d6bd760e9d2be23e5ead9ccc3f8ab6de75794c59b391c" + }, + { + "id": "1d0b097a-647b-58d2-a32f-ad369720c8a6", + "version": "MTc1NjMyODAyMTI2NjMyNDAwMA", + "digest": "sha256:019d0d18785a1ea86848ab82f7c92033c39d24597d623412d8aa2223c6b3bfbd" + }, + { + "id": "1d0b3192-9928-567c-b217-53203245ed54", + "version": "MTc1NjMyODE1NDU4NDEyMTAwMA", + "digest": "sha256:b6dad3cf0410f1684c85cfc0d348ff26dbd38b2aafda515e17a03c903bfcf851" + }, + { + "id": "1d114135-641b-5d7c-9c60-fd3086f59a49", + "version": "MTc1NjMyODA1MjYxNzgwMDAwMA", + "digest": "sha256:5d10ff41a1f7a7708e90ad619133bfe822b63096efcbc3e9d3d694accec109a2" + }, + { + "id": "1d19a50f-4996-5b77-a606-93af6e0f2b8e", + "version": "MTc1NjMyODAyNzg0MTM0MDAwMA", + "digest": "sha256:7fe5698105da965cf577b0ef471e3921e90e5ec5b2e019622c37479c0422460a" + }, + { + "id": "1d284105-55c5-50a0-b7b6-0b80c669089f", + "version": "MTc1NjMyODA1MTI3OTQ0NTAwMA", + "digest": "sha256:f6733da3d66a4986e53ea9a7d44fddd608bbf47131e88a82d9bdb0f18d08594c" + }, + { + "id": "1d32a582-9cc3-5c09-bd32-0dd192bbec49", + "version": "MTc1NjMyODAyMjE0MTU2MzAwMA", + "digest": "sha256:958ed1956bfb388ea7ab99810c2d5630ced6039765b2def61c99f5e82b184097" + }, + { + "id": "1d46ef1c-e5a1-5fdd-a5d2-932a2fd2d819", + "version": "MTc1NjMyODA1NDEwMTc1NzAwMA", + "digest": "sha256:670f70c7729465397b6577ef6eb619133cff6a02021026ab92854d5c291c995c" + }, + { + "id": "1d79f1f7-d35a-5fea-b3d6-fad755359c42", + "version": "MTc1NjMyODAzNDY5OTk3MzAwMA", + "digest": "sha256:3fa82d02e385218feb8fece54cdaf0575a5f141eabdc6942ba289914000e7c82" + }, + { + "id": "1d859551-dc37-52c2-8ecd-3edc67839bbc", + "version": "MTc1NjMyODE2MDU3ODk0MDAwMA", + "digest": "sha256:f6528147eee7b2e13d06acacbf4989023ec6bcf53181fae81956dda50ccedde0" + }, + { + "id": "1d878998-f8a4-5903-b87c-3c556ad17c18", + "version": "MTc1NjMyODAyNzc2MDMxODAwMA", + "digest": "sha256:009a2b8a683a3d505de9abcc01f60dc289701a8c8ba32902a4625a444d7076bf" + }, + { + "id": "1dbefb17-06b7-5e9b-8025-482cb292cadd", + "version": "MTc1NjMyODA3MzE1Mjc2MjAwMA", + "digest": "sha256:52e343022131bf2b68e08ffae82755cf91938b0871d5ee23e90b81029a92eb0b" + }, + { + "id": "1ddc48ba-7be3-5c35-b51f-475f317003ba", + "version": "MTc1NjMyODE3MDkyNjE5MDAwMA", + "digest": "sha256:7ca88790e8013c3225b35f4e1bf4593ff17052e9571e8d98c8564fa3fd8b1f28" + }, + { + "id": "1de79c9e-6cf9-5e6a-893e-9e1002b7029a", + "version": "MTc1NjMyODE3NTA1NjMyNjAwMA", + "digest": "sha256:14e290e01044a0fbb1b5b8b8105b1f9d0e6792074fbf9fbdeb573edb9e971dca" + }, + { + "id": "1de87bdb-21a0-5ef0-b11b-c64f07da53a8", + "version": "MTc1NjMyODEwODgwMDQ0ODAwMA", + "digest": "sha256:7dc7dd22a1a770fd0215ccd330e23b337bd7347c1d0199abed8be5e9223314dd" + }, + { + "id": "1e08d9e9-d3a2-57e6-af0f-441d607c454f", + "version": "MTc1NjMyNzk5OTM2NzgwNjAwMA", + "digest": "sha256:3f06a457afd86e1fd3f91108ce4fcd97ed74470c47410cc8d630af3594933000" + }, + { + "id": "1e2e3c58-d61e-50fa-b48f-ae868f6c432f", + "version": "MTc1NjMyODEyMDQ0Mzc0MjAwMA", + "digest": "sha256:bee28e0fb67d9d43944822fc81bbc5b8edd6bcd800b7bbbe7604bd04cd29d5b8" + }, + { + "id": "1e319ca2-219a-5856-ad73-951b26d8b211", + "version": "MTc1NjMyODAyNzgxNzI1MTAwMA", + "digest": "sha256:6c7324f50c1e85ac9d8a2adfcc4204e029ff1101521a6372cd50fab161f172b5" + }, + { + "id": "1e6a0af0-7779-57b2-86d5-907244f6811d", + "version": "MTc1NjMyODEzMTA5NjcwMTAwMA", + "digest": "sha256:449188a8bccf7f51e3d0bfbbf8da49b2b68c7d68e558757e84d18ff7a8b5cfc5" + }, + { + "id": "1e949882-45c8-5f67-84fd-cffc2fa6fe81", + "version": "MTc1NjMyODAxOTQxODI5NTAwMA", + "digest": "sha256:949ec1e0fa8c38ce33b6c58ca839b293587f2d5260a7f59a5991d1283417e59f" + }, + { + "id": "1ed15702-d238-589a-907f-87806f713565", + "version": "MTc1NjMyODE3NDc0NzkxMDAwMA", + "digest": "sha256:b21004abc205808780886cd92d8466ed0b70b2d0e9bc88e14316922d020f63ee" + }, + { + "id": "1ed8b8c6-50d8-5efc-9df9-a436c4843f02", + "version": "MTc1NjMyODA4OTIzOTgzNDAwMA", + "digest": "sha256:e9c857a08fef1014ef85084eccac0bef9dfcf1b1a48c04427191b137e6e76f2b" + }, + { + "id": "1eeed3e2-d373-5126-9deb-f355ab52e1a7", + "version": "MTc1NjMyODE0MTUyMDAxNzAwMA", + "digest": "sha256:2de6b3017258b75d3bff298c647d69da97f93b62dd8cfed11b0b32f9f16adbdb" + }, + { + "id": "1efde97c-1aa7-5e46-8721-ce4f48981b12", + "version": "MTc1NjMyODAxNTU2NjY3MDAwMA", + "digest": "sha256:f4c3e5a99b04411b9ae5545f1d061db49facf375c3426869c49cc29961e75a6d" + }, + { + "id": "1f107685-2a19-5bb4-9871-f819dc7f92a1", + "version": "MTc1NjMyODAzNjg4Mjg0MjAwMA", + "digest": "sha256:07b787f86200b17dc977e91bd946784b0f219a9a20cf7935f3c8f52f1a3e494e" + }, + { + "id": "1f1a127c-4a27-56c4-870e-c60ccefe9432", + "version": "MTc1NjMyODEzNzEyNDgyMTAwMA", + "digest": "sha256:a2aa87e481402128d84bd272cf78c83989de5230eedb4578b5795e2690ec98b6" + }, + { + "id": "1f24d1ca-acb5-564b-9034-78670bebd0b0", + "version": "MTc1NjMyODAyMzQ2ODc3MTAwMA", + "digest": "sha256:22170526522c2f00b0e95cc78b7c6a44b9526f8a4c857b7ed9676a3c5390227d" + }, + { + "id": "1f286017-006a-5e71-9cab-8e054f1170b3", + "version": "MTc1NjMyODA0NTY3ODk1MDAwMA", + "digest": "sha256:c4ebf9e459c444ec38f4da1af51c821e90ea085305603c0cf049b197f4883b78" + }, + { + "id": "1f2bfe14-7dc2-57c5-8348-aed7a4feab33", + "version": "MTc1NjMyODA2Njk0ODA2MDAwMA", + "digest": "sha256:959580e2173e49fb5632d2847d4fdb90effab1fc0bde6a6a2e9a6e457c2fbca3" + }, + { + "id": "1f3dfa23-72ef-5e4a-82c6-ad4d538471d3", + "version": "MTc1NjMyODE0MTUxNjI3NTAwMA", + "digest": "sha256:35382be0166b73ea8ce8c30f2412eac435364de55470bb8f6893e4d02119969b" + }, + { + "id": "1f3e41cc-f9a5-57d0-bb19-2c46f67b501a", + "version": "MTc1NjMyODAxOTM1NjExNjAwMA", + "digest": "sha256:0abcbbf16ece523599f4d5a0245207f3c9dda2dfe665db84c2ca5dd63b462e5b" + }, + { + "id": "1f41916a-cac8-52d7-ac92-a891b819b595", + "version": "MTc1NjMyODAxOTMwMDgxNDAwMA", + "digest": "sha256:0d9ad7ff3a1122c0eb388a3082e627a5b29e879de4c003b5e8e7882e9c54b3f1" + }, + { + "id": "1f7bff93-544b-590e-867f-7b8bea9b6860", + "version": "MTc1NjMyODE2Njc2MjkyMTAwMA", + "digest": "sha256:61210164ee6d9b150fb147bd6719986c745e9e5604f9de3af1034c0ed9e60dff" + }, + { + "id": "1f84683f-8f91-5d00-8929-4beb2f31c1af", + "version": "MTc1NjMyODA4NzYxNzgzMDAwMA", + "digest": "sha256:a7890c9cea1be078ab7946179e00eaefbf475fdc3ee8362624a526e34abc3268" + }, + { + "id": "1f9562ac-8d08-5340-ab5d-f7795d36c6b7", + "version": "MTc1NjMyODE1NjQ4ODQyODAwMA", + "digest": "sha256:30853fd4b03c6f40594ee4ea67da76166dc68e4fc51e92f54b87576bccdaacfd" + }, + { + "id": "1fa12e0a-1fdb-51f0-9f05-d9ed7f0028c6", + "version": "MTc1NjMyODEzODE4NTQ1NDAwMA", + "digest": "sha256:d2b939f5bb91f404f95b2ed72ff8936c2e7da9592a5994637ade2aa746eb7da2" + }, + { + "id": "1faee25c-9bec-5237-ab01-b151776b4182", + "version": "MTc1NjMyODA2MjYyNTU4NjAwMA", + "digest": "sha256:b3cf4083ecf0b11cff874ec8520b24cc0fab2ffedfb95a29a9ed66070d024bf6" + }, + { + "id": "1fb62cd1-e27f-5531-8f67-d00c040d5f01", + "version": "MTc1NjMyODAwNTE0MTExMTAwMA", + "digest": "sha256:02459cedee604d94bd2c58b58d0db1b9020481cae72c77a82f5478d72112d817" + }, + { + "id": "1fc53b49-2650-5cd2-a6a9-7d650aef5dd8", + "version": "MTc1NjMyODA1MjUxMTc1MjAwMA", + "digest": "sha256:7ffd028567f5c39104f46a76d5dc41ab8b2f4dc870e954a79965f67d06583d05" + }, + { + "id": "1fcf8aca-31f2-5de0-bc15-d6f685ace009", + "version": "MTc1NjMyODAzNjgzNDQ0MTAwMA", + "digest": "sha256:fedd5b7dee9e3a7ac3f7e99e4205823ff5410c15e883485e43e98311ee03847e" + }, + { + "id": "1fd6a351-5b08-590a-816b-af380a7827ef", + "version": "MTc1NjMyODA0NDg3NjUyOTAwMA", + "digest": "sha256:861afd492fa05dce14f056e54a34b729f47a73530c302296b9387e977ddf9eaf" + }, + { + "id": "1fd7b4bf-308f-565d-ab45-8cde70723ed0", + "version": "MTc1NjMyODAzMDgwMzU2MjAwMA", + "digest": "sha256:518efa86b2083bf78400add340af15afbfba45ab8df481138e0db605df9d993d" + }, + { + "id": "1fe96870-4d17-5e36-bc40-a014d32c4b40", + "version": "MTc1NjMyODEzNzQ0NzA5NTAwMA", + "digest": "sha256:6741432885caa545535bb14d9b675b647c01b5c0b47952107d64420f3702aed7" + }, + { + "id": "2002b5b2-7288-503f-a652-986411133e6e", + "version": "MTc1NjMyODAyNTk0OTU4NjAwMA", + "digest": "sha256:7825f90c348326fa3d9e9e8c70cad834e87de90c1432d4c8ac5139afc41291d2" + }, + { + "id": "2002f271-b1bd-5968-bb79-93fbb84e9ff1", + "version": "MTc1NjMyODAyNzg1NjI5ODAwMA", + "digest": "sha256:170f968ce6a738aef019effa70ddd9f0be192212848f7d4f2e3aaded55ba567c" + }, + { + "id": "20266bbc-423b-53c5-ab51-ab9e049857cc", + "version": "MTc1NjMyODAxMzgzODg5NDAwMA", + "digest": "sha256:e60834ecec90bb0194fa9e8f6c10a22831d6552c29ff9afaaa3ccc62e877a5c5" + }, + { + "id": "202d45f5-507d-5ab1-9c1d-2ded779f0351", + "version": "MTc1NjMyODAxOTMxNzA1MjAwMA", + "digest": "sha256:1df3eba4f4a7c843fc9e2e6e6e95153cde91808975b78f007a5c4a6024943b80" + }, + { + "id": "20462b4e-927f-59ea-8fbd-d345396acb0e", + "version": "MTc1NjMyODAxMTU3MTA5NjAwMA", + "digest": "sha256:b2b8d5fb4824fdf4d78f83b8733e7b0f07c16c5716af213169fd07e2d7768af3" + }, + { + "id": "2046788f-e49c-59d7-9eaa-49d1c062ad46", + "version": "MTc1NjMyODAyMjEwMjg4MTAwMA", + "digest": "sha256:74bdcdfb1a316401b54ae0d214fa41808a3f0edb93546de0e29025f5feccf527" + }, + { + "id": "204970af-9393-5f47-bd29-e1e0c9b77b68", + "version": "MTc1NjMyODAzODc0MDgwNTAwMA", + "digest": "sha256:e5442d998575dcb4e4e44cf3be223c4b4c6f0733f7c27b114aa0ca5d43d66348" + }, + { + "id": "204c0b56-23e7-51dd-8e3b-bf95daece986", + "version": "MTc1NjMyODAzNjkwODExMDAwMA", + "digest": "sha256:2e308dad55de6e44bebc32788a9a402deb0241b355ca07fa303d452de3128cd8" + }, + { + "id": "2052a992-4fd2-5d51-86d6-fa8e822175e4", + "version": "MTc1NjMyODA3NDMzNTk4ODAwMA", + "digest": "sha256:aa40a44ea677d673413a93324f82f88fd290d56b694d09ed50d1dd797da64a83" + }, + { + "id": "2085067d-e7a7-5d5e-93ad-6077da379342", + "version": "MTc1NjMyODE1NjQwNDkwMTAwMA", + "digest": "sha256:4ddbebb935346bf78aec5bbce1ffec9194c44e42c7172ec9c97df6a7bc8fa28a" + }, + { + "id": "208c7e31-b3fd-5204-8118-cef18ec500c0", + "version": "MTc1NjMyODAyNzM1OTQ4MzAwMA", + "digest": "sha256:57dbdf053299a373366442b63200e9fd52d4e38c6dcb2900187de7d14598d566" + }, + { + "id": "208e3c57-ee35-5e80-adf7-946cc9f29fd2", + "version": "MTc1NjMyODAxOTQyMDY1MTAwMA", + "digest": "sha256:418159acd4e4fade20bc90640fe7f7e9014b55bd3397bd5f5f27bc11f5cda27c" + }, + { + "id": "21083f10-89fe-5325-8a85-09ff8c638ce4", + "version": "MTc1NjMyODA5NzAyOTAzNDAwMA", + "digest": "sha256:e000b911321cce8bda5b21ebcc9d77cd416d3a4eb0383e90f0ac9591d76e52ac" + }, + { + "id": "2111c75f-78d0-50e1-9eaa-0d6872e8e9a0", + "version": "MTc1NjMyODA5NzgzODg1NTAwMA", + "digest": "sha256:9a0c0bb432cdeccedac98adb8d1dcfa694ad2e774db7180128d9fdeb389d5fbb" + }, + { + "id": "21405147-018a-5078-b0d1-34e92b2567ce", + "version": "MTc1NjMyODAyNTU4MzU0MzAwMA", + "digest": "sha256:7a40498000e8e3b4b2e7ac97fa49403bf145490dfbe6a39cdeb1f30b5b7a8061" + }, + { + "id": "2143d991-d748-52fc-bea2-d0c5b5a22b86", + "version": "MTc1NjMyODEzNzY3NTkzMTAwMA", + "digest": "sha256:c97a74aea3aab252121f27de5b332abcbf495140bd25f8b995abcb4a4408b266" + }, + { + "id": "214dcb41-11f5-54c6-8ce8-7518152c9b5e", + "version": "MTc1NjMyNzk5NTM4Nzc5MDAwMA", + "digest": "sha256:e855b45ceb9733977e329120fb3a1ca8503d8c5036d6dfd8750dd373ffd9456f" + }, + { + "id": "215a6012-390e-5348-8db3-b437ca484660", + "version": "MTc1NjMyODAwMzk5NjgyNTAwMA", + "digest": "sha256:28c2139bda3dec8e91034981a57c60e2af5e6de013262ade8b10f66a244da97f" + }, + { + "id": "218c87ee-e743-58af-ad70-15f43273d600", + "version": "MTc1NjMyODAyMDQ5NjA3NTAwMA", + "digest": "sha256:0f5ffec3e2b96410ad056762910d131c9d33a809723b645fa418f858851dd2e5" + }, + { + "id": "2196d33b-6c82-5037-9dc0-e99cb128a120", + "version": "MTc1NjMyNzk5NjcwOTc0NDAwMA", + "digest": "sha256:98dacc08f776b7194ca9507d383d7456671ac392dfd9c847cfd02a9ea932f7b1" + }, + { + "id": "21a78c88-68ac-5683-94f5-61a4ac77abfc", + "version": "MTc1NjMyODAyNTc2NDA4ODAwMA", + "digest": "sha256:62e533b093ac0d9988507a470c4d7f30b3d593d55d3788b049e11491ef2f5d31" + }, + { + "id": "21bcdb75-c371-5b6a-866f-1d4b1779a36b", + "version": "MTc1NjMyODAyNjc5MTkwNjAwMA", + "digest": "sha256:39f160a434bde072b800be33ab765caf334003b3ce78c204c0fa0ef534727117" + }, + { + "id": "21c5fe47-83bf-5ff6-ac38-e0d2ae7adbff", + "version": "MTc1NjMyNzk5MDQ0Njg1OTAwMA", + "digest": "sha256:68c46d30eec19a226474789fbc55c715bc3521212e7ff2d03615addf92641d69" + }, + { + "id": "21c89dff-b8c5-5d46-9a09-f202b58195b8", + "version": "MTc1NjMyODA4NzY0NjQ1NTAwMA", + "digest": "sha256:82b748a2cb380cd45d834e48fcc25c0b5f24e6ed0d6ace8151fd300c450ffd33" + }, + { + "id": "21da72e9-d5a2-545d-a20e-b94787681a20", + "version": "MTc1NjMyODAxMDQ0MjI1OTAwMA", + "digest": "sha256:e3edba99e3dd0a5f47f1428acb264b1920e17c2cdd632f845610c9d14942cc49" + }, + { + "id": "21dea39e-d655-5bf7-b68b-d7751c11cd31", + "version": "MTc1NjMyODAzNjg5ODg5MjAwMA", + "digest": "sha256:6942ae437dfea81f65f797f253525b5f77ea7d2a7b52fc55cb8d8c1c922843d1" + }, + { + "id": "21e47085-4476-5d3d-bdf0-417a5fac2431", + "version": "MTc1NjMyODE1NTI0NjA3MzAwMA", + "digest": "sha256:9fb65ec39952ef1244c27f4e013ca22a69dc441bc1066e3d4e3a2280132c7886" + }, + { + "id": "2221e632-7fe7-5b05-90f0-a901965785c1", + "version": "MTc1NjMyODEwODg1NTY1NDAwMA", + "digest": "sha256:89793aa406163fc1c8c631f49ee076f7db3b019208e13c0fa54ae7a6e40267b6" + }, + { + "id": "223b1d99-00d9-5f8a-a090-46cfef0f90b4", + "version": "MTc1NjMyODEwODgzODQ1NzAwMA", + "digest": "sha256:4aeb26476f4789db0cf9b7cc382b3b5aaf969e7c8b8cdfd9f2d250d21af54a57" + }, + { + "id": "22537a9a-4283-5289-a7e8-72b26212db8f", + "version": "MTc1NjMyODAyMjE1ODc5MTAwMA", + "digest": "sha256:112399fe84f35278130c2b19f3724c2d7967d0fdd879ef961384e14c66b84634" + }, + { + "id": "2259913a-f24d-5349-8968-fb5e8f6c27dd", + "version": "MTc1NjMyODAxMTA0MjI4MjAwMA", + "digest": "sha256:b29f11bb98da9fda2cb82cb9c04ab0b6e2540d6a02eda78c512e90a03b4dca05" + }, + { + "id": "225d2299-c0a1-555a-ab8b-a1d7a37883eb", + "version": "MTc1NjMyODA5NzY0OTA0MDAwMA", + "digest": "sha256:0c8275c7c1baa8f448db69872a0148e7c1be33d8317fb9977a935f4b76c45729" + }, + { + "id": "226d3a26-f9e6-58a6-8b41-93648461a8c5", + "version": "MTc1NjMyODAxODg2MDY1MDAwMA", + "digest": "sha256:a62a7cd4864772af2d9f7c86b56893b33528d70d8a349495db7f52fe64501d02" + }, + { + "id": "227edaa9-5565-5e28-9d4b-b6ca0ddfa517", + "version": "MTc1NjMyODAxMzgxNTkxMjAwMA", + "digest": "sha256:060bd42e73748d875c34ffdf3af9f905f5b451df28d788797dd415ba1f0ca52f" + }, + { + "id": "228933be-e967-59cc-ab1a-9ac3fad5490f", + "version": "MTc1NjMyODAzODY4MjA5MjAwMA", + "digest": "sha256:0ef802b7ab0b5c441bad3073580539c5f29c65d04d7d16a811986749a7649979" + }, + { + "id": "2295d033-a438-5a54-b89e-346641403609", + "version": "MTc1NjMyODA1MTMzMTk4OTAwMA", + "digest": "sha256:19080a6d03a719739a3b650916f28f8c68768b294f3063dff712be4f0a1f3a8b" + }, + { + "id": "22991ef5-b691-52e0-a518-98acfb20ee10", + "version": "MTc1NjMyODAxMDQ0MzI4NzAwMA", + "digest": "sha256:54730878d649f027ccb32e76ffd480d67564a2ee95a7d0677f9d2ab7f895b530" + }, + { + "id": "229f9523-5a3d-5432-b422-04001bbbcc9b", + "version": "MTc1NjMyODE2NzQzMjIxMTAwMA", + "digest": "sha256:cceefa3ad4878c13c58235c1a0404b7cf5f40f99a288dd0c6d1e10500d001b40" + }, + { + "id": "22b73265-5f9c-503c-8a7a-2cbea6520dec", + "version": "MTc1NjMyODE2NjQyOTYwMDAwMA", + "digest": "sha256:0db03b9d54274ab1e2b1b459dc7693bcd958d3075bb771528cd0223f315a2979" + }, + { + "id": "22b808e4-9109-5b49-a5cc-4c41ec324cbf", + "version": "MTc1NjMyODAxMzgyMzcxOTAwMA", + "digest": "sha256:3d862fb3da0f84b8d232b6062ea5b7f7542896b388e0c5bde563f960ec6a713a" + }, + { + "id": "22d31829-2442-5aa7-8340-150434e8923d", + "version": "MTc1NjMyODAxMTA4ODc2NDAwMA", + "digest": "sha256:33b3c7e317ac220d695f5adf45bb147759eec18ffda3d22e1280ba70e3be5c04" + }, + { + "id": "22f21925-e7ef-59d8-94d2-afe007cdba35", + "version": "MTc1NjMyODE2ODg2NjMyNzAwMA", + "digest": "sha256:dba11749fe98abdda8d418c9779c5df0f24922efcd32078c08538989e2199b60" + }, + { + "id": "22f4345a-977a-570c-b6ff-3b1d237e22c7", + "version": "MTc1NjMyODEwODgxMDMzMzAwMA", + "digest": "sha256:2695689451c38049065d0474ad30572ff78d575fe0205cb6397a4d03729ca0b6" + }, + { + "id": "22fd2e4a-4784-5d29-9a58-0ef89404821c", + "version": "MTc1NjMyODE3MDQ3MTk4NzAwMA", + "digest": "sha256:37b94cace8c1bff0b44e84e90f49dd1d5e6c447d26d459df111dad8ab439fb30" + }, + { + "id": "2303bea4-e21d-5613-983d-94c649db55dd", + "version": "MTc1NjMyODA2NjkxOTk2NDAwMA", + "digest": "sha256:055c67ec8401cbb7efa8a681885ed9c7c21388918377f1d093904f81bd180e10" + }, + { + "id": "2328b7d7-0442-5a6a-815d-5700b58e135c", + "version": "MTc1NjMyODA1MjYyNTAyNzAwMA", + "digest": "sha256:1233a2cedc7d13d3cfcd82040cf083e3b5c741d1c2824cb90111dbc7f28a5f00" + }, + { + "id": "23362755-3cea-59e9-b289-10f38ca64a11", + "version": "MTc1NjMyNzk5ODE1NDQyNzAwMA", + "digest": "sha256:a73ce32cc4231f53659bb6f581c6af660f46b5e2d73f22d70205bbc9cebf9549" + }, + { + "id": "234ab3bd-93fe-5086-bbe7-e7d77fd60b0e", + "version": "MTc1NjMyODA3MzIwNDU5MDAwMA", + "digest": "sha256:5fbf766dec33bc728e79d96828f9c84ce4ab58f115fcb7b067cad33186d0a057" + }, + { + "id": "238b6d88-71c6-5976-8766-1e17a3109896", + "version": "MTc1NjMyNzk5NTY3ODY0NTAwMA", + "digest": "sha256:9041705f922c7bd23720cc5ec39d9c740d8c38da07c67056724c2dea98d0fa49" + }, + { + "id": "238f9f50-68f8-5480-ba90-8308567aa7f6", + "version": "MTc1NjMyODEwNTM3MDI3NDAwMA", + "digest": "sha256:13fe602dfe31667c44b362853389a4f53a57945c15f0c7173d0a00a198bfc1b9" + }, + { + "id": "23a666c4-5523-554f-84f7-2650f48dec6b", + "version": "MTc1NjMyODE1NjQ5NTkwNDAwMA", + "digest": "sha256:95cd08f1f4ebeb9f52734c9ad79feeead800ac779c3e2732963e253bc8b9bc94" + }, + { + "id": "23b63c7a-8fc4-5dd4-b9df-962e2ef6bb0c", + "version": "MTc1NjMyNzk5NTY3NzcwNDAwMA", + "digest": "sha256:bdc2ed8db1ba17522203446b44fa665800703c0c012fc61dcd11faa654d1abb2" + }, + { + "id": "23ca3335-f997-50fc-a3ec-23e87b1e4583", + "version": "MTc1NjMyODAxMzc3NjU2OTAwMA", + "digest": "sha256:370fad232321d723a1198b45cc03dd66ab41de9f0fc52fd47539a170bb470b98" + }, + { + "id": "23d687ce-b478-50c5-b5b6-22a18cd562e3", + "version": "MTc1NjMyODEzMTE0MTgzMjAwMA", + "digest": "sha256:58bc8bd9297e4f7cf47d3b59d4d621493fcc8a6c815d6538cd337797072699ee" + }, + { + "id": "23e25905-0ec0-55ff-86bd-552e28fec052", + "version": "MTc1NjMyODAzNDczNDEwMjAwMA", + "digest": "sha256:c2e04146e5a0503bcbc837cae2873665da7fac89a5be3177ea9df53add557b99" + }, + { + "id": "23e8aff4-56c7-564c-917b-d1771c8c2822", + "version": "MTc1NjMyNzk5MDM4NTY5NzAwMA", + "digest": "sha256:79b0c991969cf20a97fc60e3b4dc50015e616f969a162291f11cc01deaa1e8c0" + }, + { + "id": "241d30e4-b3d0-5511-81d6-c66711cbe258", + "version": "MTc1NjMyODEyMDUwNDMwMDAwMA", + "digest": "sha256:6cad35a500b6e04aaf2ef7735eeb88c960396be820f43ba3700c01b9a44204fc" + }, + { + "id": "2437f9e0-9db1-5721-bb5d-a122285c99ab", + "version": "MTc1NjMyODE1NjM4NTUzNjAwMA", + "digest": "sha256:80d463ec85f9ee8e350928a5dfe226dc2b22fcef3a83cf3a82886f4724998eac" + }, + { + "id": "244e29c2-1b3a-5452-8312-1564eb550848", + "version": "MTc1NjMyODE1NDU0OTMyNDAwMA", + "digest": "sha256:480d396db23e544879c1fd16784b34a3781ad84da8e63d97c2c70acdd5272a77" + }, + { + "id": "2456e231-a2ec-5cc4-ab93-85a3285ccea6", + "version": "MTc1NjMyODE3MjQ5Njc3OTAwMA", + "digest": "sha256:302f8a32c76cf03ac1c05fc9022f1c08c0e38a2e782d5dca40ef1d73ad31ea3f" + }, + { + "id": "245f9487-7da2-5557-bd61-14bf78480426", + "version": "MTc1NjMyODA3MjU3MTM5NjAwMA", + "digest": "sha256:93992270b7dbd82bb7766ac8c6afcc524965d92964c9c0024aae0cf25ccdb21c" + }, + { + "id": "2477a6a7-4744-50f6-9b01-63a706979429", + "version": "MTc1NjMyODAwNDAxNzY1MTAwMA", + "digest": "sha256:80cb09d5dc216e13bcd80a3ce52e1fb2c1ef28f55e8dcde4f3f09c1a8d1e26eb" + }, + { + "id": "2491ba67-fbcd-5e02-8ff9-63277c64d9d5", + "version": "MTc1NjMyODAzOTcyNzA2MjAwMA", + "digest": "sha256:a2b3dd63ea1bd580c7c380d12c81e3379ef8da19e666f4e53ecce95d8dbba0a2" + }, + { + "id": "24ce1a37-55d9-5af9-97d6-42702f53ba36", + "version": "MTc1NjMyODA1MjUwOTA2OTAwMA", + "digest": "sha256:03a3163c472cb370bceca66477767e1b45e95eb8e1deec6040e6c92d2d76e551" + }, + { + "id": "24df5e4d-9056-51bf-bcb1-7f969d9ede13", + "version": "MTc1NjMyODAyNzQzMDU0NTAwMA", + "digest": "sha256:43e598107a14f7fb023d24cbc9b0a904b19d3584b7f199219d5b1d20c5a7100c" + }, + { + "id": "24f1593a-35e0-5864-8c80-7aad855bed2d", + "version": "MTc1NjMyODEwOTI1MjgxMTAwMA", + "digest": "sha256:0c4bfe517969b31a56f8114a18ccab56aeec7509b772e033edcbdf8d7d3fd085" + }, + { + "id": "24f951fe-bc36-506a-a60f-3ba26167e1f8", + "version": "MTc1NjMyODAyMzM5NzIyOTAwMA", + "digest": "sha256:44e6370c2ba66428c6a55c3388e220d7eb123eba66a0570398b1551fff22c773" + }, + { + "id": "25360a88-9762-5204-bff4-ea093a84ba98", + "version": "MTc1NjMyODEwOTIxMTI5OTAwMA", + "digest": "sha256:d5988c9e987ebebdd7ad7dfe0e0ba1d510bf1ce003ac2ef2baee4b498efd4936" + }, + { + "id": "253eeaed-d953-5d28-991b-f1cc7b5bc458", + "version": "MTc1NjMyNzk5NTY4NjU5NDAwMA", + "digest": "sha256:7246e8938b7af9d80efa78304c0db0774b0ae403d793bb87633ce20fce20e433" + }, + { + "id": "2555e43d-34ca-5961-931c-b85171002344", + "version": "MTc1NjMyODE1NjUyMzc4ODAwMA", + "digest": "sha256:7617d4ca32db3bf9a224b9a7aac7956644809fee1e0b5431f92aa91e68206d1a" + }, + { + "id": "25607529-d387-501c-83af-a6f6e2b6a543", + "version": "MTc1NjMyODAzNjg3Njg2ODAwMA", + "digest": "sha256:2350fd33ce57ed6a355f2c5e7e09be60529aa8ba28b4c9e0e8998aada5518b35" + }, + { + "id": "2588da7c-5d6c-5836-a48c-718dd17d5130", + "version": "MTc1NjMyODA3NDIzMzg2MTAwMA", + "digest": "sha256:6406213002c3f78d5cdd167e37706f00cf4a727ccaf95a497619122112dca10c" + }, + { + "id": "259ffdf5-15d0-5f1d-bdbf-89c407d87791", + "version": "MTc1NjMyODEwMTAxNDU2MDAwMA", + "digest": "sha256:2ad71dee52f26c2ddf764c86504a61a69ba543e5cc7ed98f9ecf31384645f5fd" + }, + { + "id": "25ae836d-d576-5559-a4ae-1c84daa2189d", + "version": "MTc1NjMyODE1NTI1OTA0MTAwMA", + "digest": "sha256:c9603d48322ae3a9edbae4e69b588c25874ba6eda0120fe7f5d2c46a191c8ce6" + }, + { + "id": "25ba6735-fc1c-5f06-b017-8693d6021738", + "version": "MTc1NjMyODExOTY1OTgxNTAwMA", + "digest": "sha256:4b2d96435d80b76fb1e6a587e28367f7dee3cb9d8b2f4350a8602321239e87aa" + }, + { + "id": "25be4aa6-8d46-5c6f-8213-d62996c5959c", + "version": "MTc1NjMyNzk5NTY0NTY0MTAwMA", + "digest": "sha256:5cd7bfd5a87cd65c8508448cbf3f78bd47f360cc6d0e6f204e2da517423b31e8" + }, + { + "id": "25c2ffc9-a43d-5e52-83d7-2cfce0729f02", + "version": "MTc1NjMyODE2MjM3OTc0MDAwMA", + "digest": "sha256:c409334efec8aaa719bce14673afdba35aa3d8134f78666e88f414ef58ce55e2" + }, + { + "id": "25e465c9-5940-5b1f-a3d1-90d1c0f22bbf", + "version": "MTc1NjMyODA5Njk0MzE2MzAwMA", + "digest": "sha256:abe7e8042673eeafeb3d4465378726d3376cb407e49fbdfe0ddec82e3d5d1309" + }, + { + "id": "25f6bcad-af8e-51d7-9397-49d53735bb73", + "version": "MTc1NjMyODAwNjIyMzY5ODAwMA", + "digest": "sha256:4c73ab785bdb3822c7097ee39464f17e91a97e061dce7b4bb8bf420d1b0d4d4c" + }, + { + "id": "261d0c03-e885-5580-988c-0c91338c2e29", + "version": "MTc1NjMyODAyNzc5MjMxMjAwMA", + "digest": "sha256:a488a539f52e05c9ce70c972d72d207be8afc6fa562886973380d67e3afe3194" + }, + { + "id": "26384c6b-eae5-5614-bdfd-2e101f2614dc", + "version": "MTc1NjMyODAwODk5NzkxNzAwMA", + "digest": "sha256:4164f54dad9eb3654ad6098284aed57a2869f0ef5713188bad74733c0d0b14c1" + }, + { + "id": "263f8c29-56a0-5cb2-bccc-44cd3c920001", + "version": "MTc1NjMyODE0NzU1MjE5OTAwMA", + "digest": "sha256:b714ceb6a21d08e1c00fe317ac0224689b1b38cd78291c7135f07b98a698f213" + }, + { + "id": "2646ca77-5c60-559c-9f2f-5b0d8cf664cd", + "version": "MTc1NjMyODA3MjU3MTI0MzAwMA", + "digest": "sha256:29ef07e20697beb6041d88e7cf7d03082e466893035b3da677ff1db5d719828b" + }, + { + "id": "26821a30-6132-58f9-835c-8e4df68f4d98", + "version": "MTc1NjMyODE3MDg5OTk2OTAwMA", + "digest": "sha256:de7c23ee12ef4934fa75eb423ed44357df8206168b07a2d15c031b58b1efd5aa" + }, + { + "id": "26f468b0-f72d-5d2a-b86b-1eebf29f0fd9", + "version": "MTc1NjMyODA2NzE0NjcxMDAwMA", + "digest": "sha256:57d48ce6e594ef195ddd316b39b8b6e728aa586137b0a2840e4c96ab8b68f816" + }, + { + "id": "27157513-15a5-5410-830b-4ec3d320cc24", + "version": "MTc1NjMyNzk5NzEzMzIxMTAwMA", + "digest": "sha256:fca8ac4979e36db6109483843be97723d3545424173516f63417a94b2baf4356" + }, + { + "id": "272380fe-ae67-5a55-ab20-9c5f35f539b5", + "version": "MTc1NjMyODE2ODk1NTYwMTAwMA", + "digest": "sha256:1fee6938058ab091c975bb41f64336b708f8cb8223b640c7fd9e299aa23dda6f" + }, + { + "id": "272dccdb-6386-5e3d-bd35-e349bb40cd41", + "version": "MTc1NjMyODAxNTIzNDc0NjAwMA", + "digest": "sha256:1d48e90a8072484888b71315020f5bca32395ccee0eae765b8f66f4a7b483641" + }, + { + "id": "2743c359-2f2e-590c-9e75-a714da8f0616", + "version": "MTc1NjMyODE2MzMyNjc4ODAwMA", + "digest": "sha256:33ba76061753fea024c3d06cd55a653761087997c1398dee7b4fc9c9e98188ba" + }, + { + "id": "27597425-94d3-5c2b-a8a7-3213732acf97", + "version": "MTc1NjMyODAwMzk3MzAyMTAwMA", + "digest": "sha256:7b5b785e7bee0f1b9c01efe75baf1dbc8d12c050752473e178b289b0719a4412" + }, + { + "id": "277f25ae-cc35-563d-b7da-fd02e90ff9f8", + "version": "MTc1NjMyODE3NDgzOTE5NDAwMA", + "digest": "sha256:f22f48611e77e962bd34e9331591550d3b4ee4a6a7656c11f59b4cc8011f2052" + }, + { + "id": "2791a45c-45f0-53ee-9678-d6575e422013", + "version": "MTc1NjMyODAxMzc5MTk4MjAwMA", + "digest": "sha256:dbfc91ff1ffffdf35bd7ba2eab61adb783d414a5f3a21418321409e32b8e1e30" + }, + { + "id": "279ba893-1350-5987-aaa0-73b78d781495", + "version": "MTc1NjMyODEwMTA4NjgzNjAwMA", + "digest": "sha256:d72e7af84f3979afee38f666ddc6b07a7158c985d09c4547a78b0eeb90c923cb" + }, + { + "id": "27c0276a-d0ea-59f7-b84f-55515c6faa12", + "version": "MTc1NjMyODEyMDQwODgyMDAwMA", + "digest": "sha256:8aa3130f10ce6b4d7f1056feb101b7b23f3abb4fe64673aa73eb96f0a9433ab2" + }, + { + "id": "27d3c666-62a4-529b-8c51-f44679a6bdc7", + "version": "MTc1NjMyODAzNzE0NTEyMTAwMA", + "digest": "sha256:001f0fb2405fa1c5a4607c9fccaa6aa569e15c43b34b48977c8e074ea81eacb3" + }, + { + "id": "27d77832-bed8-5fc3-b4ea-5fbfc61093bb", + "version": "MTc1NjMyODExOTIyNzc2NjAwMA", + "digest": "sha256:6e3a69d9bad779ca47103167b048cc5244dcf6c2a7e166baf06fd8f66f876c1a" + }, + { + "id": "27e05d3b-bc63-5063-ab80-d6c207bb348d", + "version": "MTc1NjMyODE2Njg1MzEwODAwMA", + "digest": "sha256:4a0ef09c079507ba741ddaf44699b5b95091865eee0042313b2922307529c315" + }, + { + "id": "27e09fd2-2561-5656-a435-4e050745fa40", + "version": "MTc1NjMyODA2MjIzNDg0NjAwMA", + "digest": "sha256:c11779c91065d8a938b088f53bc6372230eb45c4822a1d8cc8b4de13271b3269" + }, + { + "id": "27f1d061-52ce-555e-995d-9fc0f7c12038", + "version": "MTc1NjMyODE2ODk5MzMxNDAwMA", + "digest": "sha256:ce22dd22595709318297cc49f711d0a765faebd3a8649a97839e246cd8ebcda6" + }, + { + "id": "2800748b-f124-51a0-91c5-e944f6c98d23", + "version": "MTc1NjMyODE2NjQ3MDcyMDAwMA", + "digest": "sha256:6226ec04ca823b97ba123bdfb73ee18e78ec4efcf9cf6525e7ef49998c1da306" + }, + { + "id": "280dd602-be16-554f-9430-4a163cb96eed", + "version": "MTc1NjMyODAxOTI3MDk3NDAwMA", + "digest": "sha256:7538612735f7af580b8ecc1c9ff6f4ad86fddcb00716b7df9a5ff73631c3cf64" + }, + { + "id": "281d02b7-a516-5d99-8e76-57c4e92be533", + "version": "MTc1NjMyODExMzU1NDYxMjAwMA", + "digest": "sha256:4f79646ddaf8f7f61aba2206f98326ef7f6bea5cf4808a1a7a3d03e18aa45bef" + }, + { + "id": "281ff542-f153-5794-979a-b518ef5a8652", + "version": "MTc1NjMyODAyMzQyODU3NzAwMA", + "digest": "sha256:2e678a152c7aff04edae0c12c961b08babea98322f3ca99cfc47b6ce940bb3f6" + }, + { + "id": "2846d8e0-9240-5696-a865-4f60eecc85c9", + "version": "MTc1NjMyODE2MDQ3MjQ2MDAwMA", + "digest": "sha256:4d94aeb687a8d3e84873ae9a603656861c15fc4a30fb4779e419a53e79d43a4b" + }, + { + "id": "2855f807-d982-5033-82f9-4303eac8b19a", + "version": "MTc1NjMyODExOTcxMzM1MzAwMA", + "digest": "sha256:20aafb7efb188437c18e4151e8605551f8c78bd706cb3d95966f2402254f6fa7" + }, + { + "id": "28574936-9e95-5412-8561-70b28f82ec13", + "version": "MTc1NjMyODA3MjU2NTk5NTAwMA", + "digest": "sha256:31a4ec3b7184af8417f88868d5ea2332898d553d3fd709638476a9e75515c4da" + }, + { + "id": "286ee76f-d4e7-51fd-8e15-b6dcd0a6b372", + "version": "MTc1NjMyODAwOTAwNDE1NTAwMA", + "digest": "sha256:5f6ecdf9bd35eb6b41a7faa94967b99757b0907b60423533233f93e3e94c7094" + }, + { + "id": "288f9262-0deb-5534-9d18-4b13cc484346", + "version": "MTc1NjMyODAyNjgwNDIzNTAwMA", + "digest": "sha256:b91704b9579e1e330660c7562afebba3b7a5dd6228a59149899e3bb792a5d0f8" + }, + { + "id": "2899b8ca-ce50-57e9-8d1d-38c6aa21d94f", + "version": "MTc1NjMyODA0MjU1MDQwNTAwMA", + "digest": "sha256:40f40e497c57ad8d44d5fddaebd851afa0a60b6064810ab2bc5a86d535cfe582" + }, + { + "id": "289f4fe8-9661-58fb-a18d-2b012abd1c62", + "version": "MTc1NjMyODA0MTk3OTgzNDAwMA", + "digest": "sha256:e58a89e50a90d532f718e63b7391624a2cb51f90aecdcc9df9e9f424235b950a" + }, + { + "id": "28af5dda-d26a-5594-a60c-39b26d384799", + "version": "MTc1NjMyODE2MzM4NzkzNDAwMA", + "digest": "sha256:d02180bdb6fa9eb1518f2cb69cbefc23f3290151a728b0ce5a3d914bdd8df5a7" + }, + { + "id": "28b529bf-3d6b-5679-b04a-6edd73ac8047", + "version": "MTc1NjMyODA5NDQxMDU3MTAwMA", + "digest": "sha256:c73b459a8bd55ecbf87a7679757a08ddb817484c4a965630bc097dacc52dc1d7" + }, + { + "id": "28d11bfe-568b-5f42-8bd1-82296e10151d", + "version": "MTc1NjMyNzk5ODIyNTY0MzAwMA", + "digest": "sha256:848dc30dcbfe46fa31ebdc6fdaeb978dca73e9754d9173d1c53558df1b266b1c" + }, + { + "id": "28ee3d96-0889-5202-8703-f78c208f8a00", + "version": "MTc1NjMyODEwODg3MTUzODAwMA", + "digest": "sha256:0239868c38b010ed40b37201cdc0cb92dc51bcb654ef4bea21e01d97ea24fcf5" + }, + { + "id": "29343ad9-af47-540f-b6f2-9ec807536d95", + "version": "MTc1NjMyODA3MjU3MTMyMTAwMA", + "digest": "sha256:3535144d79ff3dba90520a8bbd0bcba33203e1901232f4b09387769ce4976196" + }, + { + "id": "295522a2-b86d-514e-8c61-e5831a92fbd8", + "version": "MTc1NjMyODAyMDU0Njk1MTAwMA", + "digest": "sha256:69c5604214b15ae7e04b19b3fc266bc87edff41454c1adf4005904d937c0d904" + }, + { + "id": "29587abb-ae3e-5e4e-8157-9b201c5ffeaf", + "version": "MTc1NjMyODE1NDYyMzY0MzAwMA", + "digest": "sha256:cefe214f70b230fffc4fb5fe0df6d375a4aaff696bb10f196b7309a55dbe40c3" + }, + { + "id": "29633d43-07bb-519b-b0ee-4acf00b4e9fd", + "version": "MTc1NjMyODEwOTIzODY4OTAwMA", + "digest": "sha256:c2967852095510f0d014c46a7ae6647aa21c60af925c1f3911d2db4f9e5059cd" + }, + { + "id": "296464a7-7df6-5446-91a4-75ea0b14e18e", + "version": "MTc1NjMyODA4ODQ5Mzk0NjAwMA", + "digest": "sha256:87ab2c27c3da07b7adb9b8be1bb733773a14a74c2858e2b2dd4fef78cbf142a4" + }, + { + "id": "296c089b-341d-522e-98c2-a8961fe4139b", + "version": "MTc1NjMyODAzNDUwNzQ1ODAwMA", + "digest": "sha256:6aeee375344fa58ce11b5d5a298c8b5b7d8701a95d4d654f129afafe1c843e7f" + }, + { + "id": "296f6806-69ee-5d77-83dd-0fd0c6dc22b2", + "version": "MTc1NjMyODAwNTA5NzY4MDAwMA", + "digest": "sha256:f779bc35b2c3963988477fa57912ed7cab9e2837cdbb7c14afa633dcd9b2751b" + }, + { + "id": "29783ec6-c654-5e4c-ba2d-977f1e4fdbaa", + "version": "MTc1NjMyODAxMTEwNDI1OTAwMA", + "digest": "sha256:889529cd1e27a2cb8e36aff0b31c9c1f7419a494d7d52c7aedda53f1501920b5" + }, + { + "id": "2984ea8f-53b4-58de-b25b-d47c0c3b5223", + "version": "MTc1NjMyODA3Mjk4ODg2NDAwMA", + "digest": "sha256:b59d22c2f9208242042ea8dc995271d9b3377062c396abc9a58409f251aea7f5" + }, + { + "id": "298ffac3-7551-59e3-83f0-2616e72f1ba8", + "version": "MTc1NjMyODAyMDUyNDE4MDAwMA", + "digest": "sha256:42ee120576b2a206735d058fe8504981664d4cdc090be1c4aa1c8506addbff3c" + }, + { + "id": "299e063e-02e2-5339-88d7-877073d6539a", + "version": "MTc1NjMyODEyMDM2NDEzMjAwMA", + "digest": "sha256:f3e075102ad892656fafa6e3d90b20a5dc72d66d3a096419df7e13461d699f9d" + }, + { + "id": "29cfcdfe-608e-590e-a68a-fe15fe0156cc", + "version": "MTc1NjMyODA1MjcwNDc2MTAwMA", + "digest": "sha256:a2b1bc1e5171473f4418af2ef5ed8d3067d288c67fcb2d26bbab7437aef7ba3f" + }, + { + "id": "2a1f7342-77be-592a-bcfe-dd75efb25204", + "version": "MTc1NjMyODA3MjU2NjM5MzAwMA", + "digest": "sha256:4fc2cf2fa1ef3d93865084746f995a762172854b10549c5801af382c1c4ecca3" + }, + { + "id": "2a9b4b1a-d63f-5c72-9096-8dc5f76d2759", + "version": "MTc1NjMyODAxNjUxMDA2NjAwMA", + "digest": "sha256:43d541d6d5deb7097b89a77c0d31e55791b5d6935558efa01243c8cf5e7eddaf" + }, + { + "id": "2aadc346-a4d5-5ded-8b82-b4188c890cfb", + "version": "MTc1NjMyNzk5MDQwNjU2MTAwMA", + "digest": "sha256:e02f6a824e1cd1f3d141d81c363f692ff21e0ba9a37d43fa7f2763c69dd8170e" + }, + { + "id": "2ab039fa-2882-5dcc-ae36-5d36a5f3ff99", + "version": "MTc1NjMyNzk5NjY5NTg5OTAwMA", + "digest": "sha256:e38ff6e832844ab76be2483bbcca238996efe2d33dda91b794208dfab02e4d68" + }, + { + "id": "2ac316b1-ebf4-5580-9ea5-d2e21462716e", + "version": "MTc1NjMyODExMzUwMDE4MDAwMA", + "digest": "sha256:eb9a83e419a4c095371f9adc9b0e68b77cfaf698cb501c60061deeacd8d83987" + }, + { + "id": "2ae8004b-94bb-5684-86fe-8b199077a546", + "version": "MTc1NjMyODA5Njk0NDYxNDAwMA", + "digest": "sha256:bcd81c882cc021c3ecac86491b26c50b7cc6e2e7148f18493c72af7492460cd2" + }, + { + "id": "2b4534b3-0bb3-51a5-b12e-d1041a49e381", + "version": "MTc1NjMyODE1OTA2ODEzMDAwMA", + "digest": "sha256:47688ac0519447d6d09f9e5dc8c2358904e88a52c243352c2ddd2efce9a64909" + }, + { + "id": "2b70e946-81c2-5f80-94a6-1b53029f8e25", + "version": "MTc1NjMyODAyMzQxODQ3MDAwMA", + "digest": "sha256:9de35c086133afb9e5609e6e5eb83d3d9674c0c4760fc8396b5e1084148eff7e" + }, + { + "id": "2b83c328-977c-5bb7-b965-03a6effebcf9", + "version": "MTc1NjMyODE2ODk4MjIxMTAwMA", + "digest": "sha256:b10d4871d19b6d019b7e30fb591eb9909933e94c355853e571d8c94d3aba8dae" + }, + { + "id": "2b943265-c27e-551a-8552-17773c73bd4e", + "version": "MTc1NjMyODAzNDc2MjgzMDAwMA", + "digest": "sha256:5b5fe11bcf6ce2ae52d97bd688876d3ada12f1fdca7d5662cfdfa405baaf4317" + }, + { + "id": "2b9be7da-ceaf-5ccd-9ea6-c8ca6fbc8764", + "version": "MTc1NjMyODE1NjQxMTY1NjAwMA", + "digest": "sha256:eac3a8053d3590102179b59a46209963ade8809a8abc866f403a5df3e9d1b58c" + }, + { + "id": "2b9c07d3-fca3-57a0-ad00-a53850aacde3", + "version": "MTc1NjMyODAwMzk2MjA5NzAwMA", + "digest": "sha256:166480a545d71dede0d0aaac143d09cce39cd79878205bcb160a72ab4c469133" + }, + { + "id": "2baf6668-5378-568c-bde4-144233e26bc2", + "version": "MTc1NjMyODEyNDUxMjI0NzAwMA", + "digest": "sha256:747fa9b9f73969106dfd91916191185f75e235955641d45df6eab0b6ef997855" + }, + { + "id": "2bd5bcaa-c457-507b-939e-e3fb336e34e3", + "version": "MTc1NjMyODE0Nzg0NDcyNTAwMA", + "digest": "sha256:daa50d4fc7faacd7cbe69f9463e8c85024ccdf00dd797b72c10e02ef89eabc34" + }, + { + "id": "2bd712eb-63cd-5dec-8df7-d58d86e3268a", + "version": "MTc1NjMyODAzNTk3OTgyMDAwMA", + "digest": "sha256:69f3e786a5b4f7a8e3a56d9ee86eb3c0d4404eb609fe75a9213519034b9fd0b5" + }, + { + "id": "2bdb7b65-65bc-5a82-9fda-05f349ed5129", + "version": "MTc1NjMyODAzODYyNzgwNDAwMA", + "digest": "sha256:9059ad9f1661e7eb5e874d308411a8fa023360cba6e7e67a052536bf91bb5137" + }, + { + "id": "2be69b51-48df-5837-9ea8-e8a7d9c341fa", + "version": "MTc1NjMyODEwODg3NjI1MTAwMA", + "digest": "sha256:1def8b14524ed154b0ad05ed6b0fa635a728d1c007bda0576386e820f981ec5a" + }, + { + "id": "2bfdf7a8-0a92-511d-9302-cb8945d3afa0", + "version": "MTc1NjMyODA3NDM4MDA3NTAwMA", + "digest": "sha256:edde95e10ee658f2791d86b273146fb6a0ff2f0036a805d7d4009010420c6807" + }, + { + "id": "2bfe0c80-20d3-5a9a-b59b-fd0e30e0f6a6", + "version": "MTc1NjMyODE2MjMyOTY2NzAwMA", + "digest": "sha256:6b61a6dd68ec4f7c311a24a88da19664e00bf4797636cf9419b1c6fa7ce10b1d" + }, + { + "id": "2c103564-703b-5454-8720-743233f1a8a2", + "version": "MTc1NjMyODAwMzkxMDE1MzAwMA", + "digest": "sha256:38cde0a9862c909d241c8b261a05d022426e20f0a2cf5569d3242136d28dc780" + }, + { + "id": "2c3e986c-af75-5207-91e3-b357404a2303", + "version": "MTc1NjMyODE2Njg2MzkzOTAwMA", + "digest": "sha256:12df30eac51cb2032e89ef75cf64bf88ae31371eb52bfe203cdf50113e874aae" + }, + { + "id": "2c535a3d-402d-54c7-99b8-c657f3db1e3c", + "version": "MTc1NjMyODE3NDcyMDAxNjAwMA", + "digest": "sha256:b913d8d4eb197f6fd250db009b1f339193a1391ca3e96a0176ecf9e2c9d0281f" + }, + { + "id": "2c623b0a-ccb1-5fd6-af48-13e66a02434f", + "version": "MTc1NjMyODEzMTEwNjI4NDAwMA", + "digest": "sha256:4cfae4942140cf050b80cffa1ac358f004cab24ebf4cab1cfd857efaa7028120" + }, + { + "id": "2c8164bb-d8dd-5842-b749-aca07fa18177", + "version": "MTc1NjMyODExMzQ1NTEzMzAwMA", + "digest": "sha256:316f9b31021440b7efaf46406e100db664b3fc40f7c57ca7e3aa96ea7fb30258" + }, + { + "id": "2c841781-c514-532a-955e-2e5e583676e8", + "version": "MTc1NjMyODE3NDgwNjUyODAwMA", + "digest": "sha256:3c0ad09faccb857ac0e20b556c2d7491a11d3c96a4259049996345fa7f32712d" + }, + { + "id": "2c8b99e3-1ba6-5266-b1e6-e055ae1441b3", + "version": "MTc1NjMyODE2MjI1NDAwMzAwMA", + "digest": "sha256:35892cd68017d565525dee1b66ff6ba33460510715c5629d91925c9a4385c8eb" + }, + { + "id": "2c918d7d-e44c-54ec-b07c-7a26856c57f4", + "version": "MTc1NjMyODAxNTI0NTg0MjAwMA", + "digest": "sha256:3a6a6df7b5c7a28df47d102ab8e07542c1af22d9ef42660e33d55c2b9cfadfa0" + }, + { + "id": "2ca0ac96-0cc1-534a-b65c-53c6fa9cf1a3", + "version": "MTc1NjMyODAxODg2NDU3MjAwMA", + "digest": "sha256:1657c6c84d34a66dc358341ee9a0f226e0a12905d154076c1fa06ec83d46fa89" + }, + { + "id": "2ca68bef-35c0-56ab-9b47-bba8ef7e4db1", + "version": "MTc1NjMyODAwNTE0MzYxMjAwMA", + "digest": "sha256:34d864b9a6d734d7bbe77ac44f8c8a8efdfa5187758b54a0695d213915cb50ec" + }, + { + "id": "2cae6f0a-ba2f-5417-8104-0eb4778ffd03", + "version": "MTc1NjMyODA2MjE5Mzg5NzAwMA", + "digest": "sha256:81075709eb0c7311ef27db237dc9c0bafd17902afa92af7e8eee62255b97d7cc" + }, + { + "id": "2cd747af-48bb-592d-bfdd-ed40ea8da91d", + "version": "MTc1NjMyODA2MjA4MTI1NDAwMA", + "digest": "sha256:878242dc38771f83576034fa2fa329c85d68d1680d79860defafbfcc8542535c" + }, + { + "id": "2ce6da2d-1ae2-5846-b386-f918ff37ed29", + "version": "MTc1NjMyODA0NTg3NzczNTAwMA", + "digest": "sha256:100e4514fd745171c089be43bd0583c058a81eb06cbe7f9b311c7c46b312ab16" + }, + { + "id": "2cfb6c0a-02a0-5a16-a961-42484699f018", + "version": "MTc1NjMyODAxNjQ2MDQ1NTAwMA", + "digest": "sha256:60cf8baf11d9a4688444927d8d87a9240110da691f139e85f6e0bed82016704f" + }, + { + "id": "2d41e4f9-2acb-5a48-ad09-5b4d9b842138", + "version": "MTc1NjMyODExOTE4OTE2ODAwMA", + "digest": "sha256:5c704b432bad71cd13b81cefa7fd8410cbe883d8bd30e4896ab530f0a528af8f" + }, + { + "id": "2d5719b6-641d-5045-9504-ee8a52420da8", + "version": "MTc1NjMyODEzMTEzNDAyNDAwMA", + "digest": "sha256:858fb5553980a9f4534ee3f9bece04c3ed1d82d785eb5646f0a4a6ed927e6327" + }, + { + "id": "2d603363-1649-5dcc-ba33-4c1fb5ee17b0", + "version": "MTc1NjMyODE1NTE2OTA4MDAwMA", + "digest": "sha256:6e990a3df20abe2a0818d9639039ab208081b5d9bc35f872e7796a130e8a2c84" + }, + { + "id": "2d862d7c-cc50-5c1a-b70a-53ecd5630f6f", + "version": "MTc1NjMyODExOTI0OTU2ODAwMA", + "digest": "sha256:54838f41939119fe80215cd58829aefee73d51bcc35c3c19a2f364fe3cb92874" + }, + { + "id": "2d93fde3-7b40-5751-b4de-92d9ad9a3571", + "version": "MTc1NjMyODAxNTMyMzcyMDAwMA", + "digest": "sha256:c9b249f50977bf67dd0cfd5fe358e13d45e16147b35fe793a8ad6bcba0e10fb5" + }, + { + "id": "2d978f4a-0cde-524c-91f5-22ce1f6db9f1", + "version": "MTc1NjMyODAxOTM3NDA5MjAwMA", + "digest": "sha256:a169f94c0c4c0a5c0f162eef85c48a9059619d52dc635f50fa6cc38e3b6f3206" + }, + { + "id": "2db825be-7e01-5003-be79-5109f625cd79", + "version": "MTc1NjMyODA4OTIwODAyMDAwMA", + "digest": "sha256:7a13d471d38df6fcb7f3e5a0a5c67835240cee5887bb644a182330411c8174eb" + }, + { + "id": "2db93a0a-863b-526a-9637-a87b462aa447", + "version": "MTc1NjMyODEzMTA1MjM0MjAwMA", + "digest": "sha256:357f51fbae9b0ba8682ac2bbb5f465d86623d1d3ba9707f741451c6bdda77b39" + }, + { + "id": "2de13a5a-92e3-5e20-af05-37ae4a2060f2", + "version": "MTc1NjMyODEwMTA3MDQ1OTAwMA", + "digest": "sha256:d35822e4e02d8d7179eaf4385c8e67897466d127268a7e705dcec6ec423ba64b" + }, + { + "id": "2dfe1464-22cc-5376-ab35-e75c25dbcc59", + "version": "MTc1NjMyODAwNDAyMTg2MDAwMA", + "digest": "sha256:a1af85164c34ea4bf27f7d6f2890f87ebd86a6d764be5f6a89c4cb92b911a4c3" + }, + { + "id": "2e1df0c2-eb31-5364-983b-962fe84b0004", + "version": "MTc1NjMyODAyNzc4NDMxNjAwMA", + "digest": "sha256:ffbe04fef103a08316e1c09fbd6381b4b469196e1f5ccbb8bab929691635f4ba" + }, + { + "id": "2e337ed7-fd40-5669-854c-6134ce654922", + "version": "MTc1NjMyODA5NzA0NTI4NTAwMA", + "digest": "sha256:1f7bba2dd75059043017abe6dfdcb42240c30ec66a71fc7d86f0fbeeb87f2a90" + }, + { + "id": "2e3ef8fd-9db4-5c5b-b9f9-9e057ec1dd27", + "version": "MTc1NjMyNzk5NTM0NjQ5NDAwMA", + "digest": "sha256:5195d633bf18a13f78df7b942ea175a18da2a27168a01eb025f05a9544b8dd9e" + }, + { + "id": "2e54268f-fe8f-56cc-b623-2b0186a71ed9", + "version": "MTc1NjMyODAyNzQwOTE4ODAwMA", + "digest": "sha256:95352999c7cac6e8ec510291a162d315269788edb600de53a45d8e6227b65ba3" + }, + { + "id": "2e625662-23b9-5019-935b-c8e4104ebac9", + "version": "MTc1NjMyODE2ODg2MDY4NTAwMA", + "digest": "sha256:b2572b3b970783ccecf7f8b3c9606e9df60c6324290e2b44e8201db427e7683d" + }, + { + "id": "2e8c8c50-d8e6-5d3f-b1a6-ea86342f3e22", + "version": "MTc1NjMyODAyMjA4ODUyNjAwMA", + "digest": "sha256:1c638bde5447e544aecb61c774f45c0c5ffe15a645b09734a3a8014e300f04d9" + }, + { + "id": "2ea2b47d-fa76-5463-a7f5-d249f4d41df5", + "version": "MTc1NjMyODA3NDE3NDkwNzAwMA", + "digest": "sha256:7a2012b625d13e428b2209222643835c2dec5249aac161a84c5426dcdad88aa6" + }, + { + "id": "2eacb173-dc77-5e8e-8784-6ecbfc2ed16c", + "version": "MTc1NjMyODEzNzQ0MDk3NDAwMA", + "digest": "sha256:3fe7923e7ca3bb634d8b5507cde534fa213439d9df7a2c5b539ae4824a8359cf" + }, + { + "id": "2ec59fe0-ff3f-5ae2-a50b-1c2bbc9e60f3", + "version": "MTc1NjMyODE3MDk1Nzc4NjAwMA", + "digest": "sha256:ac4563c948dc05aa4e0f00d87745c126abfb9d55a4735185bf2aa246011c278c" + }, + { + "id": "2ed60c12-816e-5c0c-87b4-73071f05f49d", + "version": "MTc1NjMyODA4ODQ4OTIzOTAwMA", + "digest": "sha256:99fe30a5a4a2bb50d69a95ec6f3ba615996bff07059ca1e20e76fbe429ddc92e" + }, + { + "id": "2eea57cf-f6fd-5a36-948d-992a747553a9", + "version": "MTc1NjMyODEyNjU4NTAzODAwMA", + "digest": "sha256:6a0986624816f9b239560552dec01c55dc99b47d0134f1bf56f31b0479ebd4c7" + }, + { + "id": "2efeb2d4-075f-5ce3-89fe-3b917769cc18", + "version": "MTc1NjMyODAxNTQ3NjQ4NTAwMA", + "digest": "sha256:83e053ec997d4103efeecc947814f492eae74a88a144b0beff6162e52ad57235" + }, + { + "id": "2f045997-95fc-5021-82af-ad934f1b56d1", + "version": "MTc1NjMyODAyNTU5NjE2NTAwMA", + "digest": "sha256:4e673d835e25314f73e696dc1a34c204114d230ab1c50dad03447095434c373d" + }, + { + "id": "2f0e4d8f-f6fc-55d9-9693-174fdcd05a2d", + "version": "MTc1NjMyODA0NTcyOTQzMDAwMA", + "digest": "sha256:4132f0cb75b941a6fb48c87f3727777d7491dc8dc767d22730ce2c2a52deb166" + }, + { + "id": "2f4e52af-a41b-56b7-ab01-63c550234809", + "version": "MTc1NjMyODAyMjExNjY2MDAwMA", + "digest": "sha256:d46b3bd6fd2572838c59db46ac5ba98771ae0c1c5afba621a6784adfcddf2040" + }, + { + "id": "2f7aa045-03fc-552e-a6c6-79d294c9c3d1", + "version": "MTc1NjMyODAyMjA5NjMxMDAwMA", + "digest": "sha256:6322b9e3ba2e044e4deb9ea40ea17ab1bd02754aa15cfc79b11883cae32fe250" + }, + { + "id": "2f7bbf6f-8045-5f8d-8dd0-2909e4eca317", + "version": "MTc1NjMyODAyMzM5Mzk1OTAwMA", + "digest": "sha256:3347a47b93763dbade11f9b7f904b20306b8727a7305d4eddf0cbcbc067a958b" + }, + { + "id": "2f948515-4c4c-5310-9ca9-c31224fc3db3", + "version": "MTc1NjMyODAwMzkzOTYwOTAwMA", + "digest": "sha256:bea5aacbfe537367ac36239451543e6de4760dbf7dbd0309b7d00978d0bd483b" + }, + { + "id": "2f9e8f99-9c69-547c-b7cc-1e2dd6cef0c9", + "version": "MTc1NjMyODAzNDcyNjIwNDAwMA", + "digest": "sha256:df4a725cf336dc729a4331b174e5ad1c198461006925ce3d4e19aa78177825bb" + }, + { + "id": "2fa74a0e-8b97-5f1d-ad48-4d3182eca102", + "version": "MTc1NjMyNzk5NTMzNzU1NjAwMA", + "digest": "sha256:12e30522dfc2bba9d28bb21877bf60be27c63567560b419971c67145be55a918" + }, + { + "id": "2fbe6fd1-12c1-5cb8-a7ef-2f74ad9b90a8", + "version": "MTc1NjMyODA3NDI2ODYyMDAwMA", + "digest": "sha256:c561019ade874a37e0874235fb4ac91c6e4d23e3fbe067df1ed4ea5882a30e5a" + }, + { + "id": "2fc3aec2-ea8a-5b8f-b9e7-29b3234b6a59", + "version": "MTc1NjMyODAxOTMxNjEyMzAwMA", + "digest": "sha256:a702a85e4aabb10e91d1ccdf85a4e5500c9e9dd6be906f02488dfa6bbeebb22d" + }, + { + "id": "2fdfe48b-536a-5e4e-9e3b-15808dd57877", + "version": "MTc1NjMyNzk5NTI5OTAxOTAwMA", + "digest": "sha256:8d131723ed1ec7c17f9f2047a38f2ab692e0885ea4006c5fb4404cbdbd225fa1" + }, + { + "id": "2fe3c3fc-7ed2-5b2a-8a3d-255e9ed64573", + "version": "MTc1NjMyODAzNDY5OTI5MDAwMA", + "digest": "sha256:07864e34dffaf02e5de0129fdbb60dc68ba1b3fc2a2c7083d21f0a51bff4cb0b" + }, + { + "id": "2feadea4-5e63-5a6a-becc-69777b82d386", + "version": "MTc1NjMyODEzNjAzMTM4NDAwMA", + "digest": "sha256:630276b154c3b4bf225e7ac077fa985f77f54a1132dc5e3a743919558042f328" + }, + { + "id": "2ff7c83b-fb9c-51c9-ae87-c660e14b18a5", + "version": "MTc1NjMyODA2MjExNzgwNDAwMA", + "digest": "sha256:b18164fb8ea9b04548643b4f8318c1d420ee253ae2455a2f642a1f1b5794f8f9" + }, + { + "id": "302e19be-4d0c-5ed0-a368-ebc5c813de9d", + "version": "MTc1NjMyODAzMDcyNTMwMDAwMA", + "digest": "sha256:24f5798f25990d554e8e0192090eaa56bb37cdb69303506d483d1e99300e3732" + }, + { + "id": "302f06dc-e9dc-542a-8604-435284bb2c82", + "version": "MTc1NjMyODA1MTMxMDYzNzAwMA", + "digest": "sha256:8534d04e92c4ec362c6c7288169d50cfc3bf0875f4f7b0f05b916f1475a9d5b7" + }, + { + "id": "305b775a-884c-525a-9d8e-955f4b7bd29f", + "version": "MTc1NjMyODE1NDUzODQ2MDAwMA", + "digest": "sha256:6d2ee482d607f9e3b91048a3f431754f1a76659e01a2e85e19946d114e296a2f" + }, + { + "id": "306fcea5-2d35-5e03-a5c7-f8c113595e08", + "version": "MTc1NjMyODA1NDA2MTQ0MjAwMA", + "digest": "sha256:6a66a4d524f8293f9e3c8379ad4191085979211010022bdf1a6c6ee8ecdb446a" + }, + { + "id": "307539ca-dac7-5e09-8367-8da9c92fcc1d", + "version": "MTc1NjMyODEzNzUyNzc2NDAwMA", + "digest": "sha256:32f7908ffaac387a7cbf284c02551ce80d8edb54919b9721de55e62eec66d439" + }, + { + "id": "30817636-be11-5298-a0fe-848d6d2c7783", + "version": "MTc1NjMyODEzNjA3NzUyODAwMA", + "digest": "sha256:46e439aea13b04395ba6f6847a87f3c52f181673a09325775e9cbdbe2b994b09" + }, + { + "id": "30968cf5-60a6-5728-8574-c3fd20e6cc2f", + "version": "MTc1NjMyODA2MjE5ODQxMzAwMA", + "digest": "sha256:b5d05ea6d3f58b028d4bf0090e287484d8649a29784f440781514a1d943c20e9" + }, + { + "id": "30bfef1a-5130-5c42-8ff7-d5a595b6bef8", + "version": "MTc1NjMyODE0NzQyODQ1MjAwMA", + "digest": "sha256:b3d2443f4cf21bdf31064add1c6dcb06632fa3b03d339b05cd7249d11e37562f" + }, + { + "id": "30cad999-524f-5d21-aa9a-3e86e8fea645", + "version": "MTc1NjMyODAwNDA0MzMyMDAwMA", + "digest": "sha256:60e06cba72615c322aa4cd824db470c4b247b6092231870ecd5ae1d8f61e85f4" + }, + { + "id": "30d7c33b-8dc5-5b8e-b88e-8e3571f41c90", + "version": "MTc1NjMyODA3NDM3MjcyOTAwMA", + "digest": "sha256:cf1ebd39552425fa8d6ef9564718ac2971303f85e298a8c0d9c2a387c3cb8fff" + }, + { + "id": "30deffaa-06ec-55e1-977b-1ea9e2e26e63", + "version": "MTc1NjMyODAyNjg2Njc0NTAwMA", + "digest": "sha256:9d6faeba831519b2e6032adad80f2efaff572353a5fe8f69eae4b72d785d57f9" + }, + { + "id": "30e01a6a-6e10-5cdf-8dad-9e1b8968d923", + "version": "MTc1NjMyODEyMjIwNTM2NDAwMA", + "digest": "sha256:fb7723f40a70c94aedc38c0ec6f051ce1dd78ea42f349a9f1ac38ef03e2da344" + }, + { + "id": "30f378eb-388c-57f7-97a6-ab8de806f13f", + "version": "MTc1NjMyODExOTE2NjEzNzAwMA", + "digest": "sha256:885ba43dcc0bfe0512c4ce01475089064884f967258425e2b3e925f200f87f2b" + }, + { + "id": "3105ee60-95ab-5938-a735-1508e69e05a1", + "version": "MTc1NjMyNzk4ODY1NDY0NjAwMA", + "digest": "sha256:7f4286093177be863bb5d86a3899b44bbc0895eaf45e1032e0d6ec634bbed8ea" + }, + { + "id": "31329e9e-7fcf-5edd-b5e1-a1b6733d4540", + "version": "MTc1NjMyODA4ODQ4Mjg0OTAwMA", + "digest": "sha256:caeb95da5b46a5fa5acc65ce9b85d58536cd71407e710a9772afc205475fc433" + }, + { + "id": "314a9399-5ef8-5d2d-8e8d-8dda4821c06c", + "version": "MTc1NjMyODAwMzkxNjU3NzAwMA", + "digest": "sha256:a910d4ca2dc260e110903866da3acc44b37389545bfa0212c78a49f4f09fcb45" + }, + { + "id": "317bc962-2b45-53ac-9c67-bedbef2de6ca", + "version": "MTc1NjMyODAwNjIxMzM2MjAwMA", + "digest": "sha256:cc9550ce2042d40482200ff2f5cd2d5b60d5a230683797b7d0cca3d69d07757e" + }, + { + "id": "317fe8b4-0b7d-5c40-8190-0bf19245eefb", + "version": "MTc1NjMyODAxNTI4ODkyNjAwMA", + "digest": "sha256:bd3cd873bb4bc5d1cf1c2f97c0759eea3ad85bed3172cc6b97373e1e36e3b40f" + }, + { + "id": "319630f2-0835-574c-98ef-c1c3de5a4bc0", + "version": "MTc1NjMyODE2NzQzNjM4NzAwMA", + "digest": "sha256:55f006b29768a206dc5e4abc094afb42e8b2d8ba7699d0864f58b060d41998a8" + }, + { + "id": "320286f4-c4ed-58b6-b7ec-f16e85ef6c0c", + "version": "MTc1NjMyODA1MjYxNDcyNzAwMA", + "digest": "sha256:6e986c19a781c641af9c3b9088ca7c6c1278b9dc4aa65a5491e4e528ef02332c" + }, + { + "id": "320c0297-2c87-53f0-93df-e0daf98ce495", + "version": "MTc1NjMyODA1NDEzNjA0MjAwMA", + "digest": "sha256:7d6218b2ffc137f1c940f6664ea5f84500b5eb717c62756747e43aa32ef5e1f8" + }, + { + "id": "321c42fc-8d8f-5a91-96c5-a00b8ccd4b0c", + "version": "MTc1NjMyODAzOTY3Mzk2NDAwMA", + "digest": "sha256:58edc16896c793d511065d34a13ed03dd0fd5463b568cc8dbccb1c741ef252b6" + }, + { + "id": "32505b09-1de1-5b10-b878-2b783e92f293", + "version": "MTc1NjMyODAxOTM2OTA0ODAwMA", + "digest": "sha256:e8bad18c6787d21d907c88d89210f976e0f74ef6bd5a7f1f8bb9dbca19b41804" + }, + { + "id": "3253d209-6820-5c04-86ab-4f44c9637454", + "version": "MTc1NjMyODA5ODY0NTkzNjAwMA", + "digest": "sha256:294499df70a4eb88cf6379c3c3a430321a79a197f41a1c8cbb98f383d8f610a4" + }, + { + "id": "325acfa7-d29b-555e-b472-1623ac6f402b", + "version": "MTc1NjMyODE2MDU3MjM3NjAwMA", + "digest": "sha256:65bd1145bb3dbf5ba62979c8ada2cbe923ba71b39cb9af11b65165492213375d" + }, + { + "id": "325c1aff-0f4e-53da-8284-b0663ec30e62", + "version": "MTc1NjMyODExMzU2MzAwMjAwMA", + "digest": "sha256:9f31b47781a8ac2dfd3d97ab4f3bf0a662d749178fd3e47125e52627dfe827b8" + }, + { + "id": "328361f2-795f-5ca4-8783-5e2647a60597", + "version": "MTc1NjMyODEzNjE5MTA4MjAwMA", + "digest": "sha256:29ebab260f8e22277b4b69bdc9e9526c40daaef5415e089b31d042f2b9caa02e" + }, + { + "id": "3290e555-eb8e-5531-a974-440591674f82", + "version": "MTc1NjMyODAzNTkwMzQwOTAwMA", + "digest": "sha256:86828877e2103e8c20cc45bce3ac80cbfb42e46eb2dc7258ec92c001584c817a" + }, + { + "id": "32a26329-c05b-5513-ad99-897e45b48e32", + "version": "MTc1NjMyODA4ODU5NDIwMTAwMA", + "digest": "sha256:1f47d4f450a2b322aafe59883f517a9ee1fc2402ecf0962c5aa4f7d4081d1ddc" + }, + { + "id": "32e45375-866e-588e-9bf0-6943a8562bd6", + "version": "MTc1NjMyODE3MjQ0ODMyNTAwMA", + "digest": "sha256:d7401a1105051bf7b2d65d4a7890f08ec79af8f8a32150fc0994841bcf880b09" + }, + { + "id": "32e93c3f-e6e6-526b-a091-edeed5c9f900", + "version": "MTc1NjMyODAyNjg1MzU3MTAwMA", + "digest": "sha256:24b8adce1bc2c9cfb70cdb99004ee2b6058b6a6b9770ab9f5a54013a0c3cea9b" + }, + { + "id": "32ed1555-7c31-5278-a5a7-9a6038eeb260", + "version": "MTc1NjMyODE2MjMyMDUyNTAwMA", + "digest": "sha256:af73543928d22e420f3d508754d14a1b103ef15f7e7577f4c20291d9e6408268" + }, + { + "id": "334535c0-6b98-53b4-8327-73785fa91d04", + "version": "MTc1NjMyODE1NjU0MDc3NjAwMA", + "digest": "sha256:6eca03f5be73cf17b5fc9df5093eaef3fa3bedf5a6257649662d06d2d15860fe" + }, + { + "id": "33486adf-1af4-5f2e-be7a-9b7c05f6c4ae", + "version": "MTc1NjMyODAzNTk5MjkzNTAwMA", + "digest": "sha256:1f72f249f85291b9cccc5caaf7029ddf7fe981981732d746f62e61aa4e7bc0d1" + }, + { + "id": "335fa8d5-046c-5b3d-9a1f-a9eb57b1bf6c", + "version": "MTc1NjMyNzk5NTM3NjMzNzAwMA", + "digest": "sha256:ccf8073abf6a40b13d43f8554858a6adcc3ab5713dd847831065866855b7af2b" + }, + { + "id": "338e8e55-1e38-5283-b105-9b3be59ac02e", + "version": "MTc1NjMyODA4OTIxOTE2MzAwMA", + "digest": "sha256:d38cd3371e8d6e4b06078c34aaa37ca4c28bf3eeb927720a72fb3a27bd32ee2a" + }, + { + "id": "3398e337-7bed-5e0e-901f-51b77b0a5c72", + "version": "MTc1NjMyODAxMDQyNzMzODAwMA", + "digest": "sha256:f066052b6bf9f0848b6b676f1955be89449a4621bc6e51a7d04072d5a5977a97" + }, + { + "id": "33aefb89-aed2-5a37-8e96-601a7fd17a6d", + "version": "MTc1NjMyODAxMTU1NzYwODAwMA", + "digest": "sha256:e9db870293b2a8adb5e4672aaa49a8d48ceb7709d05b6bd1adfcc682a73f07c5" + }, + { + "id": "33b3a407-db1b-5052-8adf-e8c9261ec8f4", + "version": "MTc1NjMyODA2Njk4NjU3MTAwMA", + "digest": "sha256:a0cf25d7b82206f216051105cc6ae07398abf824a8cb7afecc9d25ad5535f62d" + }, + { + "id": "33be0092-d085-5bc0-9d25-f68c61952433", + "version": "MTc1NjMyODA1MTMwMTUyMzAwMA", + "digest": "sha256:0b1164a397b7270adcf10a6697c56dc1b19727479236bbd437a26f22884ad619" + }, + { + "id": "33c961c4-3015-5bb2-85e0-14f32fdc1bfa", + "version": "MTc1NjMyODAzODcxNjk2MzAwMA", + "digest": "sha256:c06622ad058bf98340de099b20c8925cf38808c134b4da4cf68d64e6928d36d6" + }, + { + "id": "33cbc131-fcb3-5c76-9e68-76551b53f21c", + "version": "MTc1NjMyODAzOTY4NjM0MTAwMA", + "digest": "sha256:26ec3b6aa728742e92b568b021770f5106c22b2741ad1e524c2685e7db189726" + }, + { + "id": "33cfc7f8-4fa7-59e9-99b6-64f429332e26", + "version": "MTc1NjMyODAxODgzNjgyMzAwMA", + "digest": "sha256:6324a1b205c3d193f8b5c027aab64baccc3ebbcd6f2771a3fddcec7503ee70c9" + }, + { + "id": "33dfb4e1-b55b-5aa3-ae9c-6236e5a0c6a5", + "version": "MTc1NjMyODEwOTIxOTMwMzAwMA", + "digest": "sha256:99fd13e7adcf227cab552db0dabe8e8c41024b6901d794b0650519d930adde32" + }, + { + "id": "33eba151-12d8-55c0-91e2-7d96acadedb8", + "version": "MTc1NjMyODAyNjg4NjgxNzAwMA", + "digest": "sha256:c4d9511a81d8417817f8468ab2433db68d882ef43445bafd8f7e25aca521952f" + }, + { + "id": "33f889a4-1c2b-5ec7-a37b-ed6d1aa9140a", + "version": "MTc1NjMyODA5NzY1NzY4NDAwMA", + "digest": "sha256:4396c467df7146054fa2aa5ca40bc5ac1c7c1a2f51bbc54cf14ff1ed868bc01a" + }, + { + "id": "33faa5fd-1d64-5793-a786-97f349723395", + "version": "MTc1NjMyODEwODgyMzc0MDAwMA", + "digest": "sha256:aa4123a1449b2525124464fbd0ed3cd6d7614e7906d37e2e80df4081d56f05e4" + }, + { + "id": "3406629a-ff00-50ef-bd35-bec08e3a1575", + "version": "MTc1NjMyODE0NzU3MzcwMjAwMA", + "digest": "sha256:bc1c8190f133618508454fe8f5cba573668020928082d1e43b9ca52aebb69614" + }, + { + "id": "341b76c8-cf30-5b2e-a38e-0cfb5ca09ad8", + "version": "MTc1NjMyODAyNzQwMzQzMTAwMA", + "digest": "sha256:897ae09ad20dc216d093d921c58dc7185a1014932d26b60c41bab6681dfef46c" + }, + { + "id": "34313103-c32a-5837-a2d1-b1c29d23b0d2", + "version": "MTc1NjMyODEzODIwNjMxNTAwMA", + "digest": "sha256:0b54b2fb2e256ce1897765f30814922c13c62a0a3447bf521fc73968b6dd6688" + }, + { + "id": "34693530-208c-5087-a0c3-cc789621b43e", + "version": "MTc1NjMyODEyMjI0MjM2MTAwMA", + "digest": "sha256:3660c56d61ea9357be6cb108af9a0d3b79113b2b43cb2222d6ea1e7e8d037c23" + }, + { + "id": "346ce405-e7a5-5236-920a-df0552b283cf", + "version": "MTc1NjMyODE2NzcyNjYxOTAwMA", + "digest": "sha256:63054c3a961573d5fa02c573f3d7d0178927b94e8109179c2ef579b2840816ca" + }, + { + "id": "349f2836-72d7-5b8d-96c0-442deb91feea", + "version": "MTc1NjMyODAzNDcxMjgxMDAwMA", + "digest": "sha256:77e9fef8745988b980365f2d94ff614618473d1c8b3850f5f7da5a9c11c552db" + }, + { + "id": "34a2f133-4579-5728-9230-5d3c2a549598", + "version": "MTc1NjMyNzk4ODczNDE4ODAwMA", + "digest": "sha256:f5dad40c696fff42ac36deb46a55e829eb9de6321e4c3ea3af4314769eda60b5" + }, + { + "id": "34c61a82-723c-5d2d-9689-bdeb19a95a72", + "version": "MTc1NjMyODAxNTU2MzE2OTAwMA", + "digest": "sha256:9ab4d91800654197a9bdb6bc13ad42e09bcef4121a972812e5eccfb57921d3a3" + }, + { + "id": "34ddd1f3-6b3e-5de1-a26c-a71580d57850", + "version": "MTc1NjMyNzk5MDQxMTc4NTAwMA", + "digest": "sha256:a943b9a92e467f1cf679b04bacb118fed3aeaf13bd99d3e6e0a1ed1456af8ea0" + }, + { + "id": "34e1a866-1f49-5c65-a632-be95ab05819e", + "version": "MTc1NjMyODE2ODgyMjY4OTAwMA", + "digest": "sha256:1d5e10467ec0c21708ba29df0ecfe731c940aa2903f3d04d5006f8dd2022442f" + }, + { + "id": "352117dc-5c64-507b-b843-d775d1193e4c", + "version": "MTc1NjMyODAyNTc2NDA5MDAwMA", + "digest": "sha256:1bf72b826ecb0fc731b1d7b25a252e2675ddcaf099c722ae842e6eab7f2bcbdc" + }, + { + "id": "352362e4-4916-5f07-98dd-f3595ba41b73", + "version": "MTc1NjMyNzk5NzA2NTExNDAwMA", + "digest": "sha256:e3c5b37f9360621b4828cbbf3cbe7c0ef176bd426af2bc4f2e400ba9796ae36c" + }, + { + "id": "35337f73-5b2e-52e8-994f-0bf571b39176", + "version": "MTc1NjMyODEwNTMwMzUzODAwMA", + "digest": "sha256:eb1d55a72523a702a55e21e7a8dd596240c1a9a0dfcd01f85bd99a1573043042" + }, + { + "id": "355c92ff-86d9-5275-8fb3-11ea187e0dc4", + "version": "MTc1NjMyODA2MjE1Mzk5NzAwMA", + "digest": "sha256:8bb3cbed62041263ac600afa795b5e19be4a618f77750e9574a2e35a317946d3" + }, + { + "id": "3568ad7e-45d1-5705-98a1-4dbeaccf87c1", + "version": "MTc1NjMyODA4OTI2NzA0OTAwMA", + "digest": "sha256:3627e41d82d9caf52eca07a25c031a47389368604d0339b9960e325a98f1a39e" + }, + { + "id": "356a35a9-eeac-58bd-a6a1-8b3fa30ce4f4", + "version": "MTc1NjMyODE2MjIzMDcxNjAwMA", + "digest": "sha256:21b8d75e7ff39ec7ae0c423690c1a586d7122a1e77bffce6d115bc7724f20939" + }, + { + "id": "357df382-cc59-5680-9220-c1dbb03fcb82", + "version": "MTc1NjMyODAyNzc2NTQ5MTAwMA", + "digest": "sha256:828b767f5e175614170070c3be9f1a23e920a71b3e59f4199368e34e0dd16eed" + }, + { + "id": "358651ae-03ba-594f-a6c8-66172da26f7e", + "version": "MTc1NjMyODEyMjIxMjQzNDAwMA", + "digest": "sha256:aee4c39291fd732d2f14d9c2e242f947a028f48e8afffdbd4857d29d29f8c56f" + }, + { + "id": "35bef575-7fb1-5af1-9c36-1efa9aa9249e", + "version": "MTc1NjMyODAyNTYzOTQ2NzAwMA", + "digest": "sha256:8b3ac85dc3e4a405b715729eec7dd1cf9985f34eed6353c31080489d6229e74f" + }, + { + "id": "35cacb33-9ad7-58cc-87a5-bc8a9bc018e7", + "version": "MTc1NjMyODE2ODgzMTM2NDAwMA", + "digest": "sha256:59e656352ee06cf307df34c16401d53aa0dc1a724a395e21c496ba35cc0dafa0" + }, + { + "id": "35de8842-ab88-5416-a5a5-87ab59a2921b", + "version": "MTc1NjMyNzk5NTY5NjM2MjAwMA", + "digest": "sha256:851ccfa006be905f47c4aed64682fb8b2d38ee3c8c90f33ce44dfc6c05fc004b" + }, + { + "id": "35e3865a-4634-59a6-b58b-baf921756341", + "version": "MTc1NjMyODE0MTU4Mzg1MjAwMA", + "digest": "sha256:bf96a8068afd1658fc1eea7c2644f90b62ae1456807259631f8f58b73efc30cb" + }, + { + "id": "35e8f862-1547-5197-98bd-efc7b9cc8d22", + "version": "MTc1NjMyODAyNzM2OTg2OTAwMA", + "digest": "sha256:437204ad0cad470353cca3a126b2151096b564732563d0fc77984b38b1699875" + }, + { + "id": "35f508d7-1441-5557-a81c-7f97fb6acadb", + "version": "MTc1NjMyODEwMjI5MzYyNzAwMA", + "digest": "sha256:3ca21ed07d6436caaa3f51ffb712cf2a17915d4db578a4af549ed1b43147b99b" + }, + { + "id": "35f7551d-c0cc-579a-af04-b3310c196de1", + "version": "MTc1NjMyODAwMzkwNDQxMDAwMA", + "digest": "sha256:1b6fce4b32065006014473319cbe211cfb21842aa169e8e0762b7225cde889a0" + }, + { + "id": "361cb736-42a9-5f05-ad4f-358f8056aeb5", + "version": "MTc1NjMyODE3NDgzOTA3MDAwMA", + "digest": "sha256:dde4343efbf496251278c39f2838409e3da1cb5388bae41be719ffdd9eaa220a" + }, + { + "id": "362e5072-ba06-5ef0-a3c9-0d69f71b0dfd", + "version": "MTc1NjMyODA1NDA4NDg4MzAwMA", + "digest": "sha256:2f8d7fa8055ebb84ed4b4ad0ec2d0f5fa3312797a147ed6c2b47e779be815056" + }, + { + "id": "365934b4-b316-5944-8f68-589915a797bd", + "version": "MTc1NjMyODA5ODY2MTc3OTAwMA", + "digest": "sha256:7a11049fce147ca7455a2315bb66dbd25488116cd1118ab9018000bb989402a3" + }, + { + "id": "36676e92-644c-53e7-aa3d-20f765ba485b", + "version": "MTc1NjMyODAwMzk2MjMzOTAwMA", + "digest": "sha256:5c65cd7b9d15b9627944ea62390c351504ca82f40f4e902ad95e9540470bcb88" + }, + { + "id": "366880b1-6c0e-5de2-895e-b9d2377fdc7c", + "version": "MTc1NjMyODA5Njk1NDQ1NjAwMA", + "digest": "sha256:4d17820b0ca6982f427017dbed9f1b2164595d6c89669771f93c4ed81baff169" + }, + { + "id": "367e9148-3355-5dda-aa58-8a415740dd97", + "version": "MTc1NjMyODAyNjgyNDI5MDAwMA", + "digest": "sha256:c304ef8492738a76e38e39fb2f9e858c617e67098d4731fe65adfb53ecc0c97e" + }, + { + "id": "36a82f54-ef77-5736-86db-90526364eb41", + "version": "MTc1NjMyODAxMTA4NDUwNzAwMA", + "digest": "sha256:4973b823d02ba354292866e493353f08f8a0f154c358c8e338ee68e248e75d9f" + }, + { + "id": "36e71b4c-ff71-5c19-a028-106884e65b47", + "version": "MTc1NjMyODA0NDY3NjM1NTAwMA", + "digest": "sha256:afb30b74be7247265e2409ff29ee0fbe98647f706b1b56ca5d22d0b6216de6b6" + }, + { + "id": "370d6f7f-3ef6-5be1-8950-96b9e4f8c6a2", + "version": "MTc1NjMyODA2MjIzNTI4MDAwMA", + "digest": "sha256:61ba6ea34495416ee7aacdc93be5391b7b5553dd38551587136d8a5a7431a1ab" + }, + { + "id": "372e494f-79c0-5cae-a233-155372a5a815", + "version": "MTc1NjMyNzk5NTYyODI3ODAwMA", + "digest": "sha256:193d9793a690e658146e52b1976c52fe784c2b8266c0c2e00cd70b42ecca21df" + }, + { + "id": "37393502-c876-5af3-84b2-3c0a31425724", + "version": "MTc1NjMyODEzOTI4MzI0NjAwMA", + "digest": "sha256:41beca5ead0975355f9bc9c7abb51a660a17b88f118548fed4c57767848849d7" + }, + { + "id": "375a8880-f779-51a1-9d9d-ac7626a9ee05", + "version": "MTc1NjMyNzk5NTM3MDEwOTAwMA", + "digest": "sha256:ffb7beb1fdd1f372772ed5cb6b6313577bf0d8bad7dd78d55e4bd42ffe71cd85" + }, + { + "id": "376229fb-db52-5d8b-b42f-dd4de38bcb26", + "version": "MTc1NjMyODEwMjI4Nzc4MzAwMA", + "digest": "sha256:05e868e7c26975739660047f5ecb8ecdd3259d2d70a390206ad45e439d2dd05b" + }, + { + "id": "376d2a5c-0b3c-5640-83f0-81c79841570d", + "version": "MTc1NjMyODAyMzQ0MjYwMjAwMA", + "digest": "sha256:dacf915efa0ebc3887e1e5b7997a25ce4da7ff852b8f5fec5dc2c9a58c48e6f9" + }, + { + "id": "3775c982-213e-5756-bb03-f6910fac75c5", + "version": "MTc1NjMyODA1MTE5Nzc3ODAwMA", + "digest": "sha256:373a4f4919e1a87560d3b33845e5246c5b1eb0fb5b5cf4db845598b0a9ccf412" + }, + { + "id": "3779c8d7-b37c-52d0-8801-b24cec45076b", + "version": "MTc1NjMyODE0MTU3OTk2MDAwMA", + "digest": "sha256:794b2aa113ff5f2114f1a613c8d80163689bdcbe929bd0c64f5d31bd1367b693" + }, + { + "id": "37a81dab-0868-5570-9b5a-a6926b1df31f", + "version": "MTc1NjMyODA4OTE2MDczODAwMA", + "digest": "sha256:37916e43ad0a3a410c8bd7e55f1ff5fe2fd51e61767a443c69ab998e8ab322b1" + }, + { + "id": "37de941b-7c1e-50ea-83e5-3418bfc36e74", + "version": "MTc1NjMyODAyNTk0NjMwOTAwMA", + "digest": "sha256:42232154f163737f9a8772bd5d2ce2dd708a03cf878f3746b7289565206a177b" + }, + { + "id": "37f107e0-863c-5e17-9de7-304cf194e960", + "version": "MTc1NjMyODAzODY5MTMzNTAwMA", + "digest": "sha256:279768de9935ad4176400b34ea63a675dcf8311e952164b80450858a1ef0576c" + }, + { + "id": "38241c76-0f0a-5542-9435-b0b1dfc4ffa9", + "version": "MTc1NjMyODA4OTMyNDYxNDAwMA", + "digest": "sha256:c2b03cf39d19fda6d7a488a5940320d1c59f881f0bf00b69db55faf74ce6f457" + }, + { + "id": "3846be06-6a38-5d3e-8e6d-e4d16100bdc0", + "version": "MTc1NjMyODA0MjAyNTA5OTAwMA", + "digest": "sha256:70918b28a5536ddad3dcb67a3f71a8339571d8151cf6046f4589e2bfb9dafa6e" + }, + { + "id": "38491098-0c5a-5086-acf4-410919e23188", + "version": "MTc1NjMyODAzODY0OTA5NTAwMA", + "digest": "sha256:879da29a3d1a7dea1a4794961d3b3c3938036bf56fa9a6743848f419b8175145" + }, + { + "id": "385e4d47-1ece-55b6-b839-37e84fdb0bda", + "version": "MTc1NjMyODAyNzc4ODg5MjAwMA", + "digest": "sha256:edd0c24f86aae4068d95c6287ecbc2ea0f14a4ff03e8a06889c1697dae74eec4" + }, + { + "id": "3862c32e-94e9-5535-b4d8-6478ddb66bca", + "version": "MTc1NjMyODE2ODk2NTE0OTAwMA", + "digest": "sha256:0e0a9938e825e298786a1452be965f5af8c8650e30b4ad8420ccfb1d56b1e02c" + }, + { + "id": "386766de-7f68-5761-abaa-bbd16c55d84b", + "version": "MTc1NjMyODAyMzQ1NzYzNjAwMA", + "digest": "sha256:e777ba56165e3b2fffd3c0b6cec9e45653d37fd197aee11fa196626ee8469f4c" + }, + { + "id": "38890e48-405e-523a-a8da-9eb43ce9c493", + "version": "MTc1NjMyODEzNjAzNjc4NjAwMA", + "digest": "sha256:2cb7334066289cceea670fcf5194e073206f79a311620565f2ff609640a48ec8" + }, + { + "id": "38a34f48-5899-5c69-9645-3e9299e72792", + "version": "MTc1NjMyODAzNTkzNTMyNTAwMA", + "digest": "sha256:2546ac88696c85d7c59609f5b613907c310337611cbafeb3409397687fad2bb9" + }, + { + "id": "38ba5e1c-4bbc-55a1-b06e-87c6c1fe36b5", + "version": "MTc1NjMyODEwNTM3OTAxMzAwMA", + "digest": "sha256:d352cce7c9ebe766c92b923167250ac4740844e3aedcffbb385fd234aa904bb0" + }, + { + "id": "38bcc391-474c-50d9-b5bb-76802a1c28a8", + "version": "MTc1NjMyODAyMTMxMjMwOTAwMA", + "digest": "sha256:1f1751cd201dc8609c3bed1773d4f0d53c910cbb2da3f8f348ffac71faaa58e0" + }, + { + "id": "38ca963d-d2b8-5cbd-8d84-0cf016619d08", + "version": "MTc1NjMyODA3MjU2NjAwMjAwMA", + "digest": "sha256:dd3054c8be8584402f6c7851db33424fb2fbac03df3e679517455d12fe2b33ba" + }, + { + "id": "38e4e4ab-0372-559d-b18e-be8058794a1d", + "version": "MTc1NjMyODAzODg2NDg3NzAwMA", + "digest": "sha256:2053fe38ef9f7d3effe321ba13e7e514d7f971f898800cc7c6df3c49215606d5" + }, + { + "id": "38f5d541-9dc3-51fc-9069-3604bba62a8d", + "version": "MTc1NjMyODAwNTEwMzE1MjAwMA", + "digest": "sha256:0e507b602bed16475882af4c6205b457de708db4a1b929aea397b4fbb2f50fc7" + }, + { + "id": "38fdd979-2ec0-5f1d-8a32-3947cc5eace6", + "version": "MTc1NjMyODA2MjAxOTA4MDAwMA", + "digest": "sha256:d8d2b422978874b46f111a2ed36d755300c1163fbcf12822937f94ee95a6aed2" + }, + { + "id": "3914c069-f02c-5455-9e28-5dd32f9ec5fa", + "version": "MTc1NjMyODA1NDA2NzAyNzAwMA", + "digest": "sha256:ce1e8d263009530be44fb651f408dc0168138d0dff823bb626c9f9d45f81dfe8" + }, + { + "id": "391f7d1a-eb83-5216-8b30-d5933fc1aa1b", + "version": "MTc1NjMyODEwMjM1OTYxMzAwMA", + "digest": "sha256:71858adeecb9e9afed942c9bada4e1b56879ccf3403e79c4c7a857cc8d3ecdda" + }, + { + "id": "39208149-3af8-56c2-923f-9a2e914d085f", + "version": "MTc1NjMyODAyNTYwMDk1NTAwMA", + "digest": "sha256:0ab7b56f21dc861ebe9b03b7fac83cc0afeaefff5f55c91473eff14bf6bfd6d0" + }, + { + "id": "392504cc-55f2-5ecf-a2d4-0da57c8afcad", + "version": "MTc1NjMyODE1OTA3NzAyOTAwMA", + "digest": "sha256:d078270b041e5f0b615d09062df89f4c60e3f9f1f26f1526e51e79ba63c3f923" + }, + { + "id": "393434a0-f57a-58cf-bd82-4ffacd612c3e", + "version": "MTc1NjMyODA5NDM2MjA1NzAwMA", + "digest": "sha256:27e80363d37402a3863f9cad46ffed8d18660154e0ecf4da144fe0b73d73c8b9" + }, + { + "id": "39344905-f773-55a9-b676-57a24375c608", + "version": "MTc1NjMyODAzODg3NTgxODAwMA", + "digest": "sha256:913db23f8cf8f501843bdf5acebaee7dce4ecfeca1eded46bae2589c73e238bf" + }, + { + "id": "39353e0a-fe1c-57af-b480-94c19c2bac24", + "version": "MTc1NjMyODE3NTA3OTY5NjAwMA", + "digest": "sha256:8834201a2905f861716378c8f6babc896091cf5374e5b0529bb646fcc3e3f08e" + }, + { + "id": "3936940e-c33e-5d6c-a39f-459648404d87", + "version": "MTc1NjMyODAwNTEyOTMyMjAwMA", + "digest": "sha256:514988ce641e0f831c39a0774da227cd2f2d98a69405abeac308178cd0bbe895" + }, + { + "id": "3954cc55-7991-5bf6-b26a-8f7c46896cfb", + "version": "MTc1NjMyODEyMDQ0ODI5OTAwMA", + "digest": "sha256:477a7f36bb05e60cacfe78a0ea031b1d617832f99d1600335aa91c84f51d60d2" + }, + { + "id": "395642f9-ba2b-58f0-924f-5f2946d6f6c0", + "version": "MTc1NjMyODE0NzczMzgwMTAwMA", + "digest": "sha256:28d13ba74cd5e69af1f269c954281735e77f94048db43fe3af7d46252b6629ab" + }, + { + "id": "396a2b5c-e986-54c9-adf2-d9cdb91b0fbf", + "version": "MTc1NjMyODE3NTAyMzQzMTAwMA", + "digest": "sha256:f7252e1ed49f10ce89c5b83e873bfc3d19b0c57006c0044adf04f3d00fec8630" + }, + { + "id": "398f08bc-3ccf-518a-acd4-55dfaaf90738", + "version": "MTc1NjMyODA5NDM2MzA4ODAwMA", + "digest": "sha256:485396f604d1cdeda61fc7a4fe1e9e72e766781fc256a8595c8b8fe578869c87" + }, + { + "id": "39ae8167-ccd2-5389-871b-bdf643a8e0f6", + "version": "MTc1NjMyODA3MzIxNDI0NTAwMA", + "digest": "sha256:b2ff3f9da18f10cfd2ecd45a6b7a4841f5e87f0202ab53814212bbb780580095" + }, + { + "id": "39b9f390-0e2a-5e15-82be-aa8b4ffbca59", + "version": "MTc1NjMyODEyNDQyOTkzMzAwMA", + "digest": "sha256:81181115e2244fea9a63e03bb0c8e157bd8b981f4530e6025b11ab2991984fe2" + }, + { + "id": "39c11140-7876-57d5-9d9d-9eff393e79d1", + "version": "MTc1NjMyODAzODY5OTg0NjAwMA", + "digest": "sha256:55c4e1501867b87b2a36c2ea8f416717a85a3300eea8728e3229c4fb8f6f55f0" + }, + { + "id": "39c78547-92bb-5271-8fdb-c5c525615573", + "version": "MTc1NjMyNzk5MDQ1NDI4NjAwMA", + "digest": "sha256:444590b2710172037596092930494f2751b4e288e68c9dd7b98d897bdafe1c80" + }, + { + "id": "39d6af6a-100e-512c-9ffc-6e379adbe824", + "version": "MTc1NjMyODEwOTE2NDUzMzAwMA", + "digest": "sha256:ae7ef6f28cebe20d48ec2468b7cb4e045ddb01f4baa391f0affb7e8d4fc9e6b0" + }, + { + "id": "3a1f4fff-dcb6-5db1-87a6-b3d0d2334405", + "version": "MTc1NjMyODE0MTUwNjIyOTAwMA", + "digest": "sha256:1dc92683b622190cbd39f6d41dd092ccd8108b59296d5864bc33a324f7e91e1d" + }, + { + "id": "3a2c21f6-ac5d-5965-a286-49c655f502dc", + "version": "MTc1NjMyODEzNjk2ODQyOTAwMA", + "digest": "sha256:3a91128d13f673fba2335c58c64dc3deeebd0bd01de9f27d58e557defa2de5a6" + }, + { + "id": "3a555caa-55d6-5fb1-bb2d-bed3b5058901", + "version": "MTc1NjMyODA0MjQ1ODQwNjAwMA", + "digest": "sha256:bbe963e72549461720b9cc939aefa8928ca7aed559e8512f8af5e546a0df3adb" + }, + { + "id": "3a5cb0b3-78cb-578f-a000-8c21a01c3a7e", + "version": "MTc1NjMyODAzNTk5NzI2NzAwMA", + "digest": "sha256:644ee360746a4ddec2b88924649ae538209e89a144710292e518b520ef1c5706" + }, + { + "id": "3a7b57e1-7df8-5406-a93f-2fde361fe338", + "version": "MTc1NjMyODE3MjQzOTI2MTAwMA", + "digest": "sha256:483c09a920de9b2e7465b3920a65971b33e526252448356be447e40e743ce13e" + }, + { + "id": "3a89341a-1bee-57f9-a4a3-3e104395fd68", + "version": "MTc1NjMyODAyNTc2NzU2ODAwMA", + "digest": "sha256:6af5582e0ca826bade1aa48ac4235e6a6a20ae08b5a0c0bb9b2faf8af32c148b" + }, + { + "id": "3aafbdda-cc98-5556-aa48-82cefa3a2083", + "version": "MTc1NjMyODA1NDEwODc4OTAwMA", + "digest": "sha256:3a655c8d455ba84ca236cc0cc7e3f8645ff5197b66e30eeb96f961b822a3c562" + }, + { + "id": "3ab0fabc-7f0a-5eb5-9acf-8266a9651d03", + "version": "MTc1NjMyODA2MjEyMjYxNjAwMA", + "digest": "sha256:2c5351ef2cd132021cef066b48392c6a2c1a4bc5a5105a95404310b22e260aeb" + }, + { + "id": "3add7132-d0ce-5f23-ba7c-b1acadb4525f", + "version": "MTc1NjMyODAzOTY2MTM4MjAwMA", + "digest": "sha256:bf8480a1a93382c9532e83d0021f61b9ae4cfceb9457763fb9f6481d6ed1f4d1" + }, + { + "id": "3b3feff4-2e2d-5f83-9d55-26e938e97caf", + "version": "MTc1NjMyODAzNTkxNjQ1NzAwMA", + "digest": "sha256:e77be54b6e97661c39a15fcbc74a9098f25a63d637ac71f032b4ec3640834712" + }, + { + "id": "3b437f76-e9f0-5a8a-8bb5-e878369c49f4", + "version": "MTc1NjMyODAyMzQ3NzYyOTAwMA", + "digest": "sha256:5fb02e9520eca9081587d9f0e312872d5ddd4d9524cc3d5846dc0efe16d70c38" + }, + { + "id": "3b5f03b0-f126-5004-b8ee-70fc13efa9ef", + "version": "MTc1NjMyODA5ODcwMzA4NDAwMA", + "digest": "sha256:9f466e0d45151e22a03346188ced626fe3322e104496257f6eb04f2f55c0ae83" + }, + { + "id": "3bb1ca30-175f-5c18-bcef-26499f391efb", + "version": "MTc1NjMyODEzNzQ4ODY1NjAwMA", + "digest": "sha256:83cf92e6542a24aaa0b8739215d6897ecf1eb6ee2d8c189b24a67f716f3cb2bd" + }, + { + "id": "3bbd0862-cdb1-521f-950e-e61e3c1470b0", + "version": "MTc1NjMyODA1MTIxOTU2MDAwMA", + "digest": "sha256:afd21d14b2bf3b36e5842cc8ef05d2b854ebeaa001735daa2ab05a30db1ee13f" + }, + { + "id": "3bca6f29-7d27-5e62-b663-300a212a0dc5", + "version": "MTc1NjMyODAzNTkzODY5OTAwMA", + "digest": "sha256:920d55fa85dfe924085ecf81709fc1612e636821117c87c73c75b333b8334d65" + }, + { + "id": "3bdc0515-230e-58a9-892a-4adf627d0f9e", + "version": "MTc1NjMyODE1NjQ0ODg2NDAwMA", + "digest": "sha256:521a814a9fbb1b97172d36019a0ab6e51482daf4af5444d5c5055a7519c518c0" + }, + { + "id": "3bfd02da-159c-5580-b9af-9773b6040f49", + "version": "MTc1NjMyODAzNjgyNDMyMjAwMA", + "digest": "sha256:12c3271f30ee9ede79a1c5f2eb10201ba81babd04836d391b813cbaeca80cd40" + }, + { + "id": "3c035c08-df62-585a-bf78-65f92d79bf1c", + "version": "MTc1NjMyODA1MTI3NDU1MjAwMA", + "digest": "sha256:7c5cd95b93849f9d26d993db1883a1fb19290b8f9aa779a86cc007c895c1fa31" + }, + { + "id": "3c0b0061-81d4-56ec-ada8-4e749ddeaafa", + "version": "MTc1NjMyODEzNzU4MjE4NTAwMA", + "digest": "sha256:05d7c7cb9182b26ef87c7bdda34f0f936f99fbe0431a7da902d7acb37607d99b" + }, + { + "id": "3c165e11-27ed-539b-a2b5-1a0a6cbb9c6a", + "version": "MTc1NjMyODAyNzg0NTc0NTAwMA", + "digest": "sha256:9e3188755c9c57a0346c344b574303e584afca4e036f6664c2b16273e38ecca0" + }, + { + "id": "3c1be4b4-7adc-57a9-a98c-2c5c5b2ead0d", + "version": "MTc1NjMyODE3MDg4MDQwNzAwMA", + "digest": "sha256:f513e1b413fd1e000c83bce1cded9948d00de5df9e2b9013278a5e40fb3b2064" + }, + { + "id": "3c1c160b-7c89-56f0-a14b-f4bfc83d1386", + "version": "MTc1NjMyODAxNTQ2MTQ4NTAwMA", + "digest": "sha256:2f45c8371df88df79fbf5c03050939974fea9059caec53f2d82ba485731b1ef2" + }, + { + "id": "3c33ee3e-b879-5316-aa05-cb05dad194c3", + "version": "MTc1NjMyODA0NDcyMDQxMjAwMA", + "digest": "sha256:050847608165b8f32530e77442fb1371b8fc4d277a363749bbd9f3bef384215a" + }, + { + "id": "3c443731-7499-5b42-a41e-0a9a858b495d", + "version": "MTc1NjMyNzk5NTY0NzI3ODAwMA", + "digest": "sha256:7a65b9b3a411a15729a277c5683f33887933f5364b87e553f4b30148a238fcd7" + }, + { + "id": "3c5975dc-0b70-5776-9bbf-5cb44adea57a", + "version": "MTc1NjMyODA4OTI5OTc2NzAwMA", + "digest": "sha256:26e82f0a754ca99399b81eb627172100fae01cd34ea567b55deae1ade246391f" + }, + { + "id": "3c5c9dd2-f72c-5ded-9cca-d440db4d41c5", + "version": "MTc1NjMyNzk5NTI5OTgxNTAwMA", + "digest": "sha256:38032fa4ab6a19b981e220b5a196bf9a697e5694b52b9517135b44b22e027f81" + }, + { + "id": "3c67507d-8c42-51c8-93e0-23cab537129f", + "version": "MTc1NjMyODEyMDQxOTU4MTAwMA", + "digest": "sha256:030ce6f0ff0b715430ff2a47bbae5de8b04caa99248f2e9ce68393b91c5e88d9" + }, + { + "id": "3c75baba-433b-5fb9-a7cc-439ab5307065", + "version": "MTc1NjMyODAyNzQ0NTYzMzAwMA", + "digest": "sha256:4996234a4b7f7ab018ef5aaf1d8286173079a4b9989dd7f989908452903571d2" + }, + { + "id": "3ca18584-cf29-53c9-9dc8-44c577146143", + "version": "MTc1NjMyODAyMjE2NjUxNzAwMA", + "digest": "sha256:0901151505fe020eacc6ae7f3211ecde2ea73284aa70ecd8080b04cce11e66da" + }, + { + "id": "3cabf486-55fe-558b-8ad2-534e0c82b596", + "version": "MTc1NjMyODAzODg5Njc1MzAwMA", + "digest": "sha256:1bc2f89a990f2902250d4ca3e6f24a5b69b2a2e699023fd0cb94d755f490bab4" + }, + { + "id": "3cade85b-f1c8-5036-bf99-3e3c64512b07", + "version": "MTc1NjMyODAxMDU0MDM4OTAwMA", + "digest": "sha256:0237caceefe61601668ef3177dab88c923ab4a1e62d0ab38c15c0068c112b0a8" + }, + { + "id": "3caf021b-472b-5901-a481-3507d6004d15", + "version": "MTc1NjMyODA1MjQ4MjYyMjAwMA", + "digest": "sha256:8fee84a63c0d7708943989b757860a4fcfa715ab4babb14e6079b23d60493381" + }, + { + "id": "3cb4a892-55f6-5e18-bedc-4dd42cdc897e", + "version": "MTc1NjMyNzk5OTM4MDA5NjAwMA", + "digest": "sha256:55b189091a8ff93ecdb52172d9fda0e2c394e96ae58a62c87619e44b772ccfa7" + }, + { + "id": "3ce2058e-e0af-5bea-b170-640d812fd968", + "version": "MTc1NjMyODE1OTEyNjk2MzAwMA", + "digest": "sha256:a63c4da9f2d55aacd71770c1d3629c551701e09121469577569344282172538f" + }, + { + "id": "3cf3b155-af40-55c4-acd7-5a282a19b5dd", + "version": "MTc1NjMyODE2MjI3MjE2MjAwMA", + "digest": "sha256:2f95fab2935aba4b48944b58de1957444baca0f2748be01813f7e442b4ea3bfc" + }, + { + "id": "3cfabd78-d073-5730-b19b-1b3413787841", + "version": "MTc1NjMyODA4ODYwODUwNzAwMA", + "digest": "sha256:9c6139773c9be62f61b5084bd88560d0e15d3716d30ce18b196bcd4600da7822" + }, + { + "id": "3d1e4c27-74d4-52f3-8e85-cd3178b1f546", + "version": "MTc1NjMyODA4ODUxNjE0ODAwMA", + "digest": "sha256:a9883968f1ad19e9657df5e150ae296e1f6f230242a43e68c58840bd1fe3c95e" + }, + { + "id": "3d2fb400-16e5-51d0-bb0e-a0af79df1f99", + "version": "MTc1NjMyODAxMTU5MTE4NDAwMA", + "digest": "sha256:00f4d6684ba3d73d0681c3d4f0dcbe93e84f53c11e0ce37990c1e8b7807a43ca" + }, + { + "id": "3d4bbd5b-b0b5-5638-a35f-d43f25dd9091", + "version": "MTc1NjMyODAyNzc1NDAzOTAwMA", + "digest": "sha256:1ba4b852b897a6eeffa0bd2656c083402882e6edbec9f7fef4e3e49e51a48c2d" + }, + { + "id": "3d56def9-a854-52bd-af10-fffbc98a1fb9", + "version": "MTc1NjMyODA5ODcxNDA4OTAwMA", + "digest": "sha256:aa56ef60a8e6e8519829f8c9040794d313e8d85f47b3cd7d1201503c3d91ffd0" + }, + { + "id": "3d6860be-8c9a-5e1f-9404-e2a7d331fb6e", + "version": "MTc1NjMyODAyMTI1ODUxMzAwMA", + "digest": "sha256:6df9ce1f261db72d5379fb9b8f04a1bcb83ce2bd9a3b849ae6f6501948b5f5bd" + }, + { + "id": "3d95ade3-20cd-5ccb-9ac5-512329af8d94", + "version": "MTc1NjMyODE1NDY2NTIzMzAwMA", + "digest": "sha256:3c51c5860969ccc7530cf6a15b672c091e455427534ee98feb162070a5218bca" + }, + { + "id": "3daf8092-92a0-52c5-9993-81332b97a421", + "version": "MTc1NjMyODAxNjU1NDEwNTAwMA", + "digest": "sha256:e10b46b7215291770dad97e77768af7dcb296c5122597dfcd8511bed676ff765" + }, + { + "id": "3dc34b0d-85dd-5099-ab57-c29f1ba3e6c5", + "version": "MTc1NjMyNzk5MDM1ODUyODAwMA", + "digest": "sha256:42a54dfa269b3c5783eabbf99e3dd42ed680a8d309e220154552a601219a396d" + }, + { + "id": "3dd14683-299f-511f-8c3e-29791b4e3c88", + "version": "MTc1NjMyODE3MjQ3NzQ5MDAwMA", + "digest": "sha256:34366acd2ea6b2c98a46bbbdc08f45575e8c1d1ab02ad6f572508ec2222dc352" + }, + { + "id": "3dd32f0d-2fba-586b-aecb-980e836a3882", + "version": "MTc1NjMyODE2NjgxMjg3NjAwMA", + "digest": "sha256:54493b60dbb96f91fefa6cb5c2d57f9ab5b8d6de70735ff53bceb383b67172a3" + }, + { + "id": "3df5db82-a01f-5ebb-bc43-43b9c6d62da0", + "version": "MTc1NjMyODAyNTc2ODM0OTAwMA", + "digest": "sha256:5d3743357f003f56efd121517e664d0cc38ffa451c3fa087b67d6d0d1b6bbf15" + }, + { + "id": "3dffc796-b747-5666-8ebd-c22dbe72cafd", + "version": "MTc1NjMyODAxNTQ2MzM3NjAwMA", + "digest": "sha256:23056b55e2b225d48fdb656ab67fb5387f5c36cd95ae6c80ef833c6080e7d5a3" + }, + { + "id": "3e07fa5e-316b-5656-b7c3-0ff13c55c947", + "version": "MTc1NjMyODA5ODY1MTYwNDAwMA", + "digest": "sha256:d7d3e8cb657d623bc36c711db6717f6a96432cd8ca037510373d0981d0769b65" + }, + { + "id": "3e62f772-00a9-59ca-806a-5b0d37c07dce", + "version": "MTc1NjMyODE3NDk4NzQ4ODAwMA", + "digest": "sha256:b1961da24fd0da1943236ffe1394ba36fd884d402e94c58cb51037ab63f40e29" + }, + { + "id": "3e6448e0-8e6e-577a-b3a1-56d9652c3341", + "version": "MTc1NjMyODAyNTcyMjQ0MjAwMA", + "digest": "sha256:f0b1bcca060529c8e5f36edea02b94e267697423c0d676a95be5bc7be9eb5fb6" + }, + { + "id": "3e6ccaa0-d3f0-5745-bf19-b02b50e586dc", + "version": "MTc1NjMyODE0NzgxMTkxMzAwMA", + "digest": "sha256:c3f9bb002fb5016af7d0a67da1b3b1cc2f1b5aedda508158a5924e65027bc48f" + }, + { + "id": "3e75baa4-2fc9-56a2-bae7-4d895d374f72", + "version": "MTc1NjMyODA5Njk3NjMzODAwMA", + "digest": "sha256:6788ba6e74bb9e5fa6f0bec171b0472e3918a02890de87e7c2b316560c63cd98" + }, + { + "id": "3e9608ce-c3b1-562c-a72f-3f149d5c9e9c", + "version": "MTc1NjMyNzk5ODE2OTc4NzAwMA", + "digest": "sha256:90073c3e3eded6ab60bfc5dd1932c192c762d125a779067e3688f1ba4e8ce6c4" + }, + { + "id": "3ea50b0f-bd26-535c-b3cf-4dc2c5d5fd47", + "version": "MTc1NjMyODA3NDIzNDI4NTAwMA", + "digest": "sha256:7d33749b10398f7574638602b6c48412b770ea245c12f3eae5899b2c5113dba5" + }, + { + "id": "3eac2135-b684-50df-88d6-597fc91e88f7", + "version": "MTc1NjMyODA3MjU3MTU3NTAwMA", + "digest": "sha256:6e6652abff2f792e043d8c43402f7bff66dd482787c1a49979ed75cc5dc54a06" + }, + { + "id": "3eb83e54-6b13-5efb-a71c-34b2c7b545a8", + "version": "MTc1NjMyODA0MTg5NjU3NzAwMA", + "digest": "sha256:512ca6025e5eb0b162bd072eef59f27fcd7f79d16cddffe05e696cdd446a6ab9" + }, + { + "id": "3eb965b4-7a51-5e7a-92cd-9f4181a72d7c", + "version": "MTc1NjMyNzk5OTIzODk5OTAwMA", + "digest": "sha256:862a29111ac5d9b11c4ad29406dc8240afb9ee97c349b4f432070f18eee404b6" + }, + { + "id": "3eca9be1-f8fa-5544-a231-1ff0ff32b26c", + "version": "MTc1NjMyODE1OTE2NzQ1MDAwMA", + "digest": "sha256:976ac990a2592d848221c79c9657ed82a5c3f49bbb80ed3b2da0ec541dcdefcc" + }, + { + "id": "3ed1f6fd-da60-5744-a119-77cd0e4c28b0", + "version": "MTc1NjMyODA5NzA0ODE0NjAwMA", + "digest": "sha256:c9292fa82b29c61e901f6d3fdcc5c1ba36fc58a6c86fbb5f20b6a8567879daa0" + }, + { + "id": "3ef51620-e191-56c2-b9f6-56607ac51673", + "version": "MTc1NjMyODAwOTA3NDg1NzAwMA", + "digest": "sha256:4b2d114fe13366e0d8ada4098bc9d1b553614e8b374e7691318b6324d0e06ab4" + }, + { + "id": "3f17be6a-8632-5338-b146-a9efb75d229a", + "version": "MTc1NjMyODA2NzAxODc5NDAwMA", + "digest": "sha256:330eeb7724cc031eea7776c5ffc85a20cb912fb16289fc302344bd29af1a7215" + }, + { + "id": "3f301018-14e7-589a-8fb7-b1d436e0f1da", + "version": "MTc1NjMyODAyMTI1MzY2ODAwMA", + "digest": "sha256:723ae97931cacc903a74b7f84f7da87f8d1ba3c41a962646e2fe471bea31b368" + }, + { + "id": "3f4a83bc-b501-591f-9280-aec9e3b4c029", + "version": "MTc1NjMyODAxMTg2OTg2MjAwMA", + "digest": "sha256:37a113cfda52749c13e427a0f677b6da7a440dfee5552d76d853d461490313e7" + }, + { + "id": "3f4b6597-8b3a-5205-8b07-8d218d941eff", + "version": "MTc1NjMyODEwODg2NjM1NTAwMA", + "digest": "sha256:045a81354859e555503dee86c1ba781d7a3374dbbba288896b3f2ed62ff2fa74" + }, + { + "id": "3f523242-e9e9-50f4-bbc8-8f5eec770abe", + "version": "MTc1NjMyODExOTIxNTU5MDAwMA", + "digest": "sha256:160be9613cf582f7d260bc2c589beb20baaf50fca06df18aab183c8f61e7a782" + }, + { + "id": "3f52df89-9fb4-51e6-a446-9b667233e04e", + "version": "MTc1NjMyODA0MjU3NTMwMjAwMA", + "digest": "sha256:5dc73b3c3ac384ab6559bca78033dc304e427ecc518ae66fd4e89b31b3151123" + }, + { + "id": "3f5dd5b1-eeab-5d6a-a3ea-adce5fd6f599", + "version": "MTc1NjMyODA0MTg0MjM4OTAwMA", + "digest": "sha256:fbd6a838d52dc23566504ad47da33c48538f1de18597107bb37ebcc0a1e0b3a1" + }, + { + "id": "3f6263ba-3192-5875-89b3-790cdcfd5cd2", + "version": "MTc1NjMyODAxNTUwNjMzNDAwMA", + "digest": "sha256:685f5279ea8c56364cb93b3a8bc1a7555e2c3d05d560a4ddda453d22831b7bca" + }, + { + "id": "3f96f546-78e5-5b6a-865b-f5b20c4809cd", + "version": "MTc1NjMyODAyMzM2NzAwOTAwMA", + "digest": "sha256:09e6436a4ca5b4fe5d0143c7e983972e96be18369fced8b10e284e31b0393df5" + }, + { + "id": "3f986b0a-28c3-5f44-a74f-83bd389c7931", + "version": "MTc1NjMyODE3NDc2Njg3MjAwMA", + "digest": "sha256:508172247a30d32d541eb873c3bec0de9e27110414c8480a81b3eb5e131b5d3f" + }, + { + "id": "3fbce19e-c58c-55c7-a2cf-8e49c42e541a", + "version": "MTc1NjMyODEzNzczMjEyNTAwMA", + "digest": "sha256:668c4ee5d7e4c95c1ac731441adffce5490050dcf8afbc5e44a649998acb12ab" + }, + { + "id": "3fe27a1d-42b0-59c0-83b2-466e33e9931e", + "version": "MTc1NjMyODA1MTM3NTU4MjAwMA", + "digest": "sha256:c0bd32527f952d12f80b934bcb7269ffb4f21892d74bfbcc049b42582f0a71f9" + }, + { + "id": "400a571c-41f3-5e85-bdc5-0788337a5824", + "version": "MTc1NjMyODA4ODYxNDA4NTAwMA", + "digest": "sha256:bf2c641b989bd03cc6e44361cdf0930e058a1859a13758d34b85a3831286bc65" + }, + { + "id": "400e6b61-db31-57ec-9338-08e15e484607", + "version": "MTc1NjMyODA5NDM4ODE2OTAwMA", + "digest": "sha256:53f7ce538820c91153869a4e2db9be8ee284634eca4087eb37a8e3707ee54f26" + }, + { + "id": "403fbce8-7dec-562b-8c14-d03096175be5", + "version": "MTc1NjMyODA0MTg3NzEzMjAwMA", + "digest": "sha256:6bcd4f3005c645765c83d998825470a5fa19c95f137bc6925bef30b6a1521f0f" + }, + { + "id": "40551e26-bf84-5f70-8939-8e0580a8dded", + "version": "MTc1NjMyODAzNTkzMjkyMTAwMA", + "digest": "sha256:e743dfd2c572031e2b4cfacdedc1744c6a5eb349ba3ab4230bd1cdb485092ff1" + }, + { + "id": "40fb68d5-9ef5-51da-b32d-fe809e918e8d", + "version": "MTc1NjMyODA0NTc4Mzg1MTAwMA", + "digest": "sha256:8cad717ebab202538a38e416acc9b5c009d812adf169989ec91cf6c63a18c83c" + }, + { + "id": "41260ba9-cf7c-519f-ba00-47c4d32889e6", + "version": "MTc1NjMyODAzNDc1MTY2OTAwMA", + "digest": "sha256:6eef4efc8c46c49958789f521066d8abea257ab126e9ff4c0aeb7ac13852446e" + }, + { + "id": "4169bf52-967a-56c6-ab56-56faeef6076c", + "version": "MTc1NjMyODA3NDM1NDA4NzAwMA", + "digest": "sha256:22464a98d5a83d25afc2f2c7d5bc47d3c55cea92583e1d67fd2a6d02ca3bc3cf" + }, + { + "id": "416d12d6-15ff-5ee5-9d98-d17d8886374c", + "version": "MTc1NjMyODAwNjE1NjgxMjAwMA", + "digest": "sha256:08155a5f2f83554513f3146f8176df998a831f1fbacda220914647d4504ff289" + }, + { + "id": "41a1ab43-896b-5ba1-8109-7dfa9d9c2c54", + "version": "MTc1NjMyODA4Nzg0MDMyMzAwMA", + "digest": "sha256:f03b42bea75ab12e5d4ad701ff8739f3c1bfdb43920c7c994d871fe4d0f6e823" + }, + { + "id": "41a8518a-94c4-57e8-b3be-e627c89a5635", + "version": "MTc1NjMyODA3NDI5ODcyNDAwMA", + "digest": "sha256:24cb396b35f366d5ca97f16f1fae244bac6070d87d52791e9c943957a288fbe9" + }, + { + "id": "41a859c2-0ae5-5d1a-8542-2b80de68fa9b", + "version": "MTc1NjMyODAyNTg5MzYxMzAwMA", + "digest": "sha256:f2c807bffb067ca479c40a4b26af0b2d06c889a2c9c42147ae6aefc04fb6314b" + }, + { + "id": "41c83aca-bec9-5aa6-a192-b3ec42fece0a", + "version": "MTc1NjMyODA0MTkzNDMwNzAwMA", + "digest": "sha256:e5f401e43683d8642e9e1dbb9e799efd1b6aa6bad58d6a623f3bd404a4bf94fc" + }, + { + "id": "41c93690-b9d3-54db-bc18-62796c5c4a22", + "version": "MTc1NjMyODAzNTkyODE1MTAwMA", + "digest": "sha256:99737c7388dad9da8f2abd8246cd57643f094df4dd0a49dc729dd250c61a6a15" + }, + { + "id": "41dbd0c7-367b-556f-8b4b-2e0dcd4dc86f", + "version": "MTc1NjMyODA2MjAxOTMxODAwMA", + "digest": "sha256:cbb020f15b6641d3193aa3d9a35b5031ef08ba7d5f0822cb82123957f033ba37" + }, + { + "id": "420fcd0e-31b1-5eb3-8e42-b2cd072e2f20", + "version": "MTc1NjMyODE2ODkyNjczOTAwMA", + "digest": "sha256:e29f8e269686c58da3ffd35c5a88da3991ca2a8df27f96d3063c408b252e881b" + }, + { + "id": "422c5001-4d36-5ad7-a9d5-2c6af6f28efe", + "version": "MTc1NjMyODE2MzUxMTc4NjAwMA", + "digest": "sha256:6b86414a68e8048525fca7caad7769320545544e54da11102620390591bd4028" + }, + { + "id": "42568bc7-cc72-56f0-b370-953c9bf0aeba", + "version": "MTc1NjMyODA3MzAzOTg0MjAwMA", + "digest": "sha256:97617c3f8d1b7e74cbdc822d3848fe3ecae9a8896b8f73d3fb6528b3fd701173" + }, + { + "id": "426da8c5-a0c5-5631-b20f-49d212cb9984", + "version": "MTc1NjMyODE3MDk4Mjc1MzAwMA", + "digest": "sha256:bf9f34e3b7da3c5d726156922d6065a4c1a6257dde34342eef4bcb8835115e6e" + }, + { + "id": "427549cd-d403-58d9-b36e-7478f153b977", + "version": "MTc1NjMyODA2MjE0MTUzNDAwMA", + "digest": "sha256:e0f50175983bb80f82ba9d590fb6758c970cfe39c675479a773088853b3f8c01" + }, + { + "id": "42b41764-715d-590f-9c95-0eb4e193e89b", + "version": "MTc1NjMyODAxNTI0NDM3MzAwMA", + "digest": "sha256:448220ea46fe62d79d8b3c205dc275fa7484cbe4d1bf350c75629eadb8722885" + }, + { + "id": "42d0e4f9-433d-55f4-b959-2544daefd6c3", + "version": "MTc1NjMyODA1MTIyNjg1MDAwMA", + "digest": "sha256:71c44b966d229828d33e08511043207046dbfef7243bd69825809fb867ed49ea" + }, + { + "id": "42d82b00-3b23-55be-a18a-1871b85e3b68", + "version": "MTc1NjMyODEzODE3Nzk5NTAwMA", + "digest": "sha256:0f1ba62c0d9a731d3175c4ce6918b490ee441bc551ad409d57d02cbefc572df9" + }, + { + "id": "42e75f76-5e96-51f6-8e5c-3b9b334c5dd6", + "version": "MTc1NjMyODE2NjgxNDU4ODAwMA", + "digest": "sha256:3c8db90fa23b3279e48c1992db3ac35731196fdfc63cd272a7ac6db2fcf68bd4" + }, + { + "id": "42f549ee-eaef-57b8-846f-3a498d8393b9", + "version": "MTc1NjMyODA3NDE2MDQwMzAwMA", + "digest": "sha256:632a03b8fe0ec9283050ad60cf9cb73fa35bed16293088021a67209953aa6405" + }, + { + "id": "42f96fe5-2316-5670-a60d-33d6e803c83a", + "version": "MTc1NjMyODAzODYzNDI1MTAwMA", + "digest": "sha256:44b3945a72f9d896442b28aa14d18191a325507ef824f392172c7952f49ed5d1" + }, + { + "id": "43134a0b-4564-5273-9bdc-ef4b7fc47aaa", + "version": "MTc1NjMyNzk5MDQ2MTgyMTAwMA", + "digest": "sha256:6e5af78813a1e41cbd00977e3cecb448c23d1bc06439ad409f921ce02a39ba36" + }, + { + "id": "4317832e-b444-521a-b288-f7b451496efa", + "version": "MTc1NjMyODEzOTMwMzA5MzAwMA", + "digest": "sha256:ed012fa631715cd7139e583ab25eac79cbeac1048651de0f953c906ea2273316" + }, + { + "id": "435639af-6a3f-5052-ae4c-1a17938a4a6c", + "version": "MTc1NjMyODEwOTI0NjE4NzAwMA", + "digest": "sha256:fcf54305d3220d1f788775bb920fb93ef72ccff9ff41b013dfaf74b624fd7b16" + }, + { + "id": "435b768d-cfa3-53fe-b264-c8bd5a1d4a9f", + "version": "MTc1NjMyODAzNjg5MjA1NzAwMA", + "digest": "sha256:f6980b4380b55db742194bf6dddcb0505109460fc962f8eaa4e0e66896bd1cf8" + }, + { + "id": "435fac0d-922a-5f19-804f-c23d46724039", + "version": "MTc1NjMyODA0NDc2ODU3MzAwMA", + "digest": "sha256:cfb272ab7cfbbcc3d1450a20a07e09e55963c983d8ae848cc4d92c6927785a62" + }, + { + "id": "435fb250-51dc-5705-b71c-c882633b4e84", + "version": "MTc1NjMyODE2MjI4MjY4MjAwMA", + "digest": "sha256:c14e9d0ebd15443c0545cb9dc52a0f041af27b82f766b1e349cbfd6317ab8044" + }, + { + "id": "436d3b88-dd5d-5214-b47a-bd9099a986c6", + "version": "MTc1NjMyODAxMTg1NzE2OTAwMA", + "digest": "sha256:cba8e3780e47884a22a81593b9cf3ab040a246a75cb2da3319fe50e5797434af" + }, + { + "id": "438c5791-a789-5643-9632-286b84abeee8", + "version": "MTc1NjMyODA5NDMyNDM3ODAwMA", + "digest": "sha256:e8dee4866f3a39ebfd226111aa5ee06a4771fb82c5814707c444bcfa1a8ee178" + }, + { + "id": "43920245-0783-584c-aa3a-2ee1abe25f50", + "version": "MTc1NjMyODE0MTU1MTM0NjAwMA", + "digest": "sha256:9ca787589da204695ee5e25eceba3c9b7c98745dd7cd126971925ccfb00ca1a8" + }, + { + "id": "439dd1a4-01e2-5efa-98dc-77c71f8895b9", + "version": "MTc1NjMyODAzNzE0NjMyMDAwMA", + "digest": "sha256:ef15a227ab947122fd3247f112b6dadb1d594ef5624d3e5d4c150ca8e23330e1" + }, + { + "id": "43a7a4ae-7c51-5d36-8f69-1744445713a6", + "version": "MTc1NjMyODEwMjI3NzEzMTAwMA", + "digest": "sha256:d9f27d8d273c6a3e169b28373f7bfc8aea54d9b44dc9b205051bb965ec19c949" + }, + { + "id": "43dbeaed-b0ce-55ff-b5a5-a14ef9c06508", + "version": "MTc1NjMyODAxNTE5MjAyMzAwMA", + "digest": "sha256:ac22d687c006638d9e7b842f6a06fa293798d3b8a576ae0855ff88c850b97fe1" + }, + { + "id": "43dc3a50-824a-50cd-83e0-1b8a0b57269b", + "version": "MTc1NjMyODAwOTA0NzY4NjAwMA", + "digest": "sha256:f48efbe2d07260debaecd2b3f49aa5f28022675dfbc2882353310fb8a9ccd25a" + }, + { + "id": "43dc6313-c72d-5756-b824-a17adff74073", + "version": "MTc1NjMyODAzNjgzOTEyMDAwMA", + "digest": "sha256:52bdee712f1665cc7540f65eb8e2e65a5fd64689f54d1cfb485e3914c121eb80" + }, + { + "id": "43e166f1-5c56-5b74-8b5c-f01f2cc2afdb", + "version": "MTc1NjMyODAxOTQzNDQ2MTAwMA", + "digest": "sha256:1e0fff6af99a11a6a3766a35cc68ed390ebe0b1f9f5ddc869e4010a3ad38b91b" + }, + { + "id": "43f38ad7-ced9-5f56-a774-01411f5ee80a", + "version": "MTc1NjMyODEyMDQ1NjM2ODAwMA", + "digest": "sha256:3f7f9771c6e0b09e9a522799b3c9c3165d85309d5c162178afe62452d18ad499" + }, + { + "id": "43faa2f7-c104-5501-8d5c-b2b05479c7e6", + "version": "MTc1NjMyODA0MjAzMDczODAwMA", + "digest": "sha256:acef93f0a34ccd6f70483d590f0573294c39bba47c76e33f14054b719ee47da5" + }, + { + "id": "440cd2dd-343e-5f4c-9c20-fb5fb42d4908", + "version": "MTc1NjMyODAzNDc0MTYxNzAwMA", + "digest": "sha256:5a337c393564bea412bf2418e7853cd481b5ca6dac6385f8eeafa5e8fafa109b" + }, + { + "id": "44269fcc-810d-5fd1-8274-2c19d2cdce1f", + "version": "MTc1NjMyODEzOTI2Mjg0ODAwMA", + "digest": "sha256:3b5589d5cdc8c897adeedf6e97a93b912dfa8ef62d9263bfb44a4c01c9fbd558" + }, + { + "id": "443787a6-7a2e-57dc-ac9e-b945da3b844d", + "version": "MTc1NjMyODExMzU3OTIwNDAwMA", + "digest": "sha256:9d84828e63b08bd67148d98f37ad8829e9fff982db50088f7d3029e24f305174" + }, + { + "id": "443fc547-43a6-5f2d-9972-0306746b48b6", + "version": "MTc1NjMyODEwMTExOTQ5MzAwMA", + "digest": "sha256:8100f2bb61f55490d0207624750eed803f454162dbeb8634dbf24acfb40c36bb" + }, + { + "id": "44463c7a-ab54-5222-94f4-86e812c8e8f6", + "version": "MTc1NjMyODE2MjMzNzUxNzAwMA", + "digest": "sha256:beb5efdc93992ef521eeeed216606663c63c1a3c81550a01c5ad2fa9be22e508" + }, + { + "id": "44588c2d-b0fe-5cca-8633-615d6512a85d", + "version": "MTc1NjMyODA3NDIxNDg0MTAwMA", + "digest": "sha256:c2f0c606fe58e0bdb425ec7b2ecfe9aed21b0c2c28b4bace4aaba391db7dfb07" + }, + { + "id": "446d911e-2987-5e99-a279-d55070fda9df", + "version": "MTc1NjMyODA1NTYzMzI1NDAwMA", + "digest": "sha256:03fbdf91d2b5597ac5bb636d6619f66cc2db833d881c2a891019b57e1f7d14f1" + }, + { + "id": "447be920-c9db-57ac-95d9-a27997829a94", + "version": "MTc1NjMyODEzNzQ5NDMxNDAwMA", + "digest": "sha256:45777a1e075a8bc8a2f4b97f78f8665432650e6a2bcfff51b6a706549716d808" + }, + { + "id": "448e938f-1d86-5ac3-9547-696bf89da35a", + "version": "MTc1NjMyODAyMDUwMzUwNzAwMA", + "digest": "sha256:e82024507940519b7fce5216b8e16e9a7469df19fe6af66c052885455d58e98e" + }, + { + "id": "44cb59ec-0551-53c0-9387-2deaee138121", + "version": "MTc1NjMyODA5NDM1NTM4MzAwMA", + "digest": "sha256:ef8848ec764c05cc9790d6a0d1f965d9920d322d4b437e27203b4537a672e537" + }, + { + "id": "44ed7361-83fd-5da9-8909-8051d896d4a3", + "version": "MTc1NjMyODAwMzk2MzIzMDAwMA", + "digest": "sha256:8016c254a6ccfe2ca6f41df33539d205bf65a9d417ba2744e63af60d6361231d" + }, + { + "id": "44efa94d-77d4-5e73-aa60-c47f8a76d18c", + "version": "MTc1NjMyODAyMTI2OTg5ODAwMA", + "digest": "sha256:b9032818d1edfa71e36bd08ce9f39b8133e8f1bdac785006f652bb2165bbd942" + }, + { + "id": "451541db-a4f4-5a9c-a0c0-842a91a0875d", + "version": "MTc1NjMyODA4OTM1Mjc1MjAwMA", + "digest": "sha256:e875e632e9cbd9e313cbbe947b7c5430fce917c8513d8e85e1e4fac3c714c331" + }, + { + "id": "452262b8-24bf-59d6-9c22-b2bd503b3be0", + "version": "MTc1NjMyODAwNTEzOTUyOTAwMA", + "digest": "sha256:df824c81aa638ad23d92557f8a47374da1f8a4a6c35d02420a347ff432133153" + }, + { + "id": "454223c9-7b47-5c24-b27e-78684d95313b", + "version": "MTc1NjMyODAyNTkzNDQ0MjAwMA", + "digest": "sha256:ba6aaae2649cf73312c82c9842b72fc50922b62fc7de8c05d10082c1e4f0f7d6" + }, + { + "id": "456b30bd-c6cb-5d1a-9071-a34bf29730a4", + "version": "MTc1NjMyODExOTcxODUxMzAwMA", + "digest": "sha256:c8ba042d4ba1cc2844255e5c3decc51b1b5c0e47042ffd4b4611821445ac1949" + }, + { + "id": "4584f0c1-32cb-5428-a4f4-13bad424bb15", + "version": "MTc1NjMyODE2NzQ2Nzc2NTAwMA", + "digest": "sha256:05c93e3598f99b44fc3bfdc69a65b8a1f799e7cced95d53ddf09fdcd86ca2c9f" + }, + { + "id": "45af3959-7488-5b9e-8c20-d07000d4168a", + "version": "MTc1NjMyODE2NjQzMzI3NzAwMA", + "digest": "sha256:7de703c8dacbecca224843475b01964188dafc738ce98a282c3b0d5f1d810668" + }, + { + "id": "45b124c5-749d-56bf-982c-21da61890ead", + "version": "MTc1NjMyODAzODgyMjM3NjAwMA", + "digest": "sha256:4398dce22ccbc3b8354e478f7b43565dbbca7a70316f09ddc7c60d609aa46fda" + }, + { + "id": "45b1bcac-7ba7-588f-906c-33d6168389da", + "version": "MTc1NjMyODA2Njk2NDQzODAwMA", + "digest": "sha256:0cd0900031939b0c6db408febbe774994dc81abda7e9c70bd9c7b69804e075c6" + }, + { + "id": "45b4dea0-18ec-5712-8f6b-5d1f46b0833f", + "version": "MTc1NjMyODAzODg4ODAyMjAwMA", + "digest": "sha256:3e9e39f73cf24c56b567e894f9b6c82ca3ba251f81b7d26c5cd032aa392458cb" + }, + { + "id": "45f45b99-f237-5ea2-ad96-0443e897e821", + "version": "MTc1NjMyNzk5NTMwNDYwOTAwMA", + "digest": "sha256:f56771bc50e41ded5bd0d33b75ae07a8a606bcea72f9da0da664a7f9114b060a" + }, + { + "id": "45f53b5f-5ad5-5ff4-a9ed-efcee5be7066", + "version": "MTc1NjMyODAzNDc1MTU4ODAwMA", + "digest": "sha256:d8783ca4d1fce8add908df7f5c8b1dc605921fd99789af711b690b428227e0a0" + }, + { + "id": "460a9cc4-847f-5098-8e88-0cdfd0aafa45", + "version": "MTc1NjMyODA3MjU2NjI0MDAwMA", + "digest": "sha256:f8a2d71e726f087ee48dee49f3c1ec9371f0b454fc3e018b700ae9bf4a993bb0" + }, + { + "id": "461298bd-9fe9-5dbb-a1db-823365de6aa3", + "version": "MTc1NjMyODAyMjA5ODgyNDAwMA", + "digest": "sha256:d3b2e9bdd09e4434fa0ecaacd94ce793caf6af4691652315a80bdaedaf5837b1" + }, + { + "id": "4633e692-b3cd-5dd9-b61a-2c60d510cb65", + "version": "MTc1NjMyODEwODgzMzI0MjAwMA", + "digest": "sha256:630bc9d79c97c08e2b3f8b14b799828715ae7b3c4e6ab952736686b123213cd0" + }, + { + "id": "46413ec7-eb7d-5edc-85cb-c74f48eee5ca", + "version": "MTc1NjMyODAyNzQzNjUzNTAwMA", + "digest": "sha256:258d96fdc72cab0f46e26467092601c14a3a3e8c648ad8dceafd5e1fc357fd6c" + }, + { + "id": "4649aa4e-7aa6-5a18-ab92-ee8d2bc6bcb8", + "version": "MTc1NjMyODA2NzEwOTQ0NDAwMA", + "digest": "sha256:ff815a8645340c24c9bc5084a34fc8cc7dc71311b790b4fce32cd9acf47faf6b" + }, + { + "id": "4680781f-4fde-5d8d-84e0-0a3ae2160eb4", + "version": "MTc1NjMyODA2MjU5MjY5MjAwMA", + "digest": "sha256:577ad1db706d708679b47befd5bc9f83414c31012d7a8aa800a0c2bd92fd9a34" + }, + { + "id": "468f9434-eeff-5553-a536-d5b1ef4d573f", + "version": "MTc1NjMyNzk5NTI2MjAwNzAwMA", + "digest": "sha256:08aebfff660beacf7f76545c32468e9875284ef6dd46a66cabacb923768d3bdb" + }, + { + "id": "46ab2591-a43d-56d3-8d58-7188894fbe73", + "version": "MTc1NjMyODAxMTg2MzQ0MjAwMA", + "digest": "sha256:dab2c6f670af180da39e747cf72ff70ed7e470a87af8a6afe9162fb0295d87d3" + }, + { + "id": "46bc51bc-8593-5048-97af-47b05fba8972", + "version": "MTc1NjMyNzk5OTI3MTU1NDAwMA", + "digest": "sha256:a12019c32d6a39430f68d7e0d2f7ae576a7881946cc4c760768c033d9222b2f4" + }, + { + "id": "46ef294c-e525-586a-a331-a272899cd8d5", + "version": "MTc1NjMyODA5NzczOTIwMDAwMA", + "digest": "sha256:846de69d1f7faba6d682673944d5f4213ce21009e6d322e3b55ed278feab4db8" + }, + { + "id": "471fbbab-056d-5c99-bf12-9ac934979ddc", + "version": "MTc1NjMyODA5NzgxOTk4NjAwMA", + "digest": "sha256:44d0d4b8c89f7a098ec6338720bd662e893101ee5c6b8faf48847870a8d99398" + }, + { + "id": "4756438a-fca1-594a-9b4e-b12647f0a639", + "version": "MTc1NjMyODExOTU4NTE1NTAwMA", + "digest": "sha256:27f74247d628d5950af021da118feaf398d8b4f94e2ea12c14d29124ccf299e4" + }, + { + "id": "4758e566-851e-5a00-9e1a-c47f047e5b42", + "version": "MTc1NjMyODAyMjE2ODgwMzAwMA", + "digest": "sha256:a91276221704575312f3613ba25b2ce98e49272241c1931b27cf5c78c1a52487" + }, + { + "id": "475bb035-4419-5c08-bf68-a652071bcbdb", + "version": "MTc1NjMyODA5NzgxNTQ2MjAwMA", + "digest": "sha256:b1e683d0ea1f5e10310b6837e4850617838053cee71b070f9cd38b5c2164c257" + }, + { + "id": "4770774f-ff2b-504f-be98-76c94d242ea6", + "version": "MTc1NjMyNzk5OTMzODAwMTAwMA", + "digest": "sha256:7bf52a97869bc324fec5577ff77ac51ab4c733dddcd0f31bf2e0cd8dc306a9c9" + }, + { + "id": "47d4aa18-47e2-5155-970d-8f8c83149136", + "version": "MTc1NjMyNzk5ODIxNDg2NjAwMA", + "digest": "sha256:d95dc6fae18d46926279ccb04990aa5bfe119ce434d7eb9d1d240138667f9de1" + }, + { + "id": "47d789e2-fb9e-5f5c-bc6a-099cc118e656", + "version": "MTc1NjMyODAyNTc0ODUwMzAwMA", + "digest": "sha256:15d101b19c9e5bcfcc2332149fe9c64429558450488d28fe0029b72ee0774a8a" + }, + { + "id": "47dbe7d4-bfd1-5bd6-93d4-8c2f515abf6d", + "version": "MTc1NjMyODEzNzU4MTk2MzAwMA", + "digest": "sha256:cf871a077a72e3fb1bb3467459047889b1798f475fd8690c4f1effb9330b7fe6" + }, + { + "id": "47fc823b-d1ec-5c32-b044-92c11c1c2a01", + "version": "MTc1NjMyODE0MTY1OTQ5NTAwMA", + "digest": "sha256:9d68558b53d181b499a25945e1c76003b6804a434ea961cf907c41531f63626c" + }, + { + "id": "48115c0f-d83f-5bdf-b24a-0f95e169ad49", + "version": "MTc1NjMyODE3MjQyNjcyODAwMA", + "digest": "sha256:a0c8a2bf2ead9e62e55f00ee33d591f3c519fc7c7c8e85c5b7713011e4098619" + }, + { + "id": "48295476-ebaf-5ad6-bdcf-cb18b0ac604d", + "version": "MTc1NjMyODA3MzAxNTUyNTAwMA", + "digest": "sha256:00041d915a95b6fcdffd8e089fba4decff0940b710c0635f30992a21476ac1c7" + }, + { + "id": "484d518d-e81c-5d62-8c70-900209f7d5f6", + "version": "MTc1NjMyODAyNzc2MTI4NjAwMA", + "digest": "sha256:85bb2263c946b4cad7c61b81b0a983a92f87fdc71732c1bfb5f5eca0648bf588" + }, + { + "id": "485a4a4b-ddae-5899-b573-850a88dca67d", + "version": "MTc1NjMyODA2NzA1NTAxNTAwMA", + "digest": "sha256:beb8e427a9e838ead16bab7bb1c274d0a661b453ef7986383e7d5ec24533f825" + }, + { + "id": "48626ab7-bb34-58f5-9794-1cff43d18548", + "version": "MTc1NjMyODA3MzI2NDk0MjAwMA", + "digest": "sha256:1710ec3f7dccc2d8206524c7cd33718be51cb471e1a67b422e67f6e88254bd09" + }, + { + "id": "4888597e-f8e8-5937-a4f6-38704198a472", + "version": "MTc1NjMyNzk5NzA4OTE3MTAwMA", + "digest": "sha256:9347676fa673f31c52356ade8fad6e909117405a29f8286aaf47736f0389c00f" + }, + { + "id": "48899fbe-112c-53e7-b516-f02face4c9f7", + "version": "MTc1NjMyODAzNTk0NzUzNDAwMA", + "digest": "sha256:669da63f739b067508878de1cdca555613a2de775573a6c3c397b91df84ef326" + }, + { + "id": "48a1934d-4405-534e-a582-bc293862a58c", + "version": "MTc1NjMyODE2ODg3NDIwOTAwMA", + "digest": "sha256:831135e8d3d5a88e262187e515b54aa78e5f3de1e46d802fae78b243ea467d8c" + }, + { + "id": "48d956b6-e432-5b81-aceb-c7e3fac5a856", + "version": "MTc1NjMyODA0NDg1ODY5NDAwMA", + "digest": "sha256:1c4a5454147ab3359d8f301ac40e674918850eb93fe5989f5aa3ddb50df6b771" + }, + { + "id": "48e346eb-9dea-5602-9692-03883b6395a5", + "version": "MTc1NjMyODEyMDQzNTExMDAwMA", + "digest": "sha256:0739427e8be3637abcbdc90494c7324892ed517130c29d92f66665174da3976a" + }, + { + "id": "48fe95eb-f702-5f8a-a755-f71289785f8f", + "version": "MTc1NjMyODA4ODYxMjAzNTAwMA", + "digest": "sha256:360adf5f35a991a0e49f9358805ab82c48aba35ac7ec01fbb06121a76796ef4b" + }, + { + "id": "4913d455-2ba7-5592-8d88-22fe967b99ca", + "version": "MTc1NjMyNzk5OTMzNTkyMDAwMA", + "digest": "sha256:adf15638bd1c74e7f2cb50b17dd54187478bee8569620204b48c52f082bad8fa" + }, + { + "id": "4940bdaa-8f2c-5d44-924a-4c46255c1479", + "version": "MTc1NjMyNzk5ODE0MDIyNjAwMA", + "digest": "sha256:ccb66f5d6d1b6418d7ac681caf4115c25cf7c4630aa19008628bbdec89f254ae" + }, + { + "id": "49432e06-712d-5517-ad79-0caa37f6a6cb", + "version": "MTc1NjMyODEwOTE5NTUyODAwMA", + "digest": "sha256:8df3a87c88d07ca65eff1f6d97274f8b13cab8e08e9d925c6103241a127b0689" + }, + { + "id": "497571f8-1bc9-5c26-ba5d-3a05e4c3586f", + "version": "MTc1NjMyODA2MjE4OTcyNjAwMA", + "digest": "sha256:1caf96e121ba6654b52b85fb515c42b81444cbaf24be0409d8eb3459af8d2f79" + }, + { + "id": "49804ad9-e54a-563c-a7ce-a4a5daefe84f", + "version": "MTc1NjMyODA1NTY4MjE0NzAwMA", + "digest": "sha256:a3514c19051f3ebcba58321e5024cb8b08cde119c00029892316d5036e5f0c6e" + }, + { + "id": "4983dcf3-c8a5-56bc-bd13-17aefa08ab9f", + "version": "MTc1NjMyODEzNzUyNjM0MDAwMA", + "digest": "sha256:61402093353d743603025f15d749a42537b3c0dfef859fd68360fe12ef28b851" + }, + { + "id": "49928011-8145-5929-972e-6ceefe16c190", + "version": "MTc1NjMyODAzODc1MjExNzAwMA", + "digest": "sha256:ef7cdc20fb739e3682011d9d275afc6b9248f105b8f3939ccb79bee120fd2e1f" + }, + { + "id": "49b77597-c1ba-55b5-923c-ee74748a7781", + "version": "MTc1NjMyODA2MjU4MTc0NzAwMA", + "digest": "sha256:c4798aceddd468316fbd292882999cd8cb5a0ec1d8f30b8f6fbe35cf99263bc8" + }, + { + "id": "49c0be65-ac45-59e1-83dc-451907ccf6bf", + "version": "MTc1NjMyODAxMTA2NzM5NjAwMA", + "digest": "sha256:db4e819515b8c50baeef3fafe0db21de7fe93b83725103bd9835bb2c6b91e6fe" + }, + { + "id": "49c97c42-ef59-54c7-89bf-19630f95600e", + "version": "MTc1NjMyODAwNjI0Mzc0OTAwMA", + "digest": "sha256:4707dfa0a321a659887d13e55d917d2b8fbac7bffbc701a77f6c9c01352075b7" + }, + { + "id": "49cf8119-f5d4-5e94-8687-83971a143059", + "version": "MTc1NjMyODE2ODkzNDAzODAwMA", + "digest": "sha256:036635049e169c39bad5a211965f2449bfbd637527cc4e1657beedd9c9e1ead7" + }, + { + "id": "49d27e8a-3781-551e-8614-280fc952012c", + "version": "MTc1NjMyODE0MTQzNTc2MTAwMA", + "digest": "sha256:3734f6ace24ede454cf8db38316fc0b24555dfe4a592a084bc79084c912fcc27" + }, + { + "id": "49d5dea1-08ae-55d8-bcb4-928189b3c317", + "version": "MTc1NjMyODE2MDUyODE4OTAwMA", + "digest": "sha256:408785ea8591e481629a5c6c8b7d9aeac6f601b5bf7ec5bd99b84b03eed9f443" + }, + { + "id": "49e5c479-6fdb-5af3-b5cc-98819cdce731", + "version": "MTc1NjMyODAwOTAxOTY4MzAwMA", + "digest": "sha256:8ac7c12ec4aee969fc5485da784daddba9a8e0728db7a57d27a31728a4448c96" + }, + { + "id": "49ef1229-38fa-5c16-b2f2-d6a1dd222b35", + "version": "MTc1NjMyODAzODc2MjM4MTAwMA", + "digest": "sha256:47d7f3a5e7d8737ced130e7814f9d6df0d50162b7f4976291c76d89ea5b23891" + }, + { + "id": "49faf627-e966-531d-a35c-0c8c2f31e2c9", + "version": "MTc1NjMyODAxMTYxMjg0OTAwMA", + "digest": "sha256:00ba57dbdeed4f0b9bebecec20246bec2f9f9558ae6d419a5ac4ecd447099130" + }, + { + "id": "4a2bd2ce-10fb-5257-bc08-a7e7598a861a", + "version": "MTc1NjMyODAzODcxMTAzMTAwMA", + "digest": "sha256:e6f81d6074d99f109983b1972706735dec8ae6c08ae88e008f1f40b7297c7f5a" + }, + { + "id": "4a531696-02c9-5bdd-8b33-d4049e48398a", + "version": "MTc1NjMyODAxNTU1MDUzMzAwMA", + "digest": "sha256:2f08ea66609ffdeca77795d1c754e626416079b9b0ae4e4b40c31cafd5e8a05a" + }, + { + "id": "4a566cd1-a5fa-507f-9534-336b9a6411d8", + "version": "MTc1NjMyNzk5OTMxMzc0OTAwMA", + "digest": "sha256:f415bb9e9b01d9c83592ed2ab534bc7e812d8c779a03a1827f4ebaae431ec1f3" + }, + { + "id": "4a5a0570-f605-5e97-adf5-bacbbc68cc4e", + "version": "MTc1NjMyODExOTE1MjI4MTAwMA", + "digest": "sha256:33a0acc88d4abfcc382da6e8b604bbe94366e22fae0d3305c623fce264c29d68" + }, + { + "id": "4a7f4326-3b2d-595e-8e3d-6792552a9780", + "version": "MTc1NjMyODA1MjYwMjg0NDAwMA", + "digest": "sha256:4be88a1bfb995aa6803dc9c07a98217215c273b34cb710de58422d34ce73fbe7" + }, + { + "id": "4a939f73-47b4-56d8-9b17-09c68537fb1c", + "version": "MTc1NjMyNzk4ODY5NTU1NjAwMA", + "digest": "sha256:410a798b3cc4d91ead0884e29d0ad30556158213bd3fa1dd51120c94d4217d23" + }, + { + "id": "4aaff8d7-dc1e-5dd6-b130-19f84de6839d", + "version": "MTc1NjMyODAxMzg3MjMzMTAwMA", + "digest": "sha256:86465c9a0c10fccd563573f66dad44b7f01c43ff398ba430206db1bcbba4b359" + }, + { + "id": "4ab70a42-0f74-53ed-ba76-4991f18f644e", + "version": "MTc1NjMyODAzNDc2MzA1MDAwMA", + "digest": "sha256:438dccd4dc2b3a3550e016635d234e8b23bd9212425f055e6686992b21d47512" + }, + { + "id": "4ab99bfd-b5b1-53d5-beda-5702d4a65a96", + "version": "MTc1NjMyODE1NjUxMzE3NjAwMA", + "digest": "sha256:9ef3ea42918d5477af2e5bcff198d4282d7a00b805bbe23c2a4ade71d7554db0" + }, + { + "id": "4ae0e5de-4d09-5613-89e4-b10eb85da725", + "version": "MTc1NjMyODAxNTMxNzUzOTAwMA", + "digest": "sha256:320e99773d33b866eb09f243dd10ed022f7dcbbf05446770585a887b20fd2122" + }, + { + "id": "4affed4f-785f-56fe-9335-d32f98ccac23", + "version": "MTc1NjMyNzk5MDM2NDU2MDAwMA", + "digest": "sha256:1f459422f9c6ba2d2aca6b9ced2bb76d36310081e9d8e8c72168f352e1a7a358" + }, + { + "id": "4b027a3d-fddd-57c4-8a45-bf0778d1197d", + "version": "MTc1NjMyODE1OTE3Nzc0MDAwMA", + "digest": "sha256:bbd3425e284612e6804ed1c78cb3f65003761e8717be37172aca8c244c79d24d" + }, + { + "id": "4b0a04c5-5007-55e2-b967-f77555d6e515", + "version": "MTc1NjMyODAxOTM2NTkxMDAwMA", + "digest": "sha256:d57ea697b727725ceeb8cb72660ddf5b4e75fc0e0aa944195253aa082169e7f3" + }, + { + "id": "4b244545-b2ac-5752-acc6-69d11ebe55ef", + "version": "MTc1NjMyODAyNzQxNTE4MTAwMA", + "digest": "sha256:0d434f7056451bc9c2b87bf49badaed9a958ae30cb8f8076ba5f2a5aed25ecae" + }, + { + "id": "4b306bdb-9b86-5e41-ac2f-0650ecf37576", + "version": "MTc1NjMyODE2MzE2Mjk0NDAwMA", + "digest": "sha256:c1596f6e6965d4aecc2f9eccda78c607c29815101c400b175868997b5fb68f05" + }, + { + "id": "4b341d3c-b3ac-5053-be39-b16e903d356c", + "version": "MTc1NjMyODA2MjE4NDU2MDAwMA", + "digest": "sha256:94334e725051cd8035031e23bd15c75b1fb4894c2130bb1f6430839d8d20b07a" + }, + { + "id": "4b38fc6a-d405-545b-a1b6-7f39f8bc18e4", + "version": "MTc1NjMyODE1NDU4ODkzOTAwMA", + "digest": "sha256:6a07dc158eaf1b37107399eb4d116858b3964da48c9f42d0a03b4239a25a799c" + }, + { + "id": "4b9fb0c7-5ba8-5165-952f-3fabd16614e7", + "version": "MTc1NjMyODAyNTc2MjM5NTAwMA", + "digest": "sha256:869a85f21c75af8ca9712116921cfbe17c93f0d0a7cb17c48eee7cf8b94b2cbf" + }, + { + "id": "4ba71c50-cb96-5786-b932-3f7f93e67ba7", + "version": "MTc1NjMyODAwMzg5OTU0ODAwMA", + "digest": "sha256:928dbd0849b458f828276f064fa734a03d042dfc23156dcbad23dce10bd84bd9" + }, + { + "id": "4baff105-d184-5275-9d29-709fe79e9943", + "version": "MTc1NjMyODA4NzY4MjExMzAwMA", + "digest": "sha256:add3fba27429e3b38692d5824302015e6a276acc92f02d143ff59d1907963ffb" + }, + { + "id": "4bb675ba-be33-5b3c-86e8-5f81159b80b3", + "version": "MTc1NjMyODAyMzQxNTQzMTAwMA", + "digest": "sha256:ee13706a4a777baea3c957e76e9b13003403b5eae02f48d439b09f37717453af" + }, + { + "id": "4bdece91-7f05-5e6a-935c-472b47b6b5c8", + "version": "MTc1NjMyODA5NDU1OTA2MjAwMA", + "digest": "sha256:edc151d63a9be57f557cf6bae9740b56c45597891cc0542cfcb35b38bbbdd893" + }, + { + "id": "4be7456a-92d2-5955-b969-404a2b34a707", + "version": "MTc1NjMyODAxNjQ3NzMwODAwMA", + "digest": "sha256:f5627938a59c587d874d2a51599b8b2ff6e259c5c46b1263f83442e2ad22c429" + }, + { + "id": "4bf79ee8-37fd-587c-859f-9d5c9ff7f20b", + "version": "MTc1NjMyODAwNTExODIwMjAwMA", + "digest": "sha256:e4c8d1d68c8b0e8570f17afb32fced723bf51d7b306777fc6dbbe807a0f4b93f" + }, + { + "id": "4bfb5e05-9f46-5f36-a7de-ad66a7ea7cc0", + "version": "MTc1NjMyODE2NzQ4NDUyNTAwMA", + "digest": "sha256:ac20decab98e45601cfc589431cb2a1379df1f778b42b9575e1adc18e6916d78" + }, + { + "id": "4c1d3c7d-3e63-5379-96a3-55f9dafcb962", + "version": "MTc1NjMyODEyNjU3MDY3ODAwMA", + "digest": "sha256:538fa130cec6461082589d89faa7ece145188d450a84475d1db8b41bd137394e" + }, + { + "id": "4c398005-00ed-5f0c-a80d-b0d154d8735a", + "version": "MTc1NjMyODAzODg1MzAwMDAwMA", + "digest": "sha256:a2184dd40e4eec6a9f71d19e92afeeabf34abc1355b1a0513ccb9661fb4d9f7e" + }, + { + "id": "4c3bfd61-1fca-522d-becd-4fa19a30bb96", + "version": "MTc1NjMyODAxNjQ5MTY5NzAwMA", + "digest": "sha256:ac5adfa2faae4f73b2972be9f9d272afbab73efcb33c62aeb8c623b7ecdc4251" + }, + { + "id": "4c4e7a0e-4eb6-5740-aa36-4088b8d93c5f", + "version": "MTc1NjMyODAxMTkzMjU1NTAwMA", + "digest": "sha256:dbacf5c1114aaa4aaec01f2e850c540d702c890b6ca33e144a44b465d6b7f421" + }, + { + "id": "4c5d05f4-6159-5868-b492-2330c031ced5", + "version": "MTc1NjMyODA2NzE1NDQ1MjAwMA", + "digest": "sha256:9e9a0c5fa66f8529fcc7a9be37c62eebcbcf187ecac196ab029c5f02017c8bea" + }, + { + "id": "4c6a6258-d25f-59ab-86c5-1a622164fa86", + "version": "MTc1NjMyODA2MjE2NzE1NjAwMA", + "digest": "sha256:d9f281830194501e465bf44bee5be69cb064005901f57daacc4377a985fc6746" + }, + { + "id": "4c6c7ea5-d53f-50d3-90cb-d31fd92d2ef3", + "version": "MTc1NjMyODAwNjI0MzQwOTAwMA", + "digest": "sha256:34b260a1c602b9a7c8bc9a143a5fa0871aa82559aa47d6f68f2ad9bce0b91cad" + }, + { + "id": "4c70db5a-a3c5-5a4b-a631-6c9a8fedc2ab", + "version": "MTc1NjMyODEwMjQxMDkzMzAwMA", + "digest": "sha256:d1b6eab54305ba25fc0b9c90c34589f84adfce44a4f464f8c69f5c465f7274ad" + }, + { + "id": "4c7bd369-96fc-5619-ba62-2f8d470e5b49", + "version": "MTc1NjMyODE0MTUxMDc0MDAwMA", + "digest": "sha256:f5d84b34e0fa0337e8d0c6a0392bfab593f3b5da62aec0c6c250aa259e4cc924" + }, + { + "id": "4c8d22be-6319-5798-be4a-2c9ed3a85321", + "version": "MTc1NjMyODEzMTA0NjA3MjAwMA", + "digest": "sha256:69d6e76380f72756af13252ebbdd5968d7b9f88befb49852fb7c88b5c59ae5cd" + }, + { + "id": "4cbfaf2f-daa3-57ea-9d08-dd04976a80c0", + "version": "MTc1NjMyODE2MzQ3NjkxNDAwMA", + "digest": "sha256:769026d8076b48f4f74e47fbcefb81f5ba7fd64b1a6751fedf47342e5fb2f9b1" + }, + { + "id": "4ccc5ac1-f780-5095-b5b1-8b6f867546e0", + "version": "MTc1NjMyODE1NjMxOTM3MjAwMA", + "digest": "sha256:2020ded70c4048da6a386116674055642d4bc5af90fd502eaaa66e2d1308eb11" + }, + { + "id": "4cd84a5e-c4fa-54d6-9881-97734a06f176", + "version": "MTc1NjMyNzk4ODcyNzc0MTAwMA", + "digest": "sha256:4bda66fe0834d216f84fc57e4c8c7d29b87dd273982b023b1b7a12167bbc5f8b" + }, + { + "id": "4cd8b2b5-018b-57cc-a57b-5c8d80998a56", + "version": "MTc1NjMyNzk5MDM5ODUyODAwMA", + "digest": "sha256:738a92b95ab95407abf1f1fd6248f4b3434e6bcf9cb064d31f4ab3fd60d83875" + }, + { + "id": "4ce99ba2-e1e4-5389-b2b1-b63f17cf98d4", + "version": "MTc1NjMyODE2Njc5MzU1NDAwMA", + "digest": "sha256:def332289a1d7e1c4d67f5fd543b33f4593b422faabf9c892c3a4075838b2eec" + }, + { + "id": "4ceb5225-d5bd-517e-b699-244108c48da3", + "version": "MTc1NjMyODE3NTA2ODQzNDAwMA", + "digest": "sha256:15b47bd85ce1f108825773b37f22f0d36da0bb7585358fd988cf020f3a6bd4f9" + }, + { + "id": "4d01e495-baea-58ab-b28e-3c6837ced067", + "version": "MTc1NjMyODAzODYzNTgwMTAwMA", + "digest": "sha256:395d717c138d1198bf36feabd80e275433e6769f6b3063aea24ba0e5a7e89715" + }, + { + "id": "4d25c4bd-7d74-50fd-bf6e-f1d42d798c8c", + "version": "MTc1NjMyODE3NDc0NjQyMzAwMA", + "digest": "sha256:2ffa0af9f1224872cd5a0c62790f698e537b35422153e7abfdf6d8d373d13665" + }, + { + "id": "4d52af95-5d44-56a3-a933-6a13c951b8d2", + "version": "MTc1NjMyODEzNjk3MjE1MDAwMA", + "digest": "sha256:748e4b9e13cc35344c6c9501c171159c1d656ecc140f195bb9f227dd11cc31db" + }, + { + "id": "4d64d432-07e3-58f3-81bd-a255c3ec620a", + "version": "MTc1NjMyODE0NzM4NDMwNDAwMA", + "digest": "sha256:1d0eb7df236fdf408150aae77539e7e22ca868d3d5b3fee85ccb005abe704b6e" + }, + { + "id": "4d70750d-31f1-5517-b8cf-e3c9e02d49f0", + "version": "MTc1NjMyODEzNzUyNjg2NDAwMA", + "digest": "sha256:280707e1e9f96b7edbf782bb5d738dc712f02d2804588be848ec3b4859a34566" + }, + { + "id": "4d7e5aa2-e6bd-5afe-bace-8bac55c590e7", + "version": "MTc1NjMyODE3NDc5NjA5ODAwMA", + "digest": "sha256:87560cbeabe069423c97a607c1c98ba43d690f7d8f0045f08733c7514641bbb5" + }, + { + "id": "4d8f9ca6-00b6-5a56-b016-e752c67f241b", + "version": "MTc1NjMyODAyMjEwNDM1MzAwMA", + "digest": "sha256:735cf7331c7741c505b5548cda3f03b05cf7965a71fdc25cfa5149d33a6a3f9b" + }, + { + "id": "4da562a2-9feb-5e49-b7ec-b0976c326cf2", + "version": "MTc1NjMyODEwODc3Njc1MzAwMA", + "digest": "sha256:30063fe892da439c6d740fe77ebacddaa8e2d6a7617e0cc2e9c57cb655c70e4a" + }, + { + "id": "4db79b58-5064-5c2a-a91c-fe38ab03175b", + "version": "MTc1NjMyODAyNjg4NzkxMDAwMA", + "digest": "sha256:f38d4e4c226a280a171ec8615a251db22d0c07e1dfe8acce98fd6df4dfc1d0d5" + }, + { + "id": "4db9e7fe-65fd-5733-9fcd-0cff2bd1fea8", + "version": "MTc1NjMyODE2ODg4OTc4MjAwMA", + "digest": "sha256:fe12c5c8e41db1e81783d50cbb39f318f02a7f96c1432b5f339d8d8c669d5966" + }, + { + "id": "4dc07880-92cb-5e37-8fe2-b1e47bcf59fe", + "version": "MTc1NjMyODAxMTYxMzIxNDAwMA", + "digest": "sha256:0d68a584cabc679a2bb947ea495ab431b46cc1879ea0b0ba91dfd5bdb5dacfe0" + }, + { + "id": "4dd07d16-d41a-5e4e-af24-5383b709d97d", + "version": "MTc1NjMyODAxMzg3NzU1NDAwMA", + "digest": "sha256:d46d724699652047a68a39e52f1066cf086ba84f7cedfcbef284c1c824985d5d" + }, + { + "id": "4dd252be-9370-5aca-8713-0dd7b802a373", + "version": "MTc1NjMyODA1NDA3MDI4NTAwMA", + "digest": "sha256:57a8ed65ceeb6b9034a149bc6e5604a3e9afcd86caf285188f3d5b6267825c63" + }, + { + "id": "4dd9d6b1-afae-51c7-9e59-c87e5a0debdd", + "version": "MTc1NjMyODA3MzA3MDk3MjAwMA", + "digest": "sha256:99d78c1d5b66c86b31499a83d8387d605b76618622f597d2aee57f11a81abe4a" + }, + { + "id": "4dddda80-764b-59b3-aade-0ac8ac026a27", + "version": "MTc1NjMyODAyNTc3MDQ5OTAwMA", + "digest": "sha256:7a0832795f246fa94af2fe13a2cfb740918f2d00cf1172f6029b93eb33262051" + }, + { + "id": "4ddeee1e-c177-5b5e-9608-6eebcc8adf43", + "version": "MTc1NjMyODAzNjkwMTYwMjAwMA", + "digest": "sha256:dc3222d0ff8d5d80dbe88431bcba48b6eaecb626b1fc1dc1b8c2d2cb4c2b122d" + }, + { + "id": "4e100ffa-bd3a-55b2-99f5-1d8c9145874f", + "version": "MTc1NjMyODE3MDQ1MjAyNDAwMA", + "digest": "sha256:bcbb084ac28fbf1ea5fde17aace28fb42961b835ac3c8f0a1429d062145bd368" + }, + { + "id": "4e27b99e-220f-5434-869a-1e12dcbe6db8", + "version": "MTc1NjMyODAxNjU1NDY0ODAwMA", + "digest": "sha256:c26a31d201e1366410d5d399e301674075427252439785a6de5674ab72e31c0c" + }, + { + "id": "4e857b90-7e26-59b5-8f4f-1653908f680d", + "version": "MTc1NjMyNzk5Njc2NzUyMTAwMA", + "digest": "sha256:124e14314f8700fa3e1e7b5835f365c7466c92fa5423db4c33625f5d3027f9a9" + }, + { + "id": "4e87a83a-834a-5ceb-b06c-2b21327ed2ce", + "version": "MTc1NjMyODA3MjYzNDQyNjAwMA", + "digest": "sha256:e9bf6404d270e35873b87e8f3a1a1e96447e0b1fdf7001f5f47e29478667ae46" + }, + { + "id": "4e92fc37-eedd-5439-b76e-8bb25a38d101", + "version": "MTc1NjMyODEzNzU2NTgwMzAwMA", + "digest": "sha256:9caafaa54438426519ba4514065c99dc1a748cab62f54eb07f793809c6b53b42" + }, + { + "id": "4e95d113-3f51-573e-9e67-33b4d183bf69", + "version": "MTc1NjMyODEyMjI3ODYxNjAwMA", + "digest": "sha256:d4a12eaf490d5af7d2d4a3e5af0d593262f33531b85f80260b0b0e6d6d6eae8e" + }, + { + "id": "4ec92413-4553-5807-b54b-acb7f8975a11", + "version": "MTc1NjMyODAzMDc4MDgzNDAwMA", + "digest": "sha256:d0bdfa87c2ff30ecf7e5af802438375eb0da4202361b3dd115532e64f9d99686" + }, + { + "id": "4ecf7582-6fa9-5e28-9495-94384b0bed10", + "version": "MTc1NjMyNzk5MDM2MjY4MDAwMA", + "digest": "sha256:eb0ab7436c775e92faf885abc2ce19e8f6cf1c9eed5332d5fc0597cdd8840774" + }, + { + "id": "4ee1cc95-a113-5a6b-87d6-76bb007131d5", + "version": "MTc1NjMyODAzMDc2OTE4NzAwMA", + "digest": "sha256:8e0215b444f74d4972d4e725275693684348b4c9ea2632d95943d8171bddf25e" + }, + { + "id": "4f06e652-39e4-53d8-b36a-a63013d643d5", + "version": "MTc1NjMyODAxOTI3MDI0MTAwMA", + "digest": "sha256:f57793f70e266942669abeca9caa383896bed2ddba364878772be414911d1b40" + }, + { + "id": "4f283bb8-47ae-583e-aea2-02d9759005ae", + "version": "MTc1NjMyNzk5NjcyNDQ2NTAwMA", + "digest": "sha256:818ad677965a52b0a481e6a226e637483ac9b1cd0ea14ecf46f99effb633c50b" + }, + { + "id": "4f3a4b20-a002-5562-8c2f-00c8e7c908c2", + "version": "MTc1NjMyODA3MzA3NzA0MDAwMA", + "digest": "sha256:e4027acc5d17021843158a8df6b4330bf6a3888878b9ec9e8357b5d3702bd90b" + }, + { + "id": "4f445d3d-9de0-5d69-ba66-589c3326cf84", + "version": "MTc1NjMyODAyMjE5MTA5NjAwMA", + "digest": "sha256:c5365f3eaa2c20bb2d672a4031555b3cffdf6a1f378e78fbd308ca6e538c2ecb" + }, + { + "id": "4f4bfd07-ee9d-5dca-9025-12253a285263", + "version": "MTc1NjMyODAyNjgyNTM3NDAwMA", + "digest": "sha256:960c1804006ea00353572f38881d009af4670d4f34211a5c92776a8f0bee2132" + }, + { + "id": "4f525f6a-1a9b-506c-952c-a25e677c2eed", + "version": "MTc1NjMyODE2NjQ0NjM3OTAwMA", + "digest": "sha256:c61bf27581fcc5caf960b22ba1b99350932fef648dab1f0daddf7f29e995a126" + }, + { + "id": "4f94c0a9-f22f-5aae-9f25-77786107e572", + "version": "MTc1NjMyODA0MjQ5MjExNjAwMA", + "digest": "sha256:e4eae60d7699011dd213d332db0e1a07cd4d28986bcf0a7b4aaab54c78c62e75" + }, + { + "id": "4f9b9ad6-092a-52a9-91c8-14da189e8395", + "version": "MTc1NjMyODAxOTMwMjI5NDAwMA", + "digest": "sha256:ab6082d9b4f645cf63154cb4b3f602d63ea83d21f4b0ab6b42cf9593289ac02b" + }, + { + "id": "4f9caf6c-790c-5b27-b720-ddffbd81bc0a", + "version": "MTc1NjMyODEyMjIzNTcxODAwMA", + "digest": "sha256:c1da56062e5072dcc60ea78f319ee81e0e85488118034d687f6d77e66884f01d" + }, + { + "id": "4fba4a99-af17-5c31-b44b-39b9a1ca6924", + "version": "MTc1NjMyODAxOTM1Njc2NTAwMA", + "digest": "sha256:f94a0b45c0ba2817f97957a4f016eacfcea797c2250bf480a2ee8e6a25d429ec" + }, + { + "id": "4fd058b2-1417-501b-a5bd-6af1b975358b", + "version": "MTc1NjMyODEzODIwNzExNDAwMA", + "digest": "sha256:ca5ae4524b716c74266c6f8ebff900839695f2ba42244436f4322503c1e45de3" + }, + { + "id": "4fd0e556-92ff-5164-8644-72cb1d293fea", + "version": "MTc1NjMyODAxNTI2Nzg4MjAwMA", + "digest": "sha256:705f51da985e202b08d0b52ad035320af47d16d01c6c37fb1b62e9546aa6337c" + }, + { + "id": "4fe35ee7-2885-5e9c-90b8-6e643e05fc5d", + "version": "MTc1NjMyODA5ODY5NDg4MDAwMA", + "digest": "sha256:a274956a47b16e3c87adfa454deba2eaa3f6c67498fe9c0889ec01b9425f1f9e" + }, + { + "id": "4ff14452-3d19-5981-8618-dfd95f8e8dde", + "version": "MTc1NjMyODExOTcxMjQzODAwMA", + "digest": "sha256:2d94ee9b2ce3af2ea95d7af7d35188fa9990728026be640044d92911a23b2795" + }, + { + "id": "50157548-8944-59e7-8559-c5499b3da46d", + "version": "MTc1NjMyODAwNjE3MzEwNjAwMA", + "digest": "sha256:187e3dfb6dc82f2349ef47002d260460395b8e15119580dae33ae3a6ba63959e" + }, + { + "id": "5036b739-db30-57d4-8651-2b91092bbcea", + "version": "MTc1NjMyODE1OTAyNjk3OTAwMA", + "digest": "sha256:457f8730f5d550a5614cb25bc9ab8cebe2a5c628c454a65bf7700da7363bb874" + }, + { + "id": "503cd047-82f4-5206-894d-fcfa47fab48f", + "version": "MTc1NjMyODAxMDUyMTEzMzAwMA", + "digest": "sha256:7120edae9543d770dceac544c0c46512f7372863df14055e2f12cf14082dba28" + }, + { + "id": "503f4293-a4db-570f-8e37-eb45ba0e0f21", + "version": "MTc1NjMyODAzODgyMTY3MjAwMA", + "digest": "sha256:f5a0adc7e6c8fef4ff01ae51b51ba9091914f5f72ddc547db5eb21c35d153001" + }, + { + "id": "50435d07-f34f-5869-a708-ed01f139e542", + "version": "MTc1NjMyODAyMzM4Mjg4NzAwMA", + "digest": "sha256:98f83bcc6add19d19932eb94df5c03fb9a38dda09dda941dee9edc576f460fb8" + }, + { + "id": "504803d6-38c4-5ae7-95ea-0b037dc6ff58", + "version": "MTc1NjMyODEwMTA2MDE5MjAwMA", + "digest": "sha256:d764a22cb9e4f77455a8c3d55a58147d9835519eed0dd2572da436177dc4f01b" + }, + { + "id": "504dba50-edb2-5d98-95e9-8f86a3c69fba", + "version": "MTc1NjMyODExOTEyNzc1NTAwMA", + "digest": "sha256:88097fcebec6e04ebd10cdeea29000d766e394ca5cd5319d966be096e1c1e61b" + }, + { + "id": "508094df-c236-56bb-8eeb-aa0bb65447d3", + "version": "MTc1NjMyODEzNzAzNjI1NDAwMA", + "digest": "sha256:e446a2b8413c574ff1c701dca15757ed9bcc547af3ea174740a4df54759cc0dc" + }, + { + "id": "5082d9d3-147c-5380-be61-8350b19d05a5", + "version": "MTc1NjMyODAwNTEwMjExNTAwMA", + "digest": "sha256:11dc344d8fca002bc94b4b2e4d9077cf725abc53874d78a9c24176ae5ea7a026" + }, + { + "id": "508c7b66-c1ab-56de-be54-8453d6435255", + "version": "MTc1NjMyODA2MjYxODMyMzAwMA", + "digest": "sha256:cfdc778738958885275751f2c4f0cf71335ab261a63b05960d6d14c4cac0d268" + }, + { + "id": "50c2eb7c-60cd-5c7b-b469-74f7a878a909", + "version": "MTc1NjMyODA2MjYyODQ2NzAwMA", + "digest": "sha256:fa668dedaf90571f4166156f830c56fb19682c0c4f386ace848b4edc79c4e7c2" + }, + { + "id": "50cae7d5-540d-52fa-8533-fcab08b0fb84", + "version": "MTc1NjMyODE2NzQxOTM1NTAwMA", + "digest": "sha256:1d0991eba390461e61094505d80405ca6d7fc2ddbd9382c3a0f86006572edbbf" + }, + { + "id": "50cb91b6-4a7c-5a35-9141-92faa67bbf19", + "version": "MTc1NjMyODE2NjQyODIzODAwMA", + "digest": "sha256:5ece7ab859837827a22b6590ec57cb68bd151963ae682d59d6be5b0180548ee2" + }, + { + "id": "50faf620-6694-53fc-a8ba-1326fde074a1", + "version": "MTc1NjMyODE1NTEzMDM1NjAwMA", + "digest": "sha256:bc65b2ec8c3542f917274b22b3aab4c9eaae397320f5aebbe3e7fb459d9b34ff" + }, + { + "id": "5106a89d-dd38-58c4-b4e0-07fabbe2b8c4", + "version": "MTc1NjMyODAxNjQ4ODUwNjAwMA", + "digest": "sha256:f963f675c83dca00a025c6d1607b669bb5f3e5df5b3f9d15e212be3490d3203d" + }, + { + "id": "5119cd25-e8dc-5f6c-bfab-cddb7171ea08", + "version": "MTc1NjMyODA2MjUyMjA3NTAwMA", + "digest": "sha256:5ece679f2f5a68d041546514637fd17ec3a81b416d2dbda139355f013d373fd0" + }, + { + "id": "5121f595-76e7-55ac-9f62-67c2518b5da2", + "version": "MTc1NjMyODAxMzg4Mjg0NTAwMA", + "digest": "sha256:660744fe2c25aa03c898f072e2000bf524a4bcfbf3a077690f6377cb878f9cd7" + }, + { + "id": "516dc681-2e3a-5abb-b504-e176363a67b0", + "version": "MTc1NjMyODEwOTE2OTIxODAwMA", + "digest": "sha256:68b291ae542b604104fcc72d053734088addb8da7ad366e36207eff490059934" + }, + { + "id": "516f3a60-eec1-54b4-bd5d-dcea8485d42a", + "version": "MTc1NjMyODAxMTg3ODk1ODAwMA", + "digest": "sha256:aaea7ccc4e1bfda37b0868eca1a08f867033091908813ad6662868dfc0ad1a50" + }, + { + "id": "519df1c2-dc5f-5187-9437-d2892d43817a", + "version": "MTc1NjMyODE2ODkzOTU5NzAwMA", + "digest": "sha256:da4f36b85aa0401c432ec46e62ec27a6872696a8f725ba56e2ebc2ef6606f319" + }, + { + "id": "51d04e4d-4b6e-5c42-81d2-fa30789cd6be", + "version": "MTc1NjMyODA4OTE0NzcwNjAwMA", + "digest": "sha256:62a095a8512210d459c54b90d9b276a5da7e9c5d5a4c9dd2bbd81abf196989c5" + }, + { + "id": "51de58f0-2e1e-541a-bd26-9b3b9cf40c1d", + "version": "MTc1NjMyODA2NzEwNTc5MzAwMA", + "digest": "sha256:3d442dd17b569a024c47932b821926f058a9eaa3f49495b55914f1f06dc3c61c" + }, + { + "id": "51ea0a74-6dd9-503e-a644-ce97d9cad04f", + "version": "MTc1NjMyODA1NDA4Mjk4NTAwMA", + "digest": "sha256:8093682918cd80a29795f4f9f397b44dceebec36abf8a35f73034e6ab95bb6cd" + }, + { + "id": "51ea8e66-310f-5a10-8c23-83e5ed82e0ba", + "version": "MTc1NjMyODEzMTE0MDIzNzAwMA", + "digest": "sha256:65974dd0c30001b392b5c5d5b27bd89f8c179ab5ab69512cc5e621cd3bfb9a17" + }, + { + "id": "5212c9e8-cb8c-5920-9a9d-344de5f1081b", + "version": "MTc1NjMyODAxMTg4NjU2NjAwMA", + "digest": "sha256:131275781aa34634856261aceee7194dd97d3cbdad6011c90bc32d7e966069a8" + }, + { + "id": "522e94b4-ede5-512c-ab5e-90efe287e22e", + "version": "MTc1NjMyODAzOTcwNDY2NjAwMA", + "digest": "sha256:ebb69d5804763c7f191f563434368df6acf733649d5560c55f2b009a2e566025" + }, + { + "id": "523424a7-0812-5b77-ab70-990dca948eff", + "version": "MTc1NjMyODAyNTYyMzQ0NTAwMA", + "digest": "sha256:2f14f66d8e9b9183b9779c7238aa0694af09d8d684cfdeeafa2c5a0784bff490" + }, + { + "id": "523d8767-856c-5db1-a302-525629d1e66e", + "version": "MTc1NjMyODAyMDUxNDg0MDAwMA", + "digest": "sha256:86f25bb1329587e5c6740277f6f72cda606addac319386b0b45c0615b3dc84bb" + }, + { + "id": "52473bc5-13df-5398-85ee-b918aee40dba", + "version": "MTc1NjMyODE2NzczMDY3NTAwMA", + "digest": "sha256:cfab9cd4962796286a80e9dd53d45dbb447648ffff2207f9dd4d7859ae7eea89" + }, + { + "id": "524a1edc-e962-5c74-b229-8329c60068f7", + "version": "MTc1NjMyODEwOTE3MTMwNDAwMA", + "digest": "sha256:7e03c6445bb741c9ec8e96c62c1546926e7a0ee8ed0b161f09d21502451f9416" + }, + { + "id": "52517265-7981-5245-bf2b-70429027eee8", + "version": "MTc1NjMyODEzODI4NTMyODAwMA", + "digest": "sha256:eaa26886e19ed6a40d6766f2410a109158c2b66601ecf4918b8a39a467720a74" + }, + { + "id": "526d5171-f2da-5aa5-9ef0-620f02c7f056", + "version": "MTc1NjMyODA0MjUxNDQ4NDAwMA", + "digest": "sha256:ab0f9173724a05e0237cced46daceec499025499ac8a019a4fac9298016ae3ee" + }, + { + "id": "526e3d22-541a-50e5-a324-257ee965c7e4", + "version": "MTc1NjMyODAxOTM1NDU1NTAwMA", + "digest": "sha256:1f6b4ee3af82486dac16abf40840645bb17cc6a4065f21ded3d55e5f4c1fffeb" + }, + { + "id": "52756d4d-88dd-5c96-8290-56febae167e3", + "version": "MTc1NjMyODEwOTIwNzk2OTAwMA", + "digest": "sha256:1889652ae91a4f0d6f051bed0c54c3718debde7a5c4e8a650a56a82f4f544f26" + }, + { + "id": "52857f38-8c74-5dd6-9560-beedd5cb7d87", + "version": "MTc1NjMyODAxMzgwNzk3ODAwMA", + "digest": "sha256:d56c84ad5469b3d6dc7b92f832da1fd5dd887276315518265e99c2a7345e7f2a" + }, + { + "id": "529bcd47-d10c-56aa-912b-c5a32d0e2eec", + "version": "MTc1NjMyODAxOTM3OTgzODAwMA", + "digest": "sha256:7f9d966f52dd40e35583d591aac9f35535b187fa4a263c5fb54c9c1a70c5b9f2" + }, + { + "id": "52a91776-acc3-5c8d-86ca-e76dbfaed29f", + "version": "MTc1NjMyODA4NzgzNTc2NDAwMA", + "digest": "sha256:8cadcc97017c032b3e714a68969513abcb10431ac4fed870a610faa7836c9654" + }, + { + "id": "52cefb0f-6135-5168-910d-85ee78c34d66", + "version": "MTc1NjMyNzk5ODE2MTA3NDAwMA", + "digest": "sha256:effa7be7487fed0c8a3bea580f9bf01012745e64b4e5877b539b0d47fd5b9085" + }, + { + "id": "52cf76ec-8fa6-5079-b691-b15753e55e00", + "version": "MTc1NjMyODE2MjMxMjM5NjAwMA", + "digest": "sha256:7ecf700455a34ee5282709c186f1a0f4e34d1695da4d8720b500f527f74346df" + }, + { + "id": "53173e12-f170-5926-bf21-e652b07daf45", + "version": "MTc1NjMyODAyNTU3OTg1NjAwMA", + "digest": "sha256:555b6eec3a617512297fe96d58096b5fee34f0b48c3748c5d0146e3185141b45" + }, + { + "id": "5352c21f-6697-58fd-b792-0b1ef743d714", + "version": "MTc1NjMyODE3MDkxMDcwOTAwMA", + "digest": "sha256:c1e87f50bbe3ed7fc862178b5e56e322ecac602e5ce1746bd70c25bd45c9cf2b" + }, + { + "id": "535a233b-a40e-5b01-8a63-3051f6286bff", + "version": "MTc1NjMyODEwODc3MzAwMDAwMA", + "digest": "sha256:04305d62bbc68ff5216cb8e1dcee7dcd7cba91a85d9c217bd1a2021240e93648" + }, + { + "id": "5368e761-610c-55cf-8b9e-5043a001c154", + "version": "MTc1NjMyODAxMDUzMjgxMTAwMA", + "digest": "sha256:5ee986299d284c41c292f906065d8201ff618b8b232129ac6719091b07ee354c" + }, + { + "id": "536ab2ae-dc63-539f-a405-598c4303a332", + "version": "MTc1NjMyODExMzQ2NzUyNTAwMA", + "digest": "sha256:e246d04c4d6125ab48e411986447d309c1c3b79787e76f56a6aab283c910de76" + }, + { + "id": "53b65a4e-e3a3-57aa-aa9a-a20929f33536", + "version": "MTc1NjMyODA3NDEzNTMwMzAwMA", + "digest": "sha256:b6c592d0072242d1e691e58af2ddf8ba02ebd251fa13b0dc855b5681ee106a10" + }, + { + "id": "53bf10fe-6b96-5eb4-ac8d-17be214a4ce0", + "version": "MTc1NjMyODE0NzYyOTg0MDAwMA", + "digest": "sha256:d74cce45a3b7ae6cb005c93d10f1784b3dfeecc08600087785ac91a68b834574" + }, + { + "id": "53c352c9-8b48-5934-9248-e5c05945667e", + "version": "MTc1NjMyODA0NTc5Mzg5MzAwMA", + "digest": "sha256:e3657ae4c50d5e4610013912327b09b2c5c4d5159416bd88651ad954769ebad5" + }, + { + "id": "5405fba9-7454-5f4f-a808-ce87d3ebb17e", + "version": "MTc1NjMyODE1NjUwNzQzMDAwMA", + "digest": "sha256:ea17bb0b155654a7aa73e915bf709889591affae9b81e84bc2fce1d2d3796280" + }, + { + "id": "5408501e-8ecb-54bb-926d-13d6b6d4b6fe", + "version": "MTc1NjMyODAzNDcyNjk3MTAwMA", + "digest": "sha256:a36adf4df3478107456123c8e28fd9a664ff66af71baa4da86225de359b0f020" + }, + { + "id": "54499172-13c9-5299-b2b5-a6db0fece44f", + "version": "MTc1NjMyODAyNTczMTAwNjAwMA", + "digest": "sha256:59a5587234f585727047536ada42d8ee28e8d8ab4b75d9b0fc8e06be30c00f98" + }, + { + "id": "5455b426-f012-5030-b13c-10455dd775f2", + "version": "MTc1NjMyNzk4ODY4NzgyOTAwMA", + "digest": "sha256:c5961e46916f521cab2ed07bbd8b67f2323b4dcb3dbcabfc2cf96477d609f685" + }, + { + "id": "545f9f13-2111-5c8d-886f-973cea0f9a34", + "version": "MTc1NjMyODAwOTAzNzA5MDAwMA", + "digest": "sha256:3ce686e72aaa470f355a5a52d4787c01bcef0644ca03871fb8ab7c761d15678f" + }, + { + "id": "5469fd6f-42f9-5013-99ae-9d83ea93d563", + "version": "MTc1NjMyODA3MzA2NTk0MTAwMA", + "digest": "sha256:2c864c288d6cb8c427b1a12894076c9210f9062d88abc0fa7a295c7b8a33df85" + }, + { + "id": "549373bc-856c-5b21-a7c3-cea86eec2e81", + "version": "MTc1NjMyODA5NzY3OTQzMjAwMA", + "digest": "sha256:a6cfac820f308aa2c7f227f7f4c823cf972118002b28e08de83c7f36738103df" + }, + { + "id": "54af3b72-8320-5d8f-8a50-bc8c16f6dbf7", + "version": "MTc1NjMyODAxNTI5NTQ2MjAwMA", + "digest": "sha256:3587b91e402497687e27c5bf49df751298de239e1c5a1b2cbda3dd74ef40ad99" + }, + { + "id": "54d1271a-0280-5d0a-b082-7dc954200918", + "version": "MTc1NjMyODA5ODY3NjgyMjAwMA", + "digest": "sha256:9d13689e84b2b7b3c2398867dfbb4dfdf17b07539bd846ec587aa50d7b4a04ef" + }, + { + "id": "54f10e1e-4989-5351-ae8b-9fd10c6df45d", + "version": "MTc1NjMyODAxMTU2NjMwNzAwMA", + "digest": "sha256:40c594257428c915bd36086b135dc969fd7f761f21ddba039c4d1dcc0a73cb82" + }, + { + "id": "54f41111-f63a-502e-895e-427f3eb8a7cd", + "version": "MTc1NjMyODA2MjY5MjQyMzAwMA", + "digest": "sha256:0e4b769c4ffd199f40b028700ce29a27dff7561eafffb72dddf3a4bdd6564d4a" + }, + { + "id": "5535b2c4-158d-59e4-88a2-b4cb6bdf0904", + "version": "MTc1NjMyODAxMTg1ODYzMzAwMA", + "digest": "sha256:df9a0b0c7149d1a237fc588ba3fd97d03920f52dafb85828bde3a5276acc588d" + }, + { + "id": "555061f9-72a3-59c2-94b3-c2e52fe1b502", + "version": "MTc1NjMyODA3MjU2NTgzNjAwMA", + "digest": "sha256:534a5cdc4c2cc8d8b5f223bee4c0685b8a496c3a6cd46a5d7fe3fb1a37e3a0e5" + }, + { + "id": "55666501-7f9c-5fa6-9224-6a8965f19077", + "version": "MTc1NjMyODEzNjI2NTc3MDAwMA", + "digest": "sha256:0b18caae39a103496bf3b3160c7a6cc706913ff45f8b6e3e498550ee8a764c80" + }, + { + "id": "556b4972-1095-5950-b175-5fe0b2fb42f9", + "version": "MTc1NjMyODE3MDk4NDIwMTAwMA", + "digest": "sha256:4fc47f4ae4d160fd61e8f9fadc7eaf32318e9efb75ecc79fb59e1612ac81792f" + }, + { + "id": "55702dd2-9e56-52db-b43e-773b850b6c2e", + "version": "MTc1NjMyODA2MjA5Njk1MjAwMA", + "digest": "sha256:1a46b05cd1edc14fed2a10d6f896016d1a01eba771d3ab55ce94ec93ab172b9b" + }, + { + "id": "5570609d-4a2c-565b-aa0b-65812493a0bf", + "version": "MTc1NjMyODA3MjU2NzY4MTAwMA", + "digest": "sha256:e6e3439507904002395ea39b50f8c7982a262526d698e87e2a5dfbb0e13954bb" + }, + { + "id": "5593380c-9f5a-59ac-b8c8-95502cdc114a", + "version": "MTc1NjMyODA5Njk3ODI0OTAwMA", + "digest": "sha256:0ed093f16761754b365fe5315a947d2a1f126e6625d31ad15efcfc2f0267ab05" + }, + { + "id": "55aaf39e-20b8-52a5-b091-2885666e5c1c", + "version": "MTc1NjMyODAzMDc0NTg2MjAwMA", + "digest": "sha256:af13eb34c42805dbdf7f8366c47bc955682d3f7da2726eea0e1c5b6e5772ce94" + }, + { + "id": "55fa54f9-9399-5012-98a7-f75c58f76e7d", + "version": "MTc1NjMyODAzODYxNjA1MzAwMA", + "digest": "sha256:9d35b6e95e658a0b7926bd80396cd9f9d5e88c7199afccb2b2127708712546f4" + }, + { + "id": "560327f4-8b09-5481-8c2c-21f00848d75f", + "version": "MTc1NjMyODEyNDUxMzA0NDAwMA", + "digest": "sha256:06efe3a9d7ad6a0337042a94484eb581077b9d91482a098cc0eb9661ec654529" + }, + { + "id": "5616886d-bbc2-5e4a-8189-72513b0529a2", + "version": "MTc1NjMyODAxMTY1NzAzODAwMA", + "digest": "sha256:c7b0b84464ed2534e97f6d6b460f5af8062db926e6ab8a7b210a217dc25dc9c8" + }, + { + "id": "56195989-3e71-5878-b302-b76c633d948a", + "version": "MTc1NjMyODAxMTkwNzkzMTAwMA", + "digest": "sha256:313838b2b886b1bd1bec9b460253264fd7c8b76517986f8a0ecd0d4da8f18d3e" + }, + { + "id": "5627fa15-47f5-53af-b8dc-439523cfed2c", + "version": "MTc1NjMyODA0MjU4NTg4MzAwMA", + "digest": "sha256:edf5f5951fdfb7dc9d7d29ad9344d4c540fea63213e7e14eb11d25ea23d43f5b" + }, + { + "id": "562b247d-2b4b-5e63-b21a-fd37962b1be7", + "version": "MTc1NjMyODA5Njk5MTc2MjAwMA", + "digest": "sha256:67523abf700334b8bd4758afbf7d2481c2383f1cf73a2e7f89d5263774eba9fc" + }, + { + "id": "5643ad7e-ff56-5887-9d3b-4d49af62357a", + "version": "MTc1NjMyODExMzUwMDY5MzAwMA", + "digest": "sha256:e9c371a339453cc4cefada477ad7cbd1fceafae9a75f012c9e0dddb76770ed1a" + }, + { + "id": "56664701-80b9-5cbb-b1c1-fa6defc9bef4", + "version": "MTc1NjMyODA5NDM2ODUwMjAwMA", + "digest": "sha256:a4ae97d2391bb39b22a8ff5d9b04e3147f7701ccc6788d79a6b9ef9533489a41" + }, + { + "id": "568b3b26-4670-51b0-b7d5-10ebeb0d21e2", + "version": "MTc1NjMyNzk5NzA5NDA4MzAwMA", + "digest": "sha256:26e58992ac96de68df097502a71401887b93fb68fb1ca233ad6004240321ee49" + }, + { + "id": "568edde0-18d1-5cd0-9711-bd565d804e92", + "version": "MTc1NjMyODA2MjY0NDQyMjAwMA", + "digest": "sha256:63ffe95241315caeaa97128d58dce61b5e95305a9c8c0cb5c1522f49ffa7e7b1" + }, + { + "id": "56bc500c-da9b-5ce4-81ba-d53064ef3a8f", + "version": "MTc1NjMyODE3MDgwNjY3MDAwMA", + "digest": "sha256:a604ef2ff6dd4333ca6ac5ef4bf00f350751d5516d9ac322bc9d191fb2b24d0f" + }, + { + "id": "56cfae08-bb9a-5e6c-832a-08ce1d9e9ae9", + "version": "MTc1NjMyODEzMTA2MjEwMDAwMA", + "digest": "sha256:8aceaf971ff1c88fc46247e23319dd4f15e8af0775657543340f3ef5156d702d" + }, + { + "id": "56d8e6af-3124-5d23-9ec0-60e016d36a58", + "version": "MTc1NjMyODA4OTEzOTM4MDAwMA", + "digest": "sha256:20e5be4802ae5132741ce8a742e35d247e8e79a6992ee698f4d32511eac371a7" + }, + { + "id": "57015e29-d2a5-5d82-bf02-d38aec84b887", + "version": "MTc1NjMyODAzODg0ODMwMjAwMA", + "digest": "sha256:fef701d1038fa84da6674f6ebc881674287c128f6917defeecaa7c40c0d7ad6f" + }, + { + "id": "57498489-9c05-5b87-84a9-21d6a900e948", + "version": "MTc1NjMyODExOTE0NjM2OTAwMA", + "digest": "sha256:ac378db56ead133d25269eb1e033e707644b5c30d1d09847cd6dfaa34066ef8c" + }, + { + "id": "5750f03a-53a3-5442-90e0-849466c1cf00", + "version": "MTc1NjMyODE2ODk3NjAxMjAwMA", + "digest": "sha256:5fdf3cae4eca27cb44000c71dd7d11d77a3df6dae0f72080093f439c733731b6" + }, + { + "id": "57525b87-635b-58ce-955a-9dc5e3a77abc", + "version": "MTc1NjMyODEwNTM2NzIyOTAwMA", + "digest": "sha256:32df60a2673f1db2cb5b1d010e2579be7668e62629f9edf7b67aef8ac3795424" + }, + { + "id": "57706f23-2654-5da8-881c-2be959acaf91", + "version": "MTc1NjMyODA0NTgyMzQ5NDAwMA", + "digest": "sha256:516eee4317ed5aca6819e830db1d01a08febc515066b49049d1f407238b78829" + }, + { + "id": "577749f0-40a4-57ed-bc56-4d8147ef1c39", + "version": "MTc1NjMyODAyNjg1ODM5MDAwMA", + "digest": "sha256:d3b57aa3b2febda87fffaa230ca6de330db65a9b8df1cea6c1401176f9d55281" + }, + { + "id": "5778958f-a042-5080-848f-673f9f5b3430", + "version": "MTc1NjMyNzk5NTI5MDQzNjAwMA", + "digest": "sha256:70d1ca962a09128712466f3a739f928e4dd1d493285fb83b5c4f72a63aec0260" + }, + { + "id": "5791ff2b-efac-5dfd-9c59-49012e8ed31b", + "version": "MTc1NjMyODAwOTA1ODk2NDAwMA", + "digest": "sha256:c14811f089d7ebb609faaef43978a94d43bfe20392cefb009ee63b3cb78aee5e" + }, + { + "id": "579a5161-1b0a-554e-9cb9-9d567f131994", + "version": "MTc1NjMyODA1NTc4NDcyODAwMA", + "digest": "sha256:7be9d5c9f829cac6eaeaf8e948bd18ad627897c7180a47c6144a669e909907af" + }, + { + "id": "57a586a1-5266-5b96-a071-8639f6a5965f", + "version": "MTc1NjMyODAyNzM4MzQyNDAwMA", + "digest": "sha256:78d2186f71ad37031f84ca2b59fc9535a028983fd6aad325e9b4cfb333be123c" + }, + { + "id": "57b94e54-8ff9-56ed-8649-2576a117e0a4", + "version": "MTc1NjMyODA3NDE5Nzc0NzAwMA", + "digest": "sha256:cd7ce6faad099618811b2745d46d67ba6bde244c347864784a1ab864aab39d70" + }, + { + "id": "57c251df-be5f-5f1e-9699-0ad6381919bd", + "version": "MTc1NjMyODE2MjI5ODY1MDAwMA", + "digest": "sha256:bf9efe6190bcdd49045e800bece5dee9cb88ed425c45b5a5e34d862d443b14c0" + }, + { + "id": "57d8c153-f9d4-52fe-aead-b251679709a4", + "version": "MTc1NjMyODEyMDQ2Njk4MjAwMA", + "digest": "sha256:271ab69601bfbbebf9278febdfad02c218d62c9544202dfeb465987c125f5102" + }, + { + "id": "57fdd27e-290e-504b-906c-e428fd0107e2", + "version": "MTc1NjMyODA5NjkyODAyODAwMA", + "digest": "sha256:ac210f0067f34fe91c59241512415cb1ec60ac7f0ae8c0289ffd31d16f49664f" + }, + { + "id": "58246825-e891-5c43-9333-9bb5dbee617e", + "version": "MTc1NjMyODAyMjEwNTUyNTAwMA", + "digest": "sha256:0f66558e7a6003212f3c386a1d14903afe0846dd26e25d613c95db1d873311d8" + }, + { + "id": "583eb3d1-7768-581a-8170-5485aea55aa3", + "version": "MTc1NjMyODA1MjYwMjEwMjAwMA", + "digest": "sha256:4ed6783be60354b6e3b7503981c764b1a29f53cf65ff2f516036fb96193c0f30" + }, + { + "id": "585168b8-1600-5a1a-951a-225b577718f9", + "version": "MTc1NjMyODAxMDQ1NDA4NTAwMA", + "digest": "sha256:03ea4cd20ff9500ed2d3cfff7e857b5db01475bda70f7e9c8e522b594eaa74d5" + }, + { + "id": "58860ea3-1a57-5860-9de3-e85850e2b1b4", + "version": "MTc1NjMyODA2MjIyMDMyNzAwMA", + "digest": "sha256:64bdc29e110d3cad7f0b4cbdb11ed6e63adc84df0b4441074b6ed02c8151bf10" + }, + { + "id": "588aed4b-1ba4-5bf3-8cca-c024c9b49c7f", + "version": "MTc1NjMyODExMzU3MjM2ODAwMA", + "digest": "sha256:f620f12bde7ab5f85e5f9ae786a4588f8b8ae21a10f7fd1652ffc91e5fda162c" + }, + { + "id": "5891d4ce-520b-596b-a304-a0c1b106df33", + "version": "MTc1NjMyODE1NDU5NzAyMjAwMA", + "digest": "sha256:8558a77500bc00bc4488c330f7547c6d8c226362c8d572e96fdab8bb7dd1bcb6" + }, + { + "id": "5902eb10-f3ae-5651-88ee-dbf8100d4101", + "version": "MTc1NjMyODAxOTQ0MzQ5ODAwMA", + "digest": "sha256:7bf3a1efdb779e729bb307974711f2e49edd518850bf86eeb63741a57f60304a" + }, + { + "id": "590807b2-58ab-5d0b-ae05-22adfa59c05a", + "version": "MTc1NjMyODAxOTM3OTQwODAwMA", + "digest": "sha256:a96b657624a2a9ec19e9a59a5df71456b993d524ead92f645800e5be4c91c420" + }, + { + "id": "591c5237-1b75-511e-b59f-5bec3e9de1df", + "version": "MTc1NjMyNzk5NjY4MDYxMDAwMA", + "digest": "sha256:ec98659f63e3d3bd22a799810622e8f450fbb432ac92284bcce592d461e40669" + }, + { + "id": "59377bd7-1923-5497-b961-ae17e86f865d", + "version": "MTc1NjMyODEyNjU0MzI1ODAwMA", + "digest": "sha256:5112b6d44257000773e96cc5a91ad5b907c72bd4b2cc7a47cdd337c6fcd8fc47" + }, + { + "id": "593da382-8404-55fd-9fa2-fd935a4d5e3a", + "version": "MTc1NjMyODAyMDQ5MTMxMjAwMA", + "digest": "sha256:ff62f6047cbb413e2da16e31648f043754ca2ffc1a2706e39bae8e7ad8637d9b" + }, + { + "id": "595873ea-901a-5f17-bc2d-cbcb47595b0f", + "version": "MTc1NjMyODAzNjAxMDU5ODAwMA", + "digest": "sha256:5f63829a9dcdb5f3da65558c718fb99feab2b2e03255c154347fd2f95ff87131" + }, + { + "id": "596456c5-a594-5aab-b414-a9ec86bf8feb", + "version": "MTc1NjMyODEyNDQ0MTEzMzAwMA", + "digest": "sha256:ea7eaec80c865933c9f244451c6742ca5236135e74762e716dfcd5d9c8fe29b4" + }, + { + "id": "597781f7-6ce5-58f9-a93e-6011ad0de6dd", + "version": "MTc1NjMyODAyNTc2Mzg0NTAwMA", + "digest": "sha256:bbb5104462419e6da8643cb15ce4a2aa47705c5cfcb12b711db57609eb7121b2" + }, + { + "id": "5993663b-f547-5087-a553-1237594b4897", + "version": "MTc1NjMyODA0NDc5MDE2MzAwMA", + "digest": "sha256:f7629c0148f242d59a07a1394a2df56c35353d04b613c4df09f99e795cfbd46c" + }, + { + "id": "59cc59fb-e7fe-542b-a287-4819e8436bc4", + "version": "MTc1NjMyODE2ODg4NDI1MDAwMA", + "digest": "sha256:ba830bee5592421bc2e8e3c94711a82cab8de728239fba9bf6026f831ede94a5" + }, + { + "id": "59f04151-2c51-5f74-8ca9-9d0c2fa7ac66", + "version": "MTc1NjMyODE3MDgzMTI5NjAwMA", + "digest": "sha256:3df91d79241db933055fed5f62d90bf495730eb57b5bdff5f057f42223ba7680" + }, + { + "id": "59ff45e4-0b19-5bc8-880f-515be9d46623", + "version": "MTc1NjMyODA3MzE3ODA0MzAwMA", + "digest": "sha256:ace823855cc5b3c115cd3708f74ebbc60fdafea08389d46db291f98a57fbe64a" + }, + { + "id": "5a1a69a6-cb8b-54b2-9dfe-d8dfc694761f", + "version": "MTc1NjMyODA4ODU2OTU5MTAwMA", + "digest": "sha256:9c07eb7f116bf35af0a13f3ac96a21acd284cfd99a4db59cec571622e0f69900" + }, + { + "id": "5a21e5f0-3c17-5e77-ab39-9f6b8a2680a5", + "version": "MTc1NjMyODAyNjg0NDExOTAwMA", + "digest": "sha256:7cfd998eaee2baf2b0edf19200fb42c89f2ccf6958c4a1cb48d706a4c18afdbd" + }, + { + "id": "5a2fb19e-fa7b-57c8-b059-248493d241ef", + "version": "MTc1NjMyODEzOTI3ODMwOTAwMA", + "digest": "sha256:1d9de8f55d1fbdbaca449a02dacaf4fe35586998e47fca2580daee085b01ad91" + }, + { + "id": "5a461e8a-6f5d-5925-8556-5b3b5e606d62", + "version": "MTc1NjMyODAwNTExNDA3NjAwMA", + "digest": "sha256:c3005a4d00d17078f565a661a9e004d6b62fa2d56ea04e1f89f4b2ef4db0a647" + }, + { + "id": "5a4d3a12-f3ad-56b0-85dd-d551cbb8a4fd", + "version": "MTc1NjMyODEyNDQwNDgyMDAwMA", + "digest": "sha256:fb885313f041c753cb3bb22a068a37783b3b3ad5c9253c734479d1336e447d31" + }, + { + "id": "5a5083dc-d635-57b2-afe8-97c292ace8c5", + "version": "MTc1NjMyODAxMTU2MDAyOTAwMA", + "digest": "sha256:730c53227fdfda4c26733929f8687468446a04f12446600357fc8c6375ee2382" + }, + { + "id": "5a616bea-149c-5595-993c-41506e9db71f", + "version": "MTc1NjMyODA4Nzc0MTI4MzAwMA", + "digest": "sha256:1ef7bbdb0c9b227f31e0abe8084d5ea280b3d46c496551f46a4ffd298d040e90" + }, + { + "id": "5a61b47d-3772-5fe5-ae7b-00e38880d668", + "version": "MTc1NjMyODA4OTIyNzQ1NjAwMA", + "digest": "sha256:b831a7b5f2ea93216e557773cd5051c4ceaa8e95e36b6c664b85aeb51cf44b5a" + }, + { + "id": "5a63a729-dd32-570e-8947-829dba505077", + "version": "MTc1NjMyODEzMTE3NDY4NDAwMA", + "digest": "sha256:456bfa99a52cdea84f94189781c4d5186060b90bb3fbcd0e0cc6e673fee15d3e" + }, + { + "id": "5a72ec77-8793-5382-b748-24a35186d453", + "version": "MTc1NjMyODAzNjg3MTMyMjAwMA", + "digest": "sha256:5dcc69001ef5fd32f50ebddaea58da43f8161cbed58432b98dbc228a132de4d0" + }, + { + "id": "5a85d4a4-be98-54d5-bd18-814718c38a21", + "version": "MTc1NjMyODAwMzk0OTQzNjAwMA", + "digest": "sha256:48a8098cae2b1d11d69a835792b2c3b432f0c058e4af3ec778e8f031f985c666" + }, + { + "id": "5a9a7c9e-77ff-5a0b-9390-59f896dd30fb", + "version": "MTc1NjMyODEzNzcwNzEwMDAwMA", + "digest": "sha256:c5a8a05639bb5309e5d9ea22de05bb6ef8ae3cd46110c59a2d7bc756e01c671a" + }, + { + "id": "5ab6c4ed-b923-53f2-b8c0-090e7eb30db7", + "version": "MTc1NjMyODEzNjAzMjMzMjAwMA", + "digest": "sha256:ee8f3e59f852c99f8a52a52023216403c63c313ab310521a548ba95fe1d75940" + }, + { + "id": "5ab9319d-adda-5205-a138-c5102cf3e2f8", + "version": "MTc1NjMyODA3MzAzMTkxNzAwMA", + "digest": "sha256:7d179a0074ac56fbc6245bf45da071a6f4933337c20c0b69b839c1b2e093d12f" + }, + { + "id": "5abea1e1-8592-5ae5-be6e-b9e63654b7d3", + "version": "MTc1NjMyODE3NDgwNTc3OTAwMA", + "digest": "sha256:ae7bb34a4363c5aa8fb9dd76aac8804ce3beebf1833cfa75587e98ec3e573b9d" + }, + { + "id": "5ae7efc8-5ace-52c4-ad19-445fdf0e0353", + "version": "MTc1NjMyODEzMTE2NTg0NjAwMA", + "digest": "sha256:18f1f68c361a2ad7a58100c21bd5c30df018b67d22eada7b0339e04793ec7f20" + }, + { + "id": "5aed93bf-401a-5293-8fe9-292f27feb9d2", + "version": "MTc1NjMyODAzNDU5NzY0NjAwMA", + "digest": "sha256:5bb46245a85a5342c20be42cff5c33a52bb19dcf0199d5ea628cdc592294e1f2" + }, + { + "id": "5b0e0732-a413-522a-9fb5-6c13a93182ef", + "version": "MTc1NjMyODAxNjU4MDYxNzAwMA", + "digest": "sha256:d53887e60b7c9870e6c8de6e52e948a4b9d4a9e5586a8a80cc62f1b2c630e7ac" + }, + { + "id": "5b13d225-0ecd-58a0-a885-30b9e123cc21", + "version": "MTc1NjMyODAyNjc4Mjg3NzAwMA", + "digest": "sha256:c24d32918cf78493a395726dbd6069441abe70be57cb69003eba9051c2d5d450" + }, + { + "id": "5b58de3d-c752-5467-b93c-734f3d4cbbb0", + "version": "MTc1NjMyODA0MjUyNDc3NDAwMA", + "digest": "sha256:6b2b35918d3b507945a5c0f8e767257b780fa54f6fbf8abfdfd9676cc16194b7" + }, + { + "id": "5b624b82-05e7-5020-9a82-a27e1f2b1e1d", + "version": "MTc1NjMyODEyMDM3Njg0NjAwMA", + "digest": "sha256:de5ab976750c08dcd9caf0505c98ac243d17407af75c731cc08344ea10df0f91" + }, + { + "id": "5b6eb2c1-ed8f-550a-9429-09848fc75345", + "version": "MTc1NjMyODEyNDQ3MzQ3NDAwMA", + "digest": "sha256:8f94805b8cf188558d19a5d2310b6bf50be75e0dc74110a60f5ccf9f5a6f2eb5" + }, + { + "id": "5b894640-38d8-5388-906c-39da6301a21f", + "version": "MTc1NjMyODEzNjA3NjI5NTAwMA", + "digest": "sha256:ac2e5f16d05200464412639f66fb7a18112d17f955bd22c1d352bfdd70b469dd" + }, + { + "id": "5b9973a4-7b0f-59b7-afa8-220dc034eafb", + "version": "MTc1NjMyODEzODEzMDk4MTAwMA", + "digest": "sha256:cc0e2dd764f0a099f197fddd56fd74acfa410bb2db46de112eea14b90e0e5850" + }, + { + "id": "5b9c80d6-da69-5ec6-a62b-ab0d46a6fc1b", + "version": "MTc1NjMyODEwOTE4NTUxODAwMA", + "digest": "sha256:ebb048b857bf917c3b418cd322bb166344f6c4c76229f2c0f30dbbb45f5a5219" + }, + { + "id": "5bba15f3-e8df-50c8-91ab-5d44f9bd3c75", + "version": "MTc1NjMyODE2NjQyNDE2MzAwMA", + "digest": "sha256:800d0803a9512585c2aad6e1e51c78ebbc0ba0918f8442784066cdbc76d2d0d7" + }, + { + "id": "5bbbedff-7cea-5211-88e5-9713adc6b8b8", + "version": "MTc1NjMyODA2NzA4MTkyNzAwMA", + "digest": "sha256:8f7e8514a11c013fa40e01c268d9da99f9a007216bf1b35bb597e988c2dba6d2" + }, + { + "id": "5bbf7120-438e-53a0-9130-581f9b92fd98", + "version": "MTc1NjMyODE3MDQ1OTkwODAwMA", + "digest": "sha256:20f919451dfdb5276a99af038234eb8ebdf430b6e8bb865bfc69972fab398dff" + }, + { + "id": "5bd43b1c-b891-5b93-abda-15d666ccc072", + "version": "MTc1NjMyODAxNjUwOTA0NTAwMA", + "digest": "sha256:ec7b1207e76185654dcbc7f76d4e499dfb939b0ea411acf7ca7021a70b168970" + }, + { + "id": "5bdb073a-bafd-517d-a521-94c265a4f537", + "version": "MTc1NjMyODAzODg2NTk2NTAwMA", + "digest": "sha256:f7c63eb2c0aa9e2828bc5435bcc18931d256aae750c3470fc0d09cd97260349e" + }, + { + "id": "5beaf87b-f56a-5944-9d9c-296dd2e04cbc", + "version": "MTc1NjMyODA1NDEzOTYwNTAwMA", + "digest": "sha256:4ae4c81037c81317b49701841cfa4009fbb6ea42822018773e3b4ccabd361752" + }, + { + "id": "5c00b33f-e50c-5c42-9e4e-844bdce709a0", + "version": "MTc1NjMyODA5NzgzNzYyMzAwMA", + "digest": "sha256:e30a55198e8a32cee82f3d3f8ce8947b40409f4d4736734f52076784ef954554" + }, + { + "id": "5c3730aa-4f3a-5ded-9f7a-69391d5cfb1f", + "version": "MTc1NjMyODA5ODY2NzY0MDAwMA", + "digest": "sha256:6dbcfbbb2685e400a0101b193b0ece2414d21806bcdb78057009656628950a90" + }, + { + "id": "5c5bb67c-8169-5337-a658-93b429672511", + "version": "MTc1NjMyODA0MTg4ODczNDAwMA", + "digest": "sha256:29c1251f29824c55105dcaecc72af0c52c12454594ba50c56f2d537242a8f290" + }, + { + "id": "5c8dd79e-9ccd-5065-8d9a-f618c48a9364", + "version": "MTc1NjMyODAzMTAwNjQ3ODAwMA", + "digest": "sha256:57110535f73cbea6d38043c379153888a39a92da95a4270806eee19dca62dc87" + }, + { + "id": "5c98611f-f091-5ce3-a719-e92cd2829b99", + "version": "MTc1NjMyODE1OTEzODk0ODAwMA", + "digest": "sha256:3dffe5211246519967bda39411c910a5756401c2275644fdd12dc9161244a2d8" + }, + { + "id": "5c9b718f-6b87-5bb5-9002-1671893a78fb", + "version": "MTc1NjMyODA4OTI2Mjc0NjAwMA", + "digest": "sha256:ce986a99911d22b6a38ff220eb982a5add473d1d5c26ea8093ce4054c7f86f0a" + }, + { + "id": "5c9d00f3-9e9e-58bc-b7f0-455924b87504", + "version": "MTc1NjMyODE1OTExMzA3OTAwMA", + "digest": "sha256:85b8fd5547f2a5878bdf4b8c7d4a1bd4434832466a5293e1db5451c562b59b2d" + }, + { + "id": "5cc90e9e-63a3-5b41-95c2-7779eeb946b8", + "version": "MTc1NjMyODAxNjQ3MDMzMjAwMA", + "digest": "sha256:515504b10a43dd4dfc2eeb0954fa0a27334aa34be5aba4f677eedff3fc02a36f" + }, + { + "id": "5cd93a5e-babd-5984-9686-8463b660a5d0", + "version": "MTc1NjMyNzk5OTM4MDIxMjAwMA", + "digest": "sha256:fc07bc7a7aa4dcf4ba4ac35b0109d79aab85efc230dbb1b85bda5a7fa2f2840e" + }, + { + "id": "5cfeec1d-d8ec-58ea-8864-1cdd4da9042b", + "version": "MTc1NjMyODE3MDk3NjM1NTAwMA", + "digest": "sha256:ae9a443a2e09121126aa48b3e99a36325d9920451acd0353be798131fb5c643c" + }, + { + "id": "5d027520-6540-5b5d-99ca-f084814669d5", + "version": "MTc1NjMyODA0MjUyNzEzODAwMA", + "digest": "sha256:cef7c0a89ec60084df65cc2916f4c478d57d473709758c49fbf58c347a876231" + }, + { + "id": "5d0abd03-fd22-53d9-9387-c37282fd5bfd", + "version": "MTc1NjMyODA1NTcyMzgxNDAwMA", + "digest": "sha256:02dab720a99bdb49ea427b8e265ade56d818242ef474a7195bcc4c3e0570f28c" + }, + { + "id": "5d5b5976-c2ca-584f-8a3e-2637c6ebf5e9", + "version": "MTc1NjMyODE2MDU5MjI4ODAwMA", + "digest": "sha256:1e81cf5682bc3910193579287dbf0e023586efc9cdf440bfb39567fbcf6c4173" + }, + { + "id": "5d7d58a7-c54e-5b1a-a63a-e00ce0f31002", + "version": "MTc1NjMyODA4ODU0NzYxMzAwMA", + "digest": "sha256:6e3ddcf3c8e0452016b19f20b0aaaae73a44ee09f68b14a87dc2367006654ff8" + }, + { + "id": "5d8268e2-344a-5521-9893-b4bd7da96c97", + "version": "MTc1NjMyODAyMjE4NzkzNjAwMA", + "digest": "sha256:c200060ada4dac18a586fb0687fe0ff580d7020858086b5fb618091811077cea" + }, + { + "id": "5d8853bf-24a0-5361-b9a8-221003c4eb58", + "version": "MTc1NjMyODExOTY3MTE5NzAwMA", + "digest": "sha256:c0219df2719ba8fb35ffa40cb4027d60e0b7d96cd40ea7a6c433b6c836c6f2f5" + }, + { + "id": "5da3a9c1-080a-5751-8807-57a6af750acf", + "version": "MTc1NjMyNzk5NzEzOTQ5NzAwMA", + "digest": "sha256:d15e6b0839c70f2006209351b3caf3a092716052eed5660ba4a0c69843c8661c" + }, + { + "id": "5db82447-4179-573c-b12a-37183c15f79c", + "version": "MTc1NjMyODAzOTY5MzkzMDAwMA", + "digest": "sha256:eff47a5d1c5c833dd7685292ce6a742ce346ab4157433a3cd6f86d4ebcb80f95" + }, + { + "id": "5dc663fc-4b4d-5c7c-83b3-44a5002819ae", + "version": "MTc1NjMyODAxMzg2NTAwNDAwMA", + "digest": "sha256:234e3a9e3336d8d9a4151368e7f0dd32112b7ab346c74325985d5aa000ffae97" + }, + { + "id": "5dc8958d-c81b-54c3-a439-365177ac1357", + "version": "MTc1NjMyODAxMTYzMDYxMjAwMA", + "digest": "sha256:2ce9c4f90da5ae08c86c494cf38cbc524910c4b6b52fb23b49bbde2b27dbf4c0" + }, + { + "id": "5dcd6c96-b023-5b76-837a-6250b35b0a54", + "version": "MTc1NjMyODAzODYxNzYwODAwMA", + "digest": "sha256:5ee2748fc843dbfb68488d8b0358578ae3981bbdb5c340c181650387b1146654" + }, + { + "id": "5dd64a18-e4fa-599e-8dc4-4f19e4285c6b", + "version": "MTc1NjMyODA5NDQwODA2NzAwMA", + "digest": "sha256:27e2a46bcb64d9afa4a2557696064cf81d82ce57e1722584948de00f14fba046" + }, + { + "id": "5dfb283a-cfba-5578-989a-b6b8be45134f", + "version": "MTc1NjMyNzk5ODIzMDU0NzAwMA", + "digest": "sha256:41985f7c80e33abaa3f3ab3afb5ea3b6567f331986041a2c96a35712e15889bb" + }, + { + "id": "5e08af7a-0916-5d58-914b-47f27723c5a7", + "version": "MTc1NjMyODAxMzc4Nzg5NjAwMA", + "digest": "sha256:c35a1d71bf2f24c22b5cb8ff4c0656df385135bfc497cec85ba9cec06d791f96" + }, + { + "id": "5e08c8aa-f13c-5eca-8c13-37ab3d8d5103", + "version": "MTc1NjMyODE0MTY1OTU5MjAwMA", + "digest": "sha256:fecca26356400bd71a8ba80ef22cac95a035cd37cf559d6bb1913131e4a71a44" + }, + { + "id": "5e0acd29-20dc-57b1-8e20-19b8af7b6be7", + "version": "MTc1NjMyODA2MjEwMjg4ODAwMA", + "digest": "sha256:9cb7fdb447226974840f7667f137918dd185975a7dcd8a13d146d9a26ef0f313" + }, + { + "id": "5e3bb1ff-3254-56c4-a34f-d8e35e3e9da6", + "version": "MTc1NjMyODA0MjQyNzMyMjAwMA", + "digest": "sha256:df1694b50932634ee99f52b0ea43a52d5a4434ab2fa26f8cbecdc2c796225fd0" + }, + { + "id": "5e3c2744-54f0-5c4f-817c-8e5bc7d15a9f", + "version": "MTc1NjMyODAzMDcyMjA1NjAwMA", + "digest": "sha256:563c91f7124f22dfbb7a03cc9333a10c7f534afb7488027788c63fb4df8ddb5b" + }, + { + "id": "5e44ed01-527f-5254-b6de-3daea7f8eea1", + "version": "MTc1NjMyODEyNDUwMjMwNDAwMA", + "digest": "sha256:1c9428312f9d01fce3565ef7819eaef3b12b110a55ccc3525df59132c1e62447" + }, + { + "id": "5e8f91e7-e408-5cbd-97d3-029d9794cee7", + "version": "MTc1NjMyODE2MDQxOTUyMTAwMA", + "digest": "sha256:746978a0a414e9d1d179e5c854d360f59ee639380d1da90906004374aa05213c" + }, + { + "id": "5ece61c1-2fca-55fd-a2d7-35e463cf6d00", + "version": "MTc1NjMyNzk5MDM3MTUzNjAwMA", + "digest": "sha256:e763bbf6643c4996b2e68daa787ef844fab7812faa80809d8be6dba36c5ee9ce" + }, + { + "id": "5ed42212-ea4c-5900-8cfc-729f179e6178", + "version": "MTc1NjMyODA5Njk1OTk2MzAwMA", + "digest": "sha256:e5246bcc98516442d2fbf40fef39167075c6b987aaf1ad5d8cd628de6a025f8a" + }, + { + "id": "5ed8fafa-50a6-5271-a8ee-9051b5e96d54", + "version": "MTc1NjMyODE2NjQ0NjUxMDAwMA", + "digest": "sha256:92021c9b759c61b06da094d5f3196a7841b8b72c3fa81be22c2840c05707dae7" + }, + { + "id": "5ede08de-958b-5411-af6c-c06bd74e5244", + "version": "MTc1NjMyODA2MjY3ODg4OTAwMA", + "digest": "sha256:7f818fac3297c47d21843eb01c777b6853e5df62e1f0c8cc6f294090caca1104" + }, + { + "id": "5f173e93-0f27-54ff-92fe-48536f59c9cf", + "version": "MTc1NjMyODA0MTg1ODA3NzAwMA", + "digest": "sha256:2dbac36004b0d249a77035b8d65adff5e842aae6ff5f04f4dec2185da12b0da3" + }, + { + "id": "5f1c5497-805d-57a2-be63-499bd944313a", + "version": "MTc1NjMyODAyNjg1NTM3ODAwMA", + "digest": "sha256:41c7d796a521937e473aa8f9c589396a9a6cc539a1ca6d5c22e9a749381fe555" + }, + { + "id": "5f40e06e-c6a0-58fb-b6d8-8d58ab750cf5", + "version": "MTc1NjMyODA2MjA3ODQxMDAwMA", + "digest": "sha256:365667fc7c7e92ce5743e88329e0d12137c8fe8b4f9c28bfc681eb3f27e4eede" + }, + { + "id": "5f42dc52-bf40-5f87-af3f-914de01ce072", + "version": "MTc1NjMyODAxNTUxNTg3OTAwMA", + "digest": "sha256:4dbcc277c842d3fdd0d309551cfcd7545543f2be1ead52f17ab396597214cf80" + }, + { + "id": "5f49e861-bb54-52b9-9f62-3e4396c0e89f", + "version": "MTc1NjMyODEzNzA1ODUwMTAwMA", + "digest": "sha256:2d5ccb3def5036c0e6168346cf3c94129083e02c6ed1555e8df9e9f4034ed0ed" + }, + { + "id": "5f4ed703-d235-5ebf-867f-973a36f659cc", + "version": "MTc1NjMyODAxMTEwNTQxMjAwMA", + "digest": "sha256:14cb1268e0a2bee0dd03b81146d28e3272cb7fb242322fdb9ebcb797180b5c3e" + }, + { + "id": "5f5c548f-6b26-570f-906d-6ac9afe6bfad", + "version": "MTc1NjMyNzk5OTMwNjQzNDAwMA", + "digest": "sha256:1da8110349cbc0d4299ead838b4141eaed90f1d9138356d096a7b3c0f3b1034f" + }, + { + "id": "5f6026cd-ba57-55ad-b13e-156e5a6ca7e1", + "version": "MTc1NjMyODA4ODU4NTUxMTAwMA", + "digest": "sha256:dabb5592d721718e600b24ad586d61be4c03ccf31e07786ab2ca483460364dfb" + }, + { + "id": "5f6b609c-6b17-5f66-b1c8-d44d9ac87646", + "version": "MTc1NjMyODAxOTI2MjMyMTAwMA", + "digest": "sha256:bc39aca5694d004d2619e23f7cca82acfb5c96a0aeba840affe3fac6368a7ed6" + }, + { + "id": "5f6de6c3-a841-56b6-b073-70547b4322de", + "version": "MTc1NjMyODA5ODcxOTg2NjAwMA", + "digest": "sha256:40e65f68f46594b89718cc7c25a06a5d69aa888c2ade9cba1b9781f7561f41e6" + }, + { + "id": "5f7bfb6c-7062-5f56-a311-895bbeead134", + "version": "MTc1NjMyODEzODE2MTc5ODAwMA", + "digest": "sha256:cb71707f0f8f73e25c703065a85caf6d68198ea06ed0a1c6aa4d557f620b3233" + }, + { + "id": "5f992bf9-94a0-59c6-8097-fc9bd24e3482", + "version": "MTc1NjMyODA3MjU2NjU3MjAwMA", + "digest": "sha256:3260bd0d36dbb86dc47a005137dfd71a46c765a152c1fc89a84f0ab62f9ee195" + }, + { + "id": "5fc56072-ffcc-5148-9d8f-0e89cab6db31", + "version": "MTc1NjMyODAxNjU1MTUzNzAwMA", + "digest": "sha256:bdca803f1828178440cd5648ad03e99a8df8e4d4eec0a082724fd485957d7759" + }, + { + "id": "5fc8ebea-9ac5-5409-b368-1a5a8ba2c488", + "version": "MTc1NjMyODE3MDQzMzIyMjAwMA", + "digest": "sha256:4f23bf776a40b911be76c9db5d48c56e1e999b455dd84041b996b06737fe44ea" + }, + { + "id": "5fd3e5ed-05ad-5912-be80-6af6f4336335", + "version": "MTc1NjMyODAzNjg1MTI0NjAwMA", + "digest": "sha256:1c313b01d088726baf7288ef1c2195451031ecfff9d2e915880085c7372e5b26" + }, + { + "id": "5fee81af-4485-5eb6-897e-2660e6611082", + "version": "MTc1NjMyODAxMTYxOTQ4NDAwMA", + "digest": "sha256:2521763416f6471b22470fe1ecb2714a1fd9c611a68a4e48fbd0a0c7f3de2b19" + }, + { + "id": "5ff5eb65-3a6e-57a7-911f-826d155806f4", + "version": "MTc1NjMyODEwNTMzMDE1NzAwMA", + "digest": "sha256:b813e4fbdd1dc7a616fb82676e395f271c99c3fc96a1165389eb01d073e99a84" + }, + { + "id": "5ff907f6-f947-57d4-b88f-8e70d8e84583", + "version": "MTc1NjMyODEzNzY3Mjc4NTAwMA", + "digest": "sha256:d9c509dc1d728266fa07f44a55d2b901a155fe084405cb871a709a088c21ff7e" + }, + { + "id": "601c2a92-af80-56cb-ba0c-f255b4ac00b8", + "version": "MTc1NjMyODAwNjE4OTE0NTAwMA", + "digest": "sha256:8f318ec09e632a39251b6c516bacea4ecfbbef0c24880ce6b135f0b3df6c399f" + }, + { + "id": "6031446b-cb60-5949-9772-6e3834bab3de", + "version": "MTc1NjMyODAxNjQ4MzE1OTAwMA", + "digest": "sha256:2ad1e3f92a85cc756ca22953c709cdc9b2fd93a6034df93e34933d6e2dc83a96" + }, + { + "id": "603f5961-6aae-5e21-925b-f05b162362b5", + "version": "MTc1NjMyODAyMjA5NTU1ODAwMA", + "digest": "sha256:afebbd0fd9f8fa140def2699c495674bc9d2a29dac241ea2a85b58ccd54c4466" + }, + { + "id": "6045efec-c408-5b28-b5b8-c5f9904192f7", + "version": "MTc1NjMyODAxNTI1MDk4OTAwMA", + "digest": "sha256:abba7a9ed58144185c08014d6077f3f44341b2eaae25e357786d4d72f23e9743" + }, + { + "id": "6056b435-2bc1-5001-b638-320bd113e973", + "version": "MTc1NjMyODAyNzQ0ODE5OTAwMA", + "digest": "sha256:68148c9ee5d702920e5bfacc2f2a319222f55dc017465af0fdc394fb30b068e2" + }, + { + "id": "607411f4-e794-5585-95e5-fa6bf3574905", + "version": "MTc1NjMyODAyNzgxMTQxMDAwMA", + "digest": "sha256:948dbd1bef212a561869751f6bc2a862da7b0703103075f6f78c428a920c42ff" + }, + { + "id": "6096d370-ff61-5346-b05d-6efe246d711e", + "version": "MTc1NjMyODA5NzY2MzQ2OTAwMA", + "digest": "sha256:6bcf419da8cb716b9104e0128b502fae6e5412d17bd7b30e0b1a446a3aec74ac" + }, + { + "id": "60a3b060-c0d5-5ea8-8470-99bffd481a11", + "version": "MTc1NjMyODA3MjUxMTMwOTAwMA", + "digest": "sha256:629e247061509a42f7f4e460d4e5cd949e320b9979c8f0795ba368c140d2d5b9" + }, + { + "id": "60e8363d-cd74-5909-9aa7-3f11e72d50ab", + "version": "MTc1NjMyODAzMDc5NzQxOTAwMA", + "digest": "sha256:d0ad97abde5c5710658a4806962338559356ae772d6aebd4e7b2dee441e3bdb2" + }, + { + "id": "60e871e4-46a4-54e9-a99a-32941b6caafa", + "version": "MTc1NjMyODEzNzAyNDU5OTAwMA", + "digest": "sha256:8ae875fee7cd6bc29c50761f6f1a333633a7cfcb854bbfbdd03fe6d62290d053" + }, + { + "id": "611b16c9-8c41-5bc3-93b6-23de92bcc628", + "version": "MTc1NjMyODA1MTIzMjY5MjAwMA", + "digest": "sha256:610c8d54b077126299481ede84f57c5ca7c04432e7e56adb104c81467206c44b" + }, + { + "id": "6161ead7-2176-568b-a2e0-505125a04e85", + "version": "MTc1NjMyODAxMzg4ODE1MzAwMA", + "digest": "sha256:d1543800ec463aa93aa255df0ceb5405eefdd59e21c6f8d187c0177a103ad5ac" + }, + { + "id": "61665b1a-b731-5c10-9c61-c4376b5cc273", + "version": "MTc1NjMyODAxMTYyNjM0OTAwMA", + "digest": "sha256:7e3d77585ed41208159475470e0ae11e9371862adac2e53e201e223a2099ec64" + }, + { + "id": "617621bc-dca1-5e34-bf99-c83c3cf4c79e", + "version": "MTc1NjMyODA0MjQyMzYwNTAwMA", + "digest": "sha256:d4ef03d5c93e3561f9307eb1b624e9e676ab17f7679fc08fd309a9fd811b4f81" + }, + { + "id": "6184709d-3c8c-55f9-b81b-edc02d7f70d5", + "version": "MTc1NjMyODA2Njk0MTQ0NDAwMA", + "digest": "sha256:fe60ca8337b6a1221a3ab9934166e984739d003e54923c5f36fc91af38e12d0d" + }, + { + "id": "619a6069-be36-5f7d-9873-5ca97b9c1128", + "version": "MTc1NjMyODAxNTQ4MTU3ODAwMA", + "digest": "sha256:be328865fcf0f0f3a9044aa4ec794cce77a5001995eaab72a3511819ff5ee30f" + }, + { + "id": "61c9249d-7b01-56cc-ad97-a0b5533172ef", + "version": "MTc1NjMyODExOTU2NjAxNjAwMA", + "digest": "sha256:a75d36c0daecd03f553315e5c7b2d5858352284bf7fbf954219d6a60c7979ce0" + }, + { + "id": "61cf3036-5138-5f68-9cb5-f3900a9b2ffa", + "version": "MTc1NjMyODAzODY4Mjg1NjAwMA", + "digest": "sha256:964860f426ce65a9793dea5e472017b36a2e9df7bc7c6bc3e4776ca0be72c567" + }, + { + "id": "61d97a43-4d24-544e-868c-3961d3a4e4d6", + "version": "MTc1NjMyODEzNjE0NTQzNjAwMA", + "digest": "sha256:4d95451e810c03b5aa87ec346a4bae34301ffa820132fbe7a3d0b51e8a605537" + }, + { + "id": "61d9a3ce-6f04-58b1-a2eb-bafa90d8d80f", + "version": "MTc1NjMyODA0MjUyMDIyMjAwMA", + "digest": "sha256:97e5e112bd56b7d44300929056a53daba4e0e641a15b886cfca2b18bb0499e3f" + }, + { + "id": "6227d287-bf3e-537b-962d-1d3380bc58df", + "version": "MTc1NjMyODE2NjQzMzc1MjAwMA", + "digest": "sha256:75c287f4a0fafcf8a17daa83bc2bb8d57b48efd1f6e25a9b7eb2308f5c5aa5fb" + }, + { + "id": "6253aaa0-c00b-5a43-a507-e5e133478814", + "version": "MTc1NjMyODA0MTg4MTQzNzAwMA", + "digest": "sha256:d3aa4ac932958571e5a9f65d39371b94236c4cc36535760089d485942f6904da" + }, + { + "id": "62587f1a-a212-53cb-8193-711718db6de3", + "version": "MTc1NjMyODE0MTYyNTU2NTAwMA", + "digest": "sha256:85c86c4fe351c02e54ab92690b2ebe60d592bc46f985ec1644968055a2f7a023" + }, + { + "id": "6267344f-2d23-56dc-828d-9143461aac9a", + "version": "MTc1NjMyNzk4ODYzMjc5MjAwMA", + "digest": "sha256:ac8543eb5ae9d6791ea31fff13bdd528f8e355debf17606ddbee65c4d807c77e" + }, + { + "id": "6272a646-204d-5bf8-bb6b-07684243d7ee", + "version": "MTc1NjMyODA0MjQ3MTkxNjAwMA", + "digest": "sha256:414b358a40f6e3ccf7f7988eb840e8ea8bc3a4970e9faf2f8c1c58c1c01c2a3f" + }, + { + "id": "62775765-85d2-595f-b160-c1a153edba54", + "version": "MTc1NjMyODA3NDIyMDE5NjAwMA", + "digest": "sha256:2ce51b7a161cca46424de35749d158bd34348b167006c153aeb0b8404fd10b98" + }, + { + "id": "6297497d-12d7-5358-8446-e2a19b62033e", + "version": "MTc1NjMyODAzNTk3NDI0NjAwMA", + "digest": "sha256:c50448fc2f25b8e84bdbd1ebac8276ce2fa1f8b2bed292b43b28fc254bd89ad6" + }, + { + "id": "62a8d073-c1e2-5610-8582-7160e7bae57b", + "version": "MTc1NjMyODEyNDQyNDEwOTAwMA", + "digest": "sha256:e459afa52c183eb574f3c3d314a14bb4ceff68e6c321ca48597bf5e0e9b4ad65" + }, + { + "id": "62bded17-ed4a-5083-8502-e57467495c90", + "version": "MTc1NjMyODE2ODgwMzQzNjAwMA", + "digest": "sha256:f5837137dc0e0ab439c586ef871cf03c40495c9a75cf4b95712c2b2a3b43e02c" + }, + { + "id": "62d77d3d-dde4-53ff-a6e9-fcb1780cf89f", + "version": "MTc1NjMyODA3MzA2MTA5ODAwMA", + "digest": "sha256:42c2e7f950cbfec11f2756b82cce6a67346ffb104421cb023c07cd00acdb8f45" + }, + { + "id": "62f81e0c-0d6a-57be-885c-4e403e793842", + "version": "MTc1NjMyNzk5ODIwNDYyNDAwMA", + "digest": "sha256:6663a9721a3de3ec95d464f774947fda5b13f1e9ff54b5de773c6192127f7ed3" + }, + { + "id": "63240195-f957-521d-8661-dc3b9604e881", + "version": "MTc1NjMyODExOTIyNjM1MzAwMA", + "digest": "sha256:dc134928e0a6bf82ce35aee68581eea20cf5fef9ceb21855e7e1844205a8f1d7" + }, + { + "id": "6365d2d4-e697-5727-b72f-5841255524e0", + "version": "MTc1NjMyODAzODcyNjI4ODAwMA", + "digest": "sha256:a3924852ea5e824bac35d40b3e436bb0fa30b359237cd26f9a15a1b81a5940c2" + }, + { + "id": "636826d3-a488-5b85-9664-e83c4853b8f6", + "version": "MTc1NjMyODExMzUwNTIzMDAwMA", + "digest": "sha256:fcbbbc4f8cfdd9a1a080414ec6817f5325e6e21fd3e42253e13bca74f4772447" + }, + { + "id": "6369599d-8810-537f-9d13-abd48a82bf57", + "version": "MTc1NjMyODA2Mjc1ODYzMjAwMA", + "digest": "sha256:b4a785b1cec0bc40b670bf6455fabbee30f96b3775246051ec5da5b2917c301a" + }, + { + "id": "637d912b-e8ab-544f-9a1f-2fdcf43930da", + "version": "MTc1NjMyODA4ODYxNTAxNzAwMA", + "digest": "sha256:8385c6da9a2750935554b9fda1044577827d58490930f8d9337164d35c4972c7" + }, + { + "id": "63935457-6991-5575-a0e2-e59df9526d2d", + "version": "MTc1NjMyODE2ODkwMjcwNzAwMA", + "digest": "sha256:39119ca935727e963144c5d42323baffc7302d0bd90d312400c22128a3394a27" + }, + { + "id": "63987144-6b21-558d-9a5e-104e50dbbe81", + "version": "MTc1NjMyODAxMTY1MTA4MjAwMA", + "digest": "sha256:ab77be7b8dd9f6f5739e8b3c29aca70d9a14ceb56114918ef4ad5b3fe5b72070" + }, + { + "id": "63b29ba4-5bfa-56cc-b544-f1999427c773", + "version": "MTc1NjMyODAwNjE2MjA2NDAwMA", + "digest": "sha256:180a7000d432f478609cd9654d6b79200575ea6106a3300a1d46d1205699678e" + }, + { + "id": "63dd45f7-363f-5c71-8c66-8d1dfa84f510", + "version": "MTc1NjMyODA2MjA1MTk0NjAwMA", + "digest": "sha256:4ff0ae60e1ce16cfdbaeeb99ad6e7028b72ba5bb0cb5ba5dd48449b415e84ed2" + }, + { + "id": "6419fc95-aa95-5425-b6ab-ceb6a99f4489", + "version": "MTc1NjMyODA2MjA1MTA3ODAwMA", + "digest": "sha256:646dc2a462bf76d1d767ecda6deefffaa7c1ad68cd0a4a8ad248fed40c39ab21" + }, + { + "id": "6421aab9-c1e4-5f7b-b41f-604ac9b1eca6", + "version": "MTc1NjMyODE2ODgwMzY4OTAwMA", + "digest": "sha256:34d1b540d63c0b664b8aa0bb7ebd2f07f3aeb067363bd197b7a8156bf61aebf0" + }, + { + "id": "643dd46a-7837-588c-9552-b65d199dddf8", + "version": "MTc1NjMyODAzMDgyNTQ1NzAwMA", + "digest": "sha256:c61ff643d99280fbe72b656f5e605851a0619bcf1b1d381d13ef19f2bab0e859" + }, + { + "id": "64498708-7a4e-5d3b-8512-f57939d301bf", + "version": "MTc1NjMyNzk5OTI4Nzc0NTAwMA", + "digest": "sha256:c718ccefec231758cba2295b07a660d09d9db3fe93c3b964eaf505f04b9da8ef" + }, + { + "id": "644a9395-9195-5ed0-98b5-21317d7686c6", + "version": "MTc1NjMyODAzODgwNzIyMzAwMA", + "digest": "sha256:68fe0ba320b15e2c34c6595b260938bd868df592d47b622fdd90d923145381fc" + }, + { + "id": "6459a251-8549-5e96-ae25-afc9799d627a", + "version": "MTc1NjMyODE2NjQ4NDY1ODAwMA", + "digest": "sha256:624555c3a28a99d143ee2d885f0c8c59b5fc7f8ab35b1dc5ce0a5e513f1dcdc5" + }, + { + "id": "6472822a-278d-5808-aa8e-a708cb2fe4e3", + "version": "MTc1NjMyODE0MTYyMTc1MDAwMA", + "digest": "sha256:8d8693380335251a7c3902d97966d87c287f181d8241cb6afa5b27bce2b5d0c9" + }, + { + "id": "64c8ed8d-90c0-58e0-b521-adaf50cca6e0", + "version": "MTc1NjMyODAxMTA2MjMzNDAwMA", + "digest": "sha256:acd12631cb7ab9557a57aef586bc8e146bc6ae455a90108f3ec557873dbdb4ba" + }, + { + "id": "64d6ccac-f34e-56e7-947e-1948d55d99a6", + "version": "MTc1NjMyODE3MDQzMTc3ODAwMA", + "digest": "sha256:d1e033af7e435ab8f75dfd5ff54f86d4c7d7421042b760ff490eef4dbc76d29e" + }, + { + "id": "6526c4f5-2501-5634-8a67-786f08aa49f8", + "version": "MTc1NjMyODEyNDUyMzg5NTAwMA", + "digest": "sha256:4235af2c1bd7221808d13e2287e7193edc672af026bc07308c642f25dcccf10b" + }, + { + "id": "652e16d2-57d7-57bf-b214-d814656cdd41", + "version": "MTc1NjMyODEwODc4MzIwMTAwMA", + "digest": "sha256:44b7c56040b2bc73c2ab2e4f41f40e64587594f0657f4e8286ab2f89ff02b3a1" + }, + { + "id": "652f2300-068d-5b58-a793-cf9889d93a5d", + "version": "MTc1NjMyODAxNTIwMDIyNjAwMA", + "digest": "sha256:5b95788c867e93533d108a56a65e0f21530f0e7e1665d552fea611e44ace0cf5" + }, + { + "id": "654bb599-4b8b-5507-9468-b39bdbc7fddf", + "version": "MTc1NjMyODE1NTI0NTkwNTAwMA", + "digest": "sha256:ac83e700d18a0cdd1dad648133665136055e185c9dc8447ae2dc5e56ecb550fd" + }, + { + "id": "654c551c-d0de-59a1-92e2-0482dc30b922", + "version": "MTc1NjMyODAxMzg4NzU0NTAwMA", + "digest": "sha256:2fa17004f81d84cb41a5f07ca992b704fed6f73a8b37b1ce847c78899f8c666f" + }, + { + "id": "654d398e-94aa-51f6-83aa-8f9f8df8b69c", + "version": "MTc1NjMyODAwMzk3NzAyMjAwMA", + "digest": "sha256:e09b871ac70d46c6cb0c590855af1742f4fb6e9723561bfb7aa1f142f6556b2b" + }, + { + "id": "6563afba-53ae-51d8-9999-bc46790cde13", + "version": "MTc1NjMyODEzMTE4NzI1MTAwMA", + "digest": "sha256:5b7d89b536a991f07ca3c759f5cc6cc0c858026ab248e31fdc296409ac8973e4" + }, + { + "id": "656787c0-4f47-54dd-80f5-3e8b1c13941e", + "version": "MTc1NjMyODAyMDUwODUyODAwMA", + "digest": "sha256:6ca44ab04add49fa710a1587338e0d09d90d80d83b4c9f68f0d7775e08d882f3" + }, + { + "id": "65684bb5-d808-576c-a156-e57c8e81f341", + "version": "MTc1NjMyODEyNjUxODY3ODAwMA", + "digest": "sha256:7d756bf3e0ecdf6d4f8371679f1432b4d87a341b1c346c25858bb9053eea5170" + }, + { + "id": "65881c61-9660-5ff3-9c3f-b50428948571", + "version": "MTc1NjMyNzk5MDQ0MzgzMTAwMA", + "digest": "sha256:62434a1d0632022f31bbd4ef0e776c4e0ba0464e9f994880308c7440e4efc111" + }, + { + "id": "65a21ca4-d583-5be4-bdef-ee39c3cc00a7", + "version": "MTc1NjMyODAyMzQ3MjI2MjAwMA", + "digest": "sha256:3a1cbd54f2a6fed44e83a4ba5836396ecec5372bfdc95d8b5e9ae66ff19f10ac" + }, + { + "id": "65d36823-9c2f-5bfb-9681-dc2e74fd3cfb", + "version": "MTc1NjMyODAyNzc3Mjg3MTAwMA", + "digest": "sha256:1711d8c66b70025dcbc1cdbd78436158d331dd60c5496cb95b762ed02b03c981" + }, + { + "id": "65e64658-5dd3-5861-a043-2537e018d377", + "version": "MTc1NjMyODAxMDQ5OTg2MjAwMA", + "digest": "sha256:547c7e737f92a72e85f92294163d47b8989c3f1e02287683b57f6599ed14f4d4" + }, + { + "id": "65f19252-da69-515e-81e8-0adadfaeff58", + "version": "MTc1NjMyODAwNjI0NTQ3MjAwMA", + "digest": "sha256:523ce117086c643576d49c4dbedbfa99c25027b88df2c38e79466e5f02ff4ee7" + }, + { + "id": "65fb2290-e83c-534d-97b6-37d8c7783c2f", + "version": "MTc1NjMyODE3MjUyMjQyNDAwMA", + "digest": "sha256:d36f5af3c9eca7eb85c8b0d1412a61c6bd108187457166b25a67031df9e739e2" + }, + { + "id": "66087620-fdf3-5884-b1d4-96c3caa428a7", + "version": "MTc1NjMyODAzODc0NDA1NTAwMA", + "digest": "sha256:21773cd9f8d69c2ee5f63d4bb69c71c7b2ac319875a5033031ff9bce69c7f1a3" + }, + { + "id": "660cfd65-02d0-5211-a296-eb2aa08cf89f", + "version": "MTc1NjMyODA0MjUzMTU4OTAwMA", + "digest": "sha256:2accde8caa2de6d2644966fbf82ab4678b6ab655e1258d3039345d5f802605dd" + }, + { + "id": "660fe1b7-1d52-5f17-a415-fbd6efd70658", + "version": "MTc1NjMyODA1NDE4MzM0NTAwMA", + "digest": "sha256:12b923d6051cf084824134c6cb86de9e204bea83670f05f1183d9a1bde956b8c" + }, + { + "id": "66141d21-9a9a-557a-9292-78e3254f8310", + "version": "MTc1NjMyNzk5OTI5MzU3MTAwMA", + "digest": "sha256:380a7576a7f9c6ac05b03525fe7490f6743123c02524616e3ff2d14049c60a59" + }, + { + "id": "6617efc3-8c4e-5338-a5d6-d354bf8dac6c", + "version": "MTc1NjMyODAxMDQ5MjcxMDAwMA", + "digest": "sha256:eeb1cba30bdf7a6ba2c03b3fad00c151bf56dca69f76680a824db2d3d414d201" + }, + { + "id": "663aa5af-9ddc-5022-93f1-e82e32890ebd", + "version": "MTc1NjMyODA5NDM1Mjg0MzAwMA", + "digest": "sha256:2bdcf9faafab7017c6b297c07c14f2e2c134687f16f207e99a1da8b582cc1c42" + }, + { + "id": "6649e9e7-7929-57a4-acd9-c8c41bc09ea6", + "version": "MTc1NjMyODAyNTc3ODY0MTAwMA", + "digest": "sha256:7f02a6fcf61599ecd92085a26395c16c3dce473f97487da29220f4812ede15f5" + }, + { + "id": "665f1fa3-c7fa-5268-877a-819423b4ce37", + "version": "MTc1NjMyODE2ODg5MDA2MjAwMA", + "digest": "sha256:12d5f125374e4d59359b4bd4fa466327aa0a0304c0783a8ae7731bcb9aa7e561" + }, + { + "id": "666a4e0d-c926-5444-aa44-20c6ba14c296", + "version": "MTc1NjMyODE1OTA5MDQ3NjAwMA", + "digest": "sha256:496319854aeb5829ad60b3d5886dcb226cca2d50b5bff93e8e702e34263101f6" + }, + { + "id": "66781fc0-948b-589b-954c-6872496523b3", + "version": "MTc1NjMyODA1MTM0NDE0NTAwMA", + "digest": "sha256:31dc1d429c4d8f3592415b063889f15cc0bf5a82828b6866000991a38747d9be" + }, + { + "id": "667eba77-9c2a-59bd-b743-64eccf1e58d1", + "version": "MTc1NjMyODA5NzA0NTk1NzAwMA", + "digest": "sha256:df220fe32b0c5d009584b13584afda77a4c40333706f80736bfce9a274a84017" + }, + { + "id": "66800f00-ea83-5de8-baa6-353afa880a31", + "version": "MTc1NjMyODE1NTIxODQ0MzAwMA", + "digest": "sha256:004c8e1d66be701d4ae4321858d3906ede3ad992655bb21c1acb65aae0605f2a" + }, + { + "id": "66b23983-ce12-5359-a2b8-553b228cf55c", + "version": "MTc1NjMyODE2MzM5MzAxMjAwMA", + "digest": "sha256:e90575eb03fb7d54bb1b92e249989a4fb4a2a8cbdd9fca218f9635f0846d88fd" + }, + { + "id": "66b4578e-6d58-51d2-9db5-9597cdcd695d", + "version": "MTc1NjMyODAxMTkxOTI2MDAwMA", + "digest": "sha256:9739edac5fc1f35083ab33227f293068c2358fca741e8517626b3c22f4b7690d" + }, + { + "id": "66c269f7-c7da-5ebe-8d7e-dfa147e9b269", + "version": "MTc1NjMyODAwODk4Mzc5NjAwMA", + "digest": "sha256:7c92735039d8b3d721032a7ad95f54429cf9c16cbca421d5c0d7d8b271fd8d84" + }, + { + "id": "66cfca75-41d5-5a9e-8425-5da1a764532d", + "version": "MTc1NjMyODE0Nzc1Nzc0NDAwMA", + "digest": "sha256:b2a56ecc720da54dab7034c64473b173f812fe7568f0fb1ac0fb22a4777e9733" + }, + { + "id": "66fd3612-d095-53f3-bd1e-bd98735be54b", + "version": "MTc1NjMyNzk5OTM3ODM0NDAwMA", + "digest": "sha256:efd6d153d37152daea194e3b008af45f766d1d6be2ae15b10483b041ae464a5a" + }, + { + "id": "66fea44c-88a8-5d2f-b2e3-91b3aa76b189", + "version": "MTc1NjMyODA0MjAzMDQwNDAwMA", + "digest": "sha256:5a060d3ba382942f380500253a3fd29c6d27a08811c96fd04702b11551c12e7c" + }, + { + "id": "6723951d-b319-5d2d-a43d-c86e9cb4b16d", + "version": "MTc1NjMyODEwNTM3MDMyNjAwMA", + "digest": "sha256:03239075a640ccf57bafd41322d63bf2ae1b871bf29c93731887d12a8f9367c6" + }, + { + "id": "672f31d4-d425-56fd-a3e8-471bcf4a3bef", + "version": "MTc1NjMyODAzODY4NDAwMjAwMA", + "digest": "sha256:67b01a11d823229ecd7a173d2c92bae08dab909d9bfc5f376dc4ab4887cb2865" + }, + { + "id": "674e81d8-8fe6-5bdd-838b-9c7e45c483b4", + "version": "MTc1NjMyODAxNTQ1NTg4MTAwMA", + "digest": "sha256:32b7390f0c3fa910e579c164e409be2d6e322013eb1f61acf8db15bd68a14f8d" + }, + { + "id": "6759c003-93fc-57b2-abd9-25f625899f0f", + "version": "MTc1NjMyODExOTcyOTk3MDAwMA", + "digest": "sha256:799683f53b5019f874556da24873479f0adf9dd07c41fff1bbf5f95617227621" + }, + { + "id": "676dcf58-8e49-5afc-9436-4cd0421a436c", + "version": "MTc1NjMyODEwMTA1NDk2NTAwMA", + "digest": "sha256:69baa870541708de549fa16fff8788ed9859d1610592918debd0db19cfe1acd1" + }, + { + "id": "677d774a-f587-5290-971c-fde6399a079f", + "version": "MTc1NjMyODA0NTgwMDYxMDAwMA", + "digest": "sha256:d1c64fab422f5a26b295b615f1535b0143c099e79b6bf72bdffc8a7b79735130" + }, + { + "id": "678dcaf8-0ae8-5bec-a3ce-c7607f292380", + "version": "MTc1NjMyODEzNzYxOTQxMTAwMA", + "digest": "sha256:7411b39bd29810a595d50bdc4466e210f6252043cd284f4cf92e2a2d27243324" + }, + { + "id": "67aef746-5692-5edf-aede-279d381a130c", + "version": "MTc1NjMyODAzNzExMTE2NTAwMA", + "digest": "sha256:439548627e8d79802027374c68ab68aea1290ab194941a059b3b2a0fca4b816d" + }, + { + "id": "67df3db1-4713-56d0-b8e5-38099b64d277", + "version": "MTc1NjMyODAyNjc3MDY4MTAwMA", + "digest": "sha256:8585d57a74ca53640aa9fcf3123507858683f5611e47f2b80cdcf8126f6b107d" + }, + { + "id": "67f9358c-97de-5110-b96c-be75781b2510", + "version": "MTc1NjMyODAyMTI3MjYxNDAwMA", + "digest": "sha256:d4286444ecf6ce90ad4b308ebef4fa391b72ba4bda38e57c9747fb7ca61f33a1" + }, + { + "id": "6805a891-d1e4-597d-ac80-e004c3413911", + "version": "MTc1NjMyODA2MjU5ODQ2OTAwMA", + "digest": "sha256:20d017ff51ee07dd83491e15752fc4207feee2eb593605619fa5e137250d4cb7" + }, + { + "id": "6842b4bd-9ff5-5a69-979c-7d7604163768", + "version": "MTc1NjMyODEwMjI5Mjc4NzAwMA", + "digest": "sha256:7ce0e903c899e59644f6c3972d21a68dda7e6bb703cc416b4cc6aaed4b4acf31" + }, + { + "id": "685c7538-0c6d-5e67-8681-90fe8a6f7c44", + "version": "MTc1NjMyODE1OTE4ODkyMTAwMA", + "digest": "sha256:5a197d68c5ef2c98dbc9143fa2846912d8fa9201b84e109c81c71f1410a7f368" + }, + { + "id": "6877d224-93ff-5408-a08f-723115512e39", + "version": "MTc1NjMyODEyNDUwMTMwNjAwMA", + "digest": "sha256:05e79215c73e09d07dc969a353552b8f81d08d875b323bcae333bf6edb843323" + }, + { + "id": "6891d0d9-5b12-545a-bc5b-09a994b29aad", + "version": "MTc1NjMyODAzNTkzMTEyNTAwMA", + "digest": "sha256:3b9de8bd7e4c03a63e76ef09fe9cfb1d08869a619273a7856a1a92829e612243" + }, + { + "id": "689c2406-de0b-54e6-b15f-39be3197a639", + "version": "MTc1NjMyODA1MjY3OTQxMjAwMA", + "digest": "sha256:dd50f45325e6f06d256bf90059c18c4f5d2aa2953b642001b3b02a40b3394ab9" + }, + { + "id": "68d372f2-a526-58ec-b5e2-197734f56da5", + "version": "MTc1NjMyODE3MDQ2MzU2MzAwMA", + "digest": "sha256:f24f4d0383bc1afc23bf811c30f0c7448e7a0f66e53e678ebcaec573c6e11e01" + }, + { + "id": "68e71b1e-a07d-5ba5-9d1b-94b6b8b57bdf", + "version": "MTc1NjMyNzk5NTM5MTEwMjAwMA", + "digest": "sha256:21127047ff71c23cabecd0599efe9dece5e496d6641ac085632b50a31f24d0f9" + }, + { + "id": "6957333a-b6ac-5529-9af2-5864cc9f56d0", + "version": "MTc1NjMyODA2NzExOTY1OTAwMA", + "digest": "sha256:13e3aec0cba3f0067966eab6b0dfbd9499e1fbdbbcad64a01b5df446d5fc9546" + }, + { + "id": "69676307-bf30-50b1-837c-d1fa5b180b5e", + "version": "MTc1NjMyODE2MDU0NjI1MjAwMA", + "digest": "sha256:b0b2cc4603eaa96040184b467ab003601a3840d38e6fb9acd3e8d15fa712037f" + }, + { + "id": "696cfbd6-2c9e-5624-8905-ff00d5651d1a", + "version": "MTc1NjMyODEyMjMxMjE0NTAwMA", + "digest": "sha256:4d1bbcd98f8f948c586460b1bf0c4a28d7cc32c7883993083796624271a13b49" + }, + { + "id": "696d6524-6a56-5a33-8179-5c5da874d96f", + "version": "MTc1NjMyODA5Nzc2NDk3MTAwMA", + "digest": "sha256:7aa22361aba5d0d9c642892d0c2a3f2daa53c774884995c2fe561b6b486c6524" + }, + { + "id": "696ffbf6-63ee-5d2a-9814-83d5a1d52940", + "version": "MTc1NjMyODAzNDcxOTkzODAwMA", + "digest": "sha256:131d0f2823da13b6343d8f15ed119c098ab5903a3c07bb8796de847dfb8d3f39" + }, + { + "id": "6970f77e-17b1-5e72-9970-65010147a588", + "version": "MTc1NjMyODE2NzQ0MjU4NzAwMA", + "digest": "sha256:90d1cb9348bce30c743cb5d0166335a2506ab562965e0b3bb777f41c00ec9504" + }, + { + "id": "698c047a-dd06-595e-b7ac-171c8d5b21b2", + "version": "MTc1NjMyODE3MDg0MTEwMTAwMA", + "digest": "sha256:1ac52e0aeab2586caa6b26e2e00167fe936b4cdc8aff5026f6d24b17a7e567cb" + }, + { + "id": "698e6aef-4d61-5fd1-b0cf-5ace6d263e47", + "version": "MTc1NjMyODAxMTExMjMzNzAwMA", + "digest": "sha256:c51c907856d8496040b931695bd812811fd58163eb18b52baa70a92acdcbc771" + }, + { + "id": "69953af9-0053-511e-ad19-b8a5415dab19", + "version": "MTc1NjMyODA0MTk1NjQyNTAwMA", + "digest": "sha256:db4efed4c6e45d68f4225c1632303b27974af294fcb830fb989a4fcd9b02af20" + }, + { + "id": "69e6a61b-10a9-5dc9-bc09-58d8c082194a", + "version": "MTc1NjMyODAzODYzNzI3MDAwMA", + "digest": "sha256:00a1e549f6809c6aca9cc265a932df3ceddc2449c011a5bba145bcf58bead2bb" + }, + { + "id": "69eb206e-015a-5b1d-95ba-9314a356bbe7", + "version": "MTc1NjMyODA4OTM0MzIxMDAwMA", + "digest": "sha256:716838ba1b3dd467538d247875c54cda88d8dae855d417202c1cc428498968e3" + }, + { + "id": "6a08730e-4ac0-59ec-be8b-e85eada2f8ce", + "version": "MTc1NjMyODA0NDc5MDE3MzAwMA", + "digest": "sha256:f55ccc4b736904afbc85db59e1c3e7a0b52b95b59eb44e69732f70635c89a9e7" + }, + { + "id": "6a1053d6-7cb7-5fb2-9533-7d8e99eaf3a1", + "version": "MTc1NjMyODAyNzM2NjcyMjAwMA", + "digest": "sha256:74ab101309ba288ad75a16dcf556e893c6ef93fa08943dd9282369df68c1783d" + }, + { + "id": "6a454a79-b89b-5674-844d-f4ec79e457b6", + "version": "MTc1NjMyODA5Njk2NTgzNjAwMA", + "digest": "sha256:773ae52ae2a29a9a6a48f7feb60838253de7a486989480892e185e66881b4fac" + }, + { + "id": "6a7e6214-7f68-5634-a759-8296afdb16c4", + "version": "MTc1NjMyODAyMTMwMDAxMDAwMA", + "digest": "sha256:b807d063e290504d274edab24ad2a4d853f9c92c346a52d57b104b9a0a16275e" + }, + { + "id": "6a93f7d1-d779-5641-9a98-324b1290c15e", + "version": "MTc1NjMyODEyMDQwNjg1ODAwMA", + "digest": "sha256:4ab4a09b56ac04196a5873807424bb3bce0b7aad6454b4bfc6b9b8d8adef1084" + }, + { + "id": "6a9ae05b-8c28-5add-b774-5e90819dab53", + "version": "MTc1NjMyODE0MTQ5MTU3MjAwMA", + "digest": "sha256:2094f49aa9d3694788a47598c47260a82aff4b2d52ffb9a282573a0cf8f5d9d4" + }, + { + "id": "6a9fea72-7dbd-582e-941d-7061980411ce", + "version": "MTc1NjMyODEwMDk3MjE2OTAwMA", + "digest": "sha256:ae2d9ae17228bdf7e256afd89b0d260b4e6416a9049a17ff31501407928c1a12" + }, + { + "id": "6aa51a5e-e502-5aa9-b77a-ba34604d3043", + "version": "MTc1NjMyODEzNzExODUzODAwMA", + "digest": "sha256:0ee79586f23749e808037ea0ca7e6f78e4733e2b0e02e32a0a08a60676f83510" + }, + { + "id": "6af7aca5-3909-5ade-b10f-1f4df045b0a7", + "version": "MTc1NjMyODEzNjE2Nzg4MzAwMA", + "digest": "sha256:bba4f79ab1cde3c20372c69cb30908fc7fa1307c9a74ae695e2c76737f5f8333" + }, + { + "id": "6b1fc280-b35a-5fb9-815b-c8ce76ea261f", + "version": "MTc1NjMyNzk4ODYzODUwMzAwMA", + "digest": "sha256:fc06a77f89af62f035258c44913e907ea0c7a09cd0cc38034844b990056203ce" + }, + { + "id": "6b42e471-3f7a-5f4b-b0b6-9327d8696dcb", + "version": "MTc1NjMyODE3MDQxMDc5NTAwMA", + "digest": "sha256:e8b67bcdd0a0c855ebd1e95bd1d2a515be986fb2cafdf9470a7e0b63fb650e92" + }, + { + "id": "6b45342f-3693-5422-9c81-a3780d52a067", + "version": "MTc1NjMyNzk4ODY3NjQyMzAwMA", + "digest": "sha256:dad0782dc1827e11d49e8c9ae9526559defaeb24f8793e9ac25ef66fea5ed919" + }, + { + "id": "6b45c1d6-4a7d-542a-b237-6ab545556cbe", + "version": "MTc1NjMyODAyNTc2NzkzNzAwMA", + "digest": "sha256:9cb9b23c39a0607e1e6b0dbc290e23d802530b3e01e923f0b0f929c78d360ea5" + }, + { + "id": "6b55b3d7-1b8b-55ce-b711-71c1fa57faca", + "version": "MTc1NjMyODAyNTg5ODAxNzAwMA", + "digest": "sha256:8483aafd36761864aeeb7d78798c17497f372905a5f1e240d216737011a022f8" + }, + { + "id": "6b590956-60b9-5313-981c-322c38e4d76b", + "version": "MTc1NjMyODAxMTkzNzEzMTAwMA", + "digest": "sha256:510d1cbaa64721219f97bb9669b2199ccc210e37c2c3f1389e8b3ca334dbf96d" + }, + { + "id": "6b5f5f90-081a-5b87-8a25-165b510ea111", + "version": "MTc1NjMyODA1MTE3MjA2NTAwMA", + "digest": "sha256:f2698f84cf1c8caad9c277d05144358a6b71c6faa188d4aaa85b33e964a57893" + }, + { + "id": "6b6b9af3-8b13-5356-a297-8d9b3d77d4b3", + "version": "MTc1NjMyODAzNTk0NDA1NzAwMA", + "digest": "sha256:e7f4ab5a12a2db47edeacab1b8c9c31e17e96473f837be3561bfc3a7cb7f675e" + }, + { + "id": "6b9cbb2e-831a-5460-8e7d-3283adbe57a9", + "version": "MTc1NjMyODEwMjE4OTg4MDAwMA", + "digest": "sha256:44a7b1944ed6718ec1444a0171ad34fdb754594d535d2c908779bf062cb5c13d" + }, + { + "id": "6b9f80f6-2245-5d11-a527-b93bb8381a9f", + "version": "MTc1NjMyNzk5NzE0NDkzNzAwMA", + "digest": "sha256:c044e32ae85abd1273aeb763fca19cd34a4e857b6528ed6453c282cfd5d30756" + }, + { + "id": "6bcb3189-087d-50cc-b044-6b3bcd3ad2cd", + "version": "MTc1NjMyODAzODgxMTk1OTAwMA", + "digest": "sha256:3b69e96f8b5c065458f6bafe217dc619486e59b94c1a1c04c54880c12735ea01" + }, + { + "id": "6bd127eb-6cfa-5d6a-b0f8-a37d1910d52b", + "version": "MTc1NjMyODA3MjU2ODY1MTAwMA", + "digest": "sha256:c290553de85d58cda8e4feacde17e445f741e5a52facb099d8c6b8af00c5aab9" + }, + { + "id": "6c0c93ed-ea88-5af5-8771-30f8aca25170", + "version": "MTc1NjMyNzk5NTI4OTg4NjAwMA", + "digest": "sha256:d7392a6ac33571e254820750bd364da355511b9126c9bb38bd456bd8aaf5fa12" + }, + { + "id": "6c3b2cd3-29c6-5881-bd0a-dd9ddf382330", + "version": "MTc1NjMyODE2NjgzMDQ2MjAwMA", + "digest": "sha256:fb9298cae02d4014e0ff2a1192f805c59b88ff3a2cb3f190072d5f7b27c188de" + }, + { + "id": "6c4e3827-9371-5029-af91-142af06e12af", + "version": "MTc1NjMyODA4Nzc5Njg3NTAwMA", + "digest": "sha256:7c20585a53bc3ad616c7d569c1b1ef602d38b6672976112ffeea901a5d4be2f8" + }, + { + "id": "6c61640c-5d6e-5b74-abfa-1c2e1c017a75", + "version": "MTc1NjMyODA1NTgxMDgwNDAwMA", + "digest": "sha256:65e632a15e51a36dfff3295672da45405c00414377497053de11db7b9834af89" + }, + { + "id": "6c61d708-6f11-5490-b0fd-e4b0514f85bc", + "version": "MTc1NjMyODEwMjI5NDI3ODAwMA", + "digest": "sha256:f1c7a0386af290d14c20e9e50ec047b3333c2323d3c838cef02fb0169a3f7554" + }, + { + "id": "6c71021d-4e1c-5393-8ec2-b9cee446d786", + "version": "MTc1NjMyNzk5MDQxNzQ1ODAwMA", + "digest": "sha256:38aa77228f6789a938f86ed56ad3a51842413b8b04893d5f9516ecf1e4877917" + }, + { + "id": "6ca53b62-1b1d-5dbc-8c93-c6f91718b6aa", + "version": "MTc1NjMyODEwMjQ4MTE0NzAwMA", + "digest": "sha256:cd7cea4029156360830ad2c7f66ff4429fdebc8ac85e93853904dd8df8425f7c" + }, + { + "id": "6cb0bcd5-77a3-50f6-b39e-459e8f8727d9", + "version": "MTc1NjMyNzk5NTI3MjkxNTAwMA", + "digest": "sha256:abc8a0d5d772bf5d2ed554f22112bb078919aafd6ee01d307710d3dd0fe8dc97" + }, + { + "id": "6cdab0ad-2e9e-5976-b535-c079a76937d9", + "version": "MTc1NjMyODA5NzAxNjQwNzAwMA", + "digest": "sha256:d95898fb6a5826c264be2139a78f769d873cd92c5cac2a5504ccf45bd6f3667d" + }, + { + "id": "6cf7f66f-8321-571f-910a-71c3c5696ab3", + "version": "MTc1NjMyODAyNzQxNDIxNzAwMA", + "digest": "sha256:062fde7948d90c3a2ea4065751e21264662862057921c5720a06a845f641dc17" + }, + { + "id": "6d333ae4-e21b-5212-af4b-b8fdfe015d32", + "version": "MTc1NjMyODAyMDU2MjM1NjAwMA", + "digest": "sha256:519f428ac13646a570b55455d9030ad8a40f14eec4118f1abfbded1c44185f4e" + }, + { + "id": "6d34112e-114d-5c2d-be49-c4007a34fcd8", + "version": "MTc1NjMyODAwNTE0MDA0ODAwMA", + "digest": "sha256:9237831e95a9a1d4d1a7e39a836843d2d75ef0986dbbe11417726a34e9222fca" + }, + { + "id": "6d3c642a-505e-5a60-8456-88b5ac140bc8", + "version": "MTc1NjMyODAzODgzNzE2MjAwMA", + "digest": "sha256:d9f38a53b5fe713f2b502051b5e091b5ac937cc52ad1b4722efea9e926c1a526" + }, + { + "id": "6d5e0052-1f6c-5f62-b736-95085a4c747c", + "version": "MTc1NjMyODA1MTIzNTQ4NDAwMA", + "digest": "sha256:d9bf7f395de292d56c0a4e73ab286d68f58b55c78b048f1ea2d0813f59271f7e" + }, + { + "id": "6d5fbcd8-03e0-5b86-b019-d8327373793d", + "version": "MTc1NjMyODAxMTE0Mjc2NDAwMA", + "digest": "sha256:9b4856680df064709766aa50ad330d5dccab06a18d49a1432b88d3ec6262b793" + }, + { + "id": "6d6d3686-50f1-5ab3-abac-b5368dd11136", + "version": "MTc1NjMyODAzODc5NjI3NDAwMA", + "digest": "sha256:92933d8ef41ed48b9c4cb255a73693efd996565386a281a1965542afc59124c5" + }, + { + "id": "6d80d859-2dd6-5a23-a254-c88be22b6613", + "version": "MTc1NjMyODAyNzg3MTU1NzAwMA", + "digest": "sha256:47516c9d5bd87fefaf0c98b39ed9eb266d30c8c9450751197ceda6e3ae18af1b" + }, + { + "id": "6d8804f0-5d36-5328-b773-06e84dcfda1d", + "version": "MTc1NjMyODAzMDc1NDEzMDAwMA", + "digest": "sha256:161f88be0eb72c874b78c348b4ad63d387c4dfe5663e23a37d38dfb640ac6761" + }, + { + "id": "6d8aa90f-e4a4-515e-8c09-8ba17683361a", + "version": "MTc1NjMyODEzNzA4NjgyNTAwMA", + "digest": "sha256:c75c36357a054a0ef2a1ac3a1410b06d3332171b17e4d96a3fa84bed7b504700" + }, + { + "id": "6dd4aad8-4521-5fa6-b220-2d7ca1f16d3e", + "version": "MTc1NjMyODE2MjMyMzIxODAwMA", + "digest": "sha256:02c428528c0cb9dded10c9b66fe269b9ed3cefd7c1e69982b7cdcb568007b0eb" + }, + { + "id": "6df05778-6449-5059-bc54-167e01f680db", + "version": "MTc1NjMyODE0MTQ4Mzg2MzAwMA", + "digest": "sha256:83a489307d817ba8d74d36d09ee49c108bce35ab6854656ee24f8757334ec83c" + }, + { + "id": "6df2c8aa-b914-5e21-8e42-444144e2911b", + "version": "MTc1NjMyODAyMjEyOTA2ODAwMA", + "digest": "sha256:51f841cffdd115f1a3204417f034b1985312be79e9878fd7a6811317b2bdf65c" + }, + { + "id": "6df5c7fe-d02d-5df6-b1ed-b5a58beed5f4", + "version": "MTc1NjMyODA3NDE5MzI0OTAwMA", + "digest": "sha256:d49ce007d65a3d113bd794cf1547a5d35d407ea2b86c4e3ba8d166589c5abc7b" + }, + { + "id": "6e705a82-29c9-5ac9-96ac-497e90da7123", + "version": "MTc1NjMyODA5NjkzMTg4MzAwMA", + "digest": "sha256:84e32deda6705cd90f2864441aee59034b420023a7d3e45138d764c04f051765" + }, + { + "id": "6e72c99d-ce7e-5e57-97f4-30c8a048f71f", + "version": "MTc1NjMyODA3NDM1OTc0NTAwMA", + "digest": "sha256:ba17ddb76200964e42d774816588904f06ff752f8c82d10e3ec697fdaac97598" + }, + { + "id": "6e9ed9f6-f002-5609-86ba-ff265c2ee2d0", + "version": "MTc1NjMyNzk5OTI2NTIzOTAwMA", + "digest": "sha256:8853d96f7d9be6cac5f1a2087ec4d34ee73d24e1677ce3dde223fa4750fe6fe8" + }, + { + "id": "6ea8c3f3-cf60-5781-93ae-ba6dc24d7107", + "version": "MTc1NjMyODE1OTExODYxMzAwMA", + "digest": "sha256:b1996548185e3c9cbd21af8aa6887780cb1fe40aff11f7ae1472f23b9683cb20" + }, + { + "id": "6ec33ac2-bf7d-5265-bd00-bf01dfdc405d", + "version": "MTc1NjMyODA3NDEyODA3NDAwMA", + "digest": "sha256:b19c90cbbc89c21740f5436c9297f9976a2b452873a5123d75454f0592ff6e78" + }, + { + "id": "6edf7164-cca7-5a7b-b0b6-004aac0e9d03", + "version": "MTc1NjMyODAxNTUyNTYyNTAwMA", + "digest": "sha256:222818b8257f8891b5e93cca96e2892bf9566a14ba8fa0b3f7ce3a3508be92a2" + }, + { + "id": "6ef9695b-90aa-544d-8498-d3f2643539f2", + "version": "MTc1NjMyODEzNzY5MDEzOTAwMA", + "digest": "sha256:496c6e077b46e694be4f48053ed918adb6a8826321e2d2a1cb3e6a6063abb08b" + }, + { + "id": "6f190d0d-9813-5bf7-9991-c5e86f7364bf", + "version": "MTc1NjMyODA3MzA3MDA5NzAwMA", + "digest": "sha256:5e0b22f63ee81b7d056dc90f801fda82c76feffa6d37b11ab71c3beeea5e3da8" + }, + { + "id": "6f40d4c8-5f12-595b-89ef-406a68a8c201", + "version": "MTc1NjMyODE1NDY0MDk0MjAwMA", + "digest": "sha256:16e491ddd1a375c2f568d456d13acf9d6e1afd8b5c8b751862027a3a1532a58d" + }, + { + "id": "6f456151-8ced-5206-a861-283da5d1dbcb", + "version": "MTc1NjMyODA3MjU2MjA2MjAwMA", + "digest": "sha256:7fa6e40a730764f279b96f0a02a6d84a37a6cab86e8ca948fbcfe609b024f349" + }, + { + "id": "6f67952a-9eec-5e64-b79c-14c3b302473e", + "version": "MTc1NjMyODEyMDQ2ODM1NTAwMA", + "digest": "sha256:1e8f527e913c3aeb0e5f5abcdb63f72d7cb97af7728a7471dfb56be3ec257495" + }, + { + "id": "6f7479a4-cf51-57c5-bfff-2737daa56467", + "version": "MTc1NjMyODA5NDM5OTM0MTAwMA", + "digest": "sha256:0dac7b7c38d15e9889c6969722c7a1b8e074535d74515c3cb8c2a9ddf82dc2c7" + }, + { + "id": "6f7e824a-8b6c-5b8d-aea4-07737a2ddfd1", + "version": "MTc1NjMyODA2MjU1OTYwMzAwMA", + "digest": "sha256:f6cfd955dcb6785a89b0b10ed8b9b41d9149df58dae673b6c4bbd4b4540a128a" + }, + { + "id": "6fbeef91-6e4f-54c6-aeca-793db50181eb", + "version": "MTc1NjMyODAzNDc2Mjg5OTAwMA", + "digest": "sha256:e6bac2e00357f38afe4723c5495017b65d03ac5204ba4727781bcf598b108825" + }, + { + "id": "6fc57a1f-3cbb-5350-8230-e9c0d41c81d1", + "version": "MTc1NjMyODAyNzMzODMwODAwMA", + "digest": "sha256:d118ed56900b6cf5f666428d0959b8830a38981a30f2617cd2fda61a742744b0" + }, + { + "id": "6fe9d228-b704-55ec-9118-95eaa10ffa34", + "version": "MTc1NjMyODAyMzM5ODI0MjAwMA", + "digest": "sha256:8f5b3be39dbfcc7caa41cc7fe6d64b1883a19e42b49e36a48033902190556da5" + }, + { + "id": "6ff781e3-fa0b-5851-b50e-10ce324fa398", + "version": "MTc1NjMyODA5ODcyOTg3NDAwMA", + "digest": "sha256:3c893d5068b156820b5466a00c45920e42de1ddadde0696a99f4680ff5648a72" + }, + { + "id": "7001da97-0934-5353-a2ef-abdd0fb0cd8b", + "version": "MTc1NjMyODAyNjc5ODc5NzAwMA", + "digest": "sha256:1e91dd3e55715bd57ebb2c12b0a4d328838e6609b678d15cfdf15130b72f7a54" + }, + { + "id": "70049d92-5a95-59b2-805f-cfe64643ff8b", + "version": "MTc1NjMyODAwNjE5NzY0NTAwMA", + "digest": "sha256:c9e03966a584f5a7c3ec300aa7a165506f0aa4152ca93af5eea5bd26cdc3cec3" + }, + { + "id": "7004b0dc-e9d1-5cc8-bb88-4ad6fda6d610", + "version": "MTc1NjMyODEzNzU2MzU2NDAwMA", + "digest": "sha256:9ed04201e88d28f4b6593d9e428c6547d30e8322dabddee5eba7c3190b380010" + }, + { + "id": "7007651e-d77e-57b8-8f62-b34935b33499", + "version": "MTc1NjMyODA5NDQ2ODk3ODAwMA", + "digest": "sha256:03fc84ce52df22ecddd6765508459c08d0636bda8a63131d237b1cec862e3d35" + }, + { + "id": "7009f35a-5f0e-5d09-b031-1fa20b21b170", + "version": "MTc1NjMyODE1NTIyMDEyMTAwMA", + "digest": "sha256:f4a3a6d710c341ecdce6718371dfad9613b6546847276462184505336f50df2c" + }, + { + "id": "704af471-aebf-5c2d-812e-541899deae90", + "version": "MTc1NjMyODE0NzYxNjkxNDAwMA", + "digest": "sha256:7751c580ef64ac5b8e6c1ddff620f6ef271282eccb7144dcdda785e3183cd181" + }, + { + "id": "706b60cd-628c-5d44-9d4e-7dbf0dfe6620", + "version": "MTc1NjMyODEyMDQ5MDkwNjAwMA", + "digest": "sha256:7bf7fa90cf2269f6362d889a86044013f1544bfc917c4aa3f97ee214c4297f95" + }, + { + "id": "70706e23-8664-5049-bd82-3ac8bea62ad4", + "version": "MTc1NjMyODEyMjMxMTg3NDAwMA", + "digest": "sha256:1c05b901cefa98e40261b09cda45fcf7601ebe97b6045b3162cf6dce02db7630" + }, + { + "id": "70931680-191e-5a1e-acc6-6ad88f29ad99", + "version": "MTc1NjMyODAxOTI2NDI2NjAwMA", + "digest": "sha256:ab85b90f21b991b1f20fd16a43b68002f7fc2639433143622147645966199433" + }, + { + "id": "709e87ed-bd2f-57e9-ad33-6e73b6d253ba", + "version": "MTc1NjMyODA5NDQyMTAxMDAwMA", + "digest": "sha256:cfbc655173d941d3d86a6e44486a392d3857d7cff9b74f698dc6f92330a4404b" + }, + { + "id": "70af5a3b-d91b-5d2a-ba0e-3a0b64a2e196", + "version": "MTc1NjMyODAzNTk2MzA1ODAwMA", + "digest": "sha256:62c7796fc5b47d790c9439d75f13fe176f392490846b8056fd1c7d5a985e68c9" + }, + { + "id": "70c6d423-e601-5334-b74c-d33b78c17fb2", + "version": "MTc1NjMyODAyNzQ3NjU5NTAwMA", + "digest": "sha256:5149621f313c758c360e7dec41e3a2f54de2da41cf16bec030716aab8b3f88cb" + }, + { + "id": "70ef6c25-6dc3-5fad-9769-9b479f5cba01", + "version": "MTc1NjMyODA2NjkyMTMzNjAwMA", + "digest": "sha256:3ecabdc1c909278544c1364735a42519ff66b475f8097ab4f11ca9d0aca229d9" + }, + { + "id": "70f84fac-94b4-52ca-b8cc-3e8f433a4cc5", + "version": "MTc1NjMyODA3MzAyMTIyODAwMA", + "digest": "sha256:e3cf7390615cb33f251e7c65dd234ff49f805f1a554df6e01ba6baf142280943" + }, + { + "id": "7133f8b5-8fde-5a15-be92-d6cd82ffa494", + "version": "MTc1NjMyODEwOTE5MTkzNjAwMA", + "digest": "sha256:5260dde44daea7ed77c21448ac85e26daa93a68602007ece8391f86072f2f5aa" + }, + { + "id": "713c6650-f0e9-5c42-8578-4de05de69c48", + "version": "MTc1NjMyODEwMjI5MjAxMDAwMA", + "digest": "sha256:b11b5d559ee1b6707508c1e39ec662af6017b5b1ecd4eb7738a1eaad2f199d2d" + }, + { + "id": "714bacdf-6dd9-5bbe-8842-7b6f93845b85", + "version": "MTc1NjMyODAzNjg0NDcyMTAwMA", + "digest": "sha256:d9a4e89cb7bd57525efa891281bb3cc2dc2ef2399b84091c6b9ec2dedcd8fbf7" + }, + { + "id": "7153356a-f86b-5398-bdc4-e5de6e842a40", + "version": "MTc1NjMyNzk5ODE4Njk4OTAwMA", + "digest": "sha256:e45c26c6a055c55f5033c18808a1816e29e8937eb72d436a4fb74fcc2095ec33" + }, + { + "id": "7157aabe-2f64-58fe-aa6c-6386c9697c26", + "version": "MTc1NjMyODAxMzc3MTQ5NDAwMA", + "digest": "sha256:a6eba581349157e32f8cc3c40c3b4915b7f03cec29cfdd3cf40298286cbed795" + }, + { + "id": "71778612-5bfe-5390-aa70-1ab205076b32", + "version": "MTc1NjMyODAzOTY2Nzk3MzAwMA", + "digest": "sha256:1ed5e342fd5bda559e73665258102cfcb02d9ea3e055b96c1b4978c4f7d2d4c2" + }, + { + "id": "717e8fd0-1128-5a19-b389-c83175b10ec9", + "version": "MTc1NjMyODA2MjA1MDIzNTAwMA", + "digest": "sha256:369335f26ef8ce4c5505bf739bd9421d2228caf12d23fc40375e8b63dc5531c1" + }, + { + "id": "7185a095-071d-5927-9184-d9f63f368231", + "version": "MTc1NjMyODAzODc3MzMzNTAwMA", + "digest": "sha256:f798795046bdcc38d93881fc602a74c384995bdb5b768ae4a7e4e59343383afc" + }, + { + "id": "71a1863b-4caa-5a59-b932-e044ed8ed99c", + "version": "MTc1NjMyODE2NjQyMjY0OTAwMA", + "digest": "sha256:34aaac393ba280b2b3d29fc01855bb94b419e9bcf3498a66acd36f1c509455ee" + }, + { + "id": "71b0af4f-c8b9-5b30-bd99-28b61c23d82d", + "version": "MTc1NjMyODA3NDMyNzAyNTAwMA", + "digest": "sha256:2fbbde9a759ee7bfe06c4dd6fb37f52b3d07e759b2ca977fb6a4479d10676aae" + }, + { + "id": "71cad6af-9c91-5c58-9a97-9afbee157d40", + "version": "MTc1NjMyODEyNjU3ODEyNTAwMA", + "digest": "sha256:37faa7c4955d5dcd2146a642969c2f5cf4913a2e050af3b84a12f9dcba8382c3" + }, + { + "id": "72199b39-ed60-540c-b306-854956bb8ee9", + "version": "MTc1NjMyODA4NzgxMTk0NzAwMA", + "digest": "sha256:69923144d52b25cbecbf3c4b93490abf52276915e45a78bea34b562867d697fb" + }, + { + "id": "72277772-a6bf-58dd-85e0-5c143aa09540", + "version": "MTc1NjMyODExMzUyOTUwNDAwMA", + "digest": "sha256:a6df3a897137cdeb11e4e23880591daa3e154bc9f5d882a4b20e298b0f9c4a22" + }, + { + "id": "7237888c-65b7-5b1e-8e13-e2d9e391d04e", + "version": "MTc1NjMyODE2ODk2NTE4NTAwMA", + "digest": "sha256:4c146b2e6c2ac8866d03e65e72afca2764af1e67052658b7b9291d2d45a65f4a" + }, + { + "id": "723975c2-3af3-516c-a043-e59613389096", + "version": "MTc1NjMyODEzMTE0NjgxMDAwMA", + "digest": "sha256:eae0f845c67ef662a7b2a84bf906805bdf6723808d789abc153b948adb089408" + }, + { + "id": "723a72b7-14a6-5c53-8a88-eba041cd3cba", + "version": "MTc1NjMyODEzNzY0MjEzNzAwMA", + "digest": "sha256:9a7e14539138b53490793c10b127cb6aa208b95179d6ae5420394ba5f9a44ccd" + }, + { + "id": "72464682-66ae-551d-bf2e-7a1205a2a228", + "version": "MTc1NjMyNzk5NzEyMzE4NjAwMA", + "digest": "sha256:141cd2d5f1339055070d04be8ce71423f7b6530525f9aefaca7ae43d33e6e013" + }, + { + "id": "72567021-a2e5-5640-9781-654257ecc460", + "version": "MTc1NjMyNzk5NjcyNjcxMTAwMA", + "digest": "sha256:bfc61f8a98acd2e9c8202cd7f9e0ba2d8a8933f2300cfbfd3ce74550219cef95" + }, + { + "id": "72586dcb-99cc-53d1-913e-33f1cd283afb", + "version": "MTc1NjMyODAxNTU0MTQwNDAwMA", + "digest": "sha256:f0403280717b5e3c936f974a8d98b33ab4d9771943c51156a6c2762227c06ac6" + }, + { + "id": "7265f609-571b-5dc4-9990-0300a136627b", + "version": "MTc1NjMyODA4OTI2NzA4NjAwMA", + "digest": "sha256:1bd14c5e7de30ee4b99da3fa728f48e125a5aa1089b366c9738e011c78e00dce" + }, + { + "id": "72740165-b852-5333-b41f-ef05410d1833", + "version": "MTc1NjMyODAxMTg5MTMwNTAwMA", + "digest": "sha256:1e2f18cfbebcb21f563340aa5dd164f09ffd9b402241dfebada3f44c66e7d673" + }, + { + "id": "7275c02e-c30a-54a4-8ed0-7d3397bc876d", + "version": "MTc1NjMyODA1MTIzNjg4MzAwMA", + "digest": "sha256:2b023fccbcc9eff3eff138ee170dbaf138046e2d7276d7e29035701484a5d2ac" + }, + { + "id": "72772a0b-2cab-5315-906f-e92527ef0c2b", + "version": "MTc1NjMyODExOTEzMzIwNDAwMA", + "digest": "sha256:f04474bd9ca0f0c5fe80f8486bd2a0a08cc8e729d10abf204b358ea36c3e43e9" + }, + { + "id": "727a7e3e-6f20-56cd-a74e-7e60372a2266", + "version": "MTc1NjMyODAzNDcwNjk1NjAwMA", + "digest": "sha256:7d54365b0e6aa1539e294b539a1791db02116f04b8f9ebc47862b8852696010b" + }, + { + "id": "72889c1b-860f-5304-bed4-51a58f4118a3", + "version": "MTc1NjMyODE1NDY1MTg3MzAwMA", + "digest": "sha256:1bc804610fc0f29c1e2e5996709db9c000ef19c73b1b01023b952aeeef0a4281" + }, + { + "id": "7290f472-0d36-516d-995a-0ec1217ac015", + "version": "MTc1NjMyNzk5OTM2MjgxMjAwMA", + "digest": "sha256:eadefe24661df43d0c1b78222ed144e167b5ea6fd0f5cea3cf3ecf82a2da1841" + }, + { + "id": "72910144-8457-5ce2-a8af-71a3ebc3285c", + "version": "MTc1NjMyODE2MjMzMTY4MTAwMA", + "digest": "sha256:3ce9570cd3935c8e0644ce2a94f2ef59c88155ea72ac196e767663c39db3ce96" + }, + { + "id": "729ea357-7fa8-55cd-8186-27461fd4f995", + "version": "MTc1NjMyNzk5NTM3NTQ3MzAwMA", + "digest": "sha256:a41e3936e0b2a872252f4f65a01d1b7cb7bb90eddafbd5bddd0e0205bd2c9b4f" + }, + { + "id": "729ed3e0-26c8-554b-9dd0-71b02057708d", + "version": "MTc1NjMyODA2NzA4Njc2NDAwMA", + "digest": "sha256:aa429082574ab0eab14d107b05cb903e1eb9dcded3512f63133eb4856bff3eda" + }, + { + "id": "72d3a1bb-f718-54d9-9031-36c494357e33", + "version": "MTc1NjMyODEyMjI4Mzk1NjAwMA", + "digest": "sha256:792748f424076394ef797cbc7a2c5b7ac78cd7fcf5b49417faa1d2e975932266" + }, + { + "id": "730a8097-508a-5635-a89a-09c6e586beb0", + "version": "MTc1NjMyODAxOTQyMzY4NDAwMA", + "digest": "sha256:4bb700d4cd3ed9188a3893731978d8be148d8e663e9639db9d2ef3e633b9f419" + }, + { + "id": "73121964-d71d-5e43-ac81-cfa1eb0e1751", + "version": "MTc1NjMyODE3MDQ2MzU0MzAwMA", + "digest": "sha256:12251a9af4a4fb187d7646ac896e4112a27253a6fd4cd9c71cf770ea7a4b75e7" + }, + { + "id": "732f80ea-ea0c-50ca-92b2-e31924afb3a4", + "version": "MTc1NjMyODAzMDgxNTA5MzAwMA", + "digest": "sha256:3889f58a0c3bfe6ae9950c3c38de10b8d108113a8ba2a48833c846c44b24e741" + }, + { + "id": "7382bcd6-43bb-52fe-a927-d54304d22366", + "version": "MTc1NjMyODA1MTM1OTQ2MzAwMA", + "digest": "sha256:bd13fb419e675fc058f5bb7709662fb1ef38c227a19cd5db778915052b7eee3a" + }, + { + "id": "73aa9c20-d501-5a60-9c4d-d2da7d221bc6", + "version": "MTc1NjMyODEyMjIxOTc2NDAwMA", + "digest": "sha256:aa55a74e98a1b41b7744ebe472fa4ad22be8489e42f31a280c0aac1840ae78e3" + }, + { + "id": "73ac88ae-819d-510e-b314-5e36529b5ad1", + "version": "MTc1NjMyODAzNDcyMjQ4ODAwMA", + "digest": "sha256:6c2b9a8fc69b847540d0559fefe68845917504dd28d19ebcb8c2e3822f8134e6" + }, + { + "id": "73b92ec4-fade-5a37-801f-30e61dcaac76", + "version": "MTc1NjMyODAyNTc2OTgxNDAwMA", + "digest": "sha256:1a0ebfef15367c75d97ae40b9c1eda1553c28aec9824d560853b60e6a36deae6" + }, + { + "id": "73bc1841-1cb8-5083-95c1-ad69be60f373", + "version": "MTc1NjMyODA3MzE5Mzg3NzAwMA", + "digest": "sha256:8195f1153035dc2db1d9d74652fc40fc13d40aafb7365e7ed310f4224ea4ec29" + }, + { + "id": "73d9f222-d672-5368-96c4-c144f5adc9c5", + "version": "MTc1NjMyNzk4ODcyMDYyMjAwMA", + "digest": "sha256:ab0bedb5f7e0b23bcc6066b7635d9580568be33275fd76b3879129e4911ed061" + }, + { + "id": "73eea05c-8438-5fa8-85df-22f07d291f78", + "version": "MTc1NjMyODE2MDQzNTM4MjAwMA", + "digest": "sha256:aded76f6dbbefd2f28d114834d6bac91d429b3be68e5b315d479d82ec5987666" + }, + { + "id": "73f70552-5c09-5a92-afe5-1622b9419ee3", + "version": "MTc1NjMyODAzODY4MzQ5NjAwMA", + "digest": "sha256:4104c60cdd6d24492f9ce2b80321e3617d0b0c97b1cb96767c8b48733ce4ba79" + }, + { + "id": "73ffe891-5b0c-5f07-a01b-a8d55dd50785", + "version": "MTc1NjMyODEwOTI2MzAwMzAwMA", + "digest": "sha256:47912cb624d4c468570797ba76e51ce2d526dcf5af6dd250439b155e2a0caee0" + }, + { + "id": "74013ba8-45ad-57b6-94a9-b4cfa9b15aa1", + "version": "MTc1NjMyODA3NDMzOTg3NzAwMA", + "digest": "sha256:2501cd1da746d2b0d472bf2b5c7aa7089f8625cf5052f783293cd19ba10227bd" + }, + { + "id": "740679d6-aefb-586c-b2b5-74788189764e", + "version": "MTc1NjMyODA4Nzg0MjQ1MTAwMA", + "digest": "sha256:c1cf538c112b9abd95e758c0e2e8dbe6a35aeac6fc8d079a67058d633bfdcddc" + }, + { + "id": "742d12aa-8d66-5fb0-8668-4402976d01ad", + "version": "MTc1NjMyODAwNjE4MzA1ODAwMA", + "digest": "sha256:18441c6a54c75c7308e84afe7662419d7be05fb637ce1442abe986a365fb8232" + }, + { + "id": "743d82ff-853f-5887-9a8d-8679efaad00d", + "version": "MTc1NjMyODA5Nzg2NTk3NzAwMA", + "digest": "sha256:e3b04f097bed94bfe7d08208bc6b3a7d2a955098d826994c114700f81cbd6ebd" + }, + { + "id": "7444ae88-f965-5070-8c46-067620a9db29", + "version": "MTc1NjMyODA3MzEwNjA3MjAwMA", + "digest": "sha256:626ce08f92004a03f405169d0442ee6537a04ee74c218b03c785e0d8d4670470" + }, + { + "id": "744e6e2f-785f-5cc2-82e2-29b0c178a067", + "version": "MTc1NjMyODAxMTkwNTg3NzAwMA", + "digest": "sha256:a3a4964813d4a51b929a619aaf6205f73eef014f14f0eba69cce38fb48ba5ec7" + }, + { + "id": "746b7fa9-9f19-51fe-805c-d3e9ee8bccdf", + "version": "MTc1NjMyODE0MTY0MzE3MTAwMA", + "digest": "sha256:70f7a33851584d416a743e8fca143cab93da7686bb2f0df9ab52bc102af47ae5" + }, + { + "id": "746c1b4b-c4f3-59a5-bd6a-ee97e6a9ae56", + "version": "MTc1NjMyODAzMDc3NDc0MDAwMA", + "digest": "sha256:b1ef4535e594f6f45313b2f287f059b23b2ba51c736597cf96832b1333b23ea4" + }, + { + "id": "7473a239-e5a7-5c16-8e01-156ba5a3f3ee", + "version": "MTc1NjMyODE0MTYyMDc2NTAwMA", + "digest": "sha256:4bbffe4805f452834485dda82eae16abe2424f2c97725f24e1e61f02c96da4b1" + }, + { + "id": "748072af-a62f-5bf1-a90a-0ba5d9f46992", + "version": "MTc1NjMyODA3MzI0NzE1NzAwMA", + "digest": "sha256:60514eca358b10e27d1d39aeac6c4cf665dd4fc332175795fa85122031d20b03" + }, + { + "id": "74976c1a-0aee-54ca-aece-0fa9b0d22732", + "version": "MTc1NjMyODA1MjYyNjIxNzAwMA", + "digest": "sha256:8f59db6cb0df8c5605f1bc40324aacc19ee92a8ceda6f24c000e5f58adcf6115" + }, + { + "id": "74ab5e7f-30ce-5fc0-a8ae-ae8a16f24ff8", + "version": "MTc1NjMyODAxNTQ3OTYyNTAwMA", + "digest": "sha256:91a164459f3df75bb8377935e9a1a88e5289f8412927bfbac69e04b16be79156" + }, + { + "id": "74b21669-7b86-5e9d-afd1-2db8df4fb15c", + "version": "MTc1NjMyODEwNTMxMjUxMjAwMA", + "digest": "sha256:fa0df35c8dd869e4a5ab62252f370988d2f098d133d25c17a4f8cf5b527eafce" + }, + { + "id": "74cb9058-783f-5e1f-b389-cc3db6d0c218", + "version": "MTc1NjMyODExOTc5MDY5NTAwMA", + "digest": "sha256:342ae094ce4a40748ff11f5c9854af3f3fae8bb4566634d5a2ed579511734449" + }, + { + "id": "74d1294b-626d-556d-8fac-821d20210da4", + "version": "MTc1NjMyODAyMjE5MzU1OTAwMA", + "digest": "sha256:fca615e4545145b46976f7b6173df648b4536fa4b5f3165a4fc33804b5a948a6" + }, + { + "id": "74f8884a-cafe-5dc1-acbd-cff0398ab1f7", + "version": "MTc1NjMyODEzNzAyMjcxNTAwMA", + "digest": "sha256:71cc009a6c8a829b6c4647ceba28e9ade90c81064fe50e7e8419b3a93dece195" + }, + { + "id": "750e9041-12e0-54aa-aa17-64db4b330ef5", + "version": "MTc1NjMyODE1NjM1MDAwMDAwMA", + "digest": "sha256:c7b03c478c90835350360e5f7f7ab5c76fd2554dd56d20be2fe72bbaa2f54d64" + }, + { + "id": "750f5219-bc2e-5de5-97bd-87862939825f", + "version": "MTc1NjMyODAxMDQzNDQ4MzAwMA", + "digest": "sha256:80b36858aab84581e069fab6f5506e16561a35f5d546d302de3d282adb8d3154" + }, + { + "id": "7511833f-4d0f-5f64-bd71-819795439154", + "version": "MTc1NjMyODEzNzQ1MDA0MTAwMA", + "digest": "sha256:695309dbe92e914bcec4ce44733ac332e61a66bd3befb666c0a79d404c6ad916" + }, + { + "id": "752ec64f-c6c7-56b7-a5b7-9d219d0669c0", + "version": "MTc1NjMyODAyMzQzNDg2MTAwMA", + "digest": "sha256:ccdd3fcbb8cc7318928871990ba166867e4e48c10fd37999cf324db593b37013" + }, + { + "id": "753040e7-67f1-5feb-8b93-ff350f2ddf7b", + "version": "MTc1NjMyODE1NTE4MjcyMzAwMA", + "digest": "sha256:bbe7d3ed2b16148d595f57318cd66a317d0bc1a7e1965a544b9bc112734f4ab0" + }, + { + "id": "755a9dbd-2c02-5e31-9167-919ba3f205e7", + "version": "MTc1NjMyODAxOTI5MzUyMDAwMA", + "digest": "sha256:d92d29080276d2c667b9951e199199f891a1c79ec96a3c4c20c9a34aaeb0dab6" + }, + { + "id": "75834e3e-4246-5587-8be5-c861728bf4c5", + "version": "MTc1NjMyODAxMDUyOTE2NjAwMA", + "digest": "sha256:21f6efdeaddddbb843e6010d1b980681bb45fb3dd0dfe9f68adc0ee91d1c06a5" + }, + { + "id": "759341e1-0941-507b-9173-cd5ea2220a5c", + "version": "MTc1NjMyODAyMjE4NzI1MDAwMA", + "digest": "sha256:ebc0239abdcf9f6c04f9e1e14c41d3177291c19d67b7b71142deab50739aefd0" + }, + { + "id": "75976cf5-c287-5ab6-ae0f-18e24770dd73", + "version": "MTc1NjMyODAxOTM0MDA0NTAwMA", + "digest": "sha256:ecc1f7416b9784d9193e87837a343f39a25df7602b9e3af222fd6bef073cc54c" + }, + { + "id": "759f90a3-8553-58fc-8504-f687a8da672c", + "version": "MTc1NjMyODE2ODkxODE1MjAwMA", + "digest": "sha256:aeac4c324018de2bdd805371897392ed687a5c1cfcb06f0ab0d76b3cd0c74dff" + }, + { + "id": "75a2a733-4b9f-5fdd-8e75-a82f0e944348", + "version": "MTc1NjMyODAxMzgzMDA0MzAwMA", + "digest": "sha256:34613c5ec7934117e66226e8603f4c28fe36dcd4894a8bc88b3e70bb0f36201a" + }, + { + "id": "75a5598d-9133-5651-8cff-bf037b1c998c", + "version": "MTc1NjMyODE1NDYwMzg2OTAwMA", + "digest": "sha256:5d52bc700db2d96c382577929c188a4b07128839b9249707184cd52cf9ca6a19" + }, + { + "id": "75e53040-8126-55aa-a871-15177e0958d9", + "version": "MTc1NjMyODAxMTY0NDc3NjAwMA", + "digest": "sha256:120fcd9d2aceb2c4c7f959873b3488c1b370357de952df42a46117881bcbad16" + }, + { + "id": "75f05dc5-13b6-5522-8f8f-e91a3a7f2092", + "version": "MTc1NjMyODA3MzE4MTQ0NTAwMA", + "digest": "sha256:5e8b526bc8dc42986af7502a57ea8b86d1e1c8df5c0a5c4bcad2c4f69e31d560" + }, + { + "id": "75fbbf20-a86a-5366-afe9-560b49cf0f6c", + "version": "MTc1NjMyODA1NTcxNTc0NDAwMA", + "digest": "sha256:a59942cbd60aefabcafa6b376a5992a690436c8413feaefc9a753675fb95eab0" + }, + { + "id": "760403ca-5bfe-5d20-b2f5-b5eb7ca1ab0c", + "version": "MTc1NjMyNzk5NTY2NTc2NjAwMA", + "digest": "sha256:55fcce36792774b6b74dac868e5bee401f7b751d29c9fcb1308d5078a7c4977c" + }, + { + "id": "760dd2d4-627d-5cfc-a3c6-fac929c57516", + "version": "MTc1NjMyODAwOTA4MTk1NDAwMA", + "digest": "sha256:4b43879d275597c4c8ce6cebeb716c79a0f41efe654a2ff083a797ccdae4c00d" + }, + { + "id": "7630a0e8-5865-5045-962d-a8138d2b3b7f", + "version": "MTc1NjMyODA1MTI5Njc3NTAwMA", + "digest": "sha256:7ff82fdaefb1b61c19aec73ea6fcb17904e0498d5d1fc5bd1bac2af6e3b75493" + }, + { + "id": "7630b4fa-e22c-5a30-b624-408571f5b577", + "version": "MTc1NjMyODAxNTI5Mzk2NDAwMA", + "digest": "sha256:91d93f088ac0c0fa486a166091144f4131fcf50dfa822d74e5e9053ab73dd991" + }, + { + "id": "766efdde-0502-56d1-95ea-bcbdc7ab6df3", + "version": "MTc1NjMyODAxNTIxNDgxOTAwMA", + "digest": "sha256:6fafa7f05c0459cc79bb11c401c0fc159141cd337a34ac7abfe56f7987cec440" + }, + { + "id": "76829555-6316-527b-bae1-f5ec9c93a9fd", + "version": "MTc1NjMyODAwOTA0ODEyODAwMA", + "digest": "sha256:a5f85b14feffe2ab44f219b768c5330a473a4f7a5bcbcc1e8b3dc96edd2be8fa" + }, + { + "id": "7691ecf5-1af5-5344-9c71-b6a233544fad", + "version": "MTc1NjMyODAxMTg3MjMxNDAwMA", + "digest": "sha256:6d894cdffee36374aa08dac27d714d0e0e2596774f4584bc52adbfe967d6326c" + }, + { + "id": "76953a9c-6414-5d2f-a61f-c5e143a29f05", + "version": "MTc1NjMyODExOTI3MDg1OTAwMA", + "digest": "sha256:a86584d96b050c2fca659450407199a15b027abb19478208d87ea3d39b01f8dd" + }, + { + "id": "76b5b3b9-c679-546b-9036-d66247a9cf63", + "version": "MTc1NjMyODAyNTc2NDE5NDAwMA", + "digest": "sha256:03d1a462265613ca389f6f501f01c832bd97db66a0e9e4a92287107426e8bfee" + }, + { + "id": "76b964da-d3f6-5c0e-879c-e46537aa13a5", + "version": "MTc1NjMyODEyNDQyODg1MDAwMA", + "digest": "sha256:380c70b6887368f7d82a98ec43dd854ee69c9c380f91685e008fea7415cc6a35" + }, + { + "id": "76c0978f-b626-5481-b40a-5fa6288cb1ff", + "version": "MTc1NjMyODE3NDg1MDk2OTAwMA", + "digest": "sha256:7b8d002db524766fdad579b55077949ac7992221cf864a632b18f07f5df15dba" + }, + { + "id": "76f7d7bc-9203-5e9e-bfa5-daab599b0582", + "version": "MTc1NjMyODA4NzYzMTY3NzAwMA", + "digest": "sha256:01993ee0534ec1b03110c5d3abae7444de4ec34b8e5181110ded15e135a05af0" + }, + { + "id": "770f455a-d571-5bb9-8d6e-f5d17a8a6bbd", + "version": "MTc1NjMyODA3NDE5NjQ0OTAwMA", + "digest": "sha256:d660d3b68d419cc8a468e57a9188bc4e0d19ecccf769826792e2b495873b9e82" + }, + { + "id": "77218da2-34e9-5eb6-9644-6f02e2aaa486", + "version": "MTc1NjMyODAzNjg1MzMyNjAwMA", + "digest": "sha256:848e38566f1792867f201eb5a1e2ed1c434d8e90999a8f07b72ec9ea05d82ff1" + }, + { + "id": "77285173-01f5-5b70-a728-fac4254f453d", + "version": "MTc1NjMyODA3Mjk5MzA2OTAwMA", + "digest": "sha256:e063fb341e295052dd877af334c15baed324965850019ae6bddc2b3256986a58" + }, + { + "id": "773714db-6b15-5075-834e-a27949e84f9c", + "version": "MTc1NjMyODA1NDE5Njk0NDAwMA", + "digest": "sha256:0a2c1ebf362b3dd1c7fa0e0dc82605d5c72a81d8b59b7952926e5e605df063fe" + }, + { + "id": "773da1a8-b1b5-5ab2-b798-237170a73aae", + "version": "MTc1NjMyODExMzU3NTYzNzAwMA", + "digest": "sha256:77da1d2496341322cab08fd2f645e6a746a4795f371230f6061a1dace98e3e0d" + }, + { + "id": "776def7d-d97a-5b8f-a957-def86524b01b", + "version": "MTc1NjMyODE2MDYwNDU2MzAwMA", + "digest": "sha256:b07591d359746b3fcb38d8d73a3fdbc933110a34f1773569fecde3c3399f4720" + }, + { + "id": "777237ac-9a98-50ec-a5af-46fdce1bc31c", + "version": "MTc1NjMyODAyNzg3NzYyNDAwMA", + "digest": "sha256:c5646a34fa5d4d55e29d69bbf0ae53920e8875f43af966bae9cfdf477dbabdcb" + }, + { + "id": "7782d3c7-1598-54a7-af35-034fd8dae532", + "version": "MTc1NjMyODAzOTcxMDc0MTAwMA", + "digest": "sha256:f26ba988a6ea84672437c4aed46d137a86f5455660e356b3dc0b419e597d5460" + }, + { + "id": "779ad982-7923-56fc-bc80-501c22e805ac", + "version": "MTc1NjMyODAxNjU3NDg3MjAwMA", + "digest": "sha256:07933fb1430eb85acdcbca51dfc24f2ec5a7ef36362be34e44c52233055e07e7" + }, + { + "id": "77a34f38-c488-59e1-be95-d431bf64319a", + "version": "MTc1NjMyODAyNTU4NTE4NDAwMA", + "digest": "sha256:0b40a6117edf3c3edeb01d8b799ae60833197fb02373952b1087f3d29f6c4539" + }, + { + "id": "77e81bc3-ee07-5522-b465-b2c24bc8261a", + "version": "MTc1NjMyODEwMDk4NzMzMzAwMA", + "digest": "sha256:95661ec5bbd25d50613ac1c2f382d5cd2f60e0381accfe6f8cbc1468cda54c6c" + }, + { + "id": "77eb45ff-c8b7-5581-b35c-7f5c649b77c3", + "version": "MTc1NjMyODEwNTQyMzY0MzAwMA", + "digest": "sha256:ccef18ee31bdc22b130b3c0b9d788577d76f99bf20cd88f38d9773135397077d" + }, + { + "id": "77fad939-75a2-5f13-bded-605e7e5523c3", + "version": "MTc1NjMyODAzMDgxMTAwMDAwMA", + "digest": "sha256:ab704718cc5010670fe6e8536cff9c49c02853616abf5e2788d4860e70290beb" + }, + { + "id": "77fc570b-2882-5f84-99f6-06c55b251c2c", + "version": "MTc1NjMyODAzOTczODI5MDAwMA", + "digest": "sha256:ad8268b5c7844cafaf4103088ec371d216a2f15c9ebd57697d692099caf7bb4c" + }, + { + "id": "783754a5-9637-59be-a2f2-b3a3b3c29bd6", + "version": "MTc1NjMyODAxMDQyNTkxMzAwMA", + "digest": "sha256:1bc711cd44f98e75439e5e86e80548b56d1af64e1f1224879fd0a1a1fa410f80" + }, + { + "id": "783cfdf7-18d8-5721-8896-798f665a99b5", + "version": "MTc1NjMyODEwNTM0ODUwMjAwMA", + "digest": "sha256:a3e93121b9dd2e6a95e212756ce6a7c0e3e5bb531f6ee9ff9f17bd2c432f287f" + }, + { + "id": "7859b69e-f6af-52ea-8a21-3cdaee7c7bf9", + "version": "MTc1NjMyODEzNzAyMDk5ODAwMA", + "digest": "sha256:9304576c5c305fa740220397d0f01c74dcf95d8fabc8335dbe62f2dfb807ce9c" + }, + { + "id": "78617160-5139-5ba4-9128-830102f6e05d", + "version": "MTc1NjMyODAzODY4NzgwNTAwMA", + "digest": "sha256:783a5e1e5fadb5dfa666ce6ebcf56b355369a201ba79c798b4db07e2cade4077" + }, + { + "id": "78674309-cb32-5a29-bbda-6dfb3b3dd86a", + "version": "MTc1NjMyODA1MjUyMTkyMjAwMA", + "digest": "sha256:f47c7bb2a3d0b6548b5ec67aae9fe5d4c05ca4fa50d9829b9e679b1936fa4d8d" + }, + { + "id": "786be687-7088-530d-977e-c5160d7d9a85", + "version": "MTc1NjMyODE0MTU1NTgwNDAwMA", + "digest": "sha256:84358739af68301f9bf93ee472ad09a49d76b92ee3e3b1d920f22b44d8979616" + }, + { + "id": "7880b962-31cf-5aa9-8fe3-72212fe52618", + "version": "MTc1NjMyODAyNzc2MzM4NjAwMA", + "digest": "sha256:30bc8ab190da197d03dd17d258941614deb1d6831e505f1942002962d9329456" + }, + { + "id": "789df885-44da-51c7-b284-0005c6c7f0ab", + "version": "MTc1NjMyODAwNjE4NDY1NjAwMA", + "digest": "sha256:769b28839e248027311726cac2ab05184576b986975bba31a481840d212aef63" + }, + { + "id": "78bcca1c-9867-5628-8463-7045141a5a20", + "version": "MTc1NjMyODE2NzcyMTI2OTAwMA", + "digest": "sha256:ba02fd3bf32daf46e79e14c182d06b13933db61715e12d4ca53d88623ad34ef3" + }, + { + "id": "78c2d858-be39-5f0b-8b90-9da85e4e5f77", + "version": "MTc1NjMyODE2MDUyMTY3NTAwMA", + "digest": "sha256:e8e5cd9bf8cebdd95b7ca761e2e952a35d69e0c8455b31289a46dd56e24ea971" + }, + { + "id": "78fe0bbc-396c-5226-a2c3-10649103f9e6", + "version": "MTc1NjMyODAyMjExMzk5NDAwMA", + "digest": "sha256:93f12df9d74445ec589cb1a3f240ff36ffbc3673713a49400c9168166645ccb2" + }, + { + "id": "791698b7-a17a-5daa-8d55-53f9589044f1", + "version": "MTc1NjMyODE3MjQ0NTYwOTAwMA", + "digest": "sha256:c203f143a35ec8ac8102956c714263591110b200f99979879be9b12c2c9d9bea" + }, + { + "id": "79278e81-dc2d-51eb-a494-60d9daa4f4fc", + "version": "MTc1NjMyODA5ODYxMDQ5MzAwMA", + "digest": "sha256:5e08a10ed61f550cfde986dbb13dec181fe24c832f701cf03a125ebdfb845140" + }, + { + "id": "79611f65-48d3-52a9-a1ff-0edd4f6b5967", + "version": "MTc1NjMyODAxNTUzNjA0OTAwMA", + "digest": "sha256:edf37372ffd1c2abc05c4f5536f5a963611dcae14bea28e48e838698fac15c4d" + }, + { + "id": "797debd9-052f-5e41-83d3-837e1e3ed3c5", + "version": "MTc1NjMyODA2NzEzMTY2MjAwMA", + "digest": "sha256:36548b58bdc7277c5611dbe364e93a26fb2fcd345be2965499285982d19c471b" + }, + { + "id": "797e3630-245b-521b-b337-f38532927c21", + "version": "MTc1NjMyODAxMDQ4Nzg0MzAwMA", + "digest": "sha256:e7f660cce5bc2a430e0bedc1e78f35ec9c0006b9339522b21768504e7cb81d22" + }, + { + "id": "798c5ad1-40ce-52c5-ba48-89d55c900765", + "version": "MTc1NjMyODAyNjgzMTQ1NDAwMA", + "digest": "sha256:b494d1b0527000a3a00f1df21da72006636fd340828c305ce85026f253604aec" + }, + { + "id": "798d791e-5f6d-50b3-b07c-b16a2fe826d2", + "version": "MTc1NjMyODAxMTAzNTY1MjAwMA", + "digest": "sha256:af5fa232df1187501a90407c90ef0823676757a2780f19202c5c848af01f680a" + }, + { + "id": "799d1ffc-f8f3-5ad7-94e6-601c8beb87a3", + "version": "MTc1NjMyODAzODg2ODUyNjAwMA", + "digest": "sha256:227266b2f04f13ecbee1b4eceec3cc81fe28621321792c161c91042a789470d5" + }, + { + "id": "79b98741-876d-5e79-bfa5-674b7a064bb6", + "version": "MTc1NjMyODAzNTk0NTI5NzAwMA", + "digest": "sha256:9787457a4e2bd627d2c94fc43c0e6b3606df076a9896295ca93ef16f8114c119" + }, + { + "id": "79e840ff-cf2b-572a-becd-f140674bca36", + "version": "MTc1NjMyODAxMDQzODM5NDAwMA", + "digest": "sha256:f5c67538f5f5b12d3ca8442064c38b73bcff688c9c872626fc181c84998d55e5" + }, + { + "id": "79f75aa2-091a-5c59-bb97-f61426e62838", + "version": "MTc1NjMyODAxMTYwMjY3NzAwMA", + "digest": "sha256:f183ca45fdfc4fc262b59f3526ab2318dd66dd4f54a67d8f336b87fbf0d0f423" + }, + { + "id": "7a056d5d-38ff-5ec1-9667-950dbd3eb3b3", + "version": "MTc1NjMyODA0MjQ2NzI4NjAwMA", + "digest": "sha256:f6cedaca1c06efbd1062a6aefd0ea783a886e07f51649cc92aeb7dd5c2b4f0e2" + }, + { + "id": "7a3d2659-9bd2-5feb-9f2a-6e03bc3dcaa2", + "version": "MTc1NjMyODEzODIzMzg4MjAwMA", + "digest": "sha256:db69cdf53fcefb1fe3bd42e7fcac72391373d4d67260dda57047172c60d08616" + }, + { + "id": "7a467265-b769-5add-9a4a-91cd1700572a", + "version": "MTc1NjMyODAxMTE0MTUxMDAwMA", + "digest": "sha256:db62ab29368b507d97324a6171d741cc58d0eab226cafb59d61e40f642c741eb" + }, + { + "id": "7a4f4600-661c-52ee-b13a-5200bf16b6ba", + "version": "MTc1NjMyODE3MjQzMjIwNjAwMA", + "digest": "sha256:cf36d3bab8a56459343d9e44dc2b0fc723f583d6d538dc4f267492e5cde45f2e" + }, + { + "id": "7a620e0a-8b43-5d15-be84-d2499edcdb7d", + "version": "MTc1NjMyODAzOTY2MzE1NjAwMA", + "digest": "sha256:68fa775ad092163fff47bc9609dedbbcadc0b3df23db62c589c082522d4940ae" + }, + { + "id": "7a7076a0-10b5-50dd-9942-eda452ca8711", + "version": "MTc1NjMyODAxMDQzMzM3MDAwMA", + "digest": "sha256:d24fdc00c22bbb4d609ec5675f7b309646552420d8103f21308154f0267a46ea" + }, + { + "id": "7a971ade-9075-5523-b54c-4fce52abb5a0", + "version": "MTc1NjMyODA5Njk4MzQxNTAwMA", + "digest": "sha256:d4c57d071b3d04bdcddba47a495205426fbf1bd1ad43558c2e3ccb00c712dae3" + }, + { + "id": "7a9c48bf-36b6-56e5-b677-0a2a24b2c49a", + "version": "MTc1NjMyODA4OTI3MTA4MjAwMA", + "digest": "sha256:cae82e0053ec5c885df307eff3502c46db0046af27ac43f4c20addb8f29b6ad6" + }, + { + "id": "7aa0fae1-52d4-5ae4-8542-3ca54db16c41", + "version": "MTc1NjMyODA0NTc0MDAyMjAwMA", + "digest": "sha256:947d1763a20f57a29a06914a1575f6640ebf4a10a7ccef4fc101241c42cd745a" + }, + { + "id": "7ad2c92d-e9b6-56f2-a64a-77a6c3a3215b", + "version": "MTc1NjMyODAxMzg5MzY3ODAwMA", + "digest": "sha256:b8e0338a5b7c4d4cf1ce87b3116361ff44dc6e6c66a4bbc0198580dfc87995b8" + }, + { + "id": "7ae29903-4e94-5419-bd5a-08b8956bcb79", + "version": "MTc1NjMyODAzODgzMTU4NDAwMA", + "digest": "sha256:1a84b4fd83e714400142344e753d624bf38b80bc40e32d29d9f891a128147a93" + }, + { + "id": "7b0e3885-b390-5885-a19e-dbc769f0170b", + "version": "MTc1NjMyODA2MjEwNDQ1NjAwMA", + "digest": "sha256:f1c1b9981bbe3cba9b560cc367142824ec0415b06d9fe6857c8fb786ee211aad" + }, + { + "id": "7b3050e8-6d00-5a8c-90c1-b4cd2a9a304d", + "version": "MTc1NjMyODExOTgyNTU5NDAwMA", + "digest": "sha256:fc440549c406b7aac4f8f533f6f3124aeef132c9e5f5d3f33f7822a3f7fe3387" + }, + { + "id": "7b6279fd-9dab-5bd5-8979-697e65cc12f4", + "version": "MTc1NjMyODEzOTM0NDYxMTAwMA", + "digest": "sha256:3fe34bc3cc9b8febd83dca480ce38863e6bfe78e53298fa50e8a9b6aa1f4c4f9" + }, + { + "id": "7b7d8218-4a0f-52ab-affa-da0e48924238", + "version": "MTc1NjMyODA2NzA1Mjk4ODAwMA", + "digest": "sha256:9e70756361ff69e8c100b301e6ab92786e114ae0b8571b0cf93a6df319de44f4" + }, + { + "id": "7ba8e2d4-2cc8-583c-b928-c05e3e661132", + "version": "MTc1NjMyODEyNDQ5NTM0ODAwMA", + "digest": "sha256:6fe59701c29fdbbca711ad612e9f341c6c22fe3106cf974e81b7671475c4b4e8" + }, + { + "id": "7bab2ea2-73fc-51f6-962c-a38515cf2e9a", + "version": "MTc1NjMyODAxODgzMTEyNzAwMA", + "digest": "sha256:97c4309721c6686f0b2c8ffbb1ff1e732d778bdd339edcc7338bb9d5b7c17437" + }, + { + "id": "7bbe1c01-f0c1-5f79-aecd-3f49c1ae167c", + "version": "MTc1NjMyODE2MDQwMzcyMDAwMA", + "digest": "sha256:f48f6870dec44edea5600322afdd5f3f98ffaf05e42a040a698167e1c76f73ab" + }, + { + "id": "7bc9067f-d5f1-55ee-8b0d-549c1ef8d4f6", + "version": "MTc1NjMyODAyNTU3MTIxMTAwMA", + "digest": "sha256:2570ba014a920eac0bb93874d986a96c34462bb8697ecb5a88486d39befab36c" + }, + { + "id": "7c136fd3-e8ac-58a0-8d00-a1c14ba64ebc", + "version": "MTc1NjMyODA5NzY3MDcwMTAwMA", + "digest": "sha256:e17223b07a5045ddc02e3ecf19438c9ad43bc9db5c672dceb2cd96c365b92bca" + }, + { + "id": "7c2bf0c3-c782-5d91-bf40-9c5c35579bc2", + "version": "MTc1NjMyODE0MTU5ODU4MzAwMA", + "digest": "sha256:0a3c09be43942ad2e19deac115d023487ac8146e0d2125715402f734eba9cdd3" + }, + { + "id": "7c39efee-6220-5e80-9733-adfd7e792ff7", + "version": "MTc1NjMyODA0MjAwMjYwMDAwMA", + "digest": "sha256:c6cfd38211109aea6101645f572ca3aa7fc42371dba6e463f1339f5c6ce81419" + }, + { + "id": "7c447c98-5f77-5091-91a6-c200988786ee", + "version": "MTc1NjMyODEzNzQ1ODExNTAwMA", + "digest": "sha256:f67c303defa08a4bd9b61ac267c2fc16f86712b0ef343476fcc91c45fd769a6b" + }, + { + "id": "7c977cc1-11f2-5c5c-a5c0-fab579afb7b5", + "version": "MTc1NjMyODE2MzMyNjQwNzAwMA", + "digest": "sha256:4ab8e8a476f46a89cff914848dd0fbac19c8e6fb94640148cf0363135cce0f73" + }, + { + "id": "7cc9b388-e823-597a-abc0-7fc5b848f8a6", + "version": "MTc1NjMyODE0NzUyNjYyNjAwMA", + "digest": "sha256:5aa85118c571a59bf2f898fd095cdf1a076685e097a68980bb27372c277262e4" + }, + { + "id": "7ce5be67-3f0d-55d6-aa9d-656e3f19138a", + "version": "MTc1NjMyODExMzQ4MjcwMjAwMA", + "digest": "sha256:4f3451e0008a3275fdec7447d6bfb3f7ec43895057c3653d9dfa053c4c4391e2" + }, + { + "id": "7d0fa3bd-5d87-5739-8f9a-e24dceba56b1", + "version": "MTc1NjMyODAxMTA3ODA1MTAwMA", + "digest": "sha256:12499b17c5b5bd2eef26b3e2b14b85ed88dd7b8cf86c0e49b24d45539b0fb541" + }, + { + "id": "7d53ceeb-31d7-50cf-b9c6-192cd15ad212", + "version": "MTc1NjMyODEwNTM4MTAxODAwMA", + "digest": "sha256:c8d0a38ad883ce99abc553f3b7dbe72e4e67b17255261c23dba9af4045e69282" + }, + { + "id": "7da5e15b-1318-5f50-99c5-d32afe97f8eb", + "version": "MTc1NjMyODAxMTkzMzc4NjAwMA", + "digest": "sha256:f3529fe94e6419fee5ec89ce14283eddd1780a409570e8e556bd48e2be5a6c7e" + }, + { + "id": "7da7a892-6209-5577-9b3f-01feb0b71839", + "version": "MTc1NjMyODEzNzQwNjY4MjAwMA", + "digest": "sha256:765c6dd1e001c3b5e93e515d6de93eecf73d98e846b0a5aa52654e19397bbc5a" + }, + { + "id": "7dcb77db-d39d-51c6-ba41-3ee3442a88f8", + "version": "MTc1NjMyODAyMDQ4MTg4NjAwMA", + "digest": "sha256:347af4ec6669e0bd2aa64f93e14e02289ce414c5fb06734741f6f139c40d17b1" + }, + { + "id": "7def3039-d813-5c9d-ab3c-50f2bc632281", + "version": "MTc1NjMyODExMzUxODMzODAwMA", + "digest": "sha256:38b49c102e840673ce79815b3ddba7a712515d449a0facf093e6af93b7f69bb9" + }, + { + "id": "7df90787-659a-5a6d-bbfc-263940810a26", + "version": "MTc1NjMyODAxMDQ3NTE4ODAwMA", + "digest": "sha256:044f98f8d7dbcb4807116191dd5fb04af2916197c6b3898e84d723d729cba3e0" + }, + { + "id": "7e0237c0-0c56-536c-bbfd-a4867af19fdb", + "version": "MTc1NjMyNzk5ODE1MDczNjAwMA", + "digest": "sha256:e9c84a43b3ea5917216d760456a6eb1699e82626dee70bfccb7d8d0947b852e8" + }, + { + "id": "7e098c67-8d08-5f87-8e77-bc3420186f8b", + "version": "MTc1NjMyODAxOTM3NDc2NTAwMA", + "digest": "sha256:66f0c613ded8542e7fb82ef2669e6a8e4643dc7092b3a71b90e0907b526bec77" + }, + { + "id": "7e1f0293-0859-56d7-aea7-094b8ea70f93", + "version": "MTc1NjMyODA2Mjc0ODAxNDAwMA", + "digest": "sha256:091ca4134bcccc5d6a90a3f69290cb8def248158a4c2b90fa8db5501f9d0c261" + }, + { + "id": "7e255a28-61a9-56b2-bd6e-d797a85f5912", + "version": "MTc1NjMyODEwNTQwODIzMzAwMA", + "digest": "sha256:0695087340cc7941bed0f1578042e1735fbd0fb7fcebf863704166d3a7cf8bcd" + }, + { + "id": "7e4e047f-2ec0-51a7-9fb6-21257a791397", + "version": "MTc1NjMyODAyMDU2OTAwOTAwMA", + "digest": "sha256:74ab88fb835de5c507e2a49c5f2fcb94231f58fd222f760bc3c49d643dbf7579" + }, + { + "id": "7e54ccae-d75f-5f04-a3d4-35c324e20d69", + "version": "MTc1NjMyODAyMzQ2MjU1ODAwMA", + "digest": "sha256:174531141725fb4b0ca59fa023b72bb02b5f0bbca60d24b461869ecc47c7ece8" + }, + { + "id": "7e57f30b-c334-50c3-8269-a153b7c765f6", + "version": "MTc1NjMyODAxOTQzOTg5NzAwMA", + "digest": "sha256:d43a843f1528fb21c81d53ee47e0675e166abcfb66d64aa0947d00ffa802cd67" + }, + { + "id": "7e6e61b5-333a-5a20-9664-e12aa319a5d3", + "version": "MTc1NjMyODAxNTE5ODAxMDAwMA", + "digest": "sha256:8cc7cefafb198ca169d92bf0d6b13739106d28560ba52e569e9aaee7db4af0d2" + }, + { + "id": "7e8b0c00-2961-5cfb-8a3a-90ce08f7db28", + "version": "MTc1NjMyODAxNTIxODE3MTAwMA", + "digest": "sha256:42b3959b95a6a4910d5ac7decc464a3e0d2a0d2fcc20817ac897b71d47e4f508" + }, + { + "id": "7eac3255-f1ab-5179-a43f-82d31e2c0282", + "version": "MTc1NjMyODAxMDQ2NTEzODAwMA", + "digest": "sha256:bcb02b059cd7821a7738c210d18409f584fb40491ee766e7fe4552b6f2fc7a42" + }, + { + "id": "7ec10640-ad09-5f1a-b7b6-5ac0198d3cb5", + "version": "MTc1NjMyODE3MDQzMzA0MTAwMA", + "digest": "sha256:b565aaaad057ced894603c5687435d53d8866439d38cbf97b3a41c369fd87fac" + }, + { + "id": "7eebfe28-a784-5233-89cf-82d381e18a08", + "version": "MTc1NjMyODEwMjQ4MTU2MDAwMA", + "digest": "sha256:543fb4298b7ca751e1e0f7991baf20cf8d07f0b615373eb3110b38d0ec0869a5" + }, + { + "id": "7f046227-876a-5183-a6ef-cfaba9fb45e7", + "version": "MTc1NjMyODEzNzU0NTQ5NjAwMA", + "digest": "sha256:b133fab626242c430d873bd0317efd41b6cce85a5cc9d6ee9f98785727a46fa2" + }, + { + "id": "7f0ad24d-ae0d-5e77-92b9-9b2eb1c3188d", + "version": "MTc1NjMyODA0NDcwNTA0NTAwMA", + "digest": "sha256:83a1001c874273881d81ac8812d2630c567096bd82e20d7b9c2656d447abf118" + }, + { + "id": "7f19eb3b-96c6-5450-9de1-6e3347dc5558", + "version": "MTc1NjMyODEzNjE1MzY1NDAwMA", + "digest": "sha256:0680780a783cf2d05086708966943ed32243378456d61383ee5442559d7fc9e8" + }, + { + "id": "7f288077-a3df-5839-9b03-1da2446566da", + "version": "MTc1NjMyODAzOTY2MDU2NDAwMA", + "digest": "sha256:37992b5044fab3a7ffb5e818d82870bd33c68e38496ec0fc0aac2a1d8f06e20c" + }, + { + "id": "7f336091-21da-5e57-ad44-ce3540e868f8", + "version": "MTc1NjMyODAzNTkxNzY2NzAwMA", + "digest": "sha256:8249455f9f65beb2bb2bf26b19c7458245d2b8ab62739eae7b38f31ad0c20761" + }, + { + "id": "7f3c9cf8-f18f-50e1-beb5-d4ab416af014", + "version": "MTc1NjMyODAwNjIyMDgwMzAwMA", + "digest": "sha256:4ab227a2703091e4435e03be51c21915f5f2ae58c01c825df147a720b699a31d" + }, + { + "id": "7f4254b6-c790-571a-bb63-9daf44124607", + "version": "MTc1NjMyODE2MjIyMzgzMDAwMA", + "digest": "sha256:abfaf06fe4ab9a217c2f47c862026596c0088cdd841bf53fcf6c2cc89ef02168" + }, + { + "id": "7f447001-a05b-5979-80c0-0c0a5e13a71f", + "version": "MTc1NjMyODAyMTMxNDU4NjAwMA", + "digest": "sha256:b6c2b73ce3210f31f02b4af253feb382d656d63a950c2a4b2e9e3ed291cf323c" + }, + { + "id": "7f4b8376-b569-53cc-847c-a11f093f9f09", + "version": "MTc1NjMyODAyNTg5OTE5MTAwMA", + "digest": "sha256:58001f1489d0df721875b11c8c55f13b26cb43e0d4c2e0becdec729afe5c72ad" + }, + { + "id": "7fd7b6da-3e78-52fa-a166-5775c90358c6", + "version": "MTc1NjMyODE1NjQ3NTczNDAwMA", + "digest": "sha256:ee1c59ad8d101d5ec6d2a22a2f7205fc258a198cac748662301b2c31b900be17" + }, + { + "id": "7fe1d659-13d8-52b4-bf69-18ae4654e1f3", + "version": "MTc1NjMyNzk5MDM4NzM5MzAwMA", + "digest": "sha256:c931d539ab1b1f66db678d24c9722489c58ee4ea11f23d000bca38c2fd11f463" + }, + { + "id": "7fe7d3fd-0fb2-546a-b673-eba373cde5e6", + "version": "MTc1NjMyODAxODg1MjMzNzAwMA", + "digest": "sha256:224bb68dc70eff0ff37590c24e73df8a3cb7764cc9c771e858ec0a3bbf9c8ef7" + }, + { + "id": "8007e556-2196-5e06-8a86-f5787fba965a", + "version": "MTc1NjMyODE3MDQ0MjI1NDAwMA", + "digest": "sha256:30aa13a8b87e94e9ec95c4f9a261017a8c93ba44ec6cd34e70154ff818a2a21b" + }, + { + "id": "801dd9f9-362e-54d7-a9da-58fc64c93d11", + "version": "MTc1NjMyODE0Nzc3MTg3NTAwMA", + "digest": "sha256:6580bc3940ba4305d74bd741289e7b3d13a46724698dd07856125aa518a573e8" + }, + { + "id": "8021ff40-71e0-596b-a1cc-9a31ed987be6", + "version": "MTc1NjMyNzk5OTM5NTc5MzAwMA", + "digest": "sha256:a8b0e0f799d2e6c97f7246fc70b09fe0a8bd262998f72aede1c0aaa54d23f8b6" + }, + { + "id": "804f08ee-2601-5bbe-8f7b-8a1b0f8cee14", + "version": "MTc1NjMyODEzNjI0NDE0MTAwMA", + "digest": "sha256:fc908c34e2993ef105cbc74fed7781aef54a5bdd96b39a69326e2e99ce39efd9" + }, + { + "id": "806a39c5-a9a5-535a-a3b7-7cf9719124c8", + "version": "MTc1NjMyODE3MjUyMjU2MDAwMA", + "digest": "sha256:45802ba4fd56f18df80efe21995d5af467b2da2e39293ba5af828edb96a71120" + }, + { + "id": "80890ba5-9bf7-5e37-a99f-86169f0789f0", + "version": "MTc1NjMyODEzNjA1MTg5OTAwMA", + "digest": "sha256:97038822007cde315a85dba1b8995d0afad28a3398ca4c96880f093718d2a1c8" + }, + { + "id": "808d686d-4050-54b5-850e-4f09960fbc76", + "version": "MTc1NjMyODA0MTg5OTU2ODAwMA", + "digest": "sha256:7e16332b316bf346d0dfd99ee73520617c9c8b8ae1403782000204a342e8695b" + }, + { + "id": "80a7e732-1317-5655-86b3-dbad1618d60a", + "version": "MTc1NjMyODE2Njg4MzEyNzAwMA", + "digest": "sha256:e4045a286e9b074f7f246de69ae001c10583333b29c3b769b8815328c0bd0c08" + }, + { + "id": "80b812ac-a80f-5272-8025-998b969a552e", + "version": "MTc1NjMyODEzNzQwNDc1NDAwMA", + "digest": "sha256:95a70b8759eaacec66b80bc476e77e9d5382e978674f19dc323f3376277c440f" + }, + { + "id": "80c672d9-7a66-5a76-b049-42249fdd1c97", + "version": "MTc1NjMyODEwMTEwMzY0NDAwMA", + "digest": "sha256:d18d232382889228bb6e453528b97d4f80db53f7d401039d6d2b3b50eeb0cabc" + }, + { + "id": "80cced16-0eee-5a23-a4f9-122283ef7565", + "version": "MTc1NjMyODA0MTk0NzU4OTAwMA", + "digest": "sha256:46fde437a22a4eb4e2997fada969fe5ffa0465ac817092bab92eb5585e86fd4d" + }, + { + "id": "80f46b74-8239-57ff-8bb7-69afab535b00", + "version": "MTc1NjMyODAxMTExNjU0ODAwMA", + "digest": "sha256:0cb9db45ef87799d62a941b040b63709448f8e535b091634cae31aeac84a9aa4" + }, + { + "id": "80fb24d1-2039-538e-8f81-e5d70c7a1775", + "version": "MTc1NjMyODAxMDQyMzUzNzAwMA", + "digest": "sha256:57b251a7179850e6967b4be6ef12ce6b6102b0b3876e513641a47c5422174548" + }, + { + "id": "812aa502-e40b-5aab-bd4f-4f3419e4071d", + "version": "MTc1NjMyODA0NTcyOTczNzAwMA", + "digest": "sha256:dcab7cd993b33702a368b5f86088d6abd00f5eb71400371d0af89ed4bd68c671" + }, + { + "id": "8133fb75-3d01-5c81-a6ef-f3267bc94b24", + "version": "MTc1NjMyODAwOTA4ODIxNTAwMA", + "digest": "sha256:33b67b6e6b772f29b7cf863b4bb33fe1d6e45253c5027a3fd2ef032bcb4bea1e" + }, + { + "id": "816277c1-ab9f-5750-8d08-fc39817b33d1", + "version": "MTc1NjMyODEyMDM5NTM4MTAwMA", + "digest": "sha256:139a7db93130ab86ad51d54bc7aa65aa8744d1043ded3a17f583c18889ecf1ce" + }, + { + "id": "816815db-c211-57e4-b93f-fd3d6904bf37", + "version": "MTc1NjMyODAzODY4MDYyNDAwMA", + "digest": "sha256:1bcaf27113bb26e5f43efa21aafd70471ab5f4b80d316844c685d33430e38d57" + }, + { + "id": "817e66b0-21ce-57a8-ac4a-c0e78a3d7f8d", + "version": "MTc1NjMyODA4OTMyOTk4MDAwMA", + "digest": "sha256:306451ecf9f5ab12ca3685126a372952e83bcb794097dff2de7fb4faa485b414" + }, + { + "id": "818f00a3-4a05-5bc3-81f5-8cc5a77b6ff8", + "version": "MTc1NjMyODAwMzk3MzIwMTAwMA", + "digest": "sha256:36cc5a445f825b50df5d8e8c912d0fafd3e3bb56f0db56b315f1b8ec01780ffe" + }, + { + "id": "819bc824-2f92-554b-a11c-803160342a00", + "version": "MTc1NjMyODEyMDUxMTgwMTAwMA", + "digest": "sha256:154f1327bc7d0e948fecbdf768ab69c5e8ed18ff82dec97f75a9f0898f084dec" + }, + { + "id": "81d57961-5ec4-5916-9ae5-d9d6d94558ce", + "version": "MTc1NjMyODA1MjQ1ODU0ODAwMA", + "digest": "sha256:d293883c61276ced6bd1f8731e378e701465082a276c2223903be3831df852c7" + }, + { + "id": "820a6470-459e-55c9-a97d-98a783dad737", + "version": "MTc1NjMyODE1NTA5NjcxNjAwMA", + "digest": "sha256:e5b29686628b823b068020fcf6e0998e96d7fb39e979b0be0a7849514aa76e9d" + }, + { + "id": "821b8d72-7cb0-556a-b146-830b3e7d4cfd", + "version": "MTc1NjMyODA1MTM0NDM2MTAwMA", + "digest": "sha256:d03c28cc39ea9edf1d4ff70082e4acdce222f93cdcf2d58820b09e299419bc38" + }, + { + "id": "821be8fa-e272-5003-bda0-a703e13baee5", + "version": "MTc1NjMyODAxNjUzMDc2MDAwMA", + "digest": "sha256:fa55118744aa2dd57a9cbf400f623aacc2558ac6c5cb816df6ca50d3b6292f5b" + }, + { + "id": "8220f2b4-be2b-5bea-ba76-48478c2152dc", + "version": "MTc1NjMyODA3MzEzMTAzNDAwMA", + "digest": "sha256:edbb71691a2c434c9551fdd3656fae59e80246cf71bc8ee51ac01d19ff00045b" + }, + { + "id": "8233fae9-7c28-5990-b4e8-9202712c2ecb", + "version": "MTc1NjMyODEyMDQ1NjcxMjAwMA", + "digest": "sha256:4c3e0314688e86b42ddfeca1139b3d2f7bab14abbc56705d717ae3221a4c7d88" + }, + { + "id": "82365056-a1b9-5e56-af33-5b092442d638", + "version": "MTc1NjMyODAyMzQxODA3NDAwMA", + "digest": "sha256:d7d0f1d1678ae3da72b332a5353917a819c09d64f9ca11a2ba820e0bf078cfd2" + }, + { + "id": "8241238f-caa8-5af2-b3c3-32810795d0e0", + "version": "MTc1NjMyODAyMTI4NTY1MDAwMA", + "digest": "sha256:9bfc7fc8c2907690b750405cbd079b5ec49c17ecffed1374dbb4d2fe0ae161ce" + }, + { + "id": "825203ad-8d2b-56bb-8744-c5843d1c14e1", + "version": "MTc1NjMyODEzMTE0NjkzMzAwMA", + "digest": "sha256:3a6c00588b90cfc61cca06739a378e0d36a44a76d8ed41695d7aec6a7d370b6a" + }, + { + "id": "8252613b-1212-5dcb-9555-cb6311888aa5", + "version": "MTc1NjMyODEyNDQ3NTMxNzAwMA", + "digest": "sha256:b96081b49930d51aee1a560f599530fb3118d35a6b16cb8a84caa5976e667a88" + }, + { + "id": "825b9374-30c9-5c06-bb77-144e37c2c3d6", + "version": "MTc1NjMyODAxNjUzMTEyOTAwMA", + "digest": "sha256:2805f5d5fa21da2dfb7c339621e33a7ffdd96867273ad40d0a2770f12a0cd4cf" + }, + { + "id": "82708dbd-a34d-5a0e-b50f-d598f24d74d2", + "version": "MTc1NjMyODA1MTI3NDUwODAwMA", + "digest": "sha256:67d389f80b8ca55185adcd4ccea773414a9f96fcf090496a4a99b3b11294db85" + }, + { + "id": "828f9596-71a7-5910-9cb7-8f8193ba4761", + "version": "MTc1NjMyODAwMzkzMTM0MjAwMA", + "digest": "sha256:d758ecd7b62beff3e145329229b5f49c0887e25ff750127d57bcd40cddc74deb" + }, + { + "id": "8290864e-acd6-59ed-8d0e-4441c9904961", + "version": "MTc1NjMyODAzNTk5ODA1NzAwMA", + "digest": "sha256:c8884c54c950870ca22ebe7a4f416577a49899bf4a7b56b5b644ad082273930f" + }, + { + "id": "82943e06-0bca-5b8c-95ba-01639ead2941", + "version": "MTc1NjMyODAyNTkwOTkwODAwMA", + "digest": "sha256:465b74e4f15fd96f8248d7710776ebe8237704d5a8a09c7543d554f6b13d448d" + }, + { + "id": "82a48adf-bd6a-5160-8901-7b3a0f2ba44f", + "version": "MTc1NjMyODAxNTUyMDQ4ODAwMA", + "digest": "sha256:30e338695e81c77f2636f0b5af8f57f3304ba680cf2669c2204d266967a15a0a" + }, + { + "id": "82c6c7ef-2bef-5c46-926e-c72b9a4e4e8e", + "version": "MTc1NjMyODAzOTc0NDMyMjAwMA", + "digest": "sha256:5e3dee13c21af26b9df5feeaa4317d7fe4938c7f1be31feb8ccef0a9002278ee" + }, + { + "id": "82d1a84b-8032-5597-8860-3133f585e5de", + "version": "MTc1NjMyODEzODIxNDMxMDAwMA", + "digest": "sha256:dc9f55b501432e0eeecb096f8060778171a72393328f2578bf1e6615b601efee" + }, + { + "id": "82e12ffb-c49a-5e6f-b396-dad469acab17", + "version": "MTc1NjMyODAxOTI5NzI3MzAwMA", + "digest": "sha256:06a3c02ac6654c8decf53daa2034ab36e6a93e74a4b16b1a8aacf9c18174f563" + }, + { + "id": "83106f8e-20d4-5186-8ecf-d55a431fbc3b", + "version": "MTc1NjMyODA5ODY4MDgwMDAwMA", + "digest": "sha256:636ade580f9330b8ec774626c338590b2c65ba1d77d76952a24764236d161d51" + }, + { + "id": "8311b5c5-164e-595b-ae03-6affa0c1f434", + "version": "MTc1NjMyODAxMzc5OTI4NjAwMA", + "digest": "sha256:ae11154a478b3b77032dcfdb5d7f2368adf6122bae96f1676d03670c43d2beed" + }, + { + "id": "83135125-ed9c-5e05-930a-01763a0c122b", + "version": "MTc1NjMyODE0Nzg1NDY3ODAwMA", + "digest": "sha256:f74458adf33aa9933044af83b39ca40a2a8645f1c04994c7927a5e022946730a" + }, + { + "id": "83403b58-4b76-59b5-8374-a1105b6d82cc", + "version": "MTc1NjMyODE2Njg0NDI4MjAwMA", + "digest": "sha256:82b21e5a55ba03b0b81ddd376338058580900716427bec82a386d696d02ae6c8" + }, + { + "id": "83670ae1-2b7b-5bae-9cb1-ea2eb05ecee8", + "version": "MTc1NjMyODEwNTQwMzcyNTAwMA", + "digest": "sha256:16414bd791c391ba8427a2f2760231b1e9b7878d7925e38411aa3f58b167e377" + }, + { + "id": "8378172f-42a7-5bca-ac79-45965c23a28c", + "version": "MTc1NjMyODAzMDc5OTM2MjAwMA", + "digest": "sha256:21d3b300f08a7bafb086662210f1a2e81fc9b1c851ebe1e58769a6f90e72fcb0" + }, + { + "id": "837ae598-5da2-5bed-8cb8-cd2a9df672ba", + "version": "MTc1NjMyODA4ODUzNzY4MDAwMA", + "digest": "sha256:f751658b35906a10b6d0fe38bf19aad7db0c97d2566602fab797dbaf4990bcab" + }, + { + "id": "837cf7fe-7288-5aed-9f7c-633d4dc97601", + "version": "MTc1NjMyODEzNjIxNzk4MjAwMA", + "digest": "sha256:d5fbb3bf500eebc231ba5e483cfab30f9e9d9f05c3b8d5d8208e6a17de9d89a4" + }, + { + "id": "837d50eb-9562-56c7-a3c5-6b162ce8324e", + "version": "MTc1NjMyNzk5NzA3NjMzMjAwMA", + "digest": "sha256:93a977d82a1faefbc3426a9b2310bf2ff8bade063393395b039f252fe3fa24be" + }, + { + "id": "8387b405-c4d4-5d08-8be6-18b52dc645d0", + "version": "MTc1NjMyODAzODY4Mzk5MjAwMA", + "digest": "sha256:4bd4e4a2c9f7471590a66e5ad0666609d6586aa543251cc07c8fbe5d2a245991" + }, + { + "id": "839500d0-0fb5-522c-a7e5-6e4fdaf10082", + "version": "MTc1NjMyODA5NzY5NDQ2MTAwMA", + "digest": "sha256:0207123185999f3dd2d949465175802507e4a5b57a3d67ec2c16d48ef1d3c916" + }, + { + "id": "83b92075-be44-5e00-9e47-da8edf5e9783", + "version": "MTc1NjMyODA4ODQ3NzQyMjAwMA", + "digest": "sha256:b9c5a060a2855a58c641c0ac8e7f8fdf0cd376d2e5073c4af0e3a564a05a11b3" + }, + { + "id": "83cdfdf2-4b3c-5e9c-97d5-e1953862737a", + "version": "MTc1NjMyODAyNTg5MDM5NTAwMA", + "digest": "sha256:9085a6eea59ef6380624ddad285329946cee403dd37a8c549d287a6beba57316" + }, + { + "id": "83e30388-ad8e-5b5a-b773-a69e631a7026", + "version": "MTc1NjMyODAzNzc5OTYxODAwMA", + "digest": "sha256:4b6ee7f76cc50c2fe17a845fdf03480b9cb5fd9f30306baacdba5d39ed7f2100" + }, + { + "id": "83e791d9-c9cf-5477-91d6-59e2acae083f", + "version": "MTc1NjMyNzk5NTY1MzAxNDAwMA", + "digest": "sha256:dc29871e1230bf56683928484da529e9926dbc5625e88aa8c1dc2a8a188a23be" + }, + { + "id": "83f8e7cd-e680-5e95-a259-2c6da7d3319c", + "version": "MTc1NjMyODA1MjQ1OTIyMDAwMA", + "digest": "sha256:ef92b60ef553163a85779740f1c20ef294904283d05bc4f8c222c4330fe43f35" + }, + { + "id": "8440ab99-cb36-5475-bdb0-f7426eb3921a", + "version": "MTc1NjMyODE2MzQwODk4NTAwMA", + "digest": "sha256:1012c51ccfbdd82cc9c742cc76fac2851f17f3720f6df57649aa6752c72d7542" + }, + { + "id": "8470813b-8570-51e7-a8f2-4901f21b02d3", + "version": "MTc1NjMyNzk5NzEyNjAyNjAwMA", + "digest": "sha256:a20741177a430a54d179df6d547e0c239e5e5bb58e7b2ec7b9d782ac7862f340" + }, + { + "id": "847bca51-4251-54d9-953c-85053360e5d8", + "version": "MTc1NjMyODAxOTI4Njc2ODAwMA", + "digest": "sha256:ab214ac75ee5f4afc93bb093f4578a88bfacdeffae66bbb8666cb4d29874b35e" + }, + { + "id": "8497dbc0-f76d-5405-9be7-03e5f4bea077", + "version": "MTc1NjMyODE1NTE5Nzk0NjAwMA", + "digest": "sha256:edb5c867d69cbf87bff4b55870b01a9f0b5e9d621cf2bb7f90cebea86e1f2a24" + }, + { + "id": "84acb502-f592-5f16-89ce-e3d2f05d3ec2", + "version": "MTc1NjMyODAxMzgxMjE1MzAwMA", + "digest": "sha256:8cb4175693c26ff832866e16a0fefa13fb570ad79e77bd28fcda71ce7e72d279" + }, + { + "id": "84b5a37b-7fd1-55f0-8c8f-86ae46c7eecb", + "version": "MTc1NjMyODExOTE3ODY0ODAwMA", + "digest": "sha256:4373cea51c0fa84e230215d6e0d2faf4face7e94492c27b76c32d001144b8cbd" + }, + { + "id": "84b8c683-a903-54c1-9e65-dc8b7469ea6a", + "version": "MTc1NjMyODE1NDYwNzgxMzAwMA", + "digest": "sha256:d5bd102e2314b7637e5f4495427ff02aac4da1ec8128010b7cceae485451992d" + }, + { + "id": "84d0ba76-df71-5617-9d27-204aab29f9f9", + "version": "MTc1NjMyODAxMTkxNzE1NTAwMA", + "digest": "sha256:945d2170f530d3659166d8346eba6ec007e1569f45fd0c9283701f9e7cba77d6" + }, + { + "id": "84d4bcf8-485b-542b-8fdb-ca9a0bba57e9", + "version": "MTc1NjMyODEwMDk4MzIyNjAwMA", + "digest": "sha256:a733d63ca37bc6cf3cf4cac2fe3f9bb10fd0bfb10ec771d8faaebd704ec2208b" + }, + { + "id": "84e5c1de-4fbe-570f-b587-d294745d1ea8", + "version": "MTc1NjMyODAyNjgwMTE3MjAwMA", + "digest": "sha256:0f9eaeaf431698ccb3d08dc63a0fc4be163b21934ab0322f5f8fd01ef85b9e4b" + }, + { + "id": "84f42989-8bd4-5de1-ae7e-3beb28be60c2", + "version": "MTc1NjMyODAxMTYzNzQwNTAwMA", + "digest": "sha256:9ec9f8cd661ae6f8b85f38b1c8073bc6486ea578d39000b3941186096f0017e4" + }, + { + "id": "84ff31e6-f4f5-56fd-8c34-c8e019c4d91a", + "version": "MTc1NjMyNzk4ODY4MDUwMDAwMA", + "digest": "sha256:beebac580d67691ac6538897023aebbff21c70ccdcb40fa182f2f46898f2cc5d" + }, + { + "id": "8503dbe2-afe3-5e48-976e-7e462b64e3b8", + "version": "MTc1NjMyODAzNTk4NzAxMDAwMA", + "digest": "sha256:50c349e7f8b312a03fe719ee5cb1a91bb7f8b2f331511849448830e066043475" + }, + { + "id": "8524e815-f73c-5231-9370-803087392ee8", + "version": "MTc1NjMyODAxNTIyOTQ3OTAwMA", + "digest": "sha256:4b846aff6e00a4ff875c8c7888eed0cfedc85db94eff63686856e93cc3e6ce40" + }, + { + "id": "852a25ef-0df8-5e48-9fff-a06916a90e3a", + "version": "MTc1NjMyODA0NTcwMDE3MzAwMA", + "digest": "sha256:e589614694e4a861c061c5a3bb039c76a2bc657a2cce9be0cdf12c2e7b56e05a" + }, + { + "id": "85417098-b452-569f-b0e8-bb1abb9de0d0", + "version": "MTc1NjMyODExOTExNzM2MDAwMA", + "digest": "sha256:0e74970265ebd21e271ae4b6a69196027641952089adf3cd5ec7fd8891f3a5dd" + }, + { + "id": "854b02ff-0cf7-5099-8a86-69d9d611e29d", + "version": "MTc1NjMyODAzODc2NzM3MzAwMA", + "digest": "sha256:dadb6bff2aa41e0d4ffb70f7bb8ed7fe6d56cca5ae6f673f839c8df867186a19" + }, + { + "id": "854ba89b-99d7-58f1-8e6f-8ac4aacf442d", + "version": "MTc1NjMyODE1NjM2OTY0NjAwMA", + "digest": "sha256:d365567df3060f44c076dc288d2cf2ece1470ac1b4dc0e3f84d510697cad789f" + }, + { + "id": "859e3c89-72e2-51b3-9f54-a2dcedc9b144", + "version": "MTc1NjMyODA0MTk4Njg1NDAwMA", + "digest": "sha256:2b9db739d4124567788e77476e9db0d00943aa0ae65d5b94c641ce3c2c371eef" + }, + { + "id": "85c664f5-c7a3-5436-a671-3f231f7844b6", + "version": "MTc1NjMyODEyNjU4Mjk1NTAwMA", + "digest": "sha256:55f9191769eeed7039daaa6469e6c05817fe3fd120f820ed2656e04f352038fe" + }, + { + "id": "85cc51ed-ac0d-56e4-853e-8bb90b2cd13b", + "version": "MTc1NjMyODEyMjI1OTYxNzAwMA", + "digest": "sha256:ad1353f02eb73130b11ed41544c5aa649859dd86a751d9c595b257c1984d864c" + }, + { + "id": "85d35e87-372f-5328-b037-3f4b50458422", + "version": "MTc1NjMyODA2Mjc2MTYxMDAwMA", + "digest": "sha256:d7c062f99d17608da13682521c41912de0f4156289532335a8a09247c780095e" + }, + { + "id": "85dad957-c62a-5f6e-8cb5-83b5e322b65f", + "version": "MTc1NjMyODAxMTEyNDAzNzAwMA", + "digest": "sha256:17634e80d3f8a6c5bc8289f9d372c0428a9df8dd4e2f580387f342cc54e2b1e4" + }, + { + "id": "85dfa9fc-59c8-5776-a6f2-e6710a4813c0", + "version": "MTc1NjMyODAyNTYyODIzMzAwMA", + "digest": "sha256:dbe04ea76c8bd71c13b37def4088746d75bd1d1a309f8c9f5f959995f0b00df0" + }, + { + "id": "85e4bba3-6593-5d92-afbd-a03ad3e1da9d", + "version": "MTc1NjMyODA1MTMwMjUyNjAwMA", + "digest": "sha256:3d2358d130d1ee9af8bafeccfc710829b1c62d0aff6dcef79f4cda6cbd942493" + }, + { + "id": "85faab96-93fb-57e0-b648-7616792e733f", + "version": "MTc1NjMyODAzODg0ODUwODAwMA", + "digest": "sha256:250db664665d1148338a3d9bcca5445670d7c86b34b87d9979225aefd9e16131" + }, + { + "id": "863be443-7962-5af3-9554-a2ae1b3bf9ce", + "version": "MTc1NjMyODEzOTIyNzc0NDAwMA", + "digest": "sha256:fa58dd134e217d7713d04e0f8d3a417c41e0399810cdb25c9f1d3b0a64a3ed26" + }, + { + "id": "863cfdbb-8376-5762-ab82-9f106a96dfab", + "version": "MTc1NjMyODA2NzAxMjIyMjAwMA", + "digest": "sha256:d6cbc91a4185056d0e7df3c9d491c0eec804e6cd450fa562251c1a3fc1e5e9b0" + }, + { + "id": "863ea328-ae51-5abc-959c-f0cdfa386a17", + "version": "MTc1NjMyODE3MDQ0NTIxMTAwMA", + "digest": "sha256:bba7769d9b1022be1eab38497dc97d8f40fe2633e27572ec3fd90ba1b584a180" + }, + { + "id": "864ab25d-1a10-5799-b548-24a5238b977b", + "version": "MTc1NjMyODExOTI2OTI4NTAwMA", + "digest": "sha256:5d2b12064291611ad7aad01228e07bcf133343a6a4c524efb7551644d7cf4764" + }, + { + "id": "865a8ee2-795f-57d1-b4f9-ced2dbbeee44", + "version": "MTc1NjMyODA5NDM3MzIzOTAwMA", + "digest": "sha256:12e86ca9e844ecfb060627cf430dd0a2e0c71f63bd5b20f18588507da8561285" + }, + { + "id": "86790c74-8725-5582-9652-a2b31a501b7d", + "version": "MTc1NjMyODAxODgwOTc2OTAwMA", + "digest": "sha256:1a6ab31b00858abb1db369b2561718f11043a51682a024b79c18c0bee3b97f15" + }, + { + "id": "868664fd-22ea-5e5f-8896-9c17f774bb63", + "version": "MTc1NjMyODE2MjI5NDkyNjAwMA", + "digest": "sha256:102ceb301efd64d35dd21e02b854385076d0859a3aa85a1ff13059fa78fa7bbb" + }, + { + "id": "868e2ace-6704-5753-9f36-db82c2538aaf", + "version": "MTc1NjMyODAxMzg0MDY3OTAwMA", + "digest": "sha256:732d16df88a34ed279d6b7ac2be13cc926d3ed771c9b4d5e6eb3c9c83e355668" + }, + { + "id": "8694300a-09c9-5381-a73e-5edc2d46a606", + "version": "MTc1NjMyODA4OTM0NjQwNTAwMA", + "digest": "sha256:524755f1f1feaf377330414e3cf36137204af355d1a90565ee53331e65ae81da" + }, + { + "id": "86969828-5a43-5658-a37d-f45847b99a62", + "version": "MTc1NjMyODAxMzg3OTQ4OTAwMA", + "digest": "sha256:16dcc3e223c3ded766057777560ca66b8462f90bf015c58051ffc65c9ab517b1" + }, + { + "id": "86add83e-00e4-5421-848d-90d5cb606fbd", + "version": "MTc1NjMyODE2NjM5MzQ4NjAwMA", + "digest": "sha256:fb9f32cf24bbf2642a5c8cceb2035091ad640ba77bb4b7b827ab27c2c9f76015" + }, + { + "id": "86d827d4-4306-5721-b00c-b7f5a3e20045", + "version": "MTc1NjMyODExMzU2Mzc4NTAwMA", + "digest": "sha256:f997e93553e1f43681642f1bbf141e8d3e07f29f6d965808d2f1bf23a7373f0c" + }, + { + "id": "86e3c9fe-c3e5-5dc9-b505-989990fd9fb0", + "version": "MTc1NjMyODAxNTIzNjEwMzAwMA", + "digest": "sha256:de1d6a8a872abaf73599e5b24ef89ad33f1936c61f2f244c4c4fa053514f5496" + }, + { + "id": "870ae2d2-6c0b-59ce-aaa6-0683edda04a5", + "version": "MTc1NjMyODEzNzM2NjM3MzAwMA", + "digest": "sha256:f32c47eca9cef21e3f79fa3bab4024c74d88ceaa43c70ce60cdc48e0bf1bee9c" + }, + { + "id": "873a132c-abf5-5604-810a-cbb622d66944", + "version": "MTc1NjMyODA0NTc3ODA1MjAwMA", + "digest": "sha256:d8c3885a7a16ee0e153b5d053ed63810dfa438c5525707031d288551dd462dea" + }, + { + "id": "873a4751-d252-5d3c-91ef-dc47d50092e4", + "version": "MTc1NjMyODA2MjEzNzk4NjAwMA", + "digest": "sha256:d01b7871de2d5fb5dec18ee08d2696cddc3e59863dbbe827e93fb7e204fca2cf" + }, + { + "id": "877857b7-0e0a-5b9e-83b9-77f3e1e62594", + "version": "MTc1NjMyODEwMjI5MTQ2MjAwMA", + "digest": "sha256:0e7bd6f9ce95071157db862092cc7786095cf8b7d04097829b0b1b8edcefff1a" + }, + { + "id": "87a75870-2914-5809-816f-72c16a44c043", + "version": "MTc1NjMyODA4ODYwNTE4MDAwMA", + "digest": "sha256:b079c1c53c1285f51f33c2d27b8a5472cf819848d59e284cf6e6497d4cef1172" + }, + { + "id": "87db5209-f7ea-51ac-b631-dcd94b8c3d26", + "version": "MTc1NjMyODAyMTMxODU2ODAwMA", + "digest": "sha256:e2d2f77b174c79a483a37b258103929ecfab932d28c2e75cfdae892ee5e93f32" + }, + { + "id": "87db7481-c0be-5637-a635-e1a61c9ac533", + "version": "MTc1NjMyODEzNzcxNDQ1ODAwMA", + "digest": "sha256:d1d1117c0010495f50abd510817669d99897f636f048fd7f6a4668a236077340" + }, + { + "id": "87e4df26-a176-5e6c-b59f-ea61b80431f1", + "version": "MTc1NjMyODAxNjUyMjQ5NzAwMA", + "digest": "sha256:d7efd86479254d308881d1fa69b3496077133393e28abd1e0d9a155132fedf05" + }, + { + "id": "880f0df3-470c-592e-b94f-0a0137e36470", + "version": "MTc1NjMyODEyMjE4MzYwMzAwMA", + "digest": "sha256:0b4ea2d2ff6b12d9ec1fcacfc983e587da827c2500f18113f75c08a172e705a5" + }, + { + "id": "881a1679-21ee-54d0-b8d5-46e2b5cabc32", + "version": "MTc1NjMyODAxMTA5NDgzODAwMA", + "digest": "sha256:2d30be5a8a66e8e229d30f9ad27b81bfb80b9e3c0d7fdef8f2c4dac5318d5820" + }, + { + "id": "88575ad7-2126-5f0c-9c46-2e36a3920569", + "version": "MTc1NjMyODAyNjg1Mjk5MzAwMA", + "digest": "sha256:366013f2f71146c1e4cea39b89a706552c4bb88770594742f00ee1fed57cb37e" + }, + { + "id": "887c3024-7b6c-548f-b609-33adff93752b", + "version": "MTc1NjMyODEwMjE1MTkwNjAwMA", + "digest": "sha256:f81ead517e1efeb62287580f587ef6ecfb6a608c794238623446b9c65fa934e4" + }, + { + "id": "88a430c1-87f0-5784-bf51-9f8c2a060ac8", + "version": "MTc1NjMyODE1NTA3OTQ3NTAwMA", + "digest": "sha256:2c02c8957a5c5a70eff4503a01280982da6a179b0d04b4cb35bf2a54519a074c" + }, + { + "id": "88cded89-900b-563f-b607-0e009e108965", + "version": "MTc1NjMyODAyNzgyOTQ0OTAwMA", + "digest": "sha256:17caa01daf26de55fe1009755028a02d7fc84c125a5aca05c19561061615e4c0" + }, + { + "id": "88d3f6cf-8046-58f3-b7e6-f390e996eb94", + "version": "MTc1NjMyODE2ODk4OTg0MDAwMA", + "digest": "sha256:257ca65264b3b6db8b6c5113fba072a0d6b5d3a01ed35e75cecd3d0dd51fb7b1" + }, + { + "id": "88d9c8a2-259e-554c-b82e-97936594d41f", + "version": "MTc1NjMyODE3NTAyODM2MzAwMA", + "digest": "sha256:cd1ef3d0b720dfe9046124dd7363a9af225fc14ef35a8ca1e36c8b3f4d89c388" + }, + { + "id": "88fe5e24-a371-5e60-a8c3-59b74c439fd0", + "version": "MTc1NjMyODAxNTMxMjA4NjAwMA", + "digest": "sha256:add0d1a310204a824eeb0861719353cc3a73ed7b888121001ad42a8511e258c7" + }, + { + "id": "892ebd78-0618-5321-bf3a-f24b669366bf", + "version": "MTc1NjMyODEyMjI0MTg3OTAwMA", + "digest": "sha256:d93c7fff37417768dbe4a42385a13c1da7a6c516631f236d22a94e76bdae4ce1" + }, + { + "id": "8934efa0-4fb4-5c8c-9a16-e40006ca8f6f", + "version": "MTc1NjMyODAyNjg2ODgwOTAwMA", + "digest": "sha256:cdad1b567e01c90c63c0a5f6525b8d61b0362ed720fa98f7b167a883f26969f8" + }, + { + "id": "895ddcee-43b3-5e01-bb47-a4423b2dbb9e", + "version": "MTc1NjMyODAwNTA3NjUzMDAwMA", + "digest": "sha256:2d57160d0dffbc9bc2a64a2a176ab4a8ca06b1df6163c75a1ea890897001b222" + }, + { + "id": "89a06ef7-9cea-5229-a76c-bee2922e4e82", + "version": "MTc1NjMyODA2MjY0OTYwMjAwMA", + "digest": "sha256:60d06b8b716cbf8e1f4858551fe80f61df3fe6b2b0334d450d95d18991b46d6b" + }, + { + "id": "89c4ad6f-adfb-5421-b386-f924b16cb25b", + "version": "MTc1NjMyODAxMTg5Njk0MjAwMA", + "digest": "sha256:835356bb208f42693faa4da1c7e5efabeee7bc6b8086c184756998f166790e23" + }, + { + "id": "89c559f8-a3f3-5cc1-88a2-2a0ee8c385cb", + "version": "MTc1NjMyODAzODgwMTk0MzAwMA", + "digest": "sha256:9b6349f7692384281e076f424baaf4e0732a3d1169c0db80afb8eef8f40c4a27" + }, + { + "id": "89ddb12f-6db4-50f1-ac70-71d870ba49a8", + "version": "MTc1NjMyODAzNjkwNDcxNTAwMA", + "digest": "sha256:e596daf0ec5949f51d7f47a4f5e269fa4580dbfaacdd667092399b900f208f10" + }, + { + "id": "89e113aa-cbe1-5d4f-9f1d-131dbdad9c8b", + "version": "MTc1NjMyODEwMTAzOTYwODAwMA", + "digest": "sha256:76005e040584d2b0892708cf0eeda8bab4503bc1530b7eeaba7c993da1eef685" + }, + { + "id": "89f4d798-b002-5364-9e45-741c2b1de3bc", + "version": "MTc1NjMyODAzMDgyMDUzMzAwMA", + "digest": "sha256:9272563aea001925d88f2c480a0a6aa0bcf8ede7fc90dd791f5cf854138072fc" + }, + { + "id": "8a02841a-dda9-56ca-8390-c7d645b04074", + "version": "MTc1NjMyODA3MjU3MjA5NDAwMA", + "digest": "sha256:e246f4644fd26387dd012510c9d38d837f58a63a7a61ac489e497497dcaee484" + }, + { + "id": "8a05f4e4-9603-572c-8a9d-c0a5dc1eff47", + "version": "MTc1NjMyODAzOTY5Nzc5MDAwMA", + "digest": "sha256:b179abb4baff91a0f1c49fae8d7267ed223be21afd56975a2265a7abbe528aca" + }, + { + "id": "8a357828-07e7-512d-8544-11c4a83e252a", + "version": "MTc1NjMyODAwODk3ODA1MjAwMA", + "digest": "sha256:9aea44cc1dcf5e4a2be04a2e7e7af85d04c04c116917de5bdccf9df1ef35d0cb" + }, + { + "id": "8a5b5d61-f926-568c-a9a4-0724c3323747", + "version": "MTc1NjMyODEzODE0NjQxMjAwMA", + "digest": "sha256:ddd1e32189e45a9e9489500c9a50a024eb2badc58f31b35ff23be126b649b6f7" + }, + { + "id": "8a6cf25d-3b31-5196-b1a9-ccdc2fe8808d", + "version": "MTc1NjMyODE2NjQ2NDQ3NTAwMA", + "digest": "sha256:ab434da4d181ab574aee37b1dec0bfac91ff8a291937fb805c8cbe477a21652f" + }, + { + "id": "8a8e730f-81e4-58fd-b791-b0840ebed57d", + "version": "MTc1NjMyODEyNjUxMTQxMzAwMA", + "digest": "sha256:b6ab5f98e2f007d93893bd40a60f420aa540030f686eedca0074ac23e45eba40" + }, + { + "id": "8ac309bf-862c-5aab-b763-2793affccf81", + "version": "MTc1NjMyODAxNTIyNzE0NzAwMA", + "digest": "sha256:08ddb837ade4119b17d89931c5eb9d6af3d490692f69a35eea68d96aadf8d878" + }, + { + "id": "8b056a4a-5031-5529-92c2-4f76fad90767", + "version": "MTc1NjMyODA2MjE5NTAzNDAwMA", + "digest": "sha256:4658861314159664bdc386658aae07ba00386d59f1a6d01f7cdcca52745a72f7" + }, + { + "id": "8b2c40da-cde7-57c8-a96f-b179cac7bf7e", + "version": "MTc1NjMyODA3NDIwNzgzMzAwMA", + "digest": "sha256:8617cc9a4fb97c243bf18ac0f300c0b5cf4c9b50b251b8c481f99e16875aefeb" + }, + { + "id": "8b324b9a-9a91-58e4-b417-d57a47fe853f", + "version": "MTc1NjMyODE2ODg0MDc5MTAwMA", + "digest": "sha256:1bc58775c79e8acf580865ad1002179515a6548292d59894822ac3575cd87d53" + }, + { + "id": "8b44a6dc-c86e-5381-bf29-026f29b39c40", + "version": "MTc1NjMyODEyNDUxODkwMzAwMA", + "digest": "sha256:6f44db1f6ee2a4b2c9aa326f3c961d14eb93bfdca36e2a5106862c90e3265c3c" + }, + { + "id": "8b5cf62a-fcd2-51f5-8689-a62b73335913", + "version": "MTc1NjMyODA2MjIzMTg4MDAwMA", + "digest": "sha256:f19dde3a9417c8b034496a812bd7cc36f699f200432c50908ceae0e3cc015f2a" + }, + { + "id": "8b5ef4d8-f44e-5512-8c6b-715f423cc080", + "version": "MTc1NjMyODEyNDQxODE4MDAwMA", + "digest": "sha256:324ea8582a91c115790b5cc692ccaf289474b8939ab492ea12500ff7921bc041" + }, + { + "id": "8b67db68-035b-5c2c-abef-8419c1533322", + "version": "MTc1NjMyODAyNzQyMzY4NTAwMA", + "digest": "sha256:d58cdd618c16cd52ca0a9ddfb10dfc6c9c93413ec311f47989c5a5f87945c12d" + }, + { + "id": "8b70d528-3e3b-5b72-aaff-295353e64e60", + "version": "MTc1NjMyODAxODg4NDY4NDAwMA", + "digest": "sha256:d30996c02c4d47daf2108a2a2ddb3ec37e5b52ec5f16a35e2882491dee8bdbbd" + }, + { + "id": "8b7c0481-0722-52f9-b767-a0a5f0307ad2", + "version": "MTc1NjMyODAxMzc4OTMxNjAwMA", + "digest": "sha256:b5693b07c942a6fa97a49eb5f7f5845de3014d7de1610085d00ba7468491c093" + }, + { + "id": "8bad9cb9-cfe6-5180-adcf-e1e2c33084eb", + "version": "MTc1NjMyODEzNzA4MDEwMDAwMA", + "digest": "sha256:99ce20a58fa6cb4a65337e67002395bc2b7a12f140f3eaef339a115ae164dfad" + }, + { + "id": "8bc2bba2-5d46-51c2-b706-f2025cf8f49e", + "version": "MTc1NjMyODEyNDQ2NDczOTAwMA", + "digest": "sha256:1d314849ec07787fe859fa0c917e6ef4e66bd81689b62f745e7b647458032f94" + }, + { + "id": "8c07c920-183f-538d-8c87-621b327e6b3c", + "version": "MTc1NjMyODA1MjU1NTkyNjAwMA", + "digest": "sha256:d75c9fa9e34e83158e111abae2a9d5e75fa68dc7193fb2f027d3ef5682dc41bf" + }, + { + "id": "8c3b2abe-485b-569f-a270-b9afc8c78ee2", + "version": "MTc1NjMyODA3MzE2NTAwNDAwMA", + "digest": "sha256:52eda3365df72b450a2def742eac5d0f362c8d936a287c12e975a69cbb62c294" + }, + { + "id": "8c44b99c-22a8-5702-816a-4e1fb7529e11", + "version": "MTc1NjMyODA5NjkyNzYzOTAwMA", + "digest": "sha256:a3a3eaa2e28d74e1d0db5268940ec14fa59f8cc51eae11bdff46db63ed858ed4" + }, + { + "id": "8c539eab-b332-5bf2-9237-8ef0dcac2300", + "version": "MTc1NjMyODA2MjIwNDA5NzAwMA", + "digest": "sha256:178c8669663bd4316d826f85e5bd086f8a88d1d8bce077d423e2a64a27f1c6de" + }, + { + "id": "8c78d6e6-e34e-5673-a1e9-25fc939716aa", + "version": "MTc1NjMyODAyMTI0OTM0NDAwMA", + "digest": "sha256:094c6b8f1368ab7f8e829f6186a67f785e9724419411ff562cec1b6b1c75aafd" + }, + { + "id": "8c8b3def-e5d4-5afc-b886-aed3415d5cf1", + "version": "MTc1NjMyODE2Njc3NTE1MDAwMA", + "digest": "sha256:ae1d31ed8745a6b985ec30f24cbd0a8ade1f8b488f86b223adb202f7a07d6425" + }, + { + "id": "8c8ea748-6321-5a29-a737-7f9cb3b6e982", + "version": "MTc1NjMyODAyMTI3NjEwNjAwMA", + "digest": "sha256:43b4a8470a487fb35b7e669df3473e0d08115eead39e15544bf1815413fcbdbb" + }, + { + "id": "8c94b18a-6144-50a9-918c-4aa7bc87fe8f", + "version": "MTc1NjMyODExMzU4NTkzODAwMA", + "digest": "sha256:76dbc938a46f6ebe7c5d30471c23d0d098499671e569dda65952507d72c68de8" + }, + { + "id": "8ca51dda-b54b-514d-b380-7fc2a7b37947", + "version": "MTc1NjMyODE3MDk3NDE4NjAwMA", + "digest": "sha256:20e47aca64523a17ef9a7d50d58dff26a107c9f98f05ffadeb03647d3a48f79f" + }, + { + "id": "8ca93d06-4efc-56dc-8a43-a1fa9f942e34", + "version": "MTc1NjMyODAzNDcxNDgwNDAwMA", + "digest": "sha256:7d47994326832412d5e05756c9b60ee36db44657ba7dc20065e3e3fe7ad2e7fc" + }, + { + "id": "8ca941f7-6931-58d3-ae6a-be8978da8279", + "version": "MTc1NjMyODAzMTAwOTExMjAwMA", + "digest": "sha256:898b442cb93643318da9cb0a4097b4d8c732ee485fe4901659733981875b8eec" + }, + { + "id": "8cb0a48f-d354-5ee9-827b-9e427c880275", + "version": "MTc1NjMyODA2MjAzOTc0MDAwMA", + "digest": "sha256:2288d55f1785f3ea295cf7a06e0659fae74532234abd588ccbca4a986447f103" + }, + { + "id": "8cb9a734-6b32-5b4b-bcc8-c505f148274a", + "version": "MTc1NjMyODA3NDI3NzY2NDAwMA", + "digest": "sha256:8bbf341cef6c66b148fc10be01a8b23e730a89a1fca71e873d63e84e1dcb75e5" + }, + { + "id": "8cc2926f-1b72-5e21-be56-49a20362b6b2", + "version": "MTc1NjMyODAzNjg2OTk0MjAwMA", + "digest": "sha256:da46b133690ba278c1b5257c0a214ac069b9e9239422e5132ffc56bc4bf05d12" + }, + { + "id": "8d237618-6df5-5180-a935-2476e85fcae7", + "version": "MTc1NjMyODEwNTMyNDIzNjAwMA", + "digest": "sha256:f7e643bdf41aa5437bd188536eb9d7371320584fa8f07f57f4f76c880c62451d" + }, + { + "id": "8d243720-6362-572f-bb34-8a362a183dc1", + "version": "MTc1NjMyODEyNDQ4NTE0MTAwMA", + "digest": "sha256:810f413bd58f0679053ce85ff0e93eacaa5112cd0bac00e237c067ec7c514367" + }, + { + "id": "8d5500f1-a06b-55b1-bf3b-5792ad1b7783", + "version": "MTc1NjMyODAwOTAyMDIxOTAwMA", + "digest": "sha256:840d2b0168d2f0a1afa0806c9da9990b0f891f0f590807fcaae9c8fc72156fdb" + }, + { + "id": "8d71b6fe-996b-571d-afcb-b78c2bd218b6", + "version": "MTc1NjMyODA1NTc1ODQ5MDAwMA", + "digest": "sha256:96421f59c8873f3c206378c208e0163721ba30bdcd87f5a539c8b467a13ecfbf" + }, + { + "id": "8d9a13f8-be9f-5c2e-b098-de251194f123", + "version": "MTc1NjMyNzk5OTI2MzAyNDAwMA", + "digest": "sha256:b813594d9448d5de2395b607f559d2f7ceb97f8b8c033576da9b6df542705ecb" + }, + { + "id": "8db15a38-3d94-518a-a62f-d1a60dca576d", + "version": "MTc1NjMyODE2MzI4ODU3OTAwMA", + "digest": "sha256:db1409b90744ddc56ae43c580f763cb6da48c480209c4673ec9b3aecab50e5c6" + }, + { + "id": "8dd6c0ed-1047-5144-8fb8-a6cb2e42540d", + "version": "MTc1NjMyODAwNjE2ODUzMTAwMA", + "digest": "sha256:390aa33979e10ee598d9ec28eeb11c2263206b2b08f752a2603fe6c7036907b1" + }, + { + "id": "8de4408a-062b-509c-be6e-4a7f1c8734f9", + "version": "MTc1NjMyODE0MTU2NzE2MDAwMA", + "digest": "sha256:9a094b0c066cb8f5ed7e5c2e0cc6b190f32daecf7d7018221a83d350a0fbb505" + }, + { + "id": "8de83693-8c8b-50ec-a069-54e4f4fa6168", + "version": "MTc1NjMyODAwOTA1ODk1NjAwMA", + "digest": "sha256:84fa74d3d676121a72558f94a27f340042839b0e56070d807b62d066b76cdb3c" + }, + { + "id": "8dee2bbb-8269-5d87-808c-8797ea995435", + "version": "MTc1NjMyODEyMjMyMzM1NzAwMA", + "digest": "sha256:f1c8e711a2062e6209320862201e13ae0017cab65b78beba69210fb0c14018c3" + }, + { + "id": "8e16f848-6c29-5372-afcd-5a6e33579a7c", + "version": "MTc1NjMyNzk5ODE4NzIzNTAwMA", + "digest": "sha256:68175208c140721fed0a0f8b469c10f30ff2692f9070300bf9197263fcec7398" + }, + { + "id": "8e1b19cc-d44c-5d30-b72a-b4046b330dd5", + "version": "MTc1NjMyODA4OTE5OTE4NTAwMA", + "digest": "sha256:28b51124a372f9222944a94a41a2a60d8813ef3655dbc2a9e931ddc9a7b35716" + }, + { + "id": "8e268f17-a9d4-5aef-8212-14c809b83449", + "version": "MTc1NjMyODAyMzQwNTI1NzAwMA", + "digest": "sha256:28ff4cf71469ab47e6c3e91e51a740bec59871dd09bd53612a31dce4b5e41892" + }, + { + "id": "8e2d0188-f34e-5fac-9e54-388c62269c8a", + "version": "MTc1NjMyODAzMDU0ODc5NTAwMA", + "digest": "sha256:fe62cbe43ceaecafc257a5354f1a089cec6b0fd57d12bfc1707db39978f5e275" + }, + { + "id": "8e371089-b70d-5ce8-a6d7-07f2bfae9133", + "version": "MTc1NjMyODAxMzgyOTYzNzAwMA", + "digest": "sha256:232a8eee9f8312c614bb6f6a6484d1551073f13fb5912f9e7a83e9f886427a9a" + }, + { + "id": "8e3ca14b-8b92-51de-bcd4-5f7102719a39", + "version": "MTc1NjMyODExOTYzMzM5ODAwMA", + "digest": "sha256:3d3a4bdd727e50164b99a5804079adab002dd1f5333ad5558ef58cf66bbbb5bd" + }, + { + "id": "8e69f79e-995a-5865-9efb-473372988523", + "version": "MTc1NjMyODE2MzQ0ODAzNTAwMA", + "digest": "sha256:e94c6463900f5d2ff72d0cd80494f4753de1e7c857e884da76cad01b262ad502" + }, + { + "id": "8e7e71ab-3dc8-560d-9305-95f6d08fe53a", + "version": "MTc1NjMyODA4OTE3MDY3MDAwMA", + "digest": "sha256:806dccff7f6045988b5dcd25d289e237811808bddd52e0bd41a6900373d72cf1" + }, + { + "id": "8e97180b-da9a-5e99-aa94-15b56151e3e7", + "version": "MTc1NjMyODAyNzgwNTk0NDAwMA", + "digest": "sha256:b1859030345d9c1a1e6a4ec6238312f138ccca09d4012b71cb90b3b19de9b521" + }, + { + "id": "8e9f94b9-2031-5a4c-ab4c-d499a1e44aef", + "version": "MTc1NjMyNzk5OTI4MTUyMzAwMA", + "digest": "sha256:1a054411b163f6cec8805433cf8f3372c9e2189c999ddcd3c21b757e067f62e5" + }, + { + "id": "8ea287a4-27cb-5d01-a233-770a7ab1e4c3", + "version": "MTc1NjMyODAxMTg2NzkxMTAwMA", + "digest": "sha256:bfe82340feb202af88b54e1c0d13bc2b96f54eb8bdedf0e3f9dce3dd062d3068" + }, + { + "id": "8eb22753-3f23-5217-82d3-eaafaf82e1a4", + "version": "MTc1NjMyODAyNTc2ODk2MjAwMA", + "digest": "sha256:3b6c923d7bbe756c5fdac83d5760bbbeb6ab8d7474a519af3bcd8219c43d94fe" + }, + { + "id": "8ede1864-cf87-5d64-8a08-bece643e7bc1", + "version": "MTc1NjMyODEzNjE1MDU5NjAwMA", + "digest": "sha256:3cbf2df6220cbb14d49c0509a343e6f9a8bbd286b988a7501e92b419f565506c" + }, + { + "id": "8ee1905b-84b8-5125-bdde-20f920dc6fe6", + "version": "MTc1NjMyODEzMTA1NzAxNDAwMA", + "digest": "sha256:eec0b7f6173ee7a4a09d57d32edf14fd31352a9209a12d8e4269974e65a26c96" + }, + { + "id": "8ef41bc2-00cd-51f5-94c7-275f4a88f8d1", + "version": "MTc1NjMyODAwMzkxNTI4MDAwMA", + "digest": "sha256:bcfa2082298d9a2fea62f224d3de899d91f7abd77991bae50f1068959918c60e" + }, + { + "id": "8effa3cb-76df-5019-9756-992c2cf5b35c", + "version": "MTc1NjMyNzk5OTMwNDAxNjAwMA", + "digest": "sha256:4f7f164d5d5a5bbbd696a131ce3a7644afa725a7cbd1211ca1ab0e5456add8c8" + }, + { + "id": "8f283787-5fdf-5ec1-af61-ea16c4544b05", + "version": "MTc1NjMyODE3MjQ3Njc2MzAwMA", + "digest": "sha256:a19f7c30dc85effbfb088f043bc747ec381eb34a44ab3ef9989ee149706de1a9" + }, + { + "id": "8f2de78e-10a8-577d-8749-83ccbed26730", + "version": "MTc1NjMyODA2MjAxMzc0MjAwMA", + "digest": "sha256:064bcee79449f32491fe39f0a7626b19a9ad2f4e2ce87512642d2cec84d5176d" + }, + { + "id": "8f3c47df-48b5-563f-8b23-3f5780472ca1", + "version": "MTc1NjMyODE2ODg2ODI4NjAwMA", + "digest": "sha256:3863263c02daaf3ada4036e6e75618de433086e1b1d01579648986621d34d67c" + }, + { + "id": "8f4e03b7-27f2-50c2-9dfd-1457b3e700a9", + "version": "MTc1NjMyNzk5NTM1MzYzMzAwMA", + "digest": "sha256:b37225367170dc10f997f026bbe804a68c39a08c8a2d100755fd3fcea8542171" + }, + { + "id": "8f7b89ee-6873-5f9e-a32c-22ebb16b9aaa", + "version": "MTc1NjMyODA4OTE1NTg2MjAwMA", + "digest": "sha256:5dd55ce7d0eb03beb86cda8c538792b93ec48745ca6e5f5f0f1d31e10352f9ca" + }, + { + "id": "8f7ec9f5-4643-5ee3-8dd2-8a335ed075e1", + "version": "MTc1NjMyODE3MjU0NDc3NTAwMA", + "digest": "sha256:5eb6dbe2cb5a5f9bf5f6e671cb60e68d8bd9e360e639b5325a4a396b3c3a071b" + }, + { + "id": "8f824ce9-1f35-5840-a998-6bdc92dcfd79", + "version": "MTc1NjMyODE2MjIzOTA0MjAwMA", + "digest": "sha256:172b5fa6f3be430f57130d448b8152713f567dcb73131aed035e948e6a0d7a14" + }, + { + "id": "8f857723-20bf-59b0-a82e-7dc9fc7424d8", + "version": "MTc1NjMyODAxNTI5MTg2ODAwMA", + "digest": "sha256:90123039fe0bef003cf638dc7065cdaaff18bc0324aebf9d156b6a8d2737c500" + }, + { + "id": "8f861fd4-d154-5310-bfc7-e37408a6bb7a", + "version": "MTc1NjMyODAxMTA3MDQ5MjAwMA", + "digest": "sha256:787ace10467c64a849ea0ca78ed7feda103abe6dc7d31ba78e7192ed4a273234" + }, + { + "id": "8fb99e2f-aa47-5a28-8675-e4d736513d20", + "version": "MTc1NjMyODE1NTE3MTMyNjAwMA", + "digest": "sha256:16f746df5ee450835b24e587b1e540ce312954e8ca19c3ead4bd3fac6c340f06" + }, + { + "id": "901719e5-09af-521d-95d0-55096244237c", + "version": "MTc1NjMyODA5NDUyNjU0MDAwMA", + "digest": "sha256:dc42f647eb52c39c2542355323d34464ab64972b0aec41a69eb8314544058bde" + }, + { + "id": "902c3e12-ee26-5f74-9ea6-5cab2e640606", + "version": "MTc1NjMyODA5NDQ1NDA4NzAwMA", + "digest": "sha256:2aa1e1151f1522f07405f0b50dde175b7ce516f47bc0197a1ec2d883d595fffd" + }, + { + "id": "9066120f-91b2-5c42-958d-1939a34d8ad9", + "version": "MTc1NjMyODAxODgyNDIyNTAwMA", + "digest": "sha256:d5905e3b3b95deb9e3bde6e1048ac730c4182ec1c53c4112bdefd7a62f72de64" + }, + { + "id": "907b312c-2c0d-52e3-9876-9e59cdebfcb6", + "version": "MTc1NjMyNzk5Njc3MTg5NDAwMA", + "digest": "sha256:4fddc927bd4d2a75edaa941c6e6933ccf8a0f47748423011c95651bf19f363e4" + }, + { + "id": "9084074d-8ac0-5ebf-b5c9-340a154d48c2", + "version": "MTc1NjMyODA4OTI4ODM0MjAwMA", + "digest": "sha256:5a10468ac07d999548866bc670cdabcf2227192862537e834f9140c41b46f816" + }, + { + "id": "9086700c-a882-5e03-98eb-a989613726a1", + "version": "MTc1NjMyODAxNTI1NjQzMjAwMA", + "digest": "sha256:93186c8c87d7287d58d2b204bb22a3aadf220b55666f5b843e939a8cd183e814" + }, + { + "id": "9087dcd9-45cc-59ce-a0ee-7c69bc2b1bb2", + "version": "MTc1NjMyNzk5OTM2ODc3NzAwMA", + "digest": "sha256:e373bbb99889c2b0b60d652cfe476dca728ee94f083e0a87ed621afc9ebcc346" + }, + { + "id": "90a9af24-7ac2-53b9-b578-9680b1b08d8d", + "version": "MTc1NjMyODA0NTg0OTQ5MjAwMA", + "digest": "sha256:b2277c1f4c7c9837ff485ae7cc0b3557e56b8d154685c5955b67c2eef06089f5" + }, + { + "id": "90cd1f9d-8fc4-5456-b4ca-4da2c58e987c", + "version": "MTc1NjMyODEzNzQ2Mjk0NTAwMA", + "digest": "sha256:b35dcfa67aafaa59165adf3e5ea1910a192db2ee07d87d6a3b34941100926f04" + }, + { + "id": "90ce6508-ed21-5d98-a231-71e5412d2b2f", + "version": "MTc1NjMyODA1MjU0ODEwNTAwMA", + "digest": "sha256:6791f383100ace8f76a0ae6cb1cc56a68696e05ebc1ac830310d0bb206758d6e" + }, + { + "id": "90ee680a-9a86-52ca-9215-3bfd53eb169e", + "version": "MTc1NjMyODAxNTQ5ODQ0NTAwMA", + "digest": "sha256:20ebf2e4c298fbf0980f28098190eebbf21e19fca5fb5f3f8e923b194ac25f79" + }, + { + "id": "90f014b2-9742-5c0e-a998-f2a5181af00b", + "version": "MTc1NjMyODE3MDg1OTc5MjAwMA", + "digest": "sha256:aaedbe31053fc566a92b0773393bfde8c013047ba14199b0d6ce9ef9ed1ecbef" + }, + { + "id": "90f6d42b-062e-5282-89bc-91757e5ab968", + "version": "MTc1NjMyODE2MDYwMTk3MzAwMA", + "digest": "sha256:dc0f74dd670d06b45dc0cc11365b18edf1697bbc04040658c3773b05e6215261" + }, + { + "id": "90f9aa20-6119-5c8b-8be8-d3c60f7fa0a4", + "version": "MTc1NjMyODAxMDUzNTA5ODAwMA", + "digest": "sha256:37db142af999b68478cd6deb9d7662ee21c37a3ef3cc31bd0a5c249b8cc6e0ec" + }, + { + "id": "910008d1-8d6f-5dfe-b321-010b9efead5f", + "version": "MTc1NjMyODAyNTYwOTgzNjAwMA", + "digest": "sha256:a3795db73bde1955bdd0bbff78b7853cc4e36f4e61bd7809d2688c98ebb0dbc8" + }, + { + "id": "9107fc63-90ad-5263-a968-77e4b1c0a1a6", + "version": "MTc1NjMyODAxOTM5NTM2NTAwMA", + "digest": "sha256:5a469d986b574a46a8219b7b1d492dad2181d3b52768f1a61d3da096a1d5b9bb" + }, + { + "id": "91211844-3489-5b0f-9f84-ef4227bc1412", + "version": "MTc1NjMyODAxMTA4ODg5ODAwMA", + "digest": "sha256:986ae5ae782d607b9bd112e6f96998b4d470bd803631a97199649bc4300fcaf2" + }, + { + "id": "9121425b-ee7b-5f28-a2b0-0d4854886e17", + "version": "MTc1NjMyODAwOTAyMzU3MzAwMA", + "digest": "sha256:5d7c52fe938524027bca9c111ecc654b22bd1a0454ee31fa504a90089946ca91" + }, + { + "id": "91377be4-b7c0-5369-8b9b-3007ab6c2f62", + "version": "MTc1NjMyODEzODMyOTgyODAwMA", + "digest": "sha256:c3e324a74e0682ac8ec7112b17c8ffc4d3f857258efa8278bb68392780a0f9b8" + }, + { + "id": "914468dc-dad7-5908-a13f-306119649713", + "version": "MTc1NjMyODE3NTA1NjI2NzAwMA", + "digest": "sha256:dc4485a31dd1bf66013677e56263058dda607a034c9eb79229b5e80ab9e9c26e" + }, + { + "id": "914e41e5-40ab-528b-8c13-a805a5a436af", + "version": "MTc1NjMyODA3MzI3NjE3OTAwMA", + "digest": "sha256:371e5a15f57884c37a33d22df5f5763118678b9cfdf904446e556c7fc3fe9e96" + }, + { + "id": "9150a3af-1c46-527f-98d9-3c12dae3e4cf", + "version": "MTc1NjMyNzk5MDM2NDI3NjAwMA", + "digest": "sha256:0fb8d8539a6905375e5fde804aafa42afdd1375c5ee1a528195e353fd3d8e182" + }, + { + "id": "91682140-509c-53ef-8eab-02d4d99fc5e3", + "version": "MTc1NjMyODEyMDUxNjI2NzAwMA", + "digest": "sha256:4880e3941377f3548ae75d8b3c822aaffecbfb4b64fc0b6cf4c4ddb8ef9f0188" + }, + { + "id": "91722aed-94c8-5962-8f78-4367a014e462", + "version": "MTc1NjMyODAzNDc2NzE2MTAwMA", + "digest": "sha256:86beed1b952d893544f7ca6c035ae43f039aedc860890e8ee99fa000abed3924" + }, + { + "id": "917a7bdf-220c-52d8-a6f5-586425e4acb3", + "version": "MTc1NjMyODAxNjUwNDI4NjAwMA", + "digest": "sha256:5bc3902c34bbfe699297272852a733c5591a654d770266e891e1e56f93647e11" + }, + { + "id": "9183c49b-cdce-57b3-b61d-c496f9ec2057", + "version": "MTc1NjMyODA1MjYxMzczMzAwMA", + "digest": "sha256:2e6ed682cbf7d48419979ee5e4e1675f4a2250c34966133ff2a768eb856990ea" + }, + { + "id": "918cf984-5392-5a17-907c-108ac7c35cc9", + "version": "MTc1NjMyODEzNzQzNjU0MDAwMA", + "digest": "sha256:f4c4c8eaae30b5186f9a1fedc4eadbc8de6a666fbb6c32b7427b6ac2ec3a6497" + }, + { + "id": "9196d684-0855-5d98-a6e1-79a3421c0012", + "version": "MTc1NjMyODAzMDcxNTk4NTAwMA", + "digest": "sha256:0a7dccd4cb6d9f7405791aa8dcf7da36277269422fd0f0dd375904efbec5a261" + }, + { + "id": "91bd827c-1fc6-5221-bb04-60d4238607fe", + "version": "MTc1NjMyODE1NTE2OTAxNDAwMA", + "digest": "sha256:73eb44cd21a441dbc17314f9bc3ef6a9cd8a037fdac2c1a758467fbb16904020" + }, + { + "id": "91c02652-3163-5df5-bf0b-d3ec8f4765ba", + "version": "MTc1NjMyODExOTYyODYzNjAwMA", + "digest": "sha256:f8c30f4d57974007b6bc2200020734ed0a0815016eb387ee6d795583b3d7c71a" + }, + { + "id": "91cad1ff-b9f0-595f-8135-4e4eb905d556", + "version": "MTc1NjMyODAyMDU1MzcyMzAwMA", + "digest": "sha256:518066493b2f065a7c400c4732892c70d3bd431e3e893eab5c154ed8ebf8609a" + }, + { + "id": "91ea0a05-7805-599a-bd0b-6667ca9c4b50", + "version": "MTc1NjMyODEzNzM3MDgzNTAwMA", + "digest": "sha256:1ffa62b880469de3a058ef389cacc1374b4ee609d01edb804ee5911bb10b1b03" + }, + { + "id": "91ea84b0-c8ea-52b1-b4e0-6be5b9385218", + "version": "MTc1NjMyODEwODgyMzMyMjAwMA", + "digest": "sha256:d576e475daec3b1af3665d0715794fa25f9e9a7bb86d34f880fa5d248ea85de0" + }, + { + "id": "91f379ab-99f0-526e-ab68-d273bd0d0f48", + "version": "MTc1NjMyODAzMDczMzk5NzAwMA", + "digest": "sha256:8fbd75180a41c7ee39e80e04c653d5dd49e3e653ab60574a2270d7d1d1f31451" + }, + { + "id": "91f84638-8699-5709-8092-e0423f769353", + "version": "MTc1NjMyODAzODcwOTQyNTAwMA", + "digest": "sha256:8077d60d024879aab0d993522136ad9cea6c8d944b1fe77db838d39bdb33e9ec" + }, + { + "id": "91f8f198-c967-53e5-97b9-945482cf73bb", + "version": "MTc1NjMyODAwOTA0NDEwNzAwMA", + "digest": "sha256:5c78fe51eda7bfe760fd33045dbe3911942d00be9989531ce326ed751407b597" + }, + { + "id": "923af885-7fd1-5ad6-94fa-7921f897d362", + "version": "MTc1NjMyODA3MzAxMjQyNzAwMA", + "digest": "sha256:cc4a26e38fa191c0a0848ddb6b51d9b634d1ef251e64280f1cda4b6a8b24cb77" + }, + { + "id": "924cfd1f-7c9a-5f73-9cff-757d635081f6", + "version": "MTc1NjMyODA2MjA1OTk3NDAwMA", + "digest": "sha256:c651851dc84c767498c1228f77f86a385ed5a0478ad268dbce83d3f3181b4ae1" + }, + { + "id": "925912a2-a50f-5868-a323-ab44a5ef633f", + "version": "MTc1NjMyODA3MjU2NTk0NjAwMA", + "digest": "sha256:1779b9b36479f2e6beee82220921484687da09b5d1616a4c45fcb2e800186d01" + }, + { + "id": "926d902b-6437-55a2-9ec1-d544bc111e70", + "version": "MTc1NjMyODAyMDU2MzEzOTAwMA", + "digest": "sha256:c884e1acdc32863d9efbbffa2a02d4943feb02f51d4cd1fe5e458d10f536c2c9" + }, + { + "id": "92921f91-4d95-50b9-8384-24308a3fdf20", + "version": "MTc1NjMyODEzNjk1MjIxNTAwMA", + "digest": "sha256:31ea2c3f9c94d427c6f435db528c00175f2e8e642abdfe511e16fcd51bc13a82" + }, + { + "id": "92971189-0758-56a6-8a66-08d9162fb527", + "version": "MTc1NjMyODA1MjU1ODA0MjAwMA", + "digest": "sha256:52157158f5ca5fb7582bc52fae9f8e0cbd25b61295df51bc569ef488251384f0" + }, + { + "id": "92b36487-0be9-5374-b9c2-9740a1463d73", + "version": "MTc1NjMyODA5NzcxODI2MTAwMA", + "digest": "sha256:bd481c269cc61ec6a58f316107c5fa2c4cf6b31dd2025487395030953fd44a70" + }, + { + "id": "92b9d8b6-8c83-5891-9300-63157d993dd1", + "version": "MTc1NjMyNzk5ODE5MTkwNTAwMA", + "digest": "sha256:b40770f544cd8c702a4a23159a6ab3d788415827ffe8edc36b0cb9ab70a2a6f7" + }, + { + "id": "92c61c0a-871e-5686-b201-d4de224abd5b", + "version": "MTc1NjMyODAyNTkzMjQzMDAwMA", + "digest": "sha256:2c23d4369c9cc0648c640907ec026a0e7b32dca5869f81d3c7cf5307d3317803" + }, + { + "id": "92c64a47-f26f-58a9-86d0-09467bb549cc", + "version": "MTc1NjMyNzk5NTM3MTA3NDAwMA", + "digest": "sha256:08f3530cfb6717b1b131a6dd9bd24495282be2574fff75a0eb26a54f1ccbf384" + }, + { + "id": "92edb900-16ef-5ad5-b297-bfbd8ede9c04", + "version": "MTc1NjMyODAxNTU2MTYzOTAwMA", + "digest": "sha256:1ba10d9f7c0724b558f790fffd0a33738267072c449c41c5860d5fca7d912cb6" + }, + { + "id": "92f8921b-57a6-51ae-a3d1-12ab1a60a4a5", + "version": "MTc1NjMyODA1MjU3MTY2OTAwMA", + "digest": "sha256:604ba7e3b979092308b262a2119f85cec0d2c1f23cf531c1bf8148c92adc1d63" + }, + { + "id": "9327d8b8-e0af-52cf-b5e6-0c5600b3a71b", + "version": "MTc1NjMyODAzODg0NDAzOTAwMA", + "digest": "sha256:c27babf3df90511bbab796ce427120e682032d5bde74f4ad239c42ddf5b0df0a" + }, + { + "id": "932e755a-d7f8-5da5-ad00-44e9d1ac2e3a", + "version": "MTc1NjMyNzk4ODcwNDkxNzAwMA", + "digest": "sha256:91ca25fdfa85454db07c9cb4d97b7b8b217af690ad2da3779983d57db3dd6225" + }, + { + "id": "9336dd4b-5afc-5eb2-ad8f-f0d630fd4b3b", + "version": "MTc1NjMyNzk4ODczMjI1MjAwMA", + "digest": "sha256:24d3d6523a5bcd5804aae26981566e679a8c44836956b2ff8eb0489f5d0b4f67" + }, + { + "id": "933c7219-badc-5734-94fe-4de8bb5ccfb5", + "version": "MTc1NjMyODAxODg3NjgzNzAwMA", + "digest": "sha256:ce8fb464e2d659bd3254baa5503ff5d685bf1b72e600a289433cfbbbef34da53" + }, + { + "id": "934f93af-5c0e-5c54-aeef-f59993027040", + "version": "MTc1NjMyODE2NjgyNTA5MjAwMA", + "digest": "sha256:1503ce4bbe868fc6ddc28aeecfcf6dfee1229a4442a207632c915bf4c3ac529e" + }, + { + "id": "9351fda9-98b6-5857-9741-8a01dd784f19", + "version": "MTc1NjMyODAyNTkyODM4MDAwMA", + "digest": "sha256:3e1c2a2799d3f2391c09ec522e912dcda7944513f04dce756caa77869734d026" + }, + { + "id": "9361770f-f4cd-569d-ba81-18d55956fa4b", + "version": "MTc1NjMyODAyMDUxMDU2NDAwMA", + "digest": "sha256:b5fe2b8a6b5135cc0ae4202f2951d3832ade49411253a713b67587a52eb1dbc8" + }, + { + "id": "936559e3-fcdb-5ba1-a3d9-4fb07fab2bdf", + "version": "MTc1NjMyODA1MTE3Mjk3NDAwMA", + "digest": "sha256:db1c38fb3758be4b09e11b3643c8e059d958a254e24f0e88a1986292cb7c5391" + }, + { + "id": "936ee118-9494-5fa2-ac53-825c1c36fb7d", + "version": "MTc1NjMyNzk5Njc0MTAyNzAwMA", + "digest": "sha256:0e3a875ba11039c45f4b37692ec4db115ca9c199f9dc73003e84ed559de45733" + }, + { + "id": "9370079f-0788-5b36-8028-ed746ae4e4f2", + "version": "MTc1NjMyODAxMzgzNjkxNTAwMA", + "digest": "sha256:4cb706dc39c5c6762cccdca098e9b5f3b3c3f9f7fadf2f2a9fcd0c5e8217de85" + }, + { + "id": "9372e2aa-0294-5cce-8a8f-119a223a5b7a", + "version": "MTc1NjMyODE1NjQ5MjgyMzAwMA", + "digest": "sha256:026589392dec63806e2186bb328a9f0e5bcac3f762860d96bcb70e04070ff7e0" + }, + { + "id": "9387beb0-4abf-50ea-94f4-28294683b0ca", + "version": "MTc1NjMyODA0MTg3NjA3NjAwMA", + "digest": "sha256:5694a3d930c1b55a3c964d6c90f2425314508e45d3580da4b13908474ad5537b" + }, + { + "id": "93a4433c-3fb4-5794-9d32-aa0b51f6611c", + "version": "MTc1NjMyODAwOTAxNjEwMjAwMA", + "digest": "sha256:176e0949665014a1d1b15da967367b4509006817a5b0ef11a8abef9e74ced2a6" + }, + { + "id": "93db9f25-cada-578a-aca9-323e66d7460c", + "version": "MTc1NjMyODA0MjQ2NzU0ODAwMA", + "digest": "sha256:9451911ba68fd8ba7a127addd019831ca0db4b4361ccdcea908d5d71d08088d7" + }, + { + "id": "93e80090-b57f-59d3-9cd9-04622c7a25f2", + "version": "MTc1NjMyNzk5ODE2NjQ1NTAwMA", + "digest": "sha256:f978c768693ad61729b2179715d6777637b40a012f5368a6fbf5d0d9e4482cf5" + }, + { + "id": "93e97c0b-d906-5b8b-bd28-7887fded5e23", + "version": "MTc1NjMyODE2ODgyOTk3NTAwMA", + "digest": "sha256:bab8796774b76bbec9c1f256fcf48e93eae8f52074b08908236593c3063761a7" + }, + { + "id": "93f236dc-fabc-52e3-ae9f-51d95437ca21", + "version": "MTc1NjMyODAyNjc4NjQ1NjAwMA", + "digest": "sha256:9dd1aaf848325198979a9881a35cb74115d9c13624b42910a2dd75d62253aa6d" + }, + { + "id": "940bd929-1eb1-5b39-a04a-953e555999c5", + "version": "MTc1NjMyODAyMTMwMzY4NjAwMA", + "digest": "sha256:c0c908182036886a629a9fa915c4a8968734dcc0d302708acb4f0e33a8020b6a" + }, + { + "id": "9428fbde-f647-5e0a-8882-79d5c418b3e3", + "version": "MTc1NjMyODAyNzg4MjQ4NjAwMA", + "digest": "sha256:83bd6b9f749d8830ff1ff5dad819e78235c085da657517d82b96eded6bbfed77" + }, + { + "id": "943e17da-991e-5791-9ebf-4f4f071cdc97", + "version": "MTc1NjMyODE3MDgxMzEzMTAwMA", + "digest": "sha256:35887d0b0b2a5b7305ac5f3973211a2ad2b9ca0405e007622c06a1aec1563a55" + }, + { + "id": "944dcb9e-9146-569a-bd13-960a0e8dba56", + "version": "MTc1NjMyODA4OTM0MTAzMzAwMA", + "digest": "sha256:b76cd5228509320d7c2a5b876e093c2196280f1ed6342e8b0ae37e4e235678a8" + }, + { + "id": "9457930d-9a52-5b1b-881a-4ab698f52cf0", + "version": "MTc1NjMyODE3NDg0MTY2NzAwMA", + "digest": "sha256:9f4b5d2eee74f5dc1a2305cfa67c4e896c8e2abc26609239d905fb82c62d2115" + }, + { + "id": "9480efb3-7b14-509d-b274-55287ce15f0d", + "version": "MTc1NjMyODAzODc2NDI2NDAwMA", + "digest": "sha256:c1ae56948c9cd77ef505d4937fccd62ff16cef52550c4ecd72cb77a0c42e5320" + }, + { + "id": "948643ad-6c9f-51d2-a3c3-af8ae6922a5d", + "version": "MTc1NjMyODAzODc0Nzk5MDAwMA", + "digest": "sha256:05279d204cad25cbccbef2505aea3c170328b6990a46b400b1bdcd4c04166b27" + }, + { + "id": "94bdfdb4-f0ab-598c-836b-9c5986d5ea1c", + "version": "MTc1NjMyODE2ODk1ODYzNzAwMA", + "digest": "sha256:febd8a3bd5c0a3ad7b1a14af16d59b2abad148a542f76fee9651296f5a0ee037" + }, + { + "id": "94c92cad-414e-5a87-8da5-c62dd9d74fd0", + "version": "MTc1NjMyODAzOTc1Njg3NDAwMA", + "digest": "sha256:8db0aaae256a8032d9a3453edbba0e271b9b20b260bf52c98766253bb52a3ef4" + }, + { + "id": "94cd4d20-8339-5d67-a933-2e02a2c2d5d5", + "version": "MTc1NjMyODE2NjQ1OTM2MTAwMA", + "digest": "sha256:ba7bc3129f571563f1bf8dd843957c9659c50614b3a8e0f99ca6c7c9534f353f" + }, + { + "id": "94dc9c09-d3f2-5f85-b4ce-1002ce344443", + "version": "MTc1NjMyODAyMDU0NjU4NTAwMA", + "digest": "sha256:3cfead88e02616489cf1cd065d433c2f5e8d9e8ff17d6031180bc81c0dd0f1a8" + }, + { + "id": "94fc98cf-b5b0-5abf-b2cf-5e35e6e0b943", + "version": "MTc1NjMyODAzODY4MTYwNzAwMA", + "digest": "sha256:4e79dca22e2dd176fd4431eb5ab038345b98e9232176335eb0415c3f4bdb02e8" + }, + { + "id": "950a4969-2fd3-5711-a7f5-fa798d5aec13", + "version": "MTc1NjMyODEwMTAwNTEyMzAwMA", + "digest": "sha256:312ec2cac06e550a1eb33b504083921eed80f090fa37d763433647581d140b96" + }, + { + "id": "9527104e-b5f3-5304-993c-5651be6046f6", + "version": "MTc1NjMyODEzOTI2NTY3MjAwMA", + "digest": "sha256:7c00adb27f39ae32723cd27d6cbd81304f7eb159c91848c9a3a60d5960f3f8ed" + }, + { + "id": "9537e286-b59d-5b2d-bbbe-1cbc393983db", + "version": "MTc1NjMyODE3NDk5MTU2ODAwMA", + "digest": "sha256:302e0fb80a096352de297910d4f7a5a600d409c0cfd35f0ea0c5c8adcc23fc5f" + }, + { + "id": "9546ad74-ba50-558a-9269-93ef79d0f377", + "version": "MTc1NjMyODA4OTE3Njg0NDAwMA", + "digest": "sha256:f37487f69fda16bf3413ad98f2042520fccb78b8ed38f34eb5568370ac3e8a3c" + }, + { + "id": "95923001-ac6c-5123-a29f-ac68f233db9a", + "version": "MTc1NjMyODA0NDcxNjIxNTAwMA", + "digest": "sha256:276b8cc59f7a2780ac5c341890e5815238d9ef543883dc86f847a1421903de55" + }, + { + "id": "95959933-e357-5fea-ab89-421271e5d7e8", + "version": "MTc1NjMyODEyNjUzMjQ2MTAwMA", + "digest": "sha256:cbe1a0e90a78f56ddba7735728140e1bdfb301fe7652290ee7f95417f7c49ace" + }, + { + "id": "959f4b4a-623f-5f24-8c07-1e8528571a7e", + "version": "MTc1NjMyODAxNjU2OTQ5MTAwMA", + "digest": "sha256:ce4cbd7c9fa817e87db326e7fae97d3b422a82c54871837c468ee20dc7e30df3" + }, + { + "id": "95a24435-f48b-5572-ace6-3bd78011e7d4", + "version": "MTc1NjMyODE2NzQxOTA0MTAwMA", + "digest": "sha256:21e5f721a915e81f16dd3dca0173bff90c7a34f6b759315ea0fad108ec7659e1" + }, + { + "id": "95d72452-3ecc-57c2-9d23-95294dfa9153", + "version": "MTc1NjMyODE3NDczNDg2NTAwMA", + "digest": "sha256:bc7ebaf0bd84b7251f9fbf56648823f98da55217057262846b7ff670d8e1b902" + }, + { + "id": "95f32427-05c8-50e4-a1d9-50cafde1054d", + "version": "MTc1NjMyODAxMzc4MjI4OTAwMA", + "digest": "sha256:f834c2e107d0a6aa916f93489b48e1b5cf7af9a7e3383c9141d6387147ad8924" + }, + { + "id": "95f8b0d0-ccba-5448-b554-7556838107ca", + "version": "MTc1NjMyODAyNTc2NzI4NTAwMA", + "digest": "sha256:80f26a21eeeabd1074f6f483629deb1c7ba2f4c551e06adba5fa887f9387b393" + }, + { + "id": "95fb21f0-e229-5c09-893e-13e87cc563d2", + "version": "MTc1NjMyODAyNTg5Mzc1NjAwMA", + "digest": "sha256:292bcd9a17b809534a7e6e71b653de99dac24574d260471873afcfb0095ecfa2" + }, + { + "id": "962b1b94-2bf3-5c16-95b0-c9f01a8352ad", + "version": "MTc1NjMyODE3MDQwMTQ2NzAwMA", + "digest": "sha256:10da4181f6a29c370ba4e8c090ed688aa737daa59c090dba05ba6617e3d195a3" + }, + { + "id": "962b698b-fdac-5323-9d48-4dd8dfa4d52b", + "version": "MTc1NjMyODAyNTc2ODgwMjAwMA", + "digest": "sha256:fba005b23e9777a312724ead71eeb16e690e567220d7fec618df8015ce4b3552" + }, + { + "id": "96360c25-d592-588d-9837-4e78d91159c9", + "version": "MTc1NjMyODA4Nzc0MDI2MDAwMA", + "digest": "sha256:b506488e0a24aa5e053af3e43088362f26e6b9162c3ea4294befa708d61cbc4a" + }, + { + "id": "963e1186-2b76-5a6d-87e9-4b96adb0e26b", + "version": "MTc1NjMyODAwMzkxMjU4ODAwMA", + "digest": "sha256:33c6a3da36778ded49b10180ab9cda47c4bb73cc04193e3889509980c587d9ed" + }, + { + "id": "968a1ba3-cffc-5d44-bb93-201f915a2874", + "version": "MTc1NjMyODAxNTI2MDg1MjAwMA", + "digest": "sha256:414671b24dca63c12defc93ae5e0038e5b64caae79c6be4a1ea38dde51af245d" + }, + { + "id": "968cf148-b4c5-5f7e-98c0-f8f6c9a1d777", + "version": "MTc1NjMyODEwMjQ3MTA0MDAwMA", + "digest": "sha256:f7e66ba0d234294f5bee0e39d19cb2c893bf30425e01413e2239c2a7c48ea963" + }, + { + "id": "96a17a43-7673-52d3-b2c0-14ef4264e756", + "version": "MTc1NjMyODA5NDQ4MTAyNzAwMA", + "digest": "sha256:a4afbd355c83aad9642110f448ceffe7f9bb237953766fc56fc87cbfcf1f82bc" + }, + { + "id": "96d40177-4d7f-5e47-be5f-cd0dcf21d3f7", + "version": "MTc1NjMyODA3NDMyMTkyNDAwMA", + "digest": "sha256:7255385f8fd1fe2703386b40182c9118184c106c63d99e5d14f357d69cd4d074" + }, + { + "id": "96f1cfa2-efb1-5a74-a6c7-37e28e3612f1", + "version": "MTc1NjMyODA0MjQ1MTk0MTAwMA", + "digest": "sha256:13110a2a372c94b4576872eba19c9bdf2ccd07caa6dd2aef9b977a1a970d9e77" + }, + { + "id": "96fd5a0b-ecf2-5b7f-add7-2042a039db06", + "version": "MTc1NjMyODAxMTkxNDMzNDAwMA", + "digest": "sha256:19e89820220b74585f4c960b3bbeb5d8d1203c9c7dadb4a8f44b52d3cc5a01c8" + }, + { + "id": "97019b63-eebe-58ce-98e4-6fb3fcc4974f", + "version": "MTc1NjMyODAzOTY5MTIwNjAwMA", + "digest": "sha256:6c919917feaef075128910c334a7d1d99ac64cbdac716cbd68d947b2886cde32" + }, + { + "id": "971253ed-ae81-5cc0-8455-6a2cc1afac05", + "version": "MTc1NjMyODAwNDAyNDQ0MTAwMA", + "digest": "sha256:78ca7b9a7641dfd5957e37b821c90bf9cebf2cc03dd2775c20a9d5f6ba912b17" + }, + { + "id": "9712dd8d-98df-5106-a958-46f39e111cec", + "version": "MTc1NjMyODE3MDk0NTM0OTAwMA", + "digest": "sha256:1b2468b1b54e8ff2fabddea8852ceaf5ad6c58a235e3fedeac2a9dd0c1d6f05c" + }, + { + "id": "9713fe71-e431-58a0-be2c-25a98022985e", + "version": "MTc1NjMyODAxOTMyOTQxNTAwMA", + "digest": "sha256:21fc8cd6d538b8dfcc0f4ce2dbb663dab3e0b384f7c636c924af30a0eb8b28ed" + }, + { + "id": "9723c7b9-b424-59c7-b75b-a528b0710257", + "version": "MTc1NjMyODAyMTI4NjM5NDAwMA", + "digest": "sha256:f7963ac7617386adc3c72c17c337d8eab26ae4f99604739dea8f1ef32c09a52d" + }, + { + "id": "972bcfe4-d795-5f0b-b6fc-ec577237406e", + "version": "MTc1NjMyODA2MjU0OTMwNjAwMA", + "digest": "sha256:921339f97d0fbd72c339848765e5c49e8b8e7c73f7a4d7895ca048daf7778e2d" + }, + { + "id": "97444518-73f2-5a24-b0f3-72f2f496b9f2", + "version": "MTc1NjMyODAyMjE1ODY4MDAwMA", + "digest": "sha256:4a8bb471f0b3e3964e6c08b94f00b4d50a7a7798460ab58326688f4fcd1457d0" + }, + { + "id": "974a1669-1342-50be-abcc-046af44a28b0", + "version": "MTc1NjMyODE0NzQ4MTM1NDAwMA", + "digest": "sha256:7973ed9d5fe470598c993d706b7d11ee6af35449f725fd36c5ccadfcbe5ec839" + }, + { + "id": "975a052f-1c50-5f21-8dd2-377d8c082b9d", + "version": "MTc1NjMyODA3NDEyNjQ2OTAwMA", + "digest": "sha256:5d5b47b5ec4b0ae9e7f70a5162b78821157d192bfcf06a0dadfa4c51a307ddbb" + }, + { + "id": "9765cb62-d810-5be9-a16c-8210a11bd8bb", + "version": "MTc1NjMyODExOTI0ODAxMTAwMA", + "digest": "sha256:b8a69a1bf377c08a1bb1d763c085e6a4e19bfb9270fb86c7a67c438a988ec145" + }, + { + "id": "97b6efdf-a4a1-51ea-a4cb-ce574db57cc9", + "version": "MTc1NjMyODE1OTE1MTM0MTAwMA", + "digest": "sha256:e55d3a548b41557a79e9fec876b31349d80af220e3984044956573166441a464" + }, + { + "id": "97be1596-023f-519f-beaa-42775501e054", + "version": "MTc1NjMyODA4ODUyMDI1MjAwMA", + "digest": "sha256:03b11134450f61d79e5f60aef5a7f1f55c1fc34c19d9251d30c421d52ece4949" + }, + { + "id": "97c23752-7503-555d-a7ce-475a4dc237e8", + "version": "MTc1NjMyODA4NzczOTYwMzAwMA", + "digest": "sha256:b068ff81ebca28afb42d4c0ad01fae115e029fe49c18e5d4938b1ef5b6e2e480" + }, + { + "id": "97dbd252-6ee8-551a-810a-5f7516416b90", + "version": "MTc1NjMyODEyMDQxNTI5NzAwMA", + "digest": "sha256:6c0e8052a0eb5966374bbc22deb545205d81616bdd567d7e09edcfd7cb9c859f" + }, + { + "id": "982714e2-1748-5540-bbfe-6b3c413d44e0", + "version": "MTc1NjMyODAxMDUwMzI3MDAwMA", + "digest": "sha256:1bec2e261b72c729b069faf851b2d5cbce9720c83b9c628c84479cb0666c02ac" + }, + { + "id": "983d4b7a-bc9f-51dd-bf99-d5c6d4e7ecc9", + "version": "MTc1NjMyNzk5NTYyMzI4NDAwMA", + "digest": "sha256:c1f7050020c654ed2812591309eb41029758941a01aa5885da0b98ee90653006" + }, + { + "id": "9844c0ad-f21d-548a-8692-5ae2fdc98238", + "version": "MTc1NjMyODA3MzA1ODMxNDAwMA", + "digest": "sha256:dcdab8967b47bddcf5f614b574f10bcd5ca94eb2e8f463f624fb3f1707e29095" + }, + { + "id": "984cc60d-306d-5854-8095-a294b16919fb", + "version": "MTc1NjMyODAyMjA3OTc0OTAwMA", + "digest": "sha256:93e64a0c006ee013274b29e4c4a2887b485d2b005d4f068b07898572d415f0d7" + }, + { + "id": "98590caa-698b-532c-aaad-524554bbfd30", + "version": "MTc1NjMyODAyNzg2MTU1OTAwMA", + "digest": "sha256:1bf0a747d89e0cc3ee2b23c9fc328b7af388b75be6d415a30227472db24f4509" + }, + { + "id": "9869cffd-b3ed-5bf0-81fa-792ada019c06", + "version": "MTc1NjMyODE2ODg1MzM3NjAwMA", + "digest": "sha256:206a2d1ef17ff2e10374ad6d1ef80c1f188da6817f46f37e1b051b9ab6182bee" + }, + { + "id": "98994259-de7b-56ba-896f-6a5a1feb5d33", + "version": "MTc1NjMyODEwMTE1MjQ5MDAwMA", + "digest": "sha256:b556047bf6a12e40856f65820f5305479176d62ee92ee001dd4f62b5d22d9abc" + }, + { + "id": "989b070c-c1c8-5a46-9dc4-705d4dfa3761", + "version": "MTc1NjMyODAyNTYxODI5NjAwMA", + "digest": "sha256:df4b75873f6104c50fc394d0e412c1da87bc5900570b89c43113f0f27c63581c" + }, + { + "id": "98b75dd7-60ed-560b-b673-82418602ae4a", + "version": "MTc1NjMyODE3NDc2MjU0NzAwMA", + "digest": "sha256:a10b4460bf6a7b70f7bb871bb1b5da7d5f2029719c0b62421f7d70ab7c44c03f" + }, + { + "id": "9900dc47-ec52-5818-9eef-07e11c924e10", + "version": "MTc1NjMyODA3MzEzOTQxNjAwMA", + "digest": "sha256:d535fea9211efccf4e6e8cecc39936220ee44d9ac93284586cd52dc6b07185f8" + }, + { + "id": "9924f8dd-025c-5be1-8863-f83ff95579d7", + "version": "MTc1NjMyODA2NzA0NzQ3NzAwMA", + "digest": "sha256:97eb42016d02774f6d52ceaa218dee16651872662f71957fcc5cd7686af545ab" + }, + { + "id": "9944beba-b7f3-5831-907e-28f5f78f1d9a", + "version": "MTc1NjMyODE2MzI0NDkxODAwMA", + "digest": "sha256:fec2ea3dc36562880ad7e7442e6dc1acaa74e3d001156e716b94135216c54ba3" + }, + { + "id": "994553c8-f895-52d8-87c3-5aa87a8d34c2", + "version": "MTc1NjMyODExOTIxNDY4MDAwMA", + "digest": "sha256:bf5ec2065b84fe5b3c331ff11f5c3065f05dd5b7e8954fc4850bed33b590dc4b" + }, + { + "id": "994d0c6d-6557-5574-b2eb-2a914d277cee", + "version": "MTc1NjMyODAxMTk0MTg5ODAwMA", + "digest": "sha256:9d19ac2c59856f1704f0bc2dfda55cae11af852bd598c8a057d89a6548524865" + }, + { + "id": "9960b636-13cc-5d85-9f24-0c1c7de6f9d5", + "version": "MTc1NjMyODExOTc1Nzk5MjAwMA", + "digest": "sha256:e3d66fd931249af16c2be178ffc5e44fc1c3d9f35ca8cda061a45299789d23c3" + }, + { + "id": "9964ad55-cc00-585a-a312-8a0e27927097", + "version": "MTc1NjMyODAyNTc2OTU0MjAwMA", + "digest": "sha256:343e90c23a0b57137b31a69571e984c44a71aa6f73eea7868d888fe69e9eb6db" + }, + { + "id": "9966db1d-c9d5-5483-bc33-202177cfcf13", + "version": "MTc1NjMyODEzNjI2NjcxNTAwMA", + "digest": "sha256:f4316c16fd6d315521a8445937a913aeff323ce5c165cf3c6c2f87a4842b7c4f" + }, + { + "id": "996c2c73-5a67-54f8-83f8-e9c5908f1688", + "version": "MTc1NjMyODEyMDM3MjAyNDAwMA", + "digest": "sha256:23e3b5361892fd1e5f0dfbbaef25a720a418725b767561b8f11318db96f853fa" + }, + { + "id": "996d8ca6-1506-5e23-b2e7-78db7fd14923", + "version": "MTc1NjMyODE2ODk5OTMwNzAwMA", + "digest": "sha256:fe2e2fe455fd7f542bf788e48cac52d582e2ee51e914acb600a61f0df7e743ea" + }, + { + "id": "9973b817-ce84-5776-b0aa-67e0438ce2a5", + "version": "MTc1NjMyODA4ODUyMTQwNjAwMA", + "digest": "sha256:9a60ac99b392b9c3768569a6fb4e2b0f3e35053d2c6e653e3170baa8038127ab" + }, + { + "id": "998eaf4b-743f-52c2-a54d-1e7db36230e4", + "version": "MTc1NjMyODAxMzg2NTU5ODAwMA", + "digest": "sha256:27dbd1e3198c69e5d6a2e14780162ce51ca69051d38b979ac4811fcebc161d77" + }, + { + "id": "999e6c40-7818-56dd-a1c7-6e7f65ea48b4", + "version": "MTc1NjMyODAxMTY2MDIyODAwMA", + "digest": "sha256:6cf57b9c4d199e4e5ebb4d955281e49ecce8fd59f64c086426c65d6570e3266c" + }, + { + "id": "99a5b591-c505-5203-bf2f-3218e2b004e4", + "version": "MTc1NjMyODA1NDEyNDI0MjAwMA", + "digest": "sha256:d8acbbd87a9cb75cd877c0c798394cda6485941f3ff908efe33d26bb5b134312" + }, + { + "id": "99f07372-4a85-5606-93b0-3e6845b35452", + "version": "MTc1NjMyODAyNTkxNzIxODAwMA", + "digest": "sha256:896169664cd3df9eb0c739f8733857b86235603439040363782f75a3749b7c3e" + }, + { + "id": "99fea0aa-51eb-5899-a76b-5bba8cef1a22", + "version": "MTc1NjMyODE2MzQzNzM0MjAwMA", + "digest": "sha256:51622cca9bd0edae6e67f8ce39eb35b72dc508b2174623b4a93d9a6ec4a85bf6" + }, + { + "id": "9a158e28-87a8-5a04-a466-86cdea47887c", + "version": "MTc1NjMyODEzMTEzNDY3MDAwMA", + "digest": "sha256:5737ec471153fb619054ea55bb6f6ae879bfa7377c64d5e3382370f1a4e8b3e4" + }, + { + "id": "9a1fa603-b17e-5d0e-9938-e51f651352c8", + "version": "MTc1NjMyODAzNTk1NTA4OTAwMA", + "digest": "sha256:faeec3930ec38edbccf786228aa1977533283ad6d397fe21ce3d89ded2c20c3f" + }, + { + "id": "9a2e2e97-4850-529b-9266-e3d2383ddd86", + "version": "MTc1NjMyODAwNTA3Njg5NjAwMA", + "digest": "sha256:3675fb3db886a0a31fb8407dccccf16b9f372db6d39fc915aae9d416aefebb98" + }, + { + "id": "9a2ffc82-8a29-5423-938a-cd803ac72793", + "version": "MTc1NjMyODExMzU0ODE2NDAwMA", + "digest": "sha256:8a2205fd48b88af1a125336d888df1174ded56722070230fd19027a27a97d53f" + }, + { + "id": "9a35b483-9113-5d8d-b5d8-0e6dd3038095", + "version": "MTc1NjMyODEzMTE5NTY0OTAwMA", + "digest": "sha256:92e00f7d82f5fbdb28e1d49e2be0187458404fdb8928e90dc916ec340c34595a" + }, + { + "id": "9a4b9376-a540-5348-a8a0-b352c699f7ea", + "version": "MTc1NjMyODAxMTkyMTQ2MzAwMA", + "digest": "sha256:15ffb1b1399ad6bd9dfc37584a32da30cc313d06f28be88c5d98b85bcac7c012" + }, + { + "id": "9a51456b-4028-5697-b22b-401353ef667e", + "version": "MTc1NjMyODAxMTkyOTkyOTAwMA", + "digest": "sha256:9c71386c7339af5de9555b270de782284ef3e6bf80d85f3aab5058f1936936c0" + }, + { + "id": "9a610a85-2733-5c50-b15b-e410b552f2b4", + "version": "MTc1NjMyODEzNzUxNjczNDAwMA", + "digest": "sha256:233e9b2547f430587827db7f8dbed3d08bfbc0d569bd5152e7d90e2c336ad18e" + }, + { + "id": "9a614476-e4a4-51d1-8de1-1a75467cdb15", + "version": "MTc1NjMyODAxNTU0NjUxNDAwMA", + "digest": "sha256:bc4efce09847ba5cda46573d7cc4feedd8e5ef260df391d60360bcd88ebc364f" + }, + { + "id": "9a630860-7042-5f4c-bb27-0138ec975195", + "version": "MTc1NjMyODExOTU3NzQ5OTAwMA", + "digest": "sha256:8e66a3435b6fd9a166d5e66098426750f4039b2daaf14f8c28469c41b1136d35" + }, + { + "id": "9a69955b-29db-582e-a785-bf6488512c29", + "version": "MTc1NjMyODAyMzQ4ODI4ODAwMA", + "digest": "sha256:d91b58c16c6664a3d0160211c246b7ed377a1b2e09968f363315ff4f0afef2e7" + }, + { + "id": "9a758587-b013-55b0-9aea-8002cb9380fb", + "version": "MTc1NjMyODAyNTg5MjYyNzAwMA", + "digest": "sha256:f9fb2944285d53625f9920ceefaffb63ab0c179e8fe60383fb969a29bd5789b9" + }, + { + "id": "9a91c8b8-0c6d-551e-9eba-19e0067fcdb1", + "version": "MTc1NjMyODEzNzY2ODQ4NTAwMA", + "digest": "sha256:95fb3fa128fc24ba48b85b839911d11683e6e808f53464148267ea47a2d489e9" + }, + { + "id": "9ab173da-398e-5c1f-a28a-1bee1976ba5e", + "version": "MTc1NjMyODE2Njc2MzQ4MDAwMA", + "digest": "sha256:7903eb0992a28c904a7d255e608378cc8908c7d49c35963b8d74c68090541efe" + }, + { + "id": "9abd4d22-9a5c-552c-82f1-c218cedf8ff9", + "version": "MTc1NjMyODAzNDc0Njc1NTAwMA", + "digest": "sha256:89b1d0be20b5318b28d2bc7ca56273be17a2e244b562e67f0dae5a1bfcaf6fe5" + }, + { + "id": "9ade4eb3-795d-5f19-9c8a-ae0205972cbe", + "version": "MTc1NjMyODAxNTQ2NDM5MTAwMA", + "digest": "sha256:e948072abbf7d49ef0357573b69985c2ee19fd164ae57857625f71f34366aa06" + }, + { + "id": "9af4ae9a-6543-5f8c-ae24-4274be16747b", + "version": "MTc1NjMyODE3NTAxOTIwMDAwMA", + "digest": "sha256:28051b1cc91624c38dca1a807dce1d24360e39f16f35202f37983a1572bc3780" + }, + { + "id": "9b05c8fb-f950-553e-9861-db548a4d7c5d", + "version": "MTc1NjMyODA0NTg2NDAxNjAwMA", + "digest": "sha256:ddcf515c3e60955111663388a5a46426bd53553105b1299230ccbb459574f1ed" + }, + { + "id": "9b10639b-00a7-5c1c-9117-c3eb2860419f", + "version": "MTc1NjMyODE0NzY0Nzg4NjAwMA", + "digest": "sha256:97ee82d38a3fd304f3b02c4556b82b92144b64b41ab5311f2b3a433db5d7f3a8" + }, + { + "id": "9b246c96-9978-5cc5-af9b-00679b45ffaa", + "version": "MTc1NjMyODAzOTczNzA5MTAwMA", + "digest": "sha256:900f07607822f8dd895aefbead666e19e3d011c981aa6d25f897c3f60558a4ab" + }, + { + "id": "9b3df855-bd17-5ecd-9fd9-a361660d5841", + "version": "MTc1NjMyODA0NTczNDE2MjAwMA", + "digest": "sha256:6a52abd78f106e444c2a1f1808f2eb9c4701b18343c449254c302fc63ca1ada6" + }, + { + "id": "9b56775f-ea95-5235-9e69-8a1d19589655", + "version": "MTc1NjMyODE2NzcxMDQ0NjAwMA", + "digest": "sha256:ee63276951d0a6ac967df41c59eb84b0deef716025d0e9d14129e500deedfee6" + }, + { + "id": "9b6824ed-a122-5fb9-913d-b72826349bca", + "version": "MTc1NjMyODA1MTIwNjU2OTAwMA", + "digest": "sha256:3d6585a70e7101c17e089bd1936b9e964788e803942d79ef30a97e2dac7c381f" + }, + { + "id": "9b6c2542-dcb8-52b5-b800-137211ea9120", + "version": "MTc1NjMyODEwOTIzNTQ3OTAwMA", + "digest": "sha256:c626d84d15a2bce735033dcf7ae364c91efdde65c13288d1800c86795022c1ce" + }, + { + "id": "9b6fa4c7-f978-555a-869c-ddfa29ad7813", + "version": "MTc1NjMyODAyMzQyOTcyNTAwMA", + "digest": "sha256:8ac9ff55af1b3805cee4b565462d6fbabd272a645a352eb3f826c18f04328cae" + }, + { + "id": "9b888cbd-9909-58bc-8943-fc3aa94e8dfa", + "version": "MTc1NjMyNzk5MDQ1Mjg1NDAwMA", + "digest": "sha256:98cafa0bf0688197d3da88ce6452c00d1770c428c5851b7999a43025e262e747" + }, + { + "id": "9b8e7f4e-14c0-57c4-80c8-3e3a818f936d", + "version": "MTc1NjMyODA2NzE1MTU0MzAwMA", + "digest": "sha256:90595f45ac709f3c6ecd1200fb51ad5d7cebb6049c9fcaea108267aae2bf03ff" + }, + { + "id": "9b945051-7938-5ea6-9179-aa99b0d9cde9", + "version": "MTc1NjMyODAyNjg3MjM1NzAwMA", + "digest": "sha256:b0887962bfa3a53984ada3ae1de439ab38fddfe6c6f24916462a61a085f8441e" + }, + { + "id": "9bb0df39-d70e-5579-ad25-3477ae39ce36", + "version": "MTc1NjMyODExOTcwNTEzMTAwMA", + "digest": "sha256:c8112008f28e101df6b2fcdb9630952ba01abcbd78da8b3efa6ceaecc30c38fe" + }, + { + "id": "9bc06ab4-3270-58f9-a8fe-3ec483c9e018", + "version": "MTc1NjMyODE2MzMzNjkzODAwMA", + "digest": "sha256:14c66b79284aa3aaab76ab49aa00129eb2d024067b3514dbf0b365ac784de567" + }, + { + "id": "9bc96ae2-ecce-5061-b689-e2e334180244", + "version": "MTc1NjMyODAyMjE0NTEwNDAwMA", + "digest": "sha256:0116fb4ccd3ec3adf4ca3357510f62942aef6207cf4197bef2827c8cc7ad9b5f" + }, + { + "id": "9bcdc3ef-8e6c-5c5e-b997-53e7bfbbb62d", + "version": "MTc1NjMyODE1OTA2ODE1MDAwMA", + "digest": "sha256:065a21513c7a5c5540212524d141e765143dd6fd629776e93c6476ac09cb2154" + }, + { + "id": "9c125ae3-7510-5c7d-bc46-04250e3e4718", + "version": "MTc1NjMyODAxMzg3MjQxOTAwMA", + "digest": "sha256:e75cfe8be169faac92e6a02f77d872402570270489b88f65232136ae9f7c1a79" + }, + { + "id": "9c16e696-ae06-588a-bc30-e7949ef6b80b", + "version": "MTc1NjMyODEzNzc0Mzk3NjAwMA", + "digest": "sha256:b3761eef81998f83e91a1845462d316251c86780ead19e698b1c5ca31b05cd26" + }, + { + "id": "9c1b0aff-e690-59f7-bfbc-c0488a69a662", + "version": "MTc1NjMyODAzODg0MTYzODAwMA", + "digest": "sha256:30b25f10ea5d2a2b81c01e562e359408ce2b56bcaf7c0c5ae4815af924cb2220" + }, + { + "id": "9c26caf9-cfea-5855-92fd-9085d68edcda", + "version": "MTc1NjMyODAzNTkyNTk4OTAwMA", + "digest": "sha256:e1eeb70227cf333add8dfcdf9ee9c5b2359adad644c11aa54180519e0cfbfe0c" + }, + { + "id": "9c421125-e1ce-5914-9693-2230cd72bcda", + "version": "MTc1NjMyODAxMTEzMDI0OTAwMA", + "digest": "sha256:89cf5e14b2fd2ecfc12f732c41b0c7cb983bed8cadc83788bfb1bd5fe42c8923" + }, + { + "id": "9c436095-0088-5554-bb75-2b86f338480c", + "version": "MTc1NjMyODA1NTY3NzU4OTAwMA", + "digest": "sha256:148966d9fb2ecf7ae8c7eabe3f407d8e4a08e30a23ee281a1b74d622dce0ab4b" + }, + { + "id": "9c54b577-afe8-567a-8a5f-296d2daff7a6", + "version": "MTc1NjMyODAzMDUzOTkwODAwMA", + "digest": "sha256:bac40b41b0ec08863a0fdb56b92fd57f1aacc2e3cb302d57e1d8cda230c96434" + }, + { + "id": "9c5693ae-7d25-5b46-8c76-bc2c5d90f7e1", + "version": "MTc1NjMyODAxMzg3NDcwODAwMA", + "digest": "sha256:11a8cfea2a36e1f6b64baf382332d0b8be331012f694acb47858d07b14862010" + }, + { + "id": "9c5c037b-100e-5af0-8064-1e8ee781bf35", + "version": "MTc1NjMyODEwNTM2MDA0MTAwMA", + "digest": "sha256:28dee6575ea1187d50a8a7315b4549f6906dfe3bc8f1be5e042642d67278accb" + }, + { + "id": "9c6eba37-75e1-5dfd-9045-ddf6cab77d07", + "version": "MTc1NjMyODAxNTQ5MDk5NDAwMA", + "digest": "sha256:45d04eea226496af9f0dd7f2de1e9df70d8188ad7640a9cf2a11c4f327ba97c6" + }, + { + "id": "9c8ed563-df6d-5641-b6fb-0c8bcc4d2b9d", + "version": "MTc1NjMyODA2MjgwMTg5OTAwMA", + "digest": "sha256:507851303fffec5236fbe83a73f92804c3e854ee51a15493dbd64f4233767991" + }, + { + "id": "9c9bf1dc-ad50-5715-b061-0c04ee63f2fa", + "version": "MTc1NjMyNzk5NTYzMzA2MDAwMA", + "digest": "sha256:4ea34b4fbc7ac20252916ae10505260721cc206b61dfc76d21dbf5d6786419c3" + }, + { + "id": "9ccd23f5-a654-5f52-98f2-37407427efeb", + "version": "MTc1NjMyODA4OTE4NzI3NzAwMA", + "digest": "sha256:155a4035474fcd73cc3ba76cdd89440d44ede465b000c45aaef0b9d2f6e050c9" + }, + { + "id": "9cd55ed8-681b-5c29-a014-d5875ed302ae", + "version": "MTc1NjMyNzk5NTY5NTY0MzAwMA", + "digest": "sha256:49ebc2b225e1c958767b96de2576997d88f3285441caf0f5ad737003085c7865" + }, + { + "id": "9cfb1e74-c18d-50b3-aa4f-9708347c88b3", + "version": "MTc1NjMyODAxMTg5Nzc1NDAwMA", + "digest": "sha256:c3f0a46d82ba985416d67902f95234a361dde3bc795ffe4c35b90990f179a434" + }, + { + "id": "9cff1e00-0b36-5e85-80c5-2122b0bd7f5b", + "version": "MTc1NjMyODEzODI3OTQ5NjAwMA", + "digest": "sha256:25d2fa90309c0c103b9887d61227a77f97f820e98b3184683294850e761f53e4" + }, + { + "id": "9d43c8f7-4ca6-5ddd-8fae-c794cbe72117", + "version": "MTc1NjMyODEwOTI3NjY2NjAwMA", + "digest": "sha256:96c0b16a29b14f2d06eda3e182cc90ebff505e2655179c98ebb516086adfbb21" + }, + { + "id": "9d6030e8-79bd-5aaa-b092-612e0ab9c6b7", + "version": "MTc1NjMyODA0MjUyMDA1NDAwMA", + "digest": "sha256:9b68a542bf030f1e32c25a04e7cb54cfab2364042e5eb4110c2634ee4baf9919" + }, + { + "id": "9d7e09c5-d3c9-5b9b-bbcc-2fa2af5c9400", + "version": "MTc1NjMyODAxMTYyNzgwNjAwMA", + "digest": "sha256:f4ad9f14bd4b41738d341a1fd3f17f0f7dad4def90f5d7902bbf3b978cfec639" + }, + { + "id": "9d8fab2a-31d2-59b6-afda-29e0049f4069", + "version": "MTc1NjMyODExOTIwMjcwMjAwMA", + "digest": "sha256:3d55eb524ee34f009125867fbbb2aef2436246a48c7c09a69fd9b143a7c11e3e" + }, + { + "id": "9db03a19-4c33-5e38-ab03-a951b2537f72", + "version": "MTc1NjMyODEzNzEwMzA0OTAwMA", + "digest": "sha256:33d1b4e355f6a5119af7b79ecf59e62b2ef7b502195ace500d70602c7ff9ea43" + }, + { + "id": "9dc483e3-8d90-5b8c-bfed-66f27b2eb2bc", + "version": "MTc1NjMyODE0MTQ2NDk5NDAwMA", + "digest": "sha256:639b5db740eb23f148b62f6683addbfd5b244b5a186ec7698038ca95e38d4f7d" + }, + { + "id": "9dca034c-76fd-54ab-b380-8465260fefbb", + "version": "MTc1NjMyODA0MjUwOTk1NDAwMA", + "digest": "sha256:a55abe3e8e6bc24db85e3062184991297952f63c995e113207efd89c3d909104" + }, + { + "id": "9dd42488-4c30-5adf-8304-0da682bc0181", + "version": "MTc1NjMyODAyMzQ1MzU5MzAwMA", + "digest": "sha256:2ca4e48a10bc598b020451e76dc195ee300dda18c0050d814ae90fc979b0595f" + }, + { + "id": "9de54940-9e56-50df-a8d6-b2497c11aa37", + "version": "MTc1NjMyODA5Nzc0NzgxMDAwMA", + "digest": "sha256:53db2d7d7c4e31839a71a2bc906abe83bc70012ebbc2a2e698115d220355ed2f" + }, + { + "id": "9e1f67f4-a40f-5672-9de9-48a6008dd749", + "version": "MTc1NjMyODA0NTg1MzY2ODAwMA", + "digest": "sha256:adca3a0ca93d6926103939163cadc7a683544c911de06db2f359a0ffb3b41754" + }, + { + "id": "9e24e6d1-fff1-5659-9c38-56f7ff2d24c7", + "version": "MTc1NjMyODE1OTExNzE0MjAwMA", + "digest": "sha256:ae2cb5a4bac5af026e4eaf46a9a8ac15ca20d64869813c5ecd7cae4849919f51" + }, + { + "id": "9e26222b-d400-55c7-b9d1-3f31f69cb305", + "version": "MTc1NjMyODA2MjU3MDU3MDAwMA", + "digest": "sha256:cd5d75a05982a6f54eebcca0d1fd481c77bce8a3d1f887387534d577d2464d02" + }, + { + "id": "9e44be5c-dcee-58c8-b3ed-7b7247b36213", + "version": "MTc1NjMyODAxNTQ0MzkxMjAwMA", + "digest": "sha256:d3f42951fc22524d9045357a40cebe4d20df12ea83e54629af2e72a54cea05ea" + }, + { + "id": "9e58ebab-245d-5774-a323-484f994a530d", + "version": "MTc1NjMyODEzODEzOTgwMDAwMA", + "digest": "sha256:4a7cedd0289cdf686d6096aecb76e01dcc02293fba71afde426eb4d7f9446f0f" + }, + { + "id": "9e6ec630-5a21-5b1c-9d44-23e11f81e67b", + "version": "MTc1NjMyODA0MjYwMjU4ODAwMA", + "digest": "sha256:f973efdfc235006aa068c0980fb312427f5bdd419e880ccdd9d4964a56a1fc11" + }, + { + "id": "9e8aed1e-4d73-5d7d-8385-ab86660f23df", + "version": "MTc1NjMyNzk5MDQyMDE1MjAwMA", + "digest": "sha256:fc9560c7fbc70f62a79b8f99f67ae4532bdcf28c00393f6c6361d3ebda43947d" + }, + { + "id": "9ec8529a-3eda-5155-b08b-1197d4805d78", + "version": "MTc1NjMyODE2NjgyNDY2MjAwMA", + "digest": "sha256:3f6307c56ee76dfd6391dddf9f9b8dc9beb2848d3b02ca84401dda25cac7013b" + }, + { + "id": "9ee09be8-506d-509d-bc44-9b6b9a9bd814", + "version": "MTc1NjMyODE1NTA3ODg5NjAwMA", + "digest": "sha256:b15a881c07654b387a49fa5b6d76624437b95636fa1afd7d9809f715f053d538" + }, + { + "id": "9eecb0d9-0ab9-5d6e-b25d-80c5f70510ad", + "version": "MTc1NjMyODAyNjg3MjQwNTAwMA", + "digest": "sha256:21dc5c94817b1ffcff07bd18674375b1f51460d866fa21d8129ac6b6c2fd0a6f" + }, + { + "id": "9ef6b65b-46af-50da-a1c4-af575aedcf73", + "version": "MTc1NjMyODAyNzMzNzk5MTAwMA", + "digest": "sha256:33bfc264d43aa0377e04a42591fb8ca64892b6b4b1547692eaf689b9d84b9f26" + }, + { + "id": "9f04e521-529e-56c6-ab3b-ecb359f8581d", + "version": "MTc1NjMyODAyMjEyMDY5NTAwMA", + "digest": "sha256:524f819878a8cd6ef8580eab29db5f128615275ca8656366c3ca7c3e1bb696ed" + }, + { + "id": "9f177acb-9dae-51ea-9f6c-872640a3ec2c", + "version": "MTc1NjMyODAxOTMzNjAyMzAwMA", + "digest": "sha256:280ccce5be17771184b4bd26bcae3641b113c5c19aef20114d0b24add39a5012" + }, + { + "id": "9f411516-42ad-5591-b6b8-c97c1894fca3", + "version": "MTc1NjMyODE0NzQ1MDI4MzAwMA", + "digest": "sha256:89fd906a895ef49cc9cccbb09fe1a6548eefa678e8d0785315dbdfe1906cb658" + }, + { + "id": "9f4b06bb-3e1d-5f80-8a65-3197776b1074", + "version": "MTc1NjMyODAwNTA3NjgyNzAwMA", + "digest": "sha256:41f4ba77edaa20595f9cbefabddee1c01fe2294ee897232c708685a7133fe6a0" + }, + { + "id": "9f5b4575-b445-520b-9e74-b01151d1cbda", + "version": "MTc1NjMyODEzNzM1OTQyMTAwMA", + "digest": "sha256:d72f8d8f80014905af02778c4d53545c810f51865b75f0e67d91e7e5cddd3710" + }, + { + "id": "9f8224fd-f9d8-554c-b035-855c320fa5e1", + "version": "MTc1NjMyODE1NjM5NTcxOTAwMA", + "digest": "sha256:843af5a4da49223cd3fb45c200a9136b90403e5ec1f79d09e003dfb1d37e2a22" + }, + { + "id": "9fc1a7df-b178-5310-8ffe-cb4f36961e24", + "version": "MTc1NjMyODE3NDgyMzAzMDAwMA", + "digest": "sha256:0e983b10eb0dce3b2ce7cf243a2e2ed4f30117dbb8bd0310bf786c15b7ced63c" + }, + { + "id": "9feb8313-68b1-53f1-a3f0-51376d7c1a3c", + "version": "MTc1NjMyODA3MzI0MjQ4NDAwMA", + "digest": "sha256:2b5464b04acfb4bd5df933cf2374fc145187448b94d09e1379c608e7f997ec99" + }, + { + "id": "9ff1d319-37ec-5e20-8f33-075c93645709", + "version": "MTc1NjMyODAzODcxODk0NTAwMA", + "digest": "sha256:87ebc40e97340e03d41f106c3fcb9f9ee55f81a8759fc3be013708672d3ac154" + }, + { + "id": "a01ec2de-0be2-57c7-8bc8-7edacf01793e", + "version": "MTc1NjMyNzk4ODY1NDI0MTAwMA", + "digest": "sha256:0a1e26665e22d4ece7885962b659ff0b953981af5aa4e3bffc91f456a7c00f6e" + }, + { + "id": "a0382791-77c5-5486-8ee8-bd260cd72823", + "version": "MTc1NjMyODE2Njg0NTE5MDAwMA", + "digest": "sha256:207071fc359a4368aab4323fe5bb07b870a56a22d34a0337bd13037c5c83ac1f" + }, + { + "id": "a040ce9c-b940-58b4-8bec-f3681e3dab5b", + "version": "MTc1NjMyODA3MzI2MzA2MzAwMA", + "digest": "sha256:d8b306633ae63225f8c7e8d24cf52cabc88bfe6229a0033255e30fda52a8cef9" + }, + { + "id": "a042f5b9-a9c8-51e3-a674-0f486478d33a", + "version": "MTc1NjMyNzk5NTI3NTQ2NjAwMA", + "digest": "sha256:fc68e6fb40ac81a26f9c2d5763a59d5cd96f56c1811f151209f8cbab800bdf97" + }, + { + "id": "a0684c30-4626-50a2-93f7-120843b9b2c8", + "version": "MTc1NjMyODA1NDE3ODE1NzAwMA", + "digest": "sha256:480b334662d6297ec9246ac5a1c26bdd5b4be6a1421b7ba5847674cdceb2ab9c" + }, + { + "id": "a072c33a-c959-52b9-bcd4-78c95121118f", + "version": "MTc1NjMyNzk4ODY5NzIzOTAwMA", + "digest": "sha256:0ef7164d3bb7a2f773be33567ce6fd4240ed19a535c01c7efef4f31ac0cea882" + }, + { + "id": "a072f6e7-616c-5ea2-ad37-f3bb3a23f61d", + "version": "MTc1NjMyODE2MDQ0NDA4MDAwMA", + "digest": "sha256:ae25a99c3ecb8798d4ceb002dc2b6b2b437cfb6023542a5d3c5fe6f6dca4449a" + }, + { + "id": "a07919a0-d22a-5903-8802-f4fb52de178b", + "version": "MTc1NjMyODAyMzM2MTQzNTAwMA", + "digest": "sha256:fb8f7d6ba56c925487399eec8ecb8ff2bc488896d4f3dee8b24329884f550a27" + }, + { + "id": "a091f201-07ea-5f03-88b2-f43d28003d5e", + "version": "MTc1NjMyODEwMTE2NjcwNTAwMA", + "digest": "sha256:e1470ef173967130158e35e420840199e7a61762d19652245d7a2422e76531f1" + }, + { + "id": "a093988f-0a34-5c8c-b45e-39e541d4ac48", + "version": "MTc1NjMyODAyNTc2OTg3MjAwMA", + "digest": "sha256:a02fe1966486b656e0796b17e1729807fe79083efbf40690f988731f8ba39887" + }, + { + "id": "a0a57afb-d83e-5d35-9595-6c1c65f5cfbe", + "version": "MTc1NjMyODE0MTQ5ODczMjAwMA", + "digest": "sha256:bc062e41b80978035fdb736247b59b0cfad9fc8de90cb5dda7513ae20e74c029" + }, + { + "id": "a0a8ca3c-b573-57ca-97a9-f97dedbc456e", + "version": "MTc1NjMyNzk5NzExNjkyMzAwMA", + "digest": "sha256:e2fd8f809b2961b5773daeef70852835338bdfb04c04f16bf3e58c3973e3ec6d" + }, + { + "id": "a0b9ed36-8a1e-5c3a-a5ce-e4e989edd629", + "version": "MTc1NjMyNzk5NTM5NDAwMDAwMA", + "digest": "sha256:53cfa43207fa1ec12a87146fa12feea9d7e25cf24f51cc2de4c53427b929e3c2" + }, + { + "id": "a0da42b7-06d1-5241-9140-9413a2996b1f", + "version": "MTc1NjMyODA4OTI4MzE1ODAwMA", + "digest": "sha256:55607381fa32732368ab6c2f4561badbb78b8ab23c45af0022d165554a6fa9e6" + }, + { + "id": "a0e84d8c-db57-59a4-b913-d07c8f7c4836", + "version": "MTc1NjMyODE2Njg4MjM1OTAwMA", + "digest": "sha256:9586ee90a36febea96bd046d4f5f81e5469a84a641c65baa01783f4c5b39d07e" + }, + { + "id": "a0fe24ca-971d-54a1-b191-f6f781624373", + "version": "MTc1NjMyODAxNjUxMzIwMTAwMA", + "digest": "sha256:7aaf4ac3a9b8c013ecbf52bbb09c6dd522ba3b46450eb16c91fa340e308aa0c9" + }, + { + "id": "a10078fc-ed37-5452-84fc-9fb8b817f7e8", + "version": "MTc1NjMyODAyNTc3MDgwODAwMA", + "digest": "sha256:69e2609b7a1895440fba1eb5aacca55da21e0a5c4ba4895491285036450f8ab1" + }, + { + "id": "a1110fb3-74e7-57fb-8ad1-864b46113fcb", + "version": "MTc1NjMyODAwNjE4NzUyNTAwMA", + "digest": "sha256:8d6f5553459ebf85c38e6d77b955669ea40469945a5219aff4c5ce471cd288c3" + }, + { + "id": "a111a005-f178-5c8d-99ae-a08d8f0a1d29", + "version": "MTc1NjMyODA3NDMwMzg2NjAwMA", + "digest": "sha256:1808f4f80e6d2c7cf6972d9b7860855edf15c18bfc865a346c0da395e02a1b7f" + }, + { + "id": "a1267b35-23ee-5449-a986-adcad9365371", + "version": "MTc1NjMyODAxMzgwMTc4MDAwMA", + "digest": "sha256:bb5a6b85128edbdb5d65936c5e4e54ac05c2a33a52085c905579d1884b4fa81e" + }, + { + "id": "a12c432d-51d4-540f-a71d-873bf7a52108", + "version": "MTc1NjMyODA0NDg2MDUyNDAwMA", + "digest": "sha256:52c49be87b6b3311b474f8442d0507bb9a510eb2a1a1fc671574b5732499f0fb" + }, + { + "id": "a159f9c6-7423-56f8-b997-0f1b1b4b17b6", + "version": "MTc1NjMyODE0Nzc1NzcxNDAwMA", + "digest": "sha256:a91c5c435b3a7b11497b28a483e35e58d7282a9daa94b66c70c5cc3c2e473aa3" + }, + { + "id": "a194a414-6f8f-5423-aa97-8cb76707fc97", + "version": "MTc1NjMyODA4Nzg2Njg5NDAwMA", + "digest": "sha256:d336552f69ca93dec1e443955883772fb16538e74dd3b0b889fe44b7badd3ad4" + }, + { + "id": "a197ed79-f381-5964-9395-dee33c64277a", + "version": "MTc1NjMyODE3MDg4OTI2NzAwMA", + "digest": "sha256:6a4df894156240d2fe187ba2c1c505ab7d85a5e54b01bca6b533ec245535fbff" + }, + { + "id": "a19f1e4c-c881-55c4-b35d-8abf3704bbec", + "version": "MTc1NjMyODEwODg3MTc1NTAwMA", + "digest": "sha256:a2d9f874f96f581bc6d1eedd6171fb540ef61fd158b90b5a380946f4ed6a77a6" + }, + { + "id": "a1d0c1cd-3f68-5fff-8ad3-6de054e61f17", + "version": "MTc1NjMyODE1NTE4NTQyMjAwMA", + "digest": "sha256:5e3f133e5b0e5fc36cce80f7dc96950311cd8b48570fb843249d20331bf82e4a" + }, + { + "id": "a1d94072-1f43-5537-ba55-5ab21d7fecd6", + "version": "MTc1NjMyODE3MDk3MTYxMDAwMA", + "digest": "sha256:d639f14f2f07f2a30e32ca48a4c0a8a3dc7e3a7b4f02b40ddc6166310049a315" + }, + { + "id": "a1fc70b5-ee18-503e-9e9e-458b2cce5bf4", + "version": "MTc1NjMyODAxODg0MzM1MTAwMA", + "digest": "sha256:2c24c213c9c5f58b07b7038e4bb50029c38a758dcee821aaad5c7e93194084e2" + }, + { + "id": "a2130267-7a25-54ed-9df5-17e87b7e008c", + "version": "MTc1NjMyODA1NTY1NzE4NjAwMA", + "digest": "sha256:ab5c0ff64222d631bc688845a2c5f79dd58d749f9456ce4dc540273d00b3c768" + }, + { + "id": "a23b0a45-dd43-5d06-81b0-189d3f2c6f13", + "version": "MTc1NjMyODA2MjIxNjIzMzAwMA", + "digest": "sha256:65975b15de307775301475677e13089535ce7ca643c67a56ccc09ae5d2ba4c03" + }, + { + "id": "a248a765-65c2-5ee3-acf8-184666b8d2dc", + "version": "MTc1NjMyODA5NDQ0ODE2ODAwMA", + "digest": "sha256:342bd8c224e80907b3fee9a78104c79dbb1ffe8bd61276a07efe7589175ac42a" + }, + { + "id": "a26377df-1c9c-5595-9f7e-9fbeef29b320", + "version": "MTc1NjMyODA2MjExNzc5MDAwMA", + "digest": "sha256:63def2cfda214f10a2a5bf627df50b696ad4c51890ffdebccdd80044d0c36e83" + }, + { + "id": "a27134eb-dc34-557c-90a5-c2995d48cdd8", + "version": "MTc1NjMyODEwMjQ5ODg5ODAwMA", + "digest": "sha256:7d9541336687e7ce34bdcd4313cfa0c48001423e28e5a850ef6048aaf5e77ccb" + }, + { + "id": "a287c833-f934-5cc2-b519-1c4a7b86551c", + "version": "MTc1NjMyODE1NTEzMjI2ODAwMA", + "digest": "sha256:3e72defd9ec2700cc4a3b7abf91e778b43f4ef2e129d157c057192b683fdbcdd" + }, + { + "id": "a28ac8b0-91b2-5fcd-b6d4-fc844c2e3ed7", + "version": "MTc1NjMyODAxMDQ3MDEzNjAwMA", + "digest": "sha256:605461a48b1f59cb6088571d89740b7c42590af1714d9638ddeca464b07d4696" + }, + { + "id": "a2a85375-cf66-5f27-9b82-e1e7e22f44ef", + "version": "MTc1NjMyODAzNjkwOTM5NDAwMA", + "digest": "sha256:4ada95ad5ffb0bf266935d327d17dd451649c3393c76e95c54d9098821ef0985" + }, + { + "id": "a2ad1b62-48e2-578a-a308-fd7ec1c52a4d", + "version": "MTc1NjMyODE3MjQ2MTc4ODAwMA", + "digest": "sha256:4a129929b31fa2e726bc526cbda0a2768e16c38579d957a2ca852fb2b87e8b59" + }, + { + "id": "a2ba80ef-09a2-530f-b2b8-7d7ef34f01ce", + "version": "MTc1NjMyODAzODY0NDMwNjAwMA", + "digest": "sha256:59521763ed08235f8e9a0ec2ce70a7aa04fe8a3bd65761c840f208ae8e4e10b7" + }, + { + "id": "a2d9f71f-03f5-5458-85c6-e5de740b5997", + "version": "MTc1NjMyODEzNzQzNjEzODAwMA", + "digest": "sha256:9fb698cb2b0d60c0827492a8626c07c800f17d67f47702ddf7b48d4c2de5d505" + }, + { + "id": "a2ea2130-f9ae-5c13-83d7-83a9de4c7b21", + "version": "MTc1NjMyODA3NDI2MTQ4OTAwMA", + "digest": "sha256:45d95e4be6c6b0cf70a99c24b6584886fa7ae0b06efd0d9d94c76cd136b7e89c" + }, + { + "id": "a2f36108-62ad-5729-b2a0-6d3af316dacc", + "version": "MTc1NjMyODAwNjIzMjg0NTAwMA", + "digest": "sha256:9fa3b03dfe78cb5074c00d754dd8f3498178e32618e8c4808bb1a8947ed559f3" + }, + { + "id": "a3006e40-721d-513b-9694-c3965f286028", + "version": "MTc1NjMyODAxNTQ3Mzc5MjAwMA", + "digest": "sha256:f5097ec5273e5ad62d57141cfbf923c497e8995034c5c69cf83407d7b0952e13" + }, + { + "id": "a30c23aa-1cbf-507a-9cac-f9b230a113fa", + "version": "MTc1NjMyODEwODgxNzI2MjAwMA", + "digest": "sha256:ba1b80681d8c7bb07b3e310da3bb6b5b585966feb471b3d095219e1fe37b0630" + }, + { + "id": "a327ac06-e847-578b-8897-2cc5a4d26094", + "version": "MTc1NjMyODAwNDAyMTY3NDAwMA", + "digest": "sha256:474d85d092accf13050deb915ffcb4ee09106a055e94593f7409321d44e348be" + }, + { + "id": "a327ae17-5c44-5404-8e8b-ed1aa1101bc1", + "version": "MTc1NjMyODA1MjY2MzE4NzAwMA", + "digest": "sha256:a44e1bae213cb1d4de56e6e414e53004fc428ec520bb2bcca70bbeb4ccef8146" + }, + { + "id": "a342802a-ea31-51a1-b5a7-4a4de3c2ff26", + "version": "MTc1NjMyNzk4ODcyNDQ2NzAwMA", + "digest": "sha256:243a4d3e777480786e450bb87896944a4495985a37e3471270cad2d7add1891e" + }, + { + "id": "a359e988-9f82-5a3b-85c3-f21e5761b271", + "version": "MTc1NjMyODEzNzE0NzM1NzAwMA", + "digest": "sha256:fa49a3ba26457b794544774328ed2360c9dee57d786db5547739eb59ceaecba6" + }, + { + "id": "a37ba950-dd87-5c4f-ab7b-fe2ad766f1db", + "version": "MTc1NjMyODE2NzQ0MzgxNTAwMA", + "digest": "sha256:c12840bc484010f2318d09a64e947bebded3b59ec7e618eb268b1efa4c7966bc" + }, + { + "id": "a395a4e4-88f2-5bae-b4b9-1187bdcb4a85", + "version": "MTc1NjMyODA4OTIyNTA5NjAwMA", + "digest": "sha256:3d7961f2b9da359145d52699708eb016d48e7eaf373085c1f7d66e4c1945a9fb" + }, + { + "id": "a3b36459-a725-5df8-947b-43ad26c48de5", + "version": "MTc1NjMyODAyNTU5Njk1MDAwMA", + "digest": "sha256:78c4ba012b801f878dfc9ecf03448ea0ee442260da025cacd9f658c3a7f2f246" + }, + { + "id": "a3c1d8ea-ce71-5c61-a431-6456c0825b54", + "version": "MTc1NjMyODE1NTE0MTYzMzAwMA", + "digest": "sha256:82522badfba0da58900445e9a39a74f49081ef69116cdec6d450e672d56f30ad" + }, + { + "id": "a3dd9dd3-89b1-5f92-af1e-1e6707a04f6b", + "version": "MTc1NjMyODA1MTIxOTc3ODAwMA", + "digest": "sha256:54a95503e66f60d09ee2bbb0d01799eb0bcdfcc523eec5a2a132ec48fa2151dc" + }, + { + "id": "a3e14ec0-6caa-56f9-84ca-55144d1d8b7e", + "version": "MTc1NjMyODA3MjQxNjg3MjAwMA", + "digest": "sha256:5b258cf249e31876c6a0658993d7b5ed46bd70345dd99c3ca6087eeca6881a0f" + }, + { + "id": "a3e230b8-4c14-5998-9759-9660fce0ad9a", + "version": "MTc1NjMyODExOTgzMjIzMTAwMA", + "digest": "sha256:d6e70b9eb55b26c262db0d25088a73c870636cdf12ee7bac038ff29da6a2f1b5" + }, + { + "id": "a3e585b0-c358-5065-92ea-908ca63f57d5", + "version": "MTc1NjMyODAwNTExODc1OTAwMA", + "digest": "sha256:d180569d0392cdc1e8545b36647483425fe93e3e56a83a1a21ec9ad210a5a3c6" + }, + { + "id": "a3e96fff-36c6-50d7-a558-ab3859c8b878", + "version": "MTc1NjMyODEwMjI5Mjk2MDAwMA", + "digest": "sha256:27689c688f8728bcc8643a4ac5009a754c29aa96eeb04fb9bde79f3c6289d1b6" + }, + { + "id": "a3ef5f0f-2bf2-529f-85e8-d9cf734f991b", + "version": "MTc1NjMyODAyNzM2MDA3MzAwMA", + "digest": "sha256:b84387fb1794b44808a6e6a59920707495a8ad34d3ae8955028d3b6228bc521b" + }, + { + "id": "a4318a9b-42c6-5d3d-90d6-e963c189b403", + "version": "MTc1NjMyNzk5NzEyNTExNDAwMA", + "digest": "sha256:2bdd3a20b8bc46eb03c6316a142bc38b6b1cd9984234490803578086b9f5b245" + }, + { + "id": "a446a409-aa99-5188-aeac-6eb15bc12a06", + "version": "MTc1NjMyODAxNTUxNDIzMzAwMA", + "digest": "sha256:ac8189d3be24f11564d150847654ef230b903a209bdcaef60b2de5cbd31d5c49" + }, + { + "id": "a46252b2-ab30-5eac-997c-460561681eb7", + "version": "MTc1NjMyODA3NDI0NjM2NjAwMA", + "digest": "sha256:42718cc55794f22120b87aafcd4beee19df58078f5ded5cff4abeb60e02aa03f" + }, + { + "id": "a48ec93e-6ce6-55e6-b8f1-7b8cae135d7b", + "version": "MTc1NjMyODAzODczMjA0MTAwMA", + "digest": "sha256:9386f0a725252e1142f7531fdc96fe2a3e0094842a197e1000b20f5459527b75" + }, + { + "id": "a4a48caa-f222-5f63-a771-6e91af688905", + "version": "MTc1NjMyODE0Nzc2ODEwMjAwMA", + "digest": "sha256:187b2ac07ea9e8d1d053ff7a19c329267a1b09194cab2a5d3a7acc9004759651" + }, + { + "id": "a4bb8153-625a-5f9d-833a-ff5fd93c241a", + "version": "MTc1NjMyODA0MjUzODQ3NTAwMA", + "digest": "sha256:68f93ae479824f1732f0c8210197fe3c430d0713c9f84c8bd8b35774fb6c4f7e" + }, + { + "id": "a4cce507-48ab-5539-9d16-401a757a0f4d", + "version": "MTc1NjMyODE2ODk4Nzc4NTAwMA", + "digest": "sha256:6df46f7f5b96f3cce33c63f4fca81832309615002fd2687f4335180d9fd71a47" + }, + { + "id": "a4eb179e-3a98-53db-a2f4-332a4ddc2d9e", + "version": "MTc1NjMyODEyMDUwNDI3NDAwMA", + "digest": "sha256:8b16ea153a31a1212ec90e5c57daac43dc30f5be0d46ab8cd7a662654233740e" + }, + { + "id": "a4faaaa8-4e7b-569c-95de-76a891a80da0", + "version": "MTc1NjMyODA0MjYwMjEwODAwMA", + "digest": "sha256:2bc7f93857d74ca9629590f1169c845a2b85cc21e41be535d90db98d72ecadcd" + }, + { + "id": "a5047f07-135a-5dd5-8940-ba31252484e7", + "version": "MTc1NjMyODAyNTYzNDI3OTAwMA", + "digest": "sha256:39f184e116093cdaa95322fc8d7668fcebc56069b1d0b8b2eb94b310f42b212e" + }, + { + "id": "a50fcad4-9548-5bc6-98ce-fcb19e7e1440", + "version": "MTc1NjMyODEzOTM2MDcxNTAwMA", + "digest": "sha256:45493bae75836dd818a303a2f7b2744b87d22c59e87c97a2e97e2d1e2d6cbe62" + }, + { + "id": "a521c52b-f0da-5547-8f92-854696aa0516", + "version": "MTc1NjMyODA5ODYxMjk5NDAwMA", + "digest": "sha256:4095f0f7c776485388cf58f95d0f639bcf46ae955fad5d10eb19463b9729f764" + }, + { + "id": "a52a1c32-e8af-5b98-b1de-413e010f8955", + "version": "MTc1NjMyODAzODc1MjUzMDAwMA", + "digest": "sha256:9bb029bb3f14114a6980799afc6fdcaa3da8bb99edc85141ee2718d680e81246" + }, + { + "id": "a56ee400-daf6-553e-b1aa-242d78ec23d8", + "version": "MTc1NjMyNzk5MDQwMDcyMDAwMA", + "digest": "sha256:19959be917a0adbac192a396fddcfba9935d7df8e47bb4f1bc818aedba57e222" + }, + { + "id": "a576cdcb-f1a1-5d5a-ac41-6404fe805ef1", + "version": "MTc1NjMyODAzNjg3NDMzMTAwMA", + "digest": "sha256:5efd29286b275ce8e74a316add3ea23730336016b76abeadb58cf9f4c9580ba9" + }, + { + "id": "a5776695-d668-5b8a-83af-0604b9f5478b", + "version": "MTc1NjMyNzk5Njc3MTg1MjAwMA", + "digest": "sha256:20ac2f534202f073c5f53a5725d5629b69b286e4734ddcb3f1463a3d1f7411dc" + }, + { + "id": "a5784b8c-c1ea-5157-8be8-0e6260aa9bef", + "version": "MTc1NjMyODAxMTAzOTM0MjAwMA", + "digest": "sha256:612e6720e3c683aae5545570d1b8f0394c560b100728bb8c3da12da528ecf705" + }, + { + "id": "a5805b1b-3255-55a9-a4a8-526e1a560989", + "version": "MTc1NjMyODAwNjIzODA3ODAwMA", + "digest": "sha256:84a55ff57d1a05f6640e5d68d68db61314038a0e32ac9fdb9a530639f0fd2d39" + }, + { + "id": "a5fba0e5-b63f-56c6-b8d8-21f0a31e464a", + "version": "MTc1NjMyODE2Nzc3NzQ1ODAwMA", + "digest": "sha256:96f57a0d5b2597c91d1d9f03ced404dbf9c78878d751a050b0b26fa25c29f320" + }, + { + "id": "a6019864-b4a2-50d4-8ebb-cd6ba0968d12", + "version": "MTc1NjMyODAyNzM2NzA5NzAwMA", + "digest": "sha256:a67de2add2ebff7b9fe91974a970705abd91aa2870c730dc49f8f3e62bb89da7" + }, + { + "id": "a60eccb6-68e1-592a-b781-c5b5bdc5a323", + "version": "MTc1NjMyODA3MzExMjczODAwMA", + "digest": "sha256:375e3777fc50e7645b438a7030e6e86f8a92c1fd55713172e9038faf647e3ac1" + }, + { + "id": "a61c4c1a-3c66-5fd4-b36b-885b7cb0a97e", + "version": "MTc1NjMyODA1MjYwNzk1NjAwMA", + "digest": "sha256:b6c467ef695eebeaa9ff46b8014f9bf9be7fbf12952c74175c451c956db3094a" + }, + { + "id": "a634a695-ecb2-568d-911c-db8ff37db64f", + "version": "MTc1NjMyODAzNTk5MzY1MTAwMA", + "digest": "sha256:215360ecb1e56ba7c977e9f6977ef1d81ed37c53d16653ba076fb19411765a8b" + }, + { + "id": "a636f22e-1d05-59b2-983e-35166fb3cb90", + "version": "MTc1NjMyODA3MjU2NjM2NDAwMA", + "digest": "sha256:7307fc2849b2b5cdfcee1c96955dfebafaf30790db7a5bda4ff53ac3d1c27dbb" + }, + { + "id": "a63c7668-bd2b-5c0c-b56b-aa818ed5a467", + "version": "MTc1NjMyODEzNzE0Njk3MTAwMA", + "digest": "sha256:da162f8ffcc46ee9f2e999f182ad82c4127843584764ee2b8c7a0de2a822c7ff" + }, + { + "id": "a64933c3-175f-5fcb-bb76-d21a95495fc8", + "version": "MTc1NjMyODE1NDY4ODQ2NDAwMA", + "digest": "sha256:be37fca8e31b53b920066d6912306af287e33b2f2fadb2dfafeec15ecb1974e2" + }, + { + "id": "a668b4d7-51b6-5fc2-9bd6-9accd4a68e3e", + "version": "MTc1NjMyODAyNTkyNzc1NjAwMA", + "digest": "sha256:f336bdaa7bcc744bc84846cfcf833f6e9b4f0d2111572211ece8efb766062ebf" + }, + { + "id": "a680ed36-cca0-5537-876d-3613b50a959b", + "version": "MTc1NjMyODAxMTA1NDU2NjAwMA", + "digest": "sha256:59887bf4f975a9e8f5a31799d5eca20c0adf2bb757d48caa2bdc1b3ea32745f8" + }, + { + "id": "a6c9c8f3-cfcf-5ded-9ad8-249ea2c099d6", + "version": "MTc1NjMyODE2MDYxOTY2NzAwMA", + "digest": "sha256:c4aca2b17bf35b0f6cf1e2844a2c6d846e9b3a748ec93c40a7e2c6705665efe5" + }, + { + "id": "a6d6767e-819b-5e7f-aad5-b297bc9e5881", + "version": "MTc1NjMyODAwNDA1NjU1NjAwMA", + "digest": "sha256:98270ba3d38e06f8976dcfeff0160bf440842392110bc32c9c50344a5bd0e064" + }, + { + "id": "a6dcc4a8-28ac-5f5e-95ff-70ce05e0fc92", + "version": "MTc1NjMyODA1NDA0MDI0MTAwMA", + "digest": "sha256:d4b60c7d04894f776d83d376707697e489aa614690d6e9142c573a9aacdf7279" + }, + { + "id": "a6f5160d-cee2-5d78-9f7d-dc57f4dadea3", + "version": "MTc1NjMyODE1OTE4MTIxNzAwMA", + "digest": "sha256:469ad571952a3cce22fba3b30b5b105f2a319e6266a2e671b219bd1cc40b2f6a" + }, + { + "id": "a710a19e-b3f7-5494-968d-b60bb63195b6", + "version": "MTc1NjMyODAwMzk5NzQwOTAwMA", + "digest": "sha256:0602e8645655c38aecef26e0f5e7c0fba82a6b9a1290fe5cf86f1e18b0b3abf6" + }, + { + "id": "a7170dd4-9b20-5623-99f4-6269bf46be03", + "version": "MTc1NjMyNzk5OTMyOTUwODAwMA", + "digest": "sha256:cf81e9f69effa36884d9ed73a289134dff24030d7c98f1d1098041710b6a94a0" + }, + { + "id": "a73dd768-555e-56d1-a6f1-4371f18c9596", + "version": "MTc1NjMyODA1MjY1MTc5MzAwMA", + "digest": "sha256:f26f62193c6a0c9ee20c5d3d0739198eabe01540dcc1ada96c40966be92e2eb7" + }, + { + "id": "a7412efb-2aca-5f00-ae3d-44ace37a5298", + "version": "MTc1NjMyODE0MTQzODIzMTAwMA", + "digest": "sha256:15266e2ea9fcee2cdbad8229603b28e0ca03c8f76568563083cea9a68ede860d" + }, + { + "id": "a7569d94-7c00-52f8-9c31-be29e11cd1b5", + "version": "MTc1NjMyODAxNTUwNjUxNjAwMA", + "digest": "sha256:9c96bb4e9806a134970da461e1663b0d9a334203e1141d1ae8013833cc12ff65" + }, + { + "id": "a75f320e-d8d7-53b4-9034-05bd70950c81", + "version": "MTc1NjMyODA3Mjk4MTM5NDAwMA", + "digest": "sha256:bf0e9faff0ab6ffdd96d3032025f803c6397eed9d99a1a5616d244cba405fc70" + }, + { + "id": "a770f6b3-1497-5c3a-8e0b-56aa9edb4de4", + "version": "MTc1NjMyODE0MTM4NTg1NjAwMA", + "digest": "sha256:7262d41724f9932edb013b3203fbe9f1bee5f85d3184a3dde532912a6306290e" + }, + { + "id": "a7ca6903-fc11-5471-8d2e-88df10798115", + "version": "MTc1NjMyODE1OTE0OTUzMTAwMA", + "digest": "sha256:95e52e701d5b09993453f212d35e7f61c6fcd3d5ba07ed575c80b5f062cfb8b4" + }, + { + "id": "a7ccb8f1-fe74-5cb3-bdce-95f44ac6bc4e", + "version": "MTc1NjMyODE0Nzg4MDkyMzAwMA", + "digest": "sha256:2b12a8ccaf4d41c5dcd90ebffeb79ccc807250e397a89c73d7b2f354176d4275" + }, + { + "id": "a7d4c8c0-04df-59cb-8f42-6a17738c3c71", + "version": "MTc1NjMyODA5ODcyNTE5MjAwMA", + "digest": "sha256:870cb18a5fc355a87a76eb40c75caaed7216a44573333cc2d2fc2b1b0d3e4695" + }, + { + "id": "a7dde199-3b44-54cb-abf0-d006a462b7a3", + "version": "MTc1NjMyNzk5NTY3Mzg5NTAwMA", + "digest": "sha256:d9102ff907eada8c80e44586dea7974e00b70f8c6b60944a9bc7517a2ee77ecf" + }, + { + "id": "a7ecffa2-b70c-50e6-9f17-a636e79d4dec", + "version": "MTc1NjMyODAyNTc2NjM0NjAwMA", + "digest": "sha256:5ea111cd6e6a5db187b77d93290c40678851e740ad26e57259f7af39586a1154" + }, + { + "id": "a7efa808-6ef0-5679-8a73-5df4c429f52c", + "version": "MTc1NjMyODAzNjAwNDgwMTAwMA", + "digest": "sha256:c387ea5ff26cc632cdc9d81d904f1b80537c59f72a2329b6f8f488fb2c75e8e6" + }, + { + "id": "a7fa4a87-1c66-5d70-a90d-7fcc94741086", + "version": "MTc1NjMyODE2Njc4NTQzNzAwMA", + "digest": "sha256:d0aabf505f5fd0530566eeb78c307f8460d98a1842f64b0d626c8b1a2341aba9" + }, + { + "id": "a80f4fea-55c5-5666-a9dd-1a1b8ded6a74", + "version": "MTc1NjMyODEwMTEwODY4MjAwMA", + "digest": "sha256:437b50f8bc519e59d9f76aef3f8cfa14c861571f0ea123bddbfcc3889cdfe583" + }, + { + "id": "a828b653-b621-532c-a0d6-78697af240de", + "version": "MTc1NjMyODA5NzAzMDUxMDAwMA", + "digest": "sha256:4e6510bee62a5cddb7c1071df07d2ef82575d7d9a90ba61f98875ba3cad37709" + }, + { + "id": "a8486d67-f0ce-5e3a-a051-5deab33468ed", + "version": "MTc1NjMyODA2MjA3Mjc5NDAwMA", + "digest": "sha256:7324078663628b910be566aacf66b4406db8197977c949ff019a7459e92637b4" + }, + { + "id": "a8b91970-9330-5b34-92c4-76759081380a", + "version": "MTc1NjMyNzk5NTY0Mjg4MjAwMA", + "digest": "sha256:62964955b6541c448bdc3a54c8c67430cd56500941859d95b8d5e1df3f92da79" + }, + { + "id": "a8bd7533-9efa-5db9-a7c3-d55ccab6a250", + "version": "MTc1NjMyODE2Nzc1MDgzNzAwMA", + "digest": "sha256:cb64f2531ce29b4ec1692f8deb956ed68650fa6c7e454488c65b22195bd43272" + }, + { + "id": "a8c09272-01a4-50bb-ba2c-ae99a3e4f8ea", + "version": "MTc1NjMyODA2MjY2MzkzMTAwMA", + "digest": "sha256:54dddd12fa66ecf287e033e190382fcc8c5f7180b291fd2dda443a651bf157ca" + }, + { + "id": "a8e5e8d9-b779-599f-84d1-c4b804ec5f11", + "version": "MTc1NjMyODA3MzE4MDAwMzAwMA", + "digest": "sha256:f3059e455ef89706417a3be2af10e6c3b444819c2b47478938220a43b43a1691" + }, + { + "id": "a8f42ef9-81d7-5083-afb7-d035387249cd", + "version": "MTc1NjMyODAzMDc2NTM4MTAwMA", + "digest": "sha256:c345e546e4b1b3d73fe9f5ddf253c23d2565844e475c1191852d42913f99dfa4" + }, + { + "id": "a9221794-e5ba-5706-869f-ab7febb4dc18", + "version": "MTc1NjMyODAyMzQ0NzI1ODAwMA", + "digest": "sha256:65af05edf68b2ccc5e8224853f814b70a492ba0fdff053a03e189f814497384d" + }, + { + "id": "a9572da6-0e9a-574b-bd22-e254f8cdef29", + "version": "MTc1NjMyODAxNjUyMDM1NDAwMA", + "digest": "sha256:99a1e409a64527818c45b0f82a51ecb8bdd2180d30b82c985c991dc587bef43d" + }, + { + "id": "a95f4f45-0da6-5431-8ec9-46d61beff9a8", + "version": "MTc1NjMyODA1MTI1MzcwNjAwMA", + "digest": "sha256:6259bda67ff592284929dc72e8119ea4219edeb18a1142a7c720a52eee222c2d" + }, + { + "id": "a96c4ed3-9889-5423-b1fc-2194614a0538", + "version": "MTc1NjMyODE2MzM1MjY2MDAwMA", + "digest": "sha256:0b42e76a7a839b7baa56b339a31e819ad9b3784f0e8ac69ececc78bd68807ac3" + }, + { + "id": "a96d790e-9836-5fcb-931e-b92b89c4636a", + "version": "MTc1NjMyNzk5NzA3MjI4NjAwMA", + "digest": "sha256:73bbaca7092f826813d38a3c213322e72f75151c81ef19cc2381e8a78455e45b" + }, + { + "id": "a9830697-19f9-548a-83e1-46604b294c30", + "version": "MTc1NjMyODEzOTM0ODY1NzAwMA", + "digest": "sha256:7997ab15cfc8d8532adf896f78b12a4d32dda8736f10db6a54fe3d1a42c1c676" + }, + { + "id": "a983d8c1-644e-57d3-9474-ef3851875f90", + "version": "MTc1NjMyODA3MzIzNTc5ODAwMA", + "digest": "sha256:2c74f58665b7d75770af0de9fe906ce883c7528a9bea6f4eaab4d5f2872e0fa7" + }, + { + "id": "a9856499-6cb8-5fc9-b8e3-13d32397ce34", + "version": "MTc1NjMyODA4NzYzODU4MDAwMA", + "digest": "sha256:86072a82d696e07c665b31aab0f1d58613c127881a46065963e2563b1d426f35" + }, + { + "id": "a99e3711-948a-50c7-a0cb-8c4e9f97310e", + "version": "MTc1NjMyODEzNzYzMjI3ODAwMA", + "digest": "sha256:6a5df9c9c52759a208c7c54c52aecab8b1f32d187b2ed069a4ebdbd6a8a541de" + }, + { + "id": "a9b83f5d-50a7-5f24-961b-52afc6eb057e", + "version": "MTc1NjMyODA1MTI4MDg1NzAwMA", + "digest": "sha256:86abfc128867c3bc65e6a5460466ec706be62ad5dbd2313a33971d2f951bdf86" + }, + { + "id": "a9cb892e-ccc0-5d13-8af8-7d7d35033782", + "version": "MTc1NjMyODE2MjI2NTMyNjAwMA", + "digest": "sha256:65e14d2a3314c32c0f4d6629e2278ce6091d0ee3d5b4d550f249e3a3fbcab81d" + }, + { + "id": "a9d1d469-e79c-5354-8b7d-f4251a9fc323", + "version": "MTc1NjMyODA0NTY4MjMzNDAwMA", + "digest": "sha256:0a520ab33001d8ed12ae6dd1b681c3b944718730ecabcbd83909cb98458a1c33" + }, + { + "id": "a9d21aa3-0b1f-5761-9698-d8f39af44359", + "version": "MTc1NjMyODA4Nzc1NTU5NTAwMA", + "digest": "sha256:9f55d8f101980a66babea7038fba97664413c2c24bbafff8061abfa8adfcad1c" + }, + { + "id": "a9e63da4-22d1-5d39-80f1-37dba32d9fde", + "version": "MTc1NjMyODEzNjA2MzgxODAwMA", + "digest": "sha256:69e6b9aff9a2e9cc79757aa8917ba3465b759fc56304b1082fd0a512c3557c0e" + }, + { + "id": "a9f1b8dd-6145-5c06-a51f-a2dd44efb756", + "version": "MTc1NjMyNzk4ODY1MDE1NDAwMA", + "digest": "sha256:b2ff41636a16ca5234f837f84482bf51adfc8947195ddfb5a993388ea3b38354" + }, + { + "id": "a9f469f4-fe4c-5b6d-8698-e1466bc10506", + "version": "MTc1NjMyODE2MzE2NTU1NzAwMA", + "digest": "sha256:2325d67ab1c79af6146ea846da24f161c04e238c1a1528a6fdea2d3b6b4109cf" + }, + { + "id": "a9f48ccb-a57c-57a0-bde7-6ad30538b43a", + "version": "MTc1NjMyODA5NzAwNzY2NDAwMA", + "digest": "sha256:418752f5139fa3c470f8b5f67cd584ebaf12fd4e02f9fac70debe70226fe99b0" + }, + { + "id": "aa209e36-ddec-5f11-8642-fc3a0cf6f6f0", + "version": "MTc1NjMyODE0NzYyODI2MjAwMA", + "digest": "sha256:44d5af6079b76995249e1126f82895d795b1d29b8b630bd261a820314ca58b49" + }, + { + "id": "aa361f9c-26c7-50a4-adc3-4901d2ec8603", + "version": "MTc1NjMyODE2MjM3Mzg0MzAwMA", + "digest": "sha256:00e01fbf7376c53bc496c7c403811165e37d54a32e01a0fe33b8fb25f86106e0" + }, + { + "id": "aa565902-77f8-5bbd-a86a-4ea46587a1fc", + "version": "MTc1NjMyODEzNzM5MDc0MjAwMA", + "digest": "sha256:e9e7f6d3d715f0b8fc7023f5913349e681b8f460f91e2ae96d2b223c85f7c3b7" + }, + { + "id": "aa831bfd-302f-52b2-8618-bf96bb0a3f96", + "version": "MTc1NjMyNzk5OTI0NjU3MTAwMA", + "digest": "sha256:bf8e5849409c1dc558f527563596cdebad73b3a8c570dbe26a5523a72b0a1c5b" + }, + { + "id": "aa8715d0-167d-51f8-b296-455734d01804", + "version": "MTc1NjMyODExMzQ1NjYwMjAwMA", + "digest": "sha256:d7e0e4e356952dab0264ecc98e2edcac16ffda04b840cde09f6d1c3a3f1e725b" + }, + { + "id": "aa8c4612-b226-5df1-8a79-c66db2924d6c", + "version": "MTc1NjMyODAyMzM3Mjc0ODAwMA", + "digest": "sha256:43822fa802a5d04fb9a96aeb644b41fc82ee1a60defb158f7d3056b239965575" + }, + { + "id": "aab90398-5ea1-591a-bec7-c5b025c768d5", + "version": "MTc1NjMyODA2MjU2ODA0NjAwMA", + "digest": "sha256:96d9af186aacab358634fbcdc9f5a2cf4e1bbf02463c876c5b39d7126be042c5" + }, + { + "id": "aacf050c-f7e5-5d06-a741-f4ec019be4a7", + "version": "MTc1NjMyODAxODg1MDU0MDAwMA", + "digest": "sha256:a5894dfadc7634b5a9c2b6b5501858a6653a74abdf627b84f43959eadac5c9c0" + }, + { + "id": "aaf3649c-7400-514e-995d-d1474c0cce13", + "version": "MTc1NjMyODA3MzE0MjYxNDAwMA", + "digest": "sha256:c9e9a3e136990ccacd33c6eba0af9e4cc8a3e09f1b0323b11e0236582694f66c" + }, + { + "id": "ab1b8c15-84b9-525b-85e0-625f661b4688", + "version": "MTc1NjMyNzk5Njc2MjEzOTAwMA", + "digest": "sha256:4125ee393e0f08ffe2cfe837016e9e348d2fbb5b7b971430abe1d50707df6fa7" + }, + { + "id": "ab26565f-c0f0-5eaa-8c57-d238072455c9", + "version": "MTc1NjMyODEyMDQ3MTMyNTAwMA", + "digest": "sha256:8b6d28bd51acc6e9ad03d0ff6bf1a48bf0f7dc3de1ef8afe858a21eea0aa8da8" + }, + { + "id": "ab43499f-d61c-56f1-8473-24e5d86058d7", + "version": "MTc1NjMyODA3MzA4NzYxMjAwMA", + "digest": "sha256:3d1716c2855c43b1bcbafb1e570d2185b9cdd9a07c0d404f05f91733c52bbaae" + }, + { + "id": "ab520855-b671-58c1-b9c2-8dbbbec1178f", + "version": "MTc1NjMyODEyNDQ1ODE1MjAwMA", + "digest": "sha256:47a0bbfb2b5e24ddfe14a77398f8e0d5f453cfb5f6d0f6cb1d4eb34f8bd9d771" + }, + { + "id": "ab8801d4-297e-53d2-bc96-a6d06db1e18f", + "version": "MTc1NjMyNzk5NTYzODUzOTAwMA", + "digest": "sha256:4fdc3605da15546685e6425b3df26c97dacb3127ad151f0eb25ea4d8af255a3b" + }, + { + "id": "ab98fbcc-48e0-50a1-8b0c-c20a0bc95305", + "version": "MTc1NjMyODAyMDUwMTIxMzAwMA", + "digest": "sha256:cf9f10dfc904ab8002adff4415af328f5b5b4d2aa6f90a3bb9be0dc2cfba5ce1" + }, + { + "id": "aba1fe12-bda0-532b-83dc-1c6d41642c81", + "version": "MTc1NjMyODAyNzM5OTEyNjAwMA", + "digest": "sha256:5f40ab2dac95bf9babd13a69e9a107f23e0c53dea6f27f3edd7a8a47de9e3dac" + }, + { + "id": "abe177f2-bce2-5464-b895-3be4d22cf005", + "version": "MTc1NjMyODEwOTI3MzA1NjAwMA", + "digest": "sha256:5dd4a1ad88670c7b47a85a94d46e6b2022bfa8f7d76a833b1fbec9c629bf78c0" + }, + { + "id": "abfdf411-cfbb-5cb0-a1a8-580735e6f5fc", + "version": "MTc1NjMyODEwNTQxMTE1OTAwMA", + "digest": "sha256:5ea90abc87b603eb3c46b9afc2ac993a24ee821dfe48d039256f65f3e7b42c4b" + }, + { + "id": "ac235510-eecd-5515-a720-d44c4abcef6a", + "version": "MTc1NjMyODAxODgxNzIxOTAwMA", + "digest": "sha256:15160c55a252c38f3b588b63b9c85df9166749e34f4ef1d1e513ce8e4a6e1b56" + }, + { + "id": "ac3126b6-7c46-5292-92b3-b138c3df01eb", + "version": "MTc1NjMyODAwNjE2NjQ5NjAwMA", + "digest": "sha256:65e8606b4fb0803af1346b42c1bf78c9d3c1fcc3ab353aacafbfc104625617e7" + }, + { + "id": "ac35b35f-4e73-58b5-96a6-e3efec412db4", + "version": "MTc1NjMyODAxMDQ5MTM0NTAwMA", + "digest": "sha256:a29ba7ad8cec70604a21c96545e15b02dd6e0d756a806c954168957bee29abd8" + }, + { + "id": "ac36c7c3-a1c5-5cc2-aa48-c6aaf58eecae", + "version": "MTc1NjMyODExOTE1MjUwNTAwMA", + "digest": "sha256:e0a85f28a2a7127d4dbffc9c6c92abc2336165df72a753616603113122f47d42" + }, + { + "id": "ac39bea4-9898-5fea-ac97-7045c32a9d1c", + "version": "MTc1NjMyNzk5OTI4OTgxNTAwMA", + "digest": "sha256:85a40941410a6d53c74ca6ebf8339a055cbc540aaf06ae040140dabfbdb3e9be" + }, + { + "id": "ac4958f2-f112-5aa0-a90f-171e821b23b6", + "version": "MTc1NjMyODE2MjM3MTMwOTAwMA", + "digest": "sha256:7c72cb4a21cc97acbae0cd85a12cb859bba79cbd62cbc5d8ee1b4e10156bbcb4" + }, + { + "id": "ac6bec47-db9a-534c-b7dd-97f17c24e57d", + "version": "MTc1NjMyODAwNTE0NjQ2NzAwMA", + "digest": "sha256:e990d56bef3b32ec6017a6ba339bc66954bc57110b213ff096f92f5412dc49b6" + }, + { + "id": "ac782983-0595-5e8c-ba07-ba3ea9c8b4d9", + "version": "MTc1NjMyODAxMDQ5MTgxNzAwMA", + "digest": "sha256:3838fda08b6ed0bc2b68ff80b1ea0c403d0ddad3d284c040c8dd2711a0d26320" + }, + { + "id": "ac9966a2-0c21-5b63-825a-571c332fcdd7", + "version": "MTc1NjMyODAxNTI3NjQwNzAwMA", + "digest": "sha256:0c7c866b5b78f467ec9d44f7824d657db1cb5d2276d10b900c92eb98ad7f1b62" + }, + { + "id": "aca36b06-120f-53e3-946d-7960584f3092", + "version": "MTc1NjMyODE0MTM5NTg2OTAwMA", + "digest": "sha256:0989f6b9957fc3b7c6cd3709246aa139e61927317508964771c4e2bb2ccb92e7" + }, + { + "id": "aca7e589-56ab-53a0-ac3c-fa2ef112a99c", + "version": "MTc1NjMyODAyMjA4MzU4MjAwMA", + "digest": "sha256:3ed172b4d2ff744fe0c4ac8e3690700224fc6416b69a158c170fc2834868199e" + }, + { + "id": "accee733-a93d-521c-8b81-2185c2a13166", + "version": "MTc1NjMyODAwMzkzNzE2ODAwMA", + "digest": "sha256:845e09c406e37405ea0b0b97881051f49c90428a83e3ae7e6b8e7122ba65618f" + }, + { + "id": "acd14da0-f3f4-5ecf-9b88-fe045af66ca5", + "version": "MTc1NjMyODAxOTI1MjY3MDAwMA", + "digest": "sha256:5b2432200c9eb34cb22ce5b544acdbdae4161ba13e0b7f6e9a26498eb4e331be" + }, + { + "id": "acda2bb3-fc71-579f-9ba4-46f4c19072cf", + "version": "MTc1NjMyNzk5NjY5NDM5ODAwMA", + "digest": "sha256:7d9e1145735cf330d47a6f8f705ef24048618597a98c9813f461acd949b8f53d" + }, + { + "id": "ace3abd9-868c-58b3-9806-774829b5fb5e", + "version": "MTc1NjMyODE2NzUxMTU1NzAwMA", + "digest": "sha256:f3f91e4d94add8f25f5ba10a78e38dd10ceb6c0d027c6acd4fe159e9e95d28d8" + }, + { + "id": "aceb87fa-5f14-5fd6-914e-33281a2c776b", + "version": "MTc1NjMyODEzNjk4ODQ3NTAwMA", + "digest": "sha256:042057529e2663a86256c689bd7f73150787e47dd5340f96be927153736a06a2" + }, + { + "id": "acf73c05-ba49-5c92-9cb1-2046fe402a4e", + "version": "MTc1NjMyODAzODg5MTUxNzAwMA", + "digest": "sha256:ea078e4d8a7c574eac7219adeefad80f8ccf560723b68b40df5d2d8638110b1b" + }, + { + "id": "acfc6b07-3188-5246-abaa-2731922025d1", + "version": "MTc1NjMyNzk5MDQzNzgxNzAwMA", + "digest": "sha256:94d416c3b1e53db1d1500393162841e5ead97e9690901496167b6b3ae4ee1410" + }, + { + "id": "ad0804a0-fdea-5227-952e-eead77f5e621", + "version": "MTc1NjMyODA2MjY4MjI0NzAwMA", + "digest": "sha256:79aa33f3bf920a916167e01abaee36891fd65910a55ca454abdc398373d68f2c" + }, + { + "id": "ad0c8217-d575-54bf-a26e-7a3c8dec3102", + "version": "MTc1NjMyODE3NDc4NjgyMjAwMA", + "digest": "sha256:f8797faa10e4ee39fc8d0085304a0de1eb79f6cf5fe46d3858944221b6c43797" + }, + { + "id": "ad1fd6b9-09fd-5b91-a0bd-3dbdb9f3bf07", + "version": "MTc1NjMyODAzNjg2MDkxNjAwMA", + "digest": "sha256:19160330589269b8be098bf60b31db690a322d9ef00b2f7d87b32fd0684d8da0" + }, + { + "id": "ad483036-3bbb-588b-98ba-03341c37e570", + "version": "MTc1NjMyODAxMTg5NTM1MjAwMA", + "digest": "sha256:08f79ce0fcd9c9f307d5e1fa93ce8d6fa29cbab3950ddecb659710441b427ae2" + }, + { + "id": "ad515183-1e33-532e-8c0f-6a42898f8d45", + "version": "MTc1NjMyNzk5NTY4MjE1ODAwMA", + "digest": "sha256:e42d802e030903471c0c8c00f43e52bea296708d4c9aa9faf9de8b43872ac89a" + }, + { + "id": "ad663c18-9042-5cdb-aa27-eed500c61879", + "version": "MTc1NjMyODA3MzE0NTI4MDAwMA", + "digest": "sha256:c3d77df4cf180a9c21218d8184abfb8e0fbee308cb273de03e91497598adba34" + }, + { + "id": "ad7a9101-0363-51f5-be56-7aac043185b0", + "version": "MTc1NjMyODAyMzM4MDIwMzAwMA", + "digest": "sha256:4751f4df78def196e4f18a898729ee38bcc6ce1b6487ccd20097f564a0616bf4" + }, + { + "id": "ad920aee-49c4-5575-85b9-e3bbc0a851b5", + "version": "MTc1NjMyODAzNTk2NTg1MjAwMA", + "digest": "sha256:0fa69538ec2fff343dff4abc3e674b6c4a3bde404dd0a91ccec89fa433df8ac7" + }, + { + "id": "ada6ed20-4906-57ec-b8a8-a954aff60c8d", + "version": "MTc1NjMyNzk5Njc0Mzg1ODAwMA", + "digest": "sha256:6d44289c62dc7e960ee9b76d1c8f3b5d29f0426fc52d52b81ba40f5420de71be" + }, + { + "id": "adc1c1be-76cb-5912-9fe1-c347f9adf7d0", + "version": "MTc1NjMyODAxODgwOTk3NTAwMA", + "digest": "sha256:e802791ef073cd28b03f6615beb57a09c23e2490166eaa9d90a5f94e322e16c4" + }, + { + "id": "ade6e6f7-ad5a-5488-be12-d6e49888b770", + "version": "MTc1NjMyODAyNzg1MTg1NDAwMA", + "digest": "sha256:d0b8c4b359e0e1b1fb1f7a96e993a96da12ea2e8272c0d06dd1615dbd1830c81" + }, + { + "id": "adf369bb-ed20-5faa-b4a3-67f990b51604", + "version": "MTc1NjMyODA0MjQ5MzQ4MzAwMA", + "digest": "sha256:117574669d76b9f35bb6a5683575b86c3488851926f2adb8cfbab752017cb506" + }, + { + "id": "adfee2e7-6f60-544b-81d0-7564f8e6ced0", + "version": "MTc1NjMyODAxNjQ1NTU5ODAwMA", + "digest": "sha256:09d2d284c3cbe96b434929dbcc7b73abad9e0e0f7f49881c9aa80f3345123ad6" + }, + { + "id": "ae26543a-12c0-5e11-8be0-32f816f54df9", + "version": "MTc1NjMyODAxOTI3ODY3NzAwMA", + "digest": "sha256:07c5b979f19ea6fa7154cf6581288033f9e4e5a5af8791c894de73505e66e288" + }, + { + "id": "ae29fbf1-a926-5cd5-bcd5-bc1a73e8f1d7", + "version": "MTc1NjMyODA1NDAzODEyMTAwMA", + "digest": "sha256:a3514fbc1a52213f392b41a510b57ebbce2a975c3db4f9714937e468b718b68e" + }, + { + "id": "ae4801fa-f4f2-5c6f-afa9-70cb6201e20d", + "version": "MTc1NjMyODAyNTYzMTM5NTAwMA", + "digest": "sha256:f6df65885ccd456a1fadd870be3da9ece5ce00e45a95ab6e8bdc054204b9f933" + }, + { + "id": "ae53d5ce-7086-5976-b7f6-cd5ac69b7373", + "version": "MTc1NjMyODE3MjUwNTgzMzAwMA", + "digest": "sha256:272ee6cd5691f5bdb7f1ed72049510e3530f41bdbbb977f5266b2bc3cbc7fc71" + }, + { + "id": "ae5d9c97-8dd1-58a2-b3eb-701472c0a712", + "version": "MTc1NjMyODA4Nzg3MzY4ODAwMA", + "digest": "sha256:8cafdae20f570cff41b29f41005cb0caf24b274f4cb15f3e185ce5d4197da3a7" + }, + { + "id": "ae662b71-7ab2-541a-b615-945cb964d926", + "version": "MTc1NjMyODE3NDc5MDQ2ODAwMA", + "digest": "sha256:451bc8d85a6460db8f918d4ae23a53a1e21918cc493a84f342964776d0363838" + }, + { + "id": "ae765bc1-36d6-52e2-bca6-277720d67e08", + "version": "MTc1NjMyODEzNjEyMzAyMjAwMA", + "digest": "sha256:cff8da7914e4f999481778208b97e8b696297aa7a722d43e0c0638df1ceca059" + }, + { + "id": "ae8c3dc0-2490-530d-84af-ab31b48fab53", + "version": "MTc1NjMyODEzNjIyNTEyOTAwMA", + "digest": "sha256:fe6d69deebd19a7fb3b091d46d58895b8c62f5248ed80237d1b09e88a5573bb3" + }, + { + "id": "aebf73cc-4f5f-57b2-a25d-0066807284be", + "version": "MTc1NjMyODA1MTMxNzE4NDAwMA", + "digest": "sha256:1e7f7c1fbc32d472c3526e3359a88e6c0bcedd64079bf65caa1a1e054e9f1195" + }, + { + "id": "aed43c08-76af-513f-a56d-aac9d89df040", + "version": "MTc1NjMyODE1NDcwMzA3MDAwMA", + "digest": "sha256:010849f3402402b3877c402036cb3da1a4f4d5ad4373d9ea93b50d91d7c34d80" + }, + { + "id": "aeda50de-8499-56c5-be27-01b5bf28006c", + "version": "MTc1NjMyODEyMjI3MzI1NzAwMA", + "digest": "sha256:4b35fe59220dd4d0dc2f2a211d1deb0931b4988103a48b0858868f4a7a87f218" + }, + { + "id": "aeef26c7-6105-539a-aec9-85064ffea48e", + "version": "MTc1NjMyODEwMjQ0OTA3MDAwMA", + "digest": "sha256:37bbfb56da99dda348b6f90ee84145892e7daf0e0413141ac9d2fdbc10a39168" + }, + { + "id": "aefa33b9-8b86-5a00-a1de-d107836464b6", + "version": "MTc1NjMyODA1MjUzODgwMTAwMA", + "digest": "sha256:8ce6439872d433cf012756b95e0d7ede362eb087cdbfbcc7b4ef9b74a706f600" + }, + { + "id": "af232301-6083-5e6f-a09f-df7e410cb91f", + "version": "MTc1NjMyODAxMTk0NDQyODAwMA", + "digest": "sha256:d53c1175136e3b4b9cddfb970f86130ba980cb70b66b6b5ea48ebfc9de323490" + }, + { + "id": "af5b206c-6fee-5cc8-981d-f60f82d56671", + "version": "MTc1NjMyODAxNTIwNzY4NjAwMA", + "digest": "sha256:8a3dcd010edc219f0e3cf8931419dce1035ca4e81ff7af3bc9fdf36858a47858" + }, + { + "id": "af63ca04-9b76-50c3-9399-70b6b52f5afa", + "version": "MTc1NjMyODEzNzU1Mzc2OTAwMA", + "digest": "sha256:d3b567765e5446b488993629346f587cb9cfd0424efa9b9a3fa859a95cc267f8" + }, + { + "id": "af81cd7c-c6b4-5b0c-82b6-6ede2e7956fa", + "version": "MTc1NjMyODA0NDcyMTE2MjAwMA", + "digest": "sha256:2af81587517e4e52124a179ac499b09875d5777515b51ace0cd1d89dea1115e1" + }, + { + "id": "af81e3ca-4b72-5b6f-a11e-df62c87fdd26", + "version": "MTc1NjMyODAyMDU1NzI1MTAwMA", + "digest": "sha256:0e69984866f0f059c9f9f75638d6ce80514713324c8b3457e537fb2ef80fd382" + }, + { + "id": "af922cb8-8e35-55b2-8816-2ed8568efca5", + "version": "MTc1NjMyODAxMTA1Nzg2MzAwMA", + "digest": "sha256:54dd03014f97ce796b786b5b4256afa0b32acd16b0c6741764407435045f6e5b" + }, + { + "id": "af935933-fe63-5904-997a-dbd49fec2833", + "version": "MTc1NjMyODEzNjk5ODA3MDAwMA", + "digest": "sha256:34f664b76a99d5978ff07afd05c292acd7e9563e9bc41d2f961cc1143a26427b" + }, + { + "id": "afcbefbf-2ee2-53a4-a051-62c52bdb1a1b", + "version": "MTc1NjMyNzk5NzEzNjI1NjAwMA", + "digest": "sha256:3801d4cc4c81e5bf40a0b81e87bba3c4029a3c2d449373cdb5e2c1fd729e3581" + }, + { + "id": "afd09bac-cb39-5915-9154-6f2f3bfe7d5e", + "version": "MTc1NjMyODAyNzQ0MjIzNjAwMA", + "digest": "sha256:f33d98ceebf236dd1233549b69380db201dd00271a2371bf73e452c2f050db31" + }, + { + "id": "afddeecd-c17b-592e-a5e8-f32425ad5913", + "version": "MTc1NjMyODA1NDE1NDUzNzAwMA", + "digest": "sha256:95146e9abcb22e86f2f4cc9a561ed071be810b75460bfa16523386e061ccc34f" + }, + { + "id": "afe8ebc1-914e-5f5c-b58d-05f94867b948", + "version": "MTc1NjMyODAzODY5Nzc0MTAwMA", + "digest": "sha256:412f576229574b8ee6a2bfc762c786708f3f76d5888f229cd0f0c9bbb138da0c" + }, + { + "id": "aff6838f-5bcc-5af9-8dec-6661e44c9fc1", + "version": "MTc1NjMyODA5NDQxNDUzNDAwMA", + "digest": "sha256:37190978cba6239c1da9d8dee4ee89288b795845606a6ca2d91cb24451a013fb" + }, + { + "id": "b00f459a-8099-57d4-a702-b563a5451ca1", + "version": "MTc1NjMyODE1NDY3NzUyMDAwMA", + "digest": "sha256:cce5322f8b48d9310bf6e237c78a158d62474181c24f9a9854cca6c7f2c22afa" + }, + { + "id": "b013fba8-ba0a-586d-abc9-802ce04cad0a", + "version": "MTc1NjMyNzk5NTM4MTg1OTAwMA", + "digest": "sha256:54f51c8ab2cf75a7683def97b67e66ed059c901aa13b93a341011e29d8f4bb5c" + }, + { + "id": "b014510c-d501-508f-b674-8cc1ac22f227", + "version": "MTc1NjMyODAyNzc5MzA5MjAwMA", + "digest": "sha256:2c100e83a22ac62324e0da29be09289c53a8e34e2fbf52b894319d4e39b49713" + }, + { + "id": "b022a363-5d28-5ca0-b2e5-16ebeac02945", + "version": "MTc1NjMyODEyMDQyNjY1ODAwMA", + "digest": "sha256:6ba3cf7e777ba8c43ac4a329771283f4c94dda8fb36b2d982752ca36398f0c6d" + }, + { + "id": "b02ed109-c5a3-5b2d-add1-ff11eb167b5b", + "version": "MTc1NjMyODEzOTI2NTA5MDAwMA", + "digest": "sha256:87b8988d527d8c2288306e46b4e3e35ebe856a2385398ec706f974f9a5c0e587" + }, + { + "id": "b0430d99-a2ad-59ee-afbf-edc3434f9a3a", + "version": "MTc1NjMyODExMzU0NzgyNTAwMA", + "digest": "sha256:1b7fd988477b3e636e76fd378bd391b4d4434209ef605cee941d5586713bef66" + }, + { + "id": "b06296da-641a-5c4f-b554-a95886355536", + "version": "MTc1NjMyNzk5NTcwNDgyNDAwMA", + "digest": "sha256:94ef3e94ec06d467a714d29bef2abda79feb58950c10c7ce1a5d91f7b2abfa45" + }, + { + "id": "b0a62b42-e467-5bf8-af57-76739c509535", + "version": "MTc1NjMyODE3NDcyNjcwNDAwMA", + "digest": "sha256:b1bc00267a71f0caff30450c6b853189909c2bc52c8ebf225616453856bcbec4" + }, + { + "id": "b0af055e-023f-5499-80ad-4e07ffbe8462", + "version": "MTc1NjMyNzk5NTM2MDg3NzAwMA", + "digest": "sha256:cbf37eb25d1782afae4648809d768c07b657402b8c8d6148e5c7fbd87a19f44b" + }, + { + "id": "b0b206b4-7bb0-5933-ab82-052ba57417ab", + "version": "MTc1NjMyODAxOTI4ODQ1MDAwMA", + "digest": "sha256:b85b7a625c08c85ffea6888e48e6420182ba0205845cd9852fb960687ad07995" + }, + { + "id": "b0c1ad9d-a68d-57c1-be18-6847ddc232c1", + "version": "MTc1NjMyODEzODI0NDQ1NDAwMA", + "digest": "sha256:8b89443e2afbcdb7e51452fc9af081ff24d8c6ccf60fc1c5ccc28d3ce25c5507" + }, + { + "id": "b13f6bc7-e267-5078-80dc-deace216a64a", + "version": "MTc1NjMyODAyNzg2NTgzMzAwMA", + "digest": "sha256:bc9d8d73534ff673fb0069fcfc13e59fbd788bf83b7ae5a53220553a8f235c91" + }, + { + "id": "b15214f8-17f4-5547-a6ef-5e9cde942971", + "version": "MTc1NjMyODA1NTc4MDM0NDAwMA", + "digest": "sha256:1fff3892d129e9079f18484da3d2e3c487e8b3ff519ca5ad727e62b10e110596" + }, + { + "id": "b16a7278-52ed-5c36-b118-74dca19a33f0", + "version": "MTc1NjMyODA5NDQ0MjAyMzAwMA", + "digest": "sha256:3bb0c8138fe8efea626af0f8b6afefa5d2174b5cae3f04a4539abab5bef9abef" + }, + { + "id": "b18f9d91-5234-5c29-8e56-c8995a2d0028", + "version": "MTc1NjMyODE1NTIxMTA4MjAwMA", + "digest": "sha256:f3c2d9d7c1102490fe713eae603766f6ad88c026ab89888e32a67fa0f7305183" + }, + { + "id": "b195b3bd-2672-545b-bf8a-948bf6bc8221", + "version": "MTc1NjMyODAzNzE0MzI4NzAwMA", + "digest": "sha256:8f5293b0a34bb9ae13a189dfb810c9c4a5a47ec24e78f45bfb5b19fee1e67eb6" + }, + { + "id": "b1a9d53c-020d-5baf-bbe1-7ad99d00a29f", + "version": "MTc1NjMyODE2NjQ4MjY5NzAwMA", + "digest": "sha256:4105535b6413c20d35ee764eb3cbe666921d0b150909cfdb2be5d5e946db8058" + }, + { + "id": "b1fe1a1f-a919-50f6-85d3-4862226d4f49", + "version": "MTc1NjMyODE1NjQ3OTAyMDAwMA", + "digest": "sha256:790d24bac61ae7fb0799e59c5ad24f68a4aaf0a719447f1d81300027eef43338" + }, + { + "id": "b2335a1d-2c49-5441-ab4d-26f26d8d8897", + "version": "MTc1NjMyODA0MTg3MTAzNDAwMA", + "digest": "sha256:b0f4275c6703920d0af27e0f4468cf0b72afb8ce9a6d853ff74af3876a0473de" + }, + { + "id": "b2356315-b557-5857-94de-19e998a324bf", + "version": "MTc1NjMyODE2ODgyMDQ1NzAwMA", + "digest": "sha256:2423fcddda2ab516998a661e09e227d7811c06dd49b351311a448ee2f42f8cd9" + }, + { + "id": "b23a8225-6b5c-5ad9-86bb-7dbdb7a7a288", + "version": "MTc1NjMyODE2NzQ3Njk5NDAwMA", + "digest": "sha256:3084a770aa65bbd355c0e5303b7dceeb7b02805f2a9be1b34795a5e440e53d79" + }, + { + "id": "b24648f2-ce98-551e-9899-937a39f53f43", + "version": "MTc1NjMyODEyMjMwNzQ4MTAwMA", + "digest": "sha256:fa4c58f35467f4167dafff224a6682c4c895d5b0930f54a5d3912d4f22a1bc6c" + }, + { + "id": "b25e348a-40e3-5646-b112-98cd2ecf9bf4", + "version": "MTc1NjMyODAxNTQ1NTI4MjAwMA", + "digest": "sha256:30a0ccea1ff627ca8604e1d9baf0a6ca025d34cf202b42eb99ea069d6ccd26ec" + }, + { + "id": "b27cb85c-e439-5f0e-a2b7-38fe402aa763", + "version": "MTc1NjMyODE2NzQ0MTc1NjAwMA", + "digest": "sha256:44fa6958dd119d7e0f5088d19482c7a120d18815a2656a534e4b61cb53b88a38" + }, + { + "id": "b282ee1b-eb60-5959-aa6d-3dff86d897d1", + "version": "MTc1NjMyNzk5NTcwMjM3NDAwMA", + "digest": "sha256:4106278497585a742ccfe495565d2dadeb1e88f15bf603a370431a1389aec265" + }, + { + "id": "b2859d8d-5ec6-5096-b1d5-0f1fedc9914a", + "version": "MTc1NjMyODA5NzAyMzgyODAwMA", + "digest": "sha256:7ede7d43f3ca4f122b0ab23debb86a7fb9acb82e12731f94e6f98d07245e8356" + }, + { + "id": "b28b6f7b-8769-5511-8e64-d55dc86f68fc", + "version": "MTc1NjMyODAxMDQ0OTEwMjAwMA", + "digest": "sha256:4ff55ca1328bc1c6ff85031dee2e8a156292b497c75af753df724ecb16e6de7e" + }, + { + "id": "b2be94e9-09dd-56f4-9e13-1463e2b85143", + "version": "MTc1NjMyODA5NzgwNzQ0MzAwMA", + "digest": "sha256:bc14942e35a30ae0a53b2821b7cd209198e953e32b45daa33938fe6ec6071890" + }, + { + "id": "b2bedb86-38de-5285-ae07-fa42323ce117", + "version": "MTc1NjMyODEwMjM1OTgyODAwMA", + "digest": "sha256:2517dcf35c8c9a8d44c883c153e916243bee638ab096a67a627a7fc370bdb851" + }, + { + "id": "b2bf12ec-3ea5-5b5f-a9e9-64c9192459bd", + "version": "MTc1NjMyNzk5NjcxOTg2MDAwMA", + "digest": "sha256:f66f004e8226449aac08892a51fb169de1e2a7cba1a2e192a8b5777b86136746" + }, + { + "id": "b2c38394-74c5-5353-bb94-ee324be90fda", + "version": "MTc1NjMyODAxNjU1MDAyNzAwMA", + "digest": "sha256:06dc225968f1968e818476942e3e80797454e5ac90337840ed6e985cb4cc1375" + }, + { + "id": "b2dc7047-5814-52db-a9a7-2e7ede4a8b95", + "version": "MTc1NjMyODA2MjE0MjU5MjAwMA", + "digest": "sha256:669f557557b3e78416059d1ccb40587d9d82351c17fb9d30784fb6e50f8d2a31" + }, + { + "id": "b2e89fe1-d5fe-596d-884d-c47b059bd33d", + "version": "MTc1NjMyODA1MTI4MDYzNTAwMA", + "digest": "sha256:1c6b7de5f34edd9ed11464852e114d9c6c7fa63b5f5ce5f468aac5dc4953278e" + }, + { + "id": "b2eff058-8f3a-51cf-b502-8a7bfe7826d9", + "version": "MTc1NjMyNzk4ODY3MDUyNjAwMA", + "digest": "sha256:e7abf1dd6dbeb9b892ab4bc2a9b4b8a8be041c1f7fdc818305698a1988321793" + }, + { + "id": "b2f18383-38c2-5c44-8ca4-fbb03623cb8e", + "version": "MTc1NjMyODE3NTA2MTQ1NDAwMA", + "digest": "sha256:66adb7dfa069e9e1bad07cab17d27b5ea7010c2198a4027ce4eace052ed49465" + }, + { + "id": "b3099564-d2a3-502e-abc5-79ce8bd4c769", + "version": "MTc1NjMyODE2NjQyOTc3MTAwMA", + "digest": "sha256:b6fdf70d84a0dfd59cac65357c6724e4f16fc9aedce00bb9236037ad51948001" + }, + { + "id": "b30d5bac-b7fb-5d3f-b3be-1de3502a267a", + "version": "MTc1NjMyODAzOTY1MDI0NTAwMA", + "digest": "sha256:5e57e4719a65be629af91798f1e8125d51df334c76ba230b5ea350cb2d4a964e" + }, + { + "id": "b30fbab2-b776-5067-8887-855bf1ff83d1", + "version": "MTc1NjMyODAzNTk1ODI3ODAwMA", + "digest": "sha256:ac03c9e715d4fc3d381df5a54821611aa3d7b88279402f11f0a1ed1d17f6bcc7" + }, + { + "id": "b318ffd6-54ea-555b-b2f4-30c5c9997bff", + "version": "MTc1NjMyODA1MjYzNzM4ODAwMA", + "digest": "sha256:e19e1b10070b9ada00808d94a24b2c5763a4565a8319c37ee4a9feeed2c3e742" + }, + { + "id": "b32b3853-52d4-573f-8989-065b282fcbdb", + "version": "MTc1NjMyODExMzUyNjM5ODAwMA", + "digest": "sha256:a86764261007c72a4a526dae61144007bf7b35e8cb500b9bb46a7eb5bbb911df" + }, + { + "id": "b32c24f3-dae0-52bb-ad01-68107c1e8991", + "version": "MTc1NjMyODA3MjU3MTIyMTAwMA", + "digest": "sha256:eb16737572be0dd3c003b7c3d87e1c107cf8e7f20e7933865c18318f8422559e" + }, + { + "id": "b32fe6b3-1d6d-57a9-93f7-f8f7a34199cb", + "version": "MTc1NjMyODAxMTYwODk4NjAwMA", + "digest": "sha256:7449269b480ebbdc3206519f69dcb863b17ed594b3b99234140def34bbd2d955" + }, + { + "id": "b3300944-bf7f-513f-a426-a2f5a1c16dd0", + "version": "MTc1NjMyODA1MTIyNDcyODAwMA", + "digest": "sha256:e504d1fd43e587f891055652805528f797e482b73c0e48472109adc7686984a8" + }, + { + "id": "b333b272-cc12-5783-83f6-64b6f6c77c9c", + "version": "MTc1NjMyODA5NDUxOTc5NjAwMA", + "digest": "sha256:f020a108c218bb9bdf7028bd5328da59203fd6f31064a7f2e7c70e2f4c39c4d9" + }, + { + "id": "b358684b-a22f-57de-9b95-677b343fa211", + "version": "MTc1NjMyODAyNTg5MjU1NTAwMA", + "digest": "sha256:9ab5dd250b46119c5aba020d529e0e9b5f2dec128bc0e64b03b91b1fc71fdd58" + }, + { + "id": "b36f23de-850d-5ed7-812f-9bd7ef05f8c4", + "version": "MTc1NjMyODAxMTg4NTM3NjAwMA", + "digest": "sha256:26f606deb8230892af24684ca5a87df082da2b276223807faf5ea96b381bd278" + }, + { + "id": "b3738aad-38c6-54fa-8c85-5e6698c11201", + "version": "MTc1NjMyODE2NjQ3MTE5MTAwMA", + "digest": "sha256:464d08b62a7ab39945d487f918d81411657d8eea92e319be14d3041ecb561163" + }, + { + "id": "b389f8b0-d1d2-57a0-9959-c3d27e64d0ca", + "version": "MTc1NjMyODAyMzQwNzg5MTAwMA", + "digest": "sha256:59af20382d43778e6b6adce7a593e13bc4b39792289f816045efa8a3a5afd742" + }, + { + "id": "b39e4f6a-fef0-5849-8bf3-7995a78aeb14", + "version": "MTc1NjMyODAyNTg5MDcwMDAwMA", + "digest": "sha256:7d734487c3f213a2d5316f087d81102e54c6c83030bb4753418841984806ecc6" + }, + { + "id": "b3a4540e-9d81-5a89-87fb-29fd9849a124", + "version": "MTc1NjMyODAzOTY4MzE0NzAwMA", + "digest": "sha256:4f46e30b9ad09df2d3a0ef252573185473d3dd8ef0d09d5e79ad27038f96e7ed" + }, + { + "id": "b3d8835e-c1e3-5551-989e-666dc51955f6", + "version": "MTc1NjMyODAyNTc2Njk3NDAwMA", + "digest": "sha256:a6ae4746e29d003033e9eac8ce786c87fdaec6cf64fbc33c2f1e1b5b49a5a1e9" + }, + { + "id": "b3e54b09-fbe3-5169-b6c2-8e0468106eb6", + "version": "MTc1NjMyNzk5NjczNjQ2NjAwMA", + "digest": "sha256:64573d4fa94a861a75f4062475d9e34c49281ad53af80f9ce7d84d11fef2bd76" + }, + { + "id": "b3e90799-f51b-50fa-8d39-64a0b1301ba0", + "version": "MTc1NjMyODAyNTg5OTA0NjAwMA", + "digest": "sha256:1c65c301138973e522f1b18e7a52c1bd9ff206876a57ca3209a0f2a75685691a" + }, + { + "id": "b400054c-dd66-5561-a664-8d0296ff006c", + "version": "MTc1NjMyODAwNDAzNTEyMDAwMA", + "digest": "sha256:9508f4eede1c81790c3bab6509e0aa5e9fcb6ca3b1e0e7702d09536827d1117f" + }, + { + "id": "b44367d3-7e02-5bf0-a6aa-f8ccaf8aa02d", + "version": "MTc1NjMyNzk5NzA4MzQ3NzAwMA", + "digest": "sha256:5df49f25ee803afe39228830db5e0cbeed165222adcb30a4c115e59af44bdc48" + }, + { + "id": "b4551398-9d8c-5242-b69a-4e5564ccc1c0", + "version": "MTc1NjMyODA4ODQ4MTQzMjAwMA", + "digest": "sha256:eea8347329e49380fff49d39ec2b41c91094bf43e024f02b59b8d3116c6a6836" + }, + { + "id": "b4665b84-3c2d-57f3-af4b-64e15a1631a8", + "version": "MTc1NjMyODExOTgyMDY1NzAwMA", + "digest": "sha256:d8bc68bbf5af376de06d782814202d730cb2e0385e7d285256fcafe164be89bb" + }, + { + "id": "b467ff52-9459-5151-948a-f42b205b7205", + "version": "MTc1NjMyODEwNTI5MjA3NzAwMA", + "digest": "sha256:aa2180ed1ac4d24b4df6f1c337dcbf2694494d38198ecc0a17a02a768c4a5b42" + }, + { + "id": "b4ae43d3-e707-57b7-85f0-ac7d91838d92", + "version": "MTc1NjMyODE2ODkxMjQ4MDAwMA", + "digest": "sha256:3f585c14222ed3ef77ed480b081f8a8116f687080e2c3bac746f371cad213c89" + }, + { + "id": "b522e388-69df-58cf-b60e-a438c79f6aaa", + "version": "MTc1NjMyODE2ODg3ODgyMzAwMA", + "digest": "sha256:7b411b27d759bb5098c3ab8bb6c22e882145a8ae2cd8c60ec2336d897b767def" + }, + { + "id": "b529ecb1-b2e9-5091-b4a3-f41cd468fb7c", + "version": "MTc1NjMyODEzMTA5MzQ4MTAwMA", + "digest": "sha256:7362a83eb468baa438469912d1184e83593d0c4530de8368b639751a6108f4ea" + }, + { + "id": "b54dd92f-7516-5d41-9583-221f450f9d19", + "version": "MTc1NjMyODEyMDQyNDU5MzAwMA", + "digest": "sha256:016e300e3aeff57cd31aa9cfc13c152e333b76c6ee5d65b7655f206190aff3d4" + }, + { + "id": "b58126e9-a805-574b-974a-85606398fdbc", + "version": "MTc1NjMyODE3MDQ1ODgwMjAwMA", + "digest": "sha256:28d7993a89a20a7e3f026094fe21c54aea014f12fecde97ef5d49cafa2abbd91" + }, + { + "id": "b5af58bb-1e73-5ca8-b5cf-f0e8dd592afc", + "version": "MTc1NjMyNzk5OTMzODI4MDAwMA", + "digest": "sha256:8b181a935ec91cac63fae5ef033fd1e21106344aa413143ab0334be686d30fd7" + }, + { + "id": "b5ba9722-0393-5264-beb3-124698d92e92", + "version": "MTc1NjMyODA0NTg3MTgwMjAwMA", + "digest": "sha256:2a346782341a85a9176a4de16caaff44412c1cf2f62de21abf7cb30cf4ac9f85" + }, + { + "id": "b5bb0081-7501-51d7-9a35-4133d3f5a32d", + "version": "MTc1NjMyODA1MjQzODk2MTAwMA", + "digest": "sha256:420d9447c893cbf12c8b4883202fb38c67e3cdf8be198f647c1e83178a8648de" + }, + { + "id": "b5bea1dc-18ca-517a-b4d4-7884b69adeed", + "version": "MTc1NjMyODE1NDUzOTIyMzAwMA", + "digest": "sha256:ee33d88c0ab3ca3adb73986b900e16a8df7027e6fb156fc9fcb1e4d3135e2b50" + }, + { + "id": "b5c80d7e-2998-50ed-806b-bea1d87af397", + "version": "MTc1NjMyODA5ODY1MzQ2ODAwMA", + "digest": "sha256:bd588f7bf296845c12d6aceeb4ff807831c7c33060d68c3950b85ca5e712688b" + }, + { + "id": "b5dc87ec-9762-516f-aae0-607d46221f5f", + "version": "MTc1NjMyODAzODg4MjE1NDAwMA", + "digest": "sha256:e8b9bb291ca297b5e539a002d70d338abf737d2d0cd98903dc66ef4cc532fa0a" + }, + { + "id": "b606c874-5a44-50b2-9acd-25c20538d7ec", + "version": "MTc1NjMyODA4Nzc2NTYwODAwMA", + "digest": "sha256:3411f16ec27e2652cdbc92dba10f81604b957a59688e216913dd2423c1021cb7" + }, + { + "id": "b611b731-d1fc-5d6e-8912-db6ffae264c2", + "version": "MTc1NjMyNzk4ODY1ODk4MzAwMA", + "digest": "sha256:d921ac40ae6fed6a50e6007c1b86e2681ec52cba1206778b2a0564d8dbb49f3d" + }, + { + "id": "b61718ab-7127-567c-95e3-563f205a587e", + "version": "MTc1NjMyODAxOTMxNzgwNDAwMA", + "digest": "sha256:c6ad0869858844e94b6a0a811b40c5eec791219acc999bb618e6c6a8673539b8" + }, + { + "id": "b622cc85-30da-51a3-848c-db152f95e7ab", + "version": "MTc1NjMyODE2MDQ2MDExNTAwMA", + "digest": "sha256:f7a9fb3c5e74b94928518eeddf721f77905309d0ebec3716a98f54e21b5503b8" + }, + { + "id": "b6284265-5eaf-5290-b0cf-3071a04bde25", + "version": "MTc1NjMyODA3MjU2NTk4NDAwMA", + "digest": "sha256:bec07e699ac90fa0191cac90253acf6de5500ec2c5c55e437de3722b849e2cd0" + }, + { + "id": "b62f17aa-66eb-53e1-b2bd-c9c8e34d820d", + "version": "MTc1NjMyODAzODY4Mzk4NjAwMA", + "digest": "sha256:2468afbe2b913f064beda541fd631dca4814db0e2e5562866f018bb6b10dd9c2" + }, + { + "id": "b64273c4-3090-5dcf-b740-705d5818070e", + "version": "MTc1NjMyODA3MjY0NTMyMDAwMA", + "digest": "sha256:587233e01f093eebf869060c541b3df8af14125d2031fd1ed6e70862dbf32c78" + }, + { + "id": "b64f7cca-9cb2-551a-954d-1d54637df727", + "version": "MTc1NjMyODE2MDQ5MzMyNjAwMA", + "digest": "sha256:1f626ea2adee3d795284ac7a607d90dfc11d4643e6a5d3cbe15190f6115222fe" + }, + { + "id": "b6558a02-ca03-50b8-aaff-8605b0235a94", + "version": "MTc1NjMyODAzODczNjYxNTAwMA", + "digest": "sha256:a436819f05d2001ca5afea965a0459d30bbd0f30cab373e838a683f7847825f6" + }, + { + "id": "b65b44d7-015b-527d-bbe3-fee221f2ff0b", + "version": "MTc1NjMyODA0NDkxODcxMzAwMA", + "digest": "sha256:c10f6346720941656bdee894a5258a74d60c347ea188ca2489cb7d27d6be7920" + }, + { + "id": "b68c2829-f99b-5b57-923b-e53f5b4991d6", + "version": "MTc1NjMyODAwNDAzODE0MjAwMA", + "digest": "sha256:6c72ec3efd0a7b3641c2300db8e0f4391903de59c2423ba31b93934400ae9326" + }, + { + "id": "b69a8126-dd6c-548a-aa21-b63d48236d21", + "version": "MTc1NjMyODA5Nzg1Mjg0OTAwMA", + "digest": "sha256:eca57f118d69ef46b630361dd6d026bf71514351f758d3c04dde59029c17ca4f" + }, + { + "id": "b6bd58c0-68d6-5136-b4aa-3a192d916f52", + "version": "MTc1NjMyODA3MzE0MjI0MjAwMA", + "digest": "sha256:ae5bd9bc1c917d3d432d4c3c9bf5dfbc9d26df283184d806897e7c411d52676c" + }, + { + "id": "b6e1f2ab-95d7-5c1e-a1f4-c3d9dbc363ae", + "version": "MTc1NjMyODAyNjg4MjgwMTAwMA", + "digest": "sha256:36d721bdd08af6197f7bf4e300c0c46919c0abf522db6bbab49f43dec8820500" + }, + { + "id": "b6e37174-097a-5869-9fe4-500a89dd20ac", + "version": "MTc1NjMyODAyMDU0MjY5MDAwMA", + "digest": "sha256:7360d90fe3c22fe7861930ef5eb2bc98655d6e4d5be46f83be7f3800a68b0254" + }, + { + "id": "b73eabdd-e3b8-58c8-96e8-8efa4ff8a802", + "version": "MTc1NjMyODEwMDk0OTM3NDAwMA", + "digest": "sha256:fa867581cfe1519702c7e6fb44f14e3d57eb0176da60bd4adc169b257db1d01e" + }, + { + "id": "b756b5ad-128e-5017-a739-eecfaaa8de6a", + "version": "MTc1NjMyODE0NzU3MzIxOTAwMA", + "digest": "sha256:3fd5dbeba1d92eb5d5c75b38877198b172dcf2b22a04ca12549fcf80e277c204" + }, + { + "id": "b75c1191-ed93-5a86-b2b1-459941010c5c", + "version": "MTc1NjMyODA0NTcyODQ1NzAwMA", + "digest": "sha256:99da1e8e23c208fe39eb9880d28137e4e7058e514d355c77294ece660708c3ba" + }, + { + "id": "b76717ec-3d5e-5f1f-84d1-889484832f03", + "version": "MTc1NjMyODEyNDQxMTIyOTAwMA", + "digest": "sha256:6505a9c25e07a9dd999c3b91434255b302e6009b3bc7e35f04ebd41dc0f7316c" + }, + { + "id": "b782079d-924c-5fe5-8ff7-2f96085a30bd", + "version": "MTc1NjMyODA0NDgyMzg2MzAwMA", + "digest": "sha256:05acfac7c476787e42a4e4926a15d0b705075e61df1ad6d38865c634f93a86a4" + }, + { + "id": "b796cbf9-ffc7-55b7-a336-97e49e792709", + "version": "MTc1NjMyODAwNjIxMzcwMzAwMA", + "digest": "sha256:55fd7b865016b8a46590bee2ed2e384854015ed0ec2d4f3fa1d60479f7c44552" + }, + { + "id": "b7ad139f-0f35-5872-8f16-e62115899b5c", + "version": "MTc1NjMyODE1NjUzMzAzMjAwMA", + "digest": "sha256:bc2fea4dceadbe4f275635be440e4cd654f27e25d1807971c2bb675af53b24ed" + }, + { + "id": "b7b5a3bb-9425-564c-b6f7-cd54a1a5d028", + "version": "MTc1NjMyODEyNjUxMDM3NjAwMA", + "digest": "sha256:97c91d627c313c828febe9d1ed0fbf7565059ffd9fc8a32455a3f449e3a10ec6" + }, + { + "id": "b7bc16f7-bedd-57a6-b655-0f4a33962f3b", + "version": "MTc1NjMyNzk5NzEwMTI1MTAwMA", + "digest": "sha256:8db44569814ac5d01c560c3223084d19606ae1d6cecd7292d5a6388155d9ffa2" + }, + { + "id": "b7be0b2b-f8eb-50f4-a41a-05c3de3520fb", + "version": "MTc1NjMyODE1OTA4NDc4NDAwMA", + "digest": "sha256:b61bbaab6ae8eb5128459546d04f723393c70fbe14aa1ea478c00095517a0638" + }, + { + "id": "b7bee52a-05fb-5e52-b544-a29ced75e37e", + "version": "MTc1NjMyODEzNzU5MTcwMjAwMA", + "digest": "sha256:5399e46b93243a5bfc37d31f2b388ae2fdf0c056c6c0175bbfb080a59704a94e" + }, + { + "id": "b7d25633-4bd4-5670-83b4-b19d3ca2007a", + "version": "MTc1NjMyODA2MjA2ODIxMzAwMA", + "digest": "sha256:86f6b665d6cb8231238d36cd91e3b697b399af0500dc55d75e8f1f2d4bc46b5a" + }, + { + "id": "b81eeb25-e9f3-5fd3-825a-23e900fdd12a", + "version": "MTc1NjMyODAxOTM2MjIyNDAwMA", + "digest": "sha256:cbe8bf81f0bbce828eac48ac1034b28c35a4e120c3ced943b075725e4f762930" + }, + { + "id": "b8274142-2b34-52bb-b604-acc7b12ad01c", + "version": "MTc1NjMyODAwOTAxMDU4NjAwMA", + "digest": "sha256:bd19f2412e6603a6262c14aba28b9125a1c5a61d19fbe9dbb3a1b1b921b61e6d" + }, + { + "id": "b828ba70-748c-5c07-8c94-0119703142c9", + "version": "MTc1NjMyNzk5NzA3NzcxODAwMA", + "digest": "sha256:ec99090422efa0a8354cc2df822bc0a68e49dcf1e707bd16a5d9535539a4f3cf" + }, + { + "id": "b84b7940-0774-589b-b139-1fc423d29486", + "version": "MTc1NjMyODA1NTY4NDYxNTAwMA", + "digest": "sha256:cc4fe2ca26fed44fe03edc96d30a37424327579f0a1e35a6e28cb4ee4747c5b4" + }, + { + "id": "b85ab758-14ea-5d28-92db-a1ac71bafff1", + "version": "MTc1NjMyODAwOTA0MTYxOTAwMA", + "digest": "sha256:c82b9ec24d61a6ab35680d5eef934125e9ddcb10d88936002d71499302f385d0" + }, + { + "id": "b85defd8-d92d-546f-93c9-2975b887e09f", + "version": "MTc1NjMyODEyMDQ4NjQzMjAwMA", + "digest": "sha256:0debb0d3159387a7199d8cf9399cd8c56390b8c92240322244236377ac12cd43" + }, + { + "id": "b864cbef-58ea-5980-8b0a-b9497e678aa4", + "version": "MTc1NjMyODE2MDQ2NDM0NTAwMA", + "digest": "sha256:23ab2edc042e4e777a870ee442122d89f79f5904dd82dbc0a8ef3226e830e4ee" + }, + { + "id": "b874385c-4be8-5d8d-bfbe-44fcd8310b78", + "version": "MTc1NjMyODA3MjU2NjU2NjAwMA", + "digest": "sha256:62c328babd6709ea8f1eff04581fcb25c0831917d09f8c56587673805f12e19b" + }, + { + "id": "b88d0b08-7991-5ce4-ab8f-787eac3271b0", + "version": "MTc1NjMyODE3NDcyMzA5NzAwMA", + "digest": "sha256:e922e2dcf3886ff1be62df11df34d59bb437377d47741637946f8fb8c5ac125e" + }, + { + "id": "b8a8912f-a770-5d60-a1d2-7d367e13822d", + "version": "MTc1NjMyODAyMDUxMzg5ODAwMA", + "digest": "sha256:c09bf30f8fbad2ef1bcad0a456324046fc48be7392a32cf18dfaa61a9ea61fc3" + }, + { + "id": "b8af77b8-8bea-560c-8edc-b436272add2f", + "version": "MTc1NjMyODE1NTEyNTA5MzAwMA", + "digest": "sha256:fd007c702879fa3b7692a45c8c7f6e709f52f3b5a5e0312c6b9185707303e166" + }, + { + "id": "b8bb16b6-c444-51e1-a1bb-d55cadd5e0ff", + "version": "MTc1NjMyODE3NTAzNDUwNTAwMA", + "digest": "sha256:1ef4ab1ee1219f77b2048ecaf9e7fdaad7714e9ce72be2ecb02928571496e2e5" + }, + { + "id": "b8bf7691-f4ae-5ed0-9851-0762eed95f20", + "version": "MTc1NjMyODA5NDQyNDY1NjAwMA", + "digest": "sha256:bff93bba4f415dc46b31d0042c2ea11f5bf35b00f3385461d01d4a392f119019" + }, + { + "id": "b8cc9df2-880f-5f76-a856-065d8b912403", + "version": "MTc1NjMyODA4Nzg3MTg0NzAwMA", + "digest": "sha256:e2f0adc3dd19850154a4b6918158761493d191e2143289c55ee817ca2c057d38" + }, + { + "id": "b8dc4502-7248-5c98-bdcc-c5c09788c44e", + "version": "MTc1NjMyODAyNTg5OTQ4MDAwMA", + "digest": "sha256:903e3b4a564a8803f5636fd251657624e4049c4e7cab5f6ed80b6d126aa2479c" + }, + { + "id": "b8dcb84a-622f-54f4-b498-d8e40bd381a1", + "version": "MTc1NjMyODEwMDk3ODAxNjAwMA", + "digest": "sha256:960e74a8b82f51fff10d193dc3d7c81b877467912e0fa8fb40eaf244b1341f2c" + }, + { + "id": "b8e93027-5040-50bb-aee2-d30fd96895ff", + "version": "MTc1NjMyODAzNDcxNDY5NzAwMA", + "digest": "sha256:46206e37ca6615c758bc8a19782debca565afda051d130b2b46f328644bd5e24" + }, + { + "id": "b8f62e90-2662-5339-af22-e7a2fcb15f95", + "version": "MTc1NjMyODE2MzIyMjg4NTAwMA", + "digest": "sha256:314074e90dc662c75237fc2788a9340a21b563844da428ed8fe47abddb919148" + }, + { + "id": "b8ff4026-3f11-5a13-94cd-79e6134a7c1d", + "version": "MTc1NjMyODE0MTQxMzIxNzAwMA", + "digest": "sha256:5e18d98b335f6bc839a60914f97c5dd126718ea5a12e934a5c3505ca91c34821" + }, + { + "id": "b92c9c6d-94db-52ca-a9ad-23a4b4156a99", + "version": "MTc1NjMyODEzNzM5NjQ0NTAwMA", + "digest": "sha256:5c134a312238e90e40f333d1c16180bf9e3dbbbc3528608d92857dc53abd8484" + }, + { + "id": "b93075d9-da6b-59bc-842a-9e6c90743148", + "version": "MTc1NjMyODAzODgwMTUzMDAwMA", + "digest": "sha256:acfe018ab221df6b6f5ab76df0114d3a5169cfeec770b8c221a47f88c46dcd73" + }, + { + "id": "b9663852-6a9b-5736-ae82-cc6cc07dc0af", + "version": "MTc1NjMyODExOTExODQ1NjAwMA", + "digest": "sha256:c0eac038b954789a73c0e496e7d0da9ba2d17193498a40f867a278c0ddaeeed7" + }, + { + "id": "b9905b0d-8309-5213-b549-d8dba8b91ea4", + "version": "MTc1NjMyODExMzU1OTM0ODAwMA", + "digest": "sha256:ca2e6f57272f1075fbdba4f16b77df0b0cef06d7bcc49a1f13aab23c35061361" + }, + { + "id": "b9af8b2f-223d-5187-880c-37a15f15ca8d", + "version": "MTc1NjMyODEyNDQ3MDE1NDAwMA", + "digest": "sha256:891276e5fb96f252c13e91589d07814157bdc8790fd487b22bf7d15b3c5d0535" + }, + { + "id": "b9bd7f99-187f-5653-b618-e8e7dea76fbd", + "version": "MTc1NjMyODA3MzA5ODA4MDAwMA", + "digest": "sha256:7c0f9df2ff28204d33d36f2726d6011de3a777b67aa96f5ec4b2963c5f3dc161" + }, + { + "id": "b9dec5c8-89ee-557f-b2f6-d2fc05faf813", + "version": "MTc1NjMyODE3MjUyNzc2NjAwMA", + "digest": "sha256:712b6c49574e71047059c015a554bdfc5726002c394bee9ec2a825159ae84b97" + }, + { + "id": "b9df3bb2-2aca-5fcb-af81-b67f779be8b3", + "version": "MTc1NjMyODAyNTYxMjQwODAwMA", + "digest": "sha256:2651ee3a7f3c80e1ab395263b9df67e2bdd4fa517ff761ff8f5a4ed125b288fc" + }, + { + "id": "ba008be3-ae53-51bf-9b02-da373307e703", + "version": "MTc1NjMyODA5NzgzNzQ0NTAwMA", + "digest": "sha256:184e34a5892b316b8770d2aa3f7c369c99dc36f670ccea97ff27cd1da459b6f3" + }, + { + "id": "ba32e61f-d94d-561c-84e3-8040e59ec2fb", + "version": "MTc1NjMyODAzNzc5NzM1NTAwMA", + "digest": "sha256:7f4b54b27991345b051f476b03edcd3c4e26dd4ae4598c7ffa4c797692e24e8b" + }, + { + "id": "ba4d440a-27e9-5ff4-993d-60053e237e0d", + "version": "MTc1NjMyODEwMjE3NDY5ODAwMA", + "digest": "sha256:c6e4ec9f9842a97a2897d95124c069fddfa2aad55466ac17b0847a4f7db3f056" + }, + { + "id": "ba5747f8-c13d-54c3-b5ef-8d460c661f29", + "version": "MTc1NjMyODE2Njg0MDA5MjAwMA", + "digest": "sha256:6c8f580588af4daf95a672835b5bc4be3e16dc202b7557c815c94a0e6415d2fd" + }, + { + "id": "ba6e2452-8de3-5e1f-91c2-9c960dfd5264", + "version": "MTc1NjMyODA2Njk2NDY4NjAwMA", + "digest": "sha256:0b67634fbe10e2b5f7dbe774ee14a74812623a5f778b3ac65dfe61f756c9d40f" + }, + { + "id": "ba6efd41-5332-5dc3-a84d-a9894cf84eb3", + "version": "MTc1NjMyODAyNzc2OTA2NjAwMA", + "digest": "sha256:84c687f233d8e141ef333e9354ec269968b1adbd7f455c1383b9597a05549a8a" + }, + { + "id": "ba93f423-13d6-5918-a177-cd2fa041f305", + "version": "MTc1NjMyODA0MTk5Mjc1NDAwMA", + "digest": "sha256:f0ee1d13529410b340e30785c6b835166a0ac97039f5c8e734eab1577f7539fb" + }, + { + "id": "ba9c71e2-8055-5ece-b71c-281e1d464afe", + "version": "MTc1NjMyODE3MjQ3NDk3OTAwMA", + "digest": "sha256:8eeb74e435ec0e3bda1d352899dd4b47c6478d4b4219b44c745263fddb970b80" + }, + { + "id": "baab4490-b865-536b-975b-78d5df3bcec2", + "version": "MTc1NjMyODA3MzI3NjA1NTAwMA", + "digest": "sha256:dc94a151145936c82b9b8932c5a70874dad9af442100a7568715fcd2cb2f49cd" + }, + { + "id": "bac735d7-e694-5715-99fa-98956b33253e", + "version": "MTc1NjMyODAxMTYwNzcxOTAwMA", + "digest": "sha256:d1e8cdf142c28948e78d9905d7694604d78484bb4c51b5beb20e715e56f6d7ab" + }, + { + "id": "baeefe51-b0ac-5192-b8db-24c89b07b2c6", + "version": "MTc1NjMyODE2Njg5NDIxODAwMA", + "digest": "sha256:c3718ae881ed5129fd06a078daf0bac6b3c735e33299c0d1f2a5bf5c199c8fe3" + }, + { + "id": "bb037bca-ed94-52ec-8c91-8a9a6c0a75e6", + "version": "MTc1NjMyODA2MjIyMTAxNjAwMA", + "digest": "sha256:660c74ed07bcef09f6a8009c63d6186f63aba5eb076dcb1ed49040cf9d914f49" + }, + { + "id": "bb10600c-ae06-5b0f-97bf-1faf500a5853", + "version": "MTc1NjMyODAzNTg5NTAzNjAwMA", + "digest": "sha256:ad23d74a7f4de80a1a4cba98ca30f291a84da1f140d098e33ad274bb1c0f3feb" + }, + { + "id": "bb41f7b4-d3f3-5976-8a7a-b02b16937259", + "version": "MTc1NjMyODAzMDczMjI3NjAwMA", + "digest": "sha256:744524e8cf6755c40cfc4fb8a78cca83d060373cda798176db9df3f365586b20" + }, + { + "id": "bb5126af-9d60-5403-9656-7af790ac4a5f", + "version": "MTc1NjMyODEwNTMwOTQ2MzAwMA", + "digest": "sha256:1c9d78d877c913cbe5395523c4dfb89ebbfdd340755ef8d221512a2ed28f2ee2" + }, + { + "id": "bb6298c6-1018-5afa-ad70-6affed483d03", + "version": "MTc1NjMyODAyNzc0Mjg1ODAwMA", + "digest": "sha256:8977d5aad917a32beaf3c916b35dd90466c1b34dba1c7f214bc8c489d0239a82" + }, + { + "id": "bb7ede6c-2093-505b-8bf1-1a40747f7fb3", + "version": "MTc1NjMyODAwOTA2NDc0MjAwMA", + "digest": "sha256:04a0780610d5b5eb9d98d1bb4ed0c81734ad3796540f1b9d6d428fecc0cc90c8" + }, + { + "id": "bb809a9b-9315-5bd0-87a8-305f3305e961", + "version": "MTc1NjMyNzk5OTIyOTI0NTAwMA", + "digest": "sha256:68ecc0c9a1c73adf80ba6abadf2bc692ae187e6e0c3d28b45a2fbf7ebf3ecf94" + }, + { + "id": "bb9e85f3-3e4d-54f8-8926-3b5ac7d4c5fd", + "version": "MTc1NjMyNzk4ODY3MTQyNjAwMA", + "digest": "sha256:839da26567290af8bea86b469b2f009f554b258596673d723d97d4aae5b38d6b" + }, + { + "id": "bbc5d954-7d28-5abb-bc7e-012e6dd4a22c", + "version": "MTc1NjMyNzk5NTMwMzM5ODAwMA", + "digest": "sha256:8ed67ade242f74536808c3694d7bce07bb827ccd4167e94b37c6a9759f9554d3" + }, + { + "id": "bbdafc3e-8437-5865-957f-1f9e6472191d", + "version": "MTc1NjMyNzk5NTM4MTI0MTAwMA", + "digest": "sha256:7b3acc961ac0cbd935a9dd1952ac01f80779908218bf25fd467bb7282f19f274" + }, + { + "id": "bbfc1ba4-6b76-5716-8a28-13fc1314aa99", + "version": "MTc1NjMyODAxNTU1NDA0NTAwMA", + "digest": "sha256:683b4ae9333d854edda190a66475cfc37d08025831aa1ad9f4006d6cede137bb" + }, + { + "id": "bc380b88-0f2d-5e95-8d4d-1ec2326d4d21", + "version": "MTc1NjMyODEwOTEzNjY2ODAwMA", + "digest": "sha256:63e97b49dd253826bc957bf751233b173224a741dbb6d953ea55c53ab20d5416" + }, + { + "id": "bc799469-ac38-5bcb-8f55-63d7a6e5180a", + "version": "MTc1NjMyODA5NDMzNTI2NzAwMA", + "digest": "sha256:1abee0ae86ade5eb5f97f12121baefbca83c82c7569352edae9af5f0da9b4f09" + }, + { + "id": "bc83136a-4cfb-5876-a0e2-bae7bc0d8c84", + "version": "MTc1NjMyODE1NTE0MjkxMzAwMA", + "digest": "sha256:a645c63dd50f2ba86d053e7041fffc1a82d0f7043d87fe97ed9f5e632b360ed2" + }, + { + "id": "bc8bace1-5e10-52be-83dd-e532054d1308", + "version": "MTc1NjMyODE2MzE4NzU1OTAwMA", + "digest": "sha256:fd9beeffecebb1cfe44110a3ad345693af020b05216624a008a9ad464a272bc3" + }, + { + "id": "bc9ab5fd-d5b0-521b-80ed-5d3982ba904e", + "version": "MTc1NjMyODExOTcwODA1OTAwMA", + "digest": "sha256:c36668ba0e3776bd2feaeaa00aa395ec06e8beedeca7888d164b1461ab442b71" + }, + { + "id": "bcabd0e4-5d9e-5601-baf7-59e6e257faf8", + "version": "MTc1NjMyODAwMzkzNjQzNjAwMA", + "digest": "sha256:2a58630bbef271ba5da607c808db79e8f904b666fe12590d3f2da6750e93e2ff" + }, + { + "id": "bcc01139-df00-5d2b-96ed-03d393b37e61", + "version": "MTc1NjMyODE2MzUwNDQxMzAwMA", + "digest": "sha256:9a803d9a7aed86cf7ddc93f54c20d25d227db781d2f118a2c02d4640526f030c" + }, + { + "id": "bcf15b3f-9e16-59a1-ad84-f72f229cf08e", + "version": "MTc1NjMyNzk5OTI5ODkyMzAwMA", + "digest": "sha256:d33b52d9235d8139d6ab79878966c8d5882ca3b6456eed8cfa689777f631ae76" + }, + { + "id": "bcf272f5-b7fe-56f6-909e-1af604689bd4", + "version": "MTc1NjMyODA1NTY1MDIzNjAwMA", + "digest": "sha256:987a840b5f346828fdeb7cd4db80a889481872d155efa1c124f7ede9d64833af" + }, + { + "id": "bcf7bc85-feae-5c2f-a6dd-795467599dec", + "version": "MTc1NjMyODA4NzY4MTc2MjAwMA", + "digest": "sha256:e71fef103b857b75552b3658ea6e481eaabf30ed6ed8e50e4bc165048b692d0c" + }, + { + "id": "bd009c75-70bb-5802-b8c1-0f0b0a270ebc", + "version": "MTc1NjMyNzk5NzExMDk1MzAwMA", + "digest": "sha256:22c9ff14905380bb075dc0e54804e95202a996431a6c6006173209ec80f2fa41" + }, + { + "id": "bd086cb5-2c1c-5244-8e79-74a73d05af69", + "version": "MTc1NjMyODE3MDQ1MzM2OTAwMA", + "digest": "sha256:fcaedb562ca144ff9c37691ce46521b2da65b81f628d88abd7d7895404d5b0ba" + }, + { + "id": "bd0ffcc0-7685-535e-9147-978fd72e987b", + "version": "MTc1NjMyODAyNTkwODczMjAwMA", + "digest": "sha256:986be8893d153e22203d9ef85b16a4bd60878158968dd298c9ff6ca3f9a8f434" + }, + { + "id": "bd2131f4-9d95-5723-8957-cbb765dd94f1", + "version": "MTc1NjMyODExOTEyMDMyNzAwMA", + "digest": "sha256:850d08c71833014340e6ea8638f63901e8635c08d713b88ecd77a17171dcd1c9" + }, + { + "id": "bd25c490-0a34-5a5b-ad34-01d32ad5bc66", + "version": "MTc1NjMyODE3MjUzNzExODAwMA", + "digest": "sha256:2d50f448bcf69c8dffb5499dad0a4bb4c6e5a7acd1df038baa6de97918d02c75" + }, + { + "id": "bd32f7f3-0237-58d9-a5ee-2e5b50aaacf6", + "version": "MTc1NjMyODAzOTcyMDk5NDAwMA", + "digest": "sha256:67ec8185f3e8779e2fca2a1b2ac2235f23bb46459a87e489e579af97f65f11cf" + }, + { + "id": "bd36de10-0749-5a9e-b271-dfb589ff87eb", + "version": "MTc1NjMyODAwOTA2OTcwMzAwMA", + "digest": "sha256:ce6d4f7eb67288a6b9ce721e7332f868893eee85c549423b3ce4aabe07140f6d" + }, + { + "id": "bd420074-3e84-57f7-b04d-febde3146271", + "version": "MTc1NjMyODAxOTM0OTc2OTAwMA", + "digest": "sha256:5b208896771b3f5c81de3aa0f98151e50330de17d8bf2a8e2efcaf3ca89b4476" + }, + { + "id": "bd4e3e16-888d-5d6c-a80d-a54b1e635ad2", + "version": "MTc1NjMyODA2NzAwOTMzMTAwMA", + "digest": "sha256:eb2587cc85c47d80fba816fb359c441398a25bbc165e0166208d0768d0ff4aed" + }, + { + "id": "bd522f42-7e0e-53e7-92de-2d9e12ef2cfd", + "version": "MTc1NjMyODAxMTg1MjU4ODAwMA", + "digest": "sha256:afe2ae93b87e2fa35f77d92ebdf7c34ef39179cac362bd49883a3f4c11470d9e" + }, + { + "id": "bd652c81-ff25-537b-a651-a480f351f758", + "version": "MTc1NjMyODE2MjM4ODQ1NjAwMA", + "digest": "sha256:1b7b02d8b66a51bd3882d39a0204cdde94a9190dbfd4f38146eaff2b3ef85db3" + }, + { + "id": "bd65c742-6bb4-5c5c-92d0-5ba68077be08", + "version": "MTc1NjMyODA3MjUwMDczNjAwMA", + "digest": "sha256:4df32e19a54c9e3e1e0ea225ab33954e5773bc2a9c2b66637ef93312d6592256" + }, + { + "id": "bda97e8d-5e36-5c3f-9b85-55d0686b8110", + "version": "MTc1NjMyODAwNDA1NjI0MzAwMA", + "digest": "sha256:d774bb20873b2d8922fddd9cba0d8ab36c6fcfc94b887bf7275420723bb38cd1" + }, + { + "id": "bdaae2d9-4e1c-5492-bd58-57869b0c696b", + "version": "MTc1NjMyNzk5NTYyODM2MTAwMA", + "digest": "sha256:35cc306f6628ef01fe297fb08865a1d1a76dd05af24c9a346ecfa2cdf9d21612" + }, + { + "id": "bdd7f283-ead1-5707-b136-d04e986fea64", + "version": "MTc1NjMyODAxOTMxMTc1MzAwMA", + "digest": "sha256:c79d08ef555a6490195fc9bebd633022c13291e3261b8a4585f25897ffc38f63" + }, + { + "id": "be0850d3-addb-590c-8661-68d332cd1d76", + "version": "MTc1NjMyODA2Mjc2NDkwMjAwMA", + "digest": "sha256:02ae2b0484336d9a58f64aee4696d5037f43ca24c7b11196b8a6b2ad1c1b9793" + }, + { + "id": "be36d51e-ecfc-5436-b754-42084413ad09", + "version": "MTc1NjMyODE0NzcwMzAyODAwMA", + "digest": "sha256:2805e85fb088f6736fce8cdaacab05d358f15a1d57e0f286c8fddec54f1ae641" + }, + { + "id": "be404809-ab5f-509a-aa8f-2b23d872d7dd", + "version": "MTc1NjMyODE1NjQyODA0NTAwMA", + "digest": "sha256:bf0c30c9612e216795b904e9960feca8d9c676e38fb2f11adbdf8b9fee7e82c7" + }, + { + "id": "be64cf5a-4c0d-5cdb-ba8d-e9c0e47ee386", + "version": "MTc1NjMyODE2NzQyNTY0OTAwMA", + "digest": "sha256:43b43bdc40e4477529f48a9feef3471df01ce010b5cb535803abf2cc430c04be" + }, + { + "id": "be743644-e1b4-5415-8073-a804216af629", + "version": "MTc1NjMyODAyNzQyNDczMTAwMA", + "digest": "sha256:977d80afcbe6382c273392f1abb868ffd51fb6d06c0ddf35a10428827042a589" + }, + { + "id": "be8356cd-771c-5c01-b64b-dec3d9cfc131", + "version": "MTc1NjMyODA4Nzc1MDk2MDAwMA", + "digest": "sha256:900356ecc76271bcd53899ccd9bf57f775dcb168ab89bd6ee3f08d484201d911" + }, + { + "id": "bebb4c04-8075-5fa6-b19c-c94bb83cae4e", + "version": "MTc1NjMyODE3MDQzNzA0ODAwMA", + "digest": "sha256:5d3107d364d54670c322e5ca7e23b9512a28fd9360516589ff4ff5d6d15420df" + }, + { + "id": "bebee898-21dd-587f-bd0e-eea1e0f17811", + "version": "MTc1NjMyODEyMjI0MTM3MDAwMA", + "digest": "sha256:407311a5fa223b8a74e4c43062ef83a990b5204ef647dfd40e295818c524f803" + }, + { + "id": "bed92377-b400-5b80-ab24-ada6e8d8eb1e", + "version": "MTc1NjMyODAyNzg2ODAwMDAwMA", + "digest": "sha256:645e110ea6d8b60e75a851137d252cd889e5f49f516d00b1f66911bacd43a9d4" + }, + { + "id": "bef4a36f-760f-503c-ba8c-498fe3a97908", + "version": "MTc1NjMyODE2MzQwMjI1MjAwMA", + "digest": "sha256:c4b46ed11dd07045035628e74afc645b587b0f3e48e2629c535afd7e62f3d7f8" + }, + { + "id": "bef4fb36-28f5-5a60-82f9-ef4e4ca84a36", + "version": "MTc1NjMyODAyNTkzNDYyNDAwMA", + "digest": "sha256:3907cd396a1f6c66b314154c84241f85927d1d53ae0381eb1250612ddc819ca2" + }, + { + "id": "bf0c2a97-cbb8-53df-b9ca-5ed4f12ba939", + "version": "MTc1NjMyODEwMjEzMjMwNzAwMA", + "digest": "sha256:3897989fa984ab6e82914d94d1d7891452722ed70f325279f94defdc4ab3d444" + }, + { + "id": "bf1d1237-9c7c-5e4f-9fbf-b64aa46e7627", + "version": "MTc1NjMyODAzNjg0MjUzOTAwMA", + "digest": "sha256:350889334ae9c17007be195999ceb00695557aaf294bbccb5f3a39abb39a766e" + }, + { + "id": "bf2fe335-5a6e-51bd-8b82-97af93cb27fa", + "version": "MTc1NjMyODAzNjkwNzYxMDAwMA", + "digest": "sha256:f62642526521ed1a8d43df5331f2a2ef83185c18bd338ea9d26df9f1b61aeaa5" + }, + { + "id": "bf3d799b-f5ba-55d3-83e2-4ced526409d7", + "version": "MTc1NjMyODE1OTA3OTM3NzAwMA", + "digest": "sha256:c77c7c5a952ae2a0d84f991949a6d311c95fdae702404cdecddfee101b6d6ccf" + }, + { + "id": "bf45461a-8a7e-5d83-a22d-b590ff20d586", + "version": "MTc1NjMyODAzODg5MzA3NTAwMA", + "digest": "sha256:47e3e1fc19c24557f50b0890be22e0c55fe3c69e6b34f10289d3f69f97c2c3bd" + }, + { + "id": "bf588e35-3a37-5d26-bae0-29ae618c2590", + "version": "MTc1NjMyODEwNTM3MDg1MDAwMA", + "digest": "sha256:20cd19e1db0b6e638e4e3ed415f2ffe834ef34633cda7afd6bc23e7d6a993084" + }, + { + "id": "bf5cde2a-0fa4-5740-b2ee-14f3b53153c7", + "version": "MTc1NjMyODAyNzM0NjQxODAwMA", + "digest": "sha256:f4f394364ff4a51237edd000510d233a4e30386d9d7039998a920d4cdf0e6bdd" + }, + { + "id": "bf676fa6-975c-5b13-ad17-565a7e069517", + "version": "MTc1NjMyODEwODc5OTMyMTAwMA", + "digest": "sha256:eafd1ae6da02e1f41602c41a358b8673d017319570ba6cbde69ea92dcf948546" + }, + { + "id": "bf6ea28e-9176-56dd-a51a-f6bbba83ffc3", + "version": "MTc1NjMyODE2ODk2MDkwODAwMA", + "digest": "sha256:aa88b7d2b52ec7f013c630c1203ece8100c05a6952aeaf328c90c5e7a13d517a" + }, + { + "id": "bf780df8-cbad-5910-915d-ad6d013d2d1e", + "version": "MTc1NjMyODA2NzEyNjMwNTAwMA", + "digest": "sha256:dc958cabb3bae6c193c3694b583a19bf4d69fff04563a98c99f5daa5cb3ed872" + }, + { + "id": "bf9d1d0a-86f8-5f1b-b19a-245701dfe5a6", + "version": "MTc1NjMyODAyMTI4MzYwNzAwMA", + "digest": "sha256:47b74c11472ef777fbd3c91240b2289fe5070ad0e4c62e2826b8e2995ea2a377" + }, + { + "id": "bfa306fe-c692-5b3a-b80c-db59718d341e", + "version": "MTc1NjMyODAxMDQzMTQxMDAwMA", + "digest": "sha256:91a5871cd8d28853f5cffca52534250fdfc61d1e3684b2d1cffd0d0b7faa862f" + }, + { + "id": "bfa46053-efbd-530b-a37c-14d5d6a0e471", + "version": "MTc1NjMyODA0MjQyMzkwMTAwMA", + "digest": "sha256:a4391023449a58b29e161ff9522556189e2525c039a03e389b6a942a1d8ca5ca" + }, + { + "id": "bfaf59dc-b9db-5739-8d0f-b509c0445935", + "version": "MTc1NjMyODAzNDc2OTQwNDAwMA", + "digest": "sha256:cf84e87ca49744c1c793c66b71a79af61c7d320329a63972973a7dcd6fb4a0ae" + }, + { + "id": "bfb4126b-3277-505c-9683-7d88e8659187", + "version": "MTc1NjMyODAwNjIzOTI2ODAwMA", + "digest": "sha256:d86dcd322a4b5e80de49f61243dce074bfda6742beafb2388d63c601635f279c" + }, + { + "id": "bfd5a8e6-1c67-592d-bc94-acc2d2f4583e", + "version": "MTc1NjMyODExMzUwNzg0ODAwMA", + "digest": "sha256:4949a71c76dbc44502d5410d68f5a5e3ce872e2fd831f90f598d7d9a8f12e611" + }, + { + "id": "bfe604d1-8aed-525f-89c3-91b8b7362519", + "version": "MTc1NjMyNzk5NzEyODIzODAwMA", + "digest": "sha256:4e48650c7a638bc857d457e473ea3d1143b7f24f2f3b67deefc4fc74b31b2144" + }, + { + "id": "bfef6e00-54ba-5ce2-a60a-195a5a600ffd", + "version": "MTc1NjMyODAxODg1MzQwODAwMA", + "digest": "sha256:d245d476ed1f3a64e34b110c59c8d9f1886d071e79584b876f4698e52e8932a9" + }, + { + "id": "bff3fa7c-3e77-5628-800f-be9315c52376", + "version": "MTc1NjMyNzk5OTI5NjUzNDAwMA", + "digest": "sha256:d776226a9352dd41dc6a3a6de24db812d6c2a51bcdd04c63cd973884c4ad5ff1" + }, + { + "id": "c0188ef6-d144-5e4a-b8a0-9894f5a939b4", + "version": "MTc1NjMyODAzODg4NTM4NTAwMA", + "digest": "sha256:dd16e7d786efc2f0677ec10697e8885f32e0d4a325387e383248c5471e2d0884" + }, + { + "id": "c031c281-d11f-59cb-baad-a1b7aa02c160", + "version": "MTc1NjMyNzk5ODE1OTU4ODAwMA", + "digest": "sha256:1b99446b11eef039c4e4e19fd8890af394de94df436501759010463615df00f0" + }, + { + "id": "c0510fc4-b1e1-5dee-ac64-363afd60c88d", + "version": "MTc1NjMyODAzODc4NzYwMDAwMA", + "digest": "sha256:b77713ddfab1aa22c7e37952c78fbfe15730bf1d2aebb053e0f6269084496cfe" + }, + { + "id": "c064ccaf-34b9-5683-969b-2251ed428355", + "version": "MTc1NjMyODExOTE5NDc1OTAwMA", + "digest": "sha256:0921d0742a82c5d2a27decb1b3407ed922c9d5e24d3ad5c361c84eb6f9784614" + }, + { + "id": "c06ef7a2-1f9a-5098-bb1b-0fc682bd3654", + "version": "MTc1NjMyODAxOTQwODM2MzAwMA", + "digest": "sha256:27523d897eb64b3a7169284879369ddc9517e665f5f85843184a0a54dcc95e74" + }, + { + "id": "c07d23e2-3cb3-58a7-b1d6-60fc86a71f65", + "version": "MTc1NjMyODEzNjE4MjIyMjAwMA", + "digest": "sha256:409db443d6dae8c1901834142ce35a4bd229d2d6b8effa29b1f115cb5f1473a2" + }, + { + "id": "c083671c-47b0-52a7-9eed-f5b3a502422e", + "version": "MTc1NjMyNzk5NTYzNzUwNTAwMA", + "digest": "sha256:27956475c4c6049db5c1429c457e52c1742a728041de782107d7ed0dda22760d" + }, + { + "id": "c09431ee-50d7-5663-a689-b732dcc42ec2", + "version": "MTc1NjMyODEwMTEzNDgwOTAwMA", + "digest": "sha256:0422badd7e56e908ab747918578302f9ae68160de977d3f7a3b637806bc4ed89" + }, + { + "id": "c09c71c6-4c87-56b6-88c2-fddba7287e85", + "version": "MTc1NjMyODEwODg1NjUxMjAwMA", + "digest": "sha256:e27ff9f480d0e4591342cd37bb1a4da60a6e47a5b5ea8675fe03b04cdcb8df0f" + }, + { + "id": "c0ba33a4-4cad-5083-a2d9-0da1795a396f", + "version": "MTc1NjMyODAxNTU0NjQ5NzAwMA", + "digest": "sha256:5a9b0d9d1510226de5f87535de5f9380a90c6f5bf09d6872086d0619f177401c" + }, + { + "id": "c0fdbc7b-f7e6-53c1-939f-b2b0547492ff", + "version": "MTc1NjMyODE2NzcyNjI2MDAwMA", + "digest": "sha256:d76431fe142d73dabafdc1d053a1f93985ac988858d805c0d0cb0e7b9521b218" + }, + { + "id": "c118d582-6802-5dc6-950d-3c01d611af09", + "version": "MTc1NjMyODEzNjk2OTQxMjAwMA", + "digest": "sha256:cd4a3d5b22d8ab95cdc0cd2223e43e145a47e900366a32979a526e5e50a4c177" + }, + { + "id": "c14061aa-5754-5197-b363-96714b1b3a97", + "version": "MTc1NjMyNzk5OTI5MjkzMzAwMA", + "digest": "sha256:784d28e63383e6078b6b7b3269ac8f46700dc9d096939c6e39adb795ed659426" + }, + { + "id": "c156f11a-193e-5f49-a3bb-13c033ca576b", + "version": "MTc1NjMyODE0MTQ0ODQ3MDAwMA", + "digest": "sha256:46113f12559679fa1813e64c43d6ff907af62fcd1dffdbf6b91bcf85634c6bc9" + }, + { + "id": "c15f6119-2050-5503-9882-2ab830f94abd", + "version": "MTc1NjMyODEyNjU1MjU1MzAwMA", + "digest": "sha256:27f3fe689a69870f0dff6a649e14e0ed16f72b2a8a48c05774fad534588c5723" + }, + { + "id": "c15ff6ec-4bc4-56c6-a0a8-c7ed808d925a", + "version": "MTc1NjMyODAyMzM3NTQ1MDAwMA", + "digest": "sha256:830759da1a630e5aaa10ecd6c8c309ff47123c551d68c70055938db6bceb2dd5" + }, + { + "id": "c1627cd2-89bc-5e3b-9587-9942b88ec8ca", + "version": "MTc1NjMyODAyNzg4MDAxMzAwMA", + "digest": "sha256:bfa970737eb844045cfa12f011d775ce7ef38aeff38f7ecd213033050799747f" + }, + { + "id": "c198ebd3-ea7c-533b-b390-c3eaebda47a0", + "version": "MTc1NjMyNzk5ODE3Njg5MDAwMA", + "digest": "sha256:d0816ca6021818bc474b24a48b7beada6d136425cac970a7ef5ddbc2eeb2a9e9" + }, + { + "id": "c1c33373-4378-54e4-bdfa-fd80731e9029", + "version": "MTc1NjMyODA0MTk3MzA4MDAwMA", + "digest": "sha256:7b71d084b86b90f54657327bfded2f603b97fc1d49caa001dbf7733364d526b0" + }, + { + "id": "c1ed4610-8a13-5aa4-a4b6-e6824bddb9f1", + "version": "MTc1NjMyODExMzQ4MTkyNjAwMA", + "digest": "sha256:81b2d76807545528e8f46fc9e4defd66e1b06f5f18a61f19ecf1f20cfd86e2f5" + }, + { + "id": "c1efc062-ce73-5b1b-9d24-a1dfced3ab13", + "version": "MTc1NjMyODE0MTU1NzkyMTAwMA", + "digest": "sha256:5a6f7f67517d6ba53422720d7a197dad28204961515909f1bdfbc84282ec91cd" + }, + { + "id": "c2025a85-3d74-5b42-a4ed-df411a8a120c", + "version": "MTc1NjMyODAwMzk2ODA1NzAwMA", + "digest": "sha256:4fff282e50aa201c4a5471bf87c18dc783e265f5e37883cd96c8b1f65feaac97" + }, + { + "id": "c2078905-811a-527f-904f-8bd646bd5cd8", + "version": "MTc1NjMyODA4ODUxNDc0MDAwMA", + "digest": "sha256:a49b6b352be0c9ebab1136ab1c9c0c5bdd03884e0d1baf9029f6f760342a421a" + }, + { + "id": "c216bbde-382c-5b72-9dc0-317fc43889ab", + "version": "MTc1NjMyODA0NTc0NDI4NjAwMA", + "digest": "sha256:fd64e6e66388d480e1c88be5e0d4d1b77a6fbe50d64181d6476177c336ce8788" + }, + { + "id": "c26c9759-0e6e-5dc0-aca7-9a39b9e14926", + "version": "MTc1NjMyNzk5OTI2NzI2MDAwMA", + "digest": "sha256:29da82c9f98ab3d4d3110b3cf7d9116c36b317c58635e316dc68f4c580317346" + }, + { + "id": "c27e221d-b747-54f3-81bf-c42284c29f90", + "version": "MTc1NjMyODA2MjIzMTYzNDAwMA", + "digest": "sha256:475c7129673a784fcde9c3adbb3fb5e778df6b639dc590d52bb54f550622f964" + }, + { + "id": "c28021ed-ba0d-55aa-b0cb-79329daeab48", + "version": "MTc1NjMyODA2NzAyMjUxMzAwMA", + "digest": "sha256:4d6c57d5f49dd3a83d858b40f2f637d04269ab3329d4375251f61b14186be274" + }, + { + "id": "c2b0d9b0-1193-56d3-bfda-17c70b06287b", + "version": "MTc1NjMyODEwMTEwMzU5NjAwMA", + "digest": "sha256:08cb904c8a94a360f81bf62adfb634929318da370ebc822a4aeab06e08c37874" + }, + { + "id": "c2c74a4f-8f2d-57c5-ba96-52c931525e4f", + "version": "MTc1NjMyODE2ODkyMzE5NjAwMA", + "digest": "sha256:117a7d51104730830121a2a2781180452dd676a682171dac9aa4a06d59b4d4d4" + }, + { + "id": "c2c8b913-8687-5e86-a82e-1cc0f91807d3", + "version": "MTc1NjMyODAyNTcxNjI3NjAwMA", + "digest": "sha256:e49f4d230e7fd352f320ce4d484083d5ad79c386344a0622192975f09f2f8d85" + }, + { + "id": "c2d59b2b-3b8b-55bd-be3b-530b2d79eef2", + "version": "MTc1NjMyODAwOTAzMjcwMDAwMA", + "digest": "sha256:3e48483e49807c079a532c5af146593c9a2ce788ba89a5e567408190fe677fd1" + }, + { + "id": "c2f4c5c9-379e-5de9-870c-d54125658cf3", + "version": "MTc1NjMyODAzNjg1MDY1OTAwMA", + "digest": "sha256:5dc10802b21ae430915083ee6b7e7cd3ad5716202fb815a08d8e4050c56769c4" + }, + { + "id": "c2f86711-98cd-5f85-9951-950ec1e33027", + "version": "MTc1NjMyODEzNzA3MDM1NzAwMA", + "digest": "sha256:dfd9ccddfe3f09a7fa85080f57e74ddb99e6f09951eff86347104c5ab16b13e7" + }, + { + "id": "c3031c8d-70b3-5adc-82c4-08bd2a3b4b34", + "version": "MTc1NjMyODE2MjM1OTE4NTAwMA", + "digest": "sha256:b0d80bcb130d79318c1ea3e85898614ca07814b08814d19162c09db5a2f77de1" + }, + { + "id": "c30962bc-af5a-5127-b27d-b648cd875a5e", + "version": "MTc1NjMyODAzOTY3NjY0OTAwMA", + "digest": "sha256:5cdd2a232373d3433f1c63ae94c0f46da7f2efba11392604e41e442fac661503" + }, + { + "id": "c30967a8-f5cd-5aa7-9c1a-213569bf921a", + "version": "MTc1NjMyODEwMTAwMjQxMDAwMA", + "digest": "sha256:4a6127ea9ace3d1e1d387696ddc9368d4b23e1f9990d26650dc1c0e05a41f263" + }, + { + "id": "c3197745-3fc3-5137-82ec-b48f8c7219f5", + "version": "MTc1NjMyODA5Njk0ODEyMDAwMA", + "digest": "sha256:4da25fc8cc8c094db41f3ae339d99443374145f2a3ae70fb3ccc12c39afffb45" + }, + { + "id": "c34382ea-21ef-58b2-9a5b-0d193d45f699", + "version": "MTc1NjMyODE2Njg3NDkzNDAwMA", + "digest": "sha256:e4dd00dd41c71d5c3037a14e983ace8dc3e08981cc0fb830e1f60990c8668214" + }, + { + "id": "c349aa08-b216-59c5-84ce-0eab848cbe72", + "version": "MTc1NjMyODEyNjY1MzI4NTAwMA", + "digest": "sha256:11a2005455305ec2a0018fc9c90fc4ce1b60ff3f320743d5b773d0c89662fc80" + }, + { + "id": "c34b5427-b950-54b3-a482-aa54310cfad8", + "version": "MTc1NjMyODExOTYxODU3NjAwMA", + "digest": "sha256:3f8b5c186df0453201b7fbfa74f5f31597f40e89aaa39f30268051a28507856c" + }, + { + "id": "c3578f13-3139-5f04-a77d-db438ac043b4", + "version": "MTc1NjMyNzk5NTI4OTM1MjAwMA", + "digest": "sha256:972afdc81f2c24bc6493ef3b5d42020c80c5428395c57ad80dfadfc4bc791306" + }, + { + "id": "c383811a-3c30-523d-9e64-9c1a3b828f86", + "version": "MTc1NjMyNzk5OTIzNTA5OTAwMA", + "digest": "sha256:2e85bef6752df26183142bd8a0565c4bf15358540a1e393493b02b4a9c356c96" + }, + { + "id": "c38b2622-8328-5270-8c7d-1dc9623ab341", + "version": "MTc1NjMyODE0MTU5NjA3NzAwMA", + "digest": "sha256:6ecb8da9e1be46604c6fe0482ca60a3020e41862e74b1a85cf594415b74f7eb1" + }, + { + "id": "c39080c7-1fa0-56f5-9e0c-65c9b3875090", + "version": "MTc1NjMyODE0NzM1NjIwNDAwMA", + "digest": "sha256:5578fa690820f6400ae744c41b0cd5be0733ec1fb0debdb6e13fcba269bf3f23" + }, + { + "id": "c395f09f-4708-564e-a521-a8d003286215", + "version": "MTc1NjMyNzk5NTYyMjMwNzAwMA", + "digest": "sha256:b13308451aeef244cc20a8eed6a651330cf6b5f073df49da2d51b2bda5288ab8" + }, + { + "id": "c3b2c677-52fc-5d6d-b8ac-f4a80eb17eec", + "version": "MTc1NjMyODAzNjgyODI0NDAwMA", + "digest": "sha256:e2d42e839d47405e785f94e676e61f4bb0f85cc709a7db3c548a85283d8a6ee7" + }, + { + "id": "c3e06d0b-2874-5ee5-86ec-a861b905fd1b", + "version": "MTc1NjMyODAxODg0NjE4NDAwMA", + "digest": "sha256:74202a0bca29f1938b5c1c0392738698dde6747281ba7f5cee4bd339263d0500" + }, + { + "id": "c3e5abea-0cd1-5ff8-aa61-643ea9ee60a6", + "version": "MTc1NjMyODA2MjczNjMyNjAwMA", + "digest": "sha256:bcd24854bd6522a4dd88e4d93f5fed61a8dcb6d0346bcb4cbe5a9688ada39446" + }, + { + "id": "c3f7f10e-d1cb-5d5b-8c0c-b3213b038e97", + "version": "MTc1NjMyODAxMDQ1Nzg5OTAwMA", + "digest": "sha256:304acc1e0b9f8fa7d57a66640a1389c01300851487f38e08ad596e10721164af" + }, + { + "id": "c41947cd-86c8-52e6-96bd-e70a1e958153", + "version": "MTc1NjMyODA4OTMxMjQ2NzAwMA", + "digest": "sha256:fd7dc81c9e9ae265e18563fd007432e39e48219ec73b18b58bdb7ca3926245b6" + }, + { + "id": "c4482cf6-ca69-55e3-b58c-acecd780d456", + "version": "MTc1NjMyODA4OTIwMDc3MDAwMA", + "digest": "sha256:dc13c3c09a19ea935a65d89456cfa5d30d8685691c59ab4e9eb7a3c93519a7b0" + }, + { + "id": "c4592981-1279-5d0e-aae0-2275c4d88ba2", + "version": "MTc1NjMyODA3MzAzMzg2MTAwMA", + "digest": "sha256:08c3649705aa8f21a4d74c9e1e226c1713682e715ffcb4ef2502c42c54cad9ae" + }, + { + "id": "c475c731-7302-5a86-9e49-d38351fab767", + "version": "MTc1NjMyODAyMDQ5ODk0ODAwMA", + "digest": "sha256:2ddc4e5d5ccf4a8ff3f11d89e494bc6e839b2e5f2c073d4adcbccee019147279" + }, + { + "id": "c4875884-a064-5310-9c5a-7e9d3675c778", + "version": "MTc1NjMyODEwMTA4NjMyNzAwMA", + "digest": "sha256:3f445b901c04177003b49bce2ac664a8629db86544d3b898af6215a5d9bfd052" + }, + { + "id": "c4967cce-a235-5805-a98d-a71b47e01045", + "version": "MTc1NjMyODEzMTE5NjM2MTAwMA", + "digest": "sha256:630dd749536d9ebec4c736545af116d77fbeec1119a18541cf35166fa67ef629" + }, + { + "id": "c4a085ec-ea94-5af5-a3e7-e115e50dc1be", + "version": "MTc1NjMyODAzODYyMjg2OTAwMA", + "digest": "sha256:1c2ee695c8d62e940dfceb8482d07d5c8511ac297fb60e8c313256ebe06e7b18" + }, + { + "id": "c4a9f2f5-982f-5af4-9392-cf57545badcb", + "version": "MTc1NjMyODAwOTAzMjE4MjAwMA", + "digest": "sha256:62b01179341baa71f72a137341a4c51649a22e407fb84008e079fa6732e9636c" + }, + { + "id": "c4b7b4c4-7269-5c10-9fe1-768e76b43854", + "version": "MTc1NjMyODA3MzI1ODA5OTAwMA", + "digest": "sha256:22ae5718b540ed56697b58f76e2c8b508791212b38707dd6cf021fc387559da2" + }, + { + "id": "c4b86f97-a152-5563-afdd-743aa0d927d8", + "version": "MTc1NjMyNzk5NTI2MDA5NjAwMA", + "digest": "sha256:b745cc516532b6ddc6374c8653c5f30924b4711cb59bf0181a8b61de7c131d7d" + }, + { + "id": "c4c3f6bf-9075-5e64-9ff2-3c5c3d821791", + "version": "MTc1NjMyODAyMzQ0MTk0ODAwMA", + "digest": "sha256:d45787584073ed9d79df443579cf8768e05dbf69596ba1bfff4c45cb6ff647a1" + }, + { + "id": "c4f64e16-a388-5ad0-bd23-21268af92b82", + "version": "MTc1NjMyODE2NzQ2MDUzODAwMA", + "digest": "sha256:06bb51ce482acc64fc10287d3c6e47260f06dfd737633113ba01b4748135055b" + }, + { + "id": "c5003feb-5e98-597f-9fbf-8658ccfc9efe", + "version": "MTc1NjMyODE2ODgxNTIyODAwMA", + "digest": "sha256:a8731b953ed27b6fa1b51e734160b35fc7cbb3f973d9ff91d3d06605a3eb3d49" + }, + { + "id": "c50e27fe-485a-5c4b-acbb-6700b00804d1", + "version": "MTc1NjMyODA1MTE5NTYwMzAwMA", + "digest": "sha256:c1e32b507ea2497b8eaab6f1a368f438268d3c32babc5bab89e0708612324c2a" + }, + { + "id": "c51f1db2-06ec-5e91-9b21-0bd52a7777b3", + "version": "MTc1NjMyODEyMjIzMzMyMTAwMA", + "digest": "sha256:333c00d7e43444e8ee02be77ce8e7dfc44104c8b256d7fd53e0985c54e7849b9" + }, + { + "id": "c52a3a8a-6e20-52c0-8af9-2fb024208324", + "version": "MTc1NjMyODAzMDc1NzY1ODAwMA", + "digest": "sha256:1552e51775f2953fea4723df7c07eaaeb8999cee93f2ead2d2c1d386a25c1d6b" + }, + { + "id": "c55a46d5-2bf2-5b0c-aaef-4c2d9d98e02d", + "version": "MTc1NjMyODAwNjE1MTk5MDAwMA", + "digest": "sha256:20bd2354bb2485fe4cf9de6ad37378e95ee17ce23aa5e59866be892230929a66" + }, + { + "id": "c55d4a5d-979c-5c78-9fde-17c8f323fda8", + "version": "MTc1NjMyODE1NDY2MzI1MjAwMA", + "digest": "sha256:1fd0ec0f86a523439cd36bcca49b1b15b967c77d881ca7dbd30b4f33bb4f3acc" + }, + { + "id": "c57b8492-43f4-5887-be7f-8f7e67da8c82", + "version": "MTc1NjMyODAxODgzNDgyMjAwMA", + "digest": "sha256:a3df0523076b6342ff1cf75d2a34194190bb24a028abec1ce06b18610d98a7f8" + }, + { + "id": "c5e9620a-129c-57b4-8944-34fc79c93635", + "version": "MTc1NjMyODAxMTU3NDE4MTAwMA", + "digest": "sha256:73e31bbe210ad6ad058accf15a5e6014873385a396b38bd52d0d4160d323ed33" + }, + { + "id": "c62779fb-c8fa-5ba0-9f88-b076a6d13b31", + "version": "MTc1NjMyODAyNjg0NjAyMTAwMA", + "digest": "sha256:4cd872d37bb8261acf550636972842b75bed33dd0d83ab0d319455d42ccc92bc" + }, + { + "id": "c62cc2ba-24e8-59bc-b54e-a3072eadcea9", + "version": "MTc1NjMyODAxMTEwMTc0MTAwMA", + "digest": "sha256:fb0fa4258aef7c1b83bb479d8bb6e40ae87d5b079babbe3e70034300e7ef9b65" + }, + { + "id": "c631bc06-be85-53d9-a7d3-ba20d58e03a0", + "version": "MTc1NjMyODA2NzAzNjk2MjAwMA", + "digest": "sha256:6dab39d6f3228b082f68e2291d6fb1d1eb40790351837d0a2414d4b10481fa58" + }, + { + "id": "c6362aab-b5ac-572c-9e09-4c09b9211269", + "version": "MTc1NjMyODAyNzM1MjY0MzAwMA", + "digest": "sha256:0a2157f41ebdbb724b21046e7927ae6876d6e16b854c7ecd3bbbdd8e81f943b9" + }, + { + "id": "c642e08a-ce31-5e20-b316-2ce602cce615", + "version": "MTc1NjMyODE3NDk5MzAwNjAwMA", + "digest": "sha256:9244c55c2aab5fe9badc22fdb5353268c1f82d9a751de4df625de84e285fbc46" + }, + { + "id": "c656ec14-fcc3-5e7e-b115-c89e743530cd", + "version": "MTc1NjMyODE2MDU1MDM0MTAwMA", + "digest": "sha256:1572a9a50b05c3ba4051e17db5d6be7fd31bee0936df7fcc943e04e940689cde" + }, + { + "id": "c66d615b-d11b-5469-9d2c-3a1679433052", + "version": "MTc1NjMyODAzMDgwNDk1NjAwMA", + "digest": "sha256:6ce6b75fc729745931d72f23a6b283ff45ee78e11aee4c672759b312e20a26da" + }, + { + "id": "c6976cb8-f204-5386-88d9-6e0e35633dd1", + "version": "MTc1NjMyODE2ODk0NTIxMTAwMA", + "digest": "sha256:046342e5c3f16a4c59101d7da1d3bc34d0268f15cdf9c2928b36051d12d7b00c" + }, + { + "id": "c6be034c-b0be-57d2-a56d-ea11bf3df248", + "version": "MTc1NjMyODA4NzYzNDE2OTAwMA", + "digest": "sha256:7d8d2392c126fb3fae2b1c2f0eaa1eae33f380b85a27c3a208d4b683676dc774" + }, + { + "id": "c6fd63a3-a5d2-5874-8d4a-095bd59565f6", + "version": "MTc1NjMyODAxNTIwMTM3MjAwMA", + "digest": "sha256:6767eec960f34d22fad3f4cf0efcc58358f9b1464d2c17a6e4f919a71a36bcc3" + }, + { + "id": "c726b64c-eae7-50f1-bab0-2b07062198d9", + "version": "MTc1NjMyODAxNTIwODk0NDAwMA", + "digest": "sha256:d1583a9514224814f47727328ac5064d733772edf971c7a2d81ab6922d043723" + }, + { + "id": "c72f39d0-e43f-5014-9b71-3aea25886f3a", + "version": "MTc1NjMyODA3NDM1OTY1NTAwMA", + "digest": "sha256:46e9796f903bae0ede29abf9d73bc2e5bb7315ebc0ccdb66988681fd9e79d293" + }, + { + "id": "c73b8f43-918b-5fbe-84aa-e262db129544", + "version": "MTc1NjMyODAyNzQzNTY5NDAwMA", + "digest": "sha256:95618e2cbc399629d63a268c856cc447eabd506bdfc762cdf0bbaaa66987ac53" + }, + { + "id": "c744cdd2-7399-5f56-a162-c8873603b777", + "version": "MTc1NjMyODAwNjIwOTk5MDAwMA", + "digest": "sha256:e69849234d21733a8ab4801f0d36bfcb2fcc63844b5e84cfbe340af6399d0d38" + }, + { + "id": "c7669d75-8173-505c-a08e-4f87bc2e2430", + "version": "MTc1NjMyODA1MjQ4NTM2MDAwMA", + "digest": "sha256:28f182f6329f94dba62e9ead5439475fdce97088522f94b24ac7253fb9a3e091" + }, + { + "id": "c769c386-7e2e-5ecb-b5ae-be2a94869951", + "version": "MTc1NjMyODE3MjU0MDEwNDAwMA", + "digest": "sha256:57e18058c9dfff04d09aa680d5a72188e0c7e4a80a99eb35939935b4613a15ae" + }, + { + "id": "c77fe549-a45c-5d93-bfdd-7ee8c2c45180", + "version": "MTc1NjMyODAzNDczMTQ5ODAwMA", + "digest": "sha256:2ff37735333e56f5d72523a59edad5020ee2b994f7cccce107e72ed2b78b382d" + }, + { + "id": "c7828f95-2475-5eb5-8c0c-878e19b79702", + "version": "MTc1NjMyODAyNTk0NTY1NjAwMA", + "digest": "sha256:0b3559a350fea1ae8dd635b284f2de96ed5adcad6f6dac724c7ffc1b052a3d59" + }, + { + "id": "c78e3dae-1e10-5151-b621-97abf10bd7c0", + "version": "MTc1NjMyODAzNDczOTk0MDAwMA", + "digest": "sha256:74d327d39db6bb229b5eea2f8b974a0bcf26cc7d58ecfb7d050b1f8582374c3b" + }, + { + "id": "c79301ed-7b85-5222-9a2c-b431a87ff1ce", + "version": "MTc1NjMyNzk4ODczMTI2MTAwMA", + "digest": "sha256:c3d68ae9eedb150fa71879ee90c35e5a34d2b3f117ab1d6d05a983fe90f834c4" + }, + { + "id": "c7a5c174-65a1-5803-8388-44b94e9a030b", + "version": "MTc1NjMyODEzODE1NjU4MTAwMA", + "digest": "sha256:df464c30146da554d4f02727e2f13e6ee7531b9f475cf333363351913e15cd38" + }, + { + "id": "c7a74ac9-7cf1-5941-bc37-79b69302cf66", + "version": "MTc1NjMyNzk5NTMzNDUyMjAwMA", + "digest": "sha256:3eaee0cf7723471035ceb11c36d100808dc956047a366c00449783dcb8f97c10" + }, + { + "id": "c7a8a2e8-183f-54a1-af71-939b0d849c31", + "version": "MTc1NjMyODAzNDcxMDEwMzAwMA", + "digest": "sha256:2514da48cfcdb2f86bdd01642c6568633da275a2056e05ee1926aa9a8976967c" + }, + { + "id": "c7a8fa42-21da-55d7-bdb0-401ea7548405", + "version": "MTc1NjMyNzk5OTMwOTE1MjAwMA", + "digest": "sha256:db2dbcadc6cec0bdaddfd201916bb30b320bc82727f9c6c84add58c0225c78d5" + }, + { + "id": "c8018bbf-7f36-5026-8881-dbb3a3406d6e", + "version": "MTc1NjMyODE0MTQ1Nzc2MzAwMA", + "digest": "sha256:c648c76c557aebbeadd77abfde70208bfb3bf5748d501b173726cc4292e76b77" + }, + { + "id": "c81a32aa-823e-540d-b8a5-70831c02ea77", + "version": "MTc1NjMyODAzODc4MzEzMTAwMA", + "digest": "sha256:4a49a78f07b997fcf7590b3a25dc9d985887eeaa5ed6378a6be2daed615acb03" + }, + { + "id": "c828eac2-2046-5a59-837f-5dd1107b4c67", + "version": "MTc1NjMyODEyNjQ5MzY5MDAwMA", + "digest": "sha256:ea5f964df02e9bd107abf82e92ba1b8f73a26ca16b5e949ef118ef43a4917018" + }, + { + "id": "c874c36a-e7f3-515a-888f-130347b95b94", + "version": "MTc1NjMyNzk5OTM0OTEyODAwMA", + "digest": "sha256:e1bf507a347a5f1139dc1bbddfe52a703ffe270ee8113b54ff8757b4e8ddbf3a" + }, + { + "id": "c8a803b7-7f35-565d-b897-7c58b22020ef", + "version": "MTc1NjMyODAxMzgwMjc1ODAwMA", + "digest": "sha256:f0437ff57a8370da766ebdb7c656e05e633670209ad9dec5b2042fa2f4da3ff6" + }, + { + "id": "c8acc1e3-9c36-50df-abbd-1266b9b7917c", + "version": "MTc1NjMyODEzNzQxODYwMDAwMA", + "digest": "sha256:8a2caac015dc6c4afb99f112c76612e468a215fac7bbe83a7c461a296a0e3c3a" + }, + { + "id": "c8ad5f6b-0cc9-5d71-8ed6-547e524e57b6", + "version": "MTc1NjMyODA5ODYzNDk2NzAwMA", + "digest": "sha256:3edb98b60d42d7e613d6875f54c47fcb3075f2f8e88c63a3bd9dd6d51791fb46" + }, + { + "id": "c8af4b39-5149-5149-a369-de96c43c82c0", + "version": "MTc1NjMyODAxMTYzODQ4MjAwMA", + "digest": "sha256:dc6f6ae73fab01c0c46c8f58cddf21528ffcc01bd64a73acc51c83f29ecee9a5" + }, + { + "id": "c8d75b3d-8048-5268-815c-d28feefa9c05", + "version": "MTc1NjMyODE3NDc4NTc1MTAwMA", + "digest": "sha256:00d2f5d6790939971c30021a39f073cf317adc376f0be0a56e9dda637e61bf19" + }, + { + "id": "c8dab30c-0392-5259-81d8-f01a16f8d265", + "version": "MTc1NjMyNzk5MDM3Nzk5ODAwMA", + "digest": "sha256:ee1a2ce474319ee86bae3fc3c98d5994f147766f473794a391404d8548c573fd" + }, + { + "id": "c8e57ce3-8665-5ffd-8868-12c9b9e2a7c8", + "version": "MTc1NjMyODAxMTg1NTk3NDAwMA", + "digest": "sha256:9be45ff192a40e492dec298fc93c774379dfa2f33052753e530c024558ba23fe" + }, + { + "id": "c8ef1f46-cc6f-5fe8-bf12-6243234a6057", + "version": "MTc1NjMyODEwMjQ1NDA0MjAwMA", + "digest": "sha256:4178c07de8f910b2d853e6457bf402a8c5e6aa82de669f9b9097408b2c134ce4" + }, + { + "id": "c8ffaa8b-1f4f-5ab6-88d3-d7fc43e39715", + "version": "MTc1NjMyNzk5NzE0ODYyNzAwMA", + "digest": "sha256:0493e14279e8224c59e7b87a5d75eff1795d00058f8e74193cb0811c36165f29" + }, + { + "id": "c909de29-7e1a-5c12-98d4-711d3fb2a462", + "version": "MTc1NjMyODEyMDUwMjY1MzAwMA", + "digest": "sha256:7240bc63fd0d4f6d873f891ef4c6bcbcb9e3e3b3234de5e132f983d1b3306e30" + }, + { + "id": "c921056d-322b-5a51-b974-488cc643bce4", + "version": "MTc1NjMyODAyNzgyNzkwNjAwMA", + "digest": "sha256:8d10da059593d0d1c858e56e625eaeb4a0aec474a9dd0694ba8c619ee884a022" + }, + { + "id": "c9532fcc-23b1-513d-8da8-44b3b43b37e6", + "version": "MTc1NjMyODE3MDQxNDU2MDAwMA", + "digest": "sha256:353beedd0e972e7875e4f1c4d46b6a7e4b0bed6b413e5707533c6c4e4cd0de02" + }, + { + "id": "c9975e85-742c-579e-b837-a2b7a6af0210", + "version": "MTc1NjMyODAzODYyNjk3MDAwMA", + "digest": "sha256:8d44a24f34873a2b7f3ff487901d612e677480b10a821999f2160dd73eb22383" + }, + { + "id": "c9a66f3c-36d7-556b-99d0-617613843630", + "version": "MTc1NjMyODAwNjE5ODYzMTAwMA", + "digest": "sha256:37c16c149b9e269a79270304acbe005a153dbc2ce28cd248919b14ad642606d4" + }, + { + "id": "c9d293f2-ab1b-583e-b124-0eba558f83ae", + "version": "MTc1NjMyODAxMDQ3NTAwNjAwMA", + "digest": "sha256:47cc43d88f458f7e1403f4347f75ab1da6623c67980cc95b0438d4c1385ee5b1" + }, + { + "id": "c9f26acd-08f0-5d6d-8861-cf1c54324807", + "version": "MTc1NjMyODAzODgzNTEwNjAwMA", + "digest": "sha256:280b40ecb50436880ddb1fbe7856790f54c4f1f4142d2da38c6ed191b5ff9d24" + }, + { + "id": "ca00f85f-560c-589a-b731-41f1e45c44ae", + "version": "MTc1NjMyODAzODcyNTE0MTAwMA", + "digest": "sha256:8c89404027d10e6e0551009d8003218b60e5fcc51a17fa03afea5f63b31f0d78" + }, + { + "id": "ca0cb454-2ce8-5702-8029-f21da1428dc8", + "version": "MTc1NjMyODAxMTA2ODExNDAwMA", + "digest": "sha256:d5c3141c2507afcb68f3496b98d2f83a410dc9725cc38bb0be9fac5ace98fd9a" + }, + { + "id": "ca245a34-a264-54ee-8fdf-f78f12200fec", + "version": "MTc1NjMyODAyMTMwODE3OTAwMA", + "digest": "sha256:2bca8ae3a45a269e7367caff13d8585655a089130e6870ba7711a46ba4b033f4" + }, + { + "id": "ca4c8617-1bca-59da-9eee-d3fb9c8ca2fb", + "version": "MTc1NjMyODAyMjEzODg2NzAwMA", + "digest": "sha256:8f70c541c9c5fd9465a4b32cfed28bd200382859a8f9acaeb7af14dc86e4dc5e" + }, + { + "id": "ca5a88cf-6422-5b50-ae21-0462fdb91304", + "version": "MTc1NjMyODAzODgxNTkyODAwMA", + "digest": "sha256:e60252ae506aca7300b6d7640a80d5544b9fb551bfe3c74922a20a6198c749f7" + }, + { + "id": "ca693f92-ee1a-5a33-a8b5-ec43154ce760", + "version": "MTc1NjMyODA0NDg4NDAzNTAwMA", + "digest": "sha256:ad87fd19ade530db0662f9bf2d56bfd55e00838d415a4e2601c0fe0772199b3d" + }, + { + "id": "ca74ebfa-e291-5401-8d05-ffd62e95df43", + "version": "MTc1NjMyODAyNzM3ODQwMDAwMA", + "digest": "sha256:e613f435c5cefd2c82cd120096402ef2f34936b3a85acd199c1c569f1cd45f00" + }, + { + "id": "ca9738ec-df58-522d-a678-c9e22bab017c", + "version": "MTc1NjMyODA0NDkwODgyMjAwMA", + "digest": "sha256:bc0d52737189d38884dfa2ba84cb584552d66e93d07f1db686e3ff735315780d" + }, + { + "id": "ca989a71-fc14-505f-a45c-56b8cc389eda", + "version": "MTc1NjMyODEyMjE4ODMzMjAwMA", + "digest": "sha256:b737f301e6081cbf614b3dcc55c20bebf63726a1a78c4a6dad5320394d2befdd" + }, + { + "id": "cab3f8ee-370c-5bee-aa2c-17bf759ad86b", + "version": "MTc1NjMyODA4OTI0NDU4OTAwMA", + "digest": "sha256:9854ecdc1597b6e803235b1d4554bb2e7deb5713261df2e5ddb16cbffd710aaf" + }, + { + "id": "cac1de1f-b519-5d77-97cd-103677c0ca5b", + "version": "MTc1NjMyODAwNjE1MTYzNjAwMA", + "digest": "sha256:bd45b870378b214322b8b5ddfb9f8d973b45c9bbba93f2f5f8e5a6027f151603" + }, + { + "id": "cacae492-eb11-5a65-b781-86cf2f5ef186", + "version": "MTc1NjMyODE3MjQ1NTQ1OTAwMA", + "digest": "sha256:e5db5c4a694e8de40a94c333d41be25ee056587bbe032b0fe6bc2d04d72045b9" + }, + { + "id": "cb00d130-1387-5ba6-ac16-6e6e2fb37143", + "version": "MTc1NjMyODAzOTc0NDYzMDAwMA", + "digest": "sha256:086ce0d584adaf2ead1e3c121832ff742113a4a5a6fa88e15ba8796f8d2f26e0" + }, + { + "id": "cb57339d-2a0d-5fd2-a88b-888d8c2c8aac", + "version": "MTc1NjMyNzk5NTMyMDc0NzAwMA", + "digest": "sha256:b754e4a1709c63f8197bf4b9bb5d5622f883d1483ab786e0478c68c2d51d81b6" + }, + { + "id": "cb5fab08-9b3e-5bd5-9cca-785fcc47e7db", + "version": "MTc1NjMyODEwODgwMjYyMTAwMA", + "digest": "sha256:bcfc45e62e756e02a0ce7febe0277343190815b90078eb4722f48159d120463d" + }, + { + "id": "cba3d1c7-2279-5626-8407-874155e6ca68", + "version": "MTc1NjMyODA1MjYxNzQ5ODAwMA", + "digest": "sha256:28569155f8c6de3ec7f058e8302d81e274907a473a5ccb6313c8d5137d7312e2" + }, + { + "id": "cba4c0ef-cca9-5336-9346-a79a2f89d3f1", + "version": "MTc1NjMyODAxNTU0MTUwOTAwMA", + "digest": "sha256:1d5af5544749c2ff45b6794a78528c072d8837269403e27e3d2257f5f23af169" + }, + { + "id": "cbbfd9f9-767c-5040-9d4a-b4439e59432b", + "version": "MTc1NjMyODAyMDQ3ODg0MTAwMA", + "digest": "sha256:a0271912de8813e206d4c394670a51a33b268439a1f09d0d39c852f0e739e44e" + }, + { + "id": "cbe4a3de-f811-51dc-a4ce-1cf427c44de9", + "version": "MTc1NjMyODAzOTcwNjUxNzAwMA", + "digest": "sha256:fe5c3e19fb0906d3531a56cc851320b719e5fed51773edcbe1e878ee3c054f1c" + }, + { + "id": "cbf41358-452a-5791-8c59-9c48c3854394", + "version": "MTc1NjMyODA3MzE5MTY3MjAwMA", + "digest": "sha256:b73615341c3d70f96592626e58cd9dfea74dd1b4d9107637f7e0c2656de273b5" + }, + { + "id": "cbfa792b-402a-5ed8-b91b-bd86f87a427d", + "version": "MTc1NjMyNzk5ODIxOTE1NjAwMA", + "digest": "sha256:8539c9ae64e8da9ffc7bd25946de0692fbf9690d57889417c936f3127f7bd0de" + }, + { + "id": "cc2aca30-d32c-5330-9969-69bc2dcecf9d", + "version": "MTc1NjMyODAxOTM0NjkxOTAwMA", + "digest": "sha256:c825cbe00748433431480c418ee584c3ff2d3449eb41a893d07131ef21bbcfce" + }, + { + "id": "cc4b0907-1ca5-5a45-a254-c9427bbd802d", + "version": "MTc1NjMyODE1NDU2MDg0MDAwMA", + "digest": "sha256:8206eab21136188e09a65a269c3e7d37c0373df56e1a0ea06c84861064fa55b2" + }, + { + "id": "cc693d2a-b104-5023-a39d-686b3146358c", + "version": "MTc1NjMyODAyNTU5NTE5NDAwMA", + "digest": "sha256:4f92b39a30cda1982b7c8ec903b222ad42890362371ff8fef2dc76e7e4dac1c4" + }, + { + "id": "cc72c879-a189-5189-a9a8-3d4b837992c3", + "version": "MTc1NjMyODAzMDc1Njk5MjAwMA", + "digest": "sha256:fe9d3944d4d92d3f84b33d571978d2847f624902986e7ab795d265e0c03b3004" + }, + { + "id": "cc78b17c-ed44-5142-b1bf-6fd9a330f74a", + "version": "MTc1NjMyODEzNzUwMDY2NDAwMA", + "digest": "sha256:c17bacdedc340fe7bbfb0301e404eb6c6c8a99e1f5bfff4ab3e2150372dee1e0" + }, + { + "id": "cc7f5bdb-6145-5238-82c0-0fa5daee22d9", + "version": "MTc1NjMyODAzNDcyNjg0MDAwMA", + "digest": "sha256:6c89da02a1e5f8154729736ba6efb4d02dc4b0d42b63dd1b008d846ab8d9372b" + }, + { + "id": "cc819498-c034-5d30-896d-24252e46c708", + "version": "MTc1NjMyODA5NDM5MjAwNTAwMA", + "digest": "sha256:564d82ad1f3146809811f815a674726324fe209dfd75703eeeaae14d2a722dfb" + }, + { + "id": "cc93d9fb-311f-58e1-9852-a2f2b0cb078a", + "version": "MTc1NjMyODA1NDE0Mzg2MzAwMA", + "digest": "sha256:a56f32dc7635ce601bfbea5487a270c6974d521f18cfdb9f7ddcfe4eca38ce1c" + }, + { + "id": "cc9a145c-aea7-5438-92ef-f1d239c4ad92", + "version": "MTc1NjMyODE3NDk3MjM5ODAwMA", + "digest": "sha256:dc01dcdfc2c361eef8890dd5107fa4351e6f6d1ed401eed2d1181f1543a6777d" + }, + { + "id": "ccbacf92-df93-51ac-8a3c-9046b88d16a6", + "version": "MTc1NjMyODA3MzIxOTA0MDAwMA", + "digest": "sha256:fe77ef236cb7026eb1e9811cb76cdfb1b6feee76b3a624ea71fac04ff7647395" + }, + { + "id": "cccc13a8-518b-5df9-b6eb-03954fddfc8e", + "version": "MTc1NjMyODAxNjQ4Mjg3MTAwMA", + "digest": "sha256:0018053926deaf0d87a7d60a2de89f5a60b73fa905f00b207db2d6d5dfab2993" + }, + { + "id": "cccf8aaf-ae44-5439-85bc-07b6e0108cbb", + "version": "MTc1NjMyODE0Nzc1MDYzMjAwMA", + "digest": "sha256:688ea6e6866d72d5daefb66c53488d8b3fa80ab9915d94036640ff129b9e3ab9" + }, + { + "id": "ccdbb947-ad40-5b23-b7d6-b8e656246d1a", + "version": "MTc1NjMyODE0MTY0MzIyNDAwMA", + "digest": "sha256:1282e3343f055f422c2923f6d917cd53a5aa1f3456e13bc4a79a6a17430a636a" + }, + { + "id": "cce62d01-5085-59f3-b150-752ec174a05a", + "version": "MTc1NjMyODAxMTkyODU2NzAwMA", + "digest": "sha256:90d96034b4bb2384b767c554be7f9104efeb2cca10619e09b35a7439b06b8a36" + }, + { + "id": "ccf40feb-89f3-59e9-8306-3eb8d1cbe1d9", + "version": "MTc1NjMyODAyNjc3NjMyNzAwMA", + "digest": "sha256:30173e8c433afb78d826693c2e862048cdc526c84d214f754c22f0d71b27667c" + }, + { + "id": "cd02e4ee-e44d-591c-9991-3083f10f85a9", + "version": "MTc1NjMyODAzOTczMDQxMTAwMA", + "digest": "sha256:9870e14ad9caacd86a453ff0fcff39e14151546932d217362fcb1ac131bb9832" + }, + { + "id": "cd03fb9e-77a5-5d62-8d5c-792165c8f9cc", + "version": "MTc1NjMyODEzODI1NzAwNTAwMA", + "digest": "sha256:ee9e76be9d06774c6e1dee1ad6f3516b8bb94843f02a2a5a5292727ba6c0c315" + }, + { + "id": "cd2a8889-163c-5bf4-af51-da0f0a9db132", + "version": "MTc1NjMyODAxMTA3MjM1MjAwMA", + "digest": "sha256:47fdd8b70be0e52105be2164223a408365ff536a7839433b3a0e575d987fe505" + }, + { + "id": "cd41ab27-9e41-5613-97c5-e4d10c428711", + "version": "MTc1NjMyODExOTc3NjgzODAwMA", + "digest": "sha256:9eb846fe19eeabbc34df3227ccf3f0efe05ce1325613e5353e15ada822ae0981" + }, + { + "id": "cd44b09f-014b-53c5-a12b-a84644356ff7", + "version": "MTc1NjMyODE2MjM0MjgzNTAwMA", + "digest": "sha256:3486fea286ccbb43ae20e74d4f31aacba9faba104b75e204613c1fc6d1770ae7" + }, + { + "id": "cd515390-8380-5f84-90b7-24954c930839", + "version": "MTc1NjMyODE2Njc4MDU4NzAwMA", + "digest": "sha256:a7a40815233ce75a48c9dfa0d3ae681282bd96a9e2d14a2e0a9ddd3d22bef5eb" + }, + { + "id": "cd5b305a-924b-5aa9-a92d-4a67ca2f5121", + "version": "MTc1NjMyODA5ODYwOTc0NjAwMA", + "digest": "sha256:c527c3bc60c546eac797ec29c332bd26c8c50ae1a030d2a7edb9e68f7b5e493d" + }, + { + "id": "cd6d608d-9835-59c0-a6dd-ffe207c8c4af", + "version": "MTc1NjMyODE1NDU5MTE2MTAwMA", + "digest": "sha256:e308f4a6b728ccf5066669eed988b80257055c21c8a7a25f26ef1792e346f85a" + }, + { + "id": "cd9ac5f6-5f68-5e8d-b0c3-888397cae0d6", + "version": "MTc1NjMyODE3MDQ2NDkyODAwMA", + "digest": "sha256:45d572161b8f0734b1b9364206b90fbf77ee5df8802b902400eecb912c3c1bdd" + }, + { + "id": "cda965ff-5bac-5942-8758-03d493a20920", + "version": "MTc1NjMyODEyMjMwNDc5MjAwMA", + "digest": "sha256:20d6c3e5e87f02b8aa5f669730085c8f1e4039da91f7e49e3995682e8a43b0c8" + }, + { + "id": "cdaeea10-46ca-53ff-951a-610d91cdec94", + "version": "MTc1NjMyODE2Njc5MTA0NTAwMA", + "digest": "sha256:9c2b26bb12057a64f4d4b790eb725d53b877e001ef07a061ce0f9f628b089345" + }, + { + "id": "cddc7ab4-7b7d-5b42-a98c-5c892641ba2e", + "version": "MTc1NjMyODA0NTc0OTcwMjAwMA", + "digest": "sha256:c4efd74d79890caa52057c42cb98565552eea6aee247e7f76fc4d8782c896e20" + }, + { + "id": "cdf35a2c-60cb-5855-848c-d4e1abc7d494", + "version": "MTc1NjMyODE0MTU0NTEzMjAwMA", + "digest": "sha256:ee0d0c5cdf709c4ae5e5b3eaee7303e33ec0fa7a8358b38d4428882f5563a425" + }, + { + "id": "cdffe60e-1897-5b5f-b4ba-895d1c8f054f", + "version": "MTc1NjMyODA0NDc5NDg4NDAwMA", + "digest": "sha256:f8d265915ef8f38708bdca761fc1a4c21f32fa9bf0b659629a7ad40cb4bd203d" + }, + { + "id": "ce0868f1-3bd8-5723-bf43-2a05fd1d72c0", + "version": "MTc1NjMyODE2ODkwMzA5OTAwMA", + "digest": "sha256:24d7bdb69c6537cc59cc44b526726ff6a6924f17c55b41ad68b72cb97f8491d5" + }, + { + "id": "ce250b65-c78e-56df-a5aa-0b582ae3b237", + "version": "MTc1NjMyODAxNjU3MzA3NDAwMA", + "digest": "sha256:9d9138c30a6812cae9127c45d4dbe70a2e8825a59342228bf1d8e352aba717bb" + }, + { + "id": "ce3d2f18-7e70-59a0-9708-9533e3e9e9bf", + "version": "MTc1NjMyODEzNzEyNDA5NjAwMA", + "digest": "sha256:5bb2df46f8ddb2d1faf0d4ed0f9d62595dc97d297afd6de82c6861d70d9e088d" + }, + { + "id": "ce45f993-48f0-5614-b21f-25410595baa9", + "version": "MTc1NjMyNzk4ODcxMzc0MjAwMA", + "digest": "sha256:edaf392179724962933dba611567cf00af506e7ff04a2fc9a304485b3ea6bc78" + }, + { + "id": "ce76be1a-ff81-5142-baca-81a54b877079", + "version": "MTc1NjMyODA4OTMwNDMwODAwMA", + "digest": "sha256:b75a7b789e65ea830d023ff26663bdd579b2b704f6747b5b6136a1f4f5c6f0cd" + }, + { + "id": "cea130dd-1042-50ad-a370-eb2bbcb6a735", + "version": "MTc1NjMyODE2NzUxNjUwMDAwMA", + "digest": "sha256:6d41c68c451ccc3dbfcf77cdfb740602c058f1975009cd3ab056b9286c3892fa" + }, + { + "id": "cebd181a-4b24-5651-8c96-2212aa5cb55c", + "version": "MTc1NjMyODEzNzU1NjMyNjAwMA", + "digest": "sha256:6573f5c22c8ee9541c9065eb51c421766dfbbf3e7c31afb7be09403b41db23aa" + }, + { + "id": "cebe54ae-6fee-59f9-b957-b5b86323bbb0", + "version": "MTc1NjMyODA3MzA2MTAwOTAwMA", + "digest": "sha256:c05e6b11d493f2ce9a0448e25df2775f9c21903781419cb4fd257085066a1df5" + }, + { + "id": "cec2f3f4-51c3-5ec4-9cda-cf1fbd9f1e19", + "version": "MTc1NjMyODA3NDI2NjQ2MjAwMA", + "digest": "sha256:68a72fb6b1a8f071bb3e09320b830a23cd8b53a903784e268c6da7f01499df02" + }, + { + "id": "cec5ce05-f468-5870-98e3-d2345f30d158", + "version": "MTc1NjMyODE3NDgwMjg3MDAwMA", + "digest": "sha256:612aadb8ef632cacbae7e8f24ef472ba04d0ca2eaedc0c465807b1cfe84332b3" + }, + { + "id": "cedc3979-e031-549e-9beb-5c782f63c10c", + "version": "MTc1NjMyODE3NTA0MDU0MjAwMA", + "digest": "sha256:b795ab1d222d9f98c9d5ae74340b03ac8f374869d9e4dcf57f62f2a44de4b0f7" + }, + { + "id": "cefb9026-5039-55c5-9566-0d2392906230", + "version": "MTc1NjMyNzk5NjcwNDc0NjAwMA", + "digest": "sha256:1fe0565404f2c06328bb9f2080defb1f634452a0c69aa2e9cac2cc6a47940d3a" + }, + { + "id": "cf0f0010-f4ce-52f0-b97b-2e915ec2ff05", + "version": "MTc1NjMyODA2MjAwNzU1ODAwMA", + "digest": "sha256:2f8ec77378a89923d4e3c6f02329008e220b424a8ba908c75ffd2f7e74fa8aa9" + }, + { + "id": "cf0fbdab-4a43-51d2-bb72-f0f78dd83381", + "version": "MTc1NjMyODAxNjUzMzE4NjAwMA", + "digest": "sha256:345226535a4d86e7e18eac7e8c338803cb80e254829202b8280c9c1ae71c73da" + }, + { + "id": "cf22c6d0-0d5a-5b62-9376-72f119441936", + "version": "MTc1NjMyODAyNzc3ODk3MTAwMA", + "digest": "sha256:3bfc5492738caabdd84ffb17b1ebd33fa2f14a8bcb360c1652dd21c6f9409e7d" + }, + { + "id": "cf414095-1950-5779-a256-4b600b22e73a", + "version": "MTc1NjMyODEwODc5NDU5NTAwMA", + "digest": "sha256:22a5c5518a72a611702c8f2fb29e113757e83b0c9050903be361dedecd8d5834" + }, + { + "id": "cf977fd0-08f8-5445-b2b0-4e9fbeb9f839", + "version": "MTc1NjMyODAxMTg3OTU1OTAwMA", + "digest": "sha256:aed9b19741ed0282285655672a1e1c5fef3426a4627d84ba748a629084b69fc5" + }, + { + "id": "cfa3dbae-ed4e-5c24-b538-df1dfd5acf9c", + "version": "MTc1NjMyODAyMjEzMzA2MDAwMA", + "digest": "sha256:4b238091fa1738d614507ad5f76fecbaa2b5c211110fb2f2cc52482f0a47f330" + }, + { + "id": "cfe00554-e2a3-50b1-b1ce-997853cc4e32", + "version": "MTc1NjMyODA2MjE2NjMzNjAwMA", + "digest": "sha256:d167cf7b3cbe119ebed3bfb6b53ecc0ef81daac44c00b41f618a4d73e0d6ad4a" + }, + { + "id": "cfe1d8aa-7b7e-5546-bd80-45f17bee8101", + "version": "MTc1NjMyODEzNjk0MDI0MTAwMA", + "digest": "sha256:c5e738677320beb8a196ed1f0dc4e3e5bd0ed8c733b58239ad66c58bac036d90" + }, + { + "id": "cfe8b430-bfc8-58a8-8986-2bc30030f42d", + "version": "MTc1NjMyODE2Nzc3MDUzOTAwMA", + "digest": "sha256:7b4294d8eacbdd303b67ebda335f883b22c9cbc32bc28d09ecd976bd41b982b1" + }, + { + "id": "d010ab2a-f6f3-5ffa-8089-f6b230c9ec98", + "version": "MTc1NjMyODE1OTExODY2NDAwMA", + "digest": "sha256:31b3cc754d07624570600753249e048ed741012bb03e5268460b9391fd7465f0" + }, + { + "id": "d0169bfa-184d-52a5-b7f7-72dd2cb674d4", + "version": "MTc1NjMyODExOTY2MzA0OTAwMA", + "digest": "sha256:f7fabc89ab17b8753950e2b7da971f785919aa957d572220d1f6fb5732943011" + }, + { + "id": "d02398c8-58f3-574d-b07f-b04d9358a6c5", + "version": "MTc1NjMyNzk5OTI1NjEzMDAwMA", + "digest": "sha256:7dad292911a19dae5b7edde329c9304f741107074a5940c10c5492955171d2a8" + }, + { + "id": "d03954ea-ec04-5356-b53c-862add47cbb2", + "version": "MTc1NjMyODAwMzkyMTA0NTAwMA", + "digest": "sha256:204bd6793f1cf28b1f8d9e439c1f775eb9c7bf7ed73d19e5c0cf9b5295436c40" + }, + { + "id": "d047d4b1-c8ea-50ba-82b3-5ce04f29a08d", + "version": "MTc1NjMyODE1NDU3NzYwMDAwMA", + "digest": "sha256:738edac6c7c7b16aabadbf4576b77ed2c85db1cb955a5d1c4ccafa0aa2e4c08f" + }, + { + "id": "d08f111e-3c92-538f-9053-2f7cf087a550", + "version": "MTc1NjMyODE2MDU5MjY1MDAwMA", + "digest": "sha256:7d746246add5510a50b99078063c4177e0457031b45b5ad0ea4fe9d4fac94919" + }, + { + "id": "d0949ec9-1b60-5f5f-aaf3-699301aaf216", + "version": "MTc1NjMyNzk4ODY0Mzg1NDAwMA", + "digest": "sha256:e0c20e6c33f67716fe86fc6b260b521d529b39f1c8891e8b011c13f49b1d473d" + }, + { + "id": "d0a53278-a012-5fa8-98a0-9d756729dd2a", + "version": "MTc1NjMyODAxODg2MTUzMDAwMA", + "digest": "sha256:9dd8c63e960dc81d614c061436a5380029569544815e6af40845b08da399b112" + }, + { + "id": "d0ba736c-d494-5e66-85b4-f2a88f5b0b79", + "version": "MTc1NjMyNzk5OTI1NTY4MDAwMA", + "digest": "sha256:de2027e73f4a188b5ccbe185ac645997917c968aa371fa7984ce5e23b70c6a4f" + }, + { + "id": "d0bede5f-1d07-5bce-b268-924f900bd51b", + "version": "MTc1NjMyODAyMTMyMDI4NjAwMA", + "digest": "sha256:5bf077cfa10f995f655abc42df681ede27352beede2758c81fb93927c79db60f" + }, + { + "id": "d0d916cf-59bd-5b30-837f-e401dd764ab0", + "version": "MTc1NjMyODA4ODUyNjkyNjAwMA", + "digest": "sha256:629a934f81f86935190d4f7dda818139e6bcd0796c59bcdf5e9f06a41194e149" + }, + { + "id": "d0e9c9fc-12e2-5330-9f24-77de55928e69", + "version": "MTc1NjMyODAxOTMzODk4ODAwMA", + "digest": "sha256:8c6a6e3cbcdcdb2072f7bfa1e4567aebbda5a2e698490b884eece71fc172eff4" + }, + { + "id": "d0f6e7db-9a9c-55a4-8f20-34fe6426c437", + "version": "MTc1NjMyODAxMTY0Njk2OTAwMA", + "digest": "sha256:84f587d419ca4958296b291d8373567cef5d660b84f9a3d5b6178f61bbdbc117" + }, + { + "id": "d0f95f84-0cd2-5f48-9383-5409a80acec0", + "version": "MTc1NjMyODAyNzgzNTQ3NDAwMA", + "digest": "sha256:264ca3e420d6dafce88415866f78654195feac36a1cceb253ae5c145f71e0922" + }, + { + "id": "d103741c-e917-5d9b-99d8-4f49cf663c44", + "version": "MTc1NjMyODE3NDc1MjAwOTAwMA", + "digest": "sha256:2e81ec39a525af041b6b9bef8e8405984baadd0bb2c5639b2cb3a55cb74ae942" + }, + { + "id": "d110a0b1-86c7-5a14-b726-9ce1769290b4", + "version": "MTc1NjMyODEyMDM3MTkxMzAwMA", + "digest": "sha256:62d28806351fa83b8298d3a98b12eded51f93c9f7ba00852c564b0a13871c3e1" + }, + { + "id": "d12d251a-d3b0-5f4c-ac25-8233639e8d31", + "version": "MTc1NjMyNzk5NTY3OTYyMTAwMA", + "digest": "sha256:659e27eecba18288a494e8216d6d00295bd0723ec9bf3ddf69f505c7fcfcb7ac" + }, + { + "id": "d13f5ac9-4420-54fd-b67e-cb5d880f5b86", + "version": "MTc1NjMyODA0MjU4NDkyMTAwMA", + "digest": "sha256:08ff26f55edbf47588dcf65b5c93c1352f75567315fdd2aed012b249ed422f06" + }, + { + "id": "d159e30c-42e6-57cc-b8ca-5137ad6ab47f", + "version": "MTc1NjMyODEzOTI0OTYwNTAwMA", + "digest": "sha256:aecbd10c3630e8d754476137d1790bfb10e1879f9c28af77f692f1f21d2d3ec8" + }, + { + "id": "d1634058-6e6e-502e-9a14-940712af6006", + "version": "MTc1NjMyODAwOTA1NDU2MDAwMA", + "digest": "sha256:a318f513b26755342cc7ecca14dcac0ee288af4ac23aee9ea6b827d6db25314f" + }, + { + "id": "d1a2886b-c3d7-5b05-90f8-d9f5845b2cae", + "version": "MTc1NjMyODAyNzg0ODk2NTAwMA", + "digest": "sha256:4cf06dd91dd190b253d41df727029ffacca076da035535e7e900377e459a0ba8" + }, + { + "id": "d1a55e2b-8331-548a-8e4e-0d72a6c7e44b", + "version": "MTc1NjMyODAyNTczOTQ2NTAwMA", + "digest": "sha256:bb6f2e5cda9899898520b603dc99582ced74315f03bfe42b73cc7f335cc24a07" + }, + { + "id": "d1a8aaab-0686-5562-bf2f-ac48746b3f24", + "version": "MTc1NjMyODE3NTA3NDI2MDAwMA", + "digest": "sha256:bb4956a28757b2f39cb249849ebc2ce6488585a6f87bbfc333d9ca9cdc4c775d" + }, + { + "id": "d1c284a8-bab9-540e-a860-f5158fe091f8", + "version": "MTc1NjMyODE0NzM5NzQwOTAwMA", + "digest": "sha256:89f60213a602522947b947602988fcfa126b398e2367e9f8fce8628c15e852f3" + }, + { + "id": "d1e476ca-a4ff-55a7-a2d1-825db9e15af1", + "version": "MTc1NjMyODE2Nzc0NzUyMTAwMA", + "digest": "sha256:cf3fb0a64f04ca914941d9d68de7e3e88ebb8afed92601c5333659cdde3a32d5" + }, + { + "id": "d1fadadd-c307-5c08-9cc3-82f16f3a127e", + "version": "MTc1NjMyODEyMDM5MzY1NTAwMA", + "digest": "sha256:4c522b65d7b19dc7d99e37fa28403db12bbbbbe8b60ccf5d45a9ca683b7bad96" + }, + { + "id": "d1fd0f50-c4ee-5822-a3bd-e8c8be21f5fd", + "version": "MTc1NjMyODA5NDQ1Mjg2NDAwMA", + "digest": "sha256:84be4e54bcf1093bf9b42c8267af2fc9361c8f92ebc3da682fa2b368f2440b34" + }, + { + "id": "d2183f54-49f3-5440-9f59-799cdb8c79ec", + "version": "MTc1NjMyODA1MTMzNDA2NzAwMA", + "digest": "sha256:b2a7d20b1292d46098c2c84e9c5c97461c336c3e99a368aff022e02fa2665fcc" + }, + { + "id": "d23b8ad2-a32e-57c4-9a35-dfacaa9be946", + "version": "MTc1NjMyODEyNDQ0MTAxODAwMA", + "digest": "sha256:d3ded8231ae3c6b9edbda3429b2000b34a52b3ab2f49fe6109ea0e51ffd9e3b1" + }, + { + "id": "d27d8fa5-cc09-5c51-9e6c-fcd989ab4bbd", + "version": "MTc1NjMyODE2MzI5NTI4MzAwMA", + "digest": "sha256:bc4f2898d7ff5a0a3f82f93943e8067a05be152cad6217c1ac6e0fb68f739f4c" + }, + { + "id": "d2a26dd2-9fc4-50f8-a0ec-9ceb27988b34", + "version": "MTc1NjMyODEwOTI3OTQ4OTAwMA", + "digest": "sha256:16ad45ec9fc9232db021784fbfcac90c1e6c86fd90b4098726b387ed7aaf9ffc" + }, + { + "id": "d2b64013-bb4b-53d1-a9f9-4bc52ca74b9a", + "version": "MTc1NjMyODAyNjc4NTMzNjAwMA", + "digest": "sha256:fbf98bd88e34ea7b5effe339415e1a84152a5fea8051794700a856a0c4054b50" + }, + { + "id": "d2c79702-cdf4-57a6-a131-0a2b62aa724a", + "version": "MTc1NjMyODAxNjU0OTA3MjAwMA", + "digest": "sha256:eaa98b4a0abfa3ac4bf6e651a038313e1810a676f2a990212702a5c2a5394024" + }, + { + "id": "d2e05957-4e20-54d2-9bdf-522d193366ac", + "version": "MTc1NjMyNzk5MDQ2MjMzNDAwMA", + "digest": "sha256:b2473dad368d8a906772ac1394fa05c1d1de0aea9a83b19c61a175e5d418fd82" + }, + { + "id": "d2eca567-eb8d-5e8d-af53-1a246c465375", + "version": "MTc1NjMyNzk5ODIwOTI0ODAwMA", + "digest": "sha256:b5bcfb14de4f9cf0f16e2f71ab355e2bf005adfd574b372979194bef367555be" + }, + { + "id": "d2fd99a2-d0fd-58dd-bc3f-d2206e40d18f", + "version": "MTc1NjMyODAxOTQ0NzI2OTAwMA", + "digest": "sha256:c713aabb4c3a469365a971ed032b45f1b235fd7953506370f19023759eb5d951" + }, + { + "id": "d30b4efa-4d54-5b98-a75c-5923cbf75ccc", + "version": "MTc1NjMyODA1NDE0NjMwODAwMA", + "digest": "sha256:a24e9301c79544fa6d5925c8a03f6e59bd928e68706a24caae1c0936b8e226f9" + }, + { + "id": "d337787e-6b09-58da-9a12-d2e930e0ca04", + "version": "MTc1NjMyODAzNjgzNDg2MjAwMA", + "digest": "sha256:532dfdcc4cacd37d7a05e9d9749f40f2111a09c3c6a1e3d33647890bd7c4210b" + }, + { + "id": "d34bab0b-63d1-53da-8fcf-6d9d390a8282", + "version": "MTc1NjMyODAzODc4MDUxOTAwMA", + "digest": "sha256:d341087b5b99b9aa87389de956fb852536bd2c5e9e592e5858efcc6e84caf003" + }, + { + "id": "d35b1eff-afc3-5346-8bb2-77422a296c42", + "version": "MTc1NjMyODA0MTg1MjY3MzAwMA", + "digest": "sha256:8572b6352fc9d058c38c5562834795cfa0681f58536ecb37e2e2ce15fdd213b7" + }, + { + "id": "d36e3d71-8a71-564a-8f4c-5886bb111e3a", + "version": "MTc1NjMyNzk5NTMxMTY3ODAwMA", + "digest": "sha256:52aeb3c847d00a09d7bae1a471f55056be5407e942eab00300664db11e1b63fc" + }, + { + "id": "d377ecdb-a3e2-5130-99c0-c8f40ab21b9f", + "version": "MTc1NjMyODA3MjU3MTQ1MzAwMA", + "digest": "sha256:4d0fd81078a82fff83c002fc79235080f89d20209d6bc1749c715355a1abf8b2" + }, + { + "id": "d393877e-28d8-569b-8d64-2e2a82d4a47c", + "version": "MTc1NjMyODAyNzQ1NzM3MzAwMA", + "digest": "sha256:4c0e8a9a4a8aea2602f30ebc1ba1bcdaf1a94893c9819b27f634681d67f783b1" + }, + { + "id": "d3968585-c776-534f-b6d1-1339df02104c", + "version": "MTc1NjMyODA0MTkxNDAxNzAwMA", + "digest": "sha256:c2466b16c8530bc3b7727fb4b578691c889e5b0792292a140d543043fe8ea272" + }, + { + "id": "d396e558-ab51-58b4-8b28-945100d557a9", + "version": "MTc1NjMyODE0Nzc2MjgzNDAwMA", + "digest": "sha256:2ed8b7794b4b95d42415eaf7d5bb8efc372a7a92a952dfbd44714bdebfbbd952" + }, + { + "id": "d3bbdd10-6ddd-5b60-a47c-1917902c8414", + "version": "MTc1NjMyODA2MjY5NDIzMDAwMA", + "digest": "sha256:0935901569411649bf4c715fa98e55ad2de5c364ba63c8ad3440d10964249384" + }, + { + "id": "d3d93607-0c62-554e-b458-3fcedf8eaa85", + "version": "MTc1NjMyODAwOTAwMTY0ODAwMA", + "digest": "sha256:1324ee6545ef0c9ecb67d7edf30ab5e997b49dcb0ddfef928677e28401157c69" + }, + { + "id": "d3e1e4ee-99ee-57d3-bfea-3a1697acbac7", + "version": "MTc1NjMyODA0NDY4ODY4OTAwMA", + "digest": "sha256:7f32684a5a2246dd2e0a83d946f541364c084b6f715ca347b616ae45f31038d6" + }, + { + "id": "d402f24e-9a21-5431-949b-441bf24e4113", + "version": "MTc1NjMyODAxODg3NjAwNjAwMA", + "digest": "sha256:51d7ff96b3ea0e8d5715168c22898f7385db3f763527d4da9814d4f98938ed6e" + }, + { + "id": "d41677f7-cee0-5064-bdee-e684fea4e8c4", + "version": "MTc1NjMyODE3MDQyMzczNzAwMA", + "digest": "sha256:a1fbd73a433afe30a352e60da13b3126fa12755718f1b9c69c1991adcee8f103" + }, + { + "id": "d41fdffa-4c0c-5a48-9490-44b07123a6e3", + "version": "MTc1NjMyODA1MjUzMDczMjAwMA", + "digest": "sha256:e8d60bb716ee3313fddcdf54c620452ad9111b9f90b298df0b3de68a731593d7" + }, + { + "id": "d4367cc3-72cc-57f6-8ca4-01002b47a82b", + "version": "MTc1NjMyODAyMzQ3NTEwNDAwMA", + "digest": "sha256:fe05e48d3bcf215b2422a50625fa0a5e0859b9ce29accc0586ee4b4dbdc8ec2b" + }, + { + "id": "d46bf4ee-ad7a-5843-859d-0987db51bd8c", + "version": "MTc1NjMyODAwNTA5MTU4NzAwMA", + "digest": "sha256:558fb1929b7d088c59871232b909e6a19b08371f4591a4c9e5ac9cf58acf65b5" + }, + { + "id": "d47d2d85-b563-5890-8ab2-c4420375c278", + "version": "MTc1NjMyODAyNTU4NjU1MTAwMA", + "digest": "sha256:2c45e2798a8aa999b2a2c0eabe39fc9cda97d9c7bca0111af8934eff6695ab3e" + }, + { + "id": "d4856002-f11f-5476-b351-f24663e225b9", + "version": "MTc1NjMyODA1NTc4ODkwNjAwMA", + "digest": "sha256:c60689617dc6e19be57259919f64f38c9cbfb2c9bbff6b4351b898325a251815" + }, + { + "id": "d48f1378-2055-5f90-b33b-e63b8ac42992", + "version": "MTc1NjMyODA1NDE5OTg2NDAwMA", + "digest": "sha256:4daa3c3d12f409166679a0ec309981a150c872be7ff633595da22b09d96506bc" + }, + { + "id": "d4923b4b-2dc3-5b4c-b2a4-6164eb76bc3a", + "version": "MTc1NjMyNzk5NTM2NDgxNDAwMA", + "digest": "sha256:6b90a12137a9d508a6ddba3008bc27e06e6f42685472be0a38202d6d0c9380e0" + }, + { + "id": "d4938f9c-efe5-57a6-a14e-abfd235daa1c", + "version": "MTc1NjMyODAyNTYwNTMzODAwMA", + "digest": "sha256:9458329465009efea65b9be20ecb3234b673d38b190e259f95cfc82f63b42289" + }, + { + "id": "d4966f44-2be5-5ea3-88a9-4d5b71e34bd9", + "version": "MTc1NjMyODA2Njk5MzAzNzAwMA", + "digest": "sha256:e6fca5fd33cde32f2c289a8aa3584adc140f94252aa80d18210726326397d07e" + }, + { + "id": "d4d64ddc-1f83-5bdc-8947-cf29f768dd8d", + "version": "MTc1NjMyODEwMjUwNDU2MDAwMA", + "digest": "sha256:013d4416c39d196526d9ff6a7449b677d36460f2fa6d138d1349829092ae7780" + }, + { + "id": "d4f45d25-394e-528f-83a7-7398eb1b7211", + "version": "MTc1NjMyNzk5ODE2NzYzOTAwMA", + "digest": "sha256:37621d1489a11b44b2b398ceb0ec50d0c8aeb400851f2d42bfb199a41cbeec7f" + }, + { + "id": "d533002e-afab-5fd7-bb7c-1d9d8412097f", + "version": "MTc1NjMyODE3MDg5OTkxNjAwMA", + "digest": "sha256:81b57bef423327a9c421087c66536b706cee090722aee0360aec2aa1b5583dc0" + }, + { + "id": "d5510e08-6cc4-508a-bfd8-efac90828927", + "version": "MTc1NjMyODAxMTA0NTc3MTAwMA", + "digest": "sha256:9a31721f45a4b2ff7eeeb08457055c0be3e86bcea1e8877407cef436f3163f13" + }, + { + "id": "d556c905-f8d2-57a7-802e-bb8bc3d98c2c", + "version": "MTc1NjMyODAwNjE1MjE0NDAwMA", + "digest": "sha256:3a8ae868d79fa0a4bf744389431a3c648426573261287ddb59005a594c0b630c" + }, + { + "id": "d5723eb5-e9f2-5ee8-a806-80d11f0d1fb0", + "version": "MTc1NjMyODA2MjE0MDc0MDAwMA", + "digest": "sha256:8005310a1c20447680b68c9afbeb65c095217f8ee1bdf48b1debf13123533c2e" + }, + { + "id": "d57ae3f7-faa3-590e-a8af-ea77dd4299fa", + "version": "MTc1NjMyODEzNjI2NzMxNDAwMA", + "digest": "sha256:ab7030f433f8dc30a89b029b3d1b859742a0dc6b13dd924b26f53f73f2d402ad" + }, + { + "id": "d59d1b5d-f164-5ac6-852a-bd825f549f67", + "version": "MTc1NjMyODAxNjQ1NTUyNDAwMA", + "digest": "sha256:3a05994ee03dc9b7c928962cd273b8044c3a45b8d2730437969e674c85537f01" + }, + { + "id": "d5b9aba0-d342-50b0-9a1f-6ef4f85e71c6", + "version": "MTc1NjMyODAyNzc5NDk2MDAwMA", + "digest": "sha256:d88349f1c1e160e31c20f4dd7581addecccfdfe3960e3128fbdd9f2e383d1c37" + }, + { + "id": "d5ba41c4-249f-5155-b632-ab2ab7d2eb83", + "version": "MTc1NjMyODEyNDQwMDc2OTAwMA", + "digest": "sha256:a2e1015288bc1ecede325f49a17c643da7aa514e25214e44cbc160843059b25c" + }, + { + "id": "d5c39a54-049b-5f56-94d3-bcf1609fbc63", + "version": "MTc1NjMyODE3NTA0NzMwNTAwMA", + "digest": "sha256:3a73a89b3c8203fb3666a5fa8364c2a8621beb329bab5c67c83a67c4313ed045" + }, + { + "id": "d5db4955-5566-5b33-b8ca-29df6aa354f3", + "version": "MTc1NjMyODAyNjg1NjQ2NzAwMA", + "digest": "sha256:0011129608d55fea1839616b9a5a653ae571a4c5f5eb3b3963b8f8ec8c745c01" + }, + { + "id": "d5ea1766-3749-550e-bbd8-2aa9235fa309", + "version": "MTc1NjMyODEwMTE1NzUxMzAwMA", + "digest": "sha256:c11526550e79111a7ad822d683ae7b272f583baa57146272417a7027ae283bbd" + }, + { + "id": "d5ece6dc-4fa9-5e6a-818f-44f99ff25f35", + "version": "MTc1NjMyODE2MjIwODg2NDAwMA", + "digest": "sha256:6c949975ff030e51485b9ee60534e16dde4efa2690f95ef7c1d58f25e01b2770" + }, + { + "id": "d5f257af-29d2-5fd3-b60d-3c4c552b07e7", + "version": "MTc1NjMyODEzNzYyMDQ5ODAwMA", + "digest": "sha256:294b539e9fea3247d81ab3f39bd2d4d0ae20e1d6c6c1468f421ee774ea896d34" + }, + { + "id": "d5fd9026-e2cd-53e5-aaac-cd46214095b8", + "version": "MTc1NjMyNzk5NTY5NjI2ODAwMA", + "digest": "sha256:459e0a2d25ddc4df001530f1a94bb501414a35f6b88d9c3da9cf603fb46f6ed9" + }, + { + "id": "d6345454-faeb-5c38-81aa-98c9318a5743", + "version": "MTc1NjMyODAyNjgxNzM5NTAwMA", + "digest": "sha256:5f88e4ea426ce06b0b191c8875b8168e5c17f02e20e71291dabf28784fcbb24c" + }, + { + "id": "d64ab9f6-bac8-5c12-a3fe-4dccd0c778c9", + "version": "MTc1NjMyNzk5NTM0MzQ5ODAwMA", + "digest": "sha256:99b545ecfd13d2ed9d6f10494bf592957194e945a0d507aaa520a2be89090c7a" + }, + { + "id": "d655167f-f35c-5f43-9e1a-024534f0651d", + "version": "MTc1NjMyODA4ODU4MTc0NjAwMA", + "digest": "sha256:98dfd2faa64f061d509a8ccde93a9afa1e43dd76b69524b0258bf5809f52cec0" + }, + { + "id": "d661c350-960b-5e91-ad30-dcb0affcbbce", + "version": "MTc1NjMyNzk4ODc0MzA5NjAwMA", + "digest": "sha256:2dbab29c55f33a1e7dddd9d1b7a1d12944f7361f3cd5a1c131cdc3b0fffedd72" + }, + { + "id": "d686a2a1-ae0f-57b7-96ff-020b59e51565", + "version": "MTc1NjMyODA1NDA5NjI4NTAwMA", + "digest": "sha256:a99b2fd2ad525818952bd14bd80b0cea59439e3739d8558726d7b755b9a2f12a" + }, + { + "id": "d69dc1ff-628e-5944-9ea3-c6633f418684", + "version": "MTc1NjMyODAxOTQxMTA3ODAwMA", + "digest": "sha256:8c179dbdaa349dd7b5210d10207fc3303db4e721349c84fe016a0db676ffa729" + }, + { + "id": "d6a601fc-ba9b-5504-8c6c-8dde9be9248c", + "version": "MTc1NjMyODEyNjUyODU0MDAwMA", + "digest": "sha256:305eea294b7ec64ea71a660749584ef576e922dd0f112b82ecb14a4743bdbb2a" + }, + { + "id": "d6b5daeb-2d8f-54c6-8961-266d8fae31d6", + "version": "MTc1NjMyODAyMjE4Mjg3NDAwMA", + "digest": "sha256:20a713eb83e87b497c8c9095fc5c817cc823d9377a3565ad9d2c0f84ad1f4d0d" + }, + { + "id": "d6bfcddb-908d-56a5-bb1c-14933da332fa", + "version": "MTc1NjMyODEyNjQ3MDU4NTAwMA", + "digest": "sha256:b573f72febe48d6291527a88b8aa7d03ec29f5de89a01861daf007673571bb93" + }, + { + "id": "d6c388be-7bbf-5f03-95af-78c4afd81c6e", + "version": "MTc1NjMyODAzODgxMTI3ODAwMA", + "digest": "sha256:b07e29d4f95fe69b0365f5b6e6f2f1bfb9e0693539c239c39f8ac0b77c5b0884" + }, + { + "id": "d6c82304-ca74-528e-ba81-8c6a2d52dfa0", + "version": "MTc1NjMyODE3MDk0OTM4NjAwMA", + "digest": "sha256:3823f3c6003519f493482d784b46d96b4d654479d6d8e7ab2d1f865b4fbeb129" + }, + { + "id": "d6d4ef49-1488-54e2-9811-dd10a5a60c9c", + "version": "MTc1NjMyODA0NDgxOTI4ODAwMA", + "digest": "sha256:f97a9312e497c2cd5c48d9dc37ec2e965efb93ed52a5166ffe5013bd00dd9eb5" + }, + { + "id": "d6d8e5a5-0541-5831-9468-7df30a298121", + "version": "MTc1NjMyODA0NDc0NzI2MDAwMA", + "digest": "sha256:331777d7bd30939c73e59c3deaddf15f02287da1b4ca0abc8f11a619b483a98e" + }, + { + "id": "d6e86e95-512f-55d3-85db-c74d9cd59a7f", + "version": "MTc1NjMyODAwNTA4NzIyNzAwMA", + "digest": "sha256:297aa0eea410404fede708754698d98fff307953ff3a566e52fb19b145d3da9c" + }, + { + "id": "d6e88b18-ef13-5073-9596-890334fae80f", + "version": "MTc1NjMyODE2MDQxNDM4NTAwMA", + "digest": "sha256:2225c00439ab811bf3b483db5e4c311a0bd5b4bf97757020d7cfe668d2373557" + }, + { + "id": "d6f1fe79-d3a8-57fc-80cd-75dadb0559db", + "version": "MTc1NjMyODA4OTMxNDUwMTAwMA", + "digest": "sha256:e7b1878fa9d6827aa25caff409776a1ea9dc1bebbc9de7a250389f1c01c8f28c" + }, + { + "id": "d6f8d31d-4dac-52ff-8e32-267fdda8d396", + "version": "MTc1NjMyODAwOTAxMzI3NzAwMA", + "digest": "sha256:3b45a2699ddf8907943e06e8545d9afc2ac6134a5147ab7c9fb89c98f8f67ce1" + }, + { + "id": "d776baa3-2d83-5438-b079-ebdff5a85feb", + "version": "MTc1NjMyODA1NDA1MDM5MjAwMA", + "digest": "sha256:861a7e761f40b6f903a0b2269ec6ac50d94504870da64d3f5d89714cd92f0615" + }, + { + "id": "d786ffe3-b811-5b7a-bc22-5451c2379464", + "version": "MTc1NjMyODAwODk4NjkyNTAwMA", + "digest": "sha256:80867ac5fce1e6a18a5ac1611fc92a6571638537b555308808b650d74c566d56" + }, + { + "id": "d78b25d3-4464-54a4-b38b-49b842bf4da7", + "version": "MTc1NjMyODAxMzc5MDI3NDAwMA", + "digest": "sha256:4c2810201294de3077568045880f86947f6c49bfe29c85c3075da1291943b37e" + }, + { + "id": "d78b2984-8fed-530d-bad2-0de69fbd5b09", + "version": "MTc1NjMyODAxMTYzNzc3MDAwMA", + "digest": "sha256:136a6570cb4fcbd0a207170f0bec0097ba0c7266a993d44b5d99d380bfd7d904" + }, + { + "id": "d7cf517d-94ec-5ffb-b3b9-fa978f02a524", + "version": "MTc1NjMyODE1NTExMzkxMDAwMA", + "digest": "sha256:545aea24fb9dfe44470f4585ca4b6bf68699da8e01f0912c602c7a8e5dcdb985" + }, + { + "id": "d7e0af06-50c9-5f2e-992d-8b3d8658582a", + "version": "MTc1NjMyODEzODE0NjE4MTAwMA", + "digest": "sha256:94b6bc6320addcb37cdbbd3c4c169ba391ec631f6fef6ec22b2ae4a3ec674e24" + }, + { + "id": "d7f0c074-bc63-57e3-ba04-47cfb4d6e1b0", + "version": "MTc1NjMyODEzOTI4MjUzODAwMA", + "digest": "sha256:3f2862e9e470ceeee9511f67bbb424a499c34613ad21d00e71f08d372de2c075" + }, + { + "id": "d7f5cd71-ab7d-5277-95c6-612b4c5b53a6", + "version": "MTc1NjMyODA5NDM5MjMyMTAwMA", + "digest": "sha256:ed0e29803eb36d7ae9d35819200b4c06bfdf745926cf9a741a105d457e3bf0cb" + }, + { + "id": "d7f5ea0a-561e-5aee-b0a9-1ce8bfb5941c", + "version": "MTc1NjMyODE1NjQxNzQ2NTAwMA", + "digest": "sha256:a910f90ba180246fad827ec882ef997d527138fbc0e832af5f28f6dfefb6ca7a" + }, + { + "id": "d80f7dac-6a93-5147-bdea-d16dc0f6f47c", + "version": "MTc1NjMyODA3MzE0Mjk4NzAwMA", + "digest": "sha256:0aad9e484b63099bbc9671ec3611d11a5a4ec1189aa094b98d4f1f5d26f7a1b2" + }, + { + "id": "d824a81e-3ffc-5a33-9adb-fbd5db0e15f6", + "version": "MTc1NjMyODAyNzQ1MTIyNDAwMA", + "digest": "sha256:52a256082b1a050ac6a4f9ad07ed80352d8d486df2861099db567718de9da0a1" + }, + { + "id": "d83b7e27-0902-5266-afe7-8f4f4689c64a", + "version": "MTc1NjMyODAxMTExODY0OTAwMA", + "digest": "sha256:9116631cd25e4041868c9c8128bb31e165e946bb704ceedaa35794a5ed948de4" + }, + { + "id": "d8427f7a-c38d-565a-a34e-962f56b43c62", + "version": "MTc1NjMyODAxOTMzMDI1MTAwMA", + "digest": "sha256:a208847fd45ad1ce2070c77a2977c0640eb6707f4bda9eeb5f051875d5436d49" + }, + { + "id": "d85b699b-d4c0-5921-a3ac-49474ebcb7ca", + "version": "MTc1NjMyODAxMzg0NTAwODAwMA", + "digest": "sha256:125710fb1cc9dea3149826be367de6681903dc8a40deb72dd822da7350b45c3d" + }, + { + "id": "d869e7d0-8f7a-557b-a3c2-54e988832042", + "version": "MTc1NjMyODAxODg3MTEyNDAwMA", + "digest": "sha256:667b9081051e9aafd090e7a48629a3b50e9eba8532abefc4d735e3c5f7b17be7" + }, + { + "id": "d89ab39b-a9eb-57ab-8039-13202043a96c", + "version": "MTc1NjMyODA0NDkwNzE5NjAwMA", + "digest": "sha256:4175b7ef221382556201211e8b1b38f9db766d06b0ac09eaba24fc42e5f910e3" + }, + { + "id": "d89b3aaa-0ee8-529d-bdbe-bd2d2fea0f6e", + "version": "MTc1NjMyNzk5NzExNTcwODAwMA", + "digest": "sha256:3bf045aecddc2d3c09b83712ea01e9703352b7a4cc5bcbbf526722a000c6eda5" + }, + { + "id": "d8a2554e-7cb6-5209-aa39-cd4648498e7a", + "version": "MTc1NjMyNzk5OTI0MDQ3NjAwMA", + "digest": "sha256:a34146e2aef682c6691ac931a3bdfb16544a6eb25d5dc83631370706a5bea77b" + }, + { + "id": "d8bf1ff0-ef4b-5cb8-a4cf-3a73835c38a6", + "version": "MTc1NjMyODAzODY1MDI5MTAwMA", + "digest": "sha256:8fa306fa3b9fecdfac569b84014564cf03476714b194b19972d3b67bc54f2d85" + }, + { + "id": "d8c77820-a479-5900-8f66-bfccb5fa2872", + "version": "MTc1NjMyODE0NzU4NDY0ODAwMA", + "digest": "sha256:6cec8e16fa08c6ef79eb54f2fc13edfeb4b36075321fb29f9a458848f3be09da" + }, + { + "id": "d8cfb4bc-4c1e-530b-b517-0d7fb397253c", + "version": "MTc1NjMyNzk5NTY4NjA1MzAwMA", + "digest": "sha256:a2a22daf98ac538eedbfb559d60bd544bb0af805659d0d6899f5dec4cf0488b0" + }, + { + "id": "d8d35c03-3267-59f7-a481-56a852a1a210", + "version": "MTc1NjMyODAyMzM3MjY4MTAwMA", + "digest": "sha256:09c18eb22c861848a152e7290797ec16dd30807527eede3cf324a28fc33ced74" + }, + { + "id": "d8e07c44-e47b-501e-ad31-dab632bf0c65", + "version": "MTc1NjMyODA4NzY0NzU1MjAwMA", + "digest": "sha256:655b4be17671cbfe6c4e850f4711a756c0f09dd80445b682781992944fd00b5b" + }, + { + "id": "d9396fc8-7055-5150-952a-c3692f7018c6", + "version": "MTc1NjMyODE1NDY5ODk5NTAwMA", + "digest": "sha256:50279ee08e73c6413eb56977387609c1b5e80eb7996c3419d795a52b75313c7c" + }, + { + "id": "d94b0a72-cff5-5c9d-9f5b-6119fac44c95", + "version": "MTc1NjMyODAxNTQ4NTQ5MDAwMA", + "digest": "sha256:893b905e82b9e2028a3e2b0bbdf6981b6c19031cbd39ab990adab82a91915dd5" + }, + { + "id": "d95eb022-0487-5e75-bbea-169d439b3be8", + "version": "MTc1NjMyODE3MDQ3OTc2MjAwMA", + "digest": "sha256:4b2139f81920922600352fa194fe802f4df7cdd3dd435763ba3d2240d3e7a4ee" + }, + { + "id": "d966f4fb-ab25-54c6-8eca-0afda22c39ba", + "version": "MTc1NjMyODE2ODk0ODE2MzAwMA", + "digest": "sha256:3798cc400c1a4d09b0097cd2d813849ac7be6250011b49618ad6bb446eac58ab" + }, + { + "id": "d96b96d8-6250-548e-a64a-13de5baa700e", + "version": "MTc1NjMyODEyNDQ2NTE1MzAwMA", + "digest": "sha256:dbf2ad541e566916a8cd60fd13fcde5aef3ea5a5a4234ac141b70bdc6cbc831b" + }, + { + "id": "d9a8d592-0987-5ebc-867c-2aa44024c025", + "version": "MTc1NjMyODExMzU0MTI2MTAwMA", + "digest": "sha256:e384ec93c03a448a4c3adb0bb3ce1834d82e10f8fd30b87ac8a81a179070f99a" + }, + { + "id": "d9b853ff-1087-5b1f-b5ab-7ffb312d5193", + "version": "MTc1NjMyODA1NTcyMDM1MDAwMA", + "digest": "sha256:d90de3794ce3ac5987d805c76fd944e9bf24929211ef33c77440e17ef128c497" + }, + { + "id": "d9d2db1a-4e25-50fc-8a54-fc615d0686eb", + "version": "MTc1NjMyNzk5OTI3NzE2NjAwMA", + "digest": "sha256:e4cd778d1b899644b891667bf58e3e3ce12a15e23bd2196ac584ad825252d170" + }, + { + "id": "d9e22bdb-9600-5b75-aee9-bc5c19faa005", + "version": "MTc1NjMyNzk5MDM4MTQyMDAwMA", + "digest": "sha256:f9cbe742e559ddece5e8367b556edb87292846b904e0d37a43900c4449caf8b6" + }, + { + "id": "d9f0f468-e018-5be1-baef-10f52556fee3", + "version": "MTc1NjMyODAyNTYzNDExMDAwMA", + "digest": "sha256:6f3ebbddb0f23dab15762a2349d5872a2714bdb7db3c51cc0588c2be337b3b0a" + }, + { + "id": "d9f30574-041e-5f43-a972-61c197bf7241", + "version": "MTc1NjMyODAzNjg4MDAzNjAwMA", + "digest": "sha256:0e84a7b1a683479c5631625523f11a1c498bdfc0e39727f6436f31b392d9dfb1" + }, + { + "id": "da18b840-43e9-57c5-b5ed-d0d52e8f3dc7", + "version": "MTc1NjMyODEzODIzOTI3MTAwMA", + "digest": "sha256:1d7e8f9e2738ef9fd4d13eed5c1997eee3cbc9df6fa41ebbfc56fb848af2f865" + }, + { + "id": "da27c902-d604-5dd9-bccb-714900e016b2", + "version": "MTc1NjMyODE3MjQ2ODgxNjAwMA", + "digest": "sha256:73e7081a9ac3dca204d7f7e11ffe134b8816cde3ba512be013b8ecd518210bbc" + }, + { + "id": "da28ad71-5696-5bdc-a4f7-e1de41421425", + "version": "MTc1NjMyODAxNjQ5NzA2NzAwMA", + "digest": "sha256:240c18c367527ff222cccccbd8eed2ae23283adf6d1973e70d94d4b1ab04e45d" + }, + { + "id": "da458fee-e681-5812-8f95-30567d4c1bec", + "version": "MTc1NjMyNzk5ODIzMDQ3MDAwMA", + "digest": "sha256:670aef8794e4bf2b3cf86c78c89a279fe26baa4ae3073d80da92e48fe72bc318" + }, + { + "id": "da4ec6e4-da62-5858-8817-01c6d4e50770", + "version": "MTc1NjMyODA5ODYwMjg3MDAwMA", + "digest": "sha256:fcc0b16afe3069df4ad8d8eef7154d177f932c2e75c5d1e8a9f9eb44fff37fd2" + }, + { + "id": "da530504-91ad-513b-ae4c-954a446828a9", + "version": "MTc1NjMyODEwNTM4MTk2ODAwMA", + "digest": "sha256:69b8a04a4dbdc72357c6774900f3d3dcac8f2ee8eec95134037e88e83d240ee1" + }, + { + "id": "da60a25e-36ee-5461-ad67-4b1ee8621150", + "version": "MTc1NjMyNzk5OTM3NTIxNjAwMA", + "digest": "sha256:644229cc46f1da4ecd8e29323edc5bb896e7386e2582adbfd6a544531836e083" + }, + { + "id": "da7578aa-6c65-5bca-82d9-27102271e929", + "version": "MTc1NjMyODAyMDUzNDg4MjAwMA", + "digest": "sha256:34bb763ae40a33c13681d3d828deb2f7a04675dce1429b003030b095d2ca7e4c" + }, + { + "id": "da79585e-d70d-5b9a-bf95-29f62c974a2f", + "version": "MTc1NjMyNzk5ODE3ODM2NjAwMA", + "digest": "sha256:8215756734024a06e33a124daaa14b905893e5c6ece51de83a2b7be5656b3d7b" + }, + { + "id": "da7acec9-4928-5ccd-8e09-d471dcb3f890", + "version": "MTc1NjMyNzk5ODE5MjA0MjAwMA", + "digest": "sha256:ef83d5662551528fa2a7aceea73115e8930af044e0cf68ef870bf38785cb185d" + }, + { + "id": "da8561d4-4aa7-538a-ade2-5426922cd7c6", + "version": "MTc1NjMyODE2MzI1OTU0MjAwMA", + "digest": "sha256:857625b30c46697aa57a438b0318098c6704d35e26575593710dce36f6d9ea91" + }, + { + "id": "da8ba69f-f676-5d6a-80ee-d416f6c75606", + "version": "MTc1NjMyODEzNzYzNDEwMjAwMA", + "digest": "sha256:7558e91374c46751127ae7ce16c5c6cafb89d12c64295a108e516b95b2af0b3e" + }, + { + "id": "daa18288-8bc4-5fa3-b3f6-20fa48478cc9", + "version": "MTc1NjMyODE0NzQwNzA2MTAwMA", + "digest": "sha256:96f1084bda02e662a9da5221d3020cf02a5ef72c4f8e7b57d550059b6afaa489" + }, + { + "id": "daaee2e2-bd8d-5131-bab6-7c1ff08a6f14", + "version": "MTc1NjMyODEzMTA3OTQxNjAwMA", + "digest": "sha256:cf361ed82c6d40ff2b80911b84ecbfe27d081b425e10367f0203b0a144ec8d77" + }, + { + "id": "daafd80a-6917-5994-a78e-fc6aa471aa3f", + "version": "MTc1NjMyODEzOTM3MzQzNDAwMA", + "digest": "sha256:5ad17d28186597fb3f35db8b93e194445c4c2264353b9ab8bcb746b8e24c6104" + }, + { + "id": "dae8ccca-2d98-5b44-b25a-e9b1e0ec9556", + "version": "MTc1NjMyNzk5OTMxNDE2OTAwMA", + "digest": "sha256:76926e3b21a3eef5fe71d5b53f39a724c1a112c83c41650037ba2683ad9eff89" + }, + { + "id": "dafe0594-25ab-5bb8-9416-a46e03955d12", + "version": "MTc1NjMyODE0MTQzMzkwNDAwMA", + "digest": "sha256:7160edb2da2724dc80a4b909d9c907f79293c482caeab501452e4ad609210d0c" + }, + { + "id": "db44bfaa-1699-5ae3-80bf-18474b5cbc7a", + "version": "MTc1NjMyODAwNjIwODgwMDAwMA", + "digest": "sha256:3b6f4341eee96461bbcdc5f672cc6aa4d881b9513ec05e3dbb711494b99acb54" + }, + { + "id": "db6c6764-ba27-5c58-a3d4-8152055ad74e", + "version": "MTc1NjMyODEyMjE5NDI5NDAwMA", + "digest": "sha256:b3a0508ca7beb210bd28f125bd9020fd31620bfbc2603c26388120bf9cf6d725" + }, + { + "id": "db7e3e15-9c8e-5467-a348-4a9263d25d63", + "version": "MTc1NjMyODEyMjMxNzYxOTAwMA", + "digest": "sha256:0a80cba58da7de61e7992bf4239b04d4cd836fecb4803424c6218b1a03c567cd" + }, + { + "id": "dbaa1dd0-5c82-5a8f-b542-89cfecae9316", + "version": "MTc1NjMyNzk4ODY1OTc1MTAwMA", + "digest": "sha256:c3cfe2c123baf9967b6997b61284383b6f833022abb7fa7d6d9dfbbe9a21038f" + }, + { + "id": "dbab0418-bf05-5217-91b1-5f189090d89f", + "version": "MTc1NjMyODE2Nzc2NjM4NTAwMA", + "digest": "sha256:a0f29923bda167706a7f6b69548b6d5b43f91a95cdb09f557aef8973e72e3e0d" + }, + { + "id": "dbbe4c43-7258-55f6-bc9a-ee8cdb213bf5", + "version": "MTc1NjMyODEwMjE3MjE4MjAwMA", + "digest": "sha256:031d37c2b38130a8e9a979cbf2cc0f531c7bafd17f9f95588ff96a8734a67d5a" + }, + { + "id": "dbfc9ac4-39e4-5be2-8ae6-06b8896e7aaa", + "version": "MTc1NjMyODA2MjYwNTE5MTAwMA", + "digest": "sha256:d8a13533d40431c6197929ca6262f0d8324ac0eb8d8a1406409e330734e61c3d" + }, + { + "id": "dc2e9ec2-78fc-5d3d-b3b0-21c1602573e2", + "version": "MTc1NjMyODA0MTk2ODA5NzAwMA", + "digest": "sha256:d6d864a0cf8b1aaa7dc594c6c205a4f4b8ef219a3e3d95567bdab710d6c73266" + }, + { + "id": "dc3228cc-546b-5dc6-a730-067ece257d7b", + "version": "MTc1NjMyODEzMTA0OTQxMzAwMA", + "digest": "sha256:705e4b57cd734a64b3eb58169a72c94ae8497567563f406f8c80fb275ecf995d" + }, + { + "id": "dc412fd6-f20c-52f0-8396-3613a96fde7a", + "version": "MTc1NjMyODA2Njk3NjcxMzAwMA", + "digest": "sha256:4318fe0b6c59f187933b50569b3343edd05e09f6afdfeb338a08f52335b8c5be" + }, + { + "id": "dc4a88a5-03f2-5dba-8085-1b5a5e6ba21b", + "version": "MTc1NjMyODA2MjczMTE5NDAwMA", + "digest": "sha256:7b79dfd2f5daa7b85438e7d123c8ecbf8b451f4312051a850f2de0c398bab24f" + }, + { + "id": "dc5a7c44-6e95-54d8-90cf-f49200cdc021", + "version": "MTc1NjMyODAxNTQ3MDUxNzAwMA", + "digest": "sha256:1664c3da437fbaf23280347eb68c951f28e74b06252eb4c3aceb749e4d3f5cb6" + }, + { + "id": "dc5ce9f2-8fb7-587a-b89d-145d8810cce6", + "version": "MTc1NjMyODA2Mjc5MTEzODAwMA", + "digest": "sha256:ae0d259864f9194ea4a26860965f41829e5b634629cd783e5eb90da5d85061db" + }, + { + "id": "dcba26bd-726b-5432-b567-e1c63e82ea45", + "version": "MTc1NjMyODA2Mjc5Mzk4OTAwMA", + "digest": "sha256:0971580a759bd3006dc7f58c671e04782075824b4486f3025306e0ca0b059262" + }, + { + "id": "dcc67da2-ee97-585e-8b58-88b0ab98835a", + "version": "MTc1NjMyODE0MTQ4NjUzODAwMA", + "digest": "sha256:4adaeea49bb032c34c309097b1411a9b5f5eb693a5fd8c8257c4a3c5a0bd28d4" + }, + { + "id": "dcee11ce-0a00-5e94-8110-f26838154ae4", + "version": "MTc1NjMyODAzODY1NTQ0NzAwMA", + "digest": "sha256:60d53264da8dd1051ba3975e67332dbc6aee3274fbf4a5e3c4c0cfbb49d8141c" + }, + { + "id": "dcf58415-f1ee-5620-8296-4a9d71fa7c49", + "version": "MTc1NjMyODEwMTA0OTgxODAwMA", + "digest": "sha256:756ac5ae72557bbe1a016bca90d8f9db083192d99b5b23c61bf6ad216f55031d" + }, + { + "id": "dcfa2aaf-e158-5fb2-8d6d-1fb7c207763f", + "version": "MTc1NjMyODA3MjU2NTk3ODAwMA", + "digest": "sha256:90496b1ba029d8e1a437b84aae6d84d24fa8d2c2c24b8ab6028f55d6c5754ece" + }, + { + "id": "dd0e28ac-97a5-5fae-8a6a-fff26862dd16", + "version": "MTc1NjMyODAwMzk1NjM4ODAwMA", + "digest": "sha256:0f211d5249358747fdcf95fa61ff3a7986546f31476bc502e5fd51f37a099982" + }, + { + "id": "dd2645f3-ebcf-5d4f-ad4a-a592673758a3", + "version": "MTc1NjMyODAzMDU1MDMyMDAwMA", + "digest": "sha256:609ee8252dfb85050aa03e67f9bdec99a29d714cb41d92f25105cdcd9791eed8" + }, + { + "id": "dd2aae38-14d3-5eab-b854-ecfd04de2791", + "version": "MTc1NjMyODE2Njc2ODg1NDAwMA", + "digest": "sha256:aa2315f8b0748afc304fbd6680b2f7615ec9338f90010af3565912cb0172e309" + }, + { + "id": "dd49a8d6-b980-5770-9a37-199fe29d037f", + "version": "MTc1NjMyODA5NjkxNzAxMjAwMA", + "digest": "sha256:beb584bb006b309f89b44c2d5de89845b4c48b630f5f13690d0d03d2f7a02192" + }, + { + "id": "dd575298-3a7c-529a-8ad5-237ad5bf8565", + "version": "MTc1NjMyODEyNjU3NTU3MzAwMA", + "digest": "sha256:252cc2f0421c489f3af4466da002230222c1b7d99010880a9b8ff739e8af1a00" + }, + { + "id": "dd645465-099b-5af0-86cf-db1ccff2ded8", + "version": "MTc1NjMyODAwOTA3Mjg4NTAwMA", + "digest": "sha256:474a32eea6aa203801f28a56216848065c7c6a60becfe72a98f43ca2a6cdaf91" + }, + { + "id": "dd7c6b34-149d-5fad-a6ec-a0584a523082", + "version": "MTc1NjMyODAxMDUyNDE2MTAwMA", + "digest": "sha256:c51619e08abecbb0b93bb6b1a930d7d8bcdd6bec18ba128faffb8b8ae5051e61" + }, + { + "id": "dd88ff9f-1675-5371-a724-6793b41d552a", + "version": "MTc1NjMyNzk5MDQyNzYxMzAwMA", + "digest": "sha256:5e3a7226f88910f6e1fd55b40e774ca986c6129c88d228cbd52b62305cc31a97" + }, + { + "id": "ddb56ef6-7d24-58a9-a327-e6ea39f35508", + "version": "MTc1NjMyNzk5NTY1NTU4NTAwMA", + "digest": "sha256:a141b845a8146b61bcd42a85ace248974cc92704af385bfd456b6b8cb8c0bb3d" + }, + { + "id": "ddb8fe8c-6278-537e-8ef3-7f5c94b6533e", + "version": "MTc1NjMyODA2NzAzMDcxMDAwMA", + "digest": "sha256:6ba4a44fee1308665ccdaa46a534cb32c1f73a26401b37956b73662f4cce27d9" + }, + { + "id": "ddbf4929-a760-5094-bd90-2679aee294ec", + "version": "MTc1NjMyODE2NjQ5MTgzMzAwMA", + "digest": "sha256:82deeb169592883a5fe14e65e485d7ed711764766b2be3063d68a7ec68924dbe" + }, + { + "id": "dde2371a-ff2b-5fac-bc51-c5e52f8802d7", + "version": "MTc1NjMyODEzNzUxMjQ3MTAwMA", + "digest": "sha256:94a856bc062cff45f0af85ea37b296b4defe3f7baf4afb8551cc5165cf6cd26f" + }, + { + "id": "dde9c518-643b-5714-8c86-79033083b65e", + "version": "MTc1NjMyNzk4ODcxNjQ2OTAwMA", + "digest": "sha256:3a5847ae6670d1601fbb91fcfcd63ec555f9abba70b011a34e8e0272674206e7" + }, + { + "id": "de0911fd-8b97-5ddf-83cb-5dcf27e6dd91", + "version": "MTc1NjMyODAxMTkwODM2NjAwMA", + "digest": "sha256:fd656d21314e28aeaf9d816ee3393f84c2d96b64e5e0be580006b396e2916861" + }, + { + "id": "de212cb8-cc0d-5339-a639-85b292eaf817", + "version": "MTc1NjMyODE1NTA5OTc0MjAwMA", + "digest": "sha256:94e10923474d26d40a7559e18575b663245321c868dd749d321abe8a367c1f80" + }, + { + "id": "de2940e5-cc8a-584c-999c-2625d9524417", + "version": "MTc1NjMyODEwMTE0NDMyNjAwMA", + "digest": "sha256:b1c71d580eead55f1147c47f338bf53701bbc3cbfc577b9324ebebddefcb34c6" + }, + { + "id": "de68237a-4429-5f30-b294-66924ca7327a", + "version": "MTc1NjMyODA4NzY1MjM0NDAwMA", + "digest": "sha256:954339c1d0b83ad4f631edb035f693f640c0b0d687b9a5a406c44aafc3ecbaf3" + }, + { + "id": "de7351d4-54d8-544e-b0a4-7038105de293", + "version": "MTc1NjMyODA5NzAwMzE4MjAwMA", + "digest": "sha256:629f5bdded108ad7aff416a7a8c34644a6061f2d6f1314c317352b46fdd8653a" + }, + { + "id": "de823168-511b-5ee0-af92-efebaade0f5c", + "version": "MTc1NjMyODA1NTc5MTMyOTAwMA", + "digest": "sha256:a83f4e1bc50ff1fc579003e9f07376a1038caa4f69ae2093bb1db210c68bf637" + }, + { + "id": "de96286e-ee6f-56ac-8782-d78108340620", + "version": "MTc1NjMyODEzNzQ5OTg4MzAwMA", + "digest": "sha256:d006f0a16bc2777cb9cd41f629bdfbfd27a7775560a5093cbc57715e2610569d" + }, + { + "id": "dec28d37-763d-522a-97d0-8a487eefa3f9", + "version": "MTc1NjMyNzk5NjcxMTY5NDAwMA", + "digest": "sha256:afec03993f5e7bc8adf8223605c9c506dfb6e7188a927adc5aaa50c4c47d52f1" + }, + { + "id": "decf24ea-6429-512c-b821-6b7dac4fe7d4", + "version": "MTc1NjMyODAxMDQ2NzAwNDAwMA", + "digest": "sha256:6da4b19849bdd165dbf3b955684460110901910ff1de5855f286001274763353" + }, + { + "id": "ded9c5e8-5e36-5502-b691-70438e70bd78", + "version": "MTc1NjMyODEyMDQwNDI3NzAwMA", + "digest": "sha256:694679be78c71ac6f9e5f40d75d197103853dbd1d86036dbf152a70ccbc07c57" + }, + { + "id": "dedd03c5-d7ac-52e1-9f14-9039153e05c3", + "version": "MTc1NjMyODE2NjM5NTI1ODAwMA", + "digest": "sha256:82e1101d81a4477519ded81cea7dd0d79f2ed0426146a1e19f209107e6bee6fb" + }, + { + "id": "dedd5d5c-cd05-5451-bc3e-f7803d8e9b2b", + "version": "MTc1NjMyODA2MjY5MjQ1NzAwMA", + "digest": "sha256:01d6b62c88ed6a831ce6f16f3aeca2b775e80da739d1f8a95fe6e749532d0820" + }, + { + "id": "dee42203-a6c6-5fad-a942-0afc6f0abcc1", + "version": "MTc1NjMyODE3MDQwMDc2MDAwMA", + "digest": "sha256:b37d524c936bd3b197cefa22fa519e1d282751eb41a9d0fecea1765a00319578" + }, + { + "id": "def1a994-5406-50ca-83ac-57b3f74e64e0", + "version": "MTc1NjMyNzk5NTI4NzEwMTAwMA", + "digest": "sha256:61d7ef1a3545370e85cfb3253dfa36acc2c890b4d5a20afce844af590312d944" + }, + { + "id": "defbc126-7171-5cf1-be6a-ba6b2dbe3297", + "version": "MTc1NjMyODAxNTU2OTY5NzAwMA", + "digest": "sha256:4a96adaca966fa4a99097992d57ecb19a52fa57b8fe373f29a95e2afc2cc34b0" + }, + { + "id": "df076965-6923-5bd6-ae50-c5a4834d2f26", + "version": "MTc1NjMyODAxMTg2NDk2MTAwMA", + "digest": "sha256:4c7a6a1849119a1c0d5567f8f37595762dd2d6d53cb74a70274e6f72f4b6a6f7" + }, + { + "id": "df08fe8e-72e7-59eb-8057-7cfa8da49008", + "version": "MTc1NjMyODAzMDczNzk3ODAwMA", + "digest": "sha256:5d9748bdefb3757f9d9cb158f6633e23d74b66c0001d393f8330dc1ddbe3842c" + }, + { + "id": "df1d0d2e-72ce-56ef-9982-bf08b564d3db", + "version": "MTc1NjMyODEwODc4MzI1MzAwMA", + "digest": "sha256:c35829028016ceeb5b394f62d35eea3f4d972b6a8bf29a06155714b8266c6b98" + }, + { + "id": "df2ec3d2-cac3-50f5-84c2-001cdf7d1678", + "version": "MTc1NjMyNzk5ODIxODY2MjAwMA", + "digest": "sha256:f73a8374e29ac6d2491fd9fc3c25d4b1c162b7900046bdc67e88489b51f77aef" + }, + { + "id": "df425e00-eba0-5a15-bbc6-542ac4db8fef", + "version": "MTc1NjMyODExOTcxNTEzNzAwMA", + "digest": "sha256:2083cbea32647d3ba0ab566a23e5546c00a20fd95352c51cc2ee02ce5b53897c" + }, + { + "id": "df540a3d-845f-5740-ac83-e81c2d0ceaec", + "version": "MTc1NjMyODAxMTU5NTIyNzAwMA", + "digest": "sha256:8ddf9c1cb3c97fbd5de0e81b80cbb66f7ed84f6beeaac099a0022f475f3bde38" + }, + { + "id": "df5e54fb-59d7-53de-88d0-e97b48bddcf9", + "version": "MTc1NjMyODA1MTE5MjM4MTAwMA", + "digest": "sha256:6f763f6ba4560046c88207bce7039f378cdebe0aa9e18f26ab5d109460136496" + }, + { + "id": "df81f7b1-9c16-5962-a2a6-2bd51324ed8a", + "version": "MTc1NjMyODEwODg1NzU4NjAwMA", + "digest": "sha256:40351025b3070b03666b5d82798d6e96e1e02c9915cd0000b6a90c64db6a4787" + }, + { + "id": "dfb6c526-dfb9-51e4-8dfc-a3c0dca3dfbd", + "version": "MTc1NjMyODAxOTI4NjI1NDAwMA", + "digest": "sha256:6553a740d91fe3523021019f91d63b89f22c5372fbc5390848de6220f21af3b0" + }, + { + "id": "dfc1de2a-7add-5117-acb3-ba6fb0e044dd", + "version": "MTc1NjMyODAxNjU2NTM4MTAwMA", + "digest": "sha256:39283175fa575c4c7234068287595e1e0f498b59c742511acd253cfd4e4f25fd" + }, + { + "id": "dfd852a5-6a5a-5ac0-a3fa-87f84594cec5", + "version": "MTc1NjMyODA4NzcwNzIyNzAwMA", + "digest": "sha256:07521f85b83744462e7a8124933446fb645933da9de54b75e47012303ef1e4d1" + }, + { + "id": "dff93715-de32-5d34-9085-0f3643cbc821", + "version": "MTc1NjMyODAwNjE5MzQ4OTAwMA", + "digest": "sha256:bb01f66a88c1baee63beede9e19521a6be5bcfe2fa6d57fbb74c1f7cb9961cd9" + }, + { + "id": "e00a7e4f-5199-5d12-b649-84cdf26b9d7e", + "version": "MTc1NjMyODE2NjgzNjkyODAwMA", + "digest": "sha256:df7dddcc4315d9a3f814caaca6bfb8980868c07e6c92b186c6472f546b2316fa" + }, + { + "id": "e02da344-8d64-5a0c-a9bf-ad0a73134422", + "version": "MTc1NjMyODA1NTczNzkwNDAwMA", + "digest": "sha256:f4964e95080a877bdd76d93a62b4c88479f691560817ec859a991e54cf31a0d5" + }, + { + "id": "e0369740-5403-521e-8b9a-1b3ce858f1ea", + "version": "MTc1NjMyODA4ODYwMTI5OTAwMA", + "digest": "sha256:c2f6d321bfe9b03babc5c36cd65520e87a48d284f76a8867c5d4cc98fd50c3a4" + }, + { + "id": "e060964e-84e6-5bf9-81a0-87c9f96fc6ba", + "version": "MTc1NjMyODExOTE3Mjc1NDAwMA", + "digest": "sha256:0e944a30cbacc10fe74e398d72ce18e03ec7c16274c69797862dd7ed71687f31" + }, + { + "id": "e0658696-cc84-544c-81ef-dd67df147ee6", + "version": "MTc1NjMyODEzNzA0ODEyMDAwMA", + "digest": "sha256:42936909a791ec1c6a4245ed35cc13b6cd29e73b3d992fec65badc3ddaca701b" + }, + { + "id": "e08246dc-08b1-5036-aeb8-ebe9780c304a", + "version": "MTc1NjMyODEwMjQ5MDcxMTAwMA", + "digest": "sha256:5e56fde267f5a19159243f67fd22a211b0522ed1c289c6e32f21e956f5b8122d" + }, + { + "id": "e08dcf47-b707-5e20-bf70-98aa95d77f55", + "version": "MTc1NjMyODE2MjE5NzY5OTAwMA", + "digest": "sha256:7390bcdd572996b0bfa4ce8231ee9aa8dd01c7035c65a466af518e2ffda24b9e" + }, + { + "id": "e0923147-697f-55f3-92f2-65b645f54c16", + "version": "MTc1NjMyODAwNTEwODMxMDAwMA", + "digest": "sha256:ed97996d04e7d9c954dabd4b4938d4eff0047744d86cf34866b3efed4114ee1c" + }, + { + "id": "e09669b5-7db3-599b-9b75-c3799aeaeec8", + "version": "MTc1NjMyODA0NDg0OTg4NDAwMA", + "digest": "sha256:0a22720ad333e24ba51c0cf464b3a8177e9f6cf8db6ee92a0ab648e66bea4250" + }, + { + "id": "e0a2b113-9d66-5853-8668-e06f359d9a56", + "version": "MTc1NjMyODEyMDM3NjgyMzAwMA", + "digest": "sha256:c5c187634d702fe8879407b2f34a3e52358843aa331f8170be57c8e3754119e2" + }, + { + "id": "e0d73d47-8203-5f93-91eb-27b37aa88af0", + "version": "MTc1NjMyNzk5NjcxODM2MjAwMA", + "digest": "sha256:da4fb91f71b39acd54bf9cfc10fe79ae986bd2d7ec62c2a5a6510b32256459de" + }, + { + "id": "e0e45756-d55a-5285-a5aa-d0b52bcf9780", + "version": "MTc1NjMyODAxMTYzMDQ4MTAwMA", + "digest": "sha256:2d45cdc01bbb5d1d5e517f409e632881de5c54af5fc12bd334f9fc9ee04216e6" + }, + { + "id": "e0e60aa1-d446-513d-a85c-0e2fbf5f2da5", + "version": "MTc1NjMyODAyNzQ1NzIzNzAwMA", + "digest": "sha256:dbcea74a8510bb70beb1fcd0d198faa28a05d12bfa69dd3567013770591f39c3" + }, + { + "id": "e0f7bf7f-1001-526b-8e35-2f7a5b1812b4", + "version": "MTc1NjMyODEzNjExMTQ2NDAwMA", + "digest": "sha256:09649028ea37555e44a1ef5c2297b3a9570df7dcba0c5d1ce765e790904c094c" + }, + { + "id": "e10c70a8-2e50-5c5e-8926-8bc9b437617f", + "version": "MTc1NjMyODAxNjQ3MjU4OTAwMA", + "digest": "sha256:cd55919e269c34eafe5e18ae769709a27061c5b5e2ccc65745771d00a367c0cf" + }, + { + "id": "e1180b48-23b8-5aef-b285-33b05fa20e9d", + "version": "MTc1NjMyODAzODg4NTUyMDAwMA", + "digest": "sha256:54d3fc5374c417c73db054714dbdb34aa245100bf7c0b8c329d1c6863e79665c" + }, + { + "id": "e12b0d5b-29f3-58b9-a544-0a37c16f1d16", + "version": "MTc1NjMyODAyNjgxMjA5ODAwMA", + "digest": "sha256:25fb23bca61c63b5c64d0e398560ce630e4da0fb6f67b4aafe39959656a04ce0" + }, + { + "id": "e12cae38-e837-554d-9214-3efa5064493b", + "version": "MTc1NjMyODAxMDU0Nzc2NjAwMA", + "digest": "sha256:856e17e5ef1907da5b8b1dee925817a86955825fc3bacd05ea8b8b7deda7cc37" + }, + { + "id": "e12ffc6d-9f1f-5fed-89fa-dfee041b4e97", + "version": "MTc1NjMyODA2NzEzNTg3NzAwMA", + "digest": "sha256:c220ac78c0d2d64e3e5dd483c1ee02cfb9a4a34c29e64f053b35e5281048f44e" + }, + { + "id": "e158ad8b-2b47-539d-a966-350caa4a68a2", + "version": "MTc1NjMyODAzMDc2MjA1NTAwMA", + "digest": "sha256:4b6895baf7903d02a384950ad09854d36246cb059256f3a11f561ecca8b65191" + }, + { + "id": "e1657be3-68d0-5fa3-a370-3a2a59aa86de", + "version": "MTc1NjMyNzk5OTM1NjQxMTAwMA", + "digest": "sha256:59736a909405d350e65feffc634a8980660a19bd6baba743e4ab73eb7f253665" + }, + { + "id": "e1722f2b-dcfa-5252-93e8-37ab56d5b272", + "version": "MTc1NjMyNzk4ODcwNDU5ODAwMA", + "digest": "sha256:367869e8bcdfc48e91841c38dc64b6628d871b83c4fc24ee2cb67c0288448f2a" + }, + { + "id": "e17ad9aa-14ad-5798-8d41-9d3a4df157e1", + "version": "MTc1NjMyODEzMTE3MDYyMDAwMA", + "digest": "sha256:2c45fce829748f000690c04eb25748e16075025f0a86b537aed0005380743e0e" + }, + { + "id": "e1a2940d-d861-5df4-9409-01d66e2549e4", + "version": "MTc1NjMyODE3MjQ4MTk3NTAwMA", + "digest": "sha256:24737ce4d6775a8b584fb84cf5c0229159c7a2bdfae4f4199dc28fd83bbfb672" + }, + { + "id": "e1c31b4a-b849-5342-bd15-e2d1d139eb39", + "version": "MTc1NjMyODA3MzA3MjE0NDAwMA", + "digest": "sha256:52ea1e66b4812bb026eb34a7031842c56a2c092c2138391c2d0818788d7bc8ff" + }, + { + "id": "e1d1dc98-9aff-5960-b5cd-bb1e6be94dc9", + "version": "MTc1NjMyNzk5NTMyMzU3NzAwMA", + "digest": "sha256:c6485fd19c3e58e91b12a2760a7633d5cf14cb92477a9a4e026e136ad8440c5d" + }, + { + "id": "e1ea9309-69f1-5956-aef3-232bf6067a51", + "version": "MTc1NjMyODEyMjIzNjA2OTAwMA", + "digest": "sha256:7b1c812af390393df48ecd72740f478614b840d258c0149deefdc1b46ea0d2c8" + }, + { + "id": "e1fa5343-df83-5c58-add5-792e485cf303", + "version": "MTc1NjMyODAzNjAwOTEyMDAwMA", + "digest": "sha256:f8a064d78b9798153eb3fead85301fbbf2ef175131ebc7940e2742214a980b9f" + }, + { + "id": "e22df555-9983-51a4-acda-5cf31db60da3", + "version": "MTc1NjMyNzk5ODEzODg3MDAwMA", + "digest": "sha256:177b88d3c93552f9a608137f9bce84e697d28c6259c01c779e160de70dc9d70b" + }, + { + "id": "e26918f4-e6e6-55dd-8b65-6e2f77783ca1", + "version": "MTc1NjMyODEzODI3NTM4NTAwMA", + "digest": "sha256:dbd14f081104badca3e6064043b01c9e7bba21d1d4878742ade0d384e6d0b73d" + }, + { + "id": "e27044d7-bc16-5ae9-9068-a08e2ead3b32", + "version": "MTc1NjMyODA3MzA5NDg2MjAwMA", + "digest": "sha256:57b67d7073fcc3dd2c2310527e275a14402ede6bc4d5cd18f87f819bfe46e75c" + }, + { + "id": "e2774ee3-e4f8-5c13-a61b-fa236d1084d3", + "version": "MTc1NjMyODA1NTYzNjU0MTAwMA", + "digest": "sha256:91d54206d496d28d1bc63c1bdaf1db9ae33a4e3256c2fa121b4edb2834e6a717" + }, + { + "id": "e29a1552-a3cb-5a4b-99c9-9ca7cb68fb2f", + "version": "MTc1NjMyNzk5NjcwMTA3MjAwMA", + "digest": "sha256:8fc082bfb434c4cddffdc1aa57f86adccc957918c6f31c0aaa3d7f257f274052" + }, + { + "id": "e2c51556-dea1-5955-8fd5-91eb681d5533", + "version": "MTc1NjMyODEyMDQ0MDU3NzAwMA", + "digest": "sha256:7341362dad9e0e54a208dcf78515bf525cf08a1588d399da3e6133362834f27b" + }, + { + "id": "e2d319d2-3259-5fa0-a3e8-198df6e7993f", + "version": "MTc1NjMyODA2MjY2NjUxNzAwMA", + "digest": "sha256:b23f57f4cdadbf7d5f7e7aa2074bdf34cc33ade8d5bc1201d7ac6154d0efc25c" + }, + { + "id": "e2e2a6a0-7fb6-56e8-8e8f-f0c45f635d9f", + "version": "MTc1NjMyODEzOTMyMTMyNTAwMA", + "digest": "sha256:4b2f962e29cfbe9609368a5ab141e4c2ef5a01cfb4cd3337d9c087c5f5c4e694" + }, + { + "id": "e2f22c6a-b246-5edf-a9d5-caf426780f6f", + "version": "MTc1NjMyODAxNTMwNDUxNjAwMA", + "digest": "sha256:2592986e950aa88226bcc332cbfcb937df167f77f9b7a78c7a4d8d5648f8a013" + }, + { + "id": "e2fb4689-81f2-5e72-8fbb-057f653d4e06", + "version": "MTc1NjMyODA1NDA3NzgyNjAwMA", + "digest": "sha256:adec191efca315961748c6896cb90c23baf8febf623537306219aae1b116af79" + }, + { + "id": "e30933d2-9ec3-5802-9b33-e78405fb94e6", + "version": "MTc1NjMyODE2NjQyNDcxODAwMA", + "digest": "sha256:67b8d620cb7d63d66065d735646434e187432661d31662f2417c832a7b111df9" + }, + { + "id": "e30a4091-4522-519c-9e9f-3ef279a5457d", + "version": "MTc1NjMyODA4OTMwNDIxMTAwMA", + "digest": "sha256:e9782ac96be6a994f78dabc437706b0a6da2a2d4fee1ae645afe07840e580132" + }, + { + "id": "e310ae73-02d9-53aa-a540-0e8f80146f72", + "version": "MTc1NjMyNzk4ODc0Nzk5NTAwMA", + "digest": "sha256:a5abe96b376a528a5de6cdd628d983dc17a86ad6313fe81a64368e83297fabd6" + }, + { + "id": "e3127bca-f2d7-5225-baf5-61a06cb52939", + "version": "MTc1NjMyODE3MjQyNDEwNDAwMA", + "digest": "sha256:00ab282dabc373a54d627ec4dfc05eb83ab6a2eb8db7a8313da4a594324eba3a" + }, + { + "id": "e31cd9f7-2c97-5938-9535-3662894adebd", + "version": "MTc1NjMyODA1NTc5NjUyMzAwMA", + "digest": "sha256:86571d2030b3aece151d1cc0d25f7327358b1de77e3c24e4c65e5ec58e0d30b4" + }, + { + "id": "e324c4be-d38f-57a8-b1c3-490a6e99a2e4", + "version": "MTc1NjMyODE0NzgxMTYxNzAwMA", + "digest": "sha256:8dcbccf5835389aa9af0b1d19baf53dc960c6934de9237b9a291cf18c61935ad" + }, + { + "id": "e35091e7-5622-506e-bd6b-48287a5b8681", + "version": "MTc1NjMyODAyMDUyNTgzNjAwMA", + "digest": "sha256:24f37ad707c4729d1bf17dbeaec3f2e6a477a726eaee35a382c618323b5938b5" + }, + { + "id": "e368cd4c-0a3d-5941-9285-af7b9f8ffe1b", + "version": "MTc1NjMyODAxMTA3ODY5NDAwMA", + "digest": "sha256:5d68e9a1a2fca750aaacb05320f8386680310f6117dec8b57808f3496775673f" + }, + { + "id": "e37f0c26-eee2-5d1f-80f8-f6acee4a0dd8", + "version": "MTc1NjMyODAzODY4MzEwMTAwMA", + "digest": "sha256:efe866ba672e8da1c358c9111b1c41a057f7347666e5b182bcf543ab57bb8fd7" + }, + { + "id": "e3a72fd4-d5fc-5a11-9d0d-268f2c6e3bd9", + "version": "MTc1NjMyODEzNjk1NjQ0MzAwMA", + "digest": "sha256:b9688b688fd252d26534a3180b7dca795c6b95282b3073ec099a025d7b7c92e6" + }, + { + "id": "e3b50cc6-82f1-5d73-a527-c559af822859", + "version": "MTc1NjMyODAzNDUwNjQ2MjAwMA", + "digest": "sha256:2689268ede09259a8aca878025f1dd8efcc7cd91f0b767ebaa7a3e81da50753c" + }, + { + "id": "e3b76b80-cc33-58e7-a922-28e13fc8a528", + "version": "MTc1NjMyODE0MTU4MjU0MjAwMA", + "digest": "sha256:b274f504a1c3a43559265246b88f9db976c89ded5c5e7ab1f89af32c8832e9c3" + }, + { + "id": "e3bb09b0-c188-5485-8dac-46a984501519", + "version": "MTc1NjMyODAxMTg4NjA2OTAwMA", + "digest": "sha256:70d87e1badb0ea82c1f1380892f8af14a4a99f27b55a7924b3ee08eb4a74970c" + }, + { + "id": "e3e41716-4a6c-5c7f-b47f-bc4e36445c9b", + "version": "MTc1NjMyODAxMTYxODMzODAwMA", + "digest": "sha256:358e5a81bdb786b193a1663c5a0e588395174086b5417b7c086a1823bb08cb65" + }, + { + "id": "e3ec2d35-ec76-5dd0-b132-c40f43b8d0e8", + "version": "MTc1NjMyODE1NjMzNjQ5OTAwMA", + "digest": "sha256:b7fade073968d7ba568698a82ac43b46ba5231131491be023155b2fb941f46b5" + }, + { + "id": "e4234ddf-44a0-504a-a5e5-1ef669d8c1e4", + "version": "MTc1NjMyODAxNTQ2NDIxMDAwMA", + "digest": "sha256:156156cca13b477aa879e5bff9a013ad47f8867838ad22bafb90e3294917f651" + }, + { + "id": "e431d8bd-4da7-514c-a09e-db7f49e9355d", + "version": "MTc1NjMyODAwMzk0MTYwMzAwMA", + "digest": "sha256:6bee7a44bcb37b6c3253c9dd9434c2b7e0312e1bb3983e26860a1d4b163e4481" + }, + { + "id": "e438ea3a-1b47-5a2e-b91d-d5e0a8ec4ec4", + "version": "MTc1NjMyODAwMzkwMDI5MjAwMA", + "digest": "sha256:8fb8ebdd5369a0c0f288ca51625cc2431f47333aa9852bedfaed3e6eb0b4aa0e" + }, + { + "id": "e441a822-d2da-5a46-941f-3209db3199b1", + "version": "MTc1NjMyODAwNDAzMzY4NzAwMA", + "digest": "sha256:2f6e2418526023bd776b875c15ceeddabf8f167fc38e22cb0ae19f1d3d92ccad" + }, + { + "id": "e4479bd5-b60c-5033-b01b-5e86d539c01d", + "version": "MTc1NjMyODAzNDY5Njg4MzAwMA", + "digest": "sha256:8bf56809ccc259a54ab38d37948a3f002a9b65f959b7a90085dd98da4dda9a48" + }, + { + "id": "e4513a1f-e623-575c-99f0-33217204e3ef", + "version": "MTc1NjMyODEyNjY1Mjk3OTAwMA", + "digest": "sha256:2e46db1bc6c7a64c5222ce3274fe0daadc626f1a0b3e9ff32e0c3026a2646a95" + }, + { + "id": "e454e48c-9f23-5a4a-8b3b-c6ac05924601", + "version": "MTc1NjMyODAxNTQ5MjQ4MzAwMA", + "digest": "sha256:9f2652f9491a72be950f87df00bdc1c66316e4e5463074cd3a34faff20edd888" + }, + { + "id": "e47c0975-dcda-5b34-9ad5-c73c4c1d4c19", + "version": "MTc1NjMyODA5ODY0NTU1NjAwMA", + "digest": "sha256:ee223d043b3b2d0e801fcccaf03ce2f6c05d3f45cf32b52ebcdc855cd7212553" + }, + { + "id": "e4a7b1cb-d747-51cb-9f6d-9b84f220df6c", + "version": "MTc1NjMyODE1NDYzNTgzMjAwMA", + "digest": "sha256:d3632bb1291af88cdd95405779e548ffc4445ee16ca21f382574092b594da132" + }, + { + "id": "e4aadb6b-4f4e-5e94-9bc1-08d6a22f0427", + "version": "MTc1NjMyODAxMDUzODE1MTAwMA", + "digest": "sha256:b2201d482bb2b31c3776b60a15eca41cd0a91fe16cf80a070b66a3667d3212b3" + }, + { + "id": "e4c55cd0-36b4-5701-bf29-c8cbcef8f676", + "version": "MTc1NjMyODAzNDczMDI5NTAwMA", + "digest": "sha256:26b70ff8d01f1861847e6a7b7d932fe1856b23b19d731789ebb43f50a51bbb05" + }, + { + "id": "e4e57d22-c92a-5bb3-97c6-648373037133", + "version": "MTc1NjMyODAyMzM5MjkzNjAwMA", + "digest": "sha256:eb9afae4e73cc833a30185a9fb7fa7a24df518bc7b6f00493cb3abcb1c2559a1" + }, + { + "id": "e4ed340e-16ba-5243-87bd-007d1f9553a0", + "version": "MTc1NjMyODAwMzg5NDc0MDAwMA", + "digest": "sha256:19662a59e6c8fa4207aef88cf5bc7100285ef287c59a6f194ab5ec4555d125c9" + }, + { + "id": "e502c0d3-d278-5271-82f7-6350b077b5f5", + "version": "MTc1NjMyODE1NDU2NzA3NTAwMA", + "digest": "sha256:84fb30f1a354ef3dc057b9bda5870ea38eb2523e2b5c38add9844050d2acf563" + }, + { + "id": "e50a6663-1876-5e8e-a48c-6d49141a90d6", + "version": "MTc1NjMyODEwMDk3MzU2MTAwMA", + "digest": "sha256:a1ab725d9fa3c4e5e0ff64d4294dc5fd7f8be76ebdccd94f741fb06f7fdf1b0a" + }, + { + "id": "e50e2e72-7ad9-5fc9-a06d-7036aafc0636", + "version": "MTc1NjMyODE2MDQzMTA3NzAwMA", + "digest": "sha256:5022b85d5aa4899b2d44751dda98b620c5cb96b3cb26938e67c9b1434ba67135" + }, + { + "id": "e50f9ba6-0d23-59b5-9cba-c0cd6cd9e651", + "version": "MTc1NjMyODA1MTM1NTYwNDAwMA", + "digest": "sha256:628e6fd1fc1df54a87070b90798c85e0bb8c3aed68eff48330245b0177f5f9f1" + }, + { + "id": "e51da30d-bef0-5c93-8850-0cd9b9d4d1e5", + "version": "MTc1NjMyODAxNTI2MTA5MjAwMA", + "digest": "sha256:97fee25b9c4d132afdda3e42581bb81fc6e47afa67f14d141f73b184f76f8ce2" + }, + { + "id": "e52c3f77-244c-5ee1-94aa-6e5518cfa068", + "version": "MTc1NjMyODAxMTkyNzMyNDAwMA", + "digest": "sha256:49e2dbb9d5c17fb2e615d195434dbd013f10c0019ab5c91f544cac6097423c10" + }, + { + "id": "e52d29bd-9d5b-5174-9a72-0a1938173913", + "version": "MTc1NjMyODE2Njg2MTE5MjAwMA", + "digest": "sha256:8acb9f556efd9e06ccf80446e142e017b54fc15bcd3e382ebc15e2aed024a3b6" + }, + { + "id": "e533cc23-3950-54e9-b57e-871c972a5799", + "version": "MTc1NjMyODAzNTk4MjI1MTAwMA", + "digest": "sha256:92c3743888f21ea287dc2c222781ce891e15b84d2e3706dc7ba100985701b770" + }, + { + "id": "e54fa86e-2322-5a6e-9bf2-7346903700d1", + "version": "MTc1NjMyODAxMTg3MzE4NDAwMA", + "digest": "sha256:38f8758193484119ba7adb75e401bf8c60ba8c19f1876baea8f4269f726b3edd" + }, + { + "id": "e554fa55-6c16-5eb3-9008-fa4fabb766ae", + "version": "MTc1NjMyODE1NTIzMjYzMTAwMA", + "digest": "sha256:7fe627feb46f33b113c0d710fd5f730ee13e75e8e1f32b129caf214df553268b" + }, + { + "id": "e570737c-debd-595f-ae6d-c4b4873e7255", + "version": "MTc1NjMyODAwMzkxNjc2MjAwMA", + "digest": "sha256:cfed5fd7a29c5cd01f919ccfc7f31deaed1f4eaf8ad0b8af18b78cda946a03d0" + }, + { + "id": "e5784d54-6f85-5c6f-8ca4-b8924d2b002e", + "version": "MTc1NjMyODAyMzQ0NzA1MjAwMA", + "digest": "sha256:21bcdd202b95d8688c8c19ec004d8ae67fbf86a7d5c7a7676bbdbc61e781d6e2" + }, + { + "id": "e5812f9a-9618-5d75-bef8-e720fd07f150", + "version": "MTc1NjMyODAzNTk3MTM4NjAwMA", + "digest": "sha256:1ebf65d89f7a3c20c406ab72c09e8c37fc6ef3e9201b269150a7ab8956d06a47" + }, + { + "id": "e5831f02-901e-51bf-82c2-f308d9d7e5ff", + "version": "MTc1NjMyODA5NDUzNDk1MzAwMA", + "digest": "sha256:12c24151ddb13c4df9b4bf4d3093e7c31b7ca7eb36e4777712b4e4fe1e00f8fb" + }, + { + "id": "e594efaf-084f-5794-bf38-63987fc9eb23", + "version": "MTc1NjMyODE3MjQyODE0OTAwMA", + "digest": "sha256:f1f5e65d939f34554ab3d09653640d6a563e63b5f440611f8bda65fff4c78943" + }, + { + "id": "e59994b5-8042-5343-82a6-5dc804c77337", + "version": "MTc1NjMyNzk5ODE1NjIwMDAwMA", + "digest": "sha256:b125d27351a5d29d532800a358b9064ec65c330b965ab25ccb355adda67da838" + }, + { + "id": "e5afe5c2-4724-551d-9536-e65c423518bf", + "version": "MTc1NjMyODA5Nzg1ODg2NzAwMA", + "digest": "sha256:1acc4bef2ea757d184d5f487af656d1457049f34054922c7b822a112b5775c19" + }, + { + "id": "e5b50771-5065-575e-9a02-c2da1b98609f", + "version": "MTc1NjMyODA0NDg5NDk0ODAwMA", + "digest": "sha256:f0c0b09b0956b1d645c0494db4881b55f1fd0f6abfc256977781e0f611794b46" + }, + { + "id": "e5c9535f-2f52-56c4-9a8f-b80f2c3e0cc1", + "version": "MTc1NjMyODAzOTcxMTUzODAwMA", + "digest": "sha256:c4eebefd7d327090e09b20b06806627c3be9c1eaab4795e13ebea022af189c70" + }, + { + "id": "e5dec694-b2b0-5987-a7a8-63239a558b6a", + "version": "MTc1NjMyODA1MTI4NzQ5ODAwMA", + "digest": "sha256:5cd55f9d98c31ab7160645e2ab4a5c60cd8ad02a6c723f9c2d0cbb9db33dc7e0" + }, + { + "id": "e5e35c43-f94e-54bf-bf68-730838c0bc09", + "version": "MTc1NjMyODE1NTA5MjM5MzAwMA", + "digest": "sha256:abe948d321edb65f4dc9daef2952138ab9a4120b2a12c7e3331b287168bbac17" + }, + { + "id": "e62180b4-8cc6-5947-afe5-166d5d862f1c", + "version": "MTc1NjMyODAwNDA0NjE4MDAwMA", + "digest": "sha256:12c7c183ee5bf0a4ffc8c32b57965ce9e1b661d0a28b66d929095338f1859d72" + }, + { + "id": "e621a3a5-5b9a-5f37-8304-f7a99d409a6d", + "version": "MTc1NjMyODAyMTI3NTU5NTAwMA", + "digest": "sha256:d4896903ef48ec7d7a0a8a57d8fe684d36fceda6ba824f9f7ed0f5b0b1b680c7" + }, + { + "id": "e62643b2-8d68-5789-bfb7-c41190e7a263", + "version": "MTc1NjMyODEyMjI2Nzc4MTAwMA", + "digest": "sha256:e37e330ec611e1fab87d70d7d43c6b0f39de83c281fb009d3c5ea4f867101d27" + }, + { + "id": "e635647a-d677-5dc4-bf50-8eea7adbd300", + "version": "MTc1NjMyODAyNTk0MTY1OTAwMA", + "digest": "sha256:f1d297ac96d7ed0d6dc07c91eb4929904f81710b6dadfd07bc82069990e7ec36" + }, + { + "id": "e641a2b5-08e1-52c5-9597-1250609ba766", + "version": "MTc1NjMyODA0NTcwMTI4MDAwMA", + "digest": "sha256:a459aeef8fc4173dcfb5b9fe0dc6825b021a07aa62f6cfaea0e86145cb66c6eb" + }, + { + "id": "e642d451-e2cf-5c14-9ea6-c57dddfb1233", + "version": "MTc1NjMyODEwMjE5ODM1ODAwMA", + "digest": "sha256:c0a627c45feceb2da896f4a82382463524c2436d0a950a41c15f0b1c2ea62621" + }, + { + "id": "e661112f-bb5e-51b4-88cc-2ce46dc535d6", + "version": "MTc1NjMyODAyNTcwOTQ4MjAwMA", + "digest": "sha256:19b26462f899d1fc71c9d17cbf3466a6e528de0e2ffe1b0ae407721cadc55de1" + }, + { + "id": "e69ff811-e1e2-5601-aa89-d0536ebe1e22", + "version": "MTc1NjMyODEyNjY1Mzk0NzAwMA", + "digest": "sha256:58d427310c56c5e5cface4f3431b3176b968a66b37778334be622c609048155e" + }, + { + "id": "e6bca2a1-1234-5fb3-9149-dec347fd98ac", + "version": "MTc1NjMyODAyMDUyMjQ3NzAwMA", + "digest": "sha256:7e9031f8a0a68e93b3edb052b4618be5d3db48e8519eac59558d02d96e0c865e" + }, + { + "id": "e6d0e25c-6f7f-5dac-b556-cd96323890bf", + "version": "MTc1NjMyODA0NTcxNzA5NjAwMA", + "digest": "sha256:0c843d4279187e5b5b2f2176e27791a059edacf69737e72b407d03e0587525b1" + }, + { + "id": "e6dda29f-423a-5756-b3a5-b17d08a3a24b", + "version": "MTc1NjMyODAwNDAzNDQ2NTAwMA", + "digest": "sha256:8c63902960e50315ede9d9b382f9999ab5fe070c86106139103da3e188d3dd2d" + }, + { + "id": "e6e52ce1-1407-50aa-bd41-5fda1e558837", + "version": "MTc1NjMyODEzOTMzODUyMTAwMA", + "digest": "sha256:180930ca5871065a16d6a471cc0141f1ef9ce5c9d6471ca04e845f47ece3f837" + }, + { + "id": "e6ea46ed-e5b3-5d24-ab60-2b6db2bdd9dc", + "version": "MTc1NjMyODE0NzY5MjgwMjAwMA", + "digest": "sha256:b5017979f1d3e1691ffdda2e6639ed51514a91529afba99691031ceee60bf36b" + }, + { + "id": "e700c6d0-5ea1-57f3-94f3-ebe8e2a6bf47", + "version": "MTc1NjMyODE1NjUxMjc4MTAwMA", + "digest": "sha256:78ea5e782b24404c9a0a0aa8df6d57fc142f65a246e5e363a4bb22a87d7aa2e5" + }, + { + "id": "e7046843-c6cb-5d82-aada-86de186a4e58", + "version": "MTc1NjMyODA0NTc2NjgxMDAwMA", + "digest": "sha256:9756869066586baf0f617aa6ee7c8cb3dd6a1afbb291b30ef75cc3638700e430" + }, + { + "id": "e714bd8d-bba9-5f9e-be10-0851338d688b", + "version": "MTc1NjMyODEyNjY1NTM0NjAwMA", + "digest": "sha256:eca76df64a1b0f66fdfa2e8223a759b61f6f888c1e38b26e520ea79ab7a99009" + }, + { + "id": "e720f0f7-22c5-5a98-8de6-ab4196041897", + "version": "MTc1NjMyODAxNTMwMjIwOTAwMA", + "digest": "sha256:4009d063bf2804c30f38e067b5f82f457f4ba15a3047fab367416dcc54d703d0" + }, + { + "id": "e72687b2-7f75-5e44-83fa-382671830db8", + "version": "MTc1NjMyODAwNjIzMDE5MjAwMA", + "digest": "sha256:3af76384d313bc01994189f1edf111d1a5a15b22a843cb8229950737fdd77e34" + }, + { + "id": "e737192a-c3d4-52b1-9163-b9ed19d24c6b", + "version": "MTc1NjMyODAyNTkwMzgwMTAwMA", + "digest": "sha256:4c034998f7cb0726de3c9c948ae5c7b79186d45087e41a67ccf205b92d2ac232" + }, + { + "id": "e7714f7e-1c3a-525e-9029-d8dac2a7c980", + "version": "MTc1NjMyODAwMzkyOTQ3MjAwMA", + "digest": "sha256:bff43c6cdaf8d2f3474eb5fdc3d6f5060f42aee6f8f3de1fe4f100eff3167d76" + }, + { + "id": "e771b35d-d207-56e1-acfd-2ab657603fbf", + "version": "MTc1NjMyODA0MjAxNDUwMTAwMA", + "digest": "sha256:2a4b79a58f791cd80fbf5c59b3d2701c116a2fe69d219ec75aea94e320174124" + }, + { + "id": "e7964f3e-dd8b-5c6e-aebf-35951f1e9aed", + "version": "MTc1NjMyODE2MDQyNTUwODAwMA", + "digest": "sha256:46aaa8d555e682a90f45918c41a3931e8b64c2f9c369f477be3de2ae7f8c3460" + }, + { + "id": "e7b96723-735b-56a7-bd7c-26f7de983cfa", + "version": "MTc1NjMyODE0MTUzNjIxNjAwMA", + "digest": "sha256:e950c809af9d308feb07971d6f32d1a0419c4007d2f6ef7cc9abeca2c8faceec" + }, + { + "id": "e7fecb9e-74b9-5f91-a6b3-97cef4a6c74d", + "version": "MTc1NjMyODAyMTI1OTU4MDAwMA", + "digest": "sha256:f8d12b1c9c6255914b0400e66e943cac2254b995829f4f92bb53cccb3d7b2373" + }, + { + "id": "e8267bfa-3c24-58ff-8043-a4f98a28228b", + "version": "MTc1NjMyODAyNTc2MzQ0NzAwMA", + "digest": "sha256:dd3afbda780dc25fa8eba477b3b1977a6b61c5bcf28e2de89456bfe90ac5fa97" + }, + { + "id": "e826e51d-55e0-5e03-8c0d-95456cc16032", + "version": "MTc1NjMyODAyNjc2MzY2MjAwMA", + "digest": "sha256:9343107afdb71401ce725fb84b249bdb014025b5ecbe4700df91944044847d20" + }, + { + "id": "e84bc9c6-f1ed-55ca-b577-349a0e598262", + "version": "MTc1NjMyODE0NzY1OTU2MTAwMA", + "digest": "sha256:b1fce57b7892c67a0544bd9f573ac502f2dd204282b90c318a5b42dd8fa8b4e6" + }, + { + "id": "e852b24a-fda3-5bff-a86b-510c2305346d", + "version": "MTc1NjMyODE1OTE4MDI1NjAwMA", + "digest": "sha256:dce817e90e61a6d4a297c83b0d4751fe097ce72977edaafaf7bfcdd5a5d81532" + }, + { + "id": "e85c417b-5321-5a54-8c2a-01a27db890ea", + "version": "MTc1NjMyODAwODk4OTUyNDAwMA", + "digest": "sha256:9b106cf98762425df8e70de55f5632073eeab822635c0899164b2ffd0cc5ee19" + }, + { + "id": "e8697292-3a07-5877-a652-fc9d3b120492", + "version": "MTc1NjMyODE2MDU2MTQ2OTAwMA", + "digest": "sha256:66edf1ccc75752fff9bb7fc50539f89115a9b2b5bdbf1959a39318ea1f39db18" + }, + { + "id": "e8798f14-a710-5e73-9bde-a5d8eceab24d", + "version": "MTc1NjMyODAzODg3NjkwNTAwMA", + "digest": "sha256:a962c1e9b8c27030e270fc7ca8047c48f4fd6de4cc054b7ad5375e4cc2eec774" + }, + { + "id": "e87c8070-21a0-5bfc-b60b-cedab155c5de", + "version": "MTc1NjMyODEyNDQ1MzI2OTAwMA", + "digest": "sha256:202b90af7461d291f2e19c0631a4f2fdb7823ffbd831e081844d8891249ea294" + }, + { + "id": "e87dcdc7-7131-55f1-b608-88b2865bf5e4", + "version": "MTc1NjMyODE1OTAzNzQ2NTAwMA", + "digest": "sha256:34006287041953f6de4f43a86d7c5ed1f7f71e713a67f3d72d44cea8ce0277df" + }, + { + "id": "e887aa0a-a48b-522b-a6d0-f47991f35d6c", + "version": "MTc1NjMyNzk5Njc0MDIyNDAwMA", + "digest": "sha256:c5d4b5faca96ff21c087da7610f6e30d5fd13f06e55de4907d32d41917b8b7a0" + }, + { + "id": "e88d4c3e-424e-5046-a944-477d7c0cd912", + "version": "MTc1NjMyODE2Njg3NDUzNjAwMA", + "digest": "sha256:cee48b3382d1e1fa4309d02b5fbae6d5c8e6b6e29591fbd39fd63bbd9748125c" + }, + { + "id": "e8acf6dd-0a2e-51d7-ba7e-75284b2913fb", + "version": "MTc1NjMyNzk4ODY0ODE2MTAwMA", + "digest": "sha256:dbf5ff7c3d88544323f74745daf1d49c2696e7090b9a6398de34c64b0041751a" + }, + { + "id": "e8d3fba6-32e4-53e9-90c8-f1c6958d59c8", + "version": "MTc1NjMyODEzNzA1Njg4ODAwMA", + "digest": "sha256:a8e4f66e29b717fee52f06aae82bd1b26d43404f4cd67a02929f64b37024071e" + }, + { + "id": "e8f5ce0e-0b9c-55ec-b546-c5d7d2d040bf", + "version": "MTc1NjMyNzk5NzA2MzE5MDAwMA", + "digest": "sha256:6cdb491b1c5576409a432a9f83018e5621049ff78b37c9a66a8cfde099b78a52" + }, + { + "id": "e8f93b1f-9eb4-541e-ba19-4de2665ea72b", + "version": "MTc1NjMyODAwNDAwNzY0MzAwMA", + "digest": "sha256:aca27f2ec202240705c0bb159d1014263e3eff4773f50d4e5432c3398834546e" + }, + { + "id": "e8facaa8-25c3-5132-bde4-f22d88a9c260", + "version": "MTc1NjMyODAyNzgwNjMyMTAwMA", + "digest": "sha256:901a77d18c7b91841f714524a9ec7f273fc95af28cbd5ba50747111d14a5064a" + }, + { + "id": "e92d3ba8-aa27-524f-9906-ca1626bca2a7", + "version": "MTc1NjMyODEzODIwNjQwNDAwMA", + "digest": "sha256:4c2d50fd09e92f9e5a687c079cba4552b72080511dd40fe6e9202fdd6543c1df" + }, + { + "id": "e93660de-7324-57dd-982c-f8357812c2bb", + "version": "MTc1NjMyODAxMzg1MTMyOTAwMA", + "digest": "sha256:78f889f6f0d4465499db6fdeee9fc5c728180c6920b838042944e2668f46c177" + }, + { + "id": "e9370923-6c60-53b2-8af8-816bbc047f73", + "version": "MTc1NjMyODA2MjcxMTQ0ODAwMA", + "digest": "sha256:dcd18cb4070312c9e6ca11f5531517be5f0e6f42634cee1565da349b4ea7f0c2" + }, + { + "id": "e96c2fc9-bd15-5eda-ad4c-016b6d5905b8", + "version": "MTc1NjMyODExOTI1NjQ4NzAwMA", + "digest": "sha256:28d477b35ca972394bf0096fcf4621997eac513d1e930874c1ac2c7e38539637" + }, + { + "id": "e974e28e-436b-5d88-be80-01d7fd16aa23", + "version": "MTc1NjMyODAyNjgxMzE2MDAwMA", + "digest": "sha256:a5f3e415d638a93f85566fb9b2fdd8c70374549f6b9276a7f84b1468901f4556" + }, + { + "id": "e99c954f-9ea4-5846-abbc-f2f3204cb069", + "version": "MTc1NjMyODA0NTY5MTM2OTAwMA", + "digest": "sha256:1ca239c06cd3e8070fc00741ca8fe2efc647d21daec866545a8c6838a9228e91" + }, + { + "id": "e9a65253-c5c5-5e35-9ad9-364f73364124", + "version": "MTc1NjMyODA0NTg1MzEzNzAwMA", + "digest": "sha256:f009d779483ad68344960a59751486071117401db5625aec187929ff9808e1af" + }, + { + "id": "e9a88c29-2471-55c6-a98b-bbff7432f56e", + "version": "MTc1NjMyODEzNzA4MDg1MTAwMA", + "digest": "sha256:8f7ed2cd88f1f31cfb47df027122f57eb2f970810a7cd7b1af67ccc1e1dd3b76" + }, + { + "id": "e9aff5a1-936d-5dbb-a570-7bc8ce31ccd0", + "version": "MTc1NjMyODA1NTY2NzIwOTAwMA", + "digest": "sha256:3f147277069f4b472a6917e7cc3211a790d69ab78b6a1132ec3e3d36ded09d85" + }, + { + "id": "e9b58d3a-c8e2-55b2-80ac-c40dc40d4490", + "version": "MTc1NjMyODAxNTIyMjc0OTAwMA", + "digest": "sha256:d71aaa8144c412053c47d3399c8d43febf64b80b0eff0e6a6ecfbdef599c6a54" + }, + { + "id": "e9c2043f-eff4-5fc3-b326-d9f3f80b34c3", + "version": "MTc1NjMyODAxNjU0NjY2ODAwMA", + "digest": "sha256:35c4c0f9a07eb0d1a0514d98b99eb61b11cc78547dd96f1116454a025e077da7" + }, + { + "id": "e9c68dd6-e672-54d0-a890-b07d2a763bc3", + "version": "MTc1NjMyODEwNTM5MTQ0MTAwMA", + "digest": "sha256:df87e275cefbb83dc2a87d16e7500506b91aa9b620afdc70fd94791675bfd9d5" + }, + { + "id": "ea14988e-599b-5abb-86c1-a86fb7a38ac6", + "version": "MTc1NjMyODE2MjE5NDEwOTAwMA", + "digest": "sha256:10710fc78cc084ae004567aaff68960980f12c4bd6c781c74b02f44a277052a7" + }, + { + "id": "ea50604d-e504-57c6-a786-2df53d7c3ce9", + "version": "MTc1NjMyNzk5NTM4NjI3OTAwMA", + "digest": "sha256:acfa9b49be37957c7250ce5ee193ff43a5b5c2c55f268799475cfeaf52609045" + }, + { + "id": "ea5aac20-76c1-58c5-8eb0-c8188aa5180a", + "version": "MTc1NjMyODAwMzkyNjY1MTAwMA", + "digest": "sha256:16a76061a9bf5a82a530f3436b5ec4b673bb08e72f4518b1bd14ba17c64f0922" + }, + { + "id": "ea728e30-97e9-5744-9423-a5496ee0b275", + "version": "MTc1NjMyODAyNjc5NzY4MTAwMA", + "digest": "sha256:15c2b4386d34e49a6049be006a1668720b14e6a699e9baade5ffc1c8540133be" + }, + { + "id": "ea76498b-452e-5f72-8832-3532b27bdf07", + "version": "MTc1NjMyODE0MTQ0NjAzODAwMA", + "digest": "sha256:eb32bde3912d81b43561429f1033c5a8adb365deef844f1d7b7200fecf2a78af" + }, + { + "id": "ea9729c7-1dd3-548a-b0f8-e97255e3d33d", + "version": "MTc1NjMyODEzNzM3NzcwOTAwMA", + "digest": "sha256:738f90b36ac89c07e7d34b4e7e2b4bb6c3e243aa8ff281ab9cb56a4c6a1e6f93" + }, + { + "id": "eaa7a33a-7f06-5505-ab22-d93918305a0d", + "version": "MTc1NjMyODAzMDgwOTE5NjAwMA", + "digest": "sha256:3b552cc06ea5361a57e245e813756ae1a712adca9ecd8a206ed7f9d44b0b2b89" + }, + { + "id": "eaa9818f-df04-508c-97ea-2bdbcabb8706", + "version": "MTc1NjMyODE0Nzc4NDU0OTAwMA", + "digest": "sha256:56702f8e242329b61ca689c3e7e828df932f6b44b9fac123fc35a3f8008ef588" + }, + { + "id": "eacac340-6b76-5ac9-8a1a-8735cb1375de", + "version": "MTc1NjMyODEyNjQ5MjkzOTAwMA", + "digest": "sha256:b0016604b7f23376992ddfc63836cebfe30a65ad79c67b3c1f15bbf75a342ae5" + }, + { + "id": "ead36c14-fcf9-57e3-b95a-f4a076ae1136", + "version": "MTc1NjMyODExOTY2MzIzOTAwMA", + "digest": "sha256:0d9f78904ab6ba2ccabfe0c4aa7a65244df709ecc70fd92bcc97488d7b281816" + }, + { + "id": "eae3a62f-a511-522d-be02-d260b3516bbb", + "version": "MTc1NjMyODEwOTE1Mzk3NzAwMA", + "digest": "sha256:8595f27efbf81ce84e3566515477b347a55f2a4a3fb24d0ae96fd4839a6333fc" + }, + { + "id": "eb3bd12e-dc9e-50df-9169-bb2b393f2421", + "version": "MTc1NjMyODAyNzc4ODk1NDAwMA", + "digest": "sha256:3e5528badd98bd9fb882f44eec8b796dc0986027b4188a214afdf7677810c7f7" + }, + { + "id": "eb6d29c7-3f80-5a9e-8b86-5a84934fc181", + "version": "MTc1NjMyODEyMjI2Nzc2MjAwMA", + "digest": "sha256:219c5929e8d8b16cd1b3a90dddf699cecd29039efa916913e13f208bcb9872e1" + }, + { + "id": "eb6efddc-afd1-5dbf-912c-b9864f7ef9ba", + "version": "MTc1NjMyODEzMTA0MjE0NDAwMA", + "digest": "sha256:aaade0f42b59d29204952fe6ac2c7d28218458d01596697931ea1a039e9563bc" + }, + { + "id": "eb7498a0-b23e-5079-b6a7-69ce3d120b27", + "version": "MTc1NjMyODAyMjE0OTg5MTAwMA", + "digest": "sha256:6bebfe91e2a10eeb42e78e43e64e87527a1f9692c7f1844fe9f21d9a029237ee" + }, + { + "id": "eb8291d3-3bdc-50bf-ad8e-443e6ccdf36c", + "version": "MTc1NjMyODAwOTA2NDMxODAwMA", + "digest": "sha256:726dd94bbf0836bbf174a5e170072e44f0efc6ef8defc4fcf5194177a7ea81d9" + }, + { + "id": "eba12d47-bf40-5b5b-9fce-f70140ac9d0a", + "version": "MTc1NjMyODA0NDY5NDQxMTAwMA", + "digest": "sha256:08c2024280caf99ec7a614d2bd477ad49e75b4e0c52ba244dd8ecbd5aac43c73" + }, + { + "id": "ebaa72ea-18f9-56a8-8158-deccf066c9d6", + "version": "MTc1NjMyNzk5OTM1MzgyMDAwMA", + "digest": "sha256:e2962731c15f97d0a0311079e04232edd61e939aa2ef7a4bbca94e72b69dfd6a" + }, + { + "id": "ebaff66f-11d8-5651-8bbc-bd44542a9043", + "version": "MTc1NjMyNzk5NTI3OTY4MTAwMA", + "digest": "sha256:802d2e17866acd8f4d8b7fac2f05e1552357d1044a3b85095c1241d69498b4af" + }, + { + "id": "ebc373a0-9c4b-5291-a08a-55a7c0dce54e", + "version": "MTc1NjMyODA0NTc4OTEwNzAwMA", + "digest": "sha256:d1dd222f521c1303372eda591f15bfbbf5ebdb11ce4e7866db094b51d92929a6" + }, + { + "id": "ebe6d7cb-7b26-571c-894b-1f49918a9549", + "version": "MTc1NjMyODEyNjU2NTE2MDAwMA", + "digest": "sha256:3404b3c785b5270d491344aa0d2744e40b6958c88a5996b507e1a826e8c5bfe4" + }, + { + "id": "ebf43ad3-2b7f-53f7-87c6-11f13434c008", + "version": "MTc1NjMyODE3MDQ0NzE0MzAwMA", + "digest": "sha256:37bc88b9b1a844f4735a6f6febce07952eee162e7df6385c0b4f423e7eee188b" + }, + { + "id": "ec2969b4-88ec-5891-aca1-b4dd36a099ba", + "version": "MTc1NjMyODAxMTU5MzM0MTAwMA", + "digest": "sha256:07265d739dcd5e3e871b5156bf2180351ba1e120c4b24f131d23a3731bd57b3f" + }, + { + "id": "ec3df02a-accd-580f-8720-08a59db64962", + "version": "MTc1NjMyODAxODgyNjcxNDAwMA", + "digest": "sha256:b5c08601eb17dfde29784f42c09e08ca8d67dd2888089175cbbbf8dc0e2d1d13" + }, + { + "id": "ec566d10-708e-5382-96fe-b89d0223496b", + "version": "MTc1NjMyODE2NjQ2NTkyMTAwMA", + "digest": "sha256:d49aeeef69f0557e765d8f805bd9f430e5cfd030756d57ffa9db4a6668d72cbc" + }, + { + "id": "ec638205-0cb1-5497-9b8f-e5cf3dacc62d", + "version": "MTc1NjMyODA5Njk3MTMyMzAwMA", + "digest": "sha256:6fa8898e25edc89c5af03b189eaab1ab2a4e1dfabd424a63e9ae9d2814b51f57" + }, + { + "id": "ec6c5660-da15-5dc2-9d61-f862589083f6", + "version": "MTc1NjMyODA2MjIxMDYyMDAwMA", + "digest": "sha256:67496b83f504d28c0a763f556d0558b37162c073d661663167e204e8661ddbf5" + }, + { + "id": "ec7ca1f0-3553-5bfb-b244-eec6079653b4", + "version": "MTc1NjMyODA1MjU2OTg2OTAwMA", + "digest": "sha256:559f69ca197b5c827b85fc05a95fb0d78509225ff6aa9c59e73619386a0c8059" + }, + { + "id": "ec86e064-82d0-52a4-b3b0-b499963ec87c", + "version": "MTc1NjMyODAxODgwNDg5NzAwMA", + "digest": "sha256:cb3d222cb2b86a5d7030d3880cd90cc2ee0d351e9049903b7416457c64e83a91" + }, + { + "id": "ec9b611d-df39-5c35-9aae-48982fdd4c29", + "version": "MTc1NjMyODA2MjE1MTc0NDAwMA", + "digest": "sha256:904ec720ddb17265610ca3f349873f8ddd3154121add3c8c0fa0c57e1cfae9a8" + }, + { + "id": "ecb40bc5-b57f-5179-9320-11d78e7a3b73", + "version": "MTc1NjMyODA5NzA1MDg1MzAwMA", + "digest": "sha256:e3e213c465ce2571051f041920201d80fb74a77d6e5a01853367ae06868ed279" + }, + { + "id": "ecb65fb9-b038-535e-aa42-ba1ac5a8865b", + "version": "MTc1NjMyODAwNjE1MTUyNTAwMA", + "digest": "sha256:a410636869bdbea358e6f1084ddeb532e8948d6e1c7ff6505932e384fe32800a" + }, + { + "id": "ece81a4a-a24b-5a68-823c-9ce3918d3712", + "version": "MTc1NjMyODAxMTY1MDU3NDAwMA", + "digest": "sha256:57ade4fbf78ae92045e44a38f66bf7a1c29a81ddb7e78a4e4c83054b62724927" + }, + { + "id": "ed0032f7-701d-588d-929a-0a4d66d87647", + "version": "MTc1NjMyODAxMTA5OTAwNDAwMA", + "digest": "sha256:f1aeec70524965200e738da7c9fc47b4925e147c0e1e558e79fdd7e8ce7f16b4" + }, + { + "id": "ed052d3a-cb37-5afe-a8d2-66e426a5e332", + "version": "MTc1NjMyODAxMTA1MzI3MTAwMA", + "digest": "sha256:18f54dd035427bbe85667a744c1fca2363347602cf7703115d34d61ec8bacd13" + }, + { + "id": "ed241852-8831-584b-a607-55769844dcbe", + "version": "MTc1NjMyNzk5NjY4NzU2MDAwMA", + "digest": "sha256:4cfd1d677957abd7b2fcb3f5c7c46d659a6362624de0484e0090bad4dcf551e6" + }, + { + "id": "ed294a1c-4fc4-5b28-85e6-2b64e19d3627", + "version": "MTc1NjMyODEyNDUwMDA4MjAwMA", + "digest": "sha256:de084f36e0bcc1b6cd6f5f48c5223bd975f7c4f21248820196e652b6cded5598" + }, + { + "id": "ed2ceda1-766a-5d3a-ab6c-4b05edaff9fc", + "version": "MTc1NjMyODA4OTI2MjQ5MzAwMA", + "digest": "sha256:32b0ab1197ea7b2ec4d4bf97b6522927ad898888715c99d6e402a15749830f22" + }, + { + "id": "ed41a4d4-a28e-5bd9-b1ab-fd438138b639", + "version": "MTc1NjMyODE2MzQzODMxMzAwMA", + "digest": "sha256:800caaccbac6cc15b49da6a001ec1964a3a01a96e3600986af8d57abf3b671ef" + }, + { + "id": "ed4e2f54-6571-5f2b-82c0-09301f4153ad", + "version": "MTc1NjMyODEzNzU1OTc4NjAwMA", + "digest": "sha256:b447ae81080ceb6189257a9435264fce0dd4f4b92cf5eefdafda9cd96f64de1a" + }, + { + "id": "ed663e96-3002-5950-b0e5-3813cecb4099", + "version": "MTc1NjMyODAwNTA5MzA3MjAwMA", + "digest": "sha256:45e724938aaca79fceb7d0222fa3afcc86670a6c7aa0c622f4bddd70b9582275" + }, + { + "id": "ed671c5b-aee1-57f9-ae4b-a25493991ec1", + "version": "MTc1NjMyODE2MzIxNjE5MjAwMA", + "digest": "sha256:416ce35a91c7924e8efae1617ecc6d8ae18321b3c6851d19604acd1a324d4cf1" + }, + { + "id": "ed6f8fbb-d487-5332-bdb8-554edf3d1b6c", + "version": "MTc1NjMyODE2Njg4NzMwMDAwMA", + "digest": "sha256:ff3ab4ec073fe6f5d90e1e4042999f2f43be436d0f1687ec125edbb01f128021" + }, + { + "id": "ed77095c-83d3-5efd-8646-17f14165aebf", + "version": "MTc1NjMyODAwNjE3NTg0OTAwMA", + "digest": "sha256:7cc8d2305846c8254244518be80d54809788a15510688010acb29bec49086495" + }, + { + "id": "ed7ff03e-bd34-5139-949d-da7f3e49f2af", + "version": "MTc1NjMyODA4Nzc1NTYwNDAwMA", + "digest": "sha256:79d2f6adad496a839a99a0415eacfae036baefb8f5127d1700e1cba31d9f5e04" + }, + { + "id": "ed80eaaf-1002-5340-ada1-504706bfad1c", + "version": "MTc1NjMyODE2MDUxMzI1NzAwMA", + "digest": "sha256:74d7b1668c25660592aac27c46c533d246316e0839d5b0c6a20956ad5efc9a25" + }, + { + "id": "edae8662-a6f2-5bbc-9b71-785b24792a30", + "version": "MTc1NjMyODE2Njc5NzgwNTAwMA", + "digest": "sha256:b2a4571e94cce19ebfd0b6befae401656476404ae7d882b2b57bd34769b2fe09" + }, + { + "id": "edcd18a0-2713-54ec-b22c-7a3d1a3848f9", + "version": "MTc1NjMyODEzMTA3OTI1MDAwMA", + "digest": "sha256:f0b809e32896dd9e1a7e196341abad3235532552f4556d9d4d8529937399c401" + }, + { + "id": "edf1eed1-6a97-59e7-8c5b-49647787cdf6", + "version": "MTc1NjMyODA5ODYzNjIyODAwMA", + "digest": "sha256:38b8af13a37f420775a5e778e78b7942f0fa28932f540250430a668eac0db5cd" + }, + { + "id": "edfef904-cbad-5a69-8255-9284f51863bd", + "version": "MTc1NjMyODAxMTg2ODY1ODAwMA", + "digest": "sha256:6f1727b3249e3e87892c36092076a88e3be8da891f3499d6559879b103f5aa92" + }, + { + "id": "ee077921-5992-5de7-9f70-0dd72479e6f7", + "version": "MTc1NjMyODEzOTMwMzQxNTAwMA", + "digest": "sha256:bfb7b4d5d705ea7864653c212ecdd41459533a357ee41e200624b597e66371e7" + }, + { + "id": "ee24f6b0-a5da-5272-bd3b-d5dcdc08f69d", + "version": "MTc1NjMyODAyMzM1Njk3NjAwMA", + "digest": "sha256:5590be0f639939ab185cd870d7643e1c7458c22fa8fccb0ca5c772f0647bf8c9" + }, + { + "id": "ee27daf6-d866-5186-bc6a-1f1da3e8ceab", + "version": "MTc1NjMyODE2NjgwMzgwMDAwMA", + "digest": "sha256:c5c3d51925b691996fe4b574b8e712deaa96a7871cf7464789940e30054eb165" + }, + { + "id": "ee3203e7-4b3d-556d-b7d6-8d391007a1b5", + "version": "MTc1NjMyODA3NDMxMjM2MjAwMA", + "digest": "sha256:be5ebf0e268198ddeba1d1ca9c20369b204d4cca63ba9f613b9117be9f286eba" + }, + { + "id": "ee68c33b-1cc7-5692-9eaa-46bd072aaf8f", + "version": "MTc1NjMyODAxNTI3Mjc5NTAwMA", + "digest": "sha256:4615024d0f3e85628b6bfd067cb44308a944658d9dcf5b6b95bcdf5c7a220de9" + }, + { + "id": "ee94ec34-3c9f-5bb0-9866-129ccf78f667", + "version": "MTc1NjMyODE0MTYwOTQ5MDAwMA", + "digest": "sha256:e495ddc813ed0fb60bbb67d08a18686490dfcff59a1e146ad5408614478f98d2" + }, + { + "id": "ee97d1ec-c220-5a6b-ae35-cd182e6d1744", + "version": "MTc1NjMyODA4ODU1NDk2NDAwMA", + "digest": "sha256:ed56b254df18cea18de77ba708372603682871d6bd50aad7de39deeba65aae70" + }, + { + "id": "eecea72a-2dc5-58ff-8ed9-15a786c92294", + "version": "MTc1NjMyODA2MjA5NDc2ODAwMA", + "digest": "sha256:642acc9a9f1011f5e51dd3b007b40e4766da3070b2bc7aabbd74addcd026d73d" + }, + { + "id": "eefc5483-9737-5543-87e6-6efa62e76a6c", + "version": "MTc1NjMyODA2MjY0NTIyNjAwMA", + "digest": "sha256:69b5235fc365cccf029cfec9f6bfce2ea39e93135cc4552f62b45f7d12dbd870" + }, + { + "id": "ef31754d-7c58-542b-983a-fd6dce8feb25", + "version": "MTc1NjMyODExOTU4MDA4OTAwMA", + "digest": "sha256:9dc4d58997273671617986b22dbf206bc03b618fd426a83f43c78d2092945364" + }, + { + "id": "ef34a728-338b-536a-ba58-1569a81ccc13", + "version": "MTc1NjMyODAwOTA3Mjk1MjAwMA", + "digest": "sha256:bc9acd641c07be39f112ae8d2a5de9b55eb3cd0cc7a97f302d23bb07546d1b80" + }, + { + "id": "ef383c94-9755-5954-9270-5e760cef4632", + "version": "MTc1NjMyNzk5MDM1MjU4MjAwMA", + "digest": "sha256:58f191eb6c07539563b24232153d56b9563c4849929744775b26b5d3878879e9" + }, + { + "id": "ef3906d7-9bd2-5ef7-bc89-58de59d6a425", + "version": "MTc1NjMyODAzNzExMzk0ODAwMA", + "digest": "sha256:ea8b0729a7179319111e110aef359ae05f09ea521782e906f36fc493f0d9acfd" + }, + { + "id": "ef56ce27-515a-56c7-827a-c51bb10cad7b", + "version": "MTc1NjMyODA1MjQ2MjkzOTAwMA", + "digest": "sha256:e82699d0b6e3fd8782e9239fe6413f1682866c6d97917c7c9d50b2e6fe910f61" + }, + { + "id": "ef715c78-0f7b-5a11-af9a-62bb66eae216", + "version": "MTc1NjMyODAzNDc1ODkzNzAwMA", + "digest": "sha256:2ca8687d0b07d3c3f82efdd2eab2322ebff59a6a3b82fb8b2e7f206fae27370d" + }, + { + "id": "ef860eb8-38ac-51d4-87ee-5a8dca25f7dc", + "version": "MTc1NjMyODAxOTMyNDIxMzAwMA", + "digest": "sha256:a11e11d5446f98a22faa91db808f19b9e8e8af3114516bb60ec360079a5ba356" + }, + { + "id": "ef8eb529-6172-5d1d-aafa-11f665a0d8c0", + "version": "MTc1NjMyODE0NzQ4MjEwMTAwMA", + "digest": "sha256:bf066d4f32edba76caf9b145f2ba137816c25701fbde56056fb6bd7cd9a6f821" + }, + { + "id": "ef92c215-00de-5f30-9d96-b647bd8990b3", + "version": "MTc1NjMyODAyNjc4MDA1NzAwMA", + "digest": "sha256:a231ffaf20a588193861776a8e7e3450aee16761c583ac4cea67c642c89d5ee6" + }, + { + "id": "ef9ce834-324d-577b-9ec6-3876d4278102", + "version": "MTc1NjMyODA3MjU3Mjc2NjAwMA", + "digest": "sha256:a936ca80e6152c0ab1ede1fb79f9eb13f37865d25e57a4403bb2988dd1fa69ad" + }, + { + "id": "efa3571d-3010-5316-bc2d-d4699b2ffcc6", + "version": "MTc1NjMyODEzNjk4MjEzMTAwMA", + "digest": "sha256:683f35437b0c7fb80b344fcbb04d12df0cd9d1bf790ff4fef7084d8bdfd0921b" + }, + { + "id": "efa70274-d218-5042-b420-0ebe26d57f95", + "version": "MTc1NjMyODE1NDY2MzA5MTAwMA", + "digest": "sha256:7fb8777372a98ffdd3d8807b740ea869bb485e12f6bd95b2e2dd565b57b2c849" + }, + { + "id": "efc269d2-4bb5-5fba-bbd0-9241b36b83d9", + "version": "MTc1NjMyODAyNzc0ODUzNDAwMA", + "digest": "sha256:4bd49d844d18bee97693aadd58059553c5d67882b18d751bc6b7869b23c936ef" + }, + { + "id": "efc97cc5-98ea-5728-904f-ba8755a19580", + "version": "MTc1NjMyODA3MzIzNTU0NTAwMA", + "digest": "sha256:c5b5ac6e9ed534fd415ab4e4c0325d98fe5e2bada1a09b2426a032d58921186f" + }, + { + "id": "efd53cef-a72d-5102-95a0-baaccd71276d", + "version": "MTc1NjMyODAwOTAzMTE4OTAwMA", + "digest": "sha256:e619ce335732b72b630999649c4c75ee83378bfb3ee93977218e9e73670539ea" + }, + { + "id": "efe7123d-3340-5fdd-af32-65674580f511", + "version": "MTc1NjMyODA4NzY3NjI1MTAwMA", + "digest": "sha256:aef38562b6c3c2a075f6c1948bd50f00ae2099d46d9ba010948757fcaa7c6765" + }, + { + "id": "eff1d38a-4ce4-5e2b-84b1-1e3b36f8c8dc", + "version": "MTc1NjMyODAxNTMwOTkzMTAwMA", + "digest": "sha256:9b42b516d3c4b1dd7d44a1f12f8a9196077f61c8db323c4538fd8340bf013609" + }, + { + "id": "effe45fe-b8c8-58c3-875e-16f15af42a95", + "version": "MTc1NjMyODAwNjIyNDkxMzAwMA", + "digest": "sha256:3965a57082bb6624fc6acdcdd54651d3b05fe3a45a4ae3c6969fd19ac47d2812" + }, + { + "id": "f0198631-46d7-502f-9068-49aeaea09463", + "version": "MTc1NjMyODAxMTY0MTUyNDAwMA", + "digest": "sha256:9f18a6b10fec983583bc08576f4751d2f35a1670b76acf165cf84dc3e4e45343" + }, + { + "id": "f021be4c-0204-5898-bc9a-1243a5473dd8", + "version": "MTc1NjMyODEzNzUzMTI4MzAwMA", + "digest": "sha256:f7e02cec6f6aec5fbd903051060d29e7b90383d4feb1fed173ae02e52f7c16a3" + }, + { + "id": "f02615e3-86c3-59ac-a7a7-c1580bd2fce0", + "version": "MTc1NjMyODEzNzM4MjU5NzAwMA", + "digest": "sha256:bb01f250f167ca147062b1b793dd77701773315dd4a1dadc1873b885e63fb065" + }, + { + "id": "f02dfc60-5792-52c3-8346-d64ca5975ab3", + "version": "MTc1NjMyODAxMTAzNTM5NjAwMA", + "digest": "sha256:dd795f2477383116f13b71c02c9bb007d6dc2271b8ce5e6b01fdc59d50cb7c35" + }, + { + "id": "f0411c9b-6df8-5d97-aa3e-7483f38d4c2a", + "version": "MTc1NjMyNzk5NzE0OTI4NDAwMA", + "digest": "sha256:0855d5f9db00499639fca12c6315470aabf98daa77e9623586d5ac6c5eb82b1a" + }, + { + "id": "f04e3494-4197-564e-95fe-5686dca5cfd5", + "version": "MTc1NjMyODAzMDc4NjE4NTAwMA", + "digest": "sha256:4092f9ae11fe6046c26adff47f06992dd52b3ca78e9583d4a33522b521957272" + }, + { + "id": "f05fbfca-e039-57c5-aaf7-5db3e8255974", + "version": "MTc1NjMyODAzNTk2NTM0OTAwMA", + "digest": "sha256:77d175ba551bbdf234a3fe779e5adca18f8d8e79a80ce9ed7114febbfb6c616b" + }, + { + "id": "f0767bb4-2cb8-505d-8bc5-5f9fe46bd574", + "version": "MTc1NjMyODEwODg4OTIwMTAwMA", + "digest": "sha256:362e60860d12f8acd01a3609d2cf2e2e505583c6333d96b0268835c117074514" + }, + { + "id": "f0bcebb6-b2e3-522f-8574-e5d0933354aa", + "version": "MTc1NjMyODAxMzgxODcyNDAwMA", + "digest": "sha256:ef8d2ddb591905cd07d4f8e99aec6d892878b5bae033ea8aa8088c184cdde9c2" + }, + { + "id": "f0befedd-97a3-5b1b-8734-38bbe3eab041", + "version": "MTc1NjMyODAyMzQzMjkyOTAwMA", + "digest": "sha256:c7e7e458c58ddbdea6eb2a10bd0bd8efd768c3c25b2c399851f0d29c02c6bd3e" + }, + { + "id": "f0fd357a-dd05-59c0-b659-92ced5e231b2", + "version": "MTc1NjMyODA3MzA0MzQyNjAwMA", + "digest": "sha256:d7240051b8fe0c112756e37d8c19318300055820f1f67bf4206a800c458be9ac" + }, + { + "id": "f106c9ab-a16f-5b9f-8a04-c940435feac1", + "version": "MTc1NjMyODA1NTgxNzIxNjAwMA", + "digest": "sha256:c76e8c51344f22fe1dbae5ccc8a7f52c583bd67907fa218e35593db86ee8d066" + }, + { + "id": "f10e2533-4de7-5450-a927-90e008dce619", + "version": "MTc1NjMyODA0MjQ0NjUzMjAwMA", + "digest": "sha256:05014c8cbe7d49ee665001d23676b1fe39e85dfab5cfb190a20c3592ea0ffcf2" + }, + { + "id": "f1442c37-e910-537b-937f-101524d40fb2", + "version": "MTc1NjMyNzk4ODYzNjIwMjAwMA", + "digest": "sha256:c37eef1a1fc4224f14bddb9d3519adb6cad2121b8c1d8e8bfe446f6cc8a70ac6" + }, + { + "id": "f151d070-efb9-5eee-b804-9c72c41ad57f", + "version": "MTc1NjMyODEwMTE2NzkwMTAwMA", + "digest": "sha256:8a2025b460446bbaf6e0e0e80d3b3bb132b539c7e06b962d28a7fab8166d5b64" + }, + { + "id": "f15b4f6d-bd29-5487-a287-e26af4d67a51", + "version": "MTc1NjMyNzk5OTMyMjYyODAwMA", + "digest": "sha256:eeb3a4ae886cd723e8fee0827188856fc747201957fdd44d13273c1943e99090" + }, + { + "id": "f17e6c93-f130-543e-950c-411b16b102e7", + "version": "MTc1NjMyODA2MjcxNzk3NzAwMA", + "digest": "sha256:23a3b88e55d9727d5ac98c7f3955e74ef32bda77b0f7981ff4bc87c85828cde1" + }, + { + "id": "f193bdf0-caad-5df9-8143-0e936cfa3c21", + "version": "MTc1NjMyODAxMzgyMDc4NjAwMA", + "digest": "sha256:5ab79ada5cb792aaf452ed5d3b9779146fe554aed747c120432454b918288e6d" + }, + { + "id": "f1953b4f-6693-5d56-9f66-a748443d9a4c", + "version": "MTc1NjMyODAyMDUzNzMxOTAwMA", + "digest": "sha256:9580ac2157f92bf9b120a3cc34c8730d3b43a8f4c22a493b9ddd788edd7de622" + }, + { + "id": "f196150a-753f-5510-9563-b94825541a6a", + "version": "MTc1NjMyODA4ODUxMDMwMTAwMA", + "digest": "sha256:1bddcea955fc1132e4188cbb3bf180ad6c84c82d5ead5b3ab03525b13b532b0e" + }, + { + "id": "f1bd7501-cdf9-51bf-991d-7aeee0de08ed", + "version": "MTc1NjMyODAyNTU3ODAxOTAwMA", + "digest": "sha256:d2bc399894d40d0e16013e48b737f6686bc8f21b5205755806d37b2848bf56fe" + }, + { + "id": "f1d33218-2c05-51c0-948f-0a09653da97f", + "version": "MTc1NjMyODE2MjIyMzI1MTAwMA", + "digest": "sha256:751a35d30ddac37b1abc3c54bcd98678fcfe574404150239d37c460343332539" + }, + { + "id": "f22225e5-4b93-52c0-8b0a-d6814769e2bb", + "version": "MTc1NjMyODE0NzgzMjIxOTAwMA", + "digest": "sha256:5372752332f3c61d0a88341222b00b48c6393414388ca893108745b8c5648bae" + }, + { + "id": "f23b843d-f88e-56d1-ae25-9100dfb18459", + "version": "MTc1NjMyODExOTYxMDg0ODAwMA", + "digest": "sha256:6481346314a0c0bc4537e6ef97ea4301b422acf9189d7dfc8749916bcbebed3f" + }, + { + "id": "f2faab58-f358-547c-b5b0-4313432b5ffc", + "version": "MTc1NjMyODAxMDQ5NzM0MTAwMA", + "digest": "sha256:322d34108039ccb36eb418da0d2380309207445a79a76daf4ab213fce156fba3" + }, + { + "id": "f3039edf-5fc9-5f16-8466-6096100124b0", + "version": "MTc1NjMyODA2MjYwMjcyMDAwMA", + "digest": "sha256:2a2221c5076374fb2be24d77917057ae7a39417dbd0ac282fc253928b0dda223" + }, + { + "id": "f31dac90-d4dc-5589-b41d-b0007ef32200", + "version": "MTc1NjMyODA2NzE1MTIzNDAwMA", + "digest": "sha256:49cf6c33353974f0ae5af4c6e0d610940c1344f0526cb5543c6832e655745060" + }, + { + "id": "f330ee9a-8074-5a90-8ca3-f38a4e7b3c4b", + "version": "MTc1NjMyODAwNTA4MTIxOTAwMA", + "digest": "sha256:ba7817e823842e469b944ad96321b9b544768ad8c08535c9b046e835b26f5d64" + }, + { + "id": "f33785ba-ce77-5a81-b743-279a306cf2e8", + "version": "MTc1NjMyODAyNzgxOTY5MDAwMA", + "digest": "sha256:40224e715604225e1faf2eb648462fcbdd61ac0b2889bc32eb80f3b90304f93c" + }, + { + "id": "f33e2a5f-92ff-5fbd-9f81-30ce8d672dbd", + "version": "MTc1NjMyODAxNTUwMjg5MzAwMA", + "digest": "sha256:61b6170a7a712e9f34826ec9cba2048d848b547460b05f46de368c17751cf9c5" + }, + { + "id": "f3471e6f-6608-57ae-85b0-69427155bd04", + "version": "MTc1NjMyODExMzUxOTc3MDAwMA", + "digest": "sha256:3d08497cb4827e2664088ab672f42404dc4abd575fc45f079eb7e88e5c71929f" + }, + { + "id": "f38328f2-44dd-58d4-a357-0abeba214894", + "version": "MTc1NjMyODAyNzgwMTUyNTAwMA", + "digest": "sha256:2430ce3e3e34edeaee8ca97eba9b69d9d9d296ddcdaa3284eed7b508db0a2978" + }, + { + "id": "f39d878e-aa56-599d-9d45-719989a7e0aa", + "version": "MTc1NjMyODEyNjQ4NTU2MTAwMA", + "digest": "sha256:d2027edc0e0875ab331dd64ddedce7d7874fb1df7007824cf1316ea58b9d11ab" + }, + { + "id": "f3a28573-3f61-5831-985f-051a966b2e9f", + "version": "MTc1NjMyODEwOTE1MjcwOTAwMA", + "digest": "sha256:1622f1494cf263f01e1b49baece669097fe0b64458337bcac533996157834529" + }, + { + "id": "f3a80e1c-f2b9-5c25-ab72-c76e33578b87", + "version": "MTc1NjMyODExOTYzNjI2MDAwMA", + "digest": "sha256:5920fcea9aa42144d6d7458d23b368435d1aa1af76c65439a44ea5c00fc891e2" + }, + { + "id": "f3dbe6ca-8109-5a70-be49-0f149c691926", + "version": "MTc1NjMyODA0NDg0MTIyMDAwMA", + "digest": "sha256:40310f1bb7ec8ddc0fd0ac8a31787e2d60b8a3a860fca463cb983af65a4ac2ee" + }, + { + "id": "f412f0f3-e80b-5276-98a2-c127a5f66450", + "version": "MTc1NjMyODAwNjE5NzQ3MDAwMA", + "digest": "sha256:1817b450f259599bc7ae70a0ae2635141ed2925ba13249892832643001aa00c7" + }, + { + "id": "f41e364f-3c5e-5f64-8210-577d00e354d2", + "version": "MTc1NjMyODEzNjAyMDk4MzAwMA", + "digest": "sha256:1a149fdf294c97d8a4dfaf5e890393065674bfdfac0ff05d81ff1c05e65c10d0" + }, + { + "id": "f42d3aa7-1709-5732-8bd5-734457918a95", + "version": "MTc1NjMyODEzODE4OTA0NDAwMA", + "digest": "sha256:1af477b68f2527fc26befa3b774b949f2d9dae0f08f21d054675be60c72d7ab7" + }, + { + "id": "f44b8bb6-43bf-5b5d-87a6-55ed776aad42", + "version": "MTc1NjMyODA2MjUyMjk3MjAwMA", + "digest": "sha256:2ef1183520ad6d1d065c479f2c979d7d5dc8eb9fd8e7c262d2f04d88f2c83cc3" + }, + { + "id": "f47550f7-1f5c-575f-add4-3cb1c9ac0603", + "version": "MTc1NjMyODAzMDc4NjMwMjAwMA", + "digest": "sha256:be4a57c5750677e3496c660e966dd3bb58d7ed2111a7b30bd8f38eec31706641" + }, + { + "id": "f480ec5f-e75b-5655-be35-9cde28a275ad", + "version": "MTc1NjMyODAzNTkzNzkwNTAwMA", + "digest": "sha256:f55304e2a7089216d9218c8131146a46f5f0aa44cd1ad8817f2670a085cf8399" + }, + { + "id": "f4935d12-4919-5e31-b954-a99bdb82d9e2", + "version": "MTc1NjMyODE1OTA0NDEyMDAwMA", + "digest": "sha256:1429dc49af58c47dc2f3d4cf6dc62b17e1e28920b4f8b5047db30c5f0f8cc0d9" + }, + { + "id": "f4a5c0c8-e900-5237-a346-dc93f9c6b650", + "version": "MTc1NjMyODE2ODg0NDUxNDAwMA", + "digest": "sha256:749d5008dd36f2e5b8876508d1068c7d7d19ef5d39de78fe71d9130071866c47" + }, + { + "id": "f4c640b7-a49a-5ba4-8207-22d72e8da246", + "version": "MTc1NjMyODAwOTAxMDE3MDAwMA", + "digest": "sha256:3047406e06cbc2d7a2d4e595dcaf485ff5bf1a69745ed7aec6a11ad3635dc7ea" + }, + { + "id": "f4ca37a5-8813-5d0e-9538-43c802e5d09f", + "version": "MTc1NjMyODAxOTQxNTI2ODAwMA", + "digest": "sha256:a09d505f5ff846d197273c6eb5673f2ed9441dd350ed9f4ebc6d69ad770bc354" + }, + { + "id": "f4e021e6-6e85-599b-9e4d-a28063166c93", + "version": "MTc1NjMyODAyNTg5MjAwMDAwMA", + "digest": "sha256:078074657b1768eee168722a486d27989b21a773ddd5a13c77e2374ded4a9d46" + }, + { + "id": "f4f6bd05-ce85-516f-b5e7-1ec202bf23dc", + "version": "MTc1NjMyNzk5NjczMjE5MDAwMA", + "digest": "sha256:a9acf7e683f34f660898aa13e670779c0841f7580b5703e8f1ee96119e497d10" + }, + { + "id": "f4febd42-8e3e-54dc-b650-94668ccffe28", + "version": "MTc1NjMyODAxODgxOTE0MjAwMA", + "digest": "sha256:be4c2f626ad2b9d505a27a6e72f76c9c9cc601164389bbae654ed26d001203b7" + }, + { + "id": "f5027a66-5ff5-5e16-a3e8-f9137299a395", + "version": "MTc1NjMyODE0NzUxNjM1NDAwMA", + "digest": "sha256:cef103684858917d88bfb3b1a22068e02e173c0937663888d8cc81430f3d0e4f" + }, + { + "id": "f5129058-db67-5240-84f2-7cf0a8b56de0", + "version": "MTc1NjMyODE2MjI4NDk4NjAwMA", + "digest": "sha256:524f4c55002ab8ee7f3d8724285cfde4fbfa64f0054b9cfc76c5bec64d1aaf99" + }, + { + "id": "f517dcdc-b530-5871-820c-cf66f90fc930", + "version": "MTc1NjMyODA5NzAxNDAxNjAwMA", + "digest": "sha256:d38c9420bfe94dbd56e0b7cf6b52aebb98f6f7b3c5eec4f08c39013b1090b4b4" + }, + { + "id": "f53691b0-f5c0-5df4-a932-4cea33db01a5", + "version": "MTc1NjMyODAyNTc2NjY5NDAwMA", + "digest": "sha256:7c636ac288cdd50c94abfd47ea5364dc5160d7834b174a897c4b66f2d0a6820d" + }, + { + "id": "f53a8467-a22c-5f70-8d35-c6ed45486442", + "version": "MTc1NjMyODAwOTA1NDY0ODAwMA", + "digest": "sha256:f9b31e6f7a478d75886a0e827d95c7a536b4cef893caea3ed18756b5e655fe48" + }, + { + "id": "f53d3207-f9c1-53bf-880a-532f7906c1a6", + "version": "MTc1NjMyODEzMTEwODc5MDAwMA", + "digest": "sha256:7c770656c990f3e2c69e62dab233468f65637d54252e670885e504b5d8508a84" + }, + { + "id": "f53f0d82-1461-52bc-8e5b-61ac762d3083", + "version": "MTc1NjMyODA5NzAyMDczNjAwMA", + "digest": "sha256:92ab6c28e774a17354b1e3610a25b32e1b0ff75e2fa8629f5447d31e5aa920d2" + }, + { + "id": "f55e04b6-bd20-571b-bfc8-0e8d493b2deb", + "version": "MTc1NjMyODA4OTE2MTIxOTAwMA", + "digest": "sha256:707ebcb6352bac942dd61bacf6d554f701c59b1e47de96aa4569fba757d95748" + }, + { + "id": "f563d3d5-9304-5745-8fb5-a7fbd5aaffc8", + "version": "MTc1NjMyODExMzUwNzU1NjAwMA", + "digest": "sha256:1e20cb242992c5497628c199995514c2964635bd919884fad337ac9c3e74ecd3" + }, + { + "id": "f57503f1-771b-5e55-8878-dbafcddf54e4", + "version": "MTc1NjMyODA1MTMwNTY3NjAwMA", + "digest": "sha256:38919cf2d4565eeedea880f66ef33487cd0a8c5fdc99f0a33ec71018bc52894c" + }, + { + "id": "f57510b4-7375-5465-99ae-46e65ac81ac1", + "version": "MTc1NjMyODE2MjM0MzU1NzAwMA", + "digest": "sha256:3637242fe44445e8bbceeccaab09ee282c5ee54204598d3a5c3abf39d73b1db2" + }, + { + "id": "f58b7040-e9ff-5f83-aabe-d62d1360445f", + "version": "MTc1NjMyODExOTY3NzI4MjAwMA", + "digest": "sha256:d4ab3629c77b1a6da6677994a5ebc67e0c9d2efc6da7828b15c8f9ccb4c2bc3c" + }, + { + "id": "f58e38de-01c4-5840-acd5-5cab594654d1", + "version": "MTc1NjMyODAzNTk2MTc1NDAwMA", + "digest": "sha256:ede2521885db8deea82f85cb9f99e6fed89cddef3d00dd8edbe92f21ed2d11f3" + }, + { + "id": "f59a4e8c-7943-557b-a215-ef5339eab062", + "version": "MTc1NjMyODEwOTI1NTg1ODAwMA", + "digest": "sha256:83bb16d622d11c5c3ce90c45f97174f6a660f6421ec7b26fa6ef48ee138d4005" + }, + { + "id": "f5a3ffa3-d5ee-5665-b8d2-266aa559ec0f", + "version": "MTc1NjMyNzk5OTM1MjQyMzAwMA", + "digest": "sha256:1111fc5cbdcc49166a194a3fca98ec017ccdb95b54e5a38d1c0b806245074b10" + }, + { + "id": "f5b5706b-648c-5be2-9ede-47ed6a85f72b", + "version": "MTc1NjMyODEzOTMxNTgwMjAwMA", + "digest": "sha256:c391b2ee749548b99a0b00cab9fc9417fce61cc6d3dae9288c9f064541cb27fe" + }, + { + "id": "f5bf5f50-dfbd-5790-84a6-b09b3a0b4026", + "version": "MTc1NjMyODE1NDYyOTA0MDAwMA", + "digest": "sha256:c29047f7e5911dd89080ca788ba7c6fe28c16ea1c926005842ec720986b8195d" + }, + { + "id": "f5c3929e-9076-5e55-bc25-0fa9af779798", + "version": "MTc1NjMyODE3MDkyOTI2NzAwMA", + "digest": "sha256:a794aade628312ab03ff84debe894ff7a89e791473ad96a748596ae8a9e2904a" + }, + { + "id": "f5c4012c-b544-52c3-8827-7f1437289e58", + "version": "MTc1NjMyODAzMDgxMzgyMTAwMA", + "digest": "sha256:b75ee2a54083250706faf5107236fd187996ab4149568483043d9dfaf3a0677f" + }, + { + "id": "f5d35644-f40b-5e7c-930b-0691b95f78b2", + "version": "MTc1NjMyODAyNzM5MjczMTAwMA", + "digest": "sha256:420e33ab1c816de8ae83b08e7fd7f712d372dd9d8286b9d1f1ac0cfcafb4b79e" + }, + { + "id": "f5d637e5-76cd-5f8c-8f15-92f826a7144a", + "version": "MTc1NjMyODEyMjIzNzgxNzAwMA", + "digest": "sha256:1e26d1d2fc1706a1a1bc19148e7f9dcff2726a8ef70201b75a36a27c2766cd44" + }, + { + "id": "f5f40dad-f4f0-5ed1-b77c-e52d17e9e084", + "version": "MTc1NjMyODA0MTk1NjYwNDAwMA", + "digest": "sha256:d4e866f4e8d00e9456cf68a732ca9ef995933cfcede28fe04c97dc2b2093e47b" + }, + { + "id": "f5fc6b8d-b584-56c3-9f9f-a7f96bbdd017", + "version": "MTc1NjMyODAwNjE1MTg4NjAwMA", + "digest": "sha256:412ca549a67555fee4ad6a32809bc4d072ef0d5694eb2cb33a74935f45acf813" + }, + { + "id": "f6019d18-0c4d-51a5-a3dd-fcea8f672700", + "version": "MTc1NjMyODA0MjQ2NTc2MDAwMA", + "digest": "sha256:d2a2d5b7e2d7f021b069b08a704f37764aa6e3bcc0223e2724c577dcdc3ef377" + }, + { + "id": "f61867ef-4253-5c6f-9913-ac818befd128", + "version": "MTc1NjMyODAyNTc2NzI1MjAwMA", + "digest": "sha256:6cc710093983cdcb640fe5ea77017227bd1d9c4080982b6006c0806314f88c4a" + }, + { + "id": "f61a78b7-ead3-583d-919b-57695408b0e8", + "version": "MTc1NjMyODE3MjUxNzIyODAwMA", + "digest": "sha256:e512aed69c59ab74be6c2cb18b34c96205c8de70e21e3f5c409fa5d0aa3c8db7" + }, + { + "id": "f6321d33-b198-5ff4-b2e6-70d24b3bc011", + "version": "MTc1NjMyODE2ODg5NjIyNTAwMA", + "digest": "sha256:5c7e65f291d649ffda1f538bf8105eefcf52d1d025fe8ad914868d7a528425a3" + }, + { + "id": "f63ece32-679b-53f8-ac29-5ad5edf3b81f", + "version": "MTc1NjMyODAxMDQ4NjI2MDAwMA", + "digest": "sha256:02cc8888abc3166d4f5e88329c708292d3c5c77a88db2051516ea795c0ccf7e8" + }, + { + "id": "f648c351-1924-5c54-90b0-b21d86e6d3ab", + "version": "MTc1NjMyODAzNzE0NDk0MDAwMA", + "digest": "sha256:e1db027500f9bbfb1d3fc1ca42f370b33c6caad20c347682ddf67383fb06eeed" + }, + { + "id": "f651becf-ae06-5e5d-97e5-4fa4eeaf7dff", + "version": "MTc1NjMyODAxMTEzODMxODAwMA", + "digest": "sha256:aa021b62da5d3cc71a79d50d6ce7dcac183584afd42fd5229cc50705b4cca3c2" + }, + { + "id": "f6963410-3314-5922-ad6b-12994ea6ff40", + "version": "MTc1NjMyODA4ODU1OTU5NzAwMA", + "digest": "sha256:f96348b98ff263c10a9c65ddee062508a0bc82f960410901c893a94c4056e1e1" + }, + { + "id": "f6b01a55-c7f0-5043-9bb7-9b145a6afe91", + "version": "MTc1NjMyODE3MDg0MTMxNzAwMA", + "digest": "sha256:50f7df864922b6771073545a204c18d34d602b99aa0ef9f8df20811e27430f61" + }, + { + "id": "f6e26bc9-d7b0-553f-8492-f09adbf73240", + "version": "MTc1NjMyODEzOTIxNzM4ODAwMA", + "digest": "sha256:72782cdc4aca56bdd983ddb46eacbba4b2b10eb20d23fc4e3d1aad8a102b4613" + }, + { + "id": "f6e407a5-c3e3-5451-8a97-ed285bbd837d", + "version": "MTc1NjMyNzk4ODczNzY0ODAwMA", + "digest": "sha256:150b47d6551cf07ab4bfdea2f96a6409aaa3b20cb6b36bc06e7f07ce8523e1ee" + }, + { + "id": "f6ed4f5e-66c8-50e0-9cc7-379123c3f1e0", + "version": "MTc1NjMyODEwMTE0NjM0MDAwMA", + "digest": "sha256:1eb63bd166fa75ea73662a1c9c979d07c706f47be002917ab193be3d09d191fa" + }, + { + "id": "f6f3eed4-38a4-5a71-b42d-0cfa81b613eb", + "version": "MTc1NjMyODA2NzA3MDI5MTAwMA", + "digest": "sha256:954d91191c892d14955b0874a710802a29eb77add8b698af10f782f0a28c642a" + }, + { + "id": "f708b088-f1fa-55ce-b144-6af725fcd427", + "version": "MTc1NjMyODE2ODg4NDAwNjAwMA", + "digest": "sha256:77c875a051f619e71be6aabe8260208bd533fcc334e56a06df89f07b4067f618" + }, + { + "id": "f70b3d84-424a-59ce-bf71-3766a32bf672", + "version": "MTc1NjMyODE3NDgzMjg1NjAwMA", + "digest": "sha256:f6730cbfad58a418e5beac4fcb69e390de5e267e584a702bf48eac844725226d" + }, + { + "id": "f726f046-9aa5-56ad-a7b0-7a01f850639b", + "version": "MTc1NjMyODEwNTQyODMzNjAwMA", + "digest": "sha256:c9c664d102967ecd68150f30d0e8beb89793ce1898d5ff2817b41b64cccfb4c3" + }, + { + "id": "f750ceb9-cf63-506f-b3de-bd9a062ddd6b", + "version": "MTc1NjMyODAzODgxNTQ3MjAwMA", + "digest": "sha256:1c784bba804d9713e945ac6d7ce818f2cef5a01d91c92948bf32c878723012d4" + }, + { + "id": "f75f3f98-be32-59ba-973f-021552f976cf", + "version": "MTc1NjMyODE0MTU5MTEwNDAwMA", + "digest": "sha256:8d93991abe40eff4195def17dcc66ef4371bfb96c3e7ac4bfb41b3acecf0dc18" + }, + { + "id": "f762ef5d-abe0-586d-8b64-8edadf613d47", + "version": "MTc1NjMyODE1NDY1MTE0MTAwMA", + "digest": "sha256:0c7a443af43639008f7ce3cf92fdf14d4dfc84c8d08b0117dcbdb7cd21146b9a" + }, + { + "id": "f778d74d-86ea-553b-8c9d-b611a894d2e9", + "version": "MTc1NjMyNzk4ODY3ODY4NjAwMA", + "digest": "sha256:9245b4b496e84a78fa34c9622709b39ece72acfd8e6df1e7b2d2d356d9de20a3" + }, + { + "id": "f781c2d8-f4fc-5490-bfad-5f7155815f2b", + "version": "MTc1NjMyODAxOTQyNTY0MDAwMA", + "digest": "sha256:c1f657a1cbfaa7c72534b29e12258a3f2f87947dc96173b8b2d850670738b486" + }, + { + "id": "f7bf40df-60eb-5dde-bb5b-a3bce253499c", + "version": "MTc1NjMyODAxMzg3MjU5NDAwMA", + "digest": "sha256:209c34a24073bf6f66242a38d7a0a40ce63ced3ea67ae00d870e630dd96f2a55" + }, + { + "id": "f7ed0f14-319b-5b3d-a276-1ebab89a7e60", + "version": "MTc1NjMyODAwMzk4NDM2ODAwMA", + "digest": "sha256:2b93d07f9bbe5e7efadd958bf6e4d7ae417547e9369cf4c1ac17bd24bbe4710f" + }, + { + "id": "f7f802ec-4d59-5602-be5d-1c8fb7c8338b", + "version": "MTc1NjMyODEyMDQ1MTQ5NDAwMA", + "digest": "sha256:f25365d3b206e59198c1d6055c22786e7a03d30345e69378a7410185c3840445" + }, + { + "id": "f7fff740-8d64-5eda-b727-e51a1955b89d", + "version": "MTc1NjMyODAzODczMTQyMDAwMA", + "digest": "sha256:fbdf27de691c94ef8db46e9d995ef325bec78ff9e0051bc7f251624e2b3c30dc" + }, + { + "id": "f80f9781-93bb-548d-8a6b-9d759587c831", + "version": "MTc1NjMyODAxNTI2NTYzMzAwMA", + "digest": "sha256:2dc0852c28ecb8b0710bb731b1e917cd2ba93bc9d1b04586914e496157b940a7" + }, + { + "id": "f815063b-ce5e-530c-a06a-e54816542b44", + "version": "MTc1NjMyODAwNTA2OTM1MTAwMA", + "digest": "sha256:478b7e9135ef5dbd32f47a9720daa181343a9f2b2e46a61b11e36ecfecf445e2" + }, + { + "id": "f8159cd0-b63b-5d22-ab66-8e1a99ad685a", + "version": "MTc1NjMyODAxNTI3MDYxNTAwMA", + "digest": "sha256:7432449ff1ee9f45a5f1b4af160d88878208877a83e628655a3fdbd6f476985e" + }, + { + "id": "f8227698-3098-51ac-96d2-d4b9bfc25be1", + "version": "MTc1NjMyODEzOTM2NzMxODAwMA", + "digest": "sha256:60dc4046cff182a6db6fb795c74f2d88c48ea2ee326f852fffb59e2ffe5b3b71" + }, + { + "id": "f82ed72a-4640-5f61-b501-5e350c3cfd79", + "version": "MTc1NjMyODAxMTkzNDc1NjAwMA", + "digest": "sha256:844f1535c8e5b9d3249b51f5836696d524333986d35821fd1d94b876e989ff57" + }, + { + "id": "f82f4ac5-0bba-5c5c-8c3a-49a765fe6b0f", + "version": "MTc1NjMyODA5Njk1NDE5OTAwMA", + "digest": "sha256:c5e45210b6b36879500e150d3dfce6f887e7b4e03bd1e8d2790289bf8db21dd8" + }, + { + "id": "f8456aea-0203-5bd1-b05a-6ca2b408f206", + "version": "MTc1NjMyODAxMDQ3OTA3MjAwMA", + "digest": "sha256:3a42f26705c765aa7e56f8eca4f64bc0c4ddfd853a22dbe330b8932271c9f129" + }, + { + "id": "f84817e1-8b37-5326-a5e0-0525771e6d89", + "version": "MTc1NjMyODA2MjA0ODUzMDAwMA", + "digest": "sha256:4a47beea31f2b1fee86fe6d6a2decb91a4de20ae15d4bb08a1160281fc326fae" + }, + { + "id": "f85dd55b-635f-5ab5-b217-136d68297162", + "version": "MTc1NjMyODA3MzI2MjgwMzAwMA", + "digest": "sha256:c329d42d77af8ce58add605bca051c360377926f0a270887d64eaa32fe3a1611" + }, + { + "id": "f8717543-e5c8-56b7-a6e3-9c2e4efa8125", + "version": "MTc1NjMyODAwNTEwOTAyOTAwMA", + "digest": "sha256:03c7258e6292a6ee3911b3788eb779d36d51ae79f360141c058cbd9374f6fc88" + }, + { + "id": "f88f2898-89a6-5b9e-9dab-e120ca451280", + "version": "MTc1NjMyODA5Njk2MTUyNjAwMA", + "digest": "sha256:8964109a805b6bc5eb09b227fa0b0c80fc31a64a944efbb54912099778600889" + }, + { + "id": "f892ab36-e630-5c52-a09a-2396ba511f5a", + "version": "MTc1NjMyODE3NTAwNDA2OTAwMA", + "digest": "sha256:cbb3adf2032536d65708509fb0697f77dd3c95207175cb1e98e3de8606f46880" + }, + { + "id": "f89d39f2-857c-5236-8771-83de895b9724", + "version": "MTc1NjMyODAxMDQ1NDU0NTAwMA", + "digest": "sha256:cbeb865b0f2f7c1abea4e69a5b58b08eeba83609dffc4991ef3937164d37bdce" + }, + { + "id": "f8a099b8-c9bd-5982-b7a0-e602375a5814", + "version": "MTc1NjMyODAzODY2ODY2MzAwMA", + "digest": "sha256:e1c8c8416036b0c76e4b5ada7bccce0db4ccc9f6f142a9e8e72a05f9330c551b" + }, + { + "id": "f8dce80f-5a8e-5aaf-b995-51efab5aa4f8", + "version": "MTc1NjMyODAwOTAyNTc0NzAwMA", + "digest": "sha256:62ef3641b6a50bc92eb3f4681d07d4b5cd929364c0864611ede5fea774ff0c22" + }, + { + "id": "f8f168b8-6d88-552d-a689-f675c04bf4ac", + "version": "MTc1NjMyODAyNzM0Njg1NTAwMA", + "digest": "sha256:27e3e0a19ee2b5e53d660e152f5c752fd419d79ddaca698f281e4c085aece761" + }, + { + "id": "f8f6d092-357d-5b2f-a6e7-382f20934232", + "version": "MTc1NjMyNzk5NTMwOTk5MDAwMA", + "digest": "sha256:1ed34350c66b1aa53f319d33ca489b08ff990f16672bd18b4597a63f4f6cc2fb" + }, + { + "id": "f8fa41cc-0e79-537e-9e90-ad6142fec461", + "version": "MTc1NjMyODA1NTY5OTk4NjAwMA", + "digest": "sha256:64316c4ce274fd909e25707518cbda0e65213858d88d2df5e5ea941e1bba7417" + }, + { + "id": "f8ff2054-79bf-5026-ba41-e71f355cfd8d", + "version": "MTc1NjMyODAwNTEyNDIwNDAwMA", + "digest": "sha256:3ee008c94402e6ed141b27285f6f84ad1289075bcf8fd87c9ded5ba3ba582dae" + }, + { + "id": "f90836a2-e1ce-52ea-be00-602b02a1ec73", + "version": "MTc1NjMyODA2MjU1OTg5ODAwMA", + "digest": "sha256:a2b4e70c9de269f45e38662db7821eba4035690f03b8acea176f7a227e99fe21" + }, + { + "id": "f913db5f-f0f9-5055-a341-fc7634e496d3", + "version": "MTc1NjMyODExOTIzMjY5MTAwMA", + "digest": "sha256:8bd1cd90102c314bc3edde7de4b3a73f56f36aea875b9050906051a60fc01f9f" + }, + { + "id": "f9165bd7-6083-5ef6-9d76-216d0185d441", + "version": "MTc1NjMyODA1NDE3NTk1NzAwMA", + "digest": "sha256:43f05296e443bd9f2760bc52567cde6a2dc91ce3e8b81f01963b7a7621f4b258" + }, + { + "id": "f9264450-0ba6-5427-a7ff-8b046bc850be", + "version": "MTc1NjMyODE2ODg1MjIwNjAwMA", + "digest": "sha256:3bae1e55fe8781f1d657282b650ec19969a1303469ee94172db082427e865630" + }, + { + "id": "f92f6e5e-e32b-59aa-8a9f-2854ce897979", + "version": "MTc1NjMyODE3MjUwNzc2MDAwMA", + "digest": "sha256:0482614db259b0bde597056d6fb3f78bf8a34d348423b899551bf9e5a0d08cfb" + }, + { + "id": "f940dafc-7f5c-5b79-bcc5-32b00c276620", + "version": "MTc1NjMyODEwNTMyNjI1NDAwMA", + "digest": "sha256:f3c202334f030ae740934158777360e7812a4b28fcebf0e3799ec9556d35c31a" + }, + { + "id": "f9447481-505f-5a30-b1b6-bb2ebfcac5c5", + "version": "MTc1NjMyODAzMDc0Nzk0ODAwMA", + "digest": "sha256:e5ca71ce6c145e3082a23394312bff8ea720d01044ae6df2473f8e33166f031a" + }, + { + "id": "f94dd934-005c-56c6-a490-8ad2ea7a389d", + "version": "MTc1NjMyODAzODg2MDY5MjAwMA", + "digest": "sha256:083286e611668cc49ef9d910be24cd831ba0e9540bf6f76ab8c33192b2efb79b" + }, + { + "id": "f94f93f4-bdf8-569a-82df-fafc12634366", + "version": "MTc1NjMyODA0NTgzMzU1NTAwMA", + "digest": "sha256:ad40ecd3800799b55e7d421a0bb60ab9926db54e24ece275f3ca0ec0593e61db" + }, + { + "id": "f95f9e49-3cf8-58bf-9bcb-112675f892f7", + "version": "MTc1NjMyODAyMTI5MDYyOTAwMA", + "digest": "sha256:ed55f904a00211c86ab530a2b0add51f5fea3bf0a99ecaa51d90f85fd7505d5c" + }, + { + "id": "f96bce35-d1e5-56aa-ba08-34ba5bb6c296", + "version": "MTc1NjMyNzk4ODY5Mjg0NDAwMA", + "digest": "sha256:6a27051d263cb012d04422bd715c4cfe71841be565d2631c62dfe1d56bf0a5ea" + }, + { + "id": "f981eace-05b0-5c9c-ae06-8d6eda4e5d02", + "version": "MTc1NjMyODEzNzY5OTMyOTAwMA", + "digest": "sha256:2f435af8c62d7692110fecf135a671dae442cc925b70cecbba78890a6bd53d40" + }, + { + "id": "f986153c-8949-5d73-b0ab-72733a0ac4dd", + "version": "MTc1NjMyODEwNTMxOTIyODAwMA", + "digest": "sha256:926a45009a57801352fc78d5c5b335b8c0bcaab085251c2662957d75cd32e134" + }, + { + "id": "f9956a30-0f94-566b-abf5-e155089b12c6", + "version": "MTc1NjMyODEzNjE5NTAxODAwMA", + "digest": "sha256:fbf98ee45b3f2ea330357447a4b2a32179071eb638d8d19f7f700e9415739c7d" + }, + { + "id": "f99d6cb2-81fd-569d-816d-5822feb4096d", + "version": "MTc1NjMyODEyNDUxOTk1MjAwMA", + "digest": "sha256:9948614e26541299b399ac15837da56015df3ac6a6c994c436013a1a935bf21a" + }, + { + "id": "f9b37c78-b4e0-5601-ad66-12d5fe6d0e39", + "version": "MTc1NjMyODAyNzM3MDQyMDAwMA", + "digest": "sha256:5fba37238e7e6d274929715fd1f114adc286ee4c9159bd4b47e6d5cca1ddb667" + }, + { + "id": "f9daa582-6139-5ef3-86a6-3b29f4734d40", + "version": "MTc1NjMyODAyNTczOTUyMDAwMA", + "digest": "sha256:ff63ae32cd2b7e86b8aa87f6f6ff6de9ed2c2f95c7039e4f331d2753e4e02180" + }, + { + "id": "f9f2c3f3-768f-5ab8-a05b-7f942bf0a433", + "version": "MTc1NjMyODEzMTE3MDg3MTAwMA", + "digest": "sha256:5ef4198cd4981fd943997aa85306023db95378aa0e40948ab488f9b57bb81e8c" + }, + { + "id": "f9f8f10e-8a24-5606-b82a-dd8848617184", + "version": "MTc1NjMyNzk5NjcxMTQxNDAwMA", + "digest": "sha256:3cfc2e17ea82b9eecfd3e155d4c7e5b4cc95de33d9e07375bd7d9998a782924b" + }, + { + "id": "fa04019f-0201-57c4-a46d-5ddba8fb3ea1", + "version": "MTc1NjMyODE2MDQ4OTc2MzAwMA", + "digest": "sha256:bdc27bb356c682f92fc5734278d7e44f9a92e033d91ec8d28159c111aedf9386" + }, + { + "id": "fa367a3c-41e9-5be4-b823-aee0c37f9b63", + "version": "MTc1NjMyODAyNTg5MzEzNDAwMA", + "digest": "sha256:4476f49ce8a3ae4ca41594eba5f31f58612dcc7f203ddc24cf58b27c419c7854" + }, + { + "id": "fa54b08d-5cf2-58d4-865b-71c16d68100b", + "version": "MTc1NjMyODExMzQ1NTk3MDAwMA", + "digest": "sha256:ca835524da977300df05270a7191f87944e52ce9e63951e8fc16c0affd9aa9a2" + }, + { + "id": "fa65c759-1763-5cc0-89f0-847e3f671c92", + "version": "MTc1NjMyODA4ODU0NDA0MDAwMA", + "digest": "sha256:8460c64c23f315db08a23caff648016c25d288d11009670b9b0ca4a52bc16f4b" + }, + { + "id": "fa6689ee-ef75-570a-8ecf-723aefeb17a0", + "version": "MTc1NjMyODAwNjE0MjE5NDAwMA", + "digest": "sha256:3c1269a366f5c9deb3565cd1c7d9bd8be062d524a8be03216250135c8cf25c92" + }, + { + "id": "fa7cca71-4e35-505d-8bb1-f68e88a8be41", + "version": "MTc1NjMyODAwNTEyNzkyMTAwMA", + "digest": "sha256:b5d5cac2baa169b0a5ceaf64f7763ced141364f59c90dc09956358982bd7ccd1" + }, + { + "id": "fa82b8c5-345c-5bfc-9481-cc4c5f5733dd", + "version": "MTc1NjMyODAwMzk4MzY3NzAwMA", + "digest": "sha256:e843c844bd70526d10078eb2e69a2590cbb868a3d145d733baa8c8344e2bda6a" + }, + { + "id": "fa8a89e1-c61d-5e2d-9cef-85d048da1353", + "version": "MTc1NjMyODA1NDA0ODU4MDAwMA", + "digest": "sha256:9af533cd043b16a3062a588b06900a742e899c23af630247fc55be5b4e46b08f" + }, + { + "id": "fa9bc4f6-3ef4-5f0c-9d72-e56582ca17da", + "version": "MTc1NjMyODAxOTM4MzcxOTAwMA", + "digest": "sha256:afae8435be0222b0979819563ff6b64ad8cfe982987ec495cdafa322f60cc0fc" + }, + { + "id": "fab5316e-a706-57bf-bd8f-08603dd77d1c", + "version": "MTc1NjMyODEzOTM0MDAxMjAwMA", + "digest": "sha256:0f54b1a21cdfdace4e2a9b2ba8ce5d5080f31bc007d955711e99541ec6164cc1" + }, + { + "id": "fac05ef2-b545-583b-9ac2-e7ea43436adf", + "version": "MTc1NjMyODAyNzQxMTcyNTAwMA", + "digest": "sha256:5f2a23e22771efb22dfe8d422820ad46c9d67bbe61400c9378f4c5b04a015eec" + }, + { + "id": "fac9afc1-82ea-5e30-a7b2-20629d4b4899", + "version": "MTc1NjMyODExOTI1MjY2OTAwMA", + "digest": "sha256:bb0ce78e6fdecf9f2df8ae752604dfb58c56ac320ef062b7ee00b8dfd08ae18f" + }, + { + "id": "face14a1-9227-5d85-8ab5-526d90ad33ce", + "version": "MTc1NjMyODE0NzUzMTQxOTAwMA", + "digest": "sha256:8a780c172d446cee903d0f3f7df98e31577149a54fa3b21edf27551ae428ad8c" + }, + { + "id": "fae1566f-b600-584c-b8b9-a288187e933e", + "version": "MTc1NjMyODA3MjU2NTk4MjAwMA", + "digest": "sha256:4ef8ac7808768edb1197e3930e0163ef0c7730af1f8b295839554601cb922b14" + }, + { + "id": "fae87b34-b6dc-523b-93a2-4babe0ea16d8", + "version": "MTc1NjMyODE1NjUxMzY5MDAwMA", + "digest": "sha256:bba50df04fd50f02a0795506a77219247375c42206b86ec78ef161d2795fdd5a" + }, + { + "id": "fb2f39b4-5551-577c-8d2c-4ab7648e08af", + "version": "MTc1NjMyODA2MjY5NjczMDAwMA", + "digest": "sha256:b18ba7d9b88a1792c9997e0c2bcc8b28ddefb0db6e5d110b1e2f672c47a6591c" + }, + { + "id": "fb39d81f-8b7b-509e-af12-80e934c45cae", + "version": "MTc1NjMyODA0NDY4MTc2OTAwMA", + "digest": "sha256:d493000d5405ebfa60a4bf9ae04a4da60e3413a99c07fea29645cca793682cdb" + }, + { + "id": "fb46428d-f34f-54ae-9fb0-db2d6af2429a", + "version": "MTc1NjMyODA5NzAxNDIyNDAwMA", + "digest": "sha256:0e5d19081890da8bc092ef9539382919a5e4fd4ae9162adf72b2172b7dce9441" + }, + { + "id": "fb730cb0-d848-5676-acba-f86996af66b0", + "version": "MTc1NjMyODExMzQ3NjM5MzAwMA", + "digest": "sha256:da93540d6ec296d256faeba86b3241bd60ddd22154f591c78a6d287ea725dfbf" + }, + { + "id": "fb8d3832-45f3-5031-99d3-7fdf76265f9b", + "version": "MTc1NjMyODE1NTA5ODU1NDAwMA", + "digest": "sha256:2ba9ad30b9b9fb6a415d6c3838179f9733f5839b10573f29db939d6fd10f218c" + }, + { + "id": "fb96cb78-ccfa-5614-a55b-779ae313c8cd", + "version": "MTc1NjMyODEwOTIyNTM1NTAwMA", + "digest": "sha256:320fb5bc70a3b40c964905472c663b90c7977da88fcf7ac62aa6478607f21634" + }, + { + "id": "fbcd2a0b-ca22-563c-99c1-4e543756593e", + "version": "MTc1NjMyODAzMDc0Nzg2NzAwMA", + "digest": "sha256:50de6280e8f55d7e82957ff21fcf02bde10b21c93bce95dea35eaf7434de06f0" + }, + { + "id": "fbde8cea-4517-5bce-8f49-79acde23e89c", + "version": "MTc1NjMyODA2MjA3NDM5MjAwMA", + "digest": "sha256:97379f6cdb7c8deba0c33c963c65a125d03fca63239ab4b7de324439c1952a45" + }, + { + "id": "fbe481bf-b3d0-5d00-99ec-fb2cdb9bf058", + "version": "MTc1NjMyODAxNTMxNTk2NjAwMA", + "digest": "sha256:26efe14bc7f168711740ca25052b7cb88120caeb40e42efca32249108384df70" + }, + { + "id": "fbe8a8b1-5d87-5321-8768-9b01ff940bda", + "version": "MTc1NjMyODA3NDI1MDg3MjAwMA", + "digest": "sha256:e15bd2812548c64a94a534586d18954d4ff9486deb16f0b57084c234ad8c2e46" + }, + { + "id": "fbe99936-e445-5b64-931c-11c999e43cc4", + "version": "MTc1NjMyODAyNzM5NTUzNjAwMA", + "digest": "sha256:0836c342d19a06bb599621182b0862270569d24a31d24e66cee60d749d7b3523" + }, + { + "id": "fbef4be6-9977-58a1-b46a-e5ca92591437", + "version": "MTc1NjMyNzk5NjcyMzU3ODAwMA", + "digest": "sha256:a0b1a9928b7d42c513d4773eefddefcff80f7a207a6fa30aa9d4359e38a19fa0" + }, + { + "id": "fbfb0377-68d4-5920-9a9c-c1cf6bb8c430", + "version": "MTc1NjMyODA5NDQ1MTU2OTAwMA", + "digest": "sha256:00fae02a44cb0fc72861f48efa1a2213687cbcb473a7df47ee96680c1f6c6786" + }, + { + "id": "fc2b50e9-4ed9-56a2-92b0-69ae77d28263", + "version": "MTc1NjMyODAzMDcyNjQ2MTAwMA", + "digest": "sha256:00d7f7a651de9be02ab7cec438f072d36be5069d27d30720f4ab92a7b476238d" + }, + { + "id": "fc38d0bc-f738-5f54-9312-f811a07177eb", + "version": "MTc1NjMyODA3NDE5MjY1NDAwMA", + "digest": "sha256:ee58793bc44ea2bdbbde395bb4981c9987dc1d64552766472c9eb49984519c3b" + }, + { + "id": "fc55f395-90b3-593c-b3f0-f817c68bb0fb", + "version": "MTc1NjMyODAxMTU4NTQxNDAwMA", + "digest": "sha256:4eaf98a9c82dea9cda9f4d075b396b7432946a305e1aea995ecf2ea26e6c0a9b" + }, + { + "id": "fc7712b0-5149-5fe2-8d1e-149039facc9d", + "version": "MTc1NjMyODEzMTA4OTc5NzAwMA", + "digest": "sha256:95c40798a9a76bdc1ef272eba004bb8b0259fe797999dcdce4d21cd48f7514e1" + }, + { + "id": "fc8eb4f3-7e4f-5520-91f8-9c3342be6229", + "version": "MTc1NjMyODAzMDc1ODk0OTAwMA", + "digest": "sha256:780a0eb070e72e8d5b179fbaf9f07e8cf292e5b75cbd18d35f00c4eb4064ea79" + }, + { + "id": "fcc68966-913d-5ec4-929c-3b9b708eb4ef", + "version": "MTc1NjMyODE3NTAxODM5MTAwMA", + "digest": "sha256:db6ae64132a9d3f8b68cc13c3613aee06fd910e705c4b3b83fd43bcdd12abee7" + }, + { + "id": "fcc820b5-3e45-526a-82c7-6ff4dce0bd60", + "version": "MTc1NjMyODE2MzQyMjI0MTAwMA", + "digest": "sha256:2517cbcfd00be1c4bfac2f10a2e6549fbef8cf74a52973a165e9691e7b546b30" + }, + { + "id": "fce014e0-ba85-5b2c-a799-a0ce2e2b48ed", + "version": "MTc1NjMyNzk4ODczMjQ0ODAwMA", + "digest": "sha256:28efcc970c0dd94c81607e37999ac74e2eefb13c2519c128ae6f990a5f5b50ec" + }, + { + "id": "fd255311-1be8-564f-8953-81df7584d6d3", + "version": "MTc1NjMyODAxNTI4Njk0MDAwMA", + "digest": "sha256:48856455a02fd2a1f971b0113905e7845efb6cc7392b0c3d8708118f08a8644c" + }, + { + "id": "fd2a8ee8-11a8-5b60-b1c4-49c545d93286", + "version": "MTc1NjMyODAxMTAzNTg1OTAwMA", + "digest": "sha256:2fd647398b5f8837cdbe324ad226ce3f6271df0732f85ca6a62560d4b33e90ba" + }, + { + "id": "fd3a9013-361d-5379-bf13-3cf191e1f281", + "version": "MTc1NjMyODEwODgxMDYyMTAwMA", + "digest": "sha256:f37fd54a56eda6317967755ea822fdea396b63bf815ceaf791569acb8c11332e" + }, + { + "id": "fd6972a7-b649-5092-9e42-22e56ba5ce0b", + "version": "MTc1NjMyODAyMjEyNTM3MDAwMA", + "digest": "sha256:fa822bf076d6cc59547e8acec185995f2ab7dad0845b7bb33657a1889a3374db" + }, + { + "id": "fd734714-4f9d-5d7a-9186-10446e80ca76", + "version": "MTc1NjMyODE2NjQzNDM5ODAwMA", + "digest": "sha256:61c15c82d379539a393e47cd27318a23160e38b659c0f30e13ca792784615b69" + }, + { + "id": "fd79efab-fc82-5623-9bb3-93e6ad1bad32", + "version": "MTc1NjMyODA0NDg4Njk4NzAwMA", + "digest": "sha256:0076bf33915c7ff32460c2f8c4012ad836d7413ddf921998ff919ab64dd47408" + }, + { + "id": "fdae1c2f-13f8-5232-8dd9-c8422bafa525", + "version": "MTc1NjMyODA5NzgwMzQ3NTAwMA", + "digest": "sha256:3fcba104ce592b2a8859a3e9c457877587f2e518627bbfe9bf9792e3595b329c" + }, + { + "id": "fdb51f6c-03a0-53ce-9e57-5bb244b2e53e", + "version": "MTc1NjMyODAyMDQ4OTQ5MjAwMA", + "digest": "sha256:55210e80be796efa9213ffff8577287c81e8833f72ad6cc2c855fc3052e37153" + }, + { + "id": "fdd959f2-2af9-5b3d-bf54-99fc8f174e92", + "version": "MTc1NjMyODAxMTA0NDYxNDAwMA", + "digest": "sha256:e588afa0b161c0d08bb30f724d8eb794e30357fc34981e88e9a1c999104f1f2b" + }, + { + "id": "fdea15c4-e32c-5b3a-b8a8-97d04392718e", + "version": "MTc1NjMyODAyMjE3NjA0MTAwMA", + "digest": "sha256:7a8f88416272749977289b3a27fdc1dbcf1b6d03a10c4b1a996b529c646bcd16" + }, + { + "id": "fe0aa835-0ff1-5b4e-b01a-f4a0fab21500", + "version": "MTc1NjMyODAyMzQ1NzQzNDAwMA", + "digest": "sha256:81e58f4c848099f5775ba3035208143f54553715dbc59c20c8429710ca7d48b8" + }, + { + "id": "fe168311-cb73-54e5-9ecb-49a9f35c6969", + "version": "MTc1NjMyODEzNzU4MDYxMjAwMA", + "digest": "sha256:cd310ff59edad34139342b76b0c225a2ddc2b56b3f08320f1f366baf6fd1cf7a" + }, + { + "id": "fe4a3d68-36b1-5812-860e-6829385bdb05", + "version": "MTc1NjMyODEwOTE3OTkyNTAwMA", + "digest": "sha256:ac2470f6690fe93576edd4f379f9e517668185af4df82f61d8346399f55db1e3" + }, + { + "id": "fe61da17-3795-5ff3-9ca2-4e26f9f95cc8", + "version": "MTc1NjMyODA2MjcwMjg5NzAwMA", + "digest": "sha256:1ab0dd67eb44b3de1e6c26d95269eb42b4016c82805c95cc3ddfe79801ab2010" + }, + { + "id": "fe79aab4-0187-5a23-b018-2991763ede83", + "version": "MTc1NjMyODAzOTczMTUwMDAwMA", + "digest": "sha256:c9304afb55237b9bea727d73bbf03825bade70b2ae3c292f8e7e1ccea5cb4064" + }, + { + "id": "fea16050-5e45-5320-ab32-16ab7bace991", + "version": "MTc1NjMyODE2MjIyOTAyNTAwMA", + "digest": "sha256:e7fadfa7dfa4854a855e2fb09dcb4c59228374b7c5084690c4e96f9be1f6deba" + }, + { + "id": "feca5cb4-ded9-502d-9456-409e4ba15f53", + "version": "MTc1NjMyODA0MTkyMjEzNzAwMA", + "digest": "sha256:7c8f1d82c8a6a65a137a96d6c0018eca60c4a7a07d8afc65e11dcc6cd29687ae" + }, + { + "id": "fed91dde-398e-55ff-bd73-7e72e2c7fc4b", + "version": "MTc1NjMyODAyNTkxNTYyMTAwMA", + "digest": "sha256:08641ea11bdb15f5989165be5f93fcf1419f48c635cc836bd620ce37e54a4798" + }, + { + "id": "fee367c0-6612-5af8-8a07-8e355e95a379", + "version": "MTc1NjMyODA3NDMyNzk0MjAwMA", + "digest": "sha256:e3956e1d5c1ce2edca759b84cc6a522b5ae19ab228d5ed6c8e0bcf9692de9095" + }, + { + "id": "feeba151-7383-5762-b186-2ce39af8bdb7", + "version": "MTc1NjMyODAzNDc0OTMxMzAwMA", + "digest": "sha256:e05a972e7ad414e2eb7fdd4fe24d92e08507a4a1b20c970662c31cd6a8bed254" + }, + { + "id": "ff059354-ff0b-5e8d-b21d-8f17448dc4a9", + "version": "MTc1NjMyODE2ODg2ODE4NTAwMA", + "digest": "sha256:91c8fa21a9b5305f52e43e8040b8d96435c9b299b0d10499f5e9c1e98022cf4b" + }, + { + "id": "ff06ad4b-d867-51ab-a586-6c6105a394c0", + "version": "MTc1NjMyODE2MjI3NDk3ODAwMA", + "digest": "sha256:496994cc65d70b13cb4fdc5262dcf4117d41c8e394a74807ff0480ad0f058da2" + }, + { + "id": "ff376b4a-f510-5fad-b217-9eaefe5894f8", + "version": "MTc1NjMyODEwOTI3Mjg0MDAwMA", + "digest": "sha256:5c1dc61b1ddeeffc08e57b5f8ea0055f6ffe1a816f86e3d19054b47adb688f00" + }, + { + "id": "ff3eb733-3ac9-56a2-b851-cbcdb5ad9221", + "version": "MTc1NjMyODE0NzYyMzA4OTAwMA", + "digest": "sha256:e76f80d51b177b1121ab6f79e158af449177cd7daee7f6cb276e27c34fa0e2bb" + }, + { + "id": "ff5f2528-8817-5256-b998-ab0cf1da1e89", + "version": "MTc1NjMyODE2ODkzMTk4NDAwMA", + "digest": "sha256:4d32cf41f53727988158af33788bf4abb244698d172f07c4385a498291b79818" + }, + { + "id": "ff7be3db-1b8b-52e4-99fa-d13697f6036a", + "version": "MTc1NjMyODE3MDQyMDEyMTAwMA", + "digest": "sha256:4ef2363117f1c776fbccde6048d3c3936d274077974116f51e2567f72b09b4ef" + }, + { + "id": "ff88809d-34c4-54c4-bef6-3cb795a5aa36", + "version": "MTc1NjMyODAxNjQ2MjA4NzAwMA", + "digest": "sha256:2bfd0d8a37370b08e158bdb997fe9df7266a94bca57d260351ac2ab61c5e3fe2" + }, + { + "id": "ff92c334-4326-5f4b-94f2-16b52315e303", + "version": "MTc1NjMyODA1NTc3MzU1MDAwMA", + "digest": "sha256:eb581897d4ac53842e065c9befcd4c19bd2d77997e23ad4f87d168ff3d661eca" + }, + { + "id": "ffc1d2b1-0c57-5d45-8e84-4cee125ab350", + "version": "MTc1NjMyODE2ODgxNTA4NjAwMA", + "digest": "sha256:10ddab07613d609177a172a01f184bac13305ce1ca12db9294aef3741dca7b90" + }, + { + "id": "ffca10c3-2998-5881-98cb-88aefdb1ef31", + "version": "MTc1NjMyODExOTI2MDMwMjAwMA", + "digest": "sha256:6332bcdaeba476d4efc66ad575d8bec0337eb7fb4917f957410a8e66c8160c09" + }, + { + "id": "ffdbe305-691f-5857-9116-a26a5495972e", + "version": "MTc1NjMyODAzNTkxNzYyOTAwMA", + "digest": "sha256:2f9ba5fb1fff8f1434f532d5a64153b3b0e263c7a88422014b49d5c9e4bbe198" + }, + { + "id": "ffdd5392-810b-5b0e-a179-35edccd16e38", + "version": "MTc1NjMyNzk4ODY0MDkwNDAwMA", + "digest": "sha256:a41b02ad775200df9c9d946d6c6b6faeb8220b7094cf6ea4a33e31a01f570881" + }, + { + "id": "fff3176f-c369-5684-a5b5-671b82ace934", + "version": "MTc1NjMyODAyMjE0NzQ3ODAwMA", + "digest": "sha256:382462e7c4702ea3699b833792509ddbf1455dd88f92051c7ffd5f9f13a898d6" + }, + { + "id": "fff357ae-a743-533a-9793-ebc06f0ef4a4", + "version": "MTc1NjMyODE2NzQ4OTcwODAwMA", + "digest": "sha256:6272daa4001039820fd6959fd6294ec76b35d7c953bb5bcd99310b90f4130e07" + } + ] + }, + "counts": { + "Condition": 136, + "DocumentReference": 0, + "Observation": 3470, + "Patient": 100, + "ResearchStudy": 1, + "Specimen": 3151 + }, + "format": "canonical-ndjson-v1", + "lock_digest": "sha256:e59a63817bae3a014cee268b7b012a11ec6239a1ef600419e1da008feb2e0d10" +} diff --git a/testdata/acceptance/ncpi-tcga-brca/graphql.graphql b/testdata/acceptance/ncpi-tcga-brca/graphql.graphql new file mode 100644 index 00000000..5a8bde2b --- /dev/null +++ b/testdata/acceptance/ncpi-tcga-brca/graphql.graphql @@ -0,0 +1,6 @@ +query AcceptanceDataset($dataset: DataframeDatasetInput!, $rows: DataframeRowsInput!, $count: DataframeAggregateInput!, $facets: DataframeAggregationsInput!) { + dataset: dataframeDataset(input: $dataset) { id name projectId datasetGeneration rowCount columns { name } } + rows: dataframeRows(input: $rows) { columns rows totalCount pageInfo { hasNextPage } } + count: dataframeAggregate(input: $count) { columns rows } + facets: dataframeAggregations(input: $facets) { aggregations } +} diff --git a/testdata/acceptance/ncpi-tcga-brca/oracle.json b/testdata/acceptance/ncpi-tcga-brca/oracle.json new file mode 100644 index 00000000..5e6c9bd6 --- /dev/null +++ b/testdata/acceptance/ncpi-tcga-brca/oracle.json @@ -0,0 +1,46 @@ +{ + "row_count": 100, + "unique_patient_count": 100, + "columns": [ + "patient_id", + "submitter_id", + "birth_sex", + "race", + "ethnicity", + "primary_histology", + "pathological_stage", + "age_at_diagnosis_days", + "diagnostic_method", + "days_to_death", + "condition_count", + "specimen_count", + "tumor_specimen_count", + "normal_specimen_count", + "has_paired_tumor_normal", + "earliest_collection_day", + "latest_collection_day" + ], + "non_null_counts": { + "patient_id": 100, + "submitter_id": 100, + "birth_sex": 100, + "race": 89, + "ethnicity": 84, + "primary_histology": 100, + "pathological_stage": 100, + "age_at_diagnosis_days": 98, + "diagnostic_method": 88, + "days_to_death": 6, + "condition_count": 100, + "specimen_count": 100, + "tumor_specimen_count": 100, + "normal_specimen_count": 100, + "has_paired_tumor_normal": 100, + "earliest_collection_day": 100, + "latest_collection_day": 100 + }, + "true_counts": { + "has_paired_tumor_normal": 100 + }, + "row_digest": "sha256:f3aef4e5aa8b44e721c586571d51dac59ef7b809c1b145a6f1217a7f5da98e2a" +} diff --git a/testdata/acceptance/ncpi-tcga-brca/provenance.json b/testdata/acceptance/ncpi-tcga-brca/provenance.json new file mode 100644 index 00000000..97ec5c25 --- /dev/null +++ b/testdata/acceptance/ncpi-tcga-brca/provenance.json @@ -0,0 +1,8 @@ +{ + "source": "NCPI FHIR Aggregator", + "endpoint": "https://google-fhir.fhir-aggregator.org", + "study": "ResearchStudy/638f6162-e000-5167-8216-962d86b74a98", + "selection": "Patient resources matching part-of-study=ResearchStudy/638f6162-e000-5167-8216-962d86b74a98, sorted by id ascending, first 100", + "retrieved_at": "2026-08-30", + "redistribution": "Raw FHIR payloads are not checked into this repository; see fixture lock and content-addressed cache." +} diff --git a/testdata/acceptance/ncpi-tcga-brca/recipe.json b/testdata/acceptance/ncpi-tcga-brca/recipe.json new file mode 100644 index 00000000..cb874933 --- /dev/null +++ b/testdata/acceptance/ncpi-tcga-brca/recipe.json @@ -0,0 +1,14 @@ +{ + "recipeSchemaVersion": 1, + "name": "tcga_brca_cohort", + "translationVersion": "ncpi-tcga-brca-2026-08-30.1", + "outputs": [ + { + "name": "tcga_brca_cohort", + "rootResourceType": "Patient", + "rowGrain": "patient", + "collisionPolicy": "overwrite", + "fields": [{"name": "patient_id", "expr": {"select": "root.id"}}] + } + ] +} diff --git a/testdata/acceptance/ncpi-tcga-brca/workspace.json b/testdata/acceptance/ncpi-tcga-brca/workspace.json new file mode 100644 index 00000000..3d707c4d --- /dev/null +++ b/testdata/acceptance/ncpi-tcga-brca/workspace.json @@ -0,0 +1,48 @@ +{ + "apiVersion": "loom.calypr.org/explorer-authoring/v2", + "kind": "ExplorerBuilderWorkspace", + "explorer": {"title": "TCGA-BRCA patient cohort"}, + "documents": [ + { + "kind": "ExplorerBuilderDocument", + "output": {"id": "tcga_brca_cohort", "title": "TCGA-BRCA patient cohort", "rowLabel": "Patient"}, + "rootResourceType": "Patient", + "route": { + "occurrenceId": "base", + "resourceType": "Patient", + "children": [ + {"occurrenceId": "condition", "resourceType": "Condition", "relationship": "subject_Patient"}, + { + "occurrenceId": "specimen", + "resourceType": "Specimen", + "relationship": "subject_Patient", + "children": [ + {"occurrenceId": "collection_observation", "resourceType": "Observation", "relationship": "specimen_Specimen"} + ] + }, + {"occurrenceId": "observation", "resourceType": "Observation", "relationship": "subject_Patient"} + ] + }, + "columns": [ + {"column": "patient_id", "label": "Patient ID", "logicalType": "string", "occurrenceId": "base", "source": {"kind": "field", "fieldPath": "id", "projectionMode": "VALUE"}, "table": {"visible": true, "order": 0}}, + {"column": "submitter_id", "label": "Submitter ID", "logicalType": "string", "occurrenceId": "base", "source": {"kind": "identifierBySystem", "fieldPath": "identifier[]", "match": "https://gdc.cancer.gov/submitter_id", "projectionMode": "FIRST"}, "table": {"visible": true, "order": 1}}, + {"column": "birth_sex", "label": "Birth sex", "logicalType": "string", "occurrenceId": "base", "source": {"kind": "extensionByUrl", "fieldPath": "extension[]", "match": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", "projectionMode": "FIRST"}, "table": {"visible": true, "order": 2}}, + {"column": "race", "label": "Race", "logicalType": "string", "occurrenceId": "base", "source": {"kind": "extensionByUrl", "fieldPath": "extension[]", "match": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", "projectionMode": "FIRST"}, "table": {"visible": true, "order": 3}}, + {"column": "ethnicity", "label": "Ethnicity", "logicalType": "string", "occurrenceId": "base", "source": {"kind": "extensionByUrl", "fieldPath": "extension[]", "match": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", "projectionMode": "FIRST"}, "table": {"visible": true, "order": 4}}, + {"column": "primary_histology", "label": "Primary histology", "logicalType": "string", "occurrenceId": "condition", "source": {"kind": "aggregate", "operation": "DISTINCT_VALUES", "fieldPath": "code.coding[].display"}, "table": {"visible": true, "order": 5}}, + {"column": "pathological_stage", "label": "Pathological stage", "logicalType": "string", "occurrenceId": "condition", "source": {"kind": "aggregate", "operation": "MIN", "fieldPath": "stage[].summary.coding[].display"}, "table": {"visible": true, "order": 6}}, + {"column": "age_at_diagnosis_days", "label": "Age at diagnosis (days)", "logicalType": "decimal", "occurrenceId": "observation", "source": {"kind": "aggregate", "operation": "MIN", "fieldPath": "valueQuantity.value", "wherePath": "code.coding[].code", "whereEquals": "NCIT_C156418"}, "table": {"visible": true, "order": 7}}, + {"column": "diagnostic_method", "label": "Diagnostic method", "logicalType": "string", "occurrenceId": "observation", "source": {"kind": "aggregate", "operation": "MIN", "fieldPath": "valueString", "wherePath": "code.coding[].code", "whereEquals": "NCIT_C177576"}, "table": {"visible": true, "order": 8}}, + {"column": "days_to_death", "label": "Days to death", "logicalType": "decimal", "occurrenceId": "observation", "source": {"kind": "aggregate", "operation": "MIN", "fieldPath": "valueQuantity.value", "wherePath": "code.coding[].code", "whereEquals": "NCIT_C156419"}, "table": {"visible": true, "order": 9}}, + {"column": "condition_count", "label": "Condition count", "logicalType": "integer", "occurrenceId": "condition", "source": {"kind": "aggregate", "operation": "COUNT"}, "table": {"visible": true, "order": 10}}, + {"column": "specimen_count", "label": "Specimen count", "logicalType": "integer", "occurrenceId": "specimen", "source": {"kind": "aggregate", "operation": "COUNT"}, "table": {"visible": true, "order": 11}}, + {"column": "tumor_specimen_count", "label": "Tumor specimen count", "logicalType": "integer", "occurrenceId": "specimen", "source": {"kind": "aggregate", "operation": "COUNT", "wherePath": "type.coding[].code", "whereEquals": "Tumor"}, "table": {"visible": true, "order": 12}}, + {"column": "normal_specimen_count", "label": "Normal specimen count", "logicalType": "integer", "occurrenceId": "specimen", "source": {"kind": "aggregate", "operation": "COUNT", "wherePath": "type.coding[].code", "whereEquals": "Normal"}, "table": {"visible": true, "order": 13}}, + {"column": "has_paired_tumor_normal", "label": "Has paired tumor and normal", "logicalType": "boolean", "occurrenceId": "specimen", "source": {"kind": "aggregate", "operation": "CONTAINS_ALL", "fieldPath": "type.coding[].code", "requiredValues": ["Tumor", "Normal"]}, "table": {"visible": true, "order": 14}}, + {"column": "earliest_collection_day", "label": "Earliest collection day", "logicalType": "integer", "occurrenceId": "collection_observation", "source": {"kind": "aggregate", "operation": "MIN", "fieldPath": "component[].valueInteger", "wherePath": "code.coding[].code", "whereEquals": "81247-9"}, "table": {"visible": true, "order": 15}}, + {"column": "latest_collection_day", "label": "Latest collection day", "logicalType": "integer", "occurrenceId": "collection_observation", "source": {"kind": "aggregate", "operation": "MAX", "fieldPath": "component[].valueInteger", "wherePath": "code.coding[].code", "whereEquals": "81247-9"}, "table": {"visible": true, "order": 16}} + ] + } + ], + "tabs": [{"id": "tcga_brca_cohort_tab", "title": "TCGA-BRCA patient cohort", "outputId": "tcga_brca_cohort", "order": 0, "visible": true}] +} diff --git a/ui/.dockerignore b/ui/.dockerignore new file mode 100644 index 00000000..b5a8b40b --- /dev/null +++ b/ui/.dockerignore @@ -0,0 +1,3 @@ +node_modules +**/node_modules +**/dist diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 00000000..83ceafed --- /dev/null +++ b/ui/README.md @@ -0,0 +1,30 @@ +# Loom UI + +The `@calypr/loom-ui` package is the source of truth for the portable Explorer +Builder and the standalone Viewer's initial table workflow. Calypr will embed +the package by injecting its own `fetch` implementation; the demo uses a +same-origin, no-auth client. + +The standalone Viewer does not yet replace Calypr's complete Viewer. It loads +only the first 100 rows and searches that client-side subset. Pagination, +server-side search and sorting, facets, charts, downloads, cohort persistence, +shared filters, row details, and advanced cell renderers still need to move +before the Calypr implementation can be deleted. + +From this directory: + +```bash +npm install +npm run test +npm run build +``` + +Run the standalone app against a local Loom server with: + +```bash +LOOM_API_URL=http://127.0.0.1:8080 npm run dev --workspace @calypr/loom-demo +``` + +The demo defaults to `NCPI_ACCEPTANCE`, `default`, and Builder mode. Override +these at runtime with `?project=PROGRAM%2FPROJECT&explorer=default&mode=viewer` +or with `VITE_LOOM_PROJECT`, `VITE_LOOM_EXPLORER`, and `VITE_LOOM_MODE`. diff --git a/ui/apps/demo/Dockerfile b/ui/apps/demo/Dockerfile new file mode 100644 index 00000000..9ab88cd9 --- /dev/null +++ b/ui/apps/demo/Dockerfile @@ -0,0 +1,14 @@ +FROM node:22-alpine AS build +WORKDIR /workspace +COPY package.json package-lock.json* ./ +COPY packages ./packages +COPY apps ./apps +RUN npm ci +RUN npm run build --workspace @calypr/loom-ui +RUN npm run build --workspace @calypr/loom-demo + +FROM nginx:1.27-alpine +COPY apps/demo/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /workspace/apps/demo/dist /usr/share/nginx/html +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] diff --git a/ui/apps/demo/index.html b/ui/apps/demo/index.html new file mode 100644 index 00000000..66201fa1 --- /dev/null +++ b/ui/apps/demo/index.html @@ -0,0 +1,13 @@ + + + + + + + Loom Explorer Demo + + +
+ + + diff --git a/ui/apps/demo/nginx.conf b/ui/apps/demo/nginx.conf new file mode 100644 index 00000000..f5a4e37c --- /dev/null +++ b/ui/apps/demo/nginx.conf @@ -0,0 +1,35 @@ +server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + location /api/ { + proxy_pass http://loom-api:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /graphql/ { + proxy_pass http://loom-api:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /loom-assets/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + location = /index.html { + add_header Cache-Control "no-store"; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/ui/apps/demo/package.json b/ui/apps/demo/package.json new file mode 100644 index 00000000..cb225232 --- /dev/null +++ b/ui/apps/demo/package.json @@ -0,0 +1,21 @@ +{ + "name": "@calypr/loom-demo", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --port 8080", + "build": "tsc -p tsconfig.json --noEmit && vite build", + "preview": "vite preview --host 0.0.0.0 --port 8080" + }, + "dependencies": { + "@calypr/loom-ui": "0.1.2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^6.0.7" + } +} diff --git a/ui/apps/demo/src/main.tsx b/ui/apps/demo/src/main.tsx new file mode 100644 index 00000000..f5907273 --- /dev/null +++ b/ui/apps/demo/src/main.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { createLoomClient, LoomExplorerBuilder } from '@calypr/loom-ui/builder'; +import { LoomExplorerViewer } from '@calypr/loom-ui/viewer'; +import '@mantine/core/styles.css'; +import '@calypr/loom-ui/styles.css'; +import './styles.css'; + +const params = new URLSearchParams(window.location.search); +const project = params.get('project') ?? import.meta.env.VITE_LOOM_PROJECT ?? 'NCPI_ACCEPTANCE'; +const explorerId = params.get('explorer') ?? import.meta.env.VITE_LOOM_EXPLORER ?? 'default'; +const mode = params.get('mode') ?? import.meta.env.VITE_LOOM_MODE ?? 'builder'; +const client = createLoomClient({ + baseUrl: import.meta.env.VITE_LOOM_BASE_URL ?? '/', +}); + +const App = () => { + const [currentMode, setMode] = React.useState(mode === 'viewer' ? 'viewer' : 'builder'); + return ( +
+
+
LOOMFHIR Explorer Studio
+
+ {project} / {explorerId} + + +
+
+
+ {currentMode === 'builder' + ? + : } +
+
+ ); +}; + +const root = document.getElementById('root'); +if (!root) throw new Error('Loom demo root element is missing.'); +createRoot(root).render(); diff --git a/ui/apps/demo/src/styles.css b/ui/apps/demo/src/styles.css new file mode 100644 index 00000000..03b30cea --- /dev/null +++ b/ui/apps/demo/src/styles.css @@ -0,0 +1,11 @@ +* { box-sizing: border-box; } +body { margin: 0; background: #f8fafc; } +.demo-shell { min-height: 100vh; } +.demo-header { position: sticky; top: 0; z-index: 50; display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: .75rem 1.25rem; border-bottom: 1px solid #1e293b; color: #e2e8f0; background: #0f172a; font: 14px Inter, ui-sans-serif, system-ui, sans-serif; } +.demo-header > div:first-child { display: flex; align-items: center; gap: .65rem; } +.demo-mark { color: #93c5fd; font-size: .72rem; font-weight: 800; letter-spacing: .16em; } +.demo-controls { display: flex; align-items: center; gap: .55rem; color: #94a3b8; font-size: .75rem; } +.demo-controls button { border: 1px solid #334155; border-radius: .3rem; padding: .3rem .6rem; color: #cbd5e1; background: #1e293b; cursor: pointer; font: inherit; } +.demo-controls button.active { border-color: #60a5fa; color: white; background: #1d4ed8; } +.demo-content > div { min-height: calc(100vh - 52px); } +@media (max-width: 700px) { .demo-header { align-items: flex-start; flex-direction: column; } .demo-controls { flex-wrap: wrap; } } diff --git a/ui/apps/demo/tsconfig.json b/ui/apps/demo/tsconfig.json new file mode 100644 index 00000000..ba85f468 --- /dev/null +++ b/ui/apps/demo/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "isolatedModules": true, + "types": ["vite/client", "node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/ui/apps/demo/vite.config.ts b/ui/apps/demo/vite.config.ts new file mode 100644 index 00000000..7061b6de --- /dev/null +++ b/ui/apps/demo/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + resolve: { dedupe: ['react', 'react-dom'] }, + build: { + outDir: 'dist', + emptyOutDir: true, + assetsDir: 'loom-assets', + }, + server: { + port: 8080, + host: '0.0.0.0', + proxy: { + '/api': { target: process.env.LOOM_API_URL ?? 'http://127.0.0.1:8080', changeOrigin: true }, + '/graphql': { target: process.env.LOOM_API_URL ?? 'http://127.0.0.1:8080', changeOrigin: true }, + }, + }, + optimizeDeps: { include: ['@calypr/loom-ui'] }, +}); diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 00000000..4ff50adb --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,4571 @@ +{ + "name": "@calypr/loom-ui-workspace", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@calypr/loom-ui-workspace", + "workspaces": [ + "packages/loom-ui", + "apps/demo" + ], + "devDependencies": { + "@types/node": "^25.3.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^6.0.7", + "vitest": "^4.1.11" + }, + "engines": { + "node": ">=20", + "npm": ">=10" + } + }, + "apps/demo": { + "name": "@calypr/loom-demo", + "version": "0.1.0", + "dependencies": { + "@calypr/loom-ui": "0.1.2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^6.0.7" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@calypr/loom-demo": { + "resolved": "apps/demo", + "link": true + }, + "node_modules/@calypr/loom-ui": { + "resolved": "packages/loom-ui", + "link": true + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz", + "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.20", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.20.tgz", + "integrity": "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mantine/core": { + "version": "8.3.15", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.15.tgz", + "integrity": "sha512-wBn/GogB4x7a2Uj7Ztt3amRaApjED+9XqfE4wyCLh88R7KV55k9vnTdCx+irI/GLOOu9tXNUGm3a4t5sTajwkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "clsx": "^2.1.1", + "react-number-format": "^5.4.4", + "react-remove-scroll": "^2.7.1", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.41.0" + }, + "peerDependencies": { + "@mantine/hooks": "8.3.15", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/hooks": { + "version": "8.3.15", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.15.tgz", + "integrity": "sha512-AUSnpUlzttHzJht3CJ1YWi16iy6NWRwtyWO5RLGHHsmiW05DyG0qOPKF8+R5dLHuOCnl3XOu4roI2Y1ku9U04Q==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.5", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.5.tgz", + "integrity": "sha512-QqoryGkqEhWBuQN9bZWRKhwr3Uoj9lCGj/tg0NnIWHHEOXV+5c8cYsc7Q9TST63V92lHqcNV9P5cjvJ9ZAmblQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.81", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.81", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.81.tgz", + "integrity": "sha512-hfbafW4i7uLq7ILok8QWFFm4KMFw22lbZNJHKfHOMSOOoCk5e5m8yfr84UV9NaJajmogWaLVnp2XFU9JQejlqg==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz", + "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.417", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.417.tgz", + "integrity": "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-number-format": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz", + "integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/size-sensor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", + "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==", + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.11" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "packages/loom-ui": { + "name": "@calypr/loom-ui", + "version": "0.1.2", + "dependencies": { + "@tanstack/react-table": "8.20.5", + "@xyflow/react": "^12.10.1", + "echarts": "6.0.0", + "echarts-for-react": "3.0.6", + "elkjs": "^0.9.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@mantine/core": "8.3.15", + "@testing-library/react": "^16.3.3", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^29.1.1", + "typescript": "^5.7.2", + "vite": "^6.0.7", + "vitest": "^4.1.11" + }, + "peerDependencies": { + "@mantine/core": "8.3.15", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 00000000..c2bf6ed5 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,30 @@ +{ + "name": "@calypr/loom-ui-workspace", + "private": true, + "workspaces": [ + "packages/loom-ui", + "apps/demo" + ], + "engines": { + "node": ">=20", + "npm": ">=10" + }, + "scripts": { + "install:clean": "npm install", + "build": "npm run build --workspace @calypr/loom-ui && npm run build --workspace @calypr/loom-demo", + "test": "npm run test --workspace @calypr/loom-ui", + "check-boundaries": "npm run check-boundaries --workspace @calypr/loom-ui" + }, + "devDependencies": { + "@types/node": "^25.3.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^6.0.7", + "vitest": "^4.1.11" + } +} diff --git a/ui/packages/loom-ui/README.md b/ui/packages/loom-ui/README.md new file mode 100644 index 00000000..b15ff063 --- /dev/null +++ b/ui/packages/loom-ui/README.md @@ -0,0 +1,10 @@ +# `@calypr/loom-ui` + +Import Mantine's base stylesheet once in the host application, then import Loom's scoped styles: + +```ts +import '@mantine/core/styles.css'; +import '@calypr/loom-ui/styles.css'; +``` + +Use `@calypr/loom-ui/viewer` for the published Explorer and `@calypr/loom-ui/builder` for authoring. The root export remains available for compatibility. diff --git a/ui/packages/loom-ui/package.json b/ui/packages/loom-ui/package.json new file mode 100644 index 00000000..83ea3095 --- /dev/null +++ b/ui/packages/loom-ui/package.json @@ -0,0 +1,60 @@ +{ + "name": "@calypr/loom-ui", + "version": "0.1.2", + "description": "Portable Loom Explorer Builder and Viewer components.", + "type": "module", + "main": "dist/loom-ui.js", + "module": "dist/loom-ui.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/loom-ui.js", + "default": "./dist/loom-ui.js" + }, + "./styles.css": "./dist/styles.css", + "./viewer": { + "types": "./dist/viewer-entry.d.ts", + "import": "./dist/viewer.js", + "default": "./dist/viewer.js" + }, + "./builder": { + "types": "./dist/builder-entry.d.ts", + "import": "./dist/builder.js", + "default": "./dist/builder.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "vite build && tsc -p tsconfig.json", + "test": "npm run check-boundaries && npm run typecheck:tests && vitest run", + "typecheck:tests": "tsc -p tsconfig.test.json", + "check-boundaries": "node scripts/check-boundaries.mjs" + }, + "peerDependencies": { + "@mantine/core": "8.3.15", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "dependencies": { + "@xyflow/react": "^12.10.1", + "@tanstack/react-table": "8.20.5", + "echarts": "6.0.0", + "echarts-for-react": "3.0.6", + "elkjs": "^0.9.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@mantine/core": "8.3.15", + "@testing-library/react": "^16.3.3", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^29.1.1", + "typescript": "^5.7.2", + "vite": "^6.0.7", + "vitest": "^4.1.11" + } +} diff --git a/ui/packages/loom-ui/postcss.config.cjs b/ui/packages/loom-ui/postcss.config.cjs new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/ui/packages/loom-ui/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/ui/packages/loom-ui/scripts/check-boundaries.mjs b/ui/packages/loom-ui/scripts/check-boundaries.mjs new file mode 100644 index 00000000..60703774 --- /dev/null +++ b/ui/packages/loom-ui/scripts/check-boundaries.mjs @@ -0,0 +1,27 @@ +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +const root = new URL('../src/', import.meta.url); +const forbidden = /(?:@gen3\/core|@gen3\/frontend|from ['"]next(?:\/|['"])|ProtectedContent|CalyprAuth)/; +const effectNames = /\b(?:useEffect|useLayoutEffect|useInsertionEffect|useDeepCompareEffect)\b/; +const files = []; +const walk = async (url) => { + for (const entry of await readdir(url, { withFileTypes: true })) { + const child = new URL(entry.name + (entry.isDirectory() ? '/' : ''), url); + if (entry.isDirectory()) await walk(child); + else if (/\.(ts|tsx)$/.test(entry.name)) files.push(child); + } +}; +await walk(root); +const violations = []; +for (const file of files) { + const source = await readFile(file, 'utf8'); + if (forbidden.test(source)) violations.push(`${file.pathname}: forbidden Calypr dependency`); + const isViewerFeature = file.pathname.includes('/features/ExplorerViewer/') || /\/Viewer\.tsx$/.test(file.pathname); + if (isViewerFeature && effectNames.test(source)) violations.push(`${file.pathname}: Viewer code must not use React effects`); +} +if (violations.length) { + console.error(violations.join('\n')); + process.exit(1); +} +console.log(`Loom UI boundary check passed (${files.length} TypeScript files).`); diff --git a/ui/packages/loom-ui/src/Builder.tsx b/ui/packages/loom-ui/src/Builder.tsx new file mode 100644 index 00000000..6881f33e --- /dev/null +++ b/ui/packages/loom-ui/src/Builder.tsx @@ -0,0 +1,39 @@ +import React, { useMemo } from 'react'; +import { createLoomClient, type LoomClient } from './api'; +import BuilderWorkspace from './features/ExplorerBuilder/BuilderWorkspace'; +import { LoomProvider } from './react'; + +export interface LoomExplorerBuilderProps { + readonly project: string; + readonly explorerId?: string; + readonly client?: LoomClient; + /** Optional legacy project split used by Calypr route wrappers. */ + readonly organization?: string; + readonly onExplorerChange?: (explorerId: string) => void; + readonly className?: string; +} + +export const LoomExplorerBuilder = ({ + project, + explorerId, + client, + organization, + onExplorerChange, + className, +}: LoomExplorerBuilderProps) => { + const ownedClient = useMemo(() => client ?? createLoomClient(), [client]); + return ( + +
+ +
+
+ ); +}; + +export default LoomExplorerBuilder; diff --git a/ui/packages/loom-ui/src/Viewer.tsx b/ui/packages/loom-ui/src/Viewer.tsx new file mode 100644 index 00000000..212b6f3f --- /dev/null +++ b/ui/packages/loom-ui/src/Viewer.tsx @@ -0,0 +1,188 @@ +import React, { useMemo, useReducer, useState } from 'react'; +import { Alert, Button, Center, Group, Loader, MantineProvider, Modal, Stack, Tabs, Text, Title } from '@mantine/core'; +import { createLoomClient, type LoomClient, type LoomOutputRequest, type LoomOutputResult } from './api'; +import { ChartToggle, QuerySummary } from './features/ExplorerViewer/ViewerChrome'; +import { FilterRail, OutputCharts, OutputTable, PageControls, textFor, type ViewerRow } from './features/ExplorerViewer/components'; +import { runtimeSessionKey } from './features/ExplorerViewer/model'; +import { createViewerReducerState, viewerReducer } from './features/ExplorerViewer/reducer'; +import { outputRequestFor } from './features/ExplorerViewer/serialization'; +import { LoomProvider, useLoomClient, useLoomOutput, useLoomRuntime } from './react'; +import type { ExplorerRuntimeOutputV1, ExplorerRuntimeV1 } from './types'; + +type ViewerRuntimeAction = NonNullable[number]; + +export interface LoomViewerActionContext { + readonly project: string; + readonly runtime: ExplorerRuntimeV1; + readonly output: ExplorerRuntimeOutputV1; + readonly action: ViewerRuntimeAction; + readonly request: LoomOutputRequest; + readonly result?: LoomOutputResult; +} + +export type LoomViewerActionHandler = (context: LoomViewerActionContext, signal: AbortSignal) => Promise | void; + +export interface LoomExplorerViewerProps { + readonly project: string; + readonly explorerId?: string; + readonly client?: LoomClient; + readonly className?: string; + readonly activeOutputId?: string; + readonly onActiveOutputChange?: (outputId: string) => void; + readonly renderRowDetails?: (row: ViewerRow) => React.ReactNode; + readonly customActions?: Readonly>; +} + +const downloadBlob = (blob: Blob, fileName: string) => { + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = fileName; + anchor.click(); + queueMicrotask(() => URL.revokeObjectURL(url)); +}; + +const ViewerContent = ({ project, explorerId, activeOutputId, onActiveOutputChange, renderRowDetails, customActions }: Required> & Pick) => { + const runtimeQuery = useLoomRuntime({ project, explorerId: explorerId ?? 'default' }); + if (runtimeQuery.isLoading && !runtimeQuery.data) return
Loading published Explorer…
; + if (runtimeQuery.error || !runtimeQuery.data) return
The published Explorer could not be loaded.
; + if (runtimeQuery.data.outputs.length === 0) return
This Explorer has no published tables yet.
; + return ; +}; + +const ViewerSession = ({ project, runtime, activeOutputId: controlledOutputId, onActiveOutputChange, renderRowDetails, customActions }: { readonly project: string; readonly runtime: ExplorerRuntimeV1; readonly activeOutputId?: string; readonly onActiveOutputChange?: (outputId: string) => void; readonly renderRowDetails?: (row: ViewerRow) => React.ReactNode; readonly customActions?: LoomExplorerViewerProps['customActions'] }) => { + const [state, dispatch] = useReducer(viewerReducer, runtime, (value) => createViewerReducerState(value, controlledOutputId)); + const [pendingAction, setPendingAction] = useState(); + const [actionError, setActionError] = useState(); + const output = runtime.outputs.find((candidate) => candidate.outputId === controlledOutputId) + ?? runtime.outputs.find((candidate) => candidate.outputId === state.activeOutputId) + ?? runtime.outputs[0]; + const outputId = output?.outputId ?? ''; + const request = useMemo(() => output ? outputRequestFor(project, runtime, { ...state, activeOutputId: outputId }, output) : undefined, [output, outputId, project, runtime, state]); + const resultQuery = useLoomOutput(request ?? { project, selector: { recipe: '', translationVersion: '', output: '' }, columns: [] }, { enabled: Boolean(request) }); + const client = useLoomClient(); + if (!output || !request) return null; + + const selectOutput = (nextOutputId: string | null) => { + if (!nextOutputId) return; + dispatch({ type: 'selectOutput', outputId: nextOutputId }); + onActiveOutputChange?.(nextOutputId); + }; + const runAction = async (action?: ViewerRuntimeAction) => { + const actionKey = action?.type ?? 'download'; + setPendingAction(actionKey); + setActionError(undefined); + try { + const customAction = action ? customActions?.[action.type] : undefined; + if (action && customAction) { + await customAction({ project, runtime, output, action, request, result: resultQuery.data }, new AbortController().signal); + } else { + const targetOutput = action?.output + ? runtime.outputs.find((candidate) => candidate.outputId === action.output || candidate.name === action.output) ?? output + : output; + const targetRequest = targetOutput.outputId === output.outputId + ? request + : outputRequestFor(project, runtime, { ...state, activeOutputId: targetOutput.outputId }, targetOutput); + const exportRequest = { + ...targetRequest, + ...(action?.columns ? { columns: action.columns } : {}), + ...(action?.exportHeaders ? { exportHeaders: action.exportHeaders } : {}), + }; + const blob = await client.exportOutput(exportRequest); + downloadBlob(blob, action?.fileName ?? `${targetOutput.name || targetOutput.outputId}.csv`); + } + } catch (error) { + setActionError(error instanceof Error ? error.message : 'The action could not be completed.'); + } finally { + setPendingAction(undefined); + } + }; + const overlay = state.overlay; + + return ( +
+
+ +
+ + Loom Explorer + Published + + {output.title} +
+ + + {output.actions?.map((action, index) => ( + + ))} + +
+ {actionError ? setActionError(undefined)} mb="md">{actionError} : null} + + + {runtime.outputs.map((candidate) => ( + + {candidate.title}{candidate.rowLabel} + + ))} + + {runtime.outputs.map((candidate) => ( + + {candidate.outputId === outputId ? ( +
+ +
+ + + {resultQuery.isLoading + ? Updating results… + : resultQuery.error + ? Results could not be loaded. + : Visual overview} + {output.charts.length > 0 ? : null} + + + + +
+
+ ) : null} +
+ ))} +
+
+ dispatch({ type: 'clearOverlay' })} title="Apply shared filter?" centered> + {overlay.kind === 'sharedFilterConfirmation' ? ( + + Apply {overlay.proposal.name} to every linked output? + + + ) : null} + + dispatch({ type: 'hideOverlay' })} title="Row details" closeButtonProps={{ 'aria-label': 'Close row details' }} centered size="lg"> + {overlay.kind === 'rowDetails' + ? renderRowDetails + ? renderRowDetails(overlay.row) + :
{Object.entries(overlay.row).map(([key, value]) =>
{key}
{textFor(value)}
)}
+ : null} +
+
+ ); +}; + +export const LoomExplorerViewer = ({ project, explorerId, client, className, activeOutputId, onActiveOutputChange, renderRowDetails, customActions }: LoomExplorerViewerProps) => { + const ownedClient = useMemo(() => client ?? createLoomClient(), [client]); + return ( + + +
+ +
+
+
+ ); +}; + +export default LoomExplorerViewer; diff --git a/ui/packages/loom-ui/src/Viewer.unit.test.tsx b/ui/packages/loom-ui/src/Viewer.unit.test.tsx new file mode 100644 index 00000000..dd9c2974 --- /dev/null +++ b/ui/packages/loom-ui/src/Viewer.unit.test.tsx @@ -0,0 +1,203 @@ +// @vitest-environment jsdom + +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { createLoomClient } from './api'; +import { LoomExplorerViewer } from './Viewer'; + +const state = { + apiVersion: 'loom.calypr.org/explorer-state/v1', kind: 'ExplorerState', project: 'NCPI_ACCEPTANCE', explorerId: 'default', title: 'Clinical Explorer', management: 'interactive', + draft: { version: 1, digest: 'digest' }, active: {}, generated: {}, activeUrl: '/viewer', + runtime: { + generation: 'generation-1', outputs: [{ outputId: 'patients', name: 'patients', title: 'Patients', rowLabel: 'patient', selector: { recipe: 'r', translationVersion: 'v1', output: 'patients' }, columns: [{ column: 'patient_id', label: 'Patient ID', logicalType: 'string', visible: true, order: 0, filterable: true, chartable: false }], table: { columns: [{ column: 'patient_id', label: 'Patient ID', visible: true }] }, filters: [], charts: [], fixedFilters: {}, actions: [] }], sharedFilters: {}, diagnostics: [], + }, +}; + +describe('Loom Explorer Viewer', () => { + it('renders server rows and opens the public row details hook', async () => { + const fetch = vi.fn((input, init) => { + if (String(input).includes('/explorers/default')) return Promise.resolve(new Response(JSON.stringify(state), { status: 200 })); + const body = JSON.parse(String(init?.body)) as { query?: string }; + if (body.query?.includes('dataframeRows')) { + const payload = { data: { dataframeRows: { columns: ['patient_id'], rows: [['patient-1']], totalCount: 1, pageInfo: { hasNextPage: false } } } }; + return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 })); + } + return Promise.resolve(new Response('{}', { status: 200 })); + }); + const client = createLoomClient({ fetch }); + render( Details for {String(row.patient_id)}} />); + await waitFor(() => expect(screen.getByText('patient-1')).toBeTruthy()); + expect(screen.getAllByText('Patients').length).toBeGreaterThan(0); + const tableControls = screen.getByRole('navigation', { name: 'Table controls' }); + const table = screen.getByRole('table', { name: 'Patients results' }); + expect(tableControls.compareDocumentPosition(table) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + fireEvent.click(screen.getByRole('button', { name: 'patient-1' })); + expect(await screen.findByRole('dialog')).toHaveTextContent('Details for patient-1'); + fireEvent.click(screen.getByRole('button', { name: 'Close row details' })); + await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); + }); + + it('exposes output tabs and updates the selected output', async () => { + const multiOutputState = { + ...state, + runtime: { + ...state.runtime, + outputs: [ + state.runtime.outputs[0], + { + ...state.runtime.outputs[0], + outputId: 'biospecimens', + name: 'biospecimens', + title: 'Biospecimens', + rowLabel: 'biospecimen', + }, + ], + }, + }; + const fetch = vi.fn((input) => { + if (String(input).includes('/explorers/default')) return Promise.resolve(new Response(JSON.stringify(multiOutputState), { status: 200 })); + return Promise.resolve(new Response(JSON.stringify({ data: { dataframeRows: { columns: ['patient_id'], rows: [['patient-1']], totalCount: 1, pageInfo: { hasNextPage: false } } } }), { status: 200 })); + }); + + render(); + + const tabList = await screen.findByRole('tablist', { name: 'Output tables' }); + expect(tabList).toBeTruthy(); + const patientsTab = screen.getByRole('tab', { name: /Patients/ }); + const biospecimensTab = screen.getByRole('tab', { name: /Biospecimens/ }); + expect(patientsTab).toHaveAttribute('aria-selected', 'true'); + expect(patientsTab).toHaveAttribute('data-active'); + expect(biospecimensTab).toHaveAttribute('aria-selected', 'false'); + expect(biospecimensTab).not.toHaveAttribute('data-active'); + for (const tab of [patientsTab, biospecimensTab]) { + const panelId = tab.getAttribute('aria-controls'); + expect(panelId).toBeTruthy(); + expect(document.getElementById(panelId ?? '')).toHaveAttribute('role', 'tabpanel'); + } + + patientsTab.focus(); + fireEvent.keyDown(patientsTab, { key: 'ArrowRight' }); + + await waitFor(() => { + expect(patientsTab).toHaveAttribute('aria-selected', 'false'); + expect(patientsTab).not.toHaveAttribute('data-active'); + expect(biospecimensTab).toHaveAttribute('aria-selected', 'true'); + expect(biospecimensTab).toHaveAttribute('data-active'); + }); + }); + + it('selects visible facet values without opening a generic dropdown', async () => { + const filterState = { + ...state, + runtime: { + ...state.runtime, + outputs: state.runtime.outputs.map((output) => ({ + ...output, + filters: [{ column: 'patient_id', label: 'Patient ID', type: 'terms' }], + })), + }, + }; + const fetch = vi.fn((input, init) => { + if (String(input).includes('/explorers/default')) return Promise.resolve(new Response(JSON.stringify(filterState), { status: 200 })); + const payload = { + data: { + dataframeRows: { columns: ['patient_id'], rows: [['patient-1']], totalCount: 1, pageInfo: { hasNextPage: false } }, + dataframeAggregations: { aggregations: [{ name: 'loom:patients:patient_id', kind: 'TERMS', columns: ['key', 'doc_count'], rows: [['patient-1', 1]] }] }, + }, + }; + return Promise.resolve(new Response(JSON.stringify(payload), { status: 200 })); + }); + + render(); + + const value = await screen.findByRole('checkbox', { name: /patient-1/ }); + fireEvent.click(value); + await waitFor(() => expect((value as HTMLInputElement).checked).toBe(true)); + await waitFor(() => { + const graphQLCalls = fetch.mock.calls.filter(([input]) => String(input).includes('/graphql/graph')); + expect(graphQLCalls.length).toBeGreaterThan(1); + const request = JSON.parse(String(graphQLCalls.at(-1)?.[1]?.body)) as { variables: { input: { filters?: unknown } } }; + expect(request.variables.input.filters).toEqual([{ column: 'patient_id', op: 'EQ', value: 'patient-1' }]); + }); + }); + + it('routes named runtime actions through the host adapter', async () => { + const actionState = { + ...state, + runtime: { + ...state.runtime, + outputs: state.runtime.outputs.map((output) => ({ + ...output, + actions: [{ type: 'open-study', title: 'Open study' }], + })), + }, + }; + const fetch = vi.fn((input) => { + if (String(input).includes('/explorers/default')) return Promise.resolve(new Response(JSON.stringify(actionState), { status: 200 })); + return Promise.resolve(new Response(JSON.stringify({ data: { dataframeRows: { columns: ['patient_id'], rows: [['patient-1']], totalCount: 1, pageInfo: { hasNextPage: false } } } }), { status: 200 })); + }); + const openStudy = vi.fn(); + + render(); + + fireEvent.click(await screen.findByRole('button', { name: 'Open study' })); + await waitFor(() => expect(openStudy).toHaveBeenCalledTimes(1)); + expect(openStudy.mock.calls[0]?.[0]).toMatchObject({ + project: 'NCPI_ACCEPTANCE', + output: { outputId: 'patients' }, + action: { type: 'open-study' }, + }); + }); + + it('exports the configured output, columns, and headers for built-in actions', async () => { + const actionState = { + ...state, + runtime: { + ...state.runtime, + outputs: [ + { + ...state.runtime.outputs[0], + actions: [{ + type: 'download-related', + title: 'Download related', + output: 'biospecimens', + columns: ['sample_id'], + exportHeaders: { sample_id: 'Sample ID' }, + }], + }, + { + ...state.runtime.outputs[0], + outputId: 'biospecimens', + name: 'biospecimens', + title: 'Biospecimens', + rowLabel: 'biospecimen', + selector: { recipe: 'r', translationVersion: 'v1', output: 'biospecimens' }, + columns: [{ column: 'sample_id', label: 'Sample ID', logicalType: 'string', visible: true, order: 0, filterable: false, chartable: false }], + table: { columns: [{ column: 'sample_id', label: 'Sample ID', visible: true }] }, + actions: [], + }, + ], + }, + }; + const fetch = vi.fn((input) => { + if (String(input).includes('/explorers/default')) return Promise.resolve(new Response(JSON.stringify(actionState), { status: 200 })); + return Promise.resolve(new Response(JSON.stringify({ data: { dataframeRows: { columns: ['patient_id'], rows: [['patient-1']], totalCount: 1, pageInfo: { hasNextPage: false } } } }), { status: 200 })); + }); + const client = createLoomClient({ fetch }); + const exportOutput = vi.spyOn(client, 'exportOutput').mockResolvedValue(new Blob(['Sample ID\nsample-1'])); + Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:test') }); + Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() }); + vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); + + render(); + + fireEvent.click(await screen.findByRole('button', { name: 'Download related' })); + await waitFor(() => expect(exportOutput).toHaveBeenCalledTimes(1)); + expect(exportOutput.mock.calls[0]?.[0]).toMatchObject({ + selector: { output: 'biospecimens' }, + columns: ['sample_id'], + exportHeaders: { sample_id: 'Sample ID' }, + }); + }); +}); diff --git a/ui/packages/loom-ui/src/api.ts b/ui/packages/loom-ui/src/api.ts new file mode 100644 index 00000000..3e5e84dd --- /dev/null +++ b/ui/packages/loom-ui/src/api.ts @@ -0,0 +1,609 @@ +import { + assertExplorerBuilderCompileResult, + assertExplorerBuilderPreviewResult, + assertExplorerBuilderPublishResult, + assertExplorerBuilderState, + assertExplorerStateV1, + explorerAuthoringCapabilitiesSchema, + explorerBuilderCommandsResultSchema, + explorerBuilderSuggestionsResultSchema, + type ExplorerBuilderCatalog, + type ExplorerBuilderCommand, + type ExplorerBuilderCompileResult, + type ExplorerBuilderPreviewResult, + type ExplorerBuilderState, + type ExplorerBuilderSuggestionsResult, + type ExplorerBuilderWorkspace, + type ExplorerRuntimeV1, +} from './types'; +import type { ExplorerAuthoringDiagnostic } from './types'; + +export interface ExplorerSummary { + readonly project: string; + readonly explorerId: string; + readonly title: string; + readonly management: string; + readonly activeRevisionId?: string; + readonly updatedAt: string; +} + +export interface ExplorerAuthoringApiError { + readonly status: number | 'FETCH_ERROR' | 'CUSTOM_ERROR'; + readonly code?: string; + readonly message: string; + readonly diagnostics?: ReadonlyArray; + readonly requestId?: string; + readonly details?: Readonly>; + readonly retryable?: boolean; +} + +export interface ExplorerAuthoringStateArgs { + readonly project: string; + readonly explorerId: string; + readonly authResourcePath?: string; +} + +export interface ExplorerAuthoringProjectArgs { + readonly project: string; + readonly authResourcePath?: string; +} + +export interface ApplyExplorerBuilderCommandsArgs extends ExplorerAuthoringStateArgs { + readonly commandId: string; + readonly snapshotToken: string; + readonly expectedDraftVersion: number; + readonly expectedDraftDigest?: string; + readonly commands: ReadonlyArray; + readonly requestId?: string; +} + +export interface ReconcileExplorerBuilderArgs extends ExplorerAuthoringStateArgs { + readonly snapshotToken: string; + readonly draftVersion: number; + readonly draftDigest: string; + readonly requestId?: string; +} + +export interface PreviewExplorerBuilderArgs extends ExplorerAuthoringStateArgs { + readonly receiptId: string; + readonly outputId: string; + readonly limit?: number; + readonly requestId?: string; +} + +export interface PublishExplorerBuilderArgs extends ExplorerAuthoringStateArgs { + readonly receiptId: string; + readonly requestId?: string; +} + +export interface ExplorerCandidateSuggestionsArgs extends ExplorerAuthoringStateArgs { + readonly snapshotToken: string; + readonly nodeId: string; + readonly query?: string; + readonly requestId?: string; +} + +export interface CreateExplorerArgs extends ExplorerAuthoringProjectArgs { + readonly name: string; + readonly title?: string; + readonly sourceExplorerId?: string; + readonly requestId?: string; +} + +export interface DeleteExplorerArgs extends ExplorerAuthoringStateArgs { + readonly requestId?: string; +} + +export interface LoomClientOptions { + /** URL prefix for the Loom service. `/` is the standalone no-auth default. */ + readonly baseUrl?: string; + /** Inject Calypr's authenticated fetch implementation when embedded. */ + readonly fetch?: typeof globalThis.fetch; + readonly headers?: HeadersInit; + readonly credentials?: RequestCredentials; +} + +export interface LoomRowsOptions { + readonly project: string; + readonly first?: number; + readonly signal?: AbortSignal; +} + +export type LoomOutputFilterOperator = + | 'EQ' + | 'NEQ' + | 'IN' + | 'NOT_IN' + | 'LT' + | 'LTE' + | 'GT' + | 'GTE' + | 'CONTAINS' + | 'STARTS_WITH' + | 'EXISTS' + | 'IS_NULL' + | 'ARRAY_CONTAINS' + | 'ARRAY_OVERLAPS'; + +export interface LoomOutputFilter { + readonly column: string; + readonly op: LoomOutputFilterOperator; + readonly value: unknown; +} + +export interface LoomOutputSort { + readonly column: string; + readonly desc?: boolean; +} + +export interface LoomFacetSpec { + readonly name: string; + readonly kind: 'TERMS' | 'HISTOGRAM' | 'DATE_HISTOGRAM' | 'STATS' | 'MISSING'; + readonly column: string; + readonly size?: number; + readonly interval?: number; + readonly dateInterval?: number; + readonly excludeSelfFilter?: boolean; +} + +export interface LoomOutputRequest { + readonly project: string; + readonly selector: ExplorerRuntimeV1['outputs'][number]['selector']; + readonly columns?: ReadonlyArray; + readonly filters?: ReadonlyArray; + readonly sort?: LoomOutputSort; + readonly first?: number; + readonly after?: string; + readonly facets?: ReadonlyArray; + readonly exportHeaders?: Readonly>; +} + +export interface LoomFacetResult { + readonly name: string; + readonly kind: string; + readonly columns: ReadonlyArray; + readonly rows: ReadonlyArray>; + readonly missingCount?: number; + readonly truncated?: boolean; +} + +export interface LoomOutputResult { + readonly columns: ReadonlyArray; + readonly rows: ReadonlyArray>; + readonly totalCount: number | null; + readonly pageInfo: { + readonly hasNextPage: boolean; + readonly endCursor?: string; + }; + readonly materialization?: Readonly>; + readonly facets: ReadonlyArray; +} + +export interface LoomClient { + readonly listExplorers: ( + args: ExplorerAuthoringProjectArgs, + signal?: AbortSignal, + ) => Promise>; + readonly getBuilder: ( + args: ExplorerAuthoringStateArgs, + options?: { readonly signal?: AbortSignal; readonly reload?: boolean }, + ) => Promise; + readonly getCapability: ( + args: ExplorerAuthoringStateArgs, + signal?: AbortSignal, + ) => Promise>; + readonly getExplorer: ( + args: ExplorerAuthoringStateArgs, + signal?: AbortSignal, + ) => Promise; + readonly applyCommands: ( + args: ApplyExplorerBuilderCommandsArgs, + signal?: AbortSignal, + ) => Promise>; + readonly reconcile: ( + args: ReconcileExplorerBuilderArgs, + signal?: AbortSignal, + ) => Promise; + readonly suggestions: ( + args: ExplorerCandidateSuggestionsArgs, + signal?: AbortSignal, + ) => Promise; + readonly preview: ( + args: PreviewExplorerBuilderArgs, + signal?: AbortSignal, + ) => Promise; + readonly publish: ( + args: PublishExplorerBuilderArgs, + signal?: AbortSignal, + ) => Promise>; + readonly createExplorer: ( + args: CreateExplorerArgs, + signal?: AbortSignal, + ) => Promise; + readonly deleteExplorer: ( + args: DeleteExplorerArgs, + signal?: AbortSignal, + ) => Promise; + readonly fetchGraphQL: ( + query: string, + variables?: Readonly>, + signal?: AbortSignal, + ) => Promise; + readonly rows: ( + selector: ExplorerRuntimeV1['outputs'][number]['selector'], + columns: ReadonlyArray, + options: LoomRowsOptions, + ) => Promise<{ readonly columns: ReadonlyArray; readonly rows: ReadonlyArray>; readonly totalCount?: number | null }>; + readonly queryOutput: ( + request: LoomOutputRequest, + signal?: AbortSignal, + ) => Promise; + readonly exportOutput: ( + request: LoomOutputRequest, + signal?: AbortSignal, + ) => Promise; + readonly invalidate: (scope?: 'explorers' | 'builder' | 'all') => void; +} + +const canonicalProject = (project: string): string => { + let value = project.trim(); + try { + value = decodeURIComponent(value); + } catch { + // Leave malformed URL input for the server to reject with its contract. + } + value = value.replace(/^\/+|\/+$/g, ''); + return value; +}; + +const encodedProject = (project: string): string => + encodeURIComponent(encodeURIComponent(canonicalProject(project))); + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const parseJSON = async (response: Response): Promise => { + const text = await response.text(); + if (!text) return {}; + try { + return JSON.parse(text) as unknown; + } catch { + return { message: text }; + } +}; + +const diagnosticsFrom = (value: unknown): ReadonlyArray => + Array.isArray(value) ? value.filter(isRecord).flatMap((item) => { + if ( + typeof item.code !== 'string' || + typeof item.message !== 'string' || + (item.severity !== 'error' && item.severity !== 'warning' && item.severity !== 'info') + ) return []; + return [{ + severity: item.severity, + code: item.code, + message: item.message, + ...(typeof item.stage === 'string' ? { stage: item.stage } : {}), + ...(typeof item.path === 'string' || item.path === null ? { path: item.path } : {}), + ...(typeof item.fieldPath === 'string' || item.fieldPath === null ? { fieldPath: item.fieldPath } : {}), + ...(typeof item.requestId === 'string' ? { requestId: item.requestId } : {}), + }]; + }) : []; + +const requestError = async (response: Response): Promise => { + const payload = await parseJSON(response); + const record = isRecord(payload) ? payload : {}; + const nested = isRecord(record.error) ? record.error : record; + const code = typeof nested.code === 'string' ? nested.code : undefined; + const message = typeof nested.message === 'string' + ? nested.message + : `Loom request failed (${response.status}).`; + return { + status: response.status, + code, + message, + diagnostics: diagnosticsFrom(nested.diagnostics), + requestId: response.headers.get('x-request-id') ?? response.headers.get('request-id') ?? undefined, + details: isRecord(nested.details) ? nested.details : undefined, + retryable: response.status === 408 || response.status === 429 || response.status >= 500, + }; +}; + +export class LoomRequestError extends Error implements ExplorerAuthoringApiError { + readonly info: ExplorerAuthoringApiError; + readonly status: ExplorerAuthoringApiError['status']; + readonly code?: string; + readonly diagnostics?: ReadonlyArray; + readonly requestId?: string; + readonly details?: Readonly>; + readonly retryable?: boolean; + constructor(info: ExplorerAuthoringApiError) { + super(info.message); + this.name = 'LoomRequestError'; + this.info = info; + this.status = info.status; + this.code = info.code; + this.diagnostics = info.diagnostics; + this.requestId = info.requestId; + this.details = info.details; + this.retryable = info.retryable; + } +} + +const shapeRows = (rows: unknown, columns: ReadonlyArray): Array> => { + if (!Array.isArray(rows)) return []; + return rows.map((row) => { + const source = Array.isArray(row) + ? Object.fromEntries(columns.map((column, index) => [column, row[index]])) + : isRecord(row) ? row : {}; + const result: Record = {}; + Object.entries(source).forEach(([key, value]) => { + const parts = key.split('.').filter(Boolean); + if (parts.length < 2) { + result[key] = value; + return; + } + let cursor = result; + parts.slice(0, -1).forEach((part) => { + const nested = cursor[part]; + if (!isRecord(nested)) cursor[part] = {}; + cursor = cursor[part] as Record; + }); + cursor[parts[parts.length - 1]] = value; + }); + return result; + }); +}; + +const numberOrNull = (value: unknown): number | null => + typeof value === 'number' && Number.isFinite(value) ? value : null; + +const normalizedFacet = (value: unknown): LoomFacetResult | undefined => { + if (!isRecord(value) || typeof value.name !== 'string' || typeof value.kind !== 'string') return undefined; + const columns = Array.isArray(value.columns) ? value.columns.filter((column): column is string => typeof column === 'string') : []; + const rows = shapeRows(value.rows, columns); + const missingCount = numberOrNull(value.missingCount); + return { + name: value.name, + kind: value.kind, + columns, + rows, + ...(missingCount === null ? {} : { missingCount }), + ...(typeof value.truncated === 'boolean' ? { truncated: value.truncated } : {}), + }; +}; + +const outputQuery = (request: LoomOutputRequest): { + readonly query: string; + readonly variables: Readonly>; +} => { + const hasFacets = (request.facets?.length ?? 0) > 0; + const input: Record = { + projectId: canonicalProject(request.project), + selector: request.selector, + ...(request.columns && request.columns.length > 0 ? { columns: [...request.columns] } : {}), + ...(request.filters && request.filters.length > 0 ? { filters: request.filters.map((filter) => ({ column: filter.column, op: filter.op, value: filter.value })) } : {}), + ...(request.sort ? { sort: { column: request.sort.column, desc: request.sort.desc ?? false } } : {}), + ...(request.first === undefined ? {} : { first: request.first }), + ...(request.after ? { after: request.after } : {}), + }; + const query = hasFacets + ? `query LoomOutput($input: DataframeRowsInput!, $facetInput: DataframeAggregationsInput!) { dataframeRows(input: $input) { materialization { id name revision projectId datasetGeneration state rowCount selector { recipe translationVersion output } } columns rows totalCount pageInfo { hasNextPage endCursor } } dataframeAggregations(input: $facetInput) { aggregations } }` + : `query LoomOutput($input: DataframeRowsInput!) { dataframeRows(input: $input) { materialization { id name revision projectId datasetGeneration state rowCount selector { recipe translationVersion output } } columns rows totalCount pageInfo { hasNextPage endCursor } } }`; + const variables: Record = { input }; + if (hasFacets) { + variables.facetInput = { + projectId: canonicalProject(request.project), + selector: request.selector, + ...(request.filters && request.filters.length > 0 ? { filters: request.filters.map((filter) => ({ column: filter.column, op: filter.op, value: filter.value })) } : {}), + specs: [...(request.facets ?? [])], + }; + } + return { query, variables }; +}; + +export const createLoomClient = (options: LoomClientOptions = {}): LoomClient => { + const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis); + const baseUrl = options.baseUrl ?? '/'; + const cache = new Map>(); + + const urlFor = (path: string): string => { + if (/^https?:\/\//.test(baseUrl)) return `${baseUrl.replace(/\/$/, '')}${path}`; + return `${baseUrl.replace(/\/$/, '')}${path}` || '/'; + }; + const request = async (path: string, init: RequestInit = {}): Promise => { + try { + const response = await fetcher(urlFor(path), { + ...init, + credentials: options.credentials ?? 'same-origin', + headers: { + Accept: 'application/json', + ...(init.body ? { 'Content-Type': 'application/json' } : {}), + ...(options.headers ?? {}), + ...(init.headers ?? {}), + }, + }); + if (!response.ok) throw new LoomRequestError(await requestError(response)); + return parseJSON(response); + } catch (error) { + if (error instanceof LoomRequestError) throw error; + if (init.signal?.aborted) throw error; + throw new LoomRequestError({ + status: 'FETCH_ERROR', + message: error instanceof Error ? error.message : String(error), + retryable: true, + }); + } + }; + const getCached = (key: string, run: () => Promise, reload = false): Promise => { + if (reload) cache.delete(key); + const existing = cache.get(key); + if (existing) return existing as Promise; + const promise = run(); + cache.set(key, promise); + void promise.catch(() => cache.delete(key)); + return promise; + }; + const authoringPath = (args: ExplorerAuthoringStateArgs, suffix: string): string => + `/api/v1/projects/${encodedProject(args.project)}/explorers/${encodeURIComponent(args.explorerId)}/authoring/v2${suffix}`; + const projectPath = (args: ExplorerAuthoringProjectArgs): string => + `/api/v1/projects/${encodedProject(args.project)}/explorers`; + const authResourcePathQuery = (authResourcePath?: string): string => { + const value = authResourcePath?.trim(); + return value ? `?${new URLSearchParams({ auth_resource_path: value }).toString()}` : ''; + }; + const durableAuthoringPath = (args: ExplorerAuthoringStateArgs, suffix: string): string => + `${authoringPath(args, suffix)}${authResourcePathQuery(args.authResourcePath)}`; + const durableProjectPath = (args: ExplorerAuthoringProjectArgs): string => + `${projectPath(args)}${authResourcePathQuery(args.authResourcePath)}`; + const withJson = (body: unknown, signal?: AbortSignal, requestId?: string): RequestInit => ({ + method: 'POST', + body: JSON.stringify(body), + signal, + headers: requestId ? { 'X-Request-ID': requestId } : undefined, + }); + + const listExplorers = (args: ExplorerAuthoringProjectArgs, signal?: AbortSignal) => + getCached(`explorers:${canonicalProject(args.project)}`, async () => { + const value = await request(projectPath(args), { signal }); + if (!Array.isArray(value)) throw new LoomRequestError({ status: 502, code: 'INVALID_EXPLORER_LIST', message: 'Loom returned an invalid Explorer list.', retryable: false }); + return value as ReadonlyArray; + }); + const getBuilder = (args: ExplorerAuthoringStateArgs, queryOptions: { readonly signal?: AbortSignal; readonly reload?: boolean } = {}) => + getCached(`builder:${canonicalProject(args.project)}:${args.explorerId}`, async () => assertExplorerBuilderState(await request(authoringPath(args, '/builder'), { signal: queryOptions.signal })), queryOptions.reload); + const getCapability = (args: ExplorerAuthoringStateArgs, signal?: AbortSignal) => + getCached(`capability:${canonicalProject(args.project)}:${args.explorerId}`, async () => explorerAuthoringCapabilitiesSchema.parse(await request(authoringPath(args, '/capability'), { signal }))); + const getExplorer = (args: ExplorerAuthoringStateArgs, signal?: AbortSignal) => + getCached(`viewer:${canonicalProject(args.project)}:${args.explorerId}`, async () => { + const value = await request(`/api/v1/projects/${encodedProject(args.project)}/explorers/${encodeURIComponent(args.explorerId)}`, { signal }); + const state = assertExplorerStateV1(value); + if (!state.runtime) throw new LoomRequestError({ status: 422, code: 'EXPLORER_RUNTIME_REQUIRED', message: 'The selected Explorer has no published runtime.', retryable: false }); + return state.runtime; + }); + const applyCommands = async (args: ApplyExplorerBuilderCommandsArgs, signal?: AbortSignal) => { + const value = explorerBuilderCommandsResultSchema.parse(await request(durableAuthoringPath(args, '/commands'), withJson({ + commandId: args.commandId, + snapshotToken: args.snapshotToken, + expectedDraftVersion: args.expectedDraftVersion, + ...(args.expectedDraftDigest ? { expectedDraftDigest: args.expectedDraftDigest } : {}), + commands: args.commands, + }, signal, args.requestId))); + cache.delete(`builder:${canonicalProject(args.project)}:${args.explorerId}`); + return value; + }; + const reconcile = (args: ReconcileExplorerBuilderArgs, signal?: AbortSignal) => + request(durableAuthoringPath(args, '/reconcile'), withJson({ snapshotToken: args.snapshotToken, draftVersion: args.draftVersion, draftDigest: args.draftDigest }, signal, args.requestId)).then(assertExplorerBuilderCompileResult); + const suggestions = (args: ExplorerCandidateSuggestionsArgs, signal?: AbortSignal) => + request(authoringPath(args, '/suggestions'), withJson({ snapshotToken: args.snapshotToken, nodeId: args.nodeId, ...(args.query ? { query: args.query } : {}) }, signal, args.requestId)).then((value) => explorerBuilderSuggestionsResultSchema.parse(value)); + const preview = (args: PreviewExplorerBuilderArgs, signal?: AbortSignal) => + request(authoringPath(args, '/preview'), withJson({ receiptId: args.receiptId, outputId: args.outputId, ...(args.limit === undefined ? {} : { limit: args.limit }) }, signal, args.requestId)).then(assertExplorerBuilderPreviewResult); + const publish = async (args: PublishExplorerBuilderArgs, signal?: AbortSignal) => { + const result = assertExplorerBuilderPublishResult( + await request( + durableAuthoringPath(args, '/publish'), + withJson({ receiptId: args.receiptId }, signal, args.requestId), + ), + ); + cache.delete(`viewer:${canonicalProject(args.project)}:${args.explorerId}`); + return result; + }; + const createExplorer = async (args: CreateExplorerArgs, signal?: AbortSignal) => { + const value = await request(durableProjectPath(args), withJson({ name: args.name, ...(args.title ? { title: args.title } : {}), ...(args.sourceExplorerId ? { sourceExplorerId: args.sourceExplorerId } : {}) }, signal, args.requestId)); + cache.delete(`explorers:${canonicalProject(args.project)}`); + return value as ExplorerSummary; + }; + const deleteExplorer = async (args: DeleteExplorerArgs, signal?: AbortSignal) => { + await request(`${projectPath(args)}/${encodeURIComponent(args.explorerId)}`, { method: 'DELETE', signal, headers: args.requestId ? { 'X-Request-ID': args.requestId } : undefined }); + cache.delete(`explorers:${canonicalProject(args.project)}`); + cache.delete(`builder:${canonicalProject(args.project)}:${args.explorerId}`); + return null; + }; + const fetchGraphQL = async (query: string, variables?: Readonly>, signal?: AbortSignal): Promise => { + const payload = await request('/graphql/graph', { method: 'POST', signal, body: JSON.stringify({ query, variables }) }); + if (!isRecord(payload)) throw new LoomRequestError({ status: 502, code: 'INVALID_GRAPHQL_RESPONSE', message: 'Loom returned an invalid GraphQL response.', retryable: false }); + if (Array.isArray(payload.errors) && payload.errors.length > 0) { + const first = isRecord(payload.errors[0]) && typeof payload.errors[0].message === 'string' ? payload.errors[0].message : 'Loom GraphQL request failed.'; + throw new LoomRequestError({ status: 'CUSTOM_ERROR', code: 'GRAPHQL_ERROR', message: first, retryable: false }); + } + return payload.data as T; + }; + const queryOutput = async (outputRequest: LoomOutputRequest, signal?: AbortSignal): Promise => { + const prepared = outputQuery(outputRequest); + const data = await fetchGraphQL(prepared.query, prepared.variables, signal); + if (!isRecord(data) || !isRecord(data.dataframeRows)) { + throw new LoomRequestError({ status: 502, code: 'INVALID_OUTPUT_RESPONSE', message: 'Loom returned an invalid output response.', retryable: false }); + } + const connection = data.dataframeRows; + const columns = Array.isArray(connection.columns) ? connection.columns.filter((column): column is string => typeof column === 'string') : []; + const pageInfo = isRecord(connection.pageInfo) ? connection.pageInfo : {}; + const facets = isRecord(data.dataframeAggregations) && Array.isArray(data.dataframeAggregations.aggregations) + ? data.dataframeAggregations.aggregations.map(normalizedFacet).filter((facet): facet is LoomFacetResult => facet !== undefined) + : []; + const materialization = isRecord(connection.materialization) ? connection.materialization : undefined; + const endCursor = typeof pageInfo.endCursor === 'string' ? pageInfo.endCursor : undefined; + return { + columns, + rows: shapeRows(connection.rows, columns), + totalCount: numberOrNull(connection.totalCount), + pageInfo: { hasNextPage: pageInfo.hasNextPage === true, ...(endCursor ? { endCursor } : {}) }, + ...(materialization ? { materialization } : {}), + facets, + }; + }; + + const exportOutput = async (outputRequest: LoomOutputRequest, signal?: AbortSignal): Promise => { + const rows: Array> = []; + let columns: ReadonlyArray = outputRequest.columns ?? []; + let after: string | undefined; + const first = Math.max(outputRequest.first ?? 100, 1000); + while (true) { + if (signal?.aborted) throw new DOMException('The export was aborted.', 'AbortError'); + const page = await queryOutput({ ...outputRequest, first, after, facets: [] }, signal); + if (columns.length === 0) columns = page.columns; + rows.push(...page.rows); + if (!page.pageInfo.hasNextPage || !page.pageInfo.endCursor || page.pageInfo.endCursor === after) break; + after = page.pageInfo.endCursor; + } + const csvValue = (value: unknown): string => { + if (value === undefined || value === null) return ''; + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value); + try { return JSON.stringify(value); } catch { return String(value); } + }; + const csvCell = (value: unknown): string => { + const text = csvValue(value); + return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; + }; + const lines = [columns.map((column) => csvCell(outputRequest.exportHeaders?.[column] ?? column)).join(','), ...rows.map((row) => columns.map((column) => csvCell(row[column])).join(','))]; + return new Blob([lines.join('\n') + '\n'], { type: 'text/csv;charset=utf-8' }); + }; + + const rows = async (selector: ExplorerRuntimeV1['outputs'][number]['selector'], columns: ReadonlyArray, rowOptions: LoomRowsOptions) => { + const result = await queryOutput({ project: rowOptions.project, selector, columns, first: rowOptions.first ?? 100 }, rowOptions.signal); + return { columns: result.columns, rows: result.rows, totalCount: result.totalCount }; + }; + return { + listExplorers, + getBuilder, + getCapability, + getExplorer, + applyCommands, + reconcile, + suggestions, + preview, + publish, + createExplorer, + deleteExplorer, + fetchGraphQL, + rows, + queryOutput, + exportOutput, + invalidate: (scope = 'all') => { + if (scope === 'all' || scope === 'explorers') [...cache.keys()].filter((key) => key.startsWith('explorers:')).forEach((key) => cache.delete(key)); + if (scope === 'all' || scope === 'builder') [...cache.keys()].filter((key) => key.startsWith('builder:')).forEach((key) => cache.delete(key)); + if (scope === 'all') [...cache.keys()].filter((key) => key.startsWith('viewer:')).forEach((key) => cache.delete(key)); + }, + }; +}; + +export type { ExplorerBuilderCatalog }; diff --git a/ui/packages/loom-ui/src/api.unit.test.ts b/ui/packages/loom-ui/src/api.unit.test.ts new file mode 100644 index 00000000..f2e04363 --- /dev/null +++ b/ui/packages/loom-ui/src/api.unit.test.ts @@ -0,0 +1,276 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createLoomClient } from './api'; + +describe('Loom project paths', () => { + it('sends Calypr authorization scope only on durable Explorer mutations', async () => { + const fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ error: { code: 'TEST_STOP', message: 'stop after transport' } }), { + status: 403, + headers: { 'content-type': 'application/json' }, + }), + ); + const client = createLoomClient({ fetch }); + const scope = { + project: 'HTAN_INT/BForePC', + explorerId: 'test', + authResourcePath: '/programs/HTAN_INT/projects/BForePC', + }; + const ignoreFailure = async (request: Promise) => { + await request.catch(() => undefined); + }; + + await ignoreFailure(client.createExplorer({ ...scope, name: 'test' })); + await ignoreFailure(client.applyCommands({ + ...scope, + commandId: 'command-1', + snapshotToken: 'snapshot-1', + expectedDraftVersion: 1, + commands: [], + })); + await ignoreFailure(client.reconcile({ + ...scope, + snapshotToken: 'snapshot-1', + draftVersion: 1, + draftDigest: 'digest-1', + })); + await ignoreFailure(client.publish({ ...scope, receiptId: 'receipt-1' })); + await ignoreFailure(client.suggestions({ ...scope, snapshotToken: 'snapshot-1', nodeId: 'Patient' })); + await ignoreFailure(client.preview({ ...scope, receiptId: 'receipt-1', outputId: 'patients' })); + + const urls = fetch.mock.calls.map(([url]) => String(url)); + const encodedScope = 'auth_resource_path=%2Fprograms%2FHTAN_INT%2Fprojects%2FBForePC'; + expect(urls.slice(0, 4)).toEqual([ + `/api/v1/projects/HTAN_INT%252FBForePC/explorers?${encodedScope}`, + `/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/commands?${encodedScope}`, + `/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/reconcile?${encodedScope}`, + `/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/publish?${encodedScope}`, + ]); + expect(urls.slice(4)).toEqual([ + '/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/suggestions', + '/api/v1/projects/HTAN_INT%252FBForePC/explorers/test/authoring/v2/preview', + ]); + }); + + it('omits authorization scope cleanly for standalone mutations', async () => { + const fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ error: { code: 'TEST_STOP', message: 'stop after transport' } }), { + status: 403, + headers: { 'content-type': 'application/json' }, + }), + ); + const client = createLoomClient({ fetch }); + + await client.createExplorer({ project: 'NCPI_ACCEPTANCE', name: 'default' }).catch(() => undefined); + await client.applyCommands({ + project: 'NCPI_ACCEPTANCE', + explorerId: 'default', + commandId: 'command-1', + snapshotToken: 'snapshot-1', + expectedDraftVersion: 1, + commands: [], + }).catch(() => undefined); + + await client.reconcile({ + project: 'NCPI_ACCEPTANCE', + explorerId: 'default', + snapshotToken: 'snapshot-1', + draftVersion: 1, + draftDigest: 'digest-1', + }).catch(() => undefined); + await client.publish({ + project: 'NCPI_ACCEPTANCE', + explorerId: 'default', + receiptId: 'receipt-1', + }).catch(() => undefined); + + expect(fetch.mock.calls.map(([url]) => String(url))).toEqual([ + '/api/v1/projects/NCPI_ACCEPTANCE/explorers', + '/api/v1/projects/NCPI_ACCEPTANCE/explorers/default/authoring/v2/commands', + '/api/v1/projects/NCPI_ACCEPTANCE/explorers/default/authoring/v2/reconcile', + '/api/v1/projects/NCPI_ACCEPTANCE/explorers/default/authoring/v2/publish', + ]); + }); + + it('preserves standalone project identifiers', async () => { + const fetch = vi.fn().mockResolvedValue( + new Response('[]', { status: 200, headers: { 'content-type': 'application/json' } }), + ); + const client = createLoomClient({ fetch }); + + await client.listExplorers({ project: 'NCPI_ACCEPTANCE' }); + + expect(fetch).toHaveBeenCalledWith( + '/api/v1/projects/NCPI_ACCEPTANCE/explorers', + expect.any(Object), + ); + }); + + it('keeps Calypr organization and project identifiers double encoded', async () => { + const fetch = vi.fn().mockResolvedValue( + new Response('[]', { status: 200, headers: { 'content-type': 'application/json' } }), + ); + const client = createLoomClient({ fetch }); + + await client.listExplorers({ project: 'HTAN_INT/BForePC' }); + + expect(fetch).toHaveBeenCalledWith( + '/api/v1/projects/HTAN_INT%252FBForePC/explorers', + expect.any(Object), + ); + }); + + it('scopes dataframe row queries to the selected project', async () => { + const fetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ data: { dataframeRows: { columns: ['patient_id'], rows: [], totalCount: 0 } } }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ), + ); + const client = createLoomClient({ fetch }); + + await client.rows( + { recipe: 'cohort', translationVersion: 'v1', output: 'patients' }, + ['patient_id'], + { project: 'NCPI_ACCEPTANCE' }, + ); + + const request = fetch.mock.calls[0]?.[1]; + const body = JSON.parse(String(request?.body)) as { variables: { input: { projectId?: string } } }; + expect(body.variables.input.projectId).toBe('NCPI_ACCEPTANCE'); + }); + + it('exposes structured HTTP error fields to Builder recovery code', async () => { + const fetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + error: { + code: 'STALE_SNAPSHOT', + message: 'The catalog snapshot changed.', + diagnostics: [ + { + severity: 'error', + code: 'STALE_SNAPSHOT', + message: 'Reload the Builder state.', + }, + ], + }, + }), + { + status: 409, + headers: { + 'content-type': 'application/json', + 'x-request-id': 'request-123', + }, + }, + ), + ); + const client = createLoomClient({ fetch }); + + await expect( + client.listExplorers({ project: 'NCPI_ACCEPTANCE' }), + ).rejects.toMatchObject({ + status: 409, + code: 'STALE_SNAPSHOT', + retryable: false, + requestId: 'request-123', + diagnostics: [ + expect.objectContaining({ code: 'STALE_SNAPSHOT' }), + ], + }); + }); + + it('invalidates cached Viewer runtime after publication', async () => { + const explorerState = (generation: string) => ({ + apiVersion: 'loom.calypr.org/explorer-state/v1', + kind: 'ExplorerState', + project: 'NCPI_ACCEPTANCE', + explorerId: 'default', + title: 'Cohort', + management: 'interactive', + active: {}, + draft: { version: 1, digest: 'digest' }, + generated: {}, + activeUrl: '/viewer', + runtime: { generation, outputs: [], sharedFilters: {}, diagnostics: [] }, + }); + const fetch = vi + .fn() + .mockResolvedValueOnce( + new Response(JSON.stringify(explorerState('old')), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + apiVersion: 'loom.calypr.org/explorer-authoring/v2', + kind: 'ExplorerBuilderPublication', + receiptId: 'receipt-1', + revisionId: 'revision-2', + state: 'READY', + outputs: [], + diagnostics: [], + }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(explorerState('new')), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ); + const client = createLoomClient({ fetch }); + const scope = { project: 'NCPI_ACCEPTANCE', explorerId: 'default' }; + + await expect(client.getExplorer(scope)).resolves.toMatchObject({ generation: 'old' }); + await client.publish({ ...scope, receiptId: 'receipt-1' }); + await expect(client.getExplorer(scope)).resolves.toMatchObject({ generation: 'new' }); + expect(fetch).toHaveBeenCalledTimes(3); + }); + + it('serializes server-side filters, sort, cursors, and requested facets', async () => { + const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ + data: { + dataframeRows: { + materialization: { id: 'mat-1', state: 'READY' }, + columns: ['status'], rows: [['active']], totalCount: 1, + pageInfo: { hasNextPage: false, endCursor: null }, + }, + dataframeAggregations: { aggregations: [{ name: 'status', kind: 'TERMS', columns: ['key', 'doc_count'], rows: [{ key: 'active', doc_count: 1 }] }] }, + }, + }), { status: 200 })); + const client = createLoomClient({ fetch }); + const result = await client.queryOutput({ + project: 'NCPI_ACCEPTANCE', + selector: { recipe: 'cohort', translationVersion: 'v1', output: 'patients' }, + columns: ['status'], + filters: [{ column: 'status', op: 'IN', value: ['active', 'pending'] }], + sort: { column: 'status', desc: true }, first: 20, after: 'cursor-1', + facets: [{ name: 'status', kind: 'TERMS', column: 'status', size: 10 }], + }); + const payload = JSON.parse(String(fetch.mock.calls[0]?.[1]?.body)) as { variables: { input: Record; facetInput: Record } }; + expect(payload.variables.input).toMatchObject({ projectId: 'NCPI_ACCEPTANCE', first: 20, after: 'cursor-1', sort: { column: 'status', desc: true } }); + expect(payload.variables.input.filters).toEqual([{ column: 'status', op: 'IN', value: ['active', 'pending'] }]); + expect(payload.variables.facetInput.specs).toEqual([{ name: 'status', kind: 'TERMS', column: 'status', size: 10 }]); + expect(result.rows).toEqual([{ status: 'active' }]); + expect(result.facets[0]?.rows[0]).toEqual({ key: 'active', doc_count: 1 }); + }); + + it('exports all cursor pages as an escaped CSV Blob', async () => { + const page = (rows: unknown[][], hasNextPage: boolean, endCursor?: string) => new Response(JSON.stringify({ data: { dataframeRows: { columns: ['id', 'label'], rows, totalCount: 2, pageInfo: { hasNextPage, endCursor } } } }), { status: 200 }); + const fetch = vi.fn() + .mockResolvedValueOnce(page([['one', 'a,b']], true, 'next')) + .mockResolvedValueOnce(page([['two', 'say "hi"']], false)); + const client = createLoomClient({ fetch }); + const blob = await client.exportOutput({ project: 'NCPI_ACCEPTANCE', selector: { recipe: 'r', translationVersion: 'v1', output: 'o' }, columns: ['id', 'label'], first: 1, after: 'current-page', exportHeaders: { id: 'Record ID' } }); + expect(await blob.text()).toBe('Record ID,label\none,"a,b"\ntwo,"say ""hi"""\n'); + expect(fetch).toHaveBeenCalledTimes(2); + const secondBody = JSON.parse(String(fetch.mock.calls[1]?.[1]?.body)) as { variables: { input: { after?: string } } }; + const firstBody = JSON.parse(String(fetch.mock.calls[0]?.[1]?.body)) as { variables: { input: { after?: string; first?: number } } }; + expect(firstBody.variables.input.after).toBeUndefined(); + expect(firstBody.variables.input.first).toBe(1000); + expect(secondBody.variables.input.after).toBe('next'); + }); +}); diff --git a/ui/packages/loom-ui/src/builder-entry.ts b/ui/packages/loom-ui/src/builder-entry.ts new file mode 100644 index 00000000..fc9f07fd --- /dev/null +++ b/ui/packages/loom-ui/src/builder-entry.ts @@ -0,0 +1,8 @@ +import './styles.css'; + +export { createLoomClient, LoomRequestError } from './api'; +export type { LoomClient, LoomClientOptions, ExplorerAuthoringApiError, ExplorerSummary } from './api'; +export { LoomProvider, useLoomClient } from './react'; +export { LoomExplorerBuilder } from './Builder'; +export type { LoomExplorerBuilderProps } from './Builder'; +export * from './features/ExplorerBuilder'; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.reconciliation.unit.test.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.reconciliation.unit.test.tsx new file mode 100644 index 00000000..d6003bc2 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.reconciliation.unit.test.tsx @@ -0,0 +1,458 @@ +// @vitest-environment jsdom +import React from 'react'; +import { vi, type Mock } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + useApplyExplorerBuilderCommandsV2Mutation, + useCreateExplorerAuthoringMutation, + useDeleteExplorerAuthoringMutation, + useGetExplorerAuthoringCapabilityV2Query, + useGetExplorerAuthoringExplorersQuery, + useGetExplorerBuilderStateV2Query, + useGetExplorerCandidateSuggestionsV2Mutation, + usePreviewExplorerAuthoringV2Mutation, + usePublishExplorerAuthoringV2Mutation, + useReconcileExplorerBuilderV2Mutation, +} from '../../react'; +import BuilderWorkspace from './BuilderWorkspace'; + +vi.mock('../../react', () => ({ + useApplyExplorerBuilderCommandsV2Mutation: vi.fn(), + useCreateExplorerAuthoringMutation: vi.fn(), + useDeleteExplorerAuthoringMutation: vi.fn(), + useGetExplorerAuthoringCapabilityV2Query: vi.fn(), + useGetExplorerAuthoringExplorersQuery: vi.fn(), + useGetExplorerBuilderStateV2Query: vi.fn(), + useGetExplorerCandidateSuggestionsV2Mutation: vi.fn(), + usePreviewExplorerAuthoringV2Mutation: vi.fn(), + usePublishExplorerAuthoringV2Mutation: vi.fn(), + useReconcileExplorerBuilderV2Mutation: vi.fn(), +})); + +vi.mock('./components/BuilderToolbar', () => ({ + BuilderToolbar: ({ + onPreview, + onPublish, + previewDisabled, + publishDisabled, + publishing, + }: { + readonly onPreview: () => void; + readonly onPublish: () => void; + readonly previewDisabled: boolean; + readonly publishDisabled: boolean; + readonly publishing: boolean; + }) => ( +
+ + +
+ ), +})); + +vi.mock('./components/GuidedGraphWorkspace', () => ({ + GuidedGraphWorkspace: ({ + onChangeEdge, + }: { + readonly onChangeEdge: (occurrenceId: string, edgeId: string) => void; + }) => ( + + ), +})); + +vi.mock('./components/ColumnSelector', () => ({ + ColumnSelector: ({ + onChange, + }: { + readonly onChange: (value: unknown) => void; + }) => ( + + ), +})); + +vi.mock('./components/PreviewTable', () => ({ + PreviewTable: () =>
Preview table
, +})); + +const apiVersion = 'loom.calypr.org/explorer-authoring/v2' as const; +const column = { + column: 'specimen_identifier', + label: 'Specimen identifier', + occurrenceId: 'base', + source: { + kind: 'field' as const, + fieldPath: 'identifier[].value', + projectionMode: 'FIRST', + }, + table: { visible: true, order: 0 }, +}; +const workspace = { + apiVersion, + kind: 'ExplorerBuilderWorkspace' as const, + explorer: { title: 'Test Explorer' }, + documents: [ + { + kind: 'ExplorerBuilderDocument' as const, + output: { id: 'specimens', title: 'Specimens' }, + rootResourceType: 'Specimen', + route: { occurrenceId: 'base', resourceType: 'Specimen' }, + columns: [column], + }, + ], + tabs: [ + { + id: 'specimens-tab', + title: 'Specimens', + outputId: 'specimens', + order: 0, + visible: true, + }, + ], +}; +const catalog = { + snapshotToken: 'snapshot-1', + generation: 'generation-1', + routePolicy: { allowRepeatedEdges: false, allowSelfLoops: false }, + nodes: [ + { + nodeId: 'specimen-node', + resourceType: 'Specimen', + rowRootEligible: true, + populated: true, + documentCount: 1, + }, + ], + edges: [], + candidates: [ + { + candidateId: 'specimen-id', + nodeId: 'specimen-node', + fieldPath: 'identifier[].value', + label: 'Specimen identifier', + logicalType: 'string', + projectionModes: ['FIRST'], + defaultProjectionMode: 'FIRST', + filterable: true, + chartable: false, + }, + ], +}; +const builderState = { + apiVersion, + kind: 'ExplorerBuilderState' as const, + lifecycleState: 'READY' as const, + draftVersion: 1, + draftDigest: 'sha256:draft-1', + workspace, + catalog, +}; +const receipt = { + apiVersion, + kind: 'ExplorerBuilderReceipt' as const, + receiptId: 'receipt-1', + snapshotToken: 'snapshot-1', + builder: workspace, + outputs: [ + { + outputId: 'specimens', + columns: [ + { + column: 'specimen_identifier', + label: 'Specimen identifier', + logicalType: 'string', + filterable: true, + chartable: false, + }, + ], + }, + ], + diagnostics: [], +}; + +const resolvedRequest = (value: T) => ({ + unwrap: vi.fn().mockResolvedValue(value), + abort: vi.fn(), +}); + +const deferredRequest = () => { + let resolve: (value: T) => void = () => undefined; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { + request: { unwrap: vi.fn(() => promise), abort: vi.fn() }, + resolve, + }; +}; + +describe('BuilderWorkspace on-demand reconciliation', () => { + let applyCommands: Mock; + let reconcile: Mock; + let preview: Mock; + let publish: Mock; + + beforeEach(() => { + applyCommands = vi.fn().mockReturnValue( + resolvedRequest({ + commandId: 'command-1', + workspace, + draftVersion: 2, + draftDigest: 'sha256:draft-2', + results: [ + { + type: 'TABLE_CHANGED', + outputId: 'specimens', + column: 'specimen_identifier', + }, + ], + diagnostics: [], + }), + ); + reconcile = vi.fn().mockReturnValue(resolvedRequest(receipt)); + preview = vi.fn().mockReturnValue( + resolvedRequest({ + apiVersion, + kind: 'ExplorerBuilderPreview', + receiptId: 'receipt-1', + outputId: 'specimens', + columns: receipt.outputs[0].columns, + rows: [], + rowCount: 0, + diagnostics: [], + }), + ); + publish = vi.fn().mockReturnValue(resolvedRequest({})); + + (useGetExplorerAuthoringExplorersQuery as Mock).mockReturnValue({ + data: [{ explorerId: 'test', title: 'Test Explorer' }], + isLoading: false, + refetch: vi.fn(), + }); + (useGetExplorerBuilderStateV2Query as Mock).mockReturnValue({ + data: builderState, + isLoading: false, + refetch: vi.fn(), + }); + (useGetExplorerAuthoringCapabilityV2Query as Mock).mockReturnValue({ + data: { features: { deleteExplorer: false } }, + }); + (useApplyExplorerBuilderCommandsV2Mutation as Mock).mockReturnValue([ + applyCommands, + { isLoading: false }, + ]); + (useReconcileExplorerBuilderV2Mutation as Mock).mockReturnValue([ + reconcile, + { isLoading: false }, + ]); + (usePreviewExplorerAuthoringV2Mutation as Mock).mockReturnValue([ + preview, + { isLoading: false }, + ]); + (usePublishExplorerAuthoringV2Mutation as Mock).mockReturnValue([ + publish, + { isLoading: false }, + ]); + (useCreateExplorerAuthoringMutation as Mock).mockReturnValue([ + vi.fn(), + { isLoading: false }, + ]); + (useDeleteExplorerAuthoringMutation as Mock).mockReturnValue([ + vi.fn(), + { isLoading: false }, + ]); + (useGetExplorerCandidateSuggestionsV2Mutation as Mock).mockReturnValue( + [vi.fn(), { isLoading: false }], + ); + }); + + it('does not reconcile a hydrated draft until Preview requests a receipt', async () => { + render( + , + ); + + const previewButton = await screen.findByRole('button', { + name: 'Preview', + }); + await waitFor(() => expect(previewButton).toBeEnabled()); + expect(reconcile).not.toHaveBeenCalled(); + + fireEvent.click(previewButton); + + await waitFor(() => expect(reconcile).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(preview).toHaveBeenCalledTimes(1)); + }); + + it('publishes a server-persisted draft after the Builder is reloaded', async () => { + render( + , + ); + + const publishButton = await screen.findByRole('button', { + name: 'Publish', + }); + expect(publishButton).toBeEnabled(); + + fireEvent.click(publishButton); + + await waitFor(() => expect(reconcile).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(publish).toHaveBeenCalledTimes(1)); + }); + + it('shows publication progress until the publish response settles', async () => { + const pending = deferredRequest>(); + publish.mockReturnValue(pending.request); + render( + , + ); + + fireEvent.click(await screen.findByRole('button', { name: 'Publish' })); + + const publishingButton = await screen.findByRole('button', { + name: 'Publishing…', + }); + expect(publishingButton).toBeDisabled(); + expect(publishingButton).toHaveAttribute('aria-busy', 'true'); + expect(publish).toHaveBeenCalledTimes(1); + + pending.resolve({}); + await waitFor(() => + expect( + screen.queryByRole('button', { name: 'Publishing…' }), + ).not.toBeInTheDocument(), + ); + }); + + it('saves commands without reconciling, then reconciles once before Publish', async () => { + render( + , + ); + + fireEvent.click( + await screen.findByRole('button', { name: 'Save column change' }), + ); + await waitFor(() => expect(applyCommands).toHaveBeenCalledTimes(1)); + await waitFor(() => + expect(screen.getByRole('button', { name: 'Publish' })).toBeEnabled(), + ); + expect(reconcile).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole('button', { name: 'Publish' })); + + await waitFor(() => expect(reconcile).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(publish).toHaveBeenCalledTimes(1)); + }); + + it('saves an edited traversal relationship in place', async () => { + render( + , + ); + + fireEvent.click( + await screen.findByRole('button', { name: 'Change relationship' }), + ); + + await waitFor(() => + expect(applyCommands).toHaveBeenCalledWith( + expect.objectContaining({ + project: 'HTAN_INT/BForePC', + explorerId: 'test', + authResourcePath: '/programs/HTAN_INT/projects/BForePC', + commands: [ + { + type: 'UPDATE_ROUTE_EDGE', + outputId: 'specimens', + occurrenceId: 'patient-subject', + edgeId: 'specimen-patient-participant', + }, + ], + }), + ), + ); + }); + + it('previews after one click when reconciliation returns a normalized builder', async () => { + reconcile.mockReturnValue( + resolvedRequest({ + ...receipt, + builder: { + ...workspace, + sharedFilters: { + identifier: [ + { outputId: 'specimens', column: 'specimen_identifier' }, + ], + }, + }, + }), + ); + + render( + , + ); + + const previewButton = await screen.findByRole('button', { + name: 'Preview', + }); + await waitFor(() => expect(previewButton).toBeEnabled()); + fireEvent.click(previewButton); + + await waitFor(() => expect(reconcile).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(preview).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.route.unit.test.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.route.unit.test.tsx new file mode 100644 index 00000000..ed940d40 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.route.unit.test.tsx @@ -0,0 +1,236 @@ +// @vitest-environment jsdom +import React from 'react'; +import { vi, type Mock } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + useApplyExplorerBuilderCommandsV2Mutation, + useCreateExplorerAuthoringMutation, + useDeleteExplorerAuthoringMutation, + useGetExplorerAuthoringCapabilityV2Query, + useGetExplorerAuthoringExplorersQuery, + useGetExplorerBuilderStateV2Query, + useGetExplorerCandidateSuggestionsV2Mutation, + usePreviewExplorerAuthoringV2Mutation, + usePublishExplorerAuthoringV2Mutation, + useReconcileExplorerBuilderV2Mutation, +} from '../../react'; +import BuilderWorkspace from './BuilderWorkspace'; + +vi.mock('../../react', () => ({ + useApplyExplorerBuilderCommandsV2Mutation: vi.fn(), + useCreateExplorerAuthoringMutation: vi.fn(), + useDeleteExplorerAuthoringMutation: vi.fn(), + useGetExplorerAuthoringCapabilityV2Query: vi.fn(), + useGetExplorerAuthoringExplorersQuery: vi.fn(), + useGetExplorerBuilderStateV2Query: vi.fn(), + useGetExplorerCandidateSuggestionsV2Mutation: vi.fn(), + usePreviewExplorerAuthoringV2Mutation: vi.fn(), + usePublishExplorerAuthoringV2Mutation: vi.fn(), + useReconcileExplorerBuilderV2Mutation: vi.fn(), +})); + +const mutationResult = () => [vi.fn(), { isLoading: false }]; +const newBuilderState = () => ({ + apiVersion: 'loom.calypr.org/explorer-authoring/v2' as const, + kind: 'ExplorerBuilderState' as const, + lifecycleState: 'NEW' as const, + draftVersion: 0, + draftDigest: '', + workspace: null, + catalog: { + snapshotToken: 'snapshot-first', + generation: 'generation-first', + routePolicy: { allowRepeatedEdges: false, allowSelfLoops: false }, + nodes: [], + edges: [], + candidates: [], + }, +}); + +describe('BuilderWorkspace route selection', () => { + beforeEach(() => { + (useGetExplorerAuthoringExplorersQuery as Mock).mockReturnValue({ + data: [], + isLoading: true, + refetch: vi.fn(), + }); + (useGetExplorerBuilderStateV2Query as Mock).mockReturnValue({ + data: undefined, + isLoading: true, + refetch: vi.fn(), + }); + (useGetExplorerAuthoringCapabilityV2Query as Mock).mockReturnValue({ + data: undefined, + }); + (useCreateExplorerAuthoringMutation as Mock).mockReturnValue( + mutationResult(), + ); + (useDeleteExplorerAuthoringMutation as Mock).mockReturnValue( + mutationResult(), + ); + (useApplyExplorerBuilderCommandsV2Mutation as Mock).mockReturnValue( + mutationResult(), + ); + (useReconcileExplorerBuilderV2Mutation as Mock).mockReturnValue( + mutationResult(), + ); + (useGetExplorerCandidateSuggestionsV2Mutation as Mock).mockReturnValue( + mutationResult(), + ); + (usePreviewExplorerAuthoringV2Mutation as Mock).mockReturnValue( + mutationResult(), + ); + (usePublishExplorerAuthoringV2Mutation as Mock).mockReturnValue( + mutationResult(), + ); + }); + + it('requests the Explorer named by the route instead of default', () => { + render( + , + ); + + expect(useGetExplorerBuilderStateV2Query).toHaveBeenCalledWith({ + project: 'HTAN_INT/BForePC', + explorerId: 'test', + authResourcePath: '/programs/HTAN_INT/projects/BForePC', + }); + }); + + it('tracks a new Explorer when client-side navigation changes the query', async () => { + const view = render( + , + ); + + view.rerender( + , + ); + + await waitFor(() => + expect(useGetExplorerBuilderStateV2Query).toHaveBeenLastCalledWith({ + project: 'HTAN_INT/BForePC', + explorerId: 'second', + authResourcePath: '/programs/HTAN_INT/projects/BForePC', + }), + ); + }); + + it('waits for a controlled route update before requesting the selected Explorer', () => { + const onExplorerChange = vi.fn(); + const builderState = newBuilderState(); + (useGetExplorerAuthoringExplorersQuery as Mock).mockReturnValue({ + data: [ + { explorerId: 'first', title: 'First Explorer' }, + { explorerId: 'second', title: 'Second Explorer' }, + ], + isLoading: false, + refetch: vi.fn(), + }); + (useGetExplorerBuilderStateV2Query as Mock).mockReturnValue({ + data: builderState, + isLoading: false, + refetch: vi.fn(), + }); + (useGetExplorerBuilderStateV2Query as Mock).mockClear(); + + const view = render( + , + ); + + fireEvent.change(screen.getByRole('combobox', { name: 'Explorer' }), { + target: { value: 'second' }, + }); + + expect(onExplorerChange).toHaveBeenCalledWith('second'); + expect(useGetExplorerBuilderStateV2Query).not.toHaveBeenCalledWith({ + project: 'HTAN_INT/BForePC', + explorerId: 'second', + authResourcePath: '/programs/HTAN_INT/projects/BForePC', + }); + + view.rerender( + , + ); + + expect(useGetExplorerBuilderStateV2Query).toHaveBeenLastCalledWith({ + project: 'HTAN_INT/BForePC', + explorerId: 'second', + authResourcePath: '/programs/HTAN_INT/projects/BForePC', + }); + }); + + it('does not refetch explorers from a controlled create before the parent rerenders', async () => { + const onExplorerChange = vi.fn(); + const refetch = vi.fn(); + const createExplorer = vi.fn().mockReturnValue({ + unwrap: vi.fn().mockResolvedValue({ explorerId: 'second' }), + }); + (useGetExplorerAuthoringExplorersQuery as Mock).mockReturnValue({ + data: [{ explorerId: 'first', title: 'First Explorer' }], + isLoading: false, + refetch, + }); + (useGetExplorerBuilderStateV2Query as Mock).mockReturnValue({ + data: newBuilderState(), + isLoading: false, + refetch: vi.fn(), + }); + (useCreateExplorerAuthoringMutation as Mock).mockReturnValue([ + createExplorer, + { isLoading: false }, + ]); + (useGetExplorerBuilderStateV2Query as Mock).mockClear(); + + const view = render( + , + ); + + fireEvent.click(screen.getByText('New explorer')); + fireEvent.change(screen.getByLabelText('Explorer name'), { + target: { value: 'Second Explorer' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Create blank' })); + + await waitFor(() => { + expect(createExplorer).toHaveBeenCalledTimes(1); + expect(onExplorerChange).toHaveBeenCalledWith('second'); + }); + expect(refetch).not.toHaveBeenCalled(); + + view.rerender( + , + ); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx new file mode 100644 index 00000000..7fd2485f --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx @@ -0,0 +1,1147 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { + useApplyExplorerBuilderCommandsV2Mutation, + useCreateExplorerAuthoringMutation, + useDeleteExplorerAuthoringMutation, + useGetExplorerAuthoringCapabilityV2Query, + useGetExplorerAuthoringExplorersQuery, + useGetExplorerBuilderStateV2Query, + useGetExplorerCandidateSuggestionsV2Mutation, + usePreviewExplorerAuthoringV2Mutation, + usePublishExplorerAuthoringV2Mutation, + useReconcileExplorerBuilderV2Mutation, +} from '../../react'; +import type { ExplorerAuthoringApiError } from '../../api'; +import type { + ExplorerAuthoringDiagnostic, + ExplorerBuilderCatalog, + ExplorerBuilderCommand, + ExplorerBuilderCompileResult, + ExplorerBuilderState, +} from '../../types'; +import { BuilderToolbar } from './components/BuilderToolbar'; +import { GuidedGraphWorkspace } from './components/GuidedGraphWorkspace'; +import { ColumnSelector } from './components/ColumnSelector'; +import { PreviewTable } from './components/PreviewTable'; +import { + derivedOccurrences, + intentFingerprint, + routeNode, + routeSubtreeOccurrenceIds, + selectedTable, + stateFromBuilder, + workspaceFromState, + type BuilderAuthoringState, +} from './authoring/model'; +import { builderAuthoringReducer } from './authoring/reducer'; +import { + isPreviewResponseSizeError, + previewRecoveryAction, + type PreviewLimit, +} from './authoring/previewRecovery'; +import { useDirtyBeforeUnload } from './hooks/useDirtyBeforeUnload'; +import { usePortalHost } from './hooks/usePortalHost'; + +const emptyCatalog = (): ExplorerBuilderCatalog => ({ + snapshotToken: '', + generation: '', + routePolicy: {}, + nodes: [], + edges: [], + candidates: [], +}); +const emptyBuilderState = (project: string): BuilderAuthoringState => ({ + project, + explorerId: 'default', + catalog: emptyCatalog(), + workspace: null, + draftVersion: 0, + draftDigest: '', + tables: [], + selectedOccurrenceId: 'base', + diagnostics: [], + dirty: false, + reconciliation: 'idle', +}); + +const diagnosticsFromError = ( + error: unknown, +): ReadonlyArray => { + const value = error as ExplorerAuthoringApiError | undefined; + if (value?.diagnostics?.length) return value.diagnostics; + return [ + { + severity: 'error', + code: value?.code ?? 'EXPLORER_AUTHORING_FAILED', + message: value?.message ?? 'Loom could not process the Builder request.', + requestId: value?.requestId, + }, + ]; +}; +const isStaleSnapshot = (code: string | undefined) => + [ + 'STALE_CATALOG_SNAPSHOT', + 'STALE_SNAPSHOT', + 'SNAPSHOT_STALE', + 'STALE_RECEIPT', + 'RECEIPT_STALE', + 'COMPILE_RECEIPT_NOT_FOUND', + 'RECEIPT_RECOMPILE_REQUIRED', + ].includes(code ?? ''); +const isDraftDesynchronized = (code: string | undefined) => + ['DRAFT_CONFLICT', 'INVALID_EXPLORER_COMMAND_RESULT'].includes(code ?? ''); +const opaqueId = (prefix: 'output' | 'tab' | 'step') => + `${prefix}-${window.crypto.randomUUID()}`; + +type PreviewRequest = { + readonly outputId: string; + readonly limit: PreviewLimit; + readonly receiptRefreshes: number; +}; + +const builderDataKeyFor = ( + explorerId: string, + value: { + readonly catalog: ExplorerBuilderCatalog; + readonly workspace: unknown; + readonly draftVersion: number; + readonly draftDigest: string; + }, +) => + `${explorerId}:${value.catalog.snapshotToken}:${value.draftVersion}:${value.draftDigest}:${JSON.stringify(value.workspace)}`; + +const BuilderWorkspaceContent = ({ + organization, + project, + explorerId, + onExplorerChange, +}: { + readonly organization?: string; + readonly project: string; + readonly explorerId?: string; + readonly onExplorerChange?: (explorerId: string) => void; +}) => { + const projectId = organization ? `${organization}/${project}` : project; + const authResourcePath = organization + ? `/programs/${organization}/projects/${project}` + : undefined; + const requestedExplorerId = explorerId || 'default'; + const [selectedExplorerId, setSelectedExplorerId] = + useState(requestedExplorerId); + const explorers = useGetExplorerAuthoringExplorersQuery({ + project: projectId, + authResourcePath, + }); + const builder = useGetExplorerBuilderStateV2Query({ + project: projectId, + explorerId: selectedExplorerId, + authResourcePath, + }); + const refetchBuilder = builder.refetch; + const capabilities = useGetExplorerAuthoringCapabilityV2Query({ + project: projectId, + explorerId: selectedExplorerId, + authResourcePath, + }); + const [createExplorer, createStatus] = useCreateExplorerAuthoringMutation(); + const [deleteExplorer, deleteStatus] = useDeleteExplorerAuthoringMutation(); + const [applyBuilderCommands] = useApplyExplorerBuilderCommandsV2Mutation(); + const [reconcileBuilder, reconcileStatus] = + useReconcileExplorerBuilderV2Mutation(); + const [getSuggestions, suggestionsStatus] = + useGetExplorerCandidateSuggestionsV2Mutation(); + const [previewBuilder, previewStatus] = + usePreviewExplorerAuthoringV2Mutation(); + const [publishBuilder] = usePublishExplorerAuthoringV2Mutation(); + const builderDataKey = builder.data + ? builderDataKeyFor(selectedExplorerId, builder.data) + : ''; + const builderDataRef = useRef(builder.data); + builderDataRef.current = builder.data; + const [localState, setLocalState] = useState<{ + readonly key: string; + readonly value: BuilderAuthoringState; + }>(); + const state = + localState?.key === builderDataKey + ? localState.value + : builder.data + ? stateFromBuilder(builder.data, { + project: projectId, + explorerId: selectedExplorerId, + }) + : emptyBuilderState(projectId); + const dispatch = useCallback( + (action: Parameters[1]) => { + setLocalState((current) => { + const base = + current?.key === builderDataKey + ? current.value + : builderDataRef.current + ? stateFromBuilder(builderDataRef.current, { + project: projectId, + explorerId: selectedExplorerId, + }) + : emptyBuilderState(projectId); + return { + key: builderDataKey, + value: builderAuthoringReducer(base, action), + }; + }); + }, + [builderDataKey, projectId, selectedExplorerId], + ); + const [message, setMessage] = useState(); + const [lastPublished, setLastPublished] = useState<{ + readonly explorerId: string; + readonly draftDigest: string; + }>(); + const [pendingCommands, setPendingCommands] = useState(0); + const [publishing, setPublishing] = useState(false); + const [firstTableName, setFirstTableName] = useState(''); + const [previewLimit, setPreviewLimit] = useState(25); + const toolbarHost = usePortalHost('explorer-builder-toolbar-host'); + const [tableToolbarHost, setTableToolbarHost] = useState( + null, + ); + const compileGeneration = useRef(0); + const activeCompile = useRef<{ abort: () => void } | undefined>(undefined); + const commandQueue = useRef>(Promise.resolve()); + const serverDraft = useRef({ version: 0, digest: '' }); + const suggestionRequestKey = useRef(''); + const latestState = useRef(state); + latestState.current = state; + + const serverDraftKey = useRef(''); + if (builder.data && serverDraftKey.current !== builderDataKey) { + serverDraft.current = { + version: builder.data.draftVersion, + digest: builder.data.draftDigest, + }; + serverDraftKey.current = builderDataKey; + } + + const selectExplorer = (nextExplorerId: string) => { + compileGeneration.current += 1; + activeCompile.current?.abort(); + if (onExplorerChange) { + onExplorerChange(nextExplorerId); + } else { + setSelectedExplorerId(nextExplorerId); + } + }; + + const syncBuilderData = useCallback( + (value: ExplorerBuilderState, mode: 'hydrate' | 'catalog') => { + const nextKey = builderDataKeyFor(selectedExplorerId, value); + serverDraft.current = { + version: value.draftVersion, + digest: value.draftDigest, + }; + serverDraftKey.current = nextKey; + setLocalState((current) => { + const nextValue = + mode === 'hydrate' + ? stateFromBuilder(value, { + project: projectId, + explorerId: selectedExplorerId, + }) + : builderAuthoringReducer( + current?.key === builderDataKey + ? current.value + : latestState.current, + { type: 'catalogRefreshed', catalog: value.catalog }, + ); + return { key: nextKey, value: nextValue }; + }); + }, + [builderDataKey, projectId, selectedExplorerId], + ); + + const incomplete = state.tables.some( + (table) => !table.document.rootResourceType, + ); + + const applyCommands = useCallback( + (commands: ReadonlyArray) => { + compileGeneration.current += 1; + activeCompile.current?.abort(); + setPendingCommands((value) => value + 1); + const run = commandQueue.current.then(async () => { + const current = latestState.current; + const commandId = window.crypto.randomUUID(); + try { + const value = await applyBuilderCommands({ + project: projectId, + explorerId: current.explorerId, + authResourcePath, + commandId, + snapshotToken: current.catalog.snapshotToken, + expectedDraftVersion: serverDraft.current.version, + expectedDraftDigest: serverDraft.current.digest || undefined, + commands, + requestId: `builder-command-${commandId}`, + }).unwrap(); + serverDraft.current = { + version: value.draftVersion, + digest: value.draftDigest, + }; + serverDraftKey.current = builderDataKey; + dispatch({ type: 'commandsApplied', value }); + setMessage(undefined); + } catch (error) { + const apiError = error as ExplorerAuthoringApiError; + if ( + isDraftDesynchronized(apiError.code) || + isStaleSnapshot(apiError.code) + ) { + const refreshed = await refetchBuilder(); + if (refreshed.data) { + syncBuilderData(refreshed.data, 'hydrate'); + setMessage(undefined); + } else if (apiError.code !== 'CLIENT_CANCELLED') { + dispatch({ + type: 'repair', + diagnostics: diagnosticsFromError(apiError), + }); + } + return; + } + if (apiError.code !== 'CLIENT_CANCELLED') { + dispatch({ + type: 'repair', + diagnostics: diagnosticsFromError(apiError), + }); + } + } finally { + setPendingCommands((value) => Math.max(0, value - 1)); + } + }); + commandQueue.current = run.catch(() => undefined); + return run; + }, + [ + applyBuilderCommands, + authResourcePath, + builderDataKey, + dispatch, + projectId, + refetchBuilder, + syncBuilderData, + ], + ); + + useDirtyBeforeUnload(state.dirty); + + const table = selectedTable(state); + const occurrences = useMemo( + () => derivedOccurrences(table, state.catalog), + [state.catalog, table], + ); + const occurrence = occurrences.find( + (candidate) => candidate.id === state.selectedOccurrenceId, + ); + + const ensureSuggestions = useCallback(() => { + const current = latestState.current; + const currentOccurrence = derivedOccurrences( + selectedTable(current), + current.catalog, + ).find((candidate) => candidate.id === current.selectedOccurrenceId); + if (!currentOccurrence || !current.catalog.snapshotToken) return; + const key = `${current.explorerId}:${current.catalog.snapshotToken}:${currentOccurrence.id}`; + if (suggestionRequestKey.current === key) return; + suggestionRequestKey.current = key; + if ( + (current.catalog.candidates ?? []).some( + (candidate) => candidate.nodeId === currentOccurrence.nodeId, + ) + ) + return; + const request = getSuggestions({ + project: projectId, + explorerId: current.explorerId, + authResourcePath, + snapshotToken: current.catalog.snapshotToken, + nodeId: currentOccurrence.nodeId, + requestId: `suggestions-${currentOccurrence.id}`, + }); + void request + .unwrap() + .then((value) => { + const latest = latestState.current; + if ( + value.snapshotToken === latest.catalog.snapshotToken && + latest.selectedOccurrenceId === currentOccurrence.id + ) { + dispatch({ type: 'candidatesLoaded', candidates: value.candidates }); + } + }) + .catch((error: ExplorerAuthoringApiError) => { + if (error.code !== 'CLIENT_CANCELLED') { + const suffix = error.code ? ` (${error.code})` : ''; + setMessage( + `Available columns could not be loaded: ${error.message}${suffix}`, + ); + } + }); + }, [authResourcePath, dispatch, getSuggestions, projectId]); + const suggestionHostRef = useCallback( + (host: HTMLSpanElement | null) => { + if (host) ensureSuggestions(); + }, + [ensureSuggestions], + ); + const suggestionIdentity = `${state.explorerId}:${state.catalog.snapshotToken}:${state.selectedOccurrenceId}`; + const busy = + pendingCommands > 0 || + reconcileStatus.isLoading || + previewStatus.isLoading || + publishing || + createStatus.isLoading || + deleteStatus.isLoading; + const blockingDiagnostics = state.diagnostics.some( + (diagnostic) => diagnostic.severity === 'error', + ); + const hasVisibleSelectedColumn = Boolean( + table?.document.columns.some( + (column) => column.table?.visible ?? Boolean(column.table), + ), + ); + const previewDisabled = + !table?.document.rootResourceType || + !hasVisibleSelectedColumn || + blockingDiagnostics; + const publishDisabled = + (lastPublished?.explorerId === state.explorerId && + lastPublished.draftDigest === state.draftDigest) || + incomplete || + blockingDiagnostics || + state.tables.some((candidate) => candidate.document.columns.length === 0); + + const addTableNamed = (value: string) => { + const title = value.trim(); + if (!title) return; + const outputId = opaqueId('output'); + dispatch({ + type: 'addTable', + table: { + outputId, + tabId: opaqueId('tab'), + title, + document: { + kind: 'ExplorerBuilderDocument', + output: { id: outputId, title }, + rootResourceType: '', + route: { occurrenceId: 'base', resourceType: '' }, + columns: [], + }, + }, + }); + }; + const addTable = () => { + const title = window.prompt('Table name')?.trim(); + if (title) addTableNamed(title); + }; + const duplicateTable = () => { + if (!table) return; + void applyCommands([ + { + type: 'DUPLICATE_TABLE', + sourceOutputId: table.outputId, + title: `${table.title} copy`, + }, + ]); + }; + const createCustomExplorer = async (title: string, fromCurrent: boolean) => { + try { + const created = await createExplorer({ + project: projectId, + authResourcePath, + name: title, + title, + sourceExplorerId: fromCurrent ? state.explorerId : undefined, + }).unwrap(); + selectExplorer(created.explorerId); + if (!onExplorerChange) void explorers.refetch(); + setMessage(undefined); + } catch (error) { + const apiError = error as ExplorerAuthoringApiError; + const suffix = apiError.code ? ` (${apiError.code})` : ''; + setMessage(`Explorer creation failed: ${apiError.message}${suffix}`); + } + }; + const deleteCurrentExplorer = async () => { + if (!capabilities.data?.features.deleteExplorer) return; + const current = (explorers.data ?? []).find( + (explorer) => explorer.explorerId === selectedExplorerId, + ); + const fallback = + (explorers.data ?? []).find( + (explorer) => + explorer.explorerId !== selectedExplorerId && + explorer.explorerId === 'default', + ) ?? + (explorers.data ?? []).find( + (explorer) => explorer.explorerId !== selectedExplorerId, + ); + if (!current || !fallback) return; + if ( + !window.confirm( + `Delete Explorer "${current.title}"? This permanently removes its configuration and cannot be undone.`, + ) + ) + return; + try { + await deleteExplorer({ + project: projectId, + explorerId: selectedExplorerId, + authResourcePath, + requestId: `delete-explorer-${selectedExplorerId}`, + }).unwrap(); + compileGeneration.current += 1; + activeCompile.current?.abort(); + selectExplorer(fallback.explorerId); + if (!onExplorerChange) await explorers.refetch(); + setMessage(undefined); + } catch (error) { + const apiError = error as ExplorerAuthoringApiError; + const suffix = apiError.code ? ` (${apiError.code})` : ''; + setMessage(`Explorer deletion failed: ${apiError.message}${suffix}`); + } + }; + const reconcileCurrent = useCallback(async (): Promise< + ExplorerBuilderCompileResult | undefined + > => { + const submitted = latestState.current; + if ( + !submitted.catalog.snapshotToken || + submitted.tables.length === 0 || + submitted.tables.some((candidate) => !candidate.document.rootResourceType) + ) + return undefined; + const generation = ++compileGeneration.current; + const submittedFingerprint = intentFingerprint( + workspaceFromState(submitted), + ); + let snapshotToken = submitted.catalog.snapshotToken; + let draftVersion = submitted.draftVersion; + let draftDigest = submitted.draftDigest; + let attempt = 1; + dispatch({ type: 'compiling' }); + for (;;) { + const request = reconcileBuilder({ + project: projectId, + explorerId: submitted.explorerId, + authResourcePath, + snapshotToken, + draftVersion, + draftDigest, + requestId: `builder-${generation}-${attempt}`, + }); + activeCompile.current = request; + try { + const value = await request.unwrap(); + if (generation !== compileGeneration.current) return undefined; + const current = latestState.current; + if ( + current.explorerId !== submitted.explorerId || + intentFingerprint(workspaceFromState(current)) !== + submittedFingerprint + ) + return undefined; + dispatch({ type: 'compiled', value }); + return value.diagnostics.some( + (diagnostic) => diagnostic.severity === 'error', + ) + ? undefined + : value; + } catch (error) { + const apiError = error as ExplorerAuthoringApiError; + if ( + generation !== compileGeneration.current || + apiError.code === 'CLIENT_CANCELLED' + ) + return undefined; + if (isDraftDesynchronized(apiError.code)) { + const refreshed = await refetchBuilder(); + if (refreshed.data) { + syncBuilderData(refreshed.data, 'hydrate'); + setMessage(undefined); + } else { + dispatch({ + type: 'repair', + diagnostics: diagnosticsFromError(apiError), + }); + } + return undefined; + } + if (isStaleSnapshot(apiError.code)) { + const refreshed = await refetchBuilder(); + if (!refreshed.data) return undefined; + syncBuilderData(refreshed.data, 'catalog'); + snapshotToken = refreshed.data.catalog.snapshotToken; + draftVersion = refreshed.data.draftVersion; + draftDigest = refreshed.data.draftDigest; + attempt += 1; + continue; + } + if (apiError.retryable && attempt < 3) { + attempt += 1; + await new Promise((resolve) => window.setTimeout(resolve, 250)); + continue; + } + dispatch({ + type: 'repair', + diagnostics: diagnosticsFromError(apiError), + }); + return undefined; + } finally { + if (generation === compileGeneration.current) + activeCompile.current = undefined; + } + } + }, [ + authResourcePath, + dispatch, + projectId, + reconcileBuilder, + refetchBuilder, + syncBuilderData, + ]); + const executePreview = useCallback( + async (request: PreviewRequest, receiptId: string) => { + let activeRequest = request; + let activeReceiptId = receiptId; + const limit = request.limit; + let transientRetries = 0; + for (;;) { + try { + const value = await previewBuilder({ + project: projectId, + explorerId: latestState.current.explorerId, + authResourcePath, + receiptId: activeReceiptId, + outputId: activeRequest.outputId, + limit, + }).unwrap(); + if (value.receiptId !== activeReceiptId) return; + dispatch({ type: 'preview', value }); + setMessage(undefined); + return; + } catch (error) { + const apiError = error as ExplorerAuthoringApiError; + if (apiError.code === 'CLIENT_CANCELLED') return; + const recovery = previewRecoveryAction(apiError, { + receiptRefreshes: activeRequest.receiptRefreshes, + transientRetries, + limit, + }); + if (recovery === 'retry') { + transientRetries += 1; + continue; + } + if (recovery === 'recompile') { + activeRequest = { + ...activeRequest, + limit, + receiptRefreshes: activeRequest.receiptRefreshes + 1, + }; + const receipt = await reconcileCurrent(); + if (!receipt) return; + activeReceiptId = receipt.receiptId; + setMessage(undefined); + continue; + } + if (recovery === 'refresh-catalog') { + activeRequest = { + ...activeRequest, + limit, + receiptRefreshes: activeRequest.receiptRefreshes + 1, + }; + const refreshed = await refetchBuilder(); + if (!refreshed.data) return; + latestState.current = builderAuthoringReducer(latestState.current, { + type: 'catalogRefreshed', + catalog: refreshed.data.catalog, + }); + syncBuilderData(refreshed.data, 'catalog'); + const receipt = await reconcileCurrent(); + if (!receipt) return; + activeReceiptId = receipt.receiptId; + setMessage(undefined); + continue; + } + if (isPreviewResponseSizeError(apiError.code)) { + setMessage( + `Loom could not return ${limit} preview rows at the current table width. Choose fewer rows or hide some table columns.`, + ); + } else if ( + ['PLAN_TOO_EXPENSIVE', 'EXPENSIVE_PLAN'].includes( + apiError.code ?? '', + ) + ) { + setMessage( + 'This plan is too expensive to preview. Remove columns or shorten the route.', + ); + } else { + const suffix = apiError.code ? ` (${apiError.code})` : ''; + setMessage(`Preview failed: ${apiError.message}${suffix}`); + } + return; + } + } + }, + [ + authResourcePath, + dispatch, + previewBuilder, + projectId, + reconcileCurrent, + refetchBuilder, + syncBuilderData, + ], + ); + + const preview = async (limit: PreviewLimit = previewLimit) => { + if (!table || previewDisabled) return; + const request = { + outputId: table.outputId, + limit, + receiptRefreshes: 0, + }; + const receipt = + state.receipt && state.reconciliation === 'resolved' + ? state.receipt + : await reconcileCurrent(); + if (receipt) await executePreview(request, receipt.receiptId); + }; + const executePublish = useCallback( + async (receiptId: string) => { + let activeReceiptId = receiptId; + let refreshes = 0; + for (;;) { + try { + await publishBuilder({ + project: projectId, + explorerId: latestState.current.explorerId, + authResourcePath, + receiptId: activeReceiptId, + }).unwrap(); + setLastPublished({ + explorerId: latestState.current.explorerId, + draftDigest: latestState.current.draftDigest, + }); + dispatch({ type: 'published' }); + setMessage(undefined); + return; + } catch (error) { + const apiError = error as ExplorerAuthoringApiError; + if (isStaleSnapshot(apiError.code) && refreshes === 0) { + refreshes += 1; + const refreshed = await refetchBuilder(); + if (!refreshed.data) return; + latestState.current = builderAuthoringReducer(latestState.current, { + type: 'catalogRefreshed', + catalog: refreshed.data.catalog, + }); + syncBuilderData(refreshed.data, 'catalog'); + const receipt = await reconcileCurrent(); + if (!receipt) return; + activeReceiptId = receipt.receiptId; + continue; + } + const suffix = apiError.code ? ` (${apiError.code})` : ''; + setMessage( + `Publication failed; the previously active Viewer revision remains available: ${apiError.message}${suffix}`, + ); + return; + } + } + }, + [ + authResourcePath, + dispatch, + projectId, + publishBuilder, + reconcileCurrent, + refetchBuilder, + syncBuilderData, + ], + ); + + const publish = async () => { + if (publishDisabled || publishing) return; + setPublishing(true); + try { + const receipt = + state.receipt && state.reconciliation === 'resolved' + ? state.receipt + : await reconcileCurrent(); + if (!receipt) return; + const publishable = state.tables.every((candidate) => + receipt.outputs.some( + (output) => + output.outputId === candidate.outputId && output.columns.length > 0, + ), + ); + if (!publishable) { + setMessage( + 'Every table needs at least one output column before publishing.', + ); + return; + } + await executePublish(receipt.receiptId); + } finally { + setPublishing(false); + } + }; + + if (explorers.isLoading || builder.isLoading) { + return ( +
+ Loading the selected Explorer configuration… +
+ ); + } + if (explorers.error || !builder.data) { + return ( +
+ Loom’s V2 Builder state could not be loaded. This Builder has no V1 + fallback. +
+ ); + } + + const toolbar = ( + + void createCustomExplorer(title, fromCurrent) + } + deleteSupported={capabilities.data?.features.deleteExplorer ?? false} + deleteDisabled={ + !(explorers.data ?? []).some( + (explorer) => explorer.explorerId !== selectedExplorerId, + ) + } + onDeleteExplorer={() => void deleteCurrentExplorer()} + tables={state.tables} + selectedOutputId={state.selectedOutputId} + onSelectTable={(outputId) => dispatch({ type: 'selectTable', outputId })} + onRenameTable={(outputId, title) => { + const target = state.tables.find( + (candidate) => candidate.outputId === outputId, + ); + if (!target?.document.rootResourceType) { + dispatch({ type: 'renameTable', outputId, title }); + return; + } + void applyCommands([{ type: 'RENAME_TABLE', outputId, title }]); + }} + onNewTable={addTable} + onDuplicateTable={duplicateTable} + onDeleteTable={() => + table && + window.confirm(`Delete ${table.title}?`) && + (table.document.rootResourceType + ? void applyCommands([ + { type: 'DELETE_TABLE', outputId: table.outputId }, + ]) + : dispatch({ type: 'removeTable', outputId: table.outputId })) + } + onReorderTable={(outputId, before) => { + const moving = state.tables.find( + (candidate) => candidate.outputId === outputId, + ); + if (!moving?.document.rootResourceType || incomplete) { + dispatch({ type: 'reorderTable', outputId, before }); + return; + } + const outputIds = state.tables + .filter((candidate) => candidate.outputId !== outputId) + .map((candidate) => candidate.outputId); + const index = before ? outputIds.indexOf(before) : outputIds.length; + outputIds.splice(index < 0 ? outputIds.length : index, 0, outputId); + void applyCommands([{ type: 'REORDER_TABLES', outputIds }]); + }} + onPreview={() => void preview()} + onPublish={() => void publish()} + previewDisabled={previewDisabled} + publishDisabled={publishDisabled} + publishing={publishing} + busy={busy} + columnCreationSupported + tableToolbarHost={tableToolbarHost} + /> + ); + + return ( +
+ {toolbarHost ? createPortal(toolbar, toolbarHost) : toolbar} +
+ {(message || + blockingDiagnostics || + state.reconciliation === 'stale') && ( +
+
+ {blockingDiagnostics + ? 'Builder needs attention' + : state.reconciliation === 'stale' + ? 'Catalog or receipt changed' + : 'Builder needs attention'} +
+

{state.diagnostics[0]?.message ?? message}

+ {state.diagnostics[0]?.code ? ( +

+ Technical details · Code: {state.diagnostics[0].code} +

+ ) : null} + {(state.reconciliation === 'stale' || + state.reconciliation === 'repair') && + state.tables.length > 0 && + !incomplete ? ( + + ) : null} +
+ )} + {state.tables.length === 0 ? ( +
+

+ Create your first table +

+

+ Name the table, choose its starting resource in the dataset graph, + then select the columns you want to publish. +

+
{ + event.preventDefault(); + if (!firstTableName.trim()) return; + addTableNamed(firstTableName); + setFirstTableName(''); + }} + > + + + setFirstTableName(event.currentTarget.value) + } + placeholder="Table name, e.g. Patients" + autoFocus + className="min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm outline-blue-500 focus:border-blue-500" + /> + +
+
+ ) : ( + <> +
+
+ ); +}; + +const BuilderWorkspace = ( + props: React.ComponentProps, +) => ; + +export default BuilderWorkspace; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts new file mode 100644 index 00000000..ddc8b67e --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts @@ -0,0 +1,330 @@ +import type { + ExplorerAuthoringDiagnostic, + ExplorerBuilderCatalog, + ExplorerBuilderCompileResult, + ExplorerBuilderCommandsResult, + ExplorerBuilderDocument, + ExplorerBuilderPreviewResult, + ExplorerBuilderState, + ExplorerBuilderWorkspace, +} from '../../../types'; + +export interface DraftTable { + readonly outputId: string; + readonly tabId: string; + readonly title: string; + readonly visible?: boolean; + /** The V2 authoring document is the editable source of truth. */ + readonly document: ExplorerBuilderDocument; +} + +export interface DerivedOccurrence { + readonly id: string; + readonly index: number; + readonly nodeId: string; + readonly incomingEdgeId?: string; + readonly relationship?: string; + readonly parentId?: string; + readonly depth: number; + readonly resourceType: string; +} + +export interface BuilderAuthoringState { + readonly project: string; + readonly explorerId: string; + readonly catalog: ExplorerBuilderCatalog; + /** Canonical server workspace; typed source bindings are intentionally read-only. */ + readonly workspace: ExplorerBuilderWorkspace | null; + readonly draftVersion: number; + readonly draftDigest: string; + readonly tables: ReadonlyArray; + readonly selectedOutputId?: string; + readonly selectedOccurrenceId: string; + readonly receipt?: ExplorerBuilderCompileResult; + readonly diagnostics: ReadonlyArray; + readonly dirty: boolean; + readonly reconciliation: 'idle' | 'pending' | 'resolved' | 'stale' | 'repair'; + readonly preview?: ExplorerBuilderPreviewResult; +} + +export const derivedOccurrences = ( + table: DraftTable | undefined, + catalog: ExplorerBuilderCatalog, +): ReadonlyArray => { + if (!table) return []; + const occurrences: DerivedOccurrence[] = []; + const walk = ( + route: ExplorerBuilderDocument['route'], + parentNodeId: string | undefined, + parentId: string | undefined, + depth: number, + ) => { + const node = catalog.nodes.find( + (candidate) => candidate.resourceType === route.resourceType, + ); + if (!node) return; + const edge = parentNodeId + ? catalog.edges.find( + (candidate) => + candidate.fromNodeId === parentNodeId && + candidate.toNodeId === node.nodeId && + candidate.label === route.relationship, + ) + : undefined; + if (parentNodeId && !edge) return; + occurrences.push({ + id: route.occurrenceId, + index: occurrences.length, + nodeId: node.nodeId, + incomingEdgeId: edge?.edgeId, + relationship: route.relationship, + parentId, + depth, + resourceType: route.resourceType, + }); + route.children?.forEach((child) => + walk(child, node.nodeId, route.occurrenceId, depth + 1), + ); + }; + walk(table.document.route, undefined, undefined, 0); + return occurrences; +}; + +export const routeNode = ( + route: ExplorerBuilderDocument['route'], + occurrenceId: string, +): ExplorerBuilderDocument['route'] | undefined => { + if (route.occurrenceId === occurrenceId) return route; + for (const child of route.children ?? []) { + const found = routeNode(child, occurrenceId); + if (found) return found; + } + return undefined; +}; + +export const routeSubtreeOccurrenceIds = ( + route: ExplorerBuilderDocument['route'], +): ReadonlySet => { + const values = new Set(); + const walk = (node: ExplorerBuilderDocument['route']) => { + values.add(node.occurrenceId); + node.children?.forEach(walk); + }; + walk(route); + return values; +}; + +export const replaceRouteNode = ( + route: ExplorerBuilderDocument['route'], + occurrenceId: string, + update: ( + node: ExplorerBuilderDocument['route'], + ) => ExplorerBuilderDocument['route'] | undefined, +): ExplorerBuilderDocument['route'] | undefined => { + if (route.occurrenceId === occurrenceId) return update(route); + const children = (route.children ?? []).flatMap((child) => { + const next = replaceRouteNode(child, occurrenceId, update); + return next ? [next] : []; + }); + return { ...route, children: children.length ? children : undefined }; +}; + +const routeIdPart = (value: string): string => + value + .replace(/([a-z0-9])([A-Z])/g, '$1_$2') + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/^_+|_+$/g, '') || 'resource'; + +export const nextRouteOccurrenceId = ( + route: ExplorerBuilderDocument['route'], + parentOccurrenceId: string, + resourceType: string, + relationship: string, +): string => { + const used = routeSubtreeOccurrenceIds(route); + const resource = routeIdPart(resourceType); + const prefix = parentOccurrenceId === 'base' ? '' : `${parentOccurrenceId}__`; + let base = `${prefix}${resource}`; + if (used.has(base)) base = `${base}__${routeIdPart(relationship)}`; + let value = base; + let suffix = 2; + while (used.has(value)) value = `${base}_${suffix++}`; + return value; +}; + +const tableFromDocument = ( + document: ExplorerBuilderDocument, + tab: ExplorerBuilderWorkspace['tabs'][number] | undefined, +): DraftTable => ({ + outputId: document.output.id, + tabId: tab?.id ?? document.output.id, + title: tab?.title ?? document.output.title ?? document.output.id, + visible: tab?.visible, + document, +}); + +const tablesFromWorkspace = ( + workspace: ExplorerBuilderWorkspace | null, +): ReadonlyArray => { + if (!workspace) return []; + const documents = new Map( + workspace.documents.map((document) => [document.output.id, document]), + ); + const tables = [...workspace.tabs] + .sort((left, right) => left.order - right.order) + .flatMap((tab) => { + const document = documents.get(tab.outputId); + return document ? [tableFromDocument(document, tab)] : []; + }); + workspace.documents.forEach((document) => { + if (!tables.some((table) => table.outputId === document.output.id)) { + tables.push(tableFromDocument(document, undefined)); + } + }); + return tables; +}; + +export const stateFromBuilder = ( + value: ExplorerBuilderState, + identity: { readonly project: string; readonly explorerId: string }, +): BuilderAuthoringState => { + if (value.lifecycleState === 'READY' && !value.workspace) { + throw new Error('Loom violated the READY authoring-state invariant.'); + } + const tables = tablesFromWorkspace(value.workspace); + return { + ...identity, + catalog: value.catalog, + workspace: value.workspace, + draftVersion: value.draftVersion, + draftDigest: value.draftDigest, + tables, + selectedOutputId: tables[0]?.outputId, + selectedOccurrenceId: 'base', + diagnostics: [], + dirty: false, + reconciliation: 'idle', + }; +}; + +export const stateFromCommands = ( + state: BuilderAuthoringState, + value: ExplorerBuilderCommandsResult, +): BuilderAuthoringState => { + const normalized = stateFromBuilder( + { + apiVersion: 'loom.calypr.org/explorer-authoring/v2', + kind: 'ExplorerBuilderState', + lifecycleState: 'READY', + draftVersion: value.draftVersion, + draftDigest: value.draftDigest, + workspace: value.workspace, + catalog: state.catalog, + }, + { project: state.project, explorerId: state.explorerId }, + ); + const result = value.results.at(-1); + const selectedOutputId = normalized.tables.some( + (table) => table.outputId === result?.outputId, + ) + ? result?.outputId + : normalized.tables.some( + (table) => table.outputId === state.selectedOutputId, + ) + ? state.selectedOutputId + : normalized.tables[0]?.outputId; + const requestedOccurrenceId = + result?.occurrenceId ?? + (selectedOutputId === state.selectedOutputId + ? state.selectedOccurrenceId + : 'base'); + const selectedTable = normalized.tables.find( + (table) => table.outputId === selectedOutputId, + ); + const selectedOccurrenceId = + selectedTable && + routeNode(selectedTable.document.route, requestedOccurrenceId) + ? requestedOccurrenceId + : 'base'; + return { + ...normalized, + selectedOutputId, + selectedOccurrenceId, + diagnostics: value.diagnostics, + dirty: true, + reconciliation: 'idle', + }; +}; + +export const completeDocument = ( + table: DraftTable, +): ExplorerBuilderDocument | undefined => { + const title = table.title.trim() || table.document.output.title; + return { + ...table.document, + output: { + ...table.document.output, + id: table.outputId, + title, + }, + }; +}; + +export const workspaceFromState = ( + state: BuilderAuthoringState, +): ExplorerBuilderWorkspace => { + const complete = state.tables.flatMap((table) => { + const document = completeDocument(table); + return document ? [{ table, document }] : []; + }); + const validColumns = new Map( + complete.map(({ document }) => [ + document.output.id, + new Set(document.columns.map((column) => column.column)), + ]), + ); + const sharedFilterEntries = Object.entries( + state.workspace?.sharedFilters ?? {}, + ).flatMap(([name, bindings]) => { + const retained = bindings.filter((binding) => + validColumns.get(binding.outputId)?.has(binding.column), + ); + return retained.length ? [[name, retained] as const] : []; + }); + return { + ...state.workspace, + apiVersion: 'loom.calypr.org/explorer-authoring/v2', + kind: 'ExplorerBuilderWorkspace', + explorer: state.workspace?.explorer ?? { title: state.explorerId }, + documents: complete.map(({ document }) => document), + tabs: complete.map(({ table, document }, order) => ({ + id: table.tabId, + title: document.output.title, + outputId: table.outputId, + order, + visible: table.visible ?? true, + })), + sharedFilters: sharedFilterEntries.length + ? Object.fromEntries(sharedFilterEntries) + : undefined, + }; +}; + +const canonicalize = (value: unknown): unknown => { + if (Array.isArray(value)) return value.map(canonicalize); + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => [key, canonicalize(item)]), + ); + } + return value; +}; +export const intentFingerprint = ( + workspace: ExplorerBuilderWorkspace, +): string => JSON.stringify(canonicalize(workspace)); + +export const selectedTable = (state: BuilderAuthoringState) => + state.tables.find((table) => table.outputId === state.selectedOutputId); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.ts new file mode 100644 index 00000000..16497a18 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.ts @@ -0,0 +1,46 @@ +import type { ExplorerAuthoringApiError } from '../../../api'; + +export type PreviewLimit = 25 | 50 | 100 | 500 | 1000; +export type PreviewRecoveryAction = + | 'recompile' + | 'refresh-catalog' + | 'retry' + | 'fail'; + +const receiptRefreshCodes = new Set([ + 'RECEIPT_RECOMPILE_REQUIRED', + 'COMPILE_RECEIPT_NOT_FOUND', +]); + +const catalogRefreshCodes = new Set([ + 'STALE_CATALOG_SNAPSHOT', + 'STALE_SNAPSHOT', + 'SNAPSHOT_STALE', + 'STALE_RECEIPT', + 'RECEIPT_STALE', +]); + +const responseSizeCodes = new Set([ + 'PREVIEW_TOO_LARGE', + 'PREVIEW_RESPONSE_TOO_LARGE', + 'RESPONSE_TOO_LARGE', +]); + +export const isPreviewResponseSizeError = (code: string | undefined): boolean => + responseSizeCodes.has(code ?? ''); + +export const previewRecoveryAction = ( + error: ExplorerAuthoringApiError, + options: { + readonly receiptRefreshes: number; + readonly transientRetries: number; + readonly limit: PreviewLimit; + }, +): PreviewRecoveryAction => { + if (receiptRefreshCodes.has(error.code ?? '') && options.receiptRefreshes < 1) + return 'recompile'; + if (catalogRefreshCodes.has(error.code ?? '') && options.receiptRefreshes < 1) + return 'refresh-catalog'; + if (error.retryable && options.transientRetries < 1) return 'retry'; + return 'fail'; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.unit.test.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.unit.test.ts new file mode 100644 index 00000000..9669c137 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.unit.test.ts @@ -0,0 +1,91 @@ +import type { ExplorerAuthoringApiError } from '../../../api'; +import { + isPreviewResponseSizeError, + previewRecoveryAction, +} from './previewRecovery'; + +const failure = ( + code: string, + retryable = false, +): ExplorerAuthoringApiError => ({ + status: 409, + code, + message: code, + retryable, +}); + +describe('Builder preview recovery', () => { + it('recompiles a stale deterministic receipt exactly once', () => { + const error = failure('RECEIPT_RECOMPILE_REQUIRED'); + expect( + previewRecoveryAction(error, { + receiptRefreshes: 0, + transientRetries: 0, + limit: 25, + }), + ).toBe('recompile'); + expect( + previewRecoveryAction(error, { + receiptRefreshes: 1, + transientRetries: 0, + limit: 25, + }), + ).toBe('fail'); + }); + + it('refreshes a changed catalog exactly once', () => { + const error = failure('RECEIPT_STALE'); + expect( + previewRecoveryAction(error, { + receiptRefreshes: 0, + transientRetries: 0, + limit: 25, + }), + ).toBe('refresh-catalog'); + expect( + previewRecoveryAction(error, { + receiptRefreshes: 1, + transientRetries: 0, + limit: 25, + }), + ).toBe('fail'); + }); + + it('surfaces oversized previews instead of silently changing the row limit', () => { + const error = failure('PREVIEW_RESPONSE_TOO_LARGE'); + expect( + previewRecoveryAction(error, { + receiptRefreshes: 0, + transientRetries: 0, + limit: 25, + }), + ).toBe('fail'); + expect(isPreviewResponseSizeError(error.code)).toBe(true); + expect(isPreviewResponseSizeError('PLAN_TOO_EXPENSIVE')).toBe(false); + }); + + it('retries transient failures once and leaves semantic failures alone', () => { + const transient = failure('BACKEND_UNAVAILABLE', true); + expect( + previewRecoveryAction(transient, { + receiptRefreshes: 0, + transientRetries: 0, + limit: 25, + }), + ).toBe('retry'); + expect( + previewRecoveryAction(transient, { + receiptRefreshes: 0, + transientRetries: 1, + limit: 25, + }), + ).toBe('fail'); + expect( + previewRecoveryAction(failure('PLAN_TOO_EXPENSIVE'), { + receiptRefreshes: 0, + transientRetries: 0, + limit: 25, + }), + ).toBe('fail'); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts new file mode 100644 index 00000000..1ead27dc --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts @@ -0,0 +1,509 @@ +import type { + ExplorerAuthoringDiagnostic, + ExplorerBuilderCandidate, + ExplorerBuilderCatalog, + ExplorerBuilderColumn, + ExplorerBuilderCompileResult, + ExplorerBuilderCommandsResult, + ExplorerBuilderPreviewResult, + ExplorerBuilderState, +} from '../../../types'; +import { + derivedOccurrences, + replaceRouteNode, + routeNode, + routeSubtreeOccurrenceIds, + stateFromBuilder, + stateFromCommands, + type BuilderAuthoringState, + type DraftTable, +} from './model'; + +export type BuilderAction = + | { + readonly type: 'hydrate'; + readonly value: ExplorerBuilderState; + readonly explorerId?: string; + } + | { readonly type: 'selectTable'; readonly outputId: string } + | { readonly type: 'selectOccurrence'; readonly occurrenceId: string } + | { readonly type: 'addTable'; readonly table: DraftTable } + | { readonly type: 'removeTable'; readonly outputId: string } + | { + readonly type: 'renameTable'; + readonly outputId: string; + readonly title: string; + } + | { + readonly type: 'reorderTable'; + readonly outputId: string; + readonly before?: string; + } + | { + readonly type: 'setRoot'; + readonly outputId: string; + readonly nodeId: string; + } + | { + readonly type: 'changeRoot'; + readonly outputId: string; + readonly nodeId: string; + } + | { + readonly type: 'addRouteChild'; + readonly outputId: string; + readonly parentOccurrenceId: string; + readonly edgeId: string; + readonly occurrenceId: string; + } + | { + readonly type: 'removeRouteSubtree'; + readonly outputId: string; + readonly occurrenceId: string; + } + | { + readonly type: 'addColumn'; + readonly outputId: string; + readonly value: ExplorerBuilderColumn; + } + | { + readonly type: 'addColumns'; + readonly outputId: string; + readonly values: ReadonlyArray; + } + | { + readonly type: 'updateColumn'; + readonly outputId: string; + readonly column: string; + readonly value: ExplorerBuilderColumn; + } + | { + readonly type: 'removeColumn'; + readonly outputId: string; + readonly column: string; + } + | { readonly type: 'compiling' } + | { + readonly type: 'commandsApplied'; + readonly value: ExplorerBuilderCommandsResult; + } + | { readonly type: 'compiled'; readonly value: ExplorerBuilderCompileResult } + | { + readonly type: 'catalogRefreshed'; + readonly catalog: ExplorerBuilderCatalog; + } + | { + readonly type: 'candidatesLoaded'; + readonly candidates: ReadonlyArray; + } + | { + readonly type: 'repair'; + readonly diagnostics: ReadonlyArray; + } + | { readonly type: 'requestRecompile' } + | { readonly type: 'preview'; readonly value?: ExplorerBuilderPreviewResult } + | { readonly type: 'published' }; + +const reconcileSharedFilters = ( + state: BuilderAuthoringState, +): BuilderAuthoringState['workspace'] => { + if (!state.workspace?.sharedFilters) return state.workspace; + const columnsByOutput = new Map( + state.tables.map((table) => [ + table.outputId, + new Set(table.document.columns.map((column) => column.column)), + ]), + ); + const entries = Object.entries(state.workspace.sharedFilters).flatMap( + ([name, bindings]) => { + const retained = bindings.filter((binding) => + columnsByOutput.get(binding.outputId)?.has(binding.column), + ); + return retained.length ? [[name, retained] as const] : []; + }, + ); + return { + ...state.workspace, + sharedFilters: entries.length ? Object.fromEntries(entries) : undefined, + }; +}; + +const invalidate = ( + state: BuilderAuthoringState, + { preservePreview = false }: { readonly preservePreview?: boolean } = {}, +): BuilderAuthoringState => ({ + ...state, + workspace: reconcileSharedFilters(state), + receipt: undefined, + preview: preservePreview ? state.preview : undefined, + diagnostics: [], + dirty: true, + reconciliation: 'idle', +}); + +const updateTable = ( + state: BuilderAuthoringState, + outputId: string, + update: (table: DraftTable) => DraftTable, + options?: { readonly preservePreview?: boolean }, +): BuilderAuthoringState => { + const current = state.tables.find((table) => table.outputId === outputId); + if (!current) return state; + return invalidate( + { + ...state, + tables: state.tables.map((table) => + table.outputId === outputId ? update(table) : table, + ), + }, + options, + ); +}; + +const keepColumnReferences = ( + document: DraftTable['document'], + columns: DraftTable['document']['columns'], +): DraftTable['document'] => { + const names = new Set(columns.map((column) => column.column)); + return { + ...document, + columns, + fixedFilters: document.fixedFilters?.filter((filter) => + names.has(filter.column), + ), + actions: document.actions?.map((action) => ({ + ...action, + columns: action.columns?.filter((column) => names.has(column.column)), + })), + }; +}; + +export const builderAuthoringReducer = ( + state: BuilderAuthoringState, + action: BuilderAction, +): BuilderAuthoringState => { + switch (action.type) { + case 'hydrate': + return stateFromBuilder(action.value, { + project: state.project, + explorerId: action.explorerId ?? state.explorerId, + }); + case 'selectTable': + return state.tables.some((table) => table.outputId === action.outputId) + ? { + ...state, + selectedOutputId: action.outputId, + selectedOccurrenceId: 'base', + preview: undefined, + } + : state; + case 'selectOccurrence': { + const table = state.tables.find( + (candidate) => candidate.outputId === state.selectedOutputId, + ); + return derivedOccurrences(table, state.catalog).some( + (occurrence) => occurrence.id === action.occurrenceId, + ) + ? { ...state, selectedOccurrenceId: action.occurrenceId } + : state; + } + case 'addTable': { + const next = invalidate({ + ...state, + tables: [...state.tables, action.table], + selectedOutputId: action.table.outputId, + selectedOccurrenceId: 'base', + }); + return { + ...next, + reconciliation: 'idle', + }; + } + case 'removeTable': { + const tables = state.tables.filter( + (table) => table.outputId !== action.outputId, + ); + const next = invalidate({ + ...state, + tables, + selectedOutputId: + state.selectedOutputId === action.outputId + ? tables[0]?.outputId + : state.selectedOutputId, + selectedOccurrenceId: 'base', + }); + return { ...next, reconciliation: 'idle' }; + } + case 'renameTable': + return updateTable(state, action.outputId, (table) => ({ + ...table, + title: action.title, + })); + case 'reorderTable': { + const moving = state.tables.find( + (table) => table.outputId === action.outputId, + ); + if (!moving) return state; + const tables = state.tables.filter( + (table) => table.outputId !== action.outputId, + ); + const index = action.before + ? tables.findIndex((table) => table.outputId === action.before) + : tables.length; + tables.splice(index < 0 ? tables.length : index, 0, moving); + return invalidate({ ...state, tables }); + } + case 'setRoot': + case 'changeRoot': { + const node = state.catalog.nodes.find( + (candidate) => candidate.nodeId === action.nodeId, + ); + if (!node?.rowRootEligible) return state; + const table = state.tables.find( + (candidate) => candidate.outputId === action.outputId, + ); + const currentRoot = state.catalog.nodes.find( + (candidate) => + candidate.resourceType === table?.document.rootResourceType, + ); + if (!table || (action.type === 'setRoot' && currentRoot)) return state; + if (action.type === 'changeRoot' && currentRoot?.nodeId === action.nodeId) + return state; + return { + ...updateTable(state, action.outputId, (current) => ({ + ...current, + document: { + ...current.document, + rootResourceType: node.resourceType, + route: { + occurrenceId: 'base', + resourceType: node.resourceType, + }, + columns: [], + fixedFilters: undefined, + actions: undefined, + }, + })), + selectedOccurrenceId: 'base', + }; + } + case 'addRouteChild': { + const table = state.tables.find( + (candidate) => candidate.outputId === action.outputId, + ); + const occurrences = derivedOccurrences(table, state.catalog); + const parent = occurrences.find( + (occurrence) => occurrence.id === action.parentOccurrenceId, + ); + const edge = state.catalog.edges.find( + (candidate) => + candidate.edgeId === action.edgeId && + candidate.fromNodeId === parent?.nodeId, + ); + const target = state.catalog.nodes.find( + (candidate) => candidate.nodeId === edge?.toNodeId, + ); + if (!table || !parent || !edge || !target) return state; + const repeated = occurrences.some( + (occurrence) => occurrence.incomingEdgeId === edge.edgeId, + ); + const allowsSelfLoop = + state.catalog.routePolicy.allowSelfLoops ?? + state.catalog.routePolicy.selfLoops ?? + false; + if (repeated) return state; + if (edge.fromNodeId === edge.toNodeId && !allowsSelfLoop) return state; + const maxSteps = state.catalog.routePolicy.maxSteps; + if (maxSteps && parent.depth + 1 > maxSteps) return state; + const route = replaceRouteNode( + table.document.route, + action.parentOccurrenceId, + (current) => ({ + ...current, + children: [ + ...(current.children ?? []), + { + occurrenceId: action.occurrenceId, + resourceType: target.resourceType, + relationship: edge.label, + }, + ], + }), + ); + if (!route) return state; + return { + ...updateTable(state, action.outputId, (current) => ({ + ...current, + document: { ...current.document, route }, + })), + selectedOccurrenceId: action.occurrenceId, + }; + } + case 'removeRouteSubtree': { + const table = state.tables.find( + (candidate) => candidate.outputId === action.outputId, + ); + if (!table || action.occurrenceId === 'base') return state; + const subtree = routeNode(table.document.route, action.occurrenceId); + if (!subtree) return state; + const removedOccurrences = routeSubtreeOccurrenceIds(subtree); + const route = replaceRouteNode( + table.document.route, + action.occurrenceId, + () => undefined, + ); + if (!route) return state; + const next = updateTable(state, action.outputId, (current) => { + const columns = current.document.columns.filter( + (column) => !removedOccurrences.has(column.occurrenceId), + ); + return { + ...current, + document: { + ...keepColumnReferences(current.document, columns), + route, + }, + }; + }); + return { ...next, selectedOccurrenceId: 'base' }; + } + case 'addColumn': + return updateTable(state, action.outputId, (table) => { + if ( + table.document.columns.some( + (column) => column.column === action.value.column, + ) + ) + return table; + return { + ...table, + document: { + ...table.document, + columns: [...table.document.columns, action.value], + }, + }; + }); + case 'addColumns': + return updateTable(state, action.outputId, (table) => { + const names = new Set( + table.document.columns.map((column) => column.column), + ); + const additions = action.values.filter((column) => { + if (names.has(column.column)) return false; + names.add(column.column); + return true; + }); + if (!additions.length) return table; + return { + ...table, + document: { + ...table.document, + columns: [...table.document.columns, ...additions], + }, + }; + }); + case 'updateColumn': + return updateTable( + state, + action.outputId, + (table) => ({ + ...table, + document: { + ...table.document, + columns: table.document.columns.map((column) => + column.column === action.column ? action.value : column, + ), + }, + }), + { preservePreview: true }, + ); + case 'removeColumn': + return updateTable(state, action.outputId, (table) => ({ + ...table, + document: keepColumnReferences( + table.document, + table.document.columns.filter( + (column) => column.column !== action.column, + ), + ), + })); + case 'compiling': + return { ...state, reconciliation: 'pending', diagnostics: [] }; + case 'commandsApplied': + return stateFromCommands(state, action.value); + case 'compiled': { + const normalized = stateFromBuilder( + { + apiVersion: 'loom.calypr.org/explorer-authoring/v2', + kind: 'ExplorerBuilderState', + lifecycleState: 'READY', + draftVersion: state.draftVersion, + draftDigest: state.draftDigest, + workspace: action.value.builder, + catalog: state.catalog, + }, + { project: state.project, explorerId: state.explorerId }, + ); + const tables = normalized.tables; + return { + ...state, + workspace: action.value.builder, + tables, + selectedOutputId: tables.some( + (table) => table.outputId === state.selectedOutputId, + ) + ? state.selectedOutputId + : tables[0]?.outputId, + receipt: action.value, + diagnostics: action.value.diagnostics, + reconciliation: action.value.diagnostics.some( + (diagnostic) => diagnostic.severity === 'error', + ) + ? 'repair' + : 'resolved', + }; + } + case 'catalogRefreshed': + return { + ...state, + catalog: action.catalog, + receipt: undefined, + preview: undefined, + diagnostics: [], + reconciliation: 'idle', + }; + case 'candidatesLoaded': { + const candidates = new Map( + (state.catalog.candidates ?? []).map((candidate) => [ + candidate.candidateId, + candidate, + ]), + ); + action.candidates.forEach((candidate) => + candidates.set(candidate.candidateId, candidate), + ); + return { + ...state, + catalog: { ...state.catalog, candidates: [...candidates.values()] }, + }; + } + case 'repair': + return { + ...state, + reconciliation: 'repair', + diagnostics: action.diagnostics, + }; + case 'requestRecompile': + return { + ...state, + receipt: undefined, + preview: undefined, + diagnostics: [], + reconciliation: 'pending', + }; + case 'preview': + return { ...state, preview: action.value }; + case 'published': + return { ...state, dirty: false }; + } +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.unit.test.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.unit.test.ts new file mode 100644 index 00000000..fd60dd97 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.unit.test.ts @@ -0,0 +1,614 @@ +import type { ExplorerBuilderState } from '../../../types'; +import { + derivedOccurrences, + nextRouteOccurrenceId, + stateFromBuilder, + workspaceFromState, +} from './model'; +import { builderAuthoringReducer } from './reducer'; + +const ready: ExplorerBuilderState = { + apiVersion: 'loom.calypr.org/explorer-authoring/v2', + kind: 'ExplorerBuilderState', + lifecycleState: 'READY', + draftVersion: 1, + draftDigest: 'sha256:draft', + workspace: { + apiVersion: 'loom.calypr.org/explorer-authoring/v2', + kind: 'ExplorerBuilderWorkspace', + explorer: { title: 'BForePC' }, + documents: [ + { + kind: 'ExplorerBuilderDocument', + output: { id: 'Specimen', title: 'Specimen' }, + rootResourceType: 'Specimen', + route: { occurrenceId: 'base', resourceType: 'Specimen' }, + columns: [ + { + column: 'specimen_identifier', + label: 'HTAN_BIOSPECIMEN_ID', + occurrenceId: 'base', + source: { + kind: 'identifierBySystem', + match: 'https://aced-idp.org/project', + projectionMode: 'FIRST', + }, + table: { visible: true, order: 0 }, + }, + ], + }, + ], + tabs: [ + { + id: 'specimen', + title: 'Specimen', + outputId: 'Specimen', + order: 0, + visible: true, + }, + ], + }, + catalog: { + snapshotToken: 'snapshot', + generation: 'generation', + routePolicy: { allowRepeatedEdges: false, allowSelfLoops: false }, + nodes: [ + { + nodeId: 'specimen-node', + resourceType: 'Specimen', + rowRootEligible: true, + populated: true, + documentCount: 1, + }, + ], + edges: [], + candidates: [], + }, +}; + +describe('semantic Builder hydration', () => { + it('reopens and recompiles the canonical workspace without reconstructing sources', () => { + const state = stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }); + expect(state.tables).toHaveLength(1); + expect(workspaceFromState(state)).toEqual(ready.workspace); + }); + + it('does not serialize stale shared-filter bindings from preserved hot-reload state', () => { + const state = stateFromBuilder( + { + ...ready, + workspace: { + ...ready.workspace!, + sharedFilters: { + document_reference_patient_identifier: [ + { outputId: 'Specimen', column: 'unknown_old_column' }, + ], + }, + }, + }, + { project: 'project', explorerId: 'default' }, + ); + + expect(workspaceFromState(state).sharedFilters).toBeUndefined(); + }); + + it('serializes presentation edits from the V2 document into the next compile workspace', () => { + const preview = { + apiVersion: 'loom.calypr.org/explorer-authoring/v2' as const, + kind: 'ExplorerBuilderPreview' as const, + receiptId: 'receipt_preview', + outputId: 'Specimen', + columns: [], + rows: [{ specimen_identifier: 'sample-1' }], + rowCount: 1, + diagnostics: [], + }; + const state = { + ...stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }), + preview, + }; + const original = state.tables[0].document.columns[0]; + const edited = builderAuthoringReducer(state, { + type: 'updateColumn', + outputId: 'Specimen', + column: original.column, + value: { + ...original, + label: 'Specimen identifier', + table: { visible: false, order: 0 }, + filter: { label: 'Specimen identifier' }, + chart: { type: 'bar', title: 'Specimens' }, + }, + }); + + expect(edited.dirty).toBe(true); + expect(edited.reconciliation).toBe('idle'); + expect(edited.preview).toBe(preview); + expect(workspaceFromState(edited).documents[0].columns[0]).toMatchObject({ + column: 'specimen_identifier', + label: 'Specimen identifier', + table: { visible: false, order: 0 }, + filter: { label: 'Specimen identifier' }, + chart: { type: 'bar', title: 'Specimens' }, + source: original.source, + }); + }); + + it('removes configured columns without candidate identity reconciliation', () => { + const state = stateFromBuilder( + { + ...ready, + workspace: { + ...ready.workspace!, + sharedFilters: { + document_reference_patient_identifier: [ + { outputId: 'Specimen', column: 'specimen_identifier' }, + ], + }, + }, + }, + { + project: 'project', + explorerId: 'default', + }, + ); + const edited = builderAuthoringReducer(state, { + type: 'removeColumn', + outputId: 'Specimen', + column: 'specimen_identifier', + }); + + expect(workspaceFromState(edited).documents[0].columns).toEqual([]); + expect(workspaceFromState(edited).sharedFilters).toBeUndefined(); + }); + + it('atomically removes old shared-filter bindings when starting a new query', () => { + const state = stateFromBuilder( + { + ...ready, + workspace: { + ...ready.workspace!, + sharedFilters: { + document_reference_patient_identifier: [ + { outputId: 'Specimen', column: 'specimen_identifier' }, + ], + }, + }, + catalog: { + ...ready.catalog, + nodes: [ + ...ready.catalog.nodes, + { + nodeId: 'patient-node', + resourceType: 'Patient', + rowRootEligible: true, + populated: true, + documentCount: 1, + }, + ], + }, + }, + { + project: 'project', + explorerId: 'default', + }, + ); + const edited = builderAuthoringReducer(state, { + type: 'changeRoot', + outputId: 'Specimen', + nodeId: 'patient-node', + }); + + expect(workspaceFromState(edited).documents[0].columns).toEqual([]); + expect(workspaceFromState(edited).documents[0].route).toEqual({ + occurrenceId: 'base', + resourceType: 'Patient', + }); + expect(workspaceFromState(edited).sharedFilters).toBeUndefined(); + }); + + it('adds a complete typed column without introducing candidate selection state', () => { + const state = stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }); + const edited = builderAuthoringReducer(state, { + type: 'addColumn', + outputId: 'Specimen', + value: { + column: 'birth_date', + label: 'Birth date', + logicalType: 'date', + occurrenceId: 'base', + source: { + kind: 'field', + fieldPath: 'birthDate', + projectionMode: 'FIRST', + }, + table: { visible: true, order: 1 }, + }, + }); + + expect(workspaceFromState(edited).documents[0].columns[1]).toMatchObject({ + column: 'birth_date', + source: { kind: 'field', fieldPath: 'birthDate' }, + }); + }); + + it('does not serialize an invalid empty output title during reactive editing', () => { + const state = stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }); + const editing = builderAuthoringReducer(state, { + type: 'renameTable', + outputId: 'Specimen', + title: '', + }); + + expect(workspaceFromState(editing).documents[0].output.title).toBe( + 'Specimen', + ); + expect(workspaceFromState(editing).tabs[0].title).toBe('Specimen'); + }); + + it('rejects READY state without authoring intent', () => { + expect(() => + stateFromBuilder( + { ...ready, workspace: null }, + { project: 'project', explorerId: 'default' }, + ), + ).toThrow(/READY/); + }); + + it('keeps a refreshed catalog idle until compilation is requested', () => { + const state = stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }); + const refreshed = builderAuthoringReducer(state, { + type: 'catalogRefreshed', + catalog: { ...ready.catalog, snapshotToken: 'new-snapshot' }, + }); + expect(refreshed.reconciliation).toBe('idle'); + expect(refreshed.receipt).toBeUndefined(); + expect(refreshed.tables).toEqual(state.tables); + }); + + it('leaves an incomplete workspace editable after a catalog refresh', () => { + const state = stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }); + const incomplete = { + ...state, + tables: state.tables.map((table) => ({ + ...table, + document: { + ...table.document, + rootResourceType: '', + route: { occurrenceId: 'base', resourceType: '' }, + }, + })), + }; + const refreshed = builderAuthoringReducer(incomplete, { + type: 'catalogRefreshed', + catalog: { ...ready.catalog, snapshotToken: 'new-snapshot' }, + }); + expect(refreshed.reconciliation).toBe('idle'); + }); + + it('keeps a newly added blank table editable until a row root is selected', () => { + const state = stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }); + const blankTable = { + outputId: 'blank-output', + tabId: 'blank-tab', + title: 'Blank table', + document: { + kind: 'ExplorerBuilderDocument' as const, + output: { id: 'blank-output', title: 'Blank table' }, + rootResourceType: '', + route: { occurrenceId: 'base', resourceType: '' }, + columns: [], + }, + }; + + const added = builderAuthoringReducer(state, { + type: 'addTable', + table: blankTable, + }); + + expect(added.reconciliation).toBe('idle'); + expect(added.selectedOutputId).toBe('blank-output'); + + const rooted = builderAuthoringReducer(added, { + type: 'setRoot', + outputId: 'blank-output', + nodeId: 'specimen-node', + }); + expect(rooted.tables.at(-1)?.document.rootResourceType).toBe('Specimen'); + expect(rooted.reconciliation).toBe('idle'); + }); + + it('hydrates, edits, and removes sibling branches without dropping unrelated routes', () => { + const branched: ExplorerBuilderState = { + ...ready, + workspace: { + ...ready.workspace!, + sharedFilters: { + participant: [ + { outputId: 'Specimen', column: 'patient__identifier' }, + ], + }, + documents: [ + { + ...ready.workspace!.documents[0], + route: { + occurrenceId: 'base', + resourceType: 'Specimen', + children: [ + { + occurrenceId: 'observation', + resourceType: 'Observation', + relationship: 'focus_Specimen', + }, + { + occurrenceId: 'patient', + resourceType: 'Patient', + relationship: 'subject_Patient', + }, + ], + }, + columns: [ + ...ready.workspace!.documents[0].columns, + { + column: 'observation__status', + label: 'Observation status', + occurrenceId: 'observation', + source: { + kind: 'field', + fieldPath: 'status', + projectionMode: 'FIRST', + }, + table: { visible: true, order: 1 }, + }, + { + column: 'patient__identifier', + label: 'Participant', + occurrenceId: 'patient', + source: { + kind: 'field', + fieldPath: 'identifier[].value', + projectionMode: 'FIRST', + }, + table: { visible: true, order: 2 }, + }, + ], + }, + ], + }, + catalog: { + ...ready.catalog, + nodes: [ + ...ready.catalog.nodes, + { + nodeId: 'observation-node', + resourceType: 'Observation', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'patient-node', + resourceType: 'Patient', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'condition-node', + resourceType: 'Condition', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + ], + edges: [ + { + edgeId: 'specimen-observation', + fromNodeId: 'specimen-node', + toNodeId: 'observation-node', + label: 'focus_Specimen', + }, + { + edgeId: 'specimen-patient', + fromNodeId: 'specimen-node', + toNodeId: 'patient-node', + label: 'subject_Patient', + }, + { + edgeId: 'observation-condition', + fromNodeId: 'observation-node', + toNodeId: 'condition-node', + label: 'focus_Condition', + }, + ], + }, + }; + const state = stateFromBuilder(branched, { + project: 'project', + explorerId: 'default', + }); + expect( + derivedOccurrences(state.tables[0], state.catalog).map(({ id }) => id), + ).toEqual(['base', 'observation', 'patient']); + expect(workspaceFromState(state)).toEqual(branched.workspace); + + const occurrenceId = nextRouteOccurrenceId( + state.tables[0].document.route, + 'observation', + 'Condition', + 'focus_Condition', + ); + const withNestedBranch = builderAuthoringReducer(state, { + type: 'addRouteChild', + outputId: 'Specimen', + parentOccurrenceId: 'observation', + edgeId: 'observation-condition', + occurrenceId, + }); + expect(occurrenceId).toBe('observation__condition'); + expect( + derivedOccurrences( + withNestedBranch.tables[0], + withNestedBranch.catalog, + ).map(({ id }) => id), + ).toEqual(['base', 'observation', 'observation__condition', 'patient']); + + const edited = builderAuthoringReducer(withNestedBranch, { + type: 'removeRouteSubtree', + outputId: 'Specimen', + occurrenceId: 'patient', + }); + expect( + derivedOccurrences(edited.tables[0], edited.catalog).map(({ id }) => id), + ).toEqual(['base', 'observation', 'observation__condition']); + expect( + workspaceFromState(edited).documents[0].columns.map( + (column) => column.column, + ), + ).toContain('observation__status'); + expect( + workspaceFromState(edited).documents[0].columns.map( + (column) => column.column, + ), + ).not.toContain('patient__identifier'); + expect(workspaceFromState(edited).sharedFilters).toBeUndefined(); + }); + + it('replaces local identities with the authoritative command workspace', () => { + const state = stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }); + const workspace = { + ...ready.workspace!, + documents: ready.workspace!.documents.map((document) => ({ + ...document, + output: { ...document.output, id: 'out_backend' }, + })), + tabs: ready.workspace!.tabs.map((tab) => ({ + ...tab, + id: 'tab_backend', + outputId: 'out_backend', + })), + }; + const next = builderAuthoringReducer(state, { + type: 'commandsApplied', + value: { + commandId: 'command-1', + workspace, + draftVersion: 2, + draftDigest: 'sha256:next', + results: [ + { + type: 'TABLE_CREATED', + outputId: 'out_backend', + tabId: 'tab_backend', + occurrenceId: 'base', + }, + ], + diagnostics: [], + }, + }); + expect(next.selectedOutputId).toBe('out_backend'); + expect(next.draftVersion).toBe(2); + expect(next.workspace).toEqual(workspace); + expect(next.reconciliation).toBe('idle'); + + const requested = builderAuthoringReducer(next, { + type: 'requestRecompile', + }); + expect(requested.reconciliation).toBe('pending'); + expect(requested.dirty).toBe(true); + }); + + it('preserves the selected traversal node after a table presentation update', () => { + const workspace = { + ...ready.workspace!, + documents: ready.workspace!.documents.map((document) => ({ + ...document, + route: { + ...document.route, + children: [ + { + occurrenceId: 'observation', + resourceType: 'Observation', + relationship: 'focus_Specimen', + }, + ], + }, + })), + }; + const state = { + ...stateFromBuilder( + { ...ready, workspace }, + { project: 'project', explorerId: 'default' }, + ), + selectedOccurrenceId: 'observation', + }; + + const next = builderAuthoringReducer(state, { + type: 'commandsApplied', + value: { + commandId: 'command-2', + workspace, + draftVersion: 2, + draftDigest: 'sha256:next', + results: [ + { + type: 'TABLE_CHANGED', + outputId: 'Specimen', + column: 'observation__status', + }, + ], + diagnostics: [], + }, + }); + + expect(next.selectedOccurrenceId).toBe('observation'); + }); + + it('falls back to the root when the selected traversal node was removed', () => { + const state = { + ...stateFromBuilder(ready, { + project: 'project', + explorerId: 'default', + }), + selectedOccurrenceId: 'removed-occurrence', + }; + + const next = builderAuthoringReducer(state, { + type: 'commandsApplied', + value: { + commandId: 'command-3', + workspace: ready.workspace!, + draftVersion: 2, + draftDigest: 'sha256:next', + results: [{ type: 'TABLE_CHANGED', outputId: 'Specimen' }], + diagnostics: [], + }, + }); + + expect(next.selectedOccurrenceId).toBe('base'); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts new file mode 100644 index 00000000..2eef6d4e --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts @@ -0,0 +1,78 @@ +import type { ExplorerBuilderCatalog } from '../../../types'; +import { derivedOccurrences, type DraftTable } from './model'; + +/** + * The graph is allowed to inspect the full catalog, but authoring may only + * use edges that leave the selected authored occurrence. Keeping this rule pure makes + * it reusable by the canvas, the explicit route controls, and tests. + */ +export const legalOutgoingEdges = ( + catalog: ExplorerBuilderCatalog, + table: DraftTable | undefined, + parentOccurrenceId: string, +): ExplorerBuilderCatalog['edges'] => { + const occurrences = derivedOccurrences(table, catalog); + const parent = occurrences.find( + (occurrence) => occurrence.id === parentOccurrenceId, + ); + if (!table || !parent) return []; + const allowsSelfLoops = + catalog.routePolicy.allowSelfLoops ?? + catalog.routePolicy.selfLoops ?? + false; + const maxSteps = catalog.routePolicy.maxSteps; + if (maxSteps && parent.depth + 1 > maxSteps) return []; + const usedEdgeIds = new Set( + occurrences.map((occurrence) => occurrence.incomingEdgeId), + ); + return catalog.edges.filter((edge) => { + if (edge.fromNodeId !== parent.nodeId) return false; + if (edge.fromNodeId === edge.toNodeId && !allowsSelfLoops) return false; + return !usedEdgeIds.has(edge.edgeId); + }); +}; + +export const replaceableIncomingEdges = ( + catalog: ExplorerBuilderCatalog, + table: DraftTable | undefined, + occurrenceId: string, +): ExplorerBuilderCatalog['edges'] => { + const occurrences = derivedOccurrences(table, catalog); + const occurrence = occurrences.find((item) => item.id === occurrenceId); + const parent = occurrences.find((item) => item.id === occurrence?.parentId); + if (!occurrence || !parent || !occurrence.incomingEdgeId) return []; + const otherEdgeIds = new Set( + occurrences + .filter((item) => item.id !== occurrence.id) + .map((item) => item.incomingEdgeId), + ); + return catalog.edges.filter( + (edge) => + edge.fromNodeId === parent.nodeId && + edge.toNodeId === occurrence.nodeId && + !otherEdgeIds.has(edge.edgeId), + ); +}; + +export const legalEdgesToNode = ( + catalog: ExplorerBuilderCatalog, + table: DraftTable | undefined, + parentOccurrenceId: string, + nodeId: string | undefined, +): ExplorerBuilderCatalog['edges'] => { + if (!nodeId) return []; + return legalOutgoingEdges(catalog, table, parentOccurrenceId).filter( + (edge) => edge.toNodeId === nodeId, + ); +}; + +export const isLegalRouteExtension = ( + catalog: ExplorerBuilderCatalog, + table: DraftTable | undefined, + parentOccurrenceId: string, + edgeId: string, + targetNodeId: string, +): boolean => + legalOutgoingEdges(catalog, table, parentOccurrenceId).some( + (edge) => edge.edgeId === edgeId && edge.toNodeId === targetNodeId, + ); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.unit.test.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.unit.test.ts new file mode 100644 index 00000000..4e4853aa --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.unit.test.ts @@ -0,0 +1,205 @@ +import type { ExplorerBuilderCatalog } from '../../../types'; +import type { DraftTable } from './model'; +import { + isLegalRouteExtension, + legalEdgesToNode, + legalOutgoingEdges, + replaceableIncomingEdges, +} from './routeActions'; + +const catalog: ExplorerBuilderCatalog = { + snapshotToken: 'snapshot', + generation: 'generation', + routePolicy: { allowRepeatedEdges: true, allowSelfLoops: true }, + nodes: [ + { + nodeId: 'patient', + resourceType: 'Patient', + rowRootEligible: true, + populated: true, + documentCount: 1, + }, + { + nodeId: 'specimen', + resourceType: 'Specimen', + rowRootEligible: true, + populated: true, + documentCount: 1, + }, + ], + edges: [ + { + edgeId: 'patient-specimen', + fromNodeId: 'patient', + toNodeId: 'specimen', + label: 'specimens', + }, + { + edgeId: 'specimen-self', + fromNodeId: 'specimen', + toNodeId: 'specimen', + label: 'related', + }, + ], + candidates: [], +}; +const table = (resourceType = ''): DraftTable => ({ + outputId: 'table', + tabId: 'tab-table', + title: 'Table', + document: { + kind: 'ExplorerBuilderDocument', + output: { id: 'table', title: 'Table' }, + rootResourceType: resourceType, + route: { occurrenceId: 'base', resourceType }, + columns: [], + }, +}); + +describe('Builder V2 route actions', () => { + it('does not offer edges before a row root exists', () => { + expect(legalOutgoingEdges(catalog, table(), 'base')).toEqual([]); + }); + + it('offers directed edges from the selected occurrence', () => { + expect(legalOutgoingEdges(catalog, table('Patient'), 'base')).toEqual([ + catalog.edges[0], + ]); + expect( + legalEdgesToNode(catalog, table('Patient'), 'base', 'specimen'), + ).toEqual([catalog.edges[0]]); + expect( + isLegalRouteExtension( + catalog, + table('Patient'), + 'base', + 'patient-specimen', + 'specimen', + ), + ).toBe(true); + }); + + it('honors self-loop policy without a hidden hop cap', () => { + const atSpecimen: DraftTable = { + ...table('Patient'), + document: { + ...table('Patient').document, + route: { + occurrenceId: 'base', + resourceType: 'Patient', + children: [ + { + occurrenceId: 'specimen', + resourceType: 'Specimen', + relationship: 'specimens', + }, + ], + }, + }, + }; + expect(legalOutgoingEdges(catalog, atSpecimen, 'specimen')).toContainEqual( + catalog.edges[1], + ); + }); + + it('offers each relationship only once under the same parent occurrence', () => { + const atSpecimen: DraftTable = { + ...table('Patient'), + document: { + ...table('Patient').document, + route: { + occurrenceId: 'base', + resourceType: 'Patient', + children: [ + { + occurrenceId: 'specimen', + resourceType: 'Specimen', + relationship: 'specimens', + }, + ], + }, + }, + }; + + expect(legalOutgoingEdges(catalog, atSpecimen, 'base')).toEqual([]); + }); + + it('does not offer a relationship already used by another parent occurrence', () => { + const cyclicCatalog: ExplorerBuilderCatalog = { + ...catalog, + edges: [ + catalog.edges[0], + { + edgeId: 'specimen-patient', + fromNodeId: 'specimen', + toNodeId: 'patient', + label: 'patient', + }, + ], + }; + const cyclicTable: DraftTable = { + ...table('Patient'), + document: { + ...table('Patient').document, + route: { + occurrenceId: 'base', + resourceType: 'Patient', + children: [ + { + occurrenceId: 'specimen', + resourceType: 'Specimen', + relationship: 'specimens', + children: [ + { + occurrenceId: 'patient-again', + resourceType: 'Patient', + relationship: 'patient', + }, + ], + }, + ], + }, + }, + }; + + expect( + legalOutgoingEdges(cyclicCatalog, cyclicTable, 'patient-again'), + ).toEqual([]); + }); + + it('offers unused sibling relationships when editing an occurrence edge', () => { + const editableCatalog: ExplorerBuilderCatalog = { + ...catalog, + edges: [ + catalog.edges[0], + { + edgeId: 'patient-specimen-secondary', + fromNodeId: 'patient', + toNodeId: 'specimen', + label: 'research specimens', + }, + ], + }; + const atSpecimen: DraftTable = { + ...table('Patient'), + document: { + ...table('Patient').document, + route: { + occurrenceId: 'base', + resourceType: 'Patient', + children: [ + { + occurrenceId: 'specimen', + resourceType: 'Specimen', + relationship: 'specimens', + }, + ], + }, + }, + }; + + expect( + replaceableIncomingEdges(editableCatalog, atSpecimen, 'specimen'), + ).toEqual(editableCatalog.edges); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.tsx new file mode 100644 index 00000000..68aeb5fe --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.tsx @@ -0,0 +1,391 @@ +import React, { useState } from 'react'; +import { createPortal } from 'react-dom'; + +import type { ExplorerSummary } from '../../../api'; +import type { DraftTable } from '../authoring/model'; +import { IconCopy, IconGripVertical, IconTrash } from './icons'; +import { useDismissibleLayer } from './useDismissibleLayer'; + +interface BuilderToolbarProps { + explorers: ReadonlyArray; + selectedExplorerId: string; + onExplorerChange: (explorerId: string) => void; + onCreateExplorer: (name: string, fromDefault: boolean) => void; + deleteSupported: boolean; + deleteDisabled?: boolean; + onDeleteExplorer: () => void; + tables: ReadonlyArray; + selectedOutputId?: string; + onSelectTable: (outputId: string) => void; + onRenameTable: (outputId: string, title: string) => void; + onNewTable: () => void; + onDuplicateTable: () => void; + onDeleteTable: () => void; + onReorderTable: (outputId: string, before?: string) => void; + onPreview: () => void; + onPublish: () => void; + previewDisabled: boolean; + publishDisabled: boolean; + publishing: boolean; + busy?: boolean; + columnCreationSupported?: boolean; + tableToolbarHost?: HTMLElement | null; +} + +const TableToolbar = ({ + tables, + selectedOutputId, + onSelectTable, + onRenameTable, + onNewTable, + onDuplicateTable, + onDeleteTable, + onReorderTable, + busy, + columnCreationSupported, +}: Pick< + BuilderToolbarProps, + | 'tables' + | 'selectedOutputId' + | 'onSelectTable' + | 'onRenameTable' + | 'onNewTable' + | 'onDuplicateTable' + | 'onDeleteTable' + | 'onReorderTable' + | 'busy' + | 'columnCreationSupported' +>) => { + const [draggedOutputId, setDraggedOutputId] = useState(null); + const [tableMenuOpen, setTableMenuOpen] = useState(false); + const tableMenuRef = useDismissibleLayer( + tableMenuOpen, + setTableMenuOpen, + ); + const selectedTable = + tables.find((table) => table.outputId === selectedOutputId) ?? null; + + return ( +
+
+ { + event.preventDefault(); + setTableMenuOpen((open) => !open); + }} + className="min-w-40 max-w-56 cursor-pointer list-none truncate rounded border border-slate-300 bg-white px-2.5 py-1.5 text-sm text-slate-800 hover:bg-slate-50" + > + {selectedTable?.title || selectedTable?.outputId || 'Select table'} + +
+
+

Tables

+

+ Drag to reorder · edit names directly +

+
+
    + {tables.map((table) => ( +
  1. event.preventDefault()} + onDrop={() => { + if (draggedOutputId && draggedOutputId !== table.outputId) { + const fromIndex = tables.findIndex( + (candidate) => candidate.outputId === draggedOutputId, + ); + const targetIndex = tables.findIndex( + (candidate) => candidate.outputId === table.outputId, + ); + onReorderTable( + draggedOutputId, + fromIndex < targetIndex + ? tables[targetIndex + 1]?.outputId + : table.outputId, + ); + } + setDraggedOutputId(null); + }} + className={`flex items-center gap-2 rounded border px-2 py-1.5 ${ + table.outputId === selectedOutputId + ? 'border-blue-300 bg-blue-50' + : 'border-slate-200 bg-white hover:bg-slate-50' + }`} + > + setDraggedOutputId(table.outputId)} + onDragEnd={() => setDraggedOutputId(null)} + className="cursor-grab text-slate-400 active:cursor-grabbing" + > + + + onSelectTable(table.outputId)} + onChange={(event) => + onRenameTable(table.outputId, event.target.value) + } + className="min-w-0 flex-1 rounded border border-transparent bg-transparent px-1.5 py-1 text-sm text-slate-800 outline-none focus:border-blue-300 focus:bg-white" + /> +
  2. + ))} +
+ {columnCreationSupported ? ( + + ) : null} +
+
+ + +
+ ); +}; + +export function BuilderToolbar({ + explorers, + selectedExplorerId, + onExplorerChange, + onCreateExplorer, + deleteSupported, + deleteDisabled = false, + onDeleteExplorer, + tables, + selectedOutputId, + onSelectTable, + onRenameTable, + onNewTable, + onDuplicateTable, + onDeleteTable, + onReorderTable, + onPreview, + onPublish, + previewDisabled, + publishDisabled, + publishing, + busy = false, + columnCreationSupported = true, + tableToolbarHost, +}: BuilderToolbarProps) { + const [newExplorerOpen, setNewExplorerOpen] = useState(false); + const newExplorerRef = useDismissibleLayer( + newExplorerOpen, + setNewExplorerOpen, + ); + const [newExplorerName, setNewExplorerName] = useState(''); + const [newExplorerFromDefault, setNewExplorerFromDefault] = useState(false); + const selectedTable = tables.find( + (table) => table.outputId === selectedOutputId, + ); + const createExplorer = (fromDefault: boolean) => { + const name = newExplorerName.trim(); + if (!name) return; + onCreateExplorer(name, fromDefault); + setNewExplorerName(''); + setNewExplorerFromDefault(false); + setNewExplorerOpen(false); + }; + const tableToolbar = ( + + ); + + return ( +
+
+ + +
+ { + event.preventDefault(); + setNewExplorerOpen((open) => !open); + }} + className="cursor-pointer list-none rounded-md border border-blue-300 px-3 py-1.5 text-sm font-medium text-blue-800 hover:bg-blue-50" + > + New explorer + +
+ + setNewExplorerName(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') + createExplorer( + columnCreationSupported ? newExplorerFromDefault : true, + ); + }} + placeholder="e.g. Clinical overview" + className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm" + autoFocus={newExplorerOpen} + /> + {columnCreationSupported ? ( + + ) : ( +

+ The new Explorer will start with a copy of the current + configured tables. +

+ )} +
+ + +
+
+
+ +
+ + +
+
+ {tableToolbarHost + ? createPortal(tableToolbar, tableToolbarHost) + : tableToolbarHost === undefined + ? tableToolbar + : null} +
+ ); +} diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.unit.test.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.unit.test.tsx new file mode 100644 index 00000000..4587ddbd --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.unit.test.tsx @@ -0,0 +1,168 @@ +// @vitest-environment jsdom +import React from 'react'; +import { vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import type { DraftTable } from '../authoring/model'; +import { BuilderToolbar } from './BuilderToolbar'; + +const draftTable = (outputId: string, title: string): DraftTable => ({ + outputId, + tabId: outputId.toLowerCase(), + title, + document: { + kind: 'ExplorerBuilderDocument', + output: { id: outputId, title }, + rootResourceType: 'Patient', + route: { occurrenceId: 'base', resourceType: 'Patient' }, + columns: [], + }, +}); + +describe('BuilderToolbar', () => { + it('dismisses table and explorer menus when clicking away', () => { + const onDeleteExplorer = vi.fn(); + render( + , + ); + + const tableMenu = screen + .getByLabelText('Table selector') + .closest('details'); + expect(tableMenu).not.toBeNull(); + fireEvent.click(screen.getByLabelText('Table selector')); + expect(tableMenu).toHaveAttribute('open'); + fireEvent.pointerDown(document.body); + expect(tableMenu).not.toHaveAttribute('open'); + + fireEvent.click(screen.getByText('New explorer')); + const explorerMenu = screen + .getByLabelText('Explorer name') + .closest('details'); + expect(explorerMenu).toHaveAttribute('open'); + fireEvent.pointerDown(document.body); + expect(explorerMenu).not.toHaveAttribute('open'); + + fireEvent.click(screen.getByRole('button', { name: 'Delete explorer' })); + expect(onDeleteExplorer).toHaveBeenCalledTimes(1); + }); + + it('uses one draggable table menu for selection, naming, and ordering', () => { + const onSelectTable = vi.fn(); + const onRenameTable = vi.fn(); + const onReorderTable = vi.fn(); + render( + , + ); + + expect(screen.getByLabelText('Table selector')).toHaveTextContent( + 'Patient', + ); + expect(screen.queryByText(/Tables \(2\)/)).not.toBeInTheDocument(); + expect(screen.queryByText('Open explorer')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Table name')).not.toBeInTheDocument(); + + const specimenName = screen.getByLabelText('Table name for Specimen'); + fireEvent.focus(specimenName); + expect(onSelectTable).toHaveBeenCalledWith('Specimen'); + fireEvent.change(specimenName, { target: { value: 'Samples' } }); + expect(onRenameTable).toHaveBeenCalledWith('Specimen', 'Samples'); + + fireEvent.dragStart(screen.getByLabelText('Drag Patient')); + const specimenRow = specimenName.closest('li'); + expect(specimenRow).not.toBeNull(); + fireEvent.dragOver(specimenRow!); + fireEvent.drop(specimenRow!); + expect(onReorderTable).toHaveBeenCalledWith('Patient', undefined); + }); + + it('shows publication progress and prevents another publish', () => { + render( + , + ); + + const button = screen.getByRole('button', { name: 'Publishing…' }); + expect(button).toBeDisabled(); + expect(button).toHaveAttribute('aria-busy', 'true'); + expect(button.querySelector('.animate-spin')).not.toBeNull(); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.tsx new file mode 100644 index 00000000..60d09fc4 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.tsx @@ -0,0 +1,565 @@ +import React, { useMemo, useState } from 'react'; +import type { + ExplorerBuilderCandidate, + ExplorerBuilderCatalog, + ExplorerBuilderColumn, +} from '../../../types'; +import { derivedOccurrences, type DraftTable } from '../authoring/model'; + +const titleForResource = (value: string): string => + value + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/[_.]/g, ' ') + .replace(/\b\w/g, (letter) => letter.toUpperCase()); + +const safeName = (value: string): string => { + const normalized = value + .replace(/([a-z0-9])([A-Z])/g, '$1_$2') + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/^_+|_+$/g, ''); + if (!normalized) return 'column'; + return /^[0-9]/.test(normalized) ? `x_${normalized}` : normalized; +}; + +const candidateFieldName = (fieldPath: string): string => { + const segments = fieldPath + .replace(/^root\./, '') + .split('.') + .map((segment) => segment.replace(/\[\]/g, '')) + .filter(Boolean); + if (segments.length > 1 && segments[segments.length - 1] === 'value') + segments.pop(); + return safeName(segments.join('_')); +}; + +const isTabularCandidate = (candidate: ExplorerBuilderCandidate): boolean => + !['', 'unknown', 'object', 'array'].includes( + candidate.logicalType.trim().toLowerCase(), + ); + +type InitialPresentation = 'TABLE' | 'FILTER' | 'CHART'; + +const candidateColumnName = ( + candidate: ExplorerBuilderCandidate, + occurrenceId: string, + resourceType: string, + existing: ReadonlySet, +): string => { + const leaf = candidateFieldName(candidate.fieldPath); + const resourcePrefix = resourceType.trim() ? safeName(resourceType) : ''; + const base = + occurrenceId === 'base' + ? [resourcePrefix, leaf].filter(Boolean).join('_') + : `${safeName(occurrenceId)}__${leaf}`; + let value = base; + let suffix = 2; + while (existing.has(value)) value = `${base}_${suffix++}`; + return value; +}; + +const sourceSummary = (column: ExplorerBuilderColumn): string => + [ + column.source.kind, + column.source.fieldPath, + 'match' in column.source ? column.source.match : undefined, + ] + .filter(Boolean) + .join(' · '); + +const ConfiguredColumnRow = ({ + column, + order, + disabled, + filterable, + chartable, + onChange, + onRemove, +}: { + readonly column: ExplorerBuilderColumn; + readonly order: number; + readonly disabled: boolean; + readonly filterable: boolean; + readonly chartable: boolean; + readonly onChange: (value: ExplorerBuilderColumn) => void; + readonly onRemove: () => void; +}) => { + const [label, setLabel] = useState(column.label); + + const commitLabel = () => { + const next = label.trim(); + if (!next) { + setLabel(column.label); + return; + } + if (next !== column.label) onChange({ ...column, label: next }); + }; + const visible = column.table?.visible ?? Boolean(column.table); + + return ( +
+
+ setLabel(event.currentTarget.value)} + onBlur={commitLabel} + onKeyDown={(event) => { + if (event.key === 'Enter') event.currentTarget.blur(); + if (event.key === 'Escape') { + setLabel(column.label); + event.currentTarget.blur(); + } + }} + /> +
+ {column.column} · {sourceSummary(column)} +
+
+ + + + +
+ ); +}; + +const AvailableColumnRow = ({ + candidate, + disabled, + onAdd, +}: { + readonly candidate: ExplorerBuilderCandidate; + readonly disabled: boolean; + readonly onAdd: ( + displayName: string, + initialPresentation: InitialPresentation, + ) => void; +}) => { + const [displayName, setDisplayName] = useState(candidate.label); + const normalizedDisplayName = displayName.trim(); + + return ( +
+
+ setDisplayName(event.currentTarget.value)} + onBlur={() => { + if (!displayName.trim()) setDisplayName(candidate.label); + }} + /> +
+ {candidate.fieldPath} · {candidate.logicalType} + {candidate.repeated ? ' · repeated' : ''} +
+
+ + + + +
+ ); +}; + +export const ColumnSelector = ({ + catalog, + table, + occurrenceId, + disabled, + loadingCandidates = false, + onAdd, + onAddAll, + onChange, + onRemove, +}: { + readonly catalog: ExplorerBuilderCatalog; + readonly table?: DraftTable; + readonly occurrenceId: string; + readonly disabled: boolean; + readonly loadingCandidates?: boolean; + readonly onAdd: ( + candidate: ExplorerBuilderCandidate, + displayName: string, + initialPresentation: InitialPresentation, + ) => void; + readonly onAddAll: ( + candidates: ReadonlyArray, + ) => void; + readonly onChange: (column: ExplorerBuilderColumn) => void; + readonly onRemove: (column: string) => void; +}) => { + const [query, setQuery] = useState(''); + const occurrence = derivedOccurrences(table, catalog).find( + (candidate) => candidate.id === occurrenceId, + ); + const resourceType = + catalog.nodes.find((node) => node.nodeId === occurrence?.nodeId) + ?.resourceType ?? 'resource'; + const configured = useMemo( + () => + (table?.document.columns ?? []).filter( + (column) => column.occurrenceId === occurrenceId, + ), + [occurrenceId, table?.document.columns], + ); + const configuredFieldPaths = useMemo( + () => + new Set( + configured.flatMap((column) => + column.source.kind === 'field' && column.source.fieldPath + ? [column.source.fieldPath.replace(/^root\./, '')] + : [], + ), + ), + [configured], + ); + const configuredCapabilities = useMemo( + () => + new Map( + configured.map((column) => [ + column.column, + column.source.kind === 'field' + ? (catalog.candidates ?? []).find( + (candidate) => + candidate.nodeId === occurrence?.nodeId && + candidate.fieldPath.replace(/^root\./, '') === + column.source.fieldPath?.replace(/^root\./, ''), + ) + : undefined, + ]), + ), + [catalog.candidates, configured, occurrence?.nodeId], + ); + const available = useMemo( + () => + (catalog.candidates ?? []).filter( + (candidate) => + candidate.nodeId === occurrence?.nodeId && + isTabularCandidate(candidate) && + !configuredFieldPaths.has(candidate.fieldPath.replace(/^root\./, '')), + ), + [catalog.candidates, configuredFieldPaths, occurrence?.nodeId], + ); + const normalizedQuery = query.trim().toLowerCase(); + const rows = useMemo( + () => + [ + ...configured.map((column) => ({ + kind: 'configured' as const, + column, + })), + ...available.map((candidate) => ({ + kind: 'available' as const, + candidate, + })), + ] + .filter((row) => { + if (!normalizedQuery) return true; + const value = + row.kind === 'configured' + ? `${row.column.label} ${row.column.column} ${sourceSummary(row.column)}` + : `${row.candidate.label} ${row.candidate.fieldPath} ${row.candidate.logicalType}`; + return value.toLowerCase().includes(normalizedQuery); + }) + .sort((left, right) => { + const leftLabel = + left.kind === 'configured' + ? configuredCapabilities.get(left.column.column)?.label ?? left.column.source.fieldPath ?? left.column.column + : left.candidate.label; + const rightLabel = + right.kind === 'configured' + ? configuredCapabilities.get(right.column.column)?.label ?? right.column.source.fieldPath ?? right.column.column + : right.candidate.label; + return leftLabel.localeCompare(rightLabel); + }), + [available, configured, configuredCapabilities, normalizedQuery], + ); + const addCandidate = ( + candidate: ExplorerBuilderCandidate, + displayName: string, + initialPresentation: InitialPresentation, + ) => { + if (disabled) return; + onAdd(candidate, displayName, initialPresentation); + }; + const allTableColumnsSelected = + available.length === 0 && + configured.length > 0 && + configured.every( + (column) => column.table?.visible ?? Boolean(column.table), + ); + const toggleAllTableColumns = () => { + if (disabled) return; + if (allTableColumnsSelected) { + configured.forEach((column) => + onChange({ + ...column, + table: { + ...(column.table ?? {}), + visible: false, + }, + }), + ); + return; + } + configured + .filter((column) => !(column.table?.visible ?? Boolean(column.table))) + .forEach((column, order) => + onChange({ + ...column, + table: { + ...(column.table ?? {}), + visible: true, + order: column.table?.order ?? order, + }, + }), + ); + if (available.length > 0) onAddAll(available); + }; + + return ( + + ); +}; + +export const columnFromCandidate = ( + candidate: ExplorerBuilderCandidate, + occurrenceId: string, + existing: ReadonlyArray, + displayName = candidate.label, + resourceType = '', +): ExplorerBuilderColumn => { + const names = new Set(existing.map((column) => column.column)); + const order = + Math.max(-1, ...existing.map((column) => column.table?.order ?? -1)) + 1; + return { + column: candidateColumnName(candidate, occurrenceId, resourceType, names), + label: displayName.trim() || candidate.label, + logicalType: candidate.logicalType, + occurrenceId, + source: { + kind: 'field', + fieldPath: candidate.fieldPath, + projectionMode: candidate.defaultProjectionMode, + }, + table: { visible: true, order }, + }; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.unit.test.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.unit.test.tsx new file mode 100644 index 00000000..c9cba3ea --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.unit.test.tsx @@ -0,0 +1,366 @@ +// @vitest-environment jsdom +import React from 'react'; +import { vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import type { + ExplorerBuilderCandidate, + ExplorerBuilderCatalog, +} from '../../../types'; +import type { DraftTable } from '../authoring/model'; +import { ColumnSelector, columnFromCandidate } from './ColumnSelector'; + +const catalog: ExplorerBuilderCatalog = { + snapshotToken: 'snapshot', + generation: 'generation', + routePolicy: {}, + nodes: [ + { + nodeId: 'research-subject', + resourceType: 'ResearchSubject', + rowRootEligible: true, + populated: true, + documentCount: 3, + }, + ], + edges: [], + candidates: [], +}; + +const table: DraftTable = { + outputId: 'Patient', + tabId: 'patient', + title: 'Patient', + document: { + kind: 'ExplorerBuilderDocument', + output: { id: 'Patient', title: 'Patient' }, + rootResourceType: 'ResearchSubject', + route: { occurrenceId: 'base', resourceType: 'ResearchSubject' }, + columns: [ + { + column: 'research_subject_identifier', + label: 'Research Subject ID', + occurrenceId: 'base', + source: { + kind: 'field', + fieldPath: 'identifier[].value', + projectionMode: 'FIRST', + }, + table: { visible: true, order: 0 }, + }, + ], + }, +}; + +describe('configured V2 columns', () => { + it('renders document columns even when the catalog has no candidates', () => { + const onChange = vi.fn(); + render( + , + ); + + expect(screen.getByText('Research Subject columns')).toBeInTheDocument(); + expect(screen.getByText(/research_subject_identifier/)).toBeInTheDocument(); + expect(screen.getByText(/1 configured · 0 available/)).toBeInTheDocument(); + + const displayName = screen.getByRole('textbox', { + name: 'Display name for configured Research Subject ID', + }); + fireEvent.change(displayName, { target: { value: 'Subject ID' } }); + fireEvent.blur(displayName); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + column: 'research_subject_identifier', + label: 'Subject ID', + }), + ); + + fireEvent.click( + screen.getByRole('checkbox', { + name: 'Use Research Subject ID as filter', + }), + ); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + column: 'research_subject_identifier', + filter: { label: 'Research Subject ID' }, + }), + ); + }); + + it('keeps a configured column in place when its display name changes', () => { + const alpha: ExplorerBuilderCandidate = { + candidateId: 'c_alpha', + nodeId: 'research-subject', + fieldPath: 'alpha', + label: 'Alpha', + logicalType: 'string', + filterable: true, + chartable: true, + projectionModes: ['FIRST'], + defaultProjectionMode: 'FIRST', + }; + const beta: ExplorerBuilderCandidate = { + ...alpha, + candidateId: 'c_beta', + fieldPath: 'beta', + label: 'Beta', + }; + const initialTable: DraftTable = { + ...table, + document: { + ...table.document, + columns: [{ + column: 'alpha', + label: 'Alpha', + occurrenceId: 'base', + source: { kind: 'field', fieldPath: 'alpha', projectionMode: 'FIRST' }, + table: { visible: true, order: 0 }, + }], + }, + }; + const Harness = () => { + const [currentTable, setCurrentTable] = React.useState(initialTable); + return ( + setCurrentTable((current) => ({ + ...current, + document: { + ...current.document, + columns: current.document.columns.map((column) => column.column === next.column ? next : column), + }, + }))} + onRemove={vi.fn()} + /> + ); + }; + render(); + + const alphaInput = screen.getByRole('textbox', { name: 'Display name for configured Alpha' }); + expect(screen.getAllByRole('textbox', { name: /Display name for/ }).map((input) => (input as HTMLInputElement).value)).toEqual(['Alpha', 'Beta']); + fireEvent.change(alphaInput, { target: { value: 'Zulu' } }); + fireEvent.blur(alphaInput); + + expect(screen.getAllByRole('textbox', { name: /Display name for/ }).map((input) => (input as HTMLInputElement).value)).toEqual(['Zulu', 'Beta']); + }); + + it('shows primitive leaf fields, hides object containers, and adds from the table checkbox', () => { + const candidate: ExplorerBuilderCandidate = { + candidateId: 'c_birth_date', + nodeId: 'research-subject', + fieldPath: 'birthDate', + label: 'Birth date', + logicalType: 'date', + filterable: true, + chartable: true, + projectionModes: ['FIRST'], + defaultProjectionMode: 'FIRST', + }; + const onAdd = vi.fn(); + const onAddAll = vi.fn(); + render( + , + ); + + expect(screen.getByText(/1 configured · 1 available/)).toBeInTheDocument(); + expect(screen.queryByDisplayValue('identifier')).not.toBeInTheDocument(); + const displayName = screen.getByRole('textbox', { + name: 'Display name for available Birth date', + }); + fireEvent.change(displayName, { target: { value: 'Date of birth' } }); + fireEvent.click( + screen.getByRole('checkbox', { name: 'Add Date of birth to table' }), + ); + expect(onAdd).toHaveBeenCalledWith(candidate, 'Date of birth', 'TABLE'); + + fireEvent.click( + screen.getByRole('checkbox', { name: 'Add Date of birth as filter' }), + ); + expect(onAdd).toHaveBeenCalledWith(candidate, 'Date of birth', 'FILTER'); + + fireEvent.click( + screen.getByRole('checkbox', { name: 'Add Date of birth as chart' }), + ); + expect(onAdd).toHaveBeenCalledWith(candidate, 'Date of birth', 'CHART'); + + fireEvent.click( + screen.getByRole('button', { name: 'Select all table columns' }), + ); + expect(onAddAll).toHaveBeenCalledWith([candidate]); + }); + + it('disables only presentations the catalog reports as unsupported', () => { + const candidate: ExplorerBuilderCandidate = { + candidateId: 'c_status', + nodeId: 'research-subject', + fieldPath: 'status', + label: 'Status', + logicalType: 'string', + filterable: true, + chartable: false, + projectionModes: ['FIRST'], + defaultProjectionMode: 'FIRST', + }; + render( + , + ); + + expect( + screen.getByRole('checkbox', { name: 'Add Status as filter' }), + ).toBeEnabled(); + expect( + screen.getByRole('checkbox', { name: 'Add Status as chart' }), + ).toBeDisabled(); + expect( + screen.getByRole('checkbox', { + name: 'Use Research Subject ID as filter', + }), + ).toBeDisabled(); + expect( + screen.getByRole('checkbox', { + name: 'Use Research Subject ID as chart', + }), + ).toBeDisabled(); + }); + + it('toggles all configured table columns off without removing their configuration', () => { + const onChange = vi.fn(); + render( + , + ); + + const toggle = screen.getByRole('button', { + name: 'Deselect all table columns', + }); + expect(toggle).toHaveAttribute('aria-pressed', 'true'); + fireEvent.click(toggle); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + column: 'research_subject_identifier', + table: expect.objectContaining({ visible: false }), + }), + ); + }); + + it('converts a chosen candidate to one durable typed V2 column', () => { + const column = columnFromCandidate( + { + candidateId: 'c_birth_date', + nodeId: 'research-subject', + fieldPath: 'birthDate', + label: 'Birth date', + logicalType: 'date', + filterable: true, + chartable: true, + projectionModes: ['FIRST'], + defaultProjectionMode: 'FIRST', + }, + 'patient-step', + table.document.columns, + 'Date of birth', + ); + + expect(column).toMatchObject({ + column: 'patient_step__birth_date', + label: 'Date of birth', + occurrenceId: 'patient-step', + source: { + kind: 'field', + fieldPath: 'birthDate', + projectionMode: 'FIRST', + }, + table: { visible: true, order: 1 }, + }); + }); + + it('derives readable base column keys from resource type and FHIR leaf path', () => { + const column = columnFromCandidate( + { + candidateId: 'c_5d11d8d24ce3124cea823c9e', + nodeId: 'research-subject', + fieldPath: 'identifier[].value', + label: 'Research Subject ID', + logicalType: 'string', + repeated: true, + filterable: true, + chartable: false, + projectionModes: ['FIRST', 'ALL'], + defaultProjectionMode: 'FIRST', + }, + 'base', + [], + 'Research Subject ID', + 'ResearchSubject', + ); + + expect(column.column).toBe('research_subject_identifier'); + expect(column.source.fieldPath).toBe('identifier[].value'); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.tsx new file mode 100644 index 00000000..418b7338 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.tsx @@ -0,0 +1,650 @@ +import React, { useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Background, Controls, MarkerType, ReactFlow } from '@xyflow/react'; +import type { Edge, Node } from '@xyflow/react'; +import type { ExplorerBuilderCatalog } from '../../../types'; +import { derivedOccurrences, type DraftTable } from '../authoring/model'; +import { + legalOutgoingEdges, + replaceableIncomingEdges, +} from '../authoring/routeActions'; +import { RouteExtensionPanel } from './RouteExtensionPanel'; +import { useEscapeToClose } from '../hooks/useEscapeToClose'; +import { useGraphLayout } from '../hooks/useGraphLayout'; +import { useGraphViewportFitter } from '../hooks/useGraphViewportFitter'; +import { useTraversalOverflow } from '../hooks/useTraversalOverflow'; + +const GraphViewportFitter = ({ + expanded, + graphIdentity, + hostRef, + nodeCount, +}: { + readonly expanded: boolean; + readonly graphIdentity: string; + readonly hostRef: React.RefObject; + readonly nodeCount: number; +}) => { + useGraphViewportFitter({ expanded, graphIdentity, hostRef, nodeCount }); + return null; +}; + +export const graphEdgeOverviewLabels = ( + edges: ExplorerBuilderCatalog['edges'], +): ReadonlyMap => { + const groups = new Map }>(); + for (const edge of edges) { + const endpoints = [edge.fromNodeId, edge.toNodeId].sort(); + const key = `${endpoints[0]}\u0000${endpoints[1]}`; + const group = groups.get(key) ?? { owner: edge.edgeId, labels: new Set() }; + group.labels.add(edge.label.trim()); + groups.set(key, group); + } + return new Map([...groups.values()].map((group) => [ + group.owner, + group.labels.size === 1 ? [...group.labels][0] : `${group.labels.size} relationships`, + ])); +}; + +export const GuidedGraphWorkspace = ({ + catalog, + table, + selectedOccurrenceId, + disabled, + onSelectOccurrence, + onSetBase, + onChangeBase, + onAppendEdge, + onChangeEdge, + onTruncate, + onTableToolbarHostChange, +}: { + readonly catalog: ExplorerBuilderCatalog; + readonly table?: DraftTable; + readonly selectedOccurrenceId: string; + readonly disabled: boolean; + readonly onSelectOccurrence: (id: string) => void; + readonly onSetBase: (nodeId: string) => void; + readonly onChangeBase: (nodeId: string) => void; + readonly onAppendEdge: ( + parentOccurrenceId: string, + edgeId: string, + nodeId: string, + ) => void; + readonly onChangeEdge: (occurrenceId: string, edgeId: string) => void; + readonly onTruncate: (occurrenceId: string) => void; + readonly onTableToolbarHostChange?: (host: HTMLDivElement | null) => void; +}) => { + const graphHostRef = React.useRef(null); + const traversalRef = React.useRef(null); + const [isExpanded, setIsExpanded] = useState(false); + const [showOrphans, setShowOrphans] = useState(false); + const catalogIdentity = useMemo( + () => + JSON.stringify({ + nodes: catalog.nodes.map((node) => node.nodeId), + edges: catalog.edges.map((edge) => [ + edge.edgeId, + edge.fromNodeId, + edge.toNodeId, + ]), + }), + [catalog.edges, catalog.nodes], + ); + const layoutNodes = useMemo( + () => + catalog.nodes.map((node) => ({ + id: node.nodeId, + width: 170, + height: 54, + })), + [catalog.nodes], + ); + const layoutEdges = useMemo( + () => + catalog.edges.map((edge) => ({ + id: edge.edgeId, + source: edge.fromNodeId, + target: edge.toNodeId, + })), + [catalog.edges], + ); + const layout = useGraphLayout(catalogIdentity, layoutNodes, layoutEdges); + useEscapeToClose(isExpanded, setIsExpanded); + const tableIdentity = `${table?.outputId ?? ''}:${table?.document.rootResourceType ?? ''}`; + const [inspection, setInspection] = useState<{ + readonly tableIdentity: string; + readonly occurrenceId: string; + readonly nodeId?: string; + readonly edgeId?: string; + readonly allowExistingExtension?: boolean; + }>({ tableIdentity, occurrenceId: selectedOccurrenceId }); + const activeInspectedNodeId = + inspection.tableIdentity === tableIdentity ? inspection.nodeId : undefined; + const activeSelectedEdgeId = + inspection.tableIdentity === tableIdentity && + inspection.occurrenceId === selectedOccurrenceId + ? inspection.edgeId + : undefined; + const activeAllowExistingExtension = + inspection.tableIdentity === tableIdentity && + inspection.occurrenceId === selectedOccurrenceId && + inspection.allowExistingExtension === true; + const updateInspection = ( + nodeId?: string, + edgeId?: string, + allowExistingExtension = false, + ) => + setInspection({ + tableIdentity, + occurrenceId: selectedOccurrenceId, + nodeId, + edgeId, + allowExistingExtension, + }); + const selectEdge = (edgeId?: string) => + updateInspection( + activeInspectedNodeId, + edgeId, + activeAllowExistingExtension, + ); + const occurrences = useMemo( + () => + derivedOccurrences(table, catalog).map((occurrence) => ({ + occurrenceId: occurrence.id, + index: occurrence.index, + nodeId: occurrence.nodeId, + resourceType: + catalog.nodes.find((node) => node.nodeId === occurrence.nodeId) + ?.resourceType ?? occurrence.nodeId, + incomingEdgeId: occurrence.incomingEdgeId, + relationship: occurrence.relationship, + parentId: occurrence.parentId, + depth: occurrence.depth, + })), + [catalog, table], + ); + const traversalIdentity = `${isExpanded}:${occurrences.map((occurrence) => occurrence.occurrenceId).join(',')}`; + const isTraversalOverflowing = useTraversalOverflow( + graphHostRef, + traversalRef, + traversalIdentity, + ); + const occurrencesByNode = useMemo(() => { + const result = new Map(); + occurrences.forEach((occurrence) => { + result.set(occurrence.nodeId, [ + ...(result.get(occurrence.nodeId) ?? []), + occurrence, + ]); + }); + return result; + }, [occurrences]); + const editableEdgesByOccurrence = useMemo( + () => + new Map( + occurrences.map((occurrence) => [ + occurrence.occurrenceId, + replaceableIncomingEdges( + catalog, + table, + occurrence.occurrenceId, + ), + ]), + ), + [catalog, occurrences, table], + ); + const legalNextEdges = useMemo( + () => legalOutgoingEdges(catalog, table, selectedOccurrenceId), + [catalog, selectedOccurrenceId, table], + ); + const legalNextNodeIds = useMemo( + () => new Set(legalNextEdges.map((edge) => edge.toNodeId)), + [legalNextEdges], + ); + const nodes: Node[] = catalog.nodes.map((node, index) => { + const nodeOccurrences = occurrencesByNode.get(node.nodeId) ?? []; + const inRoute = nodeOccurrences.length > 0; + const isPendingRowStart = + occurrences.length === 0 && + node.rowRootEligible && + activeInspectedNodeId === node.nodeId; + const isSelected = + isPendingRowStart || + nodeOccurrences.some( + (occurrence) => occurrence.occurrenceId === selectedOccurrenceId, + ); + const isInspected = + !isPendingRowStart && activeInspectedNodeId === node.nodeId; + const isReachable = legalNextNodeIds.has(node.nodeId); + const canStart = occurrences.length === 0 && node.rowRootEligible; + const duplicate = + catalog.nodes.filter( + (candidate) => candidate.resourceType === node.resourceType, + ).length > 1; + return { + id: node.nodeId, + position: layout.positions.get(node.nodeId) ?? { + x: (index % 4) * 210, + y: Math.floor(index / 4) * 100, + }, + data: { + label: ( +
+
+ {node.resourceType} +
+
+ {isSelected + ? 'Editing this query node' + : isInspected + ? 'Selected resource' + : inRoute + ? 'In traversal' + : canStart + ? 'Available row start' + : occurrences.length === 0 + ? 'Not eligible as a row start' + : isReachable + ? 'Click to add to query' + : node.rowRootEligible + ? 'Click to start a new query' + : 'Available resource'} +
+ {duplicate && ( +
+ {node.nodeId.slice(-8)} +
+ )} + {nodeOccurrences.length > 1 && ( +
+ {nodeOccurrences.length} query occurrences +
+ )} +
+ {node.rowGrain ?? 'row grain unavailable'} ·{' '} + {node.populated ? node.documentCount.toLocaleString() : '0'}{' '} + documents +
+
+ ), + }, + style: { + border: isSelected + ? '3px solid #7c3aed' + : isInspected + ? '3px solid #f59e0b' + : inRoute + ? '3px solid #2f5aac' + : isReachable + ? '3px solid #16a34a' + : '1px solid #94a3b8', + borderRadius: 12, + background: isSelected + ? '#f3e8ff' + : isInspected + ? '#fffbeb' + : inRoute + ? '#dbeafe' + : isReachable + ? '#f0fdf4' + : '#ffffff', + boxShadow: + isSelected || isInspected || inRoute || isReachable + ? '0 8px 24px rgba(30,64,175,.18)' + : '0 3px 10px rgba(15,23,42,.08)', + opacity: isSelected || isInspected || inRoute || isReachable ? 1 : 0.42, + padding: 8, + width: 190, + cursor: disabled ? 'default' : 'pointer', + pointerEvents: disabled ? 'none' : 'auto', + }, + }; + }); + const edgeOverviewLabels = useMemo( + () => graphEdgeOverviewLabels(catalog.edges), + [catalog.edges], + ); + const edges: Edge[] = catalog.edges.map((edge) => { + const isRouteEdge = occurrences.some( + (occurrence) => occurrence.incomingEdgeId === edge.edgeId, + ); + const isLegalNextEdge = legalNextEdges.some( + (candidate) => candidate.edgeId === edge.edgeId, + ); + const edgeColor = isRouteEdge + ? '#2563eb' + : isLegalNextEdge + ? '#16a34a' + : '#64748b'; + const edgeWidth = isRouteEdge ? 3 : isLegalNextEdge ? 2.5 : 1.75; + return { + id: edge.edgeId, + // Loom's catalog edges are directed. Keep the catalog orientation in + // React Flow; do not synthesize a reverse edge for FHIR relationships. + source: edge.fromNodeId, + target: edge.toNodeId, + label: edgeOverviewLabels.get(edge.edgeId), + type: 'smoothstep', + markerEnd: { + type: MarkerType.ArrowClosed, + color: edgeColor, + width: 16, + height: 16, + strokeWidth: 1.5, + }, + animated: isRouteEdge, + style: { + stroke: edgeColor, + strokeWidth: edgeWidth, + opacity: isRouteEdge + ? 1 + : occurrences.length === 0 || isLegalNextEdge + ? 0.7 + : 0.28, + }, + labelStyle: { + fill: '#475569', + fontSize: 9, + fontWeight: 600, + }, + labelBgStyle: { + fill: '#ffffff', + fillOpacity: 0.9, + }, + labelBgPadding: [4, 2], + labelBgBorderRadius: 4, + }; + }); + const nodeIds = new Set(nodes.map((node) => node.id)); + const connectedNodeIds = new Set( + edges.flatMap((edge) => + nodeIds.has(edge.source) && nodeIds.has(edge.target) + ? [edge.source, edge.target] + : [], + ), + ); + const catalogNodes = showOrphans + ? nodes + : nodes.filter((node) => connectedNodeIds.has(node.id)); + const catalogNodeIds = new Set(catalogNodes.map((node) => node.id)); + const catalogEdges = edges.filter( + (edge) => + catalogNodeIds.has(edge.source) && catalogNodeIds.has(edge.target), + ); + const nodesForViewport = catalogNodes; + const graphIdentity = `${layout.identity ?? 'layout-pending'}:${nodesForViewport.map((node) => node.id).join(',')}`; + const inspectNode = (nodeId: string) => { + if (disabled) return; + if (occurrences.length === 0) { + updateInspection(nodeId); + const node = catalog.nodes.find( + (candidate) => candidate.nodeId === nodeId, + ); + if (node?.rowRootEligible) onSetBase(nodeId); + return; + } + const routeOccurrences = occurrences.filter( + (occurrence) => occurrence.nodeId === nodeId, + ); + const matchingEdges = legalNextEdges.filter( + (edge) => edge.toNodeId === nodeId, + ); + if (routeOccurrences.length > 0 && matchingEdges.length > 0) { + updateInspection(nodeId, undefined, true); + return; + } + if (routeOccurrences.length === 1) { + updateInspection(); + onSelectOccurrence(routeOccurrences[0].occurrenceId); + return; + } else if (routeOccurrences.length > 1) { + const currentIndex = routeOccurrences.findIndex( + (occurrence) => occurrence.occurrenceId === selectedOccurrenceId, + ); + const next = + routeOccurrences[(currentIndex + 1) % routeOccurrences.length]; + updateInspection(); + onSelectOccurrence(next.occurrenceId); + return; + } + if (matchingEdges.length === 1) { + updateInspection(nodeId); + onAppendEdge(selectedOccurrenceId, matchingEdges[0].edgeId, nodeId); + return; + } + updateInspection(nodeId); + }; + const inspectEdge = (edgeId: string) => { + if (disabled) return; + const edge = catalog.edges.find((candidate) => candidate.edgeId === edgeId); + if (!edge) return; + updateInspection(edge.toNodeId, edgeId); + const routeOccurrence = occurrences + .filter((occurrence) => occurrence.nodeId === edge.toNodeId) + .at(-1); + if (routeOccurrence) onSelectOccurrence(routeOccurrence.occurrenceId); + }; + const useAsRowStart = (nodeId: string) => { + if (disabled) return; + onSetBase(nodeId); + selectEdge(undefined); + }; + const addRelationship = (edgeId: string, nodeId: string) => { + if (disabled || !legalNextEdges.some((edge) => edge.edgeId === edgeId)) + return; + onAppendEdge(selectedOccurrenceId, edgeId, nodeId); + updateInspection(); + }; + const content = ( + <> + {isExpanded &&
} +
+
+
+

+ Dataset graph{' '} + + {catalog.nodes.length} resources · {catalog.edges.length}{' '} + relationships + +

+
+
+
+ {isExpanded && ( + + )} +
+
+
+ { + void instance.fitView({ + padding: 0.08, + minZoom: 0.05, + maxZoom: 1.8, + duration: 200, + }); + }} + onNodeClick={(event, node) => { + event.stopPropagation(); + inspectNode(node.id); + }} + onEdgeClick={(event, edge) => { + event.stopPropagation(); + inspectEdge(edge.id); + }} + onPaneClick={(event) => { + const target = event.target; + if ( + target instanceof Element && + target.closest('.react-flow__node, .react-flow__edge') + ) + return; + if (!isExpanded) setIsExpanded(true); + }} + > + + + + + + + +
+
+ + ); + return isExpanded && typeof document !== 'undefined' + ? createPortal(content, document.body) + : content; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.unit.test.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.unit.test.tsx new file mode 100644 index 00000000..f52fca6c --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.unit.test.tsx @@ -0,0 +1,601 @@ +// @vitest-environment jsdom +import React from 'react'; +import { vi } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import type { ExplorerBuilderCatalog } from '../../../types'; +import type { DraftTable } from '../authoring/model'; +import { graphEdgeOverviewLabels, GuidedGraphWorkspace } from './GuidedGraphWorkspace'; + +const mockFitView = vi.fn(); +const mockLayoutDatasetGraph = vi.fn(); + +vi.mock('@xyflow/react', () => { + const ReactFlow = ({ + nodes, + edges, + children, + onNodeClick, + onPaneClick, + }: { + readonly nodes: ReadonlyArray<{ + readonly id: string; + readonly data: { readonly label: React.ReactNode }; + readonly style?: { readonly border?: string }; + }>; + readonly edges: ReadonlyArray<{ + readonly id: string; + readonly source: string; + readonly target: string; + readonly markerEnd?: { readonly type?: string }; + }>; + readonly children?: React.ReactNode; + readonly onNodeClick?: ( + event: unknown, + node: { readonly id: string }, + ) => void; + readonly onPaneClick?: (event: React.MouseEvent) => void; + }) => ( +
onPaneClick?.(event)} + > + {nodes.map((node) => ( + + ))} + {edges.map((edge) => ( +
+ ))} + + {children} +
+ ); + return { + Background: () => null, + Controls: () => null, + MarkerType: { ArrowClosed: 'arrowclosed' }, + ReactFlow, + useNodesInitialized: () => true, + useReactFlow: () => ({ + fitView: mockFitView, + viewportInitialized: true, + }), + }; +}); + +vi.mock('../graphLayout', () => ({ + layoutDatasetGraph: (...args: unknown[]) => mockLayoutDatasetGraph(...args), +})); + +vi.mock('./RouteExtensionPanel', () => ({ + RouteExtensionPanel: () => null, +})); + +const catalog: ExplorerBuilderCatalog = { + snapshotToken: 'snapshot', + generation: 'generation', + routePolicy: { allowRepeatedEdges: true, allowSelfLoops: true }, + nodes: [ + { + nodeId: 'node-patient', + resourceType: 'Patient', + rowRootEligible: true, + populated: true, + documentCount: 3, + }, + { + nodeId: 'node-document', + resourceType: 'DocumentReference', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'node-file', + resourceType: 'File', + rowRootEligible: false, + populated: true, + documentCount: 1, + }, + { + nodeId: 'node-orphan', + resourceType: 'Condition', + rowRootEligible: false, + populated: false, + documentCount: 0, + }, + ], + edges: [ + { + edgeId: 'patient-document', + fromNodeId: 'node-patient', + toNodeId: 'node-document', + label: 'has document', + }, + { + edgeId: 'document-file', + fromNodeId: 'node-document', + toNodeId: 'node-file', + label: 'references file', + }, + ], + candidates: [], +}; + +const table = (rootNodeId: string): DraftTable => ({ + outputId: 'documents', + tabId: 'documents', + title: 'Documents', + document: { + kind: 'ExplorerBuilderDocument', + output: { id: 'documents', title: 'Documents' }, + rootResourceType: + catalog.nodes.find((node) => node.nodeId === rootNodeId)?.resourceType ?? + rootNodeId, + route: { + occurrenceId: 'base', + resourceType: + catalog.nodes.find((node) => node.nodeId === rootNodeId) + ?.resourceType ?? rootNodeId, + }, + columns: [], + }, +}); + +const renderGraph = (currentTable: DraftTable) => + render( + , + ); + +describe('GuidedGraphWorkspace', () => { + it('summarizes multiple relationships between the same resource pair', () => { + const labels = graphEdgeOverviewLabels([ + { edgeId: 'patient-specimen', fromNodeId: 'patient', toNodeId: 'specimen', label: 'subject_Patient' }, + { edgeId: 'specimen-patient', fromNodeId: 'specimen', toNodeId: 'patient', label: 'subject_Patient' }, + { edgeId: 'patient-focus', fromNodeId: 'patient', toNodeId: 'specimen', label: 'focus_Patient' }, + { edgeId: 'specimen-observation', fromNodeId: 'specimen', toNodeId: 'observation', label: 'focus_Specimen' }, + ]); + + expect([...labels]).toEqual([ + ['patient-specimen', '2 relationships'], + ['specimen-observation', 'focus_Specimen'], + ]); + }); + + beforeEach(() => { + mockFitView.mockReset(); + mockLayoutDatasetGraph.mockReset(); + mockLayoutDatasetGraph.mockImplementation(async (nodes) => ({ + positions: new Map( + nodes.map((node: { id: string }, index: number) => [ + node.id, + { x: index * 220, y: 0 }, + ]), + ), + routes: new Map(), + })); + }); + + it('shows the full graph and optionally includes orphans', async () => { + renderGraph(table('node-document')); + + await waitFor(() => + expect(screen.getByTestId('mock-react-flow')).toHaveAttribute( + 'data-node-count', + '3', + ), + ); + expect(screen.getByTestId('mock-react-flow')).toHaveAttribute( + 'data-edge-count', + '2', + ); + expect(screen.getByTestId('graph-edge-patient-document')).toHaveAttribute( + 'data-source', + 'node-patient', + ); + expect(screen.getByTestId('graph-edge-patient-document')).toHaveAttribute( + 'data-target', + 'node-document', + ); + expect(screen.getByTestId('graph-edge-patient-document')).toHaveAttribute( + 'data-marker-type', + 'arrowclosed', + ); + expect(screen.getByTestId('graph-node-node-document')).toBeInTheDocument(); + expect(screen.getByTestId('graph-node-node-patient')).toBeInTheDocument(); + expect( + screen.queryByTestId('graph-node-node-orphan'), + ).not.toBeInTheDocument(); + await waitFor(() => expect(mockFitView).toHaveBeenCalled()); + + fireEvent.click(screen.getByRole('checkbox', { name: 'Show orphans' })); + + expect(screen.getByTestId('mock-react-flow')).toHaveAttribute( + 'data-node-count', + '4', + ); + expect(screen.getByTestId('graph-node-node-orphan')).toBeInTheDocument(); + + expect( + screen.queryByRole('button', { name: 'Focus route' }), + ).not.toBeInTheDocument(); + }); + + it('keeps the graph populated when a stale route id does not match catalog nodes', async () => { + renderGraph(table('DocumentReference')); + + await waitFor(() => + expect(screen.getByTestId('mock-react-flow')).toHaveAttribute( + 'data-node-count', + '3', + ), + ); + expect(screen.getByTestId('mock-react-flow')).toHaveAttribute( + 'data-edge-count', + '2', + ); + }); + + it('starts a blank table directly from any eligible graph resource without expanding', async () => { + const onSetBase = vi.fn(); + render( + , + ); + + await waitFor(() => + expect(screen.getByTestId('graph-node-node-patient')).toBeInTheDocument(), + ); + fireEvent.click(screen.getByTestId('graph-node-node-patient')); + + expect(onSetBase).toHaveBeenCalledWith('node-patient'); + expect(screen.getByTestId('graph-node-node-patient')).toHaveAttribute( + 'data-border', + '3px solid #7c3aed', + ); + expect( + screen.queryByRole('dialog', { name: 'Expanded dataset graph' }), + ).not.toBeInTheDocument(); + }); + + it('does not revive an inspected node after switching tables', async () => { + const props = { + catalog, + selectedOccurrenceId: 'base', + disabled: false, + onSelectOccurrence: vi.fn(), + onSetBase: vi.fn(), + onChangeBase: vi.fn(), + onAppendEdge: vi.fn(), + onChangeEdge: vi.fn(), + onTruncate: vi.fn(), + }; + const view = render( + , + ); + await waitFor(() => expect(mockFitView).toHaveBeenCalled()); + + fireEvent.click(screen.getByRole('checkbox', { name: 'Show orphans' })); + fireEvent.click(screen.getByTestId('graph-node-node-orphan')); + expect(screen.getByTestId('graph-node-node-orphan')).toHaveAttribute( + 'data-border', + '3px solid #f59e0b', + ); + + const otherTable = { + ...table('node-document'), + outputId: 'other-table', + }; + view.rerender(); + view.rerender(); + await waitFor(() => expect(mockFitView).toHaveBeenCalled()); + + expect(screen.getByTestId('graph-node-node-orphan')).toHaveAttribute( + 'data-border', + '1px solid #94a3b8', + ); + }); + + it('renders and independently selects every sibling in a configured route tree', async () => { + const siblingCatalog: ExplorerBuilderCatalog = { + ...catalog, + nodes: [ + { + nodeId: 'node-specimen', + resourceType: 'Specimen', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'node-observation', + resourceType: 'Observation', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'node-patient', + resourceType: 'Patient', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + ], + edges: [ + { + edgeId: 'specimen-observation', + fromNodeId: 'node-specimen', + toNodeId: 'node-observation', + label: 'focus_Specimen', + }, + { + edgeId: 'specimen-patient', + fromNodeId: 'node-specimen', + toNodeId: 'node-patient', + label: 'subject_Patient', + }, + { + edgeId: 'specimen-patient-participant', + fromNodeId: 'node-specimen', + toNodeId: 'node-patient', + label: 'participant_Patient', + }, + ], + }; + const siblingTable: DraftTable = { + outputId: 'Specimen', + tabId: 'Specimen', + title: 'Specimen', + document: { + kind: 'ExplorerBuilderDocument', + output: { id: 'Specimen', title: 'Specimen' }, + rootResourceType: 'Specimen', + route: { + occurrenceId: 'base', + resourceType: 'Specimen', + children: [ + { + occurrenceId: 'observation', + resourceType: 'Observation', + relationship: 'focus_Specimen', + }, + { + occurrenceId: 'patient', + resourceType: 'Patient', + relationship: 'subject_Patient', + }, + ], + }, + columns: [], + }, + }; + const onSelectOccurrence = vi.fn(); + const onChangeEdge = vi.fn(); + + render( + , + ); + + await waitFor(() => + expect(screen.getByTestId('mock-react-flow')).toHaveAttribute( + 'data-node-count', + '3', + ), + ); + expect( + screen.getByRole('button', { name: 'Observation' }), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Patient' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Patient' })); + expect(onSelectOccurrence).toHaveBeenCalledWith('patient'); + fireEvent.change( + screen.getByRole('combobox', { + name: 'Relationship for Patient occurrence', + }), + { target: { value: 'specimen-patient-participant' } }, + ); + expect(onChangeEdge).toHaveBeenCalledWith( + 'patient', + 'specimen-patient-participant', + ); + }); + + it('inspects an existing resource when another relationship can add a new occurrence', async () => { + const multiRelationshipCatalog: ExplorerBuilderCatalog = { + ...catalog, + nodes: [ + { + nodeId: 'node-specimen', + resourceType: 'Specimen', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'node-patient', + resourceType: 'Patient', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + ], + edges: [ + { + edgeId: 'specimen-patient-subject', + fromNodeId: 'node-specimen', + toNodeId: 'node-patient', + label: 'subject_Patient', + }, + { + edgeId: 'specimen-patient-participant', + fromNodeId: 'node-specimen', + toNodeId: 'node-patient', + label: 'participant_Patient', + }, + ], + }; + const tableWithPatient: DraftTable = { + outputId: 'Specimen', + tabId: 'Specimen', + title: 'Specimen', + document: { + kind: 'ExplorerBuilderDocument', + output: { id: 'Specimen', title: 'Specimen' }, + rootResourceType: 'Specimen', + route: { + occurrenceId: 'base', + resourceType: 'Specimen', + children: [ + { + occurrenceId: 'patient-subject', + resourceType: 'Patient', + relationship: 'subject_Patient', + }, + ], + }, + columns: [], + }, + }; + const onSelectOccurrence = vi.fn(); + const onAppendEdge = vi.fn(); + + render( + , + ); + + await waitFor(() => + expect(screen.getByTestId('graph-node-node-patient')).toBeInTheDocument(), + ); + fireEvent.click(screen.getByTestId('graph-node-node-patient')); + + expect(onSelectOccurrence).not.toHaveBeenCalled(); + expect(onAppendEdge).not.toHaveBeenCalled(); + expect(screen.getByTestId('graph-node-node-patient')).toHaveAttribute( + 'data-border', + '3px solid #f59e0b', + ); + }); + + it('keeps node interaction inline, expands from the background, and directly adds one legal edge', async () => { + const onAppendEdge = vi.fn(); + const onTableToolbarHostChange = vi.fn(); + render( + , + ); + + await waitFor(() => + expect(screen.getByTestId('graph-node-node-file')).toBeInTheDocument(), + ); + expect( + screen.queryByRole('button', { name: 'Expand graph' }), + ).not.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('graph-node-node-file')); + + expect(onAppendEdge).toHaveBeenCalledWith( + 'base', + 'document-file', + 'node-file', + ); + expect( + screen.queryByRole('dialog', { name: 'Expanded dataset graph' }), + ).not.toBeInTheDocument(); + expect(screen.getByTestId('graph-node-node-file')).toHaveAttribute( + 'data-border', + '3px solid #f59e0b', + ); + + fireEvent.click(screen.getByRole('button', { name: 'Graph background' })); + + const expandedGraph = screen.getByRole('dialog', { + name: 'Expanded dataset graph', + }); + expect(expandedGraph).toBeInTheDocument(); + expect(expandedGraph).toHaveClass('fixed', 'inset-3'); + expect(expandedGraph).not.toHaveClass('relative'); + + fireEvent.click(screen.getByRole('button', { name: 'Close graph' })); + + expect( + screen.queryByRole('dialog', { name: 'Expanded dataset graph' }), + ).not.toBeInTheDocument(); + expect( + onTableToolbarHostChange.mock.calls.some(([host]) => host === null), + ).toBe(true); + const currentHost = onTableToolbarHostChange.mock.calls.at(-1)?.[0]; + expect(currentHost).toBeInstanceOf(HTMLDivElement); + expect(currentHost?.isConnected).toBe(true); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PresentationPanels.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PresentationPanels.tsx new file mode 100644 index 00000000..8623b503 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PresentationPanels.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +export const PresentationPanels = ({ + features, +}: { + readonly features?: { + readonly sharedFilters: boolean; + readonly fixedFilters: boolean; + readonly fileActions: boolean; + }; +}) => ( +
+ + Shared filters and file actions + +
+
+ Shared filters +

+ {features?.sharedFilters + ? 'Configure shared filters.' + : 'Disabled because Loom Authoring V2 does not expose safe shared-filter intent.'} +

+
+
+ Fixed filters +

+ {features?.fixedFilters + ? 'Configure fixed filters.' + : 'Disabled because Loom Authoring V2 does not expose safe fixed-filter intent.'} +

+
+
+ File actions +

+ {features?.fileActions + ? 'Configure file actions.' + : 'Disabled because Loom Authoring V2 does not expose safe file-action intent.'} +

+
+
+
+); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.tsx new file mode 100644 index 00000000..eaa5abe6 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.tsx @@ -0,0 +1,348 @@ +import React, { useState } from 'react'; +import type { + ExplorerBuilderColumn, + ExplorerBuilderEmission, + ExplorerBuilderPreviewResult, +} from '../../../types'; +import type { DraftTable } from '../authoring/model'; +import { useDismissibleLayer } from './useDismissibleLayer'; + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const scalarText = (value: unknown): string | undefined => { + if (typeof value === 'string') return value.trim() || undefined; + if ( + typeof value === 'number' || + typeof value === 'boolean' || + typeof value === 'bigint' + ) { + return String(value); + } + return undefined; +}; + +export const formatPreviewCell = (value: unknown, depth = 0): string => { + if (value === undefined || value === null) return '—'; + const scalar = scalarText(value); + if (scalar !== undefined) return scalar; + if (Array.isArray(value)) { + const items = value + .map((item) => formatPreviewCell(item, depth + 1)) + .filter((item) => item !== '—'); + return items.length > 0 ? items.join('; ') : '—'; + } + if (isRecord(value)) { + // Prefer the human-bearing fields used by common FHIR datatypes. + for (const key of ['text', 'display', 'value', 'code']) { + const preferred = scalarText(value[key]); + if (preferred !== undefined) return preferred; + } + if (value.reference !== undefined) { + return formatPreviewCell(value.reference, depth + 1); + } + if (value.coding !== undefined) { + return formatPreviewCell(value.coding, depth + 1); + } + if (depth < 3) { + const parts = Object.entries(value) + .map(([key, nested]) => { + const formatted = formatPreviewCell(nested, depth + 1); + return formatted === '—' ? undefined : `${key}: ${formatted}`; + }) + .filter((part): part is string => part !== undefined); + if (parts.length > 0) return parts.join(' · '); + } + } + return previewCellTitle(value); +}; + +export const previewCellTitle = (value: unknown): string => { + if (value === undefined || value === null) return '—'; + const scalar = scalarText(value); + if (scalar !== undefined) return scalar; + try { + return JSON.stringify(value) ?? String(value); + } catch { + return String(value); + } +}; + +export const PreviewTable = ({ + preview, + table, + limit, + onLimitChange, + onColumnChange, + onColumnsChange, +}: { + readonly preview?: ExplorerBuilderPreviewResult; + readonly table?: DraftTable; + readonly limit: number; + readonly onLimitChange: (value: 25 | 50 | 100 | 500 | 1000) => void; + readonly onColumnChange: (column: ExplorerBuilderColumn) => void; + readonly onColumnsChange: ( + columns: ReadonlyArray, + ) => void; +}) => { + const [columnsOpen, setColumnsOpen] = useState(false); + const columnsMenuRef = useDismissibleLayer( + columnsOpen, + setColumnsOpen, + ); + const [draggedColumn, setDraggedColumn] = useState(); + const [dropIndex, setDropIndex] = useState(); + const draggedColumnRef = React.useRef(undefined); + const authoredByColumn = new Map( + table?.document.columns.map((column) => [column.column, column]) ?? [], + ); + const orderedColumns: ExplorerBuilderEmission[] = (preview?.columns ?? []) + .map((column) => { + const authored = authoredByColumn.get(column.column); + return { + ...column, + outputId: preview?.outputId ?? table?.outputId ?? '', + candidateId: column.column, + occurrenceId: authored?.occurrenceId ?? 'base', + projectionMode: + authored?.source.kind !== 'aggregate' + ? (authored?.source.projectionMode ?? 'FIRST') + : 'FIRST', + emissionId: column.column, + publicColumn: column.column, + }; + }) + .sort( + (left, right) => + (authoredByColumn.get(left.column)?.table?.order ?? + Number.MAX_SAFE_INTEGER) - + (authoredByColumn.get(right.column)?.table?.order ?? + Number.MAX_SAFE_INTEGER), + ); + const columns = orderedColumns.filter((column) => { + const authored = authoredByColumn.get(column.column); + return authored?.table?.visible ?? Boolean(authored?.table); + }); + const resetDrag = () => { + draggedColumnRef.current = undefined; + setDraggedColumn(undefined); + setDropIndex(undefined); + }; + const reorderColumns = (columnName: string, insertionIndex: number) => { + const fromIndex = orderedColumns.findIndex( + (column) => column.column === columnName, + ); + if (fromIndex < 0) return; + const reordered = [...orderedColumns]; + const [moved] = reordered.splice(fromIndex, 1); + const adjustedIndex = Math.max( + 0, + Math.min( + reordered.length, + insertionIndex - (fromIndex < insertionIndex ? 1 : 0), + ), + ); + reordered.splice(adjustedIndex, 0, moved); + const updates = reordered.flatMap((column, order) => { + const authored = authoredByColumn.get(column.column); + return !authored || authored.table?.order === order + ? [] + : [ + { + ...authored, + table: { ...(authored.table ?? {}), order }, + }, + ]; + }); + if (updates.length > 0) onColumnsChange(updates); + }; + return ( +
+
+

+ Preview and configure +

+
+ + {columnsOpen && ( +
+

+ Check columns to show them. Drag rows to change table order. +

+
+ {orderedColumns.map((column, index) => { + const authored = authoredByColumn.get(column.column); + const visible = + authored?.table?.visible ?? Boolean(authored?.table); + return ( +
{ + if (!draggedColumnRef.current) return; + event.preventDefault(); + const bounds = + event.currentTarget.getBoundingClientRect(); + setDropIndex( + index + + (event.clientY > bounds.top + bounds.height / 2 + ? 1 + : 0), + ); + }} + onDrop={(event) => { + event.preventDefault(); + const columnName = + draggedColumnRef.current ?? + event.dataTransfer.getData('text/plain'); + if (columnName) { + const bounds = + event.currentTarget.getBoundingClientRect(); + const insertionIndex = + index + + (event.clientY > bounds.top + bounds.height / 2 + ? 1 + : 0); + reorderColumns(columnName, insertionIndex); + } + resetDrag(); + }} + className={`relative flex items-center gap-2 rounded px-2 py-2 text-xs hover:bg-slate-50 ${draggedColumn === column.column ? 'opacity-50' : ''}`} + > + {draggedColumn && dropIndex === index && ( + + )} + { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData( + 'text/plain', + column.column, + ); + draggedColumnRef.current = column.column; + setDraggedColumn(column.column); + setDropIndex(index); + }} + onDragEnd={resetDrag} + className="cursor-grab select-none text-base leading-none text-slate-400 active:cursor-grabbing" + > + ⋮⋮ + + +
+ ); + })} + {draggedColumn && dropIndex === orderedColumns.length && ( +
+ )} +
+
+ )} +
+ +
+
+ {!preview ? ( +

+ Choose a row resource and at least one visible column, then preview. +

+ ) : ( + + + + {columns.map((column) => ( + + ))} + + + + {(preview.rows ?? []).map((row, rowIndex) => ( + + {columns.map((column) => { + const rawValue = row[column.publicColumn]; + const value = formatPreviewCell(rawValue); + return ( + + ); + })} + + ))} + +
+ {authoredByColumn.get(column.column)?.label ?? column.label} +
+
+ {value} +
+
+ )} +
+
+ ); +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.unit.test.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.unit.test.ts new file mode 100644 index 00000000..9c28a6b1 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.unit.test.ts @@ -0,0 +1,193 @@ +// @vitest-environment jsdom +import React from 'react'; +import { vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import type { + ExplorerBuilderPreviewResult, + ExplorerBuilderColumn, +} from '../../../types'; +import type { DraftTable } from '../authoring/model'; +import { + formatPreviewCell, + PreviewTable, + previewCellTitle, +} from './PreviewTable'; + +const column = ( + name: string, + label: string, + order: number, +): ExplorerBuilderColumn => ({ + column: name, + label, + occurrenceId: 'base', + source: { kind: 'field', fieldPath: name, projectionMode: 'FIRST' }, + table: { visible: true, order }, +}); + +const firstColumn = column('first_column', 'First column', 0); +const secondColumn = column('second_column', 'Second column', 1); +const table: DraftTable = { + outputId: 'Specimen', + tabId: 'specimen', + title: 'Specimen', + document: { + kind: 'ExplorerBuilderDocument', + output: { id: 'Specimen', title: 'Specimen' }, + rootResourceType: 'Specimen', + route: { occurrenceId: 'base', resourceType: 'Specimen' }, + columns: [firstColumn, secondColumn], + }, +}; +const preview: ExplorerBuilderPreviewResult = { + apiVersion: 'loom.calypr.org/explorer-authoring/v2', + kind: 'ExplorerBuilderPreview', + receiptId: 'receipt_preview', + outputId: 'Specimen', + columns: [firstColumn, secondColumn].map((value) => ({ + column: value.column, + label: value.label, + logicalType: 'string', + filterable: true, + chartable: true, + })), + rows: [{ first_column: 'one', second_column: 'two' }], + rowCount: 1, + diagnostics: [], +}; + +describe('formatPreviewCell', () => { + it('preserves scalar values', () => { + expect(formatPreviewCell('Tissue')).toBe('Tissue'); + expect(formatPreviewCell(25)).toBe('25'); + expect(formatPreviewCell(false)).toBe('false'); + expect(formatPreviewCell(null)).toBe('—'); + }); + + it('renders common structured FHIR values as compact human text', () => { + expect(formatPreviewCell({ reference: 'Patient/example' })).toBe( + 'Patient/example', + ); + expect(formatPreviewCell([{ use: 'official', value: 'ABC' }])).toBe('ABC'); + expect( + formatPreviewCell({ + coding: [{ code: 'fix', display: 'Fixation', system: 'example' }], + text: 'Fixation', + }), + ).toBe('Fixation'); + expect( + formatPreviewCell({ + bodySite: { reference: { reference: 'BodyStructure/example' } }, + collector: { reference: 'Practitioner/example' }, + }), + ).toBe('bodySite: BodyStructure/example · collector: Practitioner/example'); + }); + + it('keeps the lossless JSON value available for the cell tooltip', () => { + expect(previewCellTitle({ reference: 'Patient/example' })).toBe( + '{"reference":"Patient/example"}', + ); + }); +}); + +describe('PreviewTable column controls', () => { + it('reports row-limit changes to the preview owner', () => { + const onLimitChange = vi.fn(); + render( + React.createElement(PreviewTable, { + preview, + table, + limit: 25, + onLimitChange, + onColumnChange: vi.fn(), + onColumnsChange: vi.fn(), + }), + ); + + fireEvent.change(screen.getByRole('combobox'), { + target: { value: '1000' }, + }); + expect(onLimitChange).toHaveBeenCalledWith(1000); + }); + + it('uses scrollable table and selector surfaces', () => { + render( + React.createElement(PreviewTable, { + preview, + table, + limit: 25, + onLimitChange: vi.fn(), + onColumnChange: vi.fn(), + onColumnsChange: vi.fn(), + }), + ); + + expect(screen.getByTestId('preview-table-scroll')).toHaveClass( + 'overflow-auto', + 'max-h-[min(65dvh,40rem)]', + ); + fireEvent.click(screen.getByRole('button', { name: 'Columns' })); + expect(screen.getByRole('list', { name: 'Table columns' })).toHaveClass( + 'overflow-y-auto', + ); + expect( + screen.getByText(/Drag rows to change table order/), + ).toBeInTheDocument(); + fireEvent.pointerDown(document.body); + expect( + screen.queryByRole('list', { name: 'Table columns' }), + ).not.toBeInTheDocument(); + }); + + it('toggles visibility and drag-reorders columns from the selector', () => { + const onColumnChange = vi.fn(); + const onColumnsChange = vi.fn(); + render( + React.createElement(PreviewTable, { + preview, + table, + limit: 25, + onLimitChange: vi.fn(), + onColumnChange, + onColumnsChange, + }), + ); + fireEvent.click(screen.getByRole('button', { name: 'Columns' })); + + fireEvent.click(screen.getByRole('checkbox', { name: 'First column' })); + expect(onColumnChange).toHaveBeenCalledWith( + expect.objectContaining({ + column: 'first_column', + table: { visible: false, order: 0 }, + }), + ); + + onColumnChange.mockClear(); + const dataTransfer = { + effectAllowed: 'move', + setData: vi.fn(), + getData: vi.fn(() => 'second_column'), + }; + fireEvent.dragStart(screen.getByLabelText('Drag Second column'), { + dataTransfer, + }); + const firstRow = screen.getByLabelText('Drag First column').parentElement; + expect(firstRow).not.toBeNull(); + fireEvent.dragOver(firstRow!, { clientY: 0, dataTransfer }); + fireEvent.drop(firstRow!, { clientY: 0, dataTransfer }); + + expect(onColumnChange).not.toHaveBeenCalled(); + expect(onColumnsChange).toHaveBeenCalledTimes(1); + expect(onColumnsChange).toHaveBeenCalledWith([ + expect.objectContaining({ + column: 'second_column', + table: expect.objectContaining({ order: 0 }), + }), + expect.objectContaining({ + column: 'first_column', + table: expect.objectContaining({ order: 1 }), + }), + ]); + }); +}); + diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.tsx new file mode 100644 index 00000000..3abfccbc --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.tsx @@ -0,0 +1,179 @@ +import React, { useMemo } from 'react'; +import type { ExplorerBuilderCatalog } from '../../../types'; +import { derivedOccurrences, type DraftTable } from '../authoring/model'; +import { legalEdgesToNode } from '../authoring/routeActions'; + +const titleForNode = ( + catalog: ExplorerBuilderCatalog, + nodeId: string | undefined, +): string => + catalog.nodes.find((node) => node.nodeId === nodeId)?.resourceType ?? + nodeId ?? + 'resource'; + +export const RouteExtensionPanel = ({ + catalog, + table, + selectedOccurrenceId, + inspectedNodeId, + selectedEdgeId, + allowExistingExtension = false, + disabled, + onSelectEdge, + onUseAsRowStart, + onChangeRowStart, + onAddEdge, +}: { + readonly catalog: ExplorerBuilderCatalog; + readonly table?: DraftTable; + readonly selectedOccurrenceId: string; + readonly inspectedNodeId?: string; + readonly selectedEdgeId?: string; + readonly allowExistingExtension?: boolean; + readonly disabled: boolean; + readonly onSelectEdge: (edgeId: string | undefined) => void; + readonly onUseAsRowStart: (nodeId: string) => void; + readonly onChangeRowStart: (nodeId: string) => void; + readonly onAddEdge: (edgeId: string, nodeId: string) => void; +}) => { + const inspectedEdges = useMemo( + () => + legalEdgesToNode(catalog, table, selectedOccurrenceId, inspectedNodeId), + [catalog, inspectedNodeId, selectedOccurrenceId, table], + ); + const selectedEdge = inspectedEdges.find( + (edge) => edge.edgeId === selectedEdgeId, + ); + const inspectedResource = titleForNode(catalog, inspectedNodeId); + const occurrences = derivedOccurrences(table, catalog); + const parent = occurrences.find((item) => item.id === selectedOccurrenceId); + const parentResource = titleForNode(catalog, parent?.nodeId); + const inspectedNode = catalog.nodes.find( + (node) => node.nodeId === inspectedNodeId, + ); + const alreadyInQuery = occurrences.some( + (occurrence) => occurrence.nodeId === inspectedNodeId, + ); + const hasRoot = occurrences.some((occurrence) => occurrence.id === 'base'); + + if ( + !table || + !inspectedNodeId || + (alreadyInQuery && !allowExistingExtension) + ) + return null; + + if (!hasRoot) { + return ( +
+
+

{inspectedResource}

+

+ Start the query with this resource. +

+
+ +
+ ); + } + + // The graph click applies the first occurrence of a single unambiguous + // relationship directly. Adding another occurrence is explicit so clicking + // a resource already in the route never changes the query by surprise. + if (inspectedEdges.length === 1) { + if (!alreadyInQuery) return null; + const edge = inspectedEdges[0]; + return ( +
+
+

+ Add another {inspectedResource} under {parentResource} +

+

+ Follow {edge.label} to create a + separate query occurrence. +

+
+ +
+ ); + } + + if (inspectedEdges.length === 0) { + if (alreadyInQuery) return null; + if (!inspectedNode?.rowRootEligible) return null; + return ( +
+
+

{inspectedResource}

+

+ Use this resource as a different query starting point. +

+
+ +
+ ); + } + + return ( +
+

+ Add {alreadyInQuery ? `another ${inspectedResource}` : inspectedResource}{' '} + under {parentResource} +

+

+ Choose which relationship this branch should follow. +

+
+ + +
+
+ ); +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.unit.test.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.unit.test.tsx new file mode 100644 index 00000000..e14ffb9f --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.unit.test.tsx @@ -0,0 +1,289 @@ +// @vitest-environment jsdom +import React from 'react'; +import { vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import type { ExplorerBuilderCatalog } from '../../../types'; +import type { DraftTable } from '../authoring/model'; +import { RouteExtensionPanel } from './RouteExtensionPanel'; + +const catalog: ExplorerBuilderCatalog = { + snapshotToken: 'snapshot', + generation: 'generation', + routePolicy: { allowRepeatedEdges: true, allowSelfLoops: false }, + nodes: [ + { + nodeId: 'specimen', + resourceType: 'Specimen', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'observation', + resourceType: 'Observation', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + { + nodeId: 'patient', + resourceType: 'Patient', + rowRootEligible: true, + populated: true, + documentCount: 2, + }, + ], + edges: [ + { + edgeId: 'specimen-observation', + fromNodeId: 'specimen', + toNodeId: 'observation', + label: 'focus_Specimen', + }, + { + edgeId: 'specimen-patient', + fromNodeId: 'specimen', + toNodeId: 'patient', + label: 'subject_Patient', + }, + { + edgeId: 'observation-patient', + fromNodeId: 'observation', + toNodeId: 'patient', + label: 'subject_Patient', + }, + ], + candidates: [], +}; + +const table: DraftTable = { + outputId: 'Specimen', + tabId: 'specimen', + title: 'Specimen', + document: { + kind: 'ExplorerBuilderDocument', + output: { id: 'Specimen', title: 'Specimen' }, + rootResourceType: 'Specimen', + route: { + occurrenceId: 'base', + resourceType: 'Specimen', + children: [ + { + occurrenceId: 'observation', + resourceType: 'Observation', + relationship: 'focus_Specimen', + }, + ], + }, + columns: [], + }, +}; + +describe('RouteExtensionPanel', () => { + it('does not render relationship controls for one unambiguous edge', () => { + const { container } = render( + , + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('only asks for a relationship when multiple edges reach the resource', () => { + const onSelectEdge = vi.fn(); + const onAddEdge = vi.fn(); + const ambiguousCatalog: ExplorerBuilderCatalog = { + ...catalog, + edges: [ + ...catalog.edges, + { + edgeId: 'specimen-patient-secondary', + fromNodeId: 'specimen', + toNodeId: 'patient', + label: 'participant_Patient', + }, + ], + }; + const { rerender } = render( + , + ); + + fireEvent.change( + screen.getByRole('combobox', { name: 'Relationship to add' }), + { + target: { value: 'specimen-patient' }, + }, + ); + expect(onSelectEdge).toHaveBeenCalledWith('specimen-patient'); + + rerender( + , + ); + fireEvent.click(screen.getByRole('button', { name: 'Add branch' })); + expect(onAddEdge).toHaveBeenCalledWith('specimen-patient', 'patient'); + }); + + it('adds another occurrence when the inspected resource is already in the route', () => { + const onSelectEdge = vi.fn(); + const onAddEdge = vi.fn(); + const multiRelationshipCatalog: ExplorerBuilderCatalog = { + ...catalog, + edges: [ + ...catalog.edges, + { + edgeId: 'specimen-patient-secondary', + fromNodeId: 'specimen', + toNodeId: 'patient', + label: 'participant_Patient', + }, + ], + }; + const tableWithPatient: DraftTable = { + ...table, + document: { + ...table.document, + route: { + ...table.document.route, + children: [ + ...(table.document.route.children ?? []), + { + occurrenceId: 'patient-subject', + resourceType: 'Patient', + relationship: 'subject_Patient', + }, + ], + }, + }, + }; + render( + , + ); + + expect( + screen.getByText('Add another Patient under Specimen'), + ).toBeInTheDocument(); + expect( + screen.queryByRole('combobox', { name: 'Relationship to add' }), + ).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Add traversal' })); + + expect(onAddEdge).toHaveBeenCalledWith( + 'specimen-patient-secondary', + 'patient', + ); + }); + + it('does not reopen extension controls after an ordinary route addition', () => { + const tableWithPatient: DraftTable = { + ...table, + document: { + ...table.document, + route: { + ...table.document.route, + children: [ + ...(table.document.route.children ?? []), + { + occurrenceId: 'patient-subject', + resourceType: 'Patient', + relationship: 'subject_Patient', + }, + ], + }, + }, + }; + const { container } = render( + , + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('offers a clear destructive restart for an eligible disconnected node', () => { + const disconnectedCatalog: ExplorerBuilderCatalog = { + ...catalog, + nodes: [ + ...catalog.nodes, + { + nodeId: 'substance', + resourceType: 'Substance', + rowRootEligible: true, + populated: true, + documentCount: 1, + }, + ], + }; + const onChangeRowStart = vi.fn(); + render( + , + ); + + expect(screen.queryByText(/not reachable/i)).not.toBeInTheDocument(); + fireEvent.click( + screen.getByRole('button', { + name: 'Start new query from Substance', + }), + ); + expect(onChangeRowStart).toHaveBeenCalledWith('substance'); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/icons.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/icons.tsx new file mode 100644 index 00000000..89e203e4 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/icons.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +interface IconProps { + readonly size?: number; + readonly stroke?: number; +} + +const Icon = ({ + size = 24, + stroke = 2, + children, +}: IconProps & { readonly children: React.ReactNode }) => ( + +); + +export const IconCopy = (props: IconProps) => ( + + + + +); + +export const IconGripVertical = (props: IconProps) => ( + + + + + + + + +); + +export const IconTrash = (props: IconProps) => ( + + + +); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/useDismissibleLayer.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/useDismissibleLayer.ts new file mode 100644 index 00000000..72bd2b3a --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/useDismissibleLayer.ts @@ -0,0 +1 @@ +export { useDismissibleLayer } from '../hooks/useDismissibleLayer'; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.ts new file mode 100644 index 00000000..bb28fbc1 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.ts @@ -0,0 +1,112 @@ +import ELK, { type ElkNode } from 'elkjs/lib/elk.bundled.js'; + +export interface GraphLayoutNode { + readonly id: string; + readonly width: number; + readonly height: number; +} + +export interface GraphLayoutEdge { + readonly id: string; + readonly source: string; + readonly target: string; +} + +export interface GraphLayoutResult { + readonly positions: ReadonlyMap< + string, + { readonly x: number; readonly y: number } + >; + readonly routes: ReadonlyMap; +} + +const elk = new ELK(); + +const routePath = ( + section: + | { + readonly startPoint: { readonly x: number; readonly y: number }; + readonly bendPoints?: ReadonlyArray<{ + readonly x: number; + readonly y: number; + }>; + readonly endPoint: { readonly x: number; readonly y: number }; + } + | undefined, +): string | undefined => { + if (!section) return undefined; + const points = [ + section.startPoint, + ...(section.bendPoints ?? []), + section.endPoint, + ]; + return points + .map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`) + .join(' '); +}; + +export const layoutDatasetGraph = async ( + nodes: ReadonlyArray, + edges: ReadonlyArray, +): Promise => { + if (nodes.length === 0) return { positions: new Map(), routes: new Map() }; + const input: ElkNode = { + id: 'dataset-graph', + layoutOptions: { + 'elk.algorithm': 'layered', + 'elk.direction': 'RIGHT', + 'elk.edgeRouting': 'ORTHOGONAL', + 'elk.aspectRatio': '2.2', + 'elk.padding': '[top=60,left=60,bottom=60,right=60]', + 'elk.spacing.nodeNode': '72', + 'elk.spacing.componentComponent': '110', + 'elk.layered.spacing.nodeNodeBetweenLayers': '170', + 'elk.layered.spacing.edgeNodeBetweenLayers': '70', + 'elk.layered.layering.strategy': 'NETWORK_SIMPLEX', + 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP', + 'elk.layered.crossingMinimization.greedySwitch.type': 'TWO_SIDED', + 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', + 'elk.layered.nodePlacement.favorStraightEdges': 'true', + 'elk.layered.cycleBreaking.strategy': 'GREEDY_MODEL_ORDER', + 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', + 'elk.separateConnectedComponents': 'true', + }, + children: nodes.map((node) => ({ + id: node.id, + width: node.width, + height: node.height, + })), + edges: edges.map((edge) => ({ + id: edge.id, + sources: [edge.source], + targets: [edge.target], + })), + }; + const graph = await elk.layout(input); + const children = graph.children ?? []; + const missingPosition = nodes.find((node) => { + const laidOut = children.find((candidate) => candidate.id === node.id); + return ( + !laidOut || !Number.isFinite(laidOut.x) || !Number.isFinite(laidOut.y) + ); + }); + if (missingPosition) { + throw new Error( + `ELK returned no finite position for graph node ${missingPosition.id}.`, + ); + } + return { + positions: new Map( + children.map((node) => [ + node.id, + { x: node.x as number, y: node.y as number }, + ]), + ), + routes: new Map( + (graph.edges ?? []).flatMap((edge) => { + const path = routePath(edge.sections?.[0]); + return path ? [[edge.id, path] as const] : []; + }), + ), + }; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.unit.test.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.unit.test.ts new file mode 100644 index 00000000..21010d3b --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.unit.test.ts @@ -0,0 +1,37 @@ +import { layoutDatasetGraph } from './graphLayout'; + +describe('layoutDatasetGraph', () => { + it('keeps the graph deterministic, layered, and routed', async () => { + const nodes = [ + { id: 'Patient', width: 220, height: 90 }, + { id: 'Observation', width: 220, height: 90 }, + { id: 'DocumentReference', width: 230, height: 90 }, + ]; + const edges = [ + { id: 'patient-observation', source: 'Patient', target: 'Observation' }, + { + id: 'observation-file', + source: 'Observation', + target: 'DocumentReference', + }, + ]; + const first = await layoutDatasetGraph(nodes, edges); + const second = await layoutDatasetGraph(nodes, edges); + expect([...first.positions]).toEqual([...second.positions]); + expect(first.positions.get('Patient')!.x).toBeLessThan( + first.positions.get('Observation')!.x, + ); + expect(first.positions.get('Observation')!.x).toBeLessThan( + first.positions.get('DocumentReference')!.x, + ); + expect( + nodes.every((node) => { + const position = first.positions.get(node.id); + return Number.isFinite(position?.x) && Number.isFinite(position?.y); + }), + ).toBe(true); + expect([...first.routes.keys()].sort()).toEqual( + edges.map((edge) => edge.id).sort(), + ); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDirtyBeforeUnload.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDirtyBeforeUnload.ts new file mode 100644 index 00000000..e7b2feae --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDirtyBeforeUnload.ts @@ -0,0 +1,11 @@ +import { useEffect } from 'react'; + +/** Owns the browser lifecycle subscription for an unsaved Builder draft. */ +export const useDirtyBeforeUnload = (dirty: boolean): void => { + useEffect(() => { + if (!dirty) return undefined; + const warn = (event: BeforeUnloadEvent) => event.preventDefault(); + window.addEventListener('beforeunload', warn); + return () => window.removeEventListener('beforeunload', warn); + }, [dirty]); +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDismissibleLayer.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDismissibleLayer.ts new file mode 100644 index 00000000..aa39e687 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDismissibleLayer.ts @@ -0,0 +1,26 @@ +import { useEffect, useRef } from 'react'; + +export const useDismissibleLayer = ( + open: boolean, + setOpen: (open: boolean) => void, +) => { + const ref = useRef(null); + + useEffect(() => { + if (!open) return undefined; + const dismissOnPointerDown = (event: PointerEvent) => { + if (!ref.current?.contains(event.target as Node)) setOpen(false); + }; + const dismissOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setOpen(false); + }; + document.addEventListener('pointerdown', dismissOnPointerDown); + document.addEventListener('keydown', dismissOnEscape); + return () => { + document.removeEventListener('pointerdown', dismissOnPointerDown); + document.removeEventListener('keydown', dismissOnEscape); + }; + }, [open, setOpen]); + + return ref; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useEscapeToClose.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useEscapeToClose.ts new file mode 100644 index 00000000..2a254f76 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useEscapeToClose.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; + +export const useEscapeToClose = ( + open: boolean, + setOpen: (open: boolean) => void, +): void => { + useEffect(() => { + if (!open) return undefined; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setOpen(false); + }; + window.addEventListener('keydown', closeOnEscape); + return () => window.removeEventListener('keydown', closeOnEscape); + }, [open, setOpen]); +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphLayout.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphLayout.ts new file mode 100644 index 00000000..05656b46 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphLayout.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'react'; +import { + layoutDatasetGraph, + type GraphLayoutEdge, + type GraphLayoutNode, +} from '../graphLayout'; + +interface GraphLayoutState { + readonly identity?: string; + readonly positions: ReadonlyMap; +} + +/** Runs the asynchronous ELK layout and ignores results for obsolete catalogs. */ +export const useGraphLayout = ( + identity: string, + nodes: ReadonlyArray, + edges: ReadonlyArray, +): GraphLayoutState => { + const [layout, setLayout] = useState({ + positions: new Map(), + }); + + useEffect(() => { + let active = true; + setLayout({ positions: new Map() }); + void layoutDatasetGraph(nodes, edges) + .then((value) => { + if (active) setLayout({ identity, positions: value.positions }); + }) + .catch(() => { + if (active) setLayout({ identity, positions: new Map() }); + }); + return () => { + active = false; + }; + }, [edges, identity, nodes]); + + return layout; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphViewportFitter.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphViewportFitter.ts new file mode 100644 index 00000000..5bd633cf --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphViewportFitter.ts @@ -0,0 +1,61 @@ +import { useEffect } from 'react'; +import { useNodesInitialized, useReactFlow } from '@xyflow/react'; + +export const useGraphViewportFitter = ({ + expanded, + graphIdentity, + hostRef, + nodeCount, +}: { + readonly expanded: boolean; + readonly graphIdentity: string; + readonly hostRef: React.RefObject; + readonly nodeCount: number; +}): void => { + const flow = useReactFlow(); + const nodesInitialized = useNodesInitialized(); + const viewportInitialized = flow.viewportInitialized; + + useEffect(() => { + const host = hostRef.current; + if (!host || !nodesInitialized || !viewportInitialized || nodeCount === 0) + return undefined; + + let firstFrame = 0; + let secondFrame = 0; + const fit = () => { + window.cancelAnimationFrame(firstFrame); + window.cancelAnimationFrame(secondFrame); + firstFrame = window.requestAnimationFrame(() => { + secondFrame = window.requestAnimationFrame(() => { + void flow.fitView({ + padding: 0.08, + minZoom: 0.05, + maxZoom: 1.8, + duration: 200, + }); + }); + }); + }; + + const observer = + typeof ResizeObserver === 'undefined' + ? undefined + : new ResizeObserver(fit); + observer?.observe(host); + fit(); + return () => { + window.cancelAnimationFrame(firstFrame); + window.cancelAnimationFrame(secondFrame); + observer?.disconnect(); + }; + }, [ + expanded, + flow, + graphIdentity, + hostRef, + nodeCount, + nodesInitialized, + viewportInitialized, + ]); +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/usePortalHost.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/usePortalHost.ts new file mode 100644 index 00000000..1643da50 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/usePortalHost.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; + +/** Resolves a host owned by the surrounding page and tracks its DOM lifetime. */ +export const usePortalHost = (id: string): HTMLElement | null => { + const [host, setHost] = useState(null); + + useEffect(() => { + if (typeof document === 'undefined') return undefined; + const findHost = () => { + const next = document.getElementById(id); + setHost((current) => (current === next ? current : next)); + }; + findHost(); + const observer = new MutationObserver(findHost); + observer.observe(document.body, { childList: true, subtree: true }); + return () => observer.disconnect(); + }, [id]); + + return host; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useTraversalOverflow.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useTraversalOverflow.ts new file mode 100644 index 00000000..34062c7a --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useTraversalOverflow.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; + +export const useTraversalOverflow = ( + graphHostRef: React.RefObject, + traversalRef: React.RefObject, + identity: string, +): boolean => { + const [overflowing, setOverflowing] = useState(false); + + useEffect(() => { + const graphHost = graphHostRef.current; + const traversal = traversalRef.current; + if (!graphHost || !traversal) return undefined; + const measure = () => { + const truncatedWidth = Array.from( + traversal.querySelectorAll('[data-traversal-label]'), + ).reduce( + (width, label) => + width + Math.max(0, label.scrollWidth - label.clientWidth), + 0, + ); + const naturalWidth = traversal.scrollWidth + truncatedWidth; + const allottedWidth = Math.max(0, graphHost.clientWidth - 9 * 16); + setOverflowing(naturalWidth > allottedWidth + 1); + }; + const observer = + typeof ResizeObserver === 'undefined' + ? undefined + : new ResizeObserver(measure); + observer?.observe(graphHost); + measure(); + return () => observer?.disconnect(); + }, [graphHostRef, identity, traversalRef]); + + return overflowing; +}; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/index.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/index.ts new file mode 100644 index 00000000..80048a92 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/index.ts @@ -0,0 +1,5 @@ +export { default as BuilderWorkspace } from './BuilderWorkspace'; +export { builderAuthoringReducer } from './authoring/reducer'; +export * from './authoring/model'; +export * from './authoring/previewRecovery'; +export * from './authoring/routeActions'; diff --git a/ui/packages/loom-ui/src/features/ExplorerViewer/OutputTable.unit.test.tsx b/ui/packages/loom-ui/src/features/ExplorerViewer/OutputTable.unit.test.tsx new file mode 100644 index 00000000..cc36e489 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerViewer/OutputTable.unit.test.tsx @@ -0,0 +1,72 @@ +// @vitest-environment jsdom + +import React from 'react'; +import { MantineProvider } from '@mantine/core'; +import { render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import type { LoomOutputResult } from '../../api'; +import type { ExplorerRuntimeV1 } from '../../types'; +import { createViewerReducerState } from './reducer'; +import { OutputTable } from './components'; + +const tableDataInputs = vi.hoisted((): unknown[][] => []); + +vi.mock('@tanstack/react-table', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useReactTable: (options: Parameters[0]) => { + tableDataInputs.push(options.data); + return actual.useReactTable(options); + }, + }; +}); + +const runtime: ExplorerRuntimeV1 = { + outputs: [{ + outputId: 'patients', + name: 'patients', + title: 'Patients', + rowLabel: 'patient', + selector: { recipe: 'recipe', translationVersion: 'v1', output: 'patients' }, + columns: [{ column: 'id', label: 'ID', logicalType: 'string', visible: true, order: 0, filterable: false, chartable: false }], + table: { columns: [{ column: 'id', label: 'ID', visible: true }] }, + filters: [], + charts: [], + fixedFilters: {}, + }], + sharedFilters: {}, + diagnostics: [], +}; + +const result: LoomOutputResult = { + columns: ['id'], + rows: [{ id: 'patient-1' }], + totalCount: 1, + pageInfo: { hasNextPage: false }, + facets: [], +}; + +describe('Explorer Viewer output table', () => { + it('keeps table data stable when result rows are unchanged', () => { + const state = createViewerReducerState(runtime); + const dispatch = vi.fn(); + const view = () => ( + + + + ); + const { rerender } = render(view()); + const firstData = tableDataInputs.at(-1); + + rerender(view()); + + expect(tableDataInputs.at(-1)).toBe(firstData); + }); +}); diff --git a/ui/packages/loom-ui/src/features/ExplorerViewer/ViewerChrome.tsx b/ui/packages/loom-ui/src/features/ExplorerViewer/ViewerChrome.tsx new file mode 100644 index 00000000..63b19077 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerViewer/ViewerChrome.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Badge, Button, CloseButton, Group, Paper, Stack, Text } from '@mantine/core'; +import type { LoomOutputResult } from '../../api'; +import type { ExplorerRuntimeOutputV1, ExplorerRuntimeV1 } from '../../types'; +import { activeOutputState, type ViewerState } from './model'; +import type { ViewerAction } from './reducer'; +import { filterLabel } from './serialization'; + +export const QuerySummary = ({ output, runtime, state, result, dispatch }: { readonly output: ExplorerRuntimeOutputV1; readonly runtime: ExplorerRuntimeV1; readonly state: ViewerState; readonly result?: LoomOutputResult; readonly dispatch: React.Dispatch }) => { + const outputState = activeOutputState(state, output.outputId); + const fixed = Object.entries(output.fixedFilters).flatMap(([column, selected]) => selected.map((value) => ({ kind: 'fixed' as const, column, value }))); + const local = Object.entries(outputState.filterValues).flatMap(([column, selected]) => selected.map((value) => ({ kind: 'local' as const, column, value }))); + const shared = Object.entries(state.sharedFilters).flatMap(([name, selected]) => selected.map((value) => ({ kind: 'shared' as const, name, value }))); + const chips = [...fixed, ...local, ...shared]; + const sort = activeOutputState(state, output.outputId).sort; + + return ( + + + + {output.rowLabel || output.title} + + {result?.totalCount === null || result?.totalCount === undefined + ? 'Published result' + : `${result.totalCount.toLocaleString()} matching rows`} + + + {sort ? Sorted by {sort.column} : null} + + {chips.length > 0 ? ( + + {chips.map((chip) => ( + chip.kind === 'local' ? dispatch({ type: 'setFilter', outputId: output.outputId, column: chip.column, values: [] }) : dispatch({ type: 'setSharedFilter', name: chip.name, values: [] })} />} + > + {chip.kind === 'shared' ? chip.name : filterLabel(output.filters.find((binding) => binding.column === chip.column) ?? { column: chip.column })}: {chip.value} + + ))} + + ) : No filters applied} + + ); +}; + +export const ChartToggle = ({ outputId, visible, dispatch }: { readonly outputId: string; readonly visible: boolean; readonly dispatch: React.Dispatch }) => ( + +); diff --git a/ui/packages/loom-ui/src/features/ExplorerViewer/components.tsx b/ui/packages/loom-ui/src/features/ExplorerViewer/components.tsx new file mode 100644 index 00000000..ec104055 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerViewer/components.tsx @@ -0,0 +1,309 @@ +import React, { useMemo } from 'react'; +import { + Badge, + Button, + Checkbox, + Group, + Paper, + Select, + Stack, + Table, + Text, + UnstyledButton, +} from '@mantine/core'; +import { flexRender, getCoreRowModel, useReactTable, type ColumnDef } from '@tanstack/react-table'; +import type { EChartsOption } from 'echarts'; +import ReactECharts from 'echarts-for-react'; +import type { LoomFacetResult, LoomOutputResult } from '../../api'; +import type { ExplorerRuntimeBindingV1, ExplorerRuntimeOutputV1, ExplorerRuntimeV1 } from '../../types'; +import { PAGE_SIZES, activeOutputState, isPageSize, type ViewerState } from './model'; +import type { ViewerAction } from './reducer'; +import { chartFacetName, facetName, filterLabel, filterType } from './serialization'; + +export type ViewerRow = Readonly>; + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +export const textFor = (value: unknown): string => { + if (value === undefined || value === null || value === '') return '—'; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value); + if (Array.isArray(value)) return value.map(textFor).filter((entry) => entry !== '—').join('; ') || '—'; + if (isRecord(value)) { + for (const key of ['text', 'display', 'value', 'code', 'reference']) { + if (value[key] !== undefined) return textFor(value[key]); + } + return JSON.stringify(value); + } + return String(value); +}; + +const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +const fileIdentifier = (value: unknown, row: ViewerRow): string | undefined => [ + value, + row.id, + row.file_id, + row.file_uuid, + row.document_reference_id, + row.document_reference_identifier, + row.uuid, + row.sha256, +].find((candidate): candidate is string => typeof candidate === 'string' && UUID_PATTERN.test(candidate.trim())); + +const fileName = (value: unknown, row: ViewerRow): string => { + for (const candidate of [ + row.document_reference_source_path, + row.document_reference_content_attachment_url, + row.document_reference_content_attachment_title, + row.file_name, + value, + ]) { + if (typeof candidate === 'string' && candidate) return candidate; + } + return ''; +}; + +const fileActionURL = (action: string, configuredURL: string | undefined, fileId: string): string | undefined => { + const baseURL = (configuredURL ?? (action === 'file_download' ? '/download' : action === 'file_image' ? '/image-viewer/view' : '')).replace(/\/+$/, ''); + if (!baseURL) return undefined; + const separator = baseURL.includes('?') ? '&' : '?'; + return `${baseURL}/${encodeURIComponent(fileId)}${action === 'file_download' ? `${separator}redirect=true` : ''}`; +}; + +const FileCell = ({ value, row, runtime }: { readonly value: unknown; readonly row: ViewerRow; readonly runtime: ExplorerRuntimeV1 }) => { + const fileId = fileIdentifier(value, row); + if (!fileId) return {textFor(value)}; + const name = fileName(value, row).split(/[?#]/, 1)[0]; + const extension = name.includes('.') ? name.split('.').pop()?.toLowerCase() ?? '' : ''; + const configuredActions = runtime.fileActions?.extensions?.[extension] + ?? runtime.fileActions?.extensions?.[`.${extension}`] + ?? runtime.fileActions?.extensions?.default + ?? ['file_download']; + const actions = configuredActions.flatMap((action) => { + const url = fileActionURL(action, runtime.fileActions?.actions?.[action], fileId); + return url ? [{ action, url }] : []; + }); + if (actions.length === 0) return null; + return ( + + {actions.map(({ action, url }) => ( + + ))} + + ); +}; + +export const facetValues = (facet: LoomFacetResult | undefined, column: string): Array<{ readonly value: string; readonly count?: string }> => { + if (!facet) return []; + const values = new Map(); + for (const row of facet.rows) { + const raw = row.key ?? row[column]; + if (raw === undefined || raw === null) continue; + const value = textFor(raw); + const count = Number(row.doc_count); + const previous = values.get(value); + values.set(value, Number.isFinite(count) ? (previous ?? 0) + count : previous); + } + return [...values].map(([value, count]) => ({ + value, + ...(count === undefined ? {} : { count: String(count) }), + })); +}; + +const COLLAPSED_FACET_VALUES = 6; + +const FacetFilter = ({ id, label, description, facet, column, values, multiple, onChange, state, dispatch }: { readonly id: string; readonly label: string; readonly description?: string; readonly facet?: LoomFacetResult; readonly column: string; readonly values: ReadonlyArray; readonly multiple: boolean; readonly onChange: (values: ReadonlyArray) => void; readonly state: ViewerState; readonly dispatch: React.Dispatch }) => { + const expanded = state.expandedFacets.includes(id); + const options = facetValues(facet, column); + const selected = new Set(values); + const orderedOptions = [...options].sort((left, right) => Number(selected.has(right.value)) - Number(selected.has(left.value))); + const visibleOptions = expanded ? orderedOptions : orderedOptions.slice(0, COLLAPSED_FACET_VALUES); + const hiddenCount = Math.max(0, orderedOptions.length - COLLAPSED_FACET_VALUES); + const toggleValue = (value: string, checked: boolean) => { + if (!checked) { + onChange(values.filter((candidate) => candidate !== value)); + return; + } + onChange(multiple ? [...values, value] : [value]); + }; + + return ( +
+ + {label} + {values.length > 0 ? ( + + ) : null} + + {description ? {description} : null} + {visibleOptions.length > 0 ? ( + + {visibleOptions.map((entry) => ( + toggleValue(entry.value, event.currentTarget.checked)} + label={( + + {entry.value} + {entry.count ?? '—'} + + )} + styles={{ root: { width: '100%' }, body: { width: '100%', alignItems: 'center' }, input: { borderWidth: 1 }, labelWrapper: { flex: 1, minWidth: 0 }, label: { width: '100%', paddingInlineStart: 8 } }} + /> + ))} + + ) : No values available} + {hiddenCount > 0 ? ( + + ) : null} + {facet?.truncated ? Showing the most common values : null} +
+ ); +}; + +const filterBindings = (output: ExplorerRuntimeOutputV1): ReadonlyArray => + output.filters.filter((binding, index, all) => all.findIndex((candidate) => candidate.column === binding.column) === index); + +export const FilterRail = ({ runtime, output, result, state, dispatch }: { readonly runtime: ExplorerRuntimeV1; readonly output: ExplorerRuntimeOutputV1; readonly result?: LoomOutputResult; readonly state: ViewerState; readonly dispatch: React.Dispatch }) => { + const outputState = activeOutputState(state, output.outputId); + const sharedEntries = Object.entries(runtime.sharedFilters).filter(([, bindings]) => bindings.some((binding) => !binding.outputId || binding.outputId === output.outputId)); + const facets = result?.facets ?? []; + const facetFor = (column: string) => facets.find((facet) => facet.name === facetName(output.outputId, column)); + const bindings = filterBindings(output).filter((binding) => !output.fixedFilters[binding.column]); + + return ( + + + Filters + {bindings.length + sharedEntries.length} + + + {sharedEntries.map(([name, sharedBindings]) => { + const binding = sharedBindings.find((candidate) => !candidate.outputId || candidate.outputId === output.outputId) ?? sharedBindings[0]; + const values = state.sharedFilters[name] ?? []; + return ( + dispatch({ type: 'proposeSharedFilter', proposal: { name, values: nextValues } })} state={state} dispatch={dispatch} /> + ); + })} + {bindings.map((binding) => { + const values = outputState.filterValues[binding.column] ?? []; + const kind = filterType(binding); + return ( + dispatch({ type: 'setFilter', outputId: output.outputId, column: binding.column, values: nextValues })} state={state} dispatch={dispatch} /> + ); + })} + {bindings.length === 0 && sharedEntries.length === 0 ? No runtime filters declared. : null} + + + ); +}; + +const chartValues = (facet: LoomFacetResult | undefined, column: string): Array<{ name: string; value: number }> => facetValues(facet, column).flatMap((entry) => { + const value = Number(entry.count); + return Number.isFinite(value) ? [{ name: entry.value, value }] : []; +}); + +export const ChartPanel = ({ binding, output, result }: { readonly binding: ExplorerRuntimeBindingV1; readonly output: ExplorerRuntimeOutputV1; readonly result?: LoomOutputResult }) => { + const chartType = binding.type?.trim().toLowerCase() ?? ''; + const supported = ['bar', 'horizontalstacked', 'pie', 'fullpie', 'donut'].includes(chartType); + if (!supported) return Unsupported chart type {binding.type || 'unknown'}.; + const values = chartValues(result?.facets.find((facet) => facet.name === chartFacetName(output.outputId, binding.column)), binding.column); + const pie = chartType === 'pie' || chartType === 'fullpie' || chartType === 'donut'; + const option: EChartsOption = pie + ? { tooltip: { trigger: 'item' }, legend: { type: 'scroll', bottom: 0 }, series: [{ type: 'pie', radius: chartType === 'donut' ? ['42%', '70%'] : ['0%', '70%'], data: values }] } + : { tooltip: { trigger: 'axis' }, grid: { left: 48, right: 18, top: 18, bottom: 52 }, xAxis: chartType === 'horizontalstacked' ? { type: 'value' } : { type: 'category', data: values.map((entry) => entry.name), axisLabel: { rotate: 25 } }, yAxis: chartType === 'horizontalstacked' ? { type: 'category', data: values.map((entry) => entry.name) } : { type: 'value' }, series: [{ name: binding.title ?? binding.column, type: 'bar', data: values.map((entry) => entry.value) }] }; + return ( + + {binding.title ?? binding.label ?? binding.column} + {values.length === 0 ? No facet values are available for this chart. : } + + ); +}; + +export const OutputCharts = ({ output, result, visible }: { readonly output: ExplorerRuntimeOutputV1; readonly result?: LoomOutputResult; readonly visible: boolean }) => { + if (!visible || output.charts.length === 0) return null; + return
{output.charts.map((binding, index) => )}
; +}; + +export const OutputTable = ({ runtime, output, result, state, dispatch }: { readonly runtime: ExplorerRuntimeV1; readonly output: ExplorerRuntimeOutputV1; readonly result?: LoomOutputResult; readonly state: ViewerState; readonly dispatch: React.Dispatch }) => { + const bindings = useMemo(() => output.table.columns.filter((binding) => binding.visible && output.columns.some((column) => column.column === binding.column && column.visible)), [output]); + const tableData = useMemo(() => [...(result?.rows ?? [])], [result?.rows]); + const tableColumns = useMemo[]>(() => bindings.map((binding) => ({ + id: binding.column, + accessorKey: binding.column, + size: 180, + header: binding.label ?? output.columns.find((column) => column.column === binding.column)?.label ?? binding.column, + cell: (info) => binding.cellRenderer === 'fileActions' ? : {textFor(info.row.original[binding.column])}, + })), [bindings, output.columns, runtime]); + const table = useReactTable({ data: tableData, columns: tableColumns, getCoreRowModel: getCoreRowModel(), enableColumnPinning: true, initialState: { columnPinning: { left: bindings.filter((binding) => binding.pinned).map((binding) => binding.column) } } }); + const activeSort = activeOutputState(state, output.outputId).sort; + + return ( + +
+ + {table.getHeaderGroups().map((headerGroup) => ( + {headerGroup.headers.map((header) => { + const sortable = output.columns.find((column) => column.column === header.column.id)?.sortable; + const sorted = activeSort?.column === header.column.id ? activeSort : undefined; + const pinned = header.column.getIsPinned(); + return ( + + dispatch({ type: 'setSort', outputId: output.outputId, sort: sortable ? { column: header.column.id, desc: sorted ? !sorted.desc : false } : undefined })}> + {flexRender(header.column.columnDef.header, header.getContext())}{sorted ? (sorted.desc ? ' ↓' : ' ↑') : null} + + + ); + })} + ))} + {table.getRowModel().rows.map((row) => ( + {row.getVisibleCells().map((cell, index) => { + const pinned = cell.column.getIsPinned(); + const opensDetails = index === 0 && bindings.find((binding) => binding.column === cell.column.id)?.cellRenderer !== 'fileActions'; + return ( + + {opensDetails + ? dispatch({ type: 'showRowDetails', outputId: output.outputId, row: row.original })}>{flexRender(cell.column.columnDef.cell, cell.getContext())} + : flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + ))} +
+
+ {(result?.rows.length ?? 0) === 0 ? No rows match the current filters. : null} +
+ ); +}; + +export const PageControls = ({ output, result, state, dispatch }: { readonly output: ExplorerRuntimeOutputV1; readonly result?: LoomOutputResult; readonly state: ViewerState; readonly dispatch: React.Dispatch }) => { + const outputState = activeOutputState(state, output.outputId); + const page = outputState.cursorHistory.length; + const setSize = (value: string | null) => { + const numeric = Number(value); + if (isPageSize(numeric)) dispatch({ type: 'setPageSize', outputId: output.outputId, pageSize: numeric }); + }; + return ( + + + + Showing {(result?.rows.length ?? 0).toLocaleString()}{result?.totalCount === null || result?.totalCount === undefined ? ' rows' : ` of ${result.totalCount.toLocaleString()} rows`} + + + + Page {page} + + Rows +