From 34149dc0cecf08043aa05b38a9ef61b680588dd1 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Fri, 28 Aug 2026 20:09:05 -0700 Subject: [PATCH 01/17] remove unused server routes --- docs/EXPLORER_AUTHORING.md | 13 +- docs/EXPLORER_COMPILATION_ARCHITECTURE.md | 4 +- docs/QUICKSTART.md | 4 +- .../explorer-v2-capability-authoring.md | 6 +- generated/loomapi/api.gen.go | 1361 ++--------------- go.sum | 8 + internal/api/bulk/load/raw_load.go | 57 - .../api/bulk/load/test_server_helpers_test.go | 1 - internal/explorer/service.go | 25 - .../server/explorer_authoring_v2_routes.go | 127 +- internal/server/explorer_config_route.go | 16 - internal/server/explorer_config_route_test.go | 14 - .../server/explorer_receipt_contract_test.go | 16 +- .../explorer_route_test_helpers_test.go | 5 - internal/server/openapi_data_routes.go | 33 - internal/server/openapi_explorer_routes.go | 142 -- .../server/openapi_route_inventory_test.go | 4 +- internal/server/routes_test.go | 3 +- openapi/openapi.yaml | 180 +-- 19 files changed, 204 insertions(+), 1815 deletions(-) delete mode 100644 internal/api/bulk/load/raw_load.go 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..2bfea737 100644 --- a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md +++ b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md @@ -42,8 +42,8 @@ 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/builder` for direct workspace compilation +- `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` 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/frontend/explorer-v2-capability-authoring.md b/docs/frontend/explorer-v2-capability-authoring.md index f6220f93..0be66687 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` | diff --git a/generated/loomapi/api.gen.go b/generated/loomapi/api.gen.go index 52c6562d..2eb325c9 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: @@ -207,21 +198,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 +506,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"` @@ -844,21 +806,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"` @@ -1039,13 +986,6 @@ type UploadProjectResourceMultipartBody struct { File openapi_types.File `json:"file"` } -// 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"` -} - // CreateDatasetGenerationMultipartRequestBody defines body for CreateDatasetGeneration for multipart/form-data ContentType. type CreateDatasetGenerationMultipartRequestBody CreateDatasetGenerationMultipartBody @@ -1058,12 +998,6 @@ 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 @@ -1429,9 +1363,6 @@ type ServerInterface interface { // 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 @@ -1448,24 +1379,12 @@ type ServerInterface interface { // (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 - // (POST /api/v1/projects/{project}/explorers/{explorerId}/authoring/v2/preview) PreviewExplorer(c fiber.Ctx, project Project, explorerId ExplorerId) error @@ -1515,9 +1434,6 @@ type ServerInterface interface { // 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 @@ -1676,35 +1592,6 @@ func (siw *ServerInterfaceWrapper) ActivateDatasetGeneration(c fiber.Ctx) error return handler(c) } -// GetRepositoryExplorerConfig operation middleware -func (siw *ServerInterfaceWrapper) GetRepositoryExplorerConfig(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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetRepositoryExplorerConfig(c, project) - } - - 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) -} - // ListExplorers operation middleware func (siw *ServerInterfaceWrapper) ListExplorers(c fiber.Ctx) error { @@ -1874,59 +1761,6 @@ func (siw *ServerInterfaceWrapper) CompileExplorerBuilder(c fiber.Ctx) error { return handler(c) } -// GetExplorerCandidateSuggestions operation middleware -func (siw *ServerInterfaceWrapper) GetExplorerCandidateSuggestions(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()) - } - - // ------------- Path parameter "snapshotToken" ------------- - var snapshotToken string - - err = runtime.BindStyledParameterWithOptions("simple", "snapshotToken", c.Params("snapshotToken"), &snapshotToken, 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 snapshotToken: %w", err).Error()) - } - - // ------------- Path parameter "candidateId" ------------- - var candidateId string - - err = runtime.BindStyledParameterWithOptions("simple", "candidateId", c.Params("candidateId"), &candidateId, 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 candidateId: %w", err).Error()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetExplorerCandidateSuggestions(c, project, explorerId, snapshotToken, candidateId) - } - - 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) -} - // GetExplorerAuthoringCapability operation middleware func (siw *ServerInterfaceWrapper) GetExplorerAuthoringCapability(c fiber.Ctx) error { @@ -2001,117 +1835,6 @@ func (siw *ServerInterfaceWrapper) ApplyExplorerBuilderCommands(c fiber.Ctx) err return handler(c) } -// CompileExplorer operation middleware -func (siw *ServerInterfaceWrapper) CompileExplorer(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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.CompileExplorer(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) -} - -// SaveExplorerDraft operation middleware -func (siw *ServerInterfaceWrapper) SaveExplorerDraft(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()) - } - - 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 - - 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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.ExportExplorerWorkspace(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) -} - // PreviewExplorer operation middleware func (siw *ServerInterfaceWrapper) PreviewExplorer(c fiber.Ctx) error { @@ -2747,57 +2470,6 @@ func (siw *ServerInterfaceWrapper) UploadProjectResource(c fiber.Ctx) error { return handler(c) } -// UploadRawNDJSON operation middleware -func (siw *ServerInterfaceWrapper) UploadRawNDJSON(c fiber.Ctx) error { - - var err error - _ = err - - // Parameter object where we will unmarshal all parameters from the context - var params UploadRawNDJSONParams - - 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 "project" ------------- - - 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()) - } - - // ------------- Optional query parameter "generation" ------------- - - 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()) - } - - // ------------- 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.UploadRawNDJSON(c, 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) -} - // GetApolloSandbox operation middleware func (siw *ServerInterfaceWrapper) GetApolloSandbox(c fiber.Ctx) error { @@ -2949,24 +2621,16 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/capability", wrapper.GetExplorerAuthoringCapability) - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/capabilities/:snapshotToken/candidates/:candidateId/suggestions", wrapper.GetExplorerCandidateSuggestions) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/suggestions", wrapper.SearchExplorerCandidates) router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/builder", wrapper.GetExplorerBuilder) router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/builder", wrapper.CompileExplorerBuilder) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/compile", wrapper.CompileExplorer) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/commands", wrapper.ApplyExplorerBuilderCommands) router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/reconcile", wrapper.ReconcileExplorerBuilder) - router.Put(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/draft", wrapper.SaveExplorerDraft) - - router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/export", wrapper.ExportExplorerWorkspace) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/preview", wrapper.PreviewExplorer) router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/publish", wrapper.PublishExplorer) @@ -2983,8 +2647,6 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation/activate", wrapper.ActivateDatasetGeneration) - router.Put(options.BaseURL+"/api/v1/raw", wrapper.UploadRawNDJSON) - router.Delete(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.AbortSnapshot) router.Get(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.GetSnapshot) @@ -3023,8 +2685,6 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option 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) - } type AuthoringBadRequestJSONResponse ErrorResponse @@ -3313,94 +2973,37 @@ func (response ActivateDatasetGeneration503JSONResponse) VisitActivateDatasetGen return ctx.JSON(&response) } -type GetRepositoryExplorerConfigRequestObject struct { +type ListExplorersRequestObject struct { Project Project `json:"project"` } -type GetRepositoryExplorerConfigResponseObject interface { - VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error +type ListExplorersResponseObject interface { + VisitListExplorersResponse(ctx fiber.Ctx) error } -type GetRepositoryExplorerConfig200JSONResponse RawJSON +type ListExplorers200JSONResponse []map[string]interface{} -func (response GetRepositoryExplorerConfig200JSONResponse) VisitGetRepositoryExplorerConfigResponse(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 GetRepositoryExplorerConfig401JSONResponse struct { +type ListExplorers401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response GetRepositoryExplorerConfig401JSONResponse) VisitGetRepositoryExplorerConfigResponse(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 GetRepositoryExplorerConfig403JSONResponse struct{ LegacyForbiddenJSONResponse } +type ListExplorers403JSONResponse struct{ LegacyForbiddenJSONResponse } -func (response GetRepositoryExplorerConfig403JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type GetRepositoryExplorerConfig404JSONResponse struct{ LegacyNotFoundJSONResponse } - -func (response GetRepositoryExplorerConfig404JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type GetRepositoryExplorerConfig500JSONResponse struct { - LegacyInternalErrorJSONResponse -} - -func (response GetRepositoryExplorerConfig500JSONResponse) VisitGetRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type ListExplorersRequestObject struct { - Project Project `json:"project"` -} - -type ListExplorersResponseObject interface { - VisitListExplorersResponse(ctx fiber.Ctx) error -} - -type ListExplorers200JSONResponse []map[string]interface{} - -func (response ListExplorers200JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type ListExplorers401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response ListExplorers401JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type ListExplorers403JSONResponse struct{ LegacyForbiddenJSONResponse } - -func (response ListExplorers403JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { +func (response ListExplorers403JSONResponse) VisitListExplorersResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(403) @@ -3724,97 +3327,6 @@ func (response CompileExplorerBuilder503JSONResponse) VisitCompileExplorerBuilde return ctx.JSON(&response) } -type GetExplorerCandidateSuggestionsRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - SnapshotToken string `json:"snapshotToken"` - CandidateId string `json:"candidateId"` -} - -type GetExplorerCandidateSuggestionsResponseObject interface { - VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error -} - -type GetExplorerCandidateSuggestions200JSONResponse CandidateSuggestions - -func (response GetExplorerCandidateSuggestions200JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type GetExplorerCandidateSuggestions400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response GetExplorerCandidateSuggestions400JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type GetExplorerCandidateSuggestions401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response GetExplorerCandidateSuggestions401JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type GetExplorerCandidateSuggestions403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response GetExplorerCandidateSuggestions403JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type GetExplorerCandidateSuggestions404JSONResponse struct{ AuthoringNotFoundJSONResponse } - -func (response GetExplorerCandidateSuggestions404JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type GetExplorerCandidateSuggestions409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response GetExplorerCandidateSuggestions409JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type GetExplorerCandidateSuggestions500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} - -func (response GetExplorerCandidateSuggestions500JSONResponse) VisitGetExplorerCandidateSuggestionsResponse(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"` @@ -3854,368 +3366,108 @@ func (response GetExplorerAuthoringCapability403JSONResponse) VisitGetExplorerAu } type GetExplorerAuthoringCapability500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} - -func (response GetExplorerAuthoringCapability500JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type ApplyExplorerBuilderCommandsRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *ApplyExplorerBuilderCommandsJSONRequestBody -} - -type ApplyExplorerBuilderCommandsResponseObject interface { - VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error -} - -type ApplyExplorerBuilderCommands200JSONResponse ApplyCommandsResponse - -func (response ApplyExplorerBuilderCommands200JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type ApplyExplorerBuilderCommands400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response ApplyExplorerBuilderCommands400JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type ApplyExplorerBuilderCommands401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response ApplyExplorerBuilderCommands401JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type ApplyExplorerBuilderCommands403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response ApplyExplorerBuilderCommands403JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type ApplyExplorerBuilderCommands409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response ApplyExplorerBuilderCommands409JSONResponse) VisitApplyExplorerBuilderCommandsResponse(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 { - AuthoringInternalErrorJSONResponse -} - -func (response ApplyExplorerBuilderCommands500JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -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 CompileExplorerRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *CompileExplorerJSONRequestBody -} - -type CompileExplorerResponseObject interface { - VisitCompileExplorerResponse(ctx fiber.Ctx) error -} - -type CompileExplorer200JSONResponse CompileResponse - -func (response CompileExplorer200JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type CompileExplorer400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response CompileExplorer400JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type CompileExplorer401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response CompileExplorer401JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type CompileExplorer403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response CompileExplorer403JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type CompileExplorer409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response CompileExplorer409JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type CompileExplorer422JSONResponse struct { - AuthoringUnprocessableJSONResponse -} - -func (response CompileExplorer422JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type CompileExplorer500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} - -func (response CompileExplorer500JSONResponse) VisitCompileExplorerResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type CompileExplorer503JSONResponse struct { - AuthoringUnavailableJSONResponse -} - -func (response CompileExplorer503JSONResponse) VisitCompileExplorerResponse(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 SaveExplorerDraftResponseObject interface { - VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error -} - -type SaveExplorerDraft200JSONResponse SaveDraftResponse - -func (response SaveExplorerDraft200JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type SaveExplorerDraft400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response SaveExplorerDraft400JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type SaveExplorerDraft401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response SaveExplorerDraft401JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - 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) - - return ctx.JSON(&response) -} - -type SaveExplorerDraft404JSONResponse struct{ AuthoringNotFoundJSONResponse } - -func (response SaveExplorerDraft404JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) - - return ctx.JSON(&response) -} - -type SaveExplorerDraft409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response SaveExplorerDraft409JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type SaveExplorerDraft422JSONResponse struct { - AuthoringUnprocessableJSONResponse -} - -func (response SaveExplorerDraft422JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type SaveExplorerDraft500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} - -func (response SaveExplorerDraft500JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type SaveExplorerDraft503JSONResponse struct { - AuthoringUnavailableJSONResponse + AuthoringInternalErrorJSONResponse } -func (response SaveExplorerDraft503JSONResponse) VisitSaveExplorerDraftResponse(ctx fiber.Ctx) error { +func (response GetExplorerAuthoringCapability500JSONResponse) VisitGetExplorerAuthoringCapabilityResponse(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 { +type ApplyExplorerBuilderCommandsRequestObject struct { Project Project `json:"project"` ExplorerId ExplorerId `json:"explorerId"` + Body *ApplyExplorerBuilderCommandsJSONRequestBody } -type ExportExplorerWorkspaceResponseObject interface { - VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error +type ApplyExplorerBuilderCommandsResponseObject interface { + VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error } -type ExportExplorerWorkspace200JSONResponse Workspace +type ApplyExplorerBuilderCommands200JSONResponse ApplyCommandsResponse -func (response ExportExplorerWorkspace200JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands200JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(200) return ctx.JSON(&response) } -type ExportExplorerWorkspace401JSONResponse struct { +type ApplyExplorerBuilderCommands400JSONResponse struct { + AuthoringBadRequestJSONResponse +} + +func (response ApplyExplorerBuilderCommands400JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(400) + + return ctx.JSON(&response) +} + +type ApplyExplorerBuilderCommands401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response ExportExplorerWorkspace401JSONResponse) VisitExportExplorerWorkspaceResponse(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 ExportExplorerWorkspace403JSONResponse struct{ AuthoringForbiddenJSONResponse } +type ApplyExplorerBuilderCommands403JSONResponse struct{ AuthoringForbiddenJSONResponse } -func (response ExportExplorerWorkspace403JSONResponse) VisitExportExplorerWorkspaceResponse(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 ExportExplorerWorkspace404JSONResponse struct{ AuthoringNotFoundJSONResponse } +type ApplyExplorerBuilderCommands409JSONResponse struct{ AuthoringConflictJSONResponse } -func (response ExportExplorerWorkspace404JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands409JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) + ctx.Status(409) return ctx.JSON(&response) } -type ExportExplorerWorkspace409JSONResponse struct{ AuthoringConflictJSONResponse } +type ApplyExplorerBuilderCommands422JSONResponse struct { + AuthoringUnprocessableJSONResponse +} -func (response ExportExplorerWorkspace409JSONResponse) VisitExportExplorerWorkspaceResponse(ctx fiber.Ctx) error { +func (response ApplyExplorerBuilderCommands422JSONResponse) VisitApplyExplorerBuilderCommandsResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) + ctx.Status(422) return ctx.JSON(&response) } -type ExportExplorerWorkspace500JSONResponse struct { +type ApplyExplorerBuilderCommands500JSONResponse struct { AuthoringInternalErrorJSONResponse } -func (response ExportExplorerWorkspace500JSONResponse) VisitExportExplorerWorkspaceResponse(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 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"` @@ -5660,95 +4912,6 @@ func (response UploadProjectResource503JSONResponse) VisitUploadProjectResourceR 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 { } @@ -6030,9 +5193,6 @@ type StrictServerInterface interface { // 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) @@ -6049,24 +5209,12 @@ type StrictServerInterface interface { // (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) @@ -6116,9 +5264,6 @@ type StrictServerInterface interface { // 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) @@ -6241,33 +5386,6 @@ func (sh *strictHandler) ActivateDatasetGeneration(ctx fiber.Ctx, project Projec 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 @@ -6418,36 +5536,6 @@ func (sh *strictHandler) CompileExplorerBuilder(ctx fiber.Ctx, project Project, 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 @@ -6510,102 +5598,6 @@ func (sh *strictHandler) ApplyExplorerBuilderCommands(ctx fiber.Ctx, project Pro 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) - - 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 -} - // PreviewExplorer operation middleware func (sh *strictHandler) PreviewExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { var request PreviewExplorerRequestObject @@ -7112,35 +6104,6 @@ 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 - - request.Params = params - - request.Body = bytes.NewReader(ctx.Request().Body()) - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.UploadRawNDJSON(ctx.Context(), request.(UploadRawNDJSONRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UploadRawNDJSON") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(UploadRawNDJSONResponseObject); ok { - if err := validResponse.VisitUploadRawNDJSONResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - // GetApolloSandbox operation middleware func (sh *strictHandler) GetApolloSandbox(ctx fiber.Ctx) error { var request GetApolloSandboxRequestObject @@ -7333,117 +6296,111 @@ 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==", + "7D39b9u4kv8KoXvAAgcnTrvdB2zupzRJ2xzSNM9Jd+/dolfQ0tjmViK1JJXULfK/H/glUbI+HdvJ7vqn", + "VjElDofzzZnh9yBkScooUCmC4+9BijlOQALXT+df05hx4BeReiI0OA5SLBfBKKA4geA4gGLAKODwR0Y4", + "RMGx5BmMAhEuIMHqzYTQS6BzuQiOX4wCuUzVu0JyQufBw8MoeAsUOJaE0YZ55sWAx8xzzdnvEMqGSVL7", + "62NmmEAMWEDDDNz++rgZBMt4CLf6l4ZpvCHrz/WgXhUpowI0NZxkcsHUb69xNIE/MhAakyGjEqj+L07T", + "mIR6n8a/C7ObxWz/4DALjoP/GBckNza/ivE554xP7Gxm7ghEyElqqCK4XQD675sPVyhiYZYAlYgIlOB4", + "xngC0QgpKDChAmX0C2X3FM0IxJEYIcYRS4gUyKEBYbcORGiaycPgYVSs7TQmQOVpzAQ8zSJDDQAKMQ0h", + "hgilHO4I3CP4CmGmxqEpzBgHpCaIQf2lsgJGZzEJdwj2RZJkEk9jQCFO8ZTERC4V3jmEQFKJhMQS1H4J", + "iWMwW5JKkhAhSYgijmdynGZTB54dHy4wnUNUXtwbxqckioDudlMUyQCVaga9JYSGJMWx2iXKJOKAI7Wq", + "hEVktkRyQQRyshNZsVJex1ss4R4vb0kCLNvhVl2vUBN8DQEiiJDiEQH8DviBIBGgCHAUEwplwC+oBE5x", + "rGfaHdwnFGUUvqYQSp+DRygFLoiQQENDWI5dZpjEGQfEwjDjvEpGV0y+YRmNdktFjiRGim4iEmEJI83G", + "JDaEbxnGcIhcgOITq/cORAohmZHQE19OzqOIgUCKEOErERVKu8bLmOHolrFLzOew2xVPFZI9IcZBZLG0", + "NCeQXABK8FeSZAlyqgYJ8q1Cc7eMvcd0acWx2O0SuJnVW0QaY1paguGaH0TBVQchExKlLCbhsryWjxTf", + "YRIrablD7imUXyGhR558ZhxGKMESOMEx+aZIFIeS3Fl5DPyOWP6a4vAL0EhJ86xYSnWNKWchCLHbVXq7", + "pcC7wzGJjM0wzaQWb/eMfxEpDg3bJZhGI8QymWaG5XxWJBpirbQgwVr0x/ESEao/q9f7luN08a/LLVhD", + "E3yv4G5apJ0Yvbu9vc5XDPQOYpaCWojkmIqUcYkKex5hDoXB5C9gOyK9ZQ0O/kIJKWkNkbNsMKKMJzjO", + "F5rLhpBlsRqmlGqUheV1bIfseu5FbpYaEJUwVmBiLkCbBppstPUwY7xYuYb/3e37ywq8Er7K8UImcRnQ", + "GhO9DNK5UgBKN0w5uxfA0ceLHDQ91yXMcbjcAs2aD/di0lgPLSwkj2lzAtXmFBHC6DkrvAp73cy2cVN3", + "wCJy6HGszL+l0b3C2L0pE0QyvkQRpDFbasIoWbVqGBGI0JBRY7/469q8lTtgYR22Lg4Vi7XYuGaq7QiV", + "nsuYAI4U4Sh70NiH6kn6u2YlTmFUxksP+o3bh+sQVomSDtg9haj4MWR0RuaZMQ7RPTb230wB7a1jG9ZG", + "7z3ImcDTq76VYf+gl6njIr7NQcyC3DINx1QtDrfIbcj9dXYsty8UsB7rKytiSaVanjEjjG2izBLLVVOw", + "WIKoZIhFxqlRzrFYWHV3Y8yxLchw++Uh9mSK5WKE/siAL0coyWJJUszlSNtdGvarM22C3eE40zEAJ9SV", + "APSMKTv1xiV63yV5MQwzbdkClsZXy7hg3D25jSlHLewCBLoncqFljlVv/jI3L+D7rrNNwsc4/CJyAc98", + "4PU4LeMNv1rHU4QsBX9h2xH7vemyFCUwbhkC9VYeB/gvFIHEJDb2MIcIm4gCjVDIOIdY42S6dAu/KJHn", + "xvXCkG0rtiLCEguQI1TExUdOiGq3jqRQ2JhGxLJMQrFxqwrDQvKR2vjCN9j9Kk8KwlQcFXKI1CO2u4Wn", + "QknTesmxDVW3hjx02LObolxrPIcRInQOwvzNExp6a7TwbHOs8xVuQ8+tQYFt+ky52Yo8y7Tp6XiLKaUX", + "haf97NKgsqciS5UfC9F7iAh2Zx47XfipmexATe7MkhwsJSm0KZylMcNREa871Ic1di59enJ98YuyRQ2I", + "QLMkOP4tiBlLDkMcL1N+yPh87A7UDvIo3/juZfBp5XBmFJyE7rwMRxFR/8fxNVfzSwIiOJ7hWMAoSL0/", + "KcTFWUL1f4mERHThyExyqt9Sk1ooMOd4qZ5nJIYrfej0fRVCSaSh09ZjJveH7pOv4hzrN/Orm6JAD5vq", + "E7wcPRbydZDUA3D4qojgHeAIeJ13XgbZfrYW2DSNl6cmIiU8o24Q0Pptc1LbAbcdO4AMirMl/ab6SELo", + "hXn3xSpdOB18xvFMnpG5XU8dBotxHnvMGE+wDI4DQuU/XwV6NpIoljnKJyNUwhy4+oygOBULJm/ZF6BD", + "SanAXPVDDfB5COyxmVawbG83I4LnlAlJwv4bepa/U8fVUXnXuubvvXcv6vbOHAn0B91idqJfq4M+d8K6", + "vvRrPrCNJIrPVZZaxlOxkPKO1FJIzk55MH4gfeCUeBhvZd1C7Sh5DVhmHPoz/Bv3wsMo+EKM4et0l3N7", + "V5ejoKzTWbl2LG+3++A0I7ESpaNAZHNrLil82oMX9T/jCPssqBAfMhqSkiKoapeCPOzHLklCKlRnT6KC", + "4xdHR0ddZFv+bIV+vO2xaCutvQqEty/t5GKl70BZ4s4ae+qGnsrPDPxFufXdLOvsB4jm/cAglCi7/5qD", + "Mvnz1CBHKbcnry/Pg1Hw5uLy9nwSjILTdyeT23qaM44fDftNbI6DBg2tUFEfY8cjR8yByg/DgLSuOGH0", + "PYs60X9dHq1olTF5xaJ+kxmP8UN/vAy1+9ymnk7OT27PP7u9Pft4fXlxWvrL+eW59zg5vzp57z9+mJyd", + "T8zzTTAKbs5vzcPnyYcPt8EoODk7+zz58PHWjH7/4Zfz/FH9dPrh8uP7q2AUfLw+U9Pmz3asff7UyzZt", + "ZeQ3nhgewMkRxCDBiV3PoJoyFgM2HKbja4yeLjA38q15zBsSu0y/1UHKtDdGdOOArxC1fkIsMG8dUkFc", + "Fa6V1VQ/WQGiDPSoiq+6LXltVM6NxBJ2pINDLHHM5p1C0w5btcceYYHVWs9Ner2EmzrRGpMZhMswhhx9", + "7hNX579qvjk5+3ftmyUbDcfxh1lw/Ftva+3TKKBZbEM9kmfQR/9WgO0w5nyrz21YHf2cOuV6A5iHi/V8", + "N9pXFOtIUS0JPMoDqno9Fp5e613LvVmfdezsA9wFs3k53LX+zsY9qA6WKrDo2bl1fNKbMh61/3Xs0kAT", + "pT3o9nROC1k3hDz8GOpNyFLo7YxuiUJsri006NNoPny+82heO9W8lAvfsVy1K4NnvrJWYHVmDoLFdxDd", + "6OEt+kafJVzr5LbO7Blv6GPJtFQE0Egjq6Sbb57Dl9uxCjH7y2oh5oJQtuqHKYvHBfpXSS6CGc5ief1I", + "R0AnyV9jBUQnRDNtZTWDFOMpxD2+E7M5CXF82yfqO0ACln2i/kyxipRVvkgBS3Mc1mHB+nvsycwCzQ5N", + "ZTR4c5Tw7JPB6gqbqKCFdrXQGUa2vV32GWdJb6eyL7GkLM3iJuSPAsl6zlj1NMyiSkB7Xyt2qZi/BatX", + "lveGuHI2J++UZeYca6DN3p8t2hHIK9VEHV/j7P4tx4Q2qIX7CWPyPCZz0iAlKtuQM0i1Yqnypc59UFxS", + "jRUN2A3G7QlOO9LzwMbmjrBqV7PWoZVCQacBsIKnUsQvxVICp8Fx8H+/nRz8Lz749vmT/c/Rwc+fP/3n", + "P4JGvdA1tfHRq3OvqTMeG+QzxNYvbHljxqpNdrqv7aVbNai8zPpzwELGlIDPgWumjJsc+gH0UdL0Kwip", + "+it6dDAKiM6/mBHgr5c3SyEh0YdiEqjyFl4vP/JYG1gRoXNvBJsK4CZ16tSh6PXyVInJXI2VHMwCkgTL", + "sB7G1aCng/aXk8uPJho8udFxvsvLYBScXdzcXlyd3nYH7PTq6/HtnzWte4rcSa2tQehVWYOnTb9UAqom", + "+GnCqmfByD2/O7l6q5918PPzydmZfjLhTfv46RHSS6cxrhcJGeYfbOSsz4/0lKdvXd1O4x7uYKz/Io3X", + "Q2Lg3oQ7P0ge5Muaipvenn5HiGViapvqj4Q0e/VfpP2WOQKp9xD0gKeJ1BSTr7q+JZe5hOBVAhl5568O", + "Qz0iPIxKjkO5vtHS7FH2Povs7Zo+yhnt0uNlj67BjavDocdEQ3WMUYQVw00bbS0Wm82AbZ7M9AlYgfN3", + "wWijCZGAEHjey4lwmbX1kWy4A24zIrpYSfaZsBrsdt93748MGosV1G6R9deGCvxwNduhO9+uPgI5LHOv", + "LWevfG7X63NvipfWiHfn2GuWxl0AFMKXMyYnQ93WTEKvUKWJjdbZhzmgNRC4GYptqqMhnWn6mkXLLsar", + "Z/Le2nuYzt4M367Ixk6OKufdDoyebNxSAVev0FmBrDdwJZ6kX69dpuWE9yBxhCUefOTvpSKvnWNbtd0b", + "s2bflA/915N09W81+ive9Lln2fqdfLs7k2sqE1Q3rZhtlMPfgJVq7GIYcprtj16Rp4damAqBvKX0Zl2y", + "1YHxlizgJnPJfrYO0V7N3fpyss0yebTUqqsK7ICyUqCtq5HyYn2uTDFq6giqZdGYRraW56ColNeKRhwG", + "K2F6J8Aq0xHdRgS7j9u1IYMBxNRPQvIslLrGUmFg7MaYVY9QBClQU09LTU2YgkGBwCjYzI0q0nvXVhpx", + "+qm/QP2QmwsDaJ5E/YLbl42MupagJVFrjYJtxrNeqCQmCZGD01YHpDf2d2orq/Yd0ny+1vXvNolksCVd", + "8nCfQ/6IRVyLOb3hHdbMkZ9VtZ+ScHZfRsMgH3NwXnUttRW7bOHxFtAd07hujDTfnJ5cnkz8UPNkcvJv", + "L9j82f3BxaQ7Y9Gj4LqoB1xLuFUq6jed5ixcct8QGeDthLD5dqtg1uLeZPivJxM3ILJaYdqpnNq9VCno", + "cCNh01W6fmToVEk90ZfC1yLbLgHjQVAQdv9gqeslNMhi1O0T7hdMgGkxowwxscCmGDVVTgm/gwjFTIgY", + "hIiXKMpMezhd8h1mkt0BPwzq4CkFtrdbStqtRodorrY8hLVMNU9gmfe9SdrDOhNXfrSezNppsd1G04Rb", + "Eqhr8ZTH14YeEpA44gbcfsc1RSCvhsiGZQrohhCEUbEgaX3Sy5BIZJXmygf/pU81YrDIuxyig+KY3U9s", + "jtm5S1ddPSjR424gnl0yljaMSfDXGwmp6BG1KInWVRBW5qtbtF+m3x4UaD0iWfcQpFxoUANfc3Sh66SD", + "g+RL3C9TqkdoorafQReyyqrmxnTAgXKMAhIibauDS8aSHwQyVTim/6AZnOA0tSELJya6QxV9uzMMDLne", + "eDVCr4mOXGwrRNZbZTVrm5a2ALd4ulacY708rp5JcGzzhYDKpurHBsTXzx4SDezFlxqwWcnMGkgUEMcT", + "oBHY2rsiV6qImX9aH68pobQpS7MdQSsr/bVUXbUDT8Ue7w3wU9yB4ENt74i8vrH1NKZ6srJatNiRkJgP", + "7faNCpTWbfFKoWPn0UWr6KuRX92HGzoxrP8kSrasE27Jd8ffdjv3CteNgq8Hc3bgJnHlr3cvD3/1sq7y", + "MQckSZnJYrU3GXivKDWicw+COZGLbHoYsmRsOueMY8aSMbGtx/IeOmP/7Qfd2YfQGdNcZCRToPSZUWMn", + "1xdKfjjyD14cHh0e2Z4FFKckOA5+PDw6/NGCofE7xikZ370YK+qbcZzAuBquF+PvJHpQY+eg15V3AVAC", + "NHgLcqLfOHcvBJU7F14eHe2ik+1Nppu+zbI477Wr+zC9Onqxi+krfb+KLoEj1+nL9GfOu0W5UiXdTFcu", + "vGZnPwjXHc1rTvfq6Mcm6HJsj6utV/V7r/q+l3eG022fsiTBfGk7kdqGXyixgkoDjVFONSsN2w5tY4D8", + "Jpbfaq/5IO2XrlQtkE8PoxLFCpBi/N1m1T6Mi1QxMf5ePDxUb4VpKNMthriKmsZTIG+od/HLw6dRkDJR", + "wyWnXPkMZwbkt9WbYEBI5xZ4ZJp3ohwrD/nAnbwX2FmtOPzs/K/PaVOOUwQz4J+LvpDNlfMlOZw76VNC", + "FVn0aAtRzpxsyoXuPtLUsIy8q228D68aSeW3dWX1c5NHR90MudoftZBkvd4s9UDsKz5WOnvqF3/u/WLe", + "+VS99+KnAcDW9MhT33j5csA3/M6CD6PgpwF4Lrf81C//OATVedfGsuT8aBrq4aKpbC7YCxo2XfbWkGpj", + "y8SwM/E2sjLc1PDnQjzXAp9z8f95oGBv+HKNTGtVD03i98Riql4A72XDjmXDM2BNRxEIe6yI8EwC96wa", + "r81pmU0tc/ps6kx20WgqXxIhz/NRj6S8jRxNDzWit0deNZZrLyqp69Rf3mmF9SIhyfydgLAWrN/1f00Z", + "2mH0nRfeZrOttyEp02X4vHj2wm3lbo8nIr5eoq1yg0dfi6Wu5f/m6N1QHcLm/h+ujYSiv39/MTb+XtyM", + "2er+lwj82fn9O6aaNZztje28dtQxzff6B4Hs7vOMSpIAKupPHyPwuo1G79LVks/em97Gfu/osVc72EWE", + "r/P6r63RYqmnWF2zbX0gKr2bNDA1VxMS75JJ3XUhP2jaDcXWXEHZl2hXrx3sKyRXL/XsS/ENFzb2tQFr", + "76vT+7Ubum80C0zRYh3Fbt48qJQy7zg8Ui017ri0ZOUWyf52Q93Fvk/JUusyRi8DouGWwifmq8fL+bDU", + "S7pL1Ne1oN4iLddNV2uOuGsViktOi77JWhV8vECuWfJTyv1H0crDn8h88O8KeFrRr/vqVxsp+l3AN68A", + "au9l2LEaqL9OoM50wpRREuLYXSVj7xCByLemdJAGU4QlS0jorkFFUyzDxV5j/K00hmur/+ScbctZthzs", + "qVRb7ZiNq7VONQz8unJZNpshRiG/pdmkO/15eXT3jtKrF0PArV6U/njx8OrlEMirt5yrD/w8aOkxASpP", + "YybA3/CnE1Lq5SG7/hZLuMfLW5IAy+SGhJy9MeTphZwBZNtCrlw+tWshVymUqkv1sWdHkX/9symq2Yu2", + "veHSi6eLC3+enKvz4p/dRKRWao2ec0zKGS7uYmM04yyx2Xo4lO5qboiQLh3as/+e/Xuxv3852JMLAHMP", + "hht6Wuonv42QdP19I7sWAw23gNRHJcxQFMFM3+rFqEC6yyuhc4QRZZE55NHpS3+/EMTz5cW2jGBztVJN", + "kG7KuLyxB3N/t0Pt+jSvV71fHKwbnml6mKKBcm5Y6ai26WxiTzaPJ5tnsP06k6Jm75GQWGZiu8kTAwoK", + "SuT2xLll+8TZv0XirE0v07fhiywxiWa5v1QvL9fT0cVF8iGjMzJ/+iqePDaUMkEk4/lR3qkBsL7i6XE5", + "7S4zfmGuaM8/8z8Hl4wlB+YihYNTliREDqul2o7M8LvU/yWkxj4jdY28xJ7Sxs3bKGxM2EX7VjYKjjDi", + "Of8dsHvqh2HzQ+L1hc6MUByTb/D00uaNheTZ2pR/MpW7Wyfmz1679guOTcxD8Z5jii0qe6cRxfi730To", + "YZfFbR2jS/3SNd9mNWxriv4c07p3GmyDVa1+atj54Obdycuf/hlsRIOzUII8EJIDTsoyobOsd2/578XQ", + "U4qhG020NonEsBJSJIoIlawsjnrLIA4xYEu926n5mpgZ9m75njn/yszpSs1QlOlbglx9EbIchvK7ZHvz", + "5CaK2j3mjCDlEJq7Q41DXF8YbhlW+RtYEi+ZfM+9e+79i3MvjRAuquEd6xKqla69cyHnmoGMDG2FHJr5", + "Srpyf2Lypz4xkQtwFacVTfCo+vqeBPfd/q+jV9me2v4i1IZXzI3pEl2cbfdwztHPcKrcTbueAr6uXjh7", + "H2Vv5fwNrJyi0Q8t7h5w/kqhnbq4+QmCk0PCjfazXrRx130FXcfA7ohiTXe/fRu/rYuNfTs+145PuTVF", + "Q76rM305SR5cVPSYywMWx6zVg9EjbjCNpuxrg03ZDvS72/eXjyeKdVRCxaCiEZg2tGZN6C3H6eJfl0iY", + "xVmUzNVf/4iLdsFaXtTaGaYfsG65p0faD+7tjT5kYZG1QcHRh3ftrOvxrn25rUGRJYlS42BHZ9UIg6M0", + "/W8bD9oPXMd4OeeatP8afOgQk+YLM55NG7PteWzPYzmPac5BjKM37y4mjXy2ABzLVgZ7Z0Y8rx22FsHW", + "u7ojR+fIIMreJAs0XCIiEGUSccDR8nCFkZUBhUIcLvSdtNwEWgu5Zz5nNyEmd/CtbQ8uyR1QEOJZ7ULd", + "gi1Vo9gCjO6JXLBMonAB4RftezkEEt3+Ri1fY/Bbe9gOR+TZIWBnZHjm4QxhDp10d78AfWlyTr1R9Qv5", + "2+p14HfOf814HBwH4+Dh08P/BwAA//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/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/test_server_helpers_test.go b/internal/api/bulk/load/test_server_helpers_test.go index 3bedb15b..bb7c62dd 100644 --- a/internal/api/bulk/load/test_server_helpers_test.go +++ b/internal/api/bulk/load/test_server_helpers_test.go @@ -31,7 +31,6 @@ 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) } diff --git a/internal/explorer/service.go b/internal/explorer/service.go index 34a4f040..fbe6cb69 100644 --- a/internal/explorer/service.go +++ b/internal/explorer/service.go @@ -217,31 +217,6 @@ func (s *Service) CreateInteractiveFrom(ctx context.Context, project, id, title, 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) -} - // ApplyWorkspaceCommands is the authoritative Builder mutation boundary. It // resolves backend-owned identities, applies a command batch atomically, and // persists both the new draft and the replay record in one compare-and-swap. diff --git a/internal/server/explorer_authoring_v2_routes.go b/internal/server/explorer_authoring_v2_routes.go index f74a354d..c1c42b35 100644 --- a/internal/server/explorer_authoring_v2_routes.go +++ b/internal/server/explorer_authoring_v2_routes.go @@ -2,7 +2,6 @@ package server import ( "context" - "encoding/json" "errors" "fmt" "net/http" @@ -23,18 +22,14 @@ import ( ) 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 + getCapability fiber.Handler + searchSuggestions fiber.Handler + getBuilder fiber.Handler + applyCommands fiber.Handler + compileBuilder fiber.Handler + reconcile fiber.Handler + preview fiber.Handler + publish fiber.Handler } // newExplorerAuthoringHandlers is the hard-cut traversal Builder contract. @@ -67,7 +62,7 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead 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}, + Operations: []explorerv2api.AuthoringCapabilityOperations{explorerv2api.Builder, explorerv2api.Suggestions, explorerv2api.Preview, explorerv2api.Publish, explorerv2api.Commands, explorerv2api.Reconcile}, PreviewLimits: []int{10, 25, 50, 100}, Features: explorerv2api.AuthoringFeatures{EmissionFilters: true, EmissionCharts: true}, }) @@ -105,41 +100,6 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead 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 @@ -185,74 +145,6 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead 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 @@ -331,7 +223,6 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead 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 diff --git a/internal/server/explorer_config_route.go b/internal/server/explorer_config_route.go index 421f410a..20d32e60 100644 --- a/internal/server/explorer_config_route.go +++ b/internal/server/explorer_config_route.go @@ -21,7 +21,6 @@ type explorerConfigReadAuthorizer func(context.Context, *authscope.Principal, st type explorerHTTPHandlers struct { publishRepositoryConfig fiber.Handler - getRepositoryConfig fiber.Handler lifecycle *explorerLifecycleHandlers authoring *explorerAuthoringHandlers } @@ -131,21 +130,6 @@ func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead expl } 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}) } - 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}} } diff --git a/internal/server/explorer_config_route_test.go b/internal/server/explorer_config_route_test.go index 09f4790f..9f60c80c 100644 --- a/internal/server/explorer_config_route_test.go +++ b/internal/server/explorer_config_route_test.go @@ -2,7 +2,6 @@ package server import ( "context" - "fmt" "io" "net/http" "net/http/httptest" @@ -90,19 +89,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_receipt_contract_test.go b/internal/server/explorer_receipt_contract_test.go index 33c9ef5f..c3293011 100644 --- a/internal/server/explorer_receipt_contract_test.go +++ b/internal/server/explorer_receipt_contract_test.go @@ -167,7 +167,7 @@ func TestNativeV2RouteUsesAuthorizedPersistedReceipt(t *testing.T) { 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) + compiled := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/builder", body) if compiled.StatusCode != http.StatusOK { t.Fatalf("compile status=%d body=%s", compiled.StatusCode, compiled.Body) } @@ -277,18 +277,24 @@ 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.CreateInteractive(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() diff --git a/internal/server/explorer_route_test_helpers_test.go b/internal/server/explorer_route_test_helpers_test.go index 9430eca9..285e27fd 100644 --- a/internal/server/explorer_route_test_helpers_test.go +++ b/internal/server/explorer_route_test_helpers_test.go @@ -18,15 +18,11 @@ func registerTestExplorerAuthoringRoutes(router fiber.Router, authorizer authsco 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) } @@ -34,7 +30,6 @@ func registerTestExplorerAuthoringRoutes(router fiber.Router, authorizer authsco 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/openapi_data_routes.go b/internal/server/openapi_data_routes.go index 36255b6d..28ac5ce9 100644 --- a/internal/server/openapi_data_routes.go +++ b/internal/server/openapi_data_routes.go @@ -144,39 +144,6 @@ func (r *HTTPRoutes) ActivateDatasetGeneration(ctx context.Context, _ loomapi.Ac } } -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 { diff --git a/internal/server/openapi_explorer_routes.go b/internal/server/openapi_explorer_routes.go index 762491e5..4adc0429 100644 --- a/internal/server/openapi_explorer_routes.go +++ b/internal/server/openapi_explorer_routes.go @@ -116,31 +116,6 @@ func (r *HTTPRoutes) PublishRepositoryExplorerConfig(ctx context.Context, _ loom } } -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 { @@ -193,37 +168,6 @@ func (r *HTTPRoutes) SearchExplorerCandidates(ctx context.Context, _ loomapi.Sea } } -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 - } - 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 - default: - return nil, unexpectedResponseStatus("getExplorerCandidateSuggestions", 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 { @@ -253,66 +197,6 @@ func (r *HTTPRoutes) GetExplorerBuilder(ctx context.Context, _ loomapi.GetExplor } } -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 - } - 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 - default: - return nil, unexpectedResponseStatus("exportExplorerWorkspace", 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 { @@ -383,32 +267,6 @@ func (r *HTTPRoutes) CompileExplorerBuilder(ctx context.Context, _ loomapi.Compi } } -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 - } - 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 - default: - return nil, unexpectedResponseStatus("compileExplorer", 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 { diff --git a/internal/server/openapi_route_inventory_test.go b/internal/server/openapi_route_inventory_test.go index a1a71c48..5d5c8abf 100644 --- a/internal/server/openapi_route_inventory_test.go +++ b/internal/server/openapi_route_inventory_test.go @@ -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) != 33 || len(want) != 33 { + t.Errorf("route count got=%d spec=%d, want 33", len(got), len(want)) } } diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go index 667b654d..0255aa25 100644 --- a/internal/server/routes_test.go +++ b/internal/server/routes_test.go @@ -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}, } { diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 567c8e69..298706f8 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' @@ -115,32 +87,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/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' @@ -194,51 +140,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' @@ -429,38 +330,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' @@ -842,22 +711,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: @@ -1105,7 +958,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} @@ -1376,23 +1229,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 +1310,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 From 9603d911030f88b3b16214f14a8e68168bdfad2d Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 11:49:21 -0700 Subject: [PATCH 02/17] fix 409 bug --- docs/DATAFRAMER_RECIPE_REFERENCE.md | 1 + docs/EXPLORER_COMPILATION_ARCHITECTURE.md | 23 +++- .../explorer-v2-capability-authoring.md | 2 +- internal/dataframe/compiler/ir/clone.go | 110 +++++++++++++++++ .../compiler/lower/plan_orchestration.go | 5 +- .../compiler/lower/recipe_compile.go | 15 +-- .../compiler/lower/recipe_output_test.go | 80 +++++++++++++ .../dataframe/compiler/lower/recipe_schema.go | 3 +- internal/dataframe/recipe/document_types.go | 54 ++++++--- .../dataframe/recipe/document_validation.go | 26 ++++ internal/dataframe/recipe/types_test.go | 17 +++ .../dataframe/semantic/recipe_plan_output.go | 4 +- .../dataframe/semantic/recipe_plan_types.go | 11 +- internal/dataframe/semantic/semantic_plan.go | 16 +-- .../explorer/arango/receipt_store_test.go | 1 + .../explorer/compilation/semantic_compile.go | 17 ++- internal/explorer/compilation/translate.go | 2 +- .../explorer/compilation/translate_test.go | 7 +- internal/explorer/compilation_receipt.go | 113 +++++++++++------- internal/explorer/compilation_receipt_test.go | 26 ++++ internal/explorer/output_contract_test.go | 1 + .../server/explorer_authoring_v2_routes.go | 4 + internal/server/explorer_lifecycle_helpers.go | 74 ++++++++++-- internal/server/explorer_preview_response.go | 1 + .../server/explorer_preview_response_test.go | 5 +- internal/server/explorer_publication.go | 2 +- internal/server/explorer_receipt_contract.go | 82 +++++++++++-- .../server/explorer_receipt_contract_test.go | 90 +++++++++++++- internal/server/server.go | 9 +- 29 files changed, 677 insertions(+), 124 deletions(-) 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/EXPLORER_COMPILATION_ARCHITECTURE.md b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md index 2bfea737..186864e4 100644 --- a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md +++ b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md @@ -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 diff --git a/docs/frontend/explorer-v2-capability-authoring.md b/docs/frontend/explorer-v2-capability-authoring.md index 0be66687..85ea0c7b 100644 --- a/docs/frontend/explorer-v2-capability-authoring.md +++ b/docs/frontend/explorer-v2-capability-authoring.md @@ -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/v9+authoring-v2-native-5", "outputs": [], "diagnostics": [] } diff --git a/internal/dataframe/compiler/ir/clone.go b/internal/dataframe/compiler/ir/clone.go index 9a88701b..961a3e5b 100644 --- a/internal/dataframe/compiler/ir/clone.go +++ b/internal/dataframe/compiler/ir/clone.go @@ -62,6 +62,116 @@ 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.DerivedLet != nil && operation.DerivedLet.Expression != nil { + canonicalizePhysicalExpression(operation.DerivedLet.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.Lookup != nil { + canonicalizePhysicalExpression(&expression.Lookup.Source) + canonicalizePhysicalExpression(&expression.Lookup.ItemKey) + canonicalizePhysicalExpression(&expression.Lookup.ItemValue) + } + 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 diff --git a/internal/dataframe/compiler/lower/plan_orchestration.go b/internal/dataframe/compiler/lower/plan_orchestration.go index de0cf5ce..03efbee7 100644 --- a/internal/dataframe/compiler/lower/plan_orchestration.go +++ b/internal/dataframe/compiler/lower/plan_orchestration.go @@ -5,6 +5,7 @@ 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" ) @@ -72,7 +73,7 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic if physicalNodeNeedsMaterializedSet(child) { childSetIndex++ childProjectionPrefix := child.Alias - if projectionPrefix != "" { + if semantic.TraversalColumnNaming != recipe.TraversalColumnNamingAlias && projectionPrefix != "" { childProjectionPrefix = projectionPrefix + "__" + child.Alias } set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) @@ -123,7 +124,7 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic // filters and projections to be applied before materialization. childSetIndex++ childProjectionPrefix := child.Alias - if projectionPrefix != "" { + if semantic.TraversalColumnNaming != recipe.TraversalColumnNamingAlias && projectionPrefix != "" { childProjectionPrefix = projectionPrefix + "__" + child.Alias } set, projections, err := buildOptionalChildPhysicalSet(&physical, childSetIndex, parent, parentVariable, child, childProjectionPrefix, policy) diff --git a/internal/dataframe/compiler/lower/recipe_compile.go b/internal/dataframe/compiler/lower/recipe_compile.go index dcd4a617..cda739af 100644 --- a/internal/dataframe/compiler/lower/recipe_compile.go +++ b/internal/dataframe/compiler/lower/recipe_compile.go @@ -131,13 +131,14 @@ func compileRecipeOutput(output semantic.OutputPlan, bindings recipe.RuntimeBind return CompiledRecipeOutput{}, fmt.Errorf("row grain %q has no canonical identity", output.RowGrain) } semanticInput := semantic.SemanticPlan{ - Version: 1, - Project: bindings.Project, - DatasetGeneration: bindings.DatasetGeneration, - AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), - AuthScopeMode: bindings.AuthScopeMode, - Root: root, - RowIdentity: &identity, + Version: 1, + Project: bindings.Project, + DatasetGeneration: bindings.DatasetGeneration, + AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), + AuthScopeMode: bindings.AuthScopeMode, + TraversalColumnNaming: output.TraversalColumnNaming, + Root: root, + RowIdentity: &identity, } physical, err := BuildGenericPhysicalPlanWithPolicy(semanticInput, policy) if err != nil { diff --git a/internal/dataframe/compiler/lower/recipe_output_test.go b/internal/dataframe/compiler/lower/recipe_output_test.go index 2df20acd..55e3c52c 100644 --- a/internal/dataframe/compiler/lower/recipe_output_test.go +++ b/internal/dataframe/compiler/lower/recipe_output_test.go @@ -148,6 +148,86 @@ 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) + } +} + +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..07185f14 100644 --- a/internal/dataframe/compiler/lower/recipe_schema.go +++ b/internal/dataframe/compiler/lower/recipe_schema.go @@ -10,6 +10,7 @@ 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" ) @@ -112,7 +113,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/recipe/document_types.go b/internal/dataframe/recipe/document_types.go index 682bbd27..c931e7a2 100644 --- a/internal/dataframe/recipe/document_types.go +++ b/internal/dataframe/recipe/document_types.go @@ -31,21 +31,45 @@ 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"` + 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"` +} + +// 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. diff --git a/internal/dataframe/recipe/document_validation.go b/internal/dataframe/recipe/document_validation.go index b546322f..e37547d9 100644 --- a/internal/dataframe/recipe/document_validation.go +++ b/internal/dataframe/recipe/document_validation.go @@ -45,6 +45,9 @@ 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.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 +58,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 +94,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/types_test.go b/internal/dataframe/recipe/types_test.go index c036d3b1..1f9e6c63 100644 --- a/internal/dataframe/recipe/types_test.go +++ b/internal/dataframe/recipe/types_test.go @@ -116,6 +116,23 @@ 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 TestExplainContainsNoPhysicalDetails(t *testing.T) { b, err := Parse([]byte(validDocument)) if err != nil { diff --git a/internal/dataframe/semantic/recipe_plan_output.go b/internal/dataframe/semantic/recipe_plan_output.go index 144771f7..7ae43b2c 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, 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, TraversalColumnNaming: output.TraversalColumnNaming.Normalized(), Collision: output.CollisionPolicy} return finishRecipeOutput(plan, output, scope) } diff --git a/internal/dataframe/semantic/recipe_plan_types.go b/internal/dataframe/semantic/recipe_plan_types.go index 8fdbc72a..942a62d7 100644 --- a/internal/dataframe/semantic/recipe_plan_types.go +++ b/internal/dataframe/semantic/recipe_plan_types.go @@ -29,11 +29,12 @@ 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 + 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 diff --git a/internal/dataframe/semantic/semantic_plan.go b/internal/dataframe/semantic/semantic_plan.go index dba76395..a733cfc1 100644 --- a/internal/dataframe/semantic/semantic_plan.go +++ b/internal/dataframe/semantic/semantic_plan.go @@ -7,17 +7,19 @@ package semantic import ( "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/dataframe/expression" + "github.com/calypr/loom/internal/dataframe/recipe" "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 + Version int + Project string + DatasetGeneration string + AuthResourcePaths []string + AuthScopeMode authscope.ReadScopeMode + RowIdentity *spec.RowIdentity + TraversalColumnNaming recipe.TraversalColumnNaming + Root SemanticNode } type SemanticNode struct { 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/compilation/semantic_compile.go b/internal/explorer/compilation/semantic_compile.go index c1823bec..ec939d54 100644 --- a/internal/explorer/compilation/semantic_compile.go +++ b/internal/explorer/compilation/semantic_compile.go @@ -126,7 +126,7 @@ func compileSemanticDocument(ctx context.Context, project, explorerID string, do contract.Columns = append(contract.Columns, OutputColumn{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), TraversalColumnNaming: recipe.TraversalColumnNamingAlias, Fields: nodes[authoringv2.RootOccurrenceID].fields, Pivots: nodes[authoringv2.RootOccurrenceID].pivots, 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 { @@ -223,20 +223,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) diff --git a/internal/explorer/compilation/translate.go b/internal/explorer/compilation/translate.go index 70e616ca..827cdec7 100644 --- a/internal/explorer/compilation/translate.go +++ b/internal/explorer/compilation/translate.go @@ -20,7 +20,7 @@ import ( "github.com/calypr/loom/internal/explorer/capability" ) -const TranslationVersion = "authoring-v2-native-4" +const TranslationVersion = "authoring-v2-native-5" // Error is a structured translation failure. Stage, Code, Path, and Details // are intentionally transport-neutral so an HTTP adapter can map it to its diff --git a/internal/explorer/compilation/translate_test.go b/internal/explorer/compilation/translate_test.go index 22f58c0b..f53123a1 100644 --- a/internal/explorer/compilation/translate_test.go +++ b/internal/explorer/compilation/translate_test.go @@ -128,6 +128,9 @@ 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 := string(result.Bundle.Outputs[0].Fields[1].Expr.Literal); got != `"project-a"` { t.Fatalf("project binding = %s", got) } @@ -225,8 +228,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") diff --git a/internal/explorer/compilation_receipt.go b/internal/explorer/compilation_receipt.go index 5a4bd233..8904e19a 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/v9" // 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/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/server/explorer_authoring_v2_routes.go b/internal/server/explorer_authoring_v2_routes.go index c1c42b35..32e849ea 100644 --- a/internal/server/explorer_authoring_v2_routes.go +++ b/internal/server/explorer_authoring_v2_routes.go @@ -421,6 +421,10 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead execution, err = capabilities.Materialize(c.Context(), receipt.Bundle, bindings) } if err != nil { + var mismatch *receiptContractMismatch + if errors.As(err, &mismatch) { + return authoringHTTPError(c, &explorer.AuthoringError{Status: http.StatusConflict, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "publish", Code: "RECEIPT_RECOMPILE_REQUIRED", Message: "receipt deterministic lowering no longer matches the stored artifact", Details: receiptMismatchDetails(receipt.ID, err)}, Cause: err}) + } 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 { diff --git a/internal/server/explorer_lifecycle_helpers.go b/internal/server/explorer_lifecycle_helpers.go index a52bf47b..48dca7d6 100644 --- a/internal/server/explorer_lifecycle_helpers.go +++ b/internal/server/explorer_lifecycle_helpers.go @@ -8,6 +8,8 @@ import ( "strings" graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" + "github.com/calypr/loom/internal/dataframe/compiler/ir" + "github.com/calypr/loom/internal/dataframe/compiler/lower" "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/dataset" @@ -35,21 +37,77 @@ func verifyQueryableOutputs(bundle recipe.Bundle, execution graphresolver.Recipe return nil } -func resolvedOutputFingerprints(resolved engine.Resolved) map[string]string { +func resolvedOutputArtifacts(resolved engine.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, provenance, nil +} + +func resolvedOutputFingerprints(resolved engine.Resolved) map[string]string { + result, _, _ := resolvedOutputArtifacts(resolved) return result } + +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_preview_response.go b/internal/server/explorer_preview_response.go index 3a0b3eb2..306d49a6 100644 --- a/internal/server/explorer_preview_response.go +++ b/internal/server/explorer_preview_response.go @@ -69,6 +69,7 @@ func receiptPreviewConflict(cause error) error { Stage: "preview", Code: "RECEIPT_RECOMPILE_REQUIRED", Message: "receipt deterministic lowering no longer matches the stored artifact", + Details: receiptMismatchDetails("", cause), }, Cause: cause, } diff --git a/internal/server/explorer_preview_response_test.go b/internal/server/explorer_preview_response_test.go index 3e6cb85f..242a878a 100644 --- a/internal/server/explorer_preview_response_test.go +++ b/internal/server/explorer_preview_response_test.go @@ -68,7 +68,7 @@ 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"}, + {"receipt", &receiptPreviewResolutionError{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 { @@ -82,6 +82,9 @@ func TestPreviewRouteFailurePreservesStableClassifications(t *testing.T) { if strings.Contains(response.Body, "private") { t.Fatalf("private cause leaked: %s", response.Body) } + if test.name == "receipt" && (!strings.Contains(response.Body, `"component":"output_execution"`) || !strings.Contains(response.Body, `"outputId":"patients"`)) { + t.Fatalf("receipt mismatch details missing: %s", response.Body) + } }) } } diff --git a/internal/server/explorer_publication.go b/internal/server/explorer_publication.go index 464d7872..2ef94de6 100644 --- a/internal/server/explorer_publication.go +++ b/internal/server/explorer_publication.go @@ -27,7 +27,7 @@ func explorerReceiptMaterializer(recipeEngine *engine.Engine, target publication 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 { + if validationErr := validateReceiptResolution(receipt, &full); validationErr != nil { return validationErr } var publishErr error diff --git a/internal/server/explorer_receipt_contract.go b/internal/server/explorer_receipt_contract.go index c7685485..645efa88 100644 --- a/internal/server/explorer_receipt_contract.go +++ b/internal/server/explorer_receipt_contract.go @@ -3,6 +3,7 @@ package server import ( "context" "encoding/json" + "errors" "fmt" "sort" "strings" @@ -16,6 +17,47 @@ import ( "github.com/calypr/loom/internal/projectid" ) +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 @@ -171,27 +213,49 @@ func validateReceiptCapability(receipt *explorer.CompilationReceipt, snapshot ca // 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 *engine.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 @@ -208,11 +272,11 @@ func compileValidatedReceiptResolution(ctx context.Context, recipeEngine *engine if err != nil { return engine.Resolved{}, err } - if err := validateReceiptResolution(receipt, resolved); err != nil { + if err := validateReceiptResolution(receipt, &resolved); err != nil { return engine.Resolved{}, err } if err := validateReceiptEnginePublicColumns(receipt, resolved); err != nil { - return engine.Resolved{}, err + return engine.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 c3293011..699f6fb7 100644 --- a/internal/server/explorer_receipt_contract_test.go +++ b/internal/server/explorer_receipt_contract_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/compiler/ir" "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataframe/recipe/engine" "github.com/calypr/loom/internal/explorer" @@ -51,12 +52,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 +85,78 @@ func TestCompileValidatedReceiptResolutionChecksAllOutputsForScopedPreview(t *te } } +func TestCompileValidatedReceiptResolutionSurvivesCompilerMetadataJSONRoundTrip(t *testing.T) { + recipeEngine, err := engine.New(engine.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 := engine.New(engine.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) + } + before := resolvedOutputFingerprints(resolved)["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"}) + after := resolvedOutputFingerprints(resolved)["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 + } + if changed := resolvedOutputFingerprints(resolved)["patients"]; changed == before { + t.Fatal("execution bind change did not change canonical fingerprint") + } +} + func TestCompiledExplorerWorkspaceConfigUsesIndependentStablePresentationOrders(t *testing.T) { compiled := explorercompilation.WorkspaceResult{ Workspace: authoringv2.Workspace{ @@ -345,7 +423,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/server.go b/internal/server/server.go index 6b8c1da5..b4d48fc0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -427,8 +427,11 @@ func run(ctx context.Context, serverConfig Config) error { 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()} + 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: mappings, EmittedColumns: emitted, OutputFingerprints: fingerprints, OutputColumnProvenance: columnProvenance, RequestID: request.RequestID, CreatedAt: time.Now().UTC()} receipt.CompilationKey, err = explorer.CompilationKey(receipt) if err != nil { return nil, err @@ -443,7 +446,7 @@ func run(ctx context.Context, serverConfig Config) error { } 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}) + return nil, receiptCompilationConflict(stored.ID, err) } receiptBytes := 0 if raw, marshalErr := json.Marshal(stored); marshalErr == nil { From 2c6629b8e87bd7dc4f678c93d3b404edd2ad95d8 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 13:55:36 -0700 Subject: [PATCH 03/17] refactor(server): remove Fiber re-entry from OpenAPI routes --- internal/api/bulk/load/operations.go | 330 ++++++++ internal/api/http/recipe_execution.go | 33 +- internal/api/http/routes.go | 35 +- internal/ingest/partition.go | 77 -- internal/ingest/partition_test.go | 37 - .../server/explorer_authoring_v2_routes.go | 6 +- internal/server/explorer_config_route.go | 12 +- internal/server/explorer_lifecycle_helpers.go | 5 - internal/server/explorer_preview_response.go | 32 +- .../server/explorer_receipt_contract_test.go | 18 +- internal/server/openapi_data_routes.go | 753 ++++++++++-------- .../openapi_explorer_authoring_direct.go | 539 +++++++++++++ internal/server/openapi_explorer_direct.go | 309 +++++++ internal/server/openapi_explorer_routes.go | 478 +++++------ internal/server/openapi_graphql_routes.go | 78 +- .../server/openapi_route_inventory_test.go | 2 +- internal/server/openapi_routes.go | 71 +- internal/server/routes.go | 2 +- 18 files changed, 1973 insertions(+), 844 deletions(-) create mode 100644 internal/api/bulk/load/operations.go delete mode 100644 internal/ingest/partition.go delete mode 100644 internal/ingest/partition_test.go create mode 100644 internal/server/openapi_explorer_authoring_direct.go create mode 100644 internal/server/openapi_explorer_direct.go diff --git a/internal/api/bulk/load/operations.go b/internal/api/bulk/load/operations.go new file mode 100644 index 00000000..5cda4061 --- /dev/null +++ b/internal/api/bulk/load/operations.go @@ -0,0 +1,330 @@ +package load + +import ( + "context" + "encoding/json" + "errors" + "io" + "mime/multipart" + "net/http" + "os" + "strconv" + "strings" + + httpapi "github.com/calypr/loom/internal/api/http" + "github.com/calypr/loom/internal/authscope" + dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" + "github.com/calypr/loom/internal/dataset" + "github.com/gofiber/fiber/v3" +) + +// UploadProjectResource parses the generated multipart request and performs +// the resource import without constructing a Fiber context. The generated +// OpenAPI strict handler owns multipart framing; this method owns the load +// operation and its authorization policy. +func (h *Handler) UploadProjectResource(ctx context.Context, project, resourceType string, body *multipart.Reader, principal *authscope.Principal) (*ImportResult, error) { + if body == nil { + return nil, fiber.NewError(fiber.StatusUnsupportedMediaType) + } + project = strings.TrimSpace(project) + resourceType = strings.TrimSpace(resourceType) + if project == "" { + return nil, dataframeerrors.NewError(dataframeerrors.CodeProjectRequired, "") + } + if resourceType == "" { + return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidResourceType, "") + } + 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.NewError(dataframeerrors.CodeInvalidRequest, "") + } + if form == nil || len(form.File["file"]) == 0 { + return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + authResourcePath := strings.TrimSpace(firstFormValue(form.Value, "auth_resource_path")) + if err := h.authz.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") + } + authResourcePath = authscope.NormalizeAuthResourcePath(authResourcePath) + fileHeader := form.File["file"][0] + stagedPath, err := stageUploadedFile(fileHeader) + if err != nil { + return nil, 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(ctx, req) + if err != nil { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidData, "") + } + return result, 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 == "" { + return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") + } + files := form.File["file"] + if len(files) == 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(files) + if err != nil { + if errors.Is(err, os.ErrInvalid) { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidGenerationFile, "") + } + if errors.Is(err, os.ErrExist) { + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeDuplicateGenerationFile, "") + } + 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 + } + return h.service.RunGeneration(ctx, req) +} + +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, dataframeerrors.Wrap(err, dataframeerrors.CodePublicationConflict, "") + } + return map[string]any{"project": project, "generation": generation, "dataframeExecutionId": executionID, "activated": true}, nil +} + +type SnapshotCreateRequest struct { + GitCommit string `json:"gitCommit"` + ExpectedResourceTypes []string `json:"expectedResourceTypes"` + AuthResourcePath string `json:"authResourcePath,omitempty"` +} + +func (h *Handler) CreateSnapshot(ctx context.Context, project, generation string, body []byte, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { + var request SnapshotCreateRequest + if err := json.Unmarshal(body, &request); err != nil { + return dataset.SnapshotGeneration{}, err + } + project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) + if request.GitCommit == "" { + request.GitCommit = generation + } + if request.GitCommit != generation { + return dataset.SnapshotGeneration{}, dataset.ErrSnapshotConflict + } + if err := h.authorizeSnapshotContext(ctx, principal, project, request.AuthResourcePath); err != nil { + return dataset.SnapshotGeneration{}, err + } + if h.snapshots == nil { + return dataset.SnapshotGeneration{}, errors.New("snapshot service dependencies are required") + } + return h.snapshots.CreateOrResume(ctx, project, generation, request.AuthResourcePath, request.ExpectedResourceTypes) +} + +func (h *Handler) SnapshotStatus(ctx context.Context, project, generation string, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { + project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) + result, err := h.snapshots.Status(ctx, project, generation) + if err != nil { + return dataset.SnapshotGeneration{}, err + } + if err := h.authorizeSnapshotContext(ctx, principal, project, result.AuthResourcePath); err != nil { + return dataset.SnapshotGeneration{}, err + } + return result, nil +} + +func (h *Handler) UploadSnapshotResource(ctx context.Context, project, generation, resourceType, checksum string, body io.Reader, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { + project, generation, resourceType = strings.TrimSpace(project), strings.TrimSpace(generation), strings.TrimSpace(resourceType) + status, err := h.snapshots.Status(ctx, project, generation) + if err != nil { + return dataset.SnapshotGeneration{}, err + } + if err := h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath); err != nil { + return dataset.SnapshotGeneration{}, err + } + content, err := io.ReadAll(body) + if err != nil { + return dataset.SnapshotGeneration{}, err + } + return h.snapshots.Upload(ctx, project, generation, resourceType, checksum, content) +} + +func (h *Handler) FinalizeSnapshot(ctx context.Context, project, generation string, principal *authscope.Principal) (dataset.SnapshotGeneration, *GenerationLoadResult, error) { + project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) + status, err := h.snapshots.Status(ctx, project, generation) + if err != nil { + return dataset.SnapshotGeneration{}, nil, err + } + if err := h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath); err != nil { + return dataset.SnapshotGeneration{}, nil, err + } + submittedBy := "" + if principal != nil { + submittedBy = principal.Subject + } + return h.snapshots.Finalize(ctx, project, generation, submittedBy) +} + +func (h *Handler) AbortSnapshot(ctx context.Context, project, generation string, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { + project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) + status, err := h.snapshots.Status(ctx, project, generation) + if err != nil { + return dataset.SnapshotGeneration{}, err + } + if err := h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath); err != nil { + return dataset.SnapshotGeneration{}, err + } + return h.snapshots.Abort(ctx, project, generation) +} + +func (h *Handler) CreateRelease(ctx context.Context, project string, body []byte, principal *authscope.Principal) (dataset.ProjectRelease, error) { + var request dataset.ActivationRequest + if err := json.Unmarshal(body, &request); err != nil { + return dataset.ProjectRelease{}, err + } + request.Project = strings.TrimSpace(project) + if err := h.authorizeReleaseGenerationContext(ctx, request.Project, request.Generation, principal); err != nil { + return dataset.ProjectRelease{}, err + } + return h.releases.Create(ctx, request) +} + +func (h *Handler) ActivateReleaseCompatibility(ctx context.Context, project string, body []byte, principal *authscope.Principal) (dataset.ActiveRelease, error) { + var request dataset.ActivationRequest + if err := json.Unmarshal(body, &request); err != nil { + return dataset.ActiveRelease{}, err + } + request.Project = strings.TrimSpace(project) + if err := h.authorizeReleaseGenerationContext(ctx, request.Project, request.Generation, principal); err != nil { + return dataset.ActiveRelease{}, err + } + return h.releases.Activate(ctx, request) +} + +func (h *Handler) ActiveRelease(ctx context.Context, project string, principal *authscope.Principal) (dataset.ActiveRelease, error) { + project = strings.TrimSpace(project) + active, err := h.releases.Active(ctx, project) + if err != nil { + return dataset.ActiveRelease{}, err + } + if err := h.authorizeReleaseGenerationContext(ctx, project, active.Release.Generation, principal); err != nil { + return dataset.ActiveRelease{}, err + } + return active, nil +} + +func (h *Handler) ReleaseStatus(ctx context.Context, project, release string, principal *authscope.Principal) (dataset.ProjectRelease, error) { + project, release = strings.TrimSpace(project), strings.TrimSpace(release) + value, err := h.releases.Releases.ReadRelease(ctx, project, release) + if err != nil { + return dataset.ProjectRelease{}, err + } + if err := h.authorizeReleaseGenerationContext(ctx, project, value.Generation, principal); err != nil { + return dataset.ProjectRelease{}, err + } + return value, nil +} + +func (h *Handler) ActivateRelease(ctx context.Context, project, release string, body []byte, principal *authscope.Principal) (dataset.ActiveRelease, error) { + project, release = strings.TrimSpace(project), strings.TrimSpace(release) + value, err := h.releases.Releases.ReadRelease(ctx, project, release) + if err != nil { + return dataset.ActiveRelease{}, err + } + if err := h.authorizeReleaseGenerationContext(ctx, project, value.Generation, principal); err != nil { + return dataset.ActiveRelease{}, err + } + var request struct { + ExpectedRevision int64 `json:"expectedRevision"` + } + if err := json.Unmarshal(body, &request); err != nil { + return dataset.ActiveRelease{}, err + } + return h.releases.ActivateExisting(ctx, project, release, request.ExpectedRevision) +} + +func (h *Handler) authorizeSnapshotContext(ctx context.Context, principal *authscope.Principal, project, authResourcePath string) error { + return h.authz.AuthorizeWrite(ctx, principal, project, authResourcePath) +} + +func (h *Handler) authorizeReleaseGenerationContext(ctx context.Context, project, generation string, principal *authscope.Principal) error { + status, err := h.snapshots.Status(ctx, project, generation) + if err != nil { + return err + } + return h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath) +} + +func firstFormValue(values map[string][]string, key string) string { + if items := values[key]; len(items) != 0 { + return items[0] + } + return "" +} + +// MapSnapshotError preserves the status and safe snapshot error envelope used +// by the legacy Fiber routes for generated OpenAPI callers. +func MapSnapshotError(err error, requestID string) (int, httpapi.ErrorResponse) { + 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: + mapped := httpapi.MapDataframeError(err, requestID) + return mapped.Status, mapped.Body + } + details := map[string]any(nil) + var requirements *dataset.ReleaseRequirementsError + if errors.As(err, &requirements) { + details = map[string]any{"verifications": requirements.Verifications} + } + return status, httpapi.ErrorResponse{Error: httpapi.HTTPErrorBody{Code: code, Message: message, Details: details, Retryable: retryable, RequestID: requestID}} +} diff --git a/internal/api/http/recipe_execution.go b/internal/api/http/recipe_execution.go index b4ce8984..493692e6 100644 --- a/internal/api/http/recipe_execution.go +++ b/internal/api/http/recipe_execution.go @@ -1,6 +1,7 @@ package httpapi import ( + "context" "errors" "net/http" "strings" @@ -18,27 +19,35 @@ type RecipeExecutionHandler struct { } func (h RecipeExecutionHandler) Handle(c fiber.Ctx) error { + body, status := h.Execute(c.Context(), c.Params("id")) + return c.Status(status).JSON(body) +} + +// Execute returns the legacy recipe execution document for the generated +// OpenAPI adapter. It deliberately keeps the historical not-found response +// indistinguishable for missing catalog entries and denied scopes. +func (h RecipeExecutionHandler) Execute(ctx context.Context, id string) (map[string]any, int) { if h.Catalog == nil { - return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "recipe execution not found"}) + return map[string]any{"error": "recipe execution not found"}, http.StatusNotFound } - id := strings.TrimSpace(c.Params("id")) - execution, err := h.Catalog.GetExecution(c.Context(), id) + id = strings.TrimSpace(id) + execution, err := h.Catalog.GetExecution(ctx, id) if err != nil || execution.ID == "" { - return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "recipe execution not found"}) + return map[string]any{"error": "recipe execution not found"}, http.StatusNotFound } 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 { + principal, _ := authscope.PrincipalFromContext(ctx) + if _, scopeErr := h.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 c.Status(status).JSON(fiber.Map{"error": "recipe execution not found"}) + return map[string]any{"error": "recipe execution not found"}, status } } - outputs := make([]fiber.Map, 0, len(execution.Outputs)) + outputs := make([]map[string]any, 0, len(execution.Outputs)) for _, output := range execution.Outputs { - columns := make([]fiber.Map, 0, len(output.Columns)) + 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 != "" { @@ -50,11 +59,11 @@ func (h RecipeExecutionHandler) Handle(c fiber.Ctx) error { 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}) + 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, fiber.Map{"name": output.Name, "state": recipeExecutionHTTPState(output.State), "rowCount": output.RowCount, "columns": columns}) + outputs = append(outputs, map[string]any{"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}) + 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 } // recipeExecutionHTTPState preserves the READY spelling used by the legacy diff --git a/internal/api/http/routes.go b/internal/api/http/routes.go index 0b3bba88..3cb321b2 100644 --- a/internal/api/http/routes.go +++ b/internal/api/http/routes.go @@ -12,31 +12,48 @@ func (s *HTTPServer) register() { } func (s *HTTPServer) HandleHealth(c fiber.Ctx) error { + body, status := s.Health(c.Context()) + return c.Status(status).JSON(body) +} + +// 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"}) + body, status := s.Liveness(c.Context()) + return c.Status(status).JSON(body) +} + +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) + body, status := s.Readiness(c.Context()) + return c.Status(status).JSON(body) +} + +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 +76,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/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/server/explorer_authoring_v2_routes.go b/internal/server/explorer_authoring_v2_routes.go index 32e849ea..10165c07 100644 --- a/internal/server/explorer_authoring_v2_routes.go +++ b/internal/server/explorer_authoring_v2_routes.go @@ -457,7 +457,11 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead } 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 validateV2ReceiptRouteValues(receipt, capabilities, explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId"))) +} + +func validateV2ReceiptRouteValues(receipt *explorer.CompilationReceipt, capabilities ExplorerV2LifecycleConfig, project, explorerID string) error { + if receipt == nil || receipt.Project != project || receipt.ExplorerID != 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 { diff --git a/internal/server/explorer_config_route.go b/internal/server/explorer_config_route.go index 20d32e60..c159502d 100644 --- a/internal/server/explorer_config_route.go +++ b/internal/server/explorer_config_route.go @@ -23,12 +23,21 @@ type explorerHTTPHandlers struct { publishRepositoryConfig fiber.Handler lifecycle *explorerLifecycleHandlers authoring *explorerAuthoringHandlers + + // The generated OpenAPI methods invoke these dependencies directly. The + // Fiber handlers above remain only for the legacy route-focused tests until + // the Explorer lifecycle is moved into its application package. + authorizer authscope.Authorizer + authorizeRead explorerConfigReadAuthorizer + explorers *explorer.Service + materialize graphresolver.ExplorerBundleMaterializer + lifecycleConfig ExplorerV2LifecycleConfig } // 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{} + handlers := &explorerHTTPHandlers{authorizer: authorizer, authorizeRead: authorizeRead, explorers: explorers, materialize: materialize} if authorizer == nil || authorizeRead == nil || explorers == nil { return handlers } @@ -133,6 +142,7 @@ func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead expl if len(lifecycle) == 0 { lifecycle = []ExplorerV2LifecycleConfig{{Materialize: materialize}} } + handlers.lifecycleConfig = lifecycle[0] handlers.lifecycle = newExplorerLifecycleHandlers(authorizer, authorizeRead, explorers, lifecycle[0]) handlers.authoring = newExplorerAuthoringHandlers(authorizer, authorizeRead, explorers, lifecycle[0]) return handlers diff --git a/internal/server/explorer_lifecycle_helpers.go b/internal/server/explorer_lifecycle_helpers.go index 48dca7d6..43f80201 100644 --- a/internal/server/explorer_lifecycle_helpers.go +++ b/internal/server/explorer_lifecycle_helpers.go @@ -86,11 +86,6 @@ func resolvedOutputArtifacts(resolved engine.Resolved) (map[string]string, map[s return result, provenance, nil } -func resolvedOutputFingerprints(resolved engine.Resolved) map[string]string { - result, _, _ := resolvedOutputArtifacts(resolved) - return result -} - 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") diff --git a/internal/server/explorer_preview_response.go b/internal/server/explorer_preview_response.go index 306d49a6..cdad9cdc 100644 --- a/internal/server/explorer_preview_response.go +++ b/internal/server/explorer_preview_response.go @@ -28,37 +28,45 @@ 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 { + return authoringHTTPError(c, previewRouteError(err)) +} + +// previewRouteError maps execution failures to the stable authoring error +// contract without requiring a Fiber context. The generated OpenAPI adapter +// uses this form directly, while the legacy Fiber route above only adds the +// transport response encoding. +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.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 { diff --git a/internal/server/explorer_receipt_contract_test.go b/internal/server/explorer_receipt_contract_test.go index 699f6fb7..510d4744 100644 --- a/internal/server/explorer_receipt_contract_test.go +++ b/internal/server/explorer_receipt_contract_test.go @@ -141,10 +141,18 @@ func TestResolvedOutputFingerprintExcludesOptimizerAndTransientProvenance(t *tes if err != nil { t.Fatal(err) } - before := resolvedOutputFingerprints(resolved)["patients"] + 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"}) - after := resolvedOutputFingerprints(resolved)["patients"] + 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) } @@ -152,7 +160,11 @@ func TestResolvedOutputFingerprintExcludesOptimizerAndTransientProvenance(t *tes resolved.Compiled.Outputs[0].Plan.BindVars[key] = "semantically-different" break } - if changed := resolvedOutputFingerprints(resolved)["patients"]; changed == before { + 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") } } diff --git a/internal/server/openapi_data_routes.go b/internal/server/openapi_data_routes.go index 28ac5ce9..1cb07852 100644 --- a/internal/server/openapi_data_routes.go +++ b/internal/server/openapi_data_routes.go @@ -2,495 +2,542 @@ package server import ( "context" + "encoding/json" + "errors" "net/http" loomapi "github.com/calypr/loom/generated/loomapi" + loadapi "github.com/calypr/loom/internal/api/bulk/load" + 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) UploadProjectResource(ctx context.Context, request loomapi.UploadProjectResourceRequestObject) (loomapi.UploadProjectResourceResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.UploadProjectResource(ctx, request.Project, request.ResourceType, request.Body, principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.UploadProjectResource200JSONResponse(value), nil + } + status, body := mapServiceError(err, ctx) switch status { case http.StatusBadRequest: - return loomapi.UploadProjectResource400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(value)}, nil + return loomapi.UploadProjectResource400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(body)}, nil case http.StatusUnauthorized: - return loomapi.UploadProjectResource401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(value)}, nil + return loomapi.UploadProjectResource401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(body)}, nil case http.StatusForbidden: - return loomapi.UploadProjectResource403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(value)}, nil + return loomapi.UploadProjectResource403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(body)}, nil case http.StatusUnsupportedMediaType: - return loomapi.UploadProjectResource415JSONResponse{ServiceUnsupportedMediaTypeJSONResponse: loomapi.ServiceUnsupportedMediaTypeJSONResponse(value)}, nil + return loomapi.UploadProjectResource415JSONResponse{ServiceUnsupportedMediaTypeJSONResponse: loomapi.ServiceUnsupportedMediaTypeJSONResponse(body)}, nil case http.StatusUnprocessableEntity: - return loomapi.UploadProjectResource422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(value)}, nil + return loomapi.UploadProjectResource422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(body)}, nil case http.StatusInternalServerError: - return loomapi.UploadProjectResource500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(value)}, nil + return loomapi.UploadProjectResource500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(body)}, nil case http.StatusServiceUnavailable: - return loomapi.UploadProjectResource503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(value)}, nil + return loomapi.UploadProjectResource503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(body)}, nil default: return nil, unexpectedResponseStatus("uploadProjectResource", 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) 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 - } +func (r *HTTPRoutes) CreateSnapshot(ctx context.Context, request loomapi.CreateSnapshotRequestObject) (loomapi.CreateSnapshotResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + body, err := marshalRequestBody(request.Body) + if err == nil { + result, operationErr := r.load.CreateSnapshot(ctx, request.Project, request.Generation, body, principal) + if operationErr == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.CreateSnapshot200JSONResponse(value), nil + } + err = operationErr + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusBadRequest: + return loomapi.CreateSnapshot400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil + case http.StatusUnauthorized: + return loomapi.CreateSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.CreateSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusConflict: + return loomapi.CreateSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.CreateSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.CreateSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, 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 - } +func (r *HTTPRoutes) GetSnapshot(ctx context.Context, request loomapi.GetSnapshotRequestObject) (loomapi.GetSnapshotResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.SnapshotStatus(ctx, request.Project, request.Generation, principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.GetSnapshot200JSONResponse(value), nil + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusUnauthorized: + return loomapi.GetSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.GetSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.GetSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.GetSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.GetSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, 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 - } +func (r *HTTPRoutes) AbortSnapshot(ctx context.Context, request loomapi.AbortSnapshotRequestObject) (loomapi.AbortSnapshotResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.AbortSnapshot(ctx, request.Project, request.Generation, principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.AbortSnapshot200JSONResponse(value), nil + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusUnauthorized: + return loomapi.AbortSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.AbortSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.AbortSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusConflict: + return loomapi.AbortSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.AbortSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.AbortSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, 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) - 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 - } +func (r *HTTPRoutes) UploadSnapshotResource(ctx context.Context, request loomapi.UploadSnapshotResourceRequestObject) (loomapi.UploadSnapshotResourceResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.UploadSnapshotResource(ctx, request.Project, request.Generation, request.ResourceType, optionalString(request.Params.XContentSHA256), request.Body, principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.UploadSnapshotResource200JSONResponse(value), nil + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusBadRequest: + return loomapi.UploadSnapshotResource400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil + case http.StatusUnauthorized: + return loomapi.UploadSnapshotResource401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.UploadSnapshotResource403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.UploadSnapshotResource404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusConflict: + return loomapi.UploadSnapshotResource409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil + case http.StatusUnprocessableEntity: + return loomapi.UploadSnapshotResource422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.UploadSnapshotResource500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.UploadSnapshotResource503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, 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 - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.FinalizeSnapshot200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } +func (r *HTTPRoutes) FinalizeSnapshot(ctx context.Context, request loomapi.FinalizeSnapshotRequestObject) (loomapi.FinalizeSnapshotResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + generation, result, err := r.load.FinalizeSnapshot(ctx, request.Project, request.Generation, principal) + if err == nil { + value, conversionErr := rawJSON(map[string]any{"generation": generation, "load": result}) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.FinalizeSnapshot200JSONResponse(value), nil + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusBadRequest: + return loomapi.FinalizeSnapshot400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil + case http.StatusUnauthorized: + return loomapi.FinalizeSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.FinalizeSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.FinalizeSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusConflict: + return loomapi.FinalizeSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil + case http.StatusUnprocessableEntity: + return loomapi.FinalizeSnapshot422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.FinalizeSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.FinalizeSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil default: return nil, unexpectedResponseStatus("finalizeSnapshot", 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 - } +func (r *HTTPRoutes) CreateRelease(ctx context.Context, request loomapi.CreateReleaseRequestObject) (loomapi.CreateReleaseResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + body, err := marshalRequestBody(request.Body) + if err == nil { + result, operationErr := r.load.CreateRelease(ctx, request.Project, body, principal) + if operationErr == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.CreateRelease200JSONResponse(value), nil + } + err = operationErr + } + return r.createReleaseError(err, ctx) +} + +func (r *HTTPRoutes) createReleaseError(err error, ctx context.Context) (loomapi.CreateReleaseResponseObject, error) { + status, mapped := snapshotError(err, ctx) 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 + case http.StatusBadRequest: + return loomapi.CreateRelease400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil + case http.StatusUnauthorized: + return loomapi.CreateRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.CreateRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.CreateRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusConflict: + return loomapi.CreateRelease409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil + case http.StatusUnprocessableEntity: + return loomapi.CreateRelease422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.CreateRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.CreateRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil default: return nil, unexpectedResponseStatus("createRelease", status) } } -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 - } +func (r *HTTPRoutes) GetActiveRelease(ctx context.Context, request loomapi.GetActiveReleaseRequestObject) (loomapi.GetActiveReleaseResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.ActiveRelease(ctx, request.Project, principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.GetActiveRelease200JSONResponse(value), nil + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusUnauthorized: + return loomapi.GetActiveRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.GetActiveRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.GetActiveRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.GetActiveRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.GetActiveRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil default: return nil, unexpectedResponseStatus("getActiveRelease", status) } } -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 - } - if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetRelease200JSONResponse](body) - return response, decodeErr - } - value, decodeErr := serviceError(body) - if decodeErr != nil { - return nil, decodeErr - } +func (r *HTTPRoutes) GetRelease(ctx context.Context, request loomapi.GetReleaseRequestObject) (loomapi.GetReleaseResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + result, err := r.load.ReleaseStatus(ctx, request.Project, request.Release, principal) + if err == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.GetRelease200JSONResponse(value), nil + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusUnauthorized: + return loomapi.GetRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.GetRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.GetRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.GetRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.GetRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, 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 - } +func (r *HTTPRoutes) ActivateRelease(ctx context.Context, request loomapi.ActivateReleaseRequestObject) (loomapi.ActivateReleaseResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + body, err := marshalRequestBody(request.Body) + if err == nil { + result, operationErr := r.load.ActivateRelease(ctx, request.Project, request.Release, body, principal) + if operationErr == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.ActivateRelease200JSONResponse(value), nil + } + err = operationErr + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusBadRequest: + return loomapi.ActivateRelease400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil + case http.StatusUnauthorized: + return loomapi.ActivateRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.ActivateRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.ActivateRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusConflict: + return loomapi.ActivateRelease409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil + case http.StatusUnprocessableEntity: + return loomapi.ActivateRelease422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.ActivateRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.ActivateRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, 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) - 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 - } +func (r *HTTPRoutes) ActivateReleaseCompatibility(ctx context.Context, request loomapi.ActivateReleaseCompatibilityRequestObject) (loomapi.ActivateReleaseCompatibilityResponseObject, error) { + principal, _ := authscope.PrincipalFromContext(ctx) + body, err := marshalRequestBody(request.Body) + if err == nil { + result, operationErr := r.load.ActivateReleaseCompatibility(ctx, request.Project, body, principal) + if operationErr == nil { + value, conversionErr := rawJSON(result) + if conversionErr != nil { + return nil, conversionErr + } + return loomapi.ActivateReleaseCompatibility200JSONResponse(value), nil + } + err = operationErr + } + status, mapped := snapshotError(err, ctx) 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 + case http.StatusBadRequest: + return loomapi.ActivateReleaseCompatibility400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil + case http.StatusUnauthorized: + return loomapi.ActivateReleaseCompatibility401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil + case http.StatusForbidden: + return loomapi.ActivateReleaseCompatibility403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil + case http.StatusNotFound: + return loomapi.ActivateReleaseCompatibility404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil + case http.StatusConflict: + return loomapi.ActivateReleaseCompatibility409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil + case http.StatusUnprocessableEntity: + return loomapi.ActivateReleaseCompatibility422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil + case http.StatusInternalServerError: + return loomapi.ActivateReleaseCompatibility500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil + case http.StatusServiceUnavailable: + return loomapi.ActivateReleaseCompatibility503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, 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) +func (r *HTTPRoutes) GetRecipeExecution(ctx context.Context, request loomapi.GetRecipeExecutionRequestObject) (loomapi.GetRecipeExecutionResponseObject, error) { + body, status := r.recipe.Execute(ctx, request.Id) + value, err := rawJSON(body) if err != nil { return nil, err } if status == http.StatusOK { - response, decodeErr := decodeResponse[loomapi.GetRecipeExecution200JSONResponse](body) - return response, decodeErr + return loomapi.GetRecipeExecution200JSONResponse(value), nil } if status == http.StatusUnauthorized { - response, decodeErr := decodeResponse[loomapi.GetRecipeExecution401JSONResponse](body) - return response, decodeErr + return loomapi.GetRecipeExecution401JSONResponse(value), nil + } + if status == http.StatusForbidden { + legacy, legacyErr := legacyErrorFromMap(value) + if legacyErr != nil { + return nil, legacyErr + } + return loomapi.GetRecipeExecution403JSONResponse{LegacyForbiddenJSONResponse: loomapi.LegacyForbiddenJSONResponse(legacy)}, nil + } + 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 optionalString(value *string) string { + if value == nil { + return "" } - value, decodeErr := legacyError(body) - if decodeErr != nil { - return nil, decodeErr + return *value +} + +func marshalRequestBody(body any) ([]byte, error) { + if body == nil { + return nil, errors.New("request body is required") } - 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 json.Marshal(body) +} + +func mapServiceError(err error, ctx context.Context) (int, loomapi.ServiceErrorResponse) { + mapped := httpapi.MapDataframeError(err, httpapi.RequestIDFromContext(ctx)) + return mapped.Status, serviceErrorResponse(mapped.Body) +} + +func snapshotError(err error, ctx context.Context) (int, loomapi.ServiceErrorResponse) { + status, body := loadapi.MapSnapshotError(err, httpapi.RequestIDFromContext(ctx)) + return status, serviceErrorResponse(body) +} + +func rawJSON(value any) (loomapi.RawJSON, error) { + if value == nil { + return loomapi.RawJSON{}, nil + } + if raw, ok := value.(loomapi.RawJSON); ok { + return raw, nil + } + payload, err := json.Marshal(value) + if err != nil { + return nil, err + } + var result loomapi.RawJSON + if err := json.Unmarshal(payload, &result); err != nil { + return nil, err } + 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..cbe04d03 --- /dev/null +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -0,0 +1,539 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + loomapi "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" +) + +// directAuthoringJSON converts the transport-neutral authoring values to the +// generated OpenAPI values. The generated types intentionally remain the +// boundary representation; the authoring package does not import transport +// code. +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: 403, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err} + } + return nil +} + +func (h *explorerHTTPHandlers) authoringWriteDirect(ctx context.Context, project 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, ""); err != nil { + return &explorer.AuthoringError{Status: 403, 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")) + } + request := *body + if strings.TrimSpace(request.SnapshotToken) == "" || strings.TrimSpace(request.NodeId) == "" { + return result, malformedRouteError("suggestions", errors.New("snapshotToken and nodeId are required")) + } + config := h.lifecycleConfig + if config.CapabilityToken == nil { + return result, explorerUnavailable("suggestions", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured") + } + snapshot, err := config.CapabilityToken(ctx, project, request.SnapshotToken) + if err != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { + return result, explorerConflict("suggestions", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale", nil) + } + query := "" + if request.Query != nil { + query = strings.ToLower(strings.TrimSpace(*request.Query)) + } + internal := authoringV2Catalog(snapshot, explorerID) + candidates := make([]loomapi.CatalogCandidate, 0) + for _, candidate := range internal.Candidates { + if candidate.NodeID != request.NodeId || (query != "" && !strings.Contains(strings.ToLower(candidate.Label), query) && !strings.Contains(strings.ToLower(candidate.ID), query)) { + continue + } + 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: request.SnapshotToken, + NodeId: request.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 + } + if h == nil || h.explorers == nil { + return result, explorerUnavailable("builder", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") + } + config := h.lifecycleConfig + if config.Capability == nil { + return result, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", "Explorer capability compiler is not configured") + } + snapshot, err := config.Capability(ctx, project, explorerID, "") + if err != nil || !snapshot.Usable() { + if err == nil { + err = capability.ErrSnapshotUnavailable + } + return result, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error()) + } + wire, err := directAuthoringJSON[loomapi.Catalog](authoringV2Catalog(snapshot, explorerID)) + if err != nil { + return result, err + } + owner, err := h.explorers.Get(ctx, project, explorerID) + if err != nil { + return result, err + } + state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, LifecycleState: authoringv2.LifecycleNew, DraftVersion: owner.DraftVersion, DraftDigest: owner.DraftDigest, Catalog: authoringV2Catalog(snapshot, explorerID)} + var activeWorkspace *authoringv2.Workspace + if owner.ActiveRevisionID != "" { + active, activeErr := h.explorers.ActiveRevision(ctx, project, explorerID) + if activeErr != nil { + return result, 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 result, 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 result, 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 result, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error()) + } + // Keep the conversion explicit; the internal state has the same wire + // representation but must not leak its transport-only hidden fields. + _ = wire + return directAuthoringJSON[loomapi.BuilderState](state) +} + +func (h *explorerHTTPHandlers) applyAuthoringCommandsDirect(ctx context.Context, project, explorerID string, body *loomapi.ApplyExplorerBuilderCommandsJSONRequestBody) (loomapi.ApplyCommandsResponse, error) { + var result loomapi.ApplyCommandsResponse + if err := h.authoringWriteDirect(ctx, project); 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) + } + if err := request.Validate(); err != nil { + return result, malformedRouteError("commands", err) + } + config := h.lifecycleConfig + if config.CapabilityToken == nil { + return result, explorerUnavailable("commands", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured") + } + snapshot, snapshotErr := config.CapabilityToken(ctx, project, request.SnapshotToken) + if snapshotErr != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { + return result, explorerConflict("commands", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale or unavailable", nil) + } + if h.explorers == nil { + return result, explorerUnavailable("commands", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") + } + response, applyErr := h.explorers.ApplyWorkspaceCommands(ctx, project, explorerID, authoringV2Catalog(snapshot, explorerID), request, subjectFromContext(ctx)) + switch { + case errors.Is(applyErr, explorer.ErrDraftConflict): + return result, explorerConflict("commands", "DRAFT_CONFLICT", "the Explorer draft changed; reload before editing", nil) + case errors.Is(applyErr, explorer.ErrAuthoringCommandConflict): + return result, explorerConflict("commands", "COMMAND_ID_CONFLICT", "commandId was already used for different intent", nil) + case applyErr != nil: + return result, authoringSemanticRoute("commands", "$.commands", "INVALID_AUTHORING_COMMAND", applyErr.Error(), nil) + default: + return directAuthoringJSON[loomapi.ApplyCommandsResponse](response) + } +} + +func (h *explorerHTTPHandlers) compileAuthoringWorkspaceDirect(ctx context.Context, project, explorerID string, workspace authoringv2.Workspace, snapshotToken string) (loomapi.CompileResponse, error) { + var result loomapi.CompileResponse + config := h.lifecycleConfig + if (config.CapabilityToken == nil && config.AuthorizedCapabilityCompile == nil) || config.CompileReceipt == nil { + return result, explorerUnavailable("compile", "CAPABILITY_UNAVAILABLE", "Explorer V2 compiler is not configured") + } + var err error + var snapshot capability.Snapshot + var authorized AuthorizedCapability + if config.AuthorizedCapabilityCompile != nil { + authorized, err = config.AuthorizedCapabilityCompile(ctx, project, snapshotToken) + snapshot = authorized.Snapshot + } else { + snapshot, err = config.CapabilityToken(ctx, project, snapshotToken) + } + if err != nil { + return result, explorerConflict("capability", "STALE_CATALOG_SNAPSHOT", "the capability snapshot is stale or unavailable", map[string]any{"snapshotToken": snapshotToken}) + } + state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, Workspace: &workspace, Catalog: authoringV2Catalog(snapshot, explorerID)} + if err := state.Validate(); err != nil { + return result, authoringSemanticRoute("intent", "$", workspaceValidationCode(err), err.Error(), nil) + } + stored, err := config.CompileReceipt(ctx, ExplorerV2ReceiptCompileRequest{Project: project, ExplorerID: explorerID, Workspace: workspace, SnapshotToken: snapshot.Token, RequestID: requestIDFromContext(ctx), Authorized: authorized}) + if err != nil { + var compileErr *explorercompilation.Error + if errors.As(err, &compileErr) { + return result, authoringSemanticRoute(compileErr.Stage, compileErr.Path, compilationErrorCode(compileErr.Code), compileErr.Message, compileErr.Details) + } + return result, err + } + if stored == nil || strings.TrimSpace(stored.ID) == "" { + return result, explorerUnavailable("compile", "COMPILATION_RECEIPT_STORE_FAILED", "compiled authoring receipt was not persisted") + } + return v2ReceiptResponse(stored, workspace), nil +} + +func (h *explorerHTTPHandlers) compileAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.CompileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { + var result loomapi.CompileResponse + if err := h.authoringWriteDirect(ctx, project); err != nil { + return result, err + } + if body == nil { + return result, malformedRouteError("intent", errors.New("request body is required")) + } + return h.compileAuthoringWorkspaceDirect(ctx, project, explorerID, body.Workspace, body.SnapshotToken) +} + +func (h *explorerHTTPHandlers) reconcileAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.ReconcileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { + var result loomapi.CompileResponse + if err := h.authoringWriteDirect(ctx, project); err != nil { + return result, err + } + if body == nil { + return result, malformedRouteError("reconcile", errors.New("request body is required")) + } + if strings.TrimSpace(body.SnapshotToken) == "" || body.DraftVersion < 0 { + return result, malformedRouteError("reconcile", errors.New("snapshotToken, draftVersion, and draftDigest are required")) + } + if h == nil || h.explorers == nil { + return result, explorerUnavailable("reconcile", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") + } + owner, err := h.explorers.Get(ctx, project, explorerID) + if err != nil { + return result, err + } + if owner.DraftVersion != body.DraftVersion || owner.DraftDigest != body.DraftDigest { + return result, explorerConflict("reconcile", "DRAFT_CONFLICT", "the Explorer draft changed; reload before reconciling", nil) + } + workspace, err := authoringv2.DecodeWorkspace(owner.DraftConfig) + if err != nil { + return result, explorerConflict("reconcile", "AUTHORING_STATE_MISSING", "the saved Explorer draft cannot be reconciled", nil) + } + return h.compileAuthoringWorkspaceDirect(ctx, project, explorerID, workspace, body.SnapshotToken) +} + +func (h *explorerHTTPHandlers) previewAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.PreviewExplorerJSONRequestBody) (loomapi.PreviewResponse, error) { + var result loomapi.PreviewResponse + started := time.Now() + previewCtx, cancel := context.WithTimeout(ctx, explorerPreviewTimeout) + defer cancel() + responseBytes, responseRows := 0, 0 + logReceiptID, logOutputID := "", "" + lookupDuration, authorizationDuration := time.Duration(0), time.Duration(0) + previewSummary := engine.PreviewSummary{} + outcome := "failure" + config := h.lifecycleConfig + defer func() { + if config.Logger != nil { + config.Logger.Info("Explorer receipt preview", "operation", "receipt_preview", "project", project, "explorer_id", 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 := h.authoringReadDirect(ctx, project); err != nil { + return result, err + } + if body == nil || strings.TrimSpace(body.ReceiptId) == "" || strings.TrimSpace(body.OutputId) == "" { + return result, malformedRouteError("preview", errors.New("receiptId and outputId are required")) + } + logReceiptID, logOutputID = body.ReceiptId, body.OutputId + limit := engine.DefaultPreviewLimit + if body.Limit != nil { + limit = *body.Limit + } + if limit < 1 || limit > engine.MaxPreviewLimit { + return result, authoringSemanticRoute("preview", "$.limit", "INVALID_PREVIEW_LIMIT", "limit must be between 1 and 1000", nil) + } + nativeReceiptPreview := config.PreviewReceipt != nil + if config.AuthorizedCapabilityExecution == nil { + return result, explorerUnavailable("preview", "PREVIEW_UNAVAILABLE", "authorized receipt execution is not configured") + } + if config.Preview == nil && config.PreviewReceipt == nil { + return result, explorerUnavailable("preview", "PREVIEW_UNAVAILABLE", "Explorer preview is not configured") + } + lookupStarted := time.Now() + receipt, err := lookupV2Receipt(previewCtx, h.explorers, config, project, explorerID, body.ReceiptId) + lookupDuration = time.Since(lookupStarted) + if err != nil { + return result, receiptRouteError("preview", err) + } + if err := validateV2ReceiptRouteValues(receipt, config, project, explorerID); err != nil { + return result, err + } + authorizationStarted := time.Now() + authorized, err := config.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 result, explorerConflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil) + } + if config.PreviewReceipt != nil { + if err := validateAuthorizedReceiptExecution(receipt, authorized); err != nil { + return result, explorerConflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil) + } + } + if nativeReceiptPreview { + if !receiptHasOutput(receipt.Bundle, body.OutputId) || validateReceiptOutputContract(receipt, body.OutputId) != nil { + return result, authoringSemanticRoute("preview", "$.outputId", "UNKNOWN_AUTHORING_OUTPUT", "outputId is not in the receipt", nil) + } + } else if !receiptHasOutput(receipt.Bundle, body.OutputId) { + return result, 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{body.OutputId}} + applyAuthorizedScope(&bindings, authorized, false) + columns := make([]explorer.EmittedColumn, 0) + for _, column := range receipt.EmittedColumns { + if column.OutputID == body.OutputId { + columns = append(columns, column) + } + } + var encoded []byte + if config.PreviewReceipt != nil { + encoder, encoderErr := newPreviewResponseEncoder(receipt, body.OutputId, columns, maxExplorerPreviewResponseBytes) + if encoderErr != nil { + return result, previewRouteError(encoderErr) + } + previewSummary, err = config.PreviewReceipt(previewCtx, receipt, bindings, encoder.Visit) + responseRows = previewSummary.RowCount + if err == nil { + encoded, err = encoder.Finish() + } + } else { + var rows map[string][]map[string]any + rows, err = config.Preview(previewCtx, receipt.Bundle, bindings) + if err == nil { + responseRows = len(rows[body.OutputId]) + encoded, err = encodeExplorerPreviewResponse(receipt, body.OutputId, columns, rows[body.OutputId], maxExplorerPreviewResponseBytes) + } + } + if err != nil { + return result, previewRouteError(err) + } + responseBytes = len(encoded) + if err := json.Unmarshal(encoded, &result); err != nil { + return result, err + } + outcome = "success" + return result, nil +} + +func (h *explorerHTTPHandlers) publishAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.PublishExplorerJSONRequestBody) (loomapi.PublishResponse, error) { + var result loomapi.PublishResponse + if err := h.authoringWriteDirect(ctx, project); err != nil { + return result, err + } + if body == nil || strings.TrimSpace(body.ReceiptId) == "" { + return result, malformedRouteError("publish", errors.New("receiptId is required")) + } + config := h.lifecycleConfig + if (config.Materialize == nil && config.MaterializeReceipt == nil) || config.ActivateRelease == nil || config.ValidateReleaseGeneration == nil || (config.CapabilityToken == nil && config.AuthorizedCapabilityExecution == nil) { + return result, explorerUnavailable("publish", "PUBLICATION_UNAVAILABLE", "Explorer publication is not configured") + } + receipt, err := lookupV2Receipt(ctx, h.explorers, config, project, explorerID, body.ReceiptId) + if err != nil { + return result, receiptRouteError("publish", err) + } + if err := validateV2ReceiptRouteValues(receipt, config, project, explorerID); err != nil { + return result, err + } + var snapshot capability.Snapshot + var authorized AuthorizedCapability + if config.AuthorizedCapabilityExecution != nil { + authorized, err = config.AuthorizedCapabilityExecution(ctx, receipt.Project, receipt.SnapshotToken) + snapshot = authorized.Snapshot + } else { + snapshot, err = config.CapabilityToken(ctx, receipt.Project, receipt.SnapshotToken) + } + if err != nil || strings.TrimSpace(snapshot.Identity.Generation) != strings.TrimSpace(receipt.SourceGeneration) { + return result, explorerConflict("publish", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil) + } + if config.CompileReceipt != nil { + if err := validateReceiptCapability(receipt, snapshot); err != nil { + return result, explorerConflict("publish", "RECEIPT_STALE", err.Error(), nil) + } + } + if len(receipt.EmittedColumns) == 0 || len(receipt.CompiledConfig) == 0 { + return result, authoringSemanticRoute("publish", "$.receiptId", "NO_SELECTED_COLUMNS", "select at least one output column before publishing", nil) + } + if config.CompileReceipt != nil { + workspace, decodeErr := authoringv2.DecodeWorkspace(receipt.NormalizedBundle) + if decodeErr != nil { + return result, explorerConflict("publish", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt contains invalid authoring intent", nil) + } + if err := workspace.ValidateForPublication(); err != nil { + return result, authoringSemanticRoute("publish", "$.receiptId", "NO_SELECTED_COLUMNS", "select at least one visible output column for every visible table before publishing", nil) + } + } + if err := config.ValidateReleaseGeneration(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration); err != nil { + return result, 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 config.MaterializeReceipt != nil { + execution, err = config.MaterializeReceipt(ctx, receipt, bindings) + } else { + execution, err = config.Materialize(ctx, receipt.Bundle, bindings) + } + if err != nil { + var mismatch *receiptContractMismatch + if errors.As(err, &mismatch) { + return result, &explorer.AuthoringError{Status: http.StatusConflict, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "publish", Code: "RECEIPT_RECOMPILE_REQUIRED", Message: "receipt deterministic lowering no longer matches the stored artifact", Details: receiptMismatchDetails(receipt.ID, err)}, Cause: err} + } + return result, &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 result, &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 := config.ActivateRelease(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration, selectorsForBundle(receipt.Bundle)); err != nil { + return result, &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 := h.explorers.PublishAuthoring(ctx, *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: subjectFromContext(ctx), CreatedAt: now, ReadyAt: &now}) + if err != nil { + return result, err + } + outputs := make([]loomapi.PublicationOutput, 0, len(revision.Materializations)) + for _, materialization := range 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: receipt.ID, RevisionId: revision.ID, State: string(revision.Status), Outputs: outputs, Diagnostics: []loomapi.Diagnostic{}}, nil +} + +// authoringErrorForOpenAPI preserves the generated ErrorResponse contract and +// keeps internal causes out of the response while retaining request IDs and +// structured diagnostics. +func authoringErrorForOpenAPI(ctx context.Context, operation string, err error) (int, loomapi.ErrorResponse) { + requestID := requestIDFromContext(ctx) + status := 500 + code, message, stage, path := "INTERNAL_ERROR", "internal server error", "internal", "" + var details map[string]any + var cause error = err + var authoringErr *explorer.AuthoringError + if errors.As(err, &authoringErr) { + status = authoringErr.Status + code, message, stage, path = authoringErr.Diagnostic.Code, authoringErr.Diagnostic.Message, authoringErr.Diagnostic.Stage, authoringErr.Diagnostic.JSONPath + details = authoringErr.Diagnostic.Details + if authoringErr.Cause != nil { + cause = authoringErr.Cause + } + } else if errors.Is(err, explorer.ErrNotFound) { + status, code, message = 404, "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)) + } + severity := "error" + diagnostic := loomapi.Diagnostic{Severity: severity, 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} +} diff --git a/internal/server/openapi_explorer_direct.go b/internal/server/openapi_explorer_direct.go new file mode 100644 index 00000000..2f48f457 --- /dev/null +++ b/internal/server/openapi_explorer_direct.go @@ -0,0 +1,309 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + loomapi "github.com/calypr/loom/generated/loomapi" + graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" + httpapi "github.com/calypr/loom/internal/api/http" + "github.com/calypr/loom/internal/authscope" + "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/projectid" +) + +// explorerLegacyOperationError carries the response contract used by the +// legacy Explorer endpoints. The old Fiber handlers encoded either a +// structured {code,message} value or a plain error string depending on the +// route; keeping that distinction here lets generated OpenAPI responses use +// the same wire shape without executing a second handler. +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 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.explorers == 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) + } + values, err := r.explorers.List(ctx, project) + if err != nil { + return result, legacyOperationFailure(http.StatusInternalServerError, "EXPLORER_READ_FAILED", err.Error(), err) + } + 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, + }) + } + raw, err := json.Marshal(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 string, body *loomapi.CreateExplorerJSONRequestBody) (loomapi.CreateExplorer201JSONResponse, error) { + var result loomapi.CreateExplorer201JSONResponse + if r == nil || r.explorers == nil || r.authorizeRead == 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, ""); err != nil { + return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) + } + var request struct { + Name string `json:"name"` + Title string `json:"title,omitempty"` + SourceExplorerID string `json:"sourceExplorerId,omitempty"` + } + if body == nil { + return result, legacyOperationFailure(http.StatusBadRequest, "MALFORMED_REQUEST", "name is required", nil) + } + raw, err := json.Marshal(*body) + if err != nil || decodeStrict(raw, &request) != nil || strings.TrimSpace(request.Name) == "" { + return result, legacyOperationFailure(http.StatusBadRequest, "MALFORMED_REQUEST", "name is required", err) + } + id := explorer.StableExplorerID(request.Name) + if id == "default" { + return result, legacyOperationFailure(http.StatusConflict, "EXPLORER_EXISTS", "the repository default already exists", nil) + } + title := strings.TrimSpace(request.Title) + if title == "" { + title = strings.TrimSpace(request.Name) + } + value, err := r.explorers.CreateInteractiveFrom(ctx, project, id, title, strings.TrimSpace(request.SourceExplorerID), subjectFromContext(ctx)) + if errors.Is(err, explorer.ErrDraftConflict) { + return result, legacyOperationFailure(http.StatusConflict, "EXPLORER_EXISTS", "an Explorer with this name already exists", err) + } + if err != nil { + return result, legacyOperationFailure(http.StatusUnprocessableEntity, "INVALID_EXPLORER", err.Error(), err) + } + summary := explorer.ExplorerSummaryV1{Project: project, ExplorerID: value.ExplorerID, Title: value.Title, Management: value.ManagementMode, UpdatedAt: value.UpdatedAt} + raw, err = json.Marshal(summary) + 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.explorers == nil || r.authorizeRead == nil { + return result, legacyOperationFailure(http.StatusInternalServerError, "INTERNAL_ERROR", "Explorer lifecycle is not configured", nil) + } + project := projectid.Canonical(rawProject) + id := strings.TrimSpace(rawID) + principal, _ := authscope.PrincipalFromContext(ctx) + if err := r.authorizeRead(ctx, principal, project); err != nil { + return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) + } + state, err := r.explorers.LoadExplorerState(ctx, project, id) + if errors.Is(err, explorer.ErrNotFound) { + return result, legacyOperationFailure(http.StatusNotFound, "EXPLORER_NOT_FOUND", "Explorer not found", err) + } + if err != nil { + return result, legacyOperationFailure(http.StatusInternalServerError, "EXPLORER_READ_FAILED", err.Error(), err) + } + raw, err := json.Marshal(state) + if err != nil { + return result, err + } + if err := json.Unmarshal(raw, &result); err != nil { + return result, err + } + return result, nil +} + +func subjectFromContext(ctx context.Context) string { + principal, _ := authscope.PrincipalFromContext(ctx) + if principal == nil { + return "" + } + return principal.Subject +} + +func (r *explorerHTTPHandlers) publishRepositoryExplorerConfig(ctx context.Context, request loomapi.PublishRepositoryExplorerConfigRequestObject) (loomapi.PublishRepositoryExplorerConfig200JSONResponse, error) { + var result loomapi.PublishRepositoryExplorerConfig200JSONResponse + if r == nil || r.explorers == 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) + } + config := r.lifecycleConfig + if config.Capability == nil || config.AuthorizedCapabilityCompile == nil || config.CompileReceipt == nil { + return result, legacyStringFailure(http.StatusServiceUnavailable, "repository V2 compilation is not configured", nil) + } + snapshot, err := config.Capability(ctx, 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 result, legacyStringFailure(http.StatusConflict, fmt.Sprintf("resolve repository V2 capability: %v", err), err) + } + authorized, err := config.AuthorizedCapabilityCompile(ctx, project, snapshot.Token) + if err != nil { + return result, legacyStringFailure(http.StatusForbidden, fmt.Sprintf("authorize repository V2 compilation: %v", err), err) + } + receipt, err := config.CompileReceipt(ctx, ExplorerV2ReceiptCompileRequest{Project: project, ExplorerID: "default", Workspace: workspace, SnapshotToken: snapshot.Token, RequestID: requestIDFromContext(ctx), Authorized: authorized}) + if err != nil || receipt == nil { + if err == nil { + err = fmt.Errorf("compiler returned no receipt") + } + return result, legacyStringFailure(http.StatusUnprocessableEntity, fmt.Sprintf("compile repository V2 workspace: %v", err), err) + } + if receipt.SourceGeneration != generation { + return result, legacyStringFailure(http.StatusConflict, "compiled receipt generation does not match deployment generation", nil) + } + if config.AuthorizedCapabilityExecution != nil { + authorized, err = config.AuthorizedCapabilityExecution(ctx, project, receipt.SnapshotToken) + if err != nil { + return result, legacyStringFailure(http.StatusForbidden, fmt.Sprintf("authorize repository V2 materialization: %v", err), 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(ctx, receipt, bindings) + } else if r.materialize != nil { + execution, err = r.materialize(ctx, receipt.Bundle, bindings) + } else { + err = fmt.Errorf("repository V2 materialization is not configured") + } + if err != nil { + return result, legacyStringFailure(http.StatusUnprocessableEntity, fmt.Sprintf("materialize repository V2 workspace: %v", err), err) + } + if err := verifyQueryableOutputs(receipt.Bundle, execution); err != nil { + return result, legacyStringFailure(http.StatusUnprocessableEntity, err.Error(), err) + } + materializations := explorerMaterializations(receipt.Bundle, execution) + datasetMetadata := datasetMetadataFromExecution(receipt.Bundle, generation, receipt.ResolvedSchemaDigest, execution) + publication := explorer.PublicationMetadata{State: string(explorer.RevisionReady), Generation: generation, ExecutionID: execution.ID, UpdatedAt: nowUTC()} + owner, revision, err := r.explorers.UpsertRepositoryV2(ctx, *receipt, commit, subjectFromContext(ctx), materializations, datasetMetadata, publication) + if err != nil { + return result, legacyStringFailure(http.StatusInternalServerError, fmt.Sprintf("persist Explorer lifecycle V2: %v", err), err) + } + if owner.ManagementMode != explorer.ManagementRepository { + return result, legacyStringFailure(http.StatusConflict, "default Explorer has invalid management mode", nil) + } + if config.ActivateRelease == nil { + return result, legacyStringFailure(http.StatusServiceUnavailable, "release activation is not configured", nil) + } + if err := config.ActivateRelease(ctx, projectid.Legacy(project), generation, selectorsForBundle(receipt.Bundle)); err != nil { + _, _ = r.explorers.FailRevision(ctx, revision.ID, []explorer.Diagnostic{{Severity: "ERROR", Code: "RELEASE_ACTIVATION_FAILED", Message: err.Error(), Retryable: true}}) + return result, legacyStringFailure(http.StatusConflict, fmt.Sprintf("activate published ExplorerConfigV2: %v", err), err) + } + if err := r.explorers.ActivateRepositoryGeneration(ctx, project, generation, revision.ID); err != nil { + return result, legacyStringFailure(http.StatusConflict, fmt.Sprintf("activate ExplorerConfigV2: %v", err), err) + } + publication.State = string(explorer.RevisionActive) + publication.RevisionID = revision.ID + publication.UpdatedAt = nowUTC() + if _, err := r.explorers.SaveRepositoryConfig(ctx, 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: datasetMetadata, Publication: publication, + }); err != nil { + return result, legacyStringFailure(http.StatusInternalServerError, fmt.Sprintf("persist ExplorerConfigV2: %v", err), err) + } + raw := loomapi.RawJSON{"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} + result = loomapi.PublishRepositoryExplorerConfig200JSONResponse(raw) + return result, nil +} + +func requestIDFromContext(ctx context.Context) string { + return httpapi.RequestIDFromContext(ctx) +} + +func nowUTC() (now time.Time) { + return time.Now().UTC() +} diff --git a/internal/server/openapi_explorer_routes.go b/internal/server/openapi_explorer_routes.go index 4adc0429..846640e1 100644 --- a/internal/server/openapi_explorer_routes.go +++ b/internal/server/openapi_explorer_routes.go @@ -2,368 +2,330 @@ 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), 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) 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 +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) } - 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.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 + 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("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 +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) } - 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.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 + 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("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 +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) } - value, _, e := authoringError(body, status) - if e != nil { - return nil, e + value, err := r.explorer.applyAuthoringCommandsDirect(ctx, string(request.Project), string(request.ExplorerId), request.Body) + if err == nil { + return loomapi.ApplyExplorerBuilderCommands200JSONResponse(value), nil } + status, failure := authoringErrorForOpenAPI(ctx, "applyExplorerBuilderCommands", err) 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 + 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("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 +func (r *HTTPRoutes) CompileExplorerBuilder(ctx context.Context, request loomapi.CompileExplorerBuilderRequestObject) (loomapi.CompileExplorerBuilderResponseObject, error) { + project := string(request.Project) + explorerID := string(request.ExplorerId) + if r == nil || r.explorer == nil { + _, failure := authoringErrorForOpenAPI(ctx, "compileExplorerBuilder", explorerUnavailable("compile", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) + return loomapi.CompileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil } - if status == http.StatusOK { + value, err := r.explorer.compileAuthoringDirect(ctx, project, explorerID, request.Body) + if err == nil { return loomapi.CompileExplorerBuilder200JSONResponse(value), nil } + status, failure := authoringErrorForOpenAPI(ctx, "compileExplorerBuilder", err) switch status { - case 400: + case http.StatusBadRequest: return loomapi.CompileExplorerBuilder400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil - case 403: + case http.StatusForbidden: return loomapi.CompileExplorerBuilder403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil - case 409: + case http.StatusConflict: return loomapi.CompileExplorerBuilder409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil - case 422: + case http.StatusUnprocessableEntity: return loomapi.CompileExplorerBuilder422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil - case 500: + case http.StatusInternalServerError: return loomapi.CompileExplorerBuilder500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil - case 503: + case http.StatusServiceUnavailable: return loomapi.CompileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil default: return nil, unexpectedResponseStatus("compileExplorerBuilder", 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, 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, 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_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_route_inventory_test.go b/internal/server/openapi_route_inventory_test.go index 5d5c8abf..9dea473f 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{} diff --git a/internal/server/openapi_routes.go b/internal/server/openapi_routes.go index d654ab10..950aff1c 100644 --- a/internal/server/openapi_routes.go +++ b/internal/server/openapi_routes.go @@ -1,20 +1,14 @@ 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" ) -type strictFiberContextKey struct{} - type HTTPRoutes struct { server *httpapi.HTTPServer load *loadapi.Handler @@ -25,57 +19,32 @@ type HTTPRoutes struct { 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/routes.go b/internal/server/routes.go index 492d31a1..93f6d20a 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -35,7 +35,7 @@ func registerRoutes(server *api.HTTPServer, resourceService *loadapi.Service, sn 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 } From d1ca83827ed7cd57faf4ff475b9aa50226e65b39 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 14:33:49 -0700 Subject: [PATCH 04/17] refactor(explorer): extract lifecycle application service --- docs/EXPLORER_COMPILATION_ARCHITECTURE.md | 8 +- .../arango/capability_snapshot_store.go | 19 +- internal/explorer/capability/builder.go | 2 - internal/explorer/capability/domain.go | 19 - .../repository.go | 28 +- internal/explorer/lifecycle/errors.go | 94 ++++ internal/explorer/lifecycle/policy.go | 250 +++++++++ internal/explorer/lifecycle/service.go | 467 +++++++++++++++++ internal/explorer/lifecycle/service_test.go | 244 +++++++++ internal/explorer/lifecycle/types.go | 213 ++++++++ internal/server/explorer_authoring_http.go | 26 +- .../server/explorer_authoring_v2_routes.go | 493 ++---------------- internal/server/explorer_builder_lifecycle.go | 37 +- internal/server/explorer_capability.go | 20 +- internal/server/explorer_config_route.go | 160 ++---- internal/server/explorer_lifecycle_adapter.go | 97 ++++ internal/server/explorer_lifecycle_helpers.go | 26 - internal/server/explorer_lifecycle_types.go | 14 +- internal/server/explorer_lifecycle_wire.go | 18 + internal/server/explorer_preview_security.go | 59 --- .../server/explorer_preview_security_test.go | 70 --- internal/server/explorer_receipt_contract.go | 17 - internal/server/http_test_helpers_test.go | 5 +- .../openapi_explorer_authoring_direct.go | 408 +++------------ internal/server/openapi_explorer_direct.go | 202 ++----- 25 files changed, 1657 insertions(+), 1339 deletions(-) rename internal/explorer/{capabilitystore => capability}/repository.go (52%) create mode 100644 internal/explorer/lifecycle/errors.go create mode 100644 internal/explorer/lifecycle/policy.go create mode 100644 internal/explorer/lifecycle/service.go create mode 100644 internal/explorer/lifecycle/service_test.go create mode 100644 internal/explorer/lifecycle/types.go create mode 100644 internal/server/explorer_lifecycle_adapter.go create mode 100644 internal/server/explorer_lifecycle_wire.go diff --git a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md index 186864e4..730eaf40 100644 --- a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md +++ b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md @@ -248,8 +248,8 @@ execution contract. | 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` | +| Explorer list/create/authoring/receipt/preview/publication workflows | `internal/explorer/lifecycle` | | 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` | +| 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/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/capability/builder.go b/internal/explorer/capability/builder.go index 256ca622..4cdc4c37 100644 --- a/internal/explorer/capability/builder.go +++ b/internal/explorer/capability/builder.go @@ -105,8 +105,6 @@ type Builder struct { Compiler CompilerProbe } -type CapabilityBuilder = Builder - const DefaultSuggestionLimit = 32 func NewBuilder(identity SnapshotIdentity, resources ResourceInventory, relationships RelationshipEvidence, fields FieldEvidence, compiler CompilerProbe) Builder { 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/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..a74395ae --- /dev/null +++ b/internal/explorer/lifecycle/policy.go @@ -0,0 +1,250 @@ +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" +) + +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 || receipt.Project != 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/service.go b/internal/explorer/lifecycle/service.go new file mode 100644 index 00000000..d9d170b7 --- /dev/null +++ b/internal/explorer/lifecycle/service.go @@ -0,0 +1,467 @@ +package lifecycle + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "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" +) + +// Service is the Explorer application layer. It coordinates persistence and +// deployment adapters while leaving all wire representation to transports. +type Service struct { + store Store + config Config + now func() time.Time +} + +func New(store Store, 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 +} + +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 { + continue + } + if query != "" && !strings.Contains(strings.ToLower(candidate.Label), query) && !strings.Contains(strings.ToLower(candidate.ID), query) { + continue + } + 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 = &workspace + state.LifecycleState = authoringv2.LifecycleReady + } else { + state.Workspace = activeWorkspace + if activeWorkspace != nil { + state.LifecycleState = authoringv2.LifecycleReady + } + } + if err := state.Validate(); err != nil { + return authoringv2.BuilderState{}, unavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error(), err) + } + return state, nil +} + +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) 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 = engine.DefaultPreviewLimit + } + if request.Limit > engine.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.Preview == nil && 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 s.config.PreviewReceipt != nil { + 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) || (s.config.PreviewReceipt != nil && 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 s.config.PreviewReceipt != nil { + 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 + } + result.Rows, err = s.config.Preview(ctx, receipt.Bundle, bindings) + if err != nil { + return PreviewResult{}, err + } + return result, nil +} + +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.Materialize == nil && s.config.MaterializeReceipt == nil { + return PublishResult{}, unavailable("publish", "PUBLICATION_UNAVAILABLE", "Explorer publication is not configured", nil) + } + if s.config.ActivateRelease == 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) + var execution Execution + if s.config.MaterializeReceipt != nil { + execution, err = s.config.MaterializeReceipt(ctx, receipt, bindings) + } else { + execution, err = s.config.Materialize(ctx, receipt.Bundle, bindings) + } + if err != nil { + 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) + } + if err := s.config.ActivateRelease(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration, selectorsForBundle(receipt.Bundle)); err != nil { + return PublishResult{}, unavailable("activation", "MATERIALIZATION_ACTIVATION_FAILED", "dataset release activation failed; the prior Explorer revision was retained", err) + } + now := s.now() + revisionID := "authoring_" + strings.TrimPrefix(receipt.ID, "receipt_") + revision, err := s.store.PublishAuthoring(ctx, *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: 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}) + if err != nil { + return PublishResult{}, err + } + return PublishResult{Receipt: receipt, Revision: revision, Execution: execution}, nil +} + +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 { + execution, err = s.config.MaterializeReceipt(ctx, receipt, bindings) + } else if s.config.Materialize != nil { + execution, err = s.config.Materialize(ctx, receipt.Bundle, bindings) + } else { + err = errors.New("repository V2 materialization is not configured") + } + if err != nil { + 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() + if _, err := s.store.SaveRepositoryConfig(ctx, explorer.RepositoryConfig{Project: request.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: request.Generation, SourceCommit: request.Commit, ExecutionID: execution.ID, Materializations: materialized, Dataset: datasetMetadata, Publication: publication}); err != nil { + return RepositoryPublishResult{}, internal("repository_publish", "PERSISTENCE_FAILED", fmt.Sprintf("persist ExplorerConfigV2: %v", err), err) + } + return RepositoryPublishResult{Receipt: receipt, Owner: owner, Revision: revision, Execution: execution, Materializations: materialized, Dataset: datasetMetadata, Publication: publication}, nil +} + +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 +} + +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/service_test.go b/internal/explorer/lifecycle/service_test.go new file mode 100644 index 00000000..8d8dc4f6 --- /dev/null +++ b/internal/explorer/lifecycle/service_test.go @@ -0,0 +1,244 @@ +package lifecycle + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "sort" + "strings" + "testing" + "time" + + "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/explorer/capability" +) + +type fakeStore struct { + Store + listValues []explorer.Explorer + state explorer.ExplorerStateV1 + created *explorer.Explorer + applyErr error + receipt *explorer.CompilationReceipt + publishErr error + published bool + order *[]string +} + +func (f *fakeStore) List(context.Context, string) ([]explorer.Explorer, error) { + return append([]explorer.Explorer(nil), f.listValues...), nil +} + +func (f *fakeStore) LoadExplorerState(context.Context, string, string) (explorer.ExplorerStateV1, error) { + return f.state, nil +} + +func (f *fakeStore) CreateInteractiveFrom(_ context.Context, project, id, title, source, actor string) (*explorer.Explorer, error) { + value := &explorer.Explorer{Project: project, ExplorerID: id, Title: title, ManagementMode: explorer.ManagementInteractive, UpdatedBy: actor} + 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 + } + return &explorer.Explorer{Project: "project-a", ExplorerID: "patients", Title: "Patients"}, nil +} + +func (f *fakeStore) ApplyWorkspaceCommands(context.Context, string, string, authoringv2.CatalogSnapshot, authoringv2.ApplyCommandsRequest, string) (*authoringv2.ApplyCommandsResponse, error) { + if f.applyErr != nil { + return nil, f.applyErr + } + return &authoringv2.ApplyCommandsResponse{}, nil +} + +func (f *fakeStore) CompilationReceiptForExplorer(context.Context, string, string, string) (*explorer.CompilationReceipt, error) { + if f.receipt == nil { + return nil, explorer.ErrNotFound + } + return f.receipt, nil +} + +func (f *fakeStore) ActiveRevision(context.Context, string, string) (*explorer.Revision, error) { + return nil, explorer.ErrNotFound +} + +func (f *fakeStore) PublishAuthoring(_ context.Context, _ explorer.CompilationReceipt, revision explorer.Revision) (*explorer.Revision, error) { + if f.order != nil { + *f.order = append(*f.order, "persist") + } + if f.publishErr != nil { + return nil, f.publishErr + } + f.published = true + return &revision, nil +} + +func (f *fakeStore) UpsertRepositoryV2(context.Context, explorer.CompilationReceipt, string, string, []explorer.Materialization, explorer.DatasetMetadata, explorer.PublicationMetadata) (*explorer.Explorer, *explorer.Revision, error) { + return nil, nil, errors.New("not used") +} + +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 (f *fakeStore) SaveRepositoryConfig(context.Context, explorer.RepositoryConfig) (*explorer.RepositoryConfig, error) { + if f.order != nil { + *f.order = append(*f.order, "save-config") + } + return &explorer.RepositoryConfig{}, nil +} + +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{SnapshotToken: snapshot.Token, Complete: true, Nodes: []authoringv2.CatalogNode{{ID: "node", ResourceType: "Patient", RowRootEligible: true}}} + }, + }} +} + +func TestListGetCreateUseApplicationStore(t *testing.T) { + store := &fakeStore{listValues: []explorer.Explorer{{ExplorerID: "patients", Title: "Patients", ManagementMode: explorer.ManagementInteractive}}} + service, err := New(store, Config{}) + if err != nil { + t.Fatal(err) + } + 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, _ := New(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 := &explorer.CompilationReceipt{ID: "receipt-a", Project: "project-a", ExplorerID: "patients", SnapshotToken: "token", SourceGeneration: "generation-a", RecipeDigest: "recipe", Bundle: recipe.Bundle{Outputs: []recipe.Output{{Name: "patients"}}}} + 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.Preview = func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (map[string][]map[string]any, error) { + return nil, nil + } + service, _ := New(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 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 TestPublishPersistsOnlyAfterActivationAndRetainsRevisionOnFailure(t *testing.T) { + snapshot := readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) + receipt := &explorer.CompilationReceipt{ID: "receipt-a", Project: "project-a", ExplorerID: "patients", SnapshotToken: "token", SourceGeneration: "generation-a", RecipeDigest: "recipe", CompiledConfig: []byte(`{}`), EmittedColumns: []explorer.EmittedColumn{{OutputID: "patients", PublicColumn: "patient_id"}}, Bundle: recipe.Bundle{Outputs: []recipe.Output{{Name: "patients"}}}} + 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.ActivateRelease = func(context.Context, string, string, []dataset.DataframeSelector) error { + order = append(order, "activate-release") + return nil + } + config.Materialize = func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (Execution, error) { + order = append(order, "materialize") + return Execution{ID: "execution-a", Outputs: []ExecutionOutput{{Name: "patients", State: "READY"}}}, nil + } + service, _ := New(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 activate-release persist]"; got != want { + t.Fatalf("ordering=%s, want %s", got, want) + } + + 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) + } +} + +func TestServiceUsesConfiguredClock(t *testing.T) { + clock := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC) + service, err := New(&fakeStore{}, Config{Now: func() time.Time { return clock }}) + if err != nil || service.now() != clock { + t.Fatalf("clock=%v, err=%v", service.now(), err) + } +} diff --git a/internal/explorer/lifecycle/types.go b/internal/explorer/lifecycle/types.go new file mode 100644 index 00000000..f754b10c --- /dev/null +++ b/internal/explorer/lifecycle/types.go @@ -0,0 +1,213 @@ +// 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" + "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/dataset" + "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/authoringv2" + "github.com/calypr/loom/internal/explorer/capability" +) + +// Store is the persistence surface required by Explorer workflows. The +// concrete Explorer service implements this interface; tests can provide a +// small fake without constructing a database-backed service. +type Store interface { + List(context.Context, string) ([]explorer.Explorer, error) + Get(context.Context, string, string) (*explorer.Explorer, error) + LoadExplorerState(context.Context, string, string) (explorer.ExplorerStateV1, error) + CreateInteractiveFrom(context.Context, string, string, string, string, string) (*explorer.Explorer, error) + ApplyWorkspaceCommands(context.Context, string, string, authoringv2.CatalogSnapshot, authoringv2.ApplyCommandsRequest, string) (*authoringv2.ApplyCommandsResponse, error) + ActiveRevision(context.Context, string, string) (*explorer.Revision, error) + CompilationReceiptForExplorer(context.Context, string, string, string) (*explorer.CompilationReceipt, error) + PublishAuthoring(context.Context, explorer.CompilationReceipt, explorer.Revision) (*explorer.Revision, error) + UpsertRepositoryV2(context.Context, explorer.CompilationReceipt, string, string, []explorer.Materialization, explorer.DatasetMetadata, explorer.PublicationMetadata) (*explorer.Explorer, *explorer.Revision, error) + FailRevision(context.Context, string, []explorer.Diagnostic) (*explorer.Revision, error) + ActivateRepositoryGeneration(context.Context, string, string, string) error + SaveRepositoryConfig(context.Context, explorer.RepositoryConfig) (*explorer.RepositoryConfig, error) +} + +// 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 Previewer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (map[string][]map[string]any, error) +type ReceiptPreviewer func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (engine.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 Materializer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (Execution, error) +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 + +// 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 + Preview Previewer + PreviewReceipt ReceiptPreviewer + Materialize Materializer + MaterializeReceipt ReceiptMaterializer + ReceiptLookup ReceiptReader + + ValidateReleaseGeneration GenerationValidator + ActivateRelease ReleaseActivator + 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 engine.PreviewSummary + Rows map[string][]map[string]any +} + +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/server/explorer_authoring_http.go b/internal/server/explorer_authoring_http.go index 3bb6fca0..0c2fd383 100644 --- a/internal/server/explorer_authoring_http.go +++ b/internal/server/explorer_authoring_http.go @@ -10,8 +10,8 @@ import ( "strings" "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/gofiber/fiber/v3" ) @@ -56,10 +56,6 @@ 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}} } @@ -68,17 +64,19 @@ 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 applicationErr *lifecycle.Error + if errors.As(err, &applicationErr) { + status := lifecycleErrorStatus(applicationErr.Class) + details := applicationErr.Details + if details == nil { + details = map[string]any{} + } + logAuthoringRequestFailure(c, requestID, status, applicationErr.Stage, applicationErr.Code, applicationErr.Path, applicationErr.Message, applicationErr.Details, applicationErr.Cause) + diagnostic := fiber.Map{"severity": "error", "stage": applicationErr.Stage, "code": applicationErr.Code, "path": applicationErr.Path, "message": applicationErr.Message, "requestId": requestID} + return c.Status(status).JSON(fiber.Map{"code": applicationErr.Code, "message": applicationErr.Message, "diagnostics": []fiber.Map{diagnostic}, "requestId": requestID, "details": details}) + } var value *explorer.AuthoringError if errors.As(err, &value) { value.Diagnostic.RequestID = requestID diff --git a/internal/server/explorer_authoring_v2_routes.go b/internal/server/explorer_authoring_v2_routes.go index 10165c07..f128755a 100644 --- a/internal/server/explorer_authoring_v2_routes.go +++ b/internal/server/explorer_authoring_v2_routes.go @@ -2,22 +2,15 @@ package server import ( "context" - "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/calypr/loom/internal/explorer/lifecycle" "github.com/gofiber/fiber/v3" ) @@ -32,119 +25,48 @@ type explorerAuthoringHandlers struct { publish fiber.Handler } -// newExplorerAuthoringHandlers is the hard-cut traversal Builder contract. -// Generated OpenAPI code owns all method/path registration. +// newExplorerAuthoringHandlers is a transport adapter. All Builder, receipt, +// preview, and publication policy is implemented by lifecycle.Service. 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 - } - + application := newExplorerLifecycleApplication(explorers, capabilities) 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.Suggestions, explorerv2api.Preview, explorerv2api.Publish, explorerv2api.Commands, explorerv2api.Reconcile}, - PreviewLimits: []int{10, 25, 50, 100}, - Features: explorerv2api.AuthoringFeatures{EmissionFilters: true, EmissionCharts: true}, - }) + return c.JSON(explorerv2api.AuthoringCapability{ApiVersion: explorerv2api.LoomCalyprOrgexplorerAuthoringv2, Kind: explorerv2api.ExplorerAuthoringCapabilities, Operations: []explorerv2api.AuthoringCapabilityOperations{explorerv2api.Builder, 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") - } + if err := decodeAuthoringStrict(c.Body(), &request); err != nil { 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)) + query = *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) - } + value, err := application.Suggestions(c.Context(), lifecycle.SuggestionsRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), SnapshotToken: request.SnapshotToken, NodeID: request.NodeId, Query: query}) + if err != nil { + return authoringHTTPError(c, err) } - return c.JSON(fiber.Map{"apiVersion": authoringv2.APIVersion, "kind": "ExplorerBuilderCandidateSuggestions", "snapshotToken": request.SnapshotToken, "nodeId": request.NodeId, "candidates": candidates, "diagnostics": []any{}}) + return c.JSON(fiber.Map{"apiVersion": authoringv2.APIVersion, "kind": "ExplorerBuilderCandidateSuggestions", "snapshotToken": value.SnapshotToken, "nodeId": value.NodeID, "candidates": value.Candidates, "diagnostics": []any{}}) } - 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) + value, err := application.Builder(c.Context(), lifecycle.BuilderRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId"))}) 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) + return c.JSON(value) } - handlers.applyCommands = func(c fiber.Ctx) error { if err := authoringWrite(c, authorizer); err != nil { return err @@ -153,66 +75,13 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead 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}) + value, err := application.ApplyCommands(c.Context(), explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")), request, subjectFromFiber(c)) 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)) + return c.JSON(value) } - compileHandler := func(c fiber.Ctx) error { + handlers.compileBuilder = func(c fiber.Ctx) error { if err := authoringWrite(c, authorizer); err != nil { return err } @@ -220,338 +89,93 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead if err := decodeAuthoringStrict(c.Body(), &request); err != nil { return authoringHTTPError(c, malformedRouteError("intent", err)) } - return compileWorkspace(c, request.Workspace, request.SnapshotToken) + receipt, err := application.Compile(c.Context(), lifecycle.CompileRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), Workspace: request.Workspace, SnapshotToken: request.SnapshotToken, RequestID: requestIDFromFiber(c)}) + if err != nil { + return authoringHTTPError(c, err) + } + return c.JSON(v2ReceiptResponse(receipt, request.Workspace)) } - handlers.compileBuilder = 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") - } + var request explorerv2api.ReconcileRequest + if err := decodeAuthoringStrict(c.Body(), &request); err != nil { return authoringHTTPError(c, malformedRouteError("reconcile", err)) } - owner, err := explorers.Get(c.Context(), explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId"))) + receipt, err := application.Reconcile(c.Context(), lifecycle.ReconcileRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), SnapshotToken: request.SnapshotToken, DraftVersion: request.DraftVersion, DraftDigest: request.DraftDigest}) 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) + return c.JSON(v2ReceiptResponse(receipt, authoringv2.Workspace{})) } - 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 } + previewCtx, cancel := context.WithTimeout(c.Context(), explorerPreviewTimeout) + defer cancel() 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") - } + if err := decodeAuthoringStrict(c.Body(), &request); err != nil { 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) + var finish func() ([]byte, error) + value, err := application.Preview(previewCtx, lifecycle.PreviewRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), ReceiptID: request.ReceiptId, OutputID: request.OutputId, Limit: previewLimit(request.Limit), SinkFactory: func(receipt *explorer.CompilationReceipt, columns []explorer.EmittedColumn) (func(map[string]any) error, error) { + encoder, encoderErr := newPreviewResponseEncoder(receipt, request.OutputId, columns, maxExplorerPreviewResponseBytes) + if encoderErr != nil { + return nil, encoderErr + } + finish = encoder.Finish + return encoder.Visit, nil + }}) 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 value.Rows != nil { + encoded, err = encodeExplorerPreviewResponse(value.Receipt, request.OutputId, value.Columns, value.Rows[request.OutputId], maxExplorerPreviewResponseBytes) + } else if finish != nil { + encoded, err = finish() } 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") - } + if err := decodeAuthoringStrict(c.Body(), &request); err != nil { 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) + value, err := application.Publish(c.Context(), lifecycle.PublishRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), ReceiptID: request.ReceiptId, Actor: subjectFromFiber(c)}) 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 { - var mismatch *receiptContractMismatch - if errors.As(err, &mismatch) { - return authoringHTTPError(c, &explorer.AuthoringError{Status: http.StatusConflict, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "publish", Code: "RECEIPT_RECOMPILE_REQUIRED", Message: "receipt deterministic lowering no longer matches the stored artifact", Details: receiptMismatchDetails(receipt.ID, err)}, Cause: err}) - } - 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 := make([]explorerv2api.PublicationOutput, 0, len(value.Revision.Materializations)) + for _, materialization := range value.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 c.JSON(explorerv2api.PublishResponse{ApiVersion: explorerv2api.LoomCalyprOrgexplorerAuthoringv2, Kind: explorerv2api.ExplorerBuilderPublication, ReceiptId: value.Receipt.ID, RevisionId: value.Revision.ID, State: string(value.Revision.Status), Outputs: outputs, Diagnostics: []explorerv2api.Diagnostic{}}) } return handlers } -func validateV2ReceiptRoute(c fiber.Ctx, receipt *explorer.CompilationReceipt, capabilities ExplorerV2LifecycleConfig) error { - return validateV2ReceiptRouteValues(receipt, capabilities, explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId"))) -} - -func validateV2ReceiptRouteValues(receipt *explorer.CompilationReceipt, capabilities ExplorerV2LifecycleConfig, project, explorerID string) error { - if receipt == nil || receipt.Project != project || receipt.ExplorerID != 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"}} +func previewLimit(value *int) int { + if value == nil { + return engine.DefaultPreviewLimit } - 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) + if *value == 0 { + return -1 } - 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 + return *value } +// v2ReceiptResponse is transport conversion only; receipt construction and +// validation belong to lifecycle.Service. 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 { @@ -580,12 +204,5 @@ func v2ReceiptResponse(receipt *explorer.CompilationReceipt, workspace authoring } 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{}, - } + 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 index dd083b9d..bc8d274a 100644 --- a/internal/server/explorer_builder_lifecycle.go +++ b/internal/server/explorer_builder_lifecycle.go @@ -1,12 +1,12 @@ package server import ( - "errors" "net/http" "strings" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/explorer" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/gofiber/fiber/v3" ) @@ -23,20 +23,17 @@ func newExplorerLifecycleHandlers(authorizer authscope.Authorizer, authorizeRead if authorizer == nil || authorizeRead == nil || explorers == nil { return handlers } + application := newExplorerLifecycleApplication(explorers, capabilities) 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) + value, err := application.List(c.Context(), project) if err != nil { - return explorerV2Error(c, 500, "EXPLORER_READ_FAILED", err.Error()) + return authoringHTTPError(c, err) } - 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) + return c.JSON(value.Summaries) } handlers.get = func(c fiber.Ctx) error { @@ -44,12 +41,9 @@ func newExplorerLifecycleHandlers(authorizer authscope.Authorizer, authorizeRead 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") - } + state, err := application.Get(c.Context(), project, id) if err != nil { - return explorerV2Error(c, 500, "EXPLORER_READ_FAILED", err.Error()) + return authoringHTTPError(c, err) } return c.JSON(state) } @@ -67,22 +61,11 @@ func newExplorerLifecycleHandlers(authorizer authscope.Authorizer, authorizeRead 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") - } + value, err := application.Create(c.Context(), lifecycle.CreateRequest{Project: project, Name: request.Name, Title: request.Title, SourceExplorerID: request.SourceExplorerID, Actor: subjectFromFiber(c)}) if err != nil { - return explorerV2Error(c, 422, "INVALID_EXPLORER", err.Error()) + return authoringHTTPError(c, err) } - return c.Status(http.StatusCreated).JSON(explorer.ExplorerSummaryV1{Project: project, ExplorerID: value.ExplorerID, Title: value.Title, Management: value.ManagementMode, UpdatedAt: value.UpdatedAt}) + return c.Status(http.StatusCreated).JSON(value) } return handlers } diff --git a/internal/server/explorer_capability.go b/internal/server/explorer_capability.go index 09174383..7ef10f08 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,7 +44,7 @@ type explorerCapabilityResolver struct { evidence catalog.CapabilityEvidenceReader scopes *authscope.ScopeResolver manifests dataset.ActiveResolver - snapshots capabilitystore.Repository + snapshots capability.Repository builds singleflight.Group } @@ -55,17 +55,7 @@ type ExplorerCapabilityTokenReader func(context.Context, string, string) (capabi // 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()} -} +type AuthorizedCapability = lifecycle.AuthorizedCapability // ExplorerAuthorizedCapabilityCompilationReader accepts an opaque snapshot // token for a new compile. Implementations must require that token to belong @@ -90,7 +80,7 @@ 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,7 +140,7 @@ 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) diff --git a/internal/server/explorer_config_route.go b/internal/server/explorer_config_route.go index c159502d..9a3d844d 100644 --- a/internal/server/explorer_config_route.go +++ b/internal/server/explorer_config_route.go @@ -2,18 +2,15 @@ package server import ( "context" - "fmt" + "errors" "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/calypr/loom/internal/explorer/lifecycle" "github.com/gofiber/fiber/v3" ) @@ -23,22 +20,29 @@ type explorerHTTPHandlers struct { publishRepositoryConfig fiber.Handler lifecycle *explorerLifecycleHandlers authoring *explorerAuthoringHandlers - - // The generated OpenAPI methods invoke these dependencies directly. The - // Fiber handlers above remain only for the legacy route-focused tests until - // the Explorer lifecycle is moved into its application package. - authorizer authscope.Authorizer - authorizeRead explorerConfigReadAuthorizer - explorers *explorer.Service - materialize graphresolver.ExplorerBundleMaterializer - lifecycleConfig ExplorerV2LifecycleConfig + authorizer authscope.Authorizer + authorizeRead explorerConfigReadAuthorizer + explorers *explorer.Service + materialize graphresolver.ExplorerBundleMaterializer + lifecycleConfig ExplorerV2LifecycleConfig + 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 { +// newExplorerHTTPHandlers wires transport adapters to the single Explorer +// application service. HTTP response conversion and deployment adapters stay +// in server; lifecycle policy does not. +func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, materialize graphresolver.ExplorerBundleMaterializer, configs ...ExplorerV2LifecycleConfig) *explorerHTTPHandlers { handlers := &explorerHTTPHandlers{authorizer: authorizer, authorizeRead: authorizeRead, explorers: explorers, materialize: materialize} - if authorizer == nil || authorizeRead == nil || explorers == nil { + config := ExplorerV2LifecycleConfig{Materialize: materialize} + if len(configs) > 0 { + config = configs[0] + if config.Materialize == nil { + config.Materialize = materialize + } + } + handlers.lifecycleConfig = config + handlers.application = newExplorerLifecycleApplication(explorers, config) + if authorizer == nil || authorizeRead == nil || explorers == nil || handlers.application == nil { return handlers } handlers.publishRepositoryConfig = func(c fiber.Ctx) error { @@ -46,11 +50,8 @@ func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead expl 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 { + path := authscope.NormalizeAuthResourcePath(strings.TrimSpace(c.Query("auth_resource_path"))) + if err := authorizer.AuthorizeWrite(c.Context(), principalFromFiber(c), project, path); err != nil { return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "forbidden"}) } workspace, err := authoringv2.DecodeWorkspace(c.Body()) @@ -64,113 +65,18 @@ func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead expl 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) + value, err := handlers.application.PublishRepository(c.Context(), lifecycle.RepositoryPublishRequest{Project: project, Generation: generation, Workspace: workspace, Commit: commit, Actor: subjectFromFiber(c)}) 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") + status := http.StatusInternalServerError + var applicationErr *lifecycle.Error + if errors.As(err, &applicationErr) { + status = lifecycleErrorStatus(applicationErr.Class) } - 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}) + return c.Status(status).JSON(fiber.Map{"error": err.Error()}) } - 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}) + return c.Status(http.StatusOK).JSON(fiber.Map{"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}) } - if len(lifecycle) == 0 { - lifecycle = []ExplorerV2LifecycleConfig{{Materialize: materialize}} - } - handlers.lifecycleConfig = lifecycle[0] - handlers.lifecycle = newExplorerLifecycleHandlers(authorizer, authorizeRead, explorers, lifecycle[0]) - handlers.authoring = newExplorerAuthoringHandlers(authorizer, authorizeRead, explorers, lifecycle[0]) + handlers.lifecycle = newExplorerLifecycleHandlers(authorizer, authorizeRead, explorers, config) + handlers.authoring = newExplorerAuthoringHandlers(authorizer, authorizeRead, explorers, config) 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_lifecycle_adapter.go b/internal/server/explorer_lifecycle_adapter.go new file mode 100644 index 00000000..09e63510 --- /dev/null +++ b/internal/server/explorer_lifecycle_adapter.go @@ -0,0 +1,97 @@ +package server + +import ( + "context" + + 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" + "github.com/calypr/loom/internal/explorer/capability" + "github.com/calypr/loom/internal/explorer/lifecycle" +) + +// newExplorerLifecycleApplication is the only server-to-application wiring +// seam. GraphQL execution values are converted here so the lifecycle package +// never depends on resolver transport types. +func newExplorerLifecycleApplication(store *explorer.Service, config ExplorerV2LifecycleConfig) *lifecycle.Service { + if store == nil { + return nil + } + toExecution := func(value graphresolver.RecipeExecution) lifecycle.Execution { + result := lifecycle.Execution{ID: value.ID, Name: value.Name, RecipeDigest: value.RecipeDigest, ResolvedSchemaDigest: value.ResolvedSchemaDigest, SourceGeneration: value.SourceGeneration, State: value.State} + result.Outputs = make([]lifecycle.ExecutionOutput, 0, len(value.Outputs)) + for _, output := range value.Outputs { + result.Outputs = append(result.Outputs, lifecycle.ExecutionOutput{Name: output.Name, State: output.State, RowCount: output.RowCount, Columns: output.Columns}) + } + return result + } + appConfig := lifecycle.Config{ + Capability: lifecycle.CapabilityResolver{ + Current: config.Capability, + Token: config.CapabilityToken, + Catalog: func(snapshot capability.Snapshot, explorerID string) authoringv2.CatalogSnapshot { + return authoringV2Catalog(snapshot, explorerID) + }, + }, + } + if config.CompileReceipt != nil { + appConfig.CompileReceipt = func(ctx context.Context, request lifecycle.CompileReceiptRequest) (*explorer.CompilationReceipt, error) { + return config.CompileReceipt(ctx, request) + } + } + if config.Preview != nil { + appConfig.Preview = func(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (map[string][]map[string]any, error) { + return config.Preview(ctx, bundle, bindings) + } + } + if config.PreviewReceipt != nil { + appConfig.PreviewReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (engine.PreviewSummary, error) { + return config.PreviewReceipt(ctx, receipt, bindings, visit) + } + } + if config.ValidateReleaseGeneration != nil { + appConfig.ValidateReleaseGeneration = func(ctx context.Context, project, generation string) error { + return config.ValidateReleaseGeneration(ctx, project, generation) + } + } + if config.ActivateRelease != nil { + appConfig.ActivateRelease = func(ctx context.Context, project, generation string, selectors []dataset.DataframeSelector) error { + return config.ActivateRelease(ctx, project, generation, selectors) + } + } + if config.AuthorizedCapabilityCompile != nil { + appConfig.Capability.ForCompilation = config.AuthorizedCapabilityCompile + } + if config.AuthorizedCapabilityExecution != nil { + appConfig.Capability.ForExecution = config.AuthorizedCapabilityExecution + } + if config.Materialize != nil { + appConfig.Materialize = func(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { + value, err := config.Materialize(ctx, bundle, bindings) + return toExecution(value), err + } + } + if config.MaterializeReceipt != nil { + appConfig.MaterializeReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { + value, err := config.MaterializeReceipt(ctx, receipt, bindings) + return toExecution(value), err + } + } + if config.ReceiptLookup != nil { + appConfig.ReceiptLookup = func(ctx context.Context, project, explorerID, receiptID string) (*explorer.CompilationReceipt, error) { + return config.ReceiptLookup(ctx, project, explorerID, receiptID) + } + } + if config.Logger != nil { + // Logging remains a transport/deployment concern. Preview metrics are + // emitted by the existing server adapter around the sink callback. + } + application, err := lifecycle.New(store, appConfig) + if err != nil { + return nil + } + return application +} diff --git a/internal/server/explorer_lifecycle_helpers.go b/internal/server/explorer_lifecycle_helpers.go index 43f80201..1056d87a 100644 --- a/internal/server/explorer_lifecycle_helpers.go +++ b/internal/server/explorer_lifecycle_helpers.go @@ -5,38 +5,12 @@ import ( "encoding/hex" "encoding/json" "fmt" - "strings" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" "github.com/calypr/loom/internal/dataframe/compiler/ir" "github.com/calypr/loom/internal/dataframe/compiler/lower" - "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataframe/recipe/engine" - "github.com/calypr/loom/internal/dataset" ) -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 resolvedOutputArtifacts(resolved engine.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)) diff --git a/internal/server/explorer_lifecycle_types.go b/internal/server/explorer_lifecycle_types.go index 8be75728..22f5d754 100644 --- a/internal/server/explorer_lifecycle_types.go +++ b/internal/server/explorer_lifecycle_types.go @@ -9,22 +9,14 @@ import ( "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" + "github.com/calypr/loom/internal/explorer/lifecycle" ) 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 ExplorerV2ReceiptCompileRequest = lifecycle.CompileReceiptRequest +type ExplorerV2ReceiptCompiler = lifecycle.ReceiptCompiler 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) 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_security.go b/internal/server/explorer_preview_security.go index 9284aa9c..75287a56 100644 --- a/internal/server/explorer_preview_security.go +++ b/internal/server/explorer_preview_security.go @@ -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,31 +37,6 @@ 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 diff --git a/internal/server/explorer_preview_security_test.go b/internal/server/explorer_preview_security_test.go index 059e5c71..20b62786 100644 --- a/internal/server/explorer_preview_security_test.go +++ b/internal/server/explorer_preview_security_test.go @@ -14,47 +14,6 @@ import ( "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,35 +27,6 @@ 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"}) diff --git a/internal/server/explorer_receipt_contract.go b/internal/server/explorer_receipt_contract.go index 645efa88..51f002e5 100644 --- a/internal/server/explorer_receipt_contract.go +++ b/internal/server/explorer_receipt_contract.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -193,22 +192,6 @@ 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 diff --git a/internal/server/http_test_helpers_test.go b/internal/server/http_test_helpers_test.go index 8523ef25..75bc25e8 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) } diff --git a/internal/server/openapi_explorer_authoring_direct.go b/internal/server/openapi_explorer_authoring_direct.go index cbe04d03..a5427310 100644 --- a/internal/server/openapi_explorer_authoring_direct.go +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -7,25 +7,15 @@ import ( "fmt" "log/slog" "net/http" - "strings" - "time" loomapi "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/calypr/loom/internal/explorer/lifecycle" ) -// directAuthoringJSON converts the transport-neutral authoring values to the -// generated OpenAPI values. The generated types intentionally remain the -// boundary representation; the authoring package does not import transport -// code. func directAuthoringJSON[T any](value any) (T, error) { var result T raw, err := json.Marshal(value) @@ -44,7 +34,7 @@ func (h *explorerHTTPHandlers) authoringReadDirect(ctx context.Context, project } principal, _ := authscope.PrincipalFromContext(ctx) if err := h.authorizeRead(ctx, principal, project); err != nil { - return &explorer.AuthoringError{Status: 403, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err} + return &explorer.AuthoringError{Status: http.StatusForbidden, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err} } return nil } @@ -55,7 +45,7 @@ func (h *explorerHTTPHandlers) authoringWriteDirect(ctx context.Context, project } principal, _ := authscope.PrincipalFromContext(ctx) if err := h.authorizer.AuthorizeWrite(ctx, principal, project, ""); err != nil { - return &explorer.AuthoringError{Status: 403, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err} + return &explorer.AuthoringError{Status: http.StatusForbidden, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "authorization", Code: "FORBIDDEN", Message: "forbidden"}, Cause: err} } return nil } @@ -65,13 +55,7 @@ func (h *explorerHTTPHandlers) getAuthoringCapabilityDirect(ctx context.Context, 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 + 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) { @@ -82,42 +66,23 @@ func (h *explorerHTTPHandlers) searchAuthoringSuggestionsDirect(ctx context.Cont if body == nil { return result, malformedRouteError("suggestions", errors.New("request body is required")) } - request := *body - if strings.TrimSpace(request.SnapshotToken) == "" || strings.TrimSpace(request.NodeId) == "" { - return result, malformedRouteError("suggestions", errors.New("snapshotToken and nodeId are required")) - } - config := h.lifecycleConfig - if config.CapabilityToken == nil { - return result, explorerUnavailable("suggestions", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured") + query := "" + if body.Query != nil { + query = *body.Query } - snapshot, err := config.CapabilityToken(ctx, project, request.SnapshotToken) - if err != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { - return result, explorerConflict("suggestions", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale", nil) + 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 } - query := "" - if request.Query != nil { - query = strings.ToLower(strings.TrimSpace(*request.Query)) - } - internal := authoringV2Catalog(snapshot, explorerID) - candidates := make([]loomapi.CatalogCandidate, 0) - for _, candidate := range internal.Candidates { - if candidate.NodeID != request.NodeId || (query != "" && !strings.Contains(strings.ToLower(candidate.Label), query) && !strings.Contains(strings.ToLower(candidate.ID), query)) { - continue - } + 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: request.SnapshotToken, - NodeId: request.NodeId, - Candidates: candidates, - Diagnostics: []loomapi.Diagnostic{}, - }, nil + 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) { @@ -125,61 +90,11 @@ func (h *explorerHTTPHandlers) getAuthoringBuilderDirect(ctx context.Context, pr if err := h.authoringReadDirect(ctx, project); err != nil { return result, err } - if h == nil || h.explorers == nil { - return result, explorerUnavailable("builder", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") - } - config := h.lifecycleConfig - if config.Capability == nil { - return result, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", "Explorer capability compiler is not configured") - } - snapshot, err := config.Capability(ctx, project, explorerID, "") - if err != nil || !snapshot.Usable() { - if err == nil { - err = capability.ErrSnapshotUnavailable - } - return result, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error()) - } - wire, err := directAuthoringJSON[loomapi.Catalog](authoringV2Catalog(snapshot, explorerID)) - if err != nil { - return result, err - } - owner, err := h.explorers.Get(ctx, project, explorerID) + value, err := h.application.Builder(ctx, lifecycle.BuilderRequest{Project: project, ExplorerID: explorerID}) if err != nil { return result, err } - state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, LifecycleState: authoringv2.LifecycleNew, DraftVersion: owner.DraftVersion, DraftDigest: owner.DraftDigest, Catalog: authoringV2Catalog(snapshot, explorerID)} - var activeWorkspace *authoringv2.Workspace - if owner.ActiveRevisionID != "" { - active, activeErr := h.explorers.ActiveRevision(ctx, project, explorerID) - if activeErr != nil { - return result, 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 result, 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 result, 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 result, explorerUnavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error()) - } - // Keep the conversion explicit; the internal state has the same wire - // representation but must not leak its transport-only hidden fields. - _ = wire - return directAuthoringJSON[loomapi.BuilderState](state) + return directAuthoringJSON[loomapi.BuilderState](value) } func (h *explorerHTTPHandlers) applyAuthoringCommandsDirect(ctx context.Context, project, explorerID string, body *loomapi.ApplyExplorerBuilderCommandsJSONRequestBody) (loomapi.ApplyCommandsResponse, error) { @@ -194,67 +109,11 @@ func (h *explorerHTTPHandlers) applyAuthoringCommandsDirect(ctx context.Context, if err != nil { return result, malformedRouteError("commands", err) } - if err := request.Validate(); err != nil { - return result, malformedRouteError("commands", err) - } - config := h.lifecycleConfig - if config.CapabilityToken == nil { - return result, explorerUnavailable("commands", "CAPABILITY_UNAVAILABLE", "Explorer capability lookup is not configured") - } - snapshot, snapshotErr := config.CapabilityToken(ctx, project, request.SnapshotToken) - if snapshotErr != nil || snapshot.ValidateToken(request.SnapshotToken) != nil { - return result, explorerConflict("commands", "STALE_CATALOG_SNAPSHOT", "the catalog snapshot is stale or unavailable", nil) - } - if h.explorers == nil { - return result, explorerUnavailable("commands", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") - } - response, applyErr := h.explorers.ApplyWorkspaceCommands(ctx, project, explorerID, authoringV2Catalog(snapshot, explorerID), request, subjectFromContext(ctx)) - switch { - case errors.Is(applyErr, explorer.ErrDraftConflict): - return result, explorerConflict("commands", "DRAFT_CONFLICT", "the Explorer draft changed; reload before editing", nil) - case errors.Is(applyErr, explorer.ErrAuthoringCommandConflict): - return result, explorerConflict("commands", "COMMAND_ID_CONFLICT", "commandId was already used for different intent", nil) - case applyErr != nil: - return result, authoringSemanticRoute("commands", "$.commands", "INVALID_AUTHORING_COMMAND", applyErr.Error(), nil) - default: - return directAuthoringJSON[loomapi.ApplyCommandsResponse](response) - } -} - -func (h *explorerHTTPHandlers) compileAuthoringWorkspaceDirect(ctx context.Context, project, explorerID string, workspace authoringv2.Workspace, snapshotToken string) (loomapi.CompileResponse, error) { - var result loomapi.CompileResponse - config := h.lifecycleConfig - if (config.CapabilityToken == nil && config.AuthorizedCapabilityCompile == nil) || config.CompileReceipt == nil { - return result, explorerUnavailable("compile", "CAPABILITY_UNAVAILABLE", "Explorer V2 compiler is not configured") - } - var err error - var snapshot capability.Snapshot - var authorized AuthorizedCapability - if config.AuthorizedCapabilityCompile != nil { - authorized, err = config.AuthorizedCapabilityCompile(ctx, project, snapshotToken) - snapshot = authorized.Snapshot - } else { - snapshot, err = config.CapabilityToken(ctx, project, snapshotToken) - } + value, err := h.application.ApplyCommands(ctx, project, explorerID, request, subjectFromContext(ctx)) if err != nil { - return result, explorerConflict("capability", "STALE_CATALOG_SNAPSHOT", "the capability snapshot is stale or unavailable", map[string]any{"snapshotToken": snapshotToken}) - } - state := authoringv2.BuilderState{APIVersion: authoringv2.APIVersion, Kind: authoringv2.StateKind, Workspace: &workspace, Catalog: authoringV2Catalog(snapshot, explorerID)} - if err := state.Validate(); err != nil { - return result, authoringSemanticRoute("intent", "$", workspaceValidationCode(err), err.Error(), nil) - } - stored, err := config.CompileReceipt(ctx, ExplorerV2ReceiptCompileRequest{Project: project, ExplorerID: explorerID, Workspace: workspace, SnapshotToken: snapshot.Token, RequestID: requestIDFromContext(ctx), Authorized: authorized}) - if err != nil { - var compileErr *explorercompilation.Error - if errors.As(err, &compileErr) { - return result, authoringSemanticRoute(compileErr.Stage, compileErr.Path, compilationErrorCode(compileErr.Code), compileErr.Message, compileErr.Details) - } return result, err } - if stored == nil || strings.TrimSpace(stored.ID) == "" { - return result, explorerUnavailable("compile", "COMPILATION_RECEIPT_STORE_FAILED", "compiled authoring receipt was not persisted") - } - return v2ReceiptResponse(stored, workspace), nil + return directAuthoringJSON[loomapi.ApplyCommandsResponse](value) } func (h *explorerHTTPHandlers) compileAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.CompileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { @@ -265,7 +124,11 @@ func (h *explorerHTTPHandlers) compileAuthoringDirect(ctx context.Context, proje if body == nil { return result, malformedRouteError("intent", errors.New("request body is required")) } - return h.compileAuthoringWorkspaceDirect(ctx, project, explorerID, body.Workspace, body.SnapshotToken) + receipt, err := h.application.Compile(ctx, lifecycle.CompileRequest{Project: project, ExplorerID: explorerID, Workspace: body.Workspace, SnapshotToken: body.SnapshotToken, RequestID: requestIDFromContext(ctx)}) + if err != nil { + return result, err + } + return v2ReceiptResponse(receipt, body.Workspace), nil } func (h *explorerHTTPHandlers) reconcileAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.ReconcileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { @@ -276,126 +139,52 @@ func (h *explorerHTTPHandlers) reconcileAuthoringDirect(ctx context.Context, pro if body == nil { return result, malformedRouteError("reconcile", errors.New("request body is required")) } - if strings.TrimSpace(body.SnapshotToken) == "" || body.DraftVersion < 0 { - return result, malformedRouteError("reconcile", errors.New("snapshotToken, draftVersion, and draftDigest are required")) - } - if h == nil || h.explorers == nil { - return result, explorerUnavailable("reconcile", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured") - } - owner, err := h.explorers.Get(ctx, project, explorerID) + 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 } - if owner.DraftVersion != body.DraftVersion || owner.DraftDigest != body.DraftDigest { - return result, explorerConflict("reconcile", "DRAFT_CONFLICT", "the Explorer draft changed; reload before reconciling", nil) - } - workspace, err := authoringv2.DecodeWorkspace(owner.DraftConfig) - if err != nil { - return result, explorerConflict("reconcile", "AUTHORING_STATE_MISSING", "the saved Explorer draft cannot be reconciled", nil) - } - return h.compileAuthoringWorkspaceDirect(ctx, project, explorerID, workspace, body.SnapshotToken) + 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 - started := time.Now() - previewCtx, cancel := context.WithTimeout(ctx, explorerPreviewTimeout) - defer cancel() - responseBytes, responseRows := 0, 0 - logReceiptID, logOutputID := "", "" - lookupDuration, authorizationDuration := time.Duration(0), time.Duration(0) - previewSummary := engine.PreviewSummary{} - outcome := "failure" - config := h.lifecycleConfig - defer func() { - if config.Logger != nil { - config.Logger.Info("Explorer receipt preview", "operation", "receipt_preview", "project", project, "explorer_id", 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 := h.authoringReadDirect(ctx, project); err != nil { return result, err } - if body == nil || strings.TrimSpace(body.ReceiptId) == "" || strings.TrimSpace(body.OutputId) == "" { + if body == nil { return result, malformedRouteError("preview", errors.New("receiptId and outputId are required")) } - logReceiptID, logOutputID = body.ReceiptId, body.OutputId limit := engine.DefaultPreviewLimit if body.Limit != nil { limit = *body.Limit - } - if limit < 1 || limit > engine.MaxPreviewLimit { - return result, authoringSemanticRoute("preview", "$.limit", "INVALID_PREVIEW_LIMIT", "limit must be between 1 and 1000", nil) - } - nativeReceiptPreview := config.PreviewReceipt != nil - if config.AuthorizedCapabilityExecution == nil { - return result, explorerUnavailable("preview", "PREVIEW_UNAVAILABLE", "authorized receipt execution is not configured") - } - if config.Preview == nil && config.PreviewReceipt == nil { - return result, explorerUnavailable("preview", "PREVIEW_UNAVAILABLE", "Explorer preview is not configured") - } - lookupStarted := time.Now() - receipt, err := lookupV2Receipt(previewCtx, h.explorers, config, project, explorerID, body.ReceiptId) - lookupDuration = time.Since(lookupStarted) - if err != nil { - return result, receiptRouteError("preview", err) - } - if err := validateV2ReceiptRouteValues(receipt, config, project, explorerID); err != nil { - return result, err - } - authorizationStarted := time.Now() - authorized, err := config.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 result, explorerConflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil) - } - if config.PreviewReceipt != nil { - if err := validateAuthorizedReceiptExecution(receipt, authorized); err != nil { - return result, explorerConflict("preview", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil) - } - } - if nativeReceiptPreview { - if !receiptHasOutput(receipt.Bundle, body.OutputId) || validateReceiptOutputContract(receipt, body.OutputId) != nil { - return result, authoringSemanticRoute("preview", "$.outputId", "UNKNOWN_AUTHORING_OUTPUT", "outputId is not in the receipt", nil) - } - } else if !receiptHasOutput(receipt.Bundle, body.OutputId) { - return result, 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{body.OutputId}} - applyAuthorizedScope(&bindings, authorized, false) - columns := make([]explorer.EmittedColumn, 0) - for _, column := range receipt.EmittedColumns { - if column.OutputID == body.OutputId { - columns = append(columns, column) + if limit == 0 { + limit = -1 } } - var encoded []byte - if config.PreviewReceipt != nil { + var finish func() ([]byte, error) + value, 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 result, previewRouteError(encoderErr) - } - previewSummary, err = config.PreviewReceipt(previewCtx, receipt, bindings, encoder.Visit) - responseRows = previewSummary.RowCount - if err == nil { - encoded, err = encoder.Finish() - } - } else { - var rows map[string][]map[string]any - rows, err = config.Preview(previewCtx, receipt.Bundle, bindings) - if err == nil { - responseRows = len(rows[body.OutputId]) - encoded, err = encodeExplorerPreviewResponse(receipt, body.OutputId, columns, rows[body.OutputId], maxExplorerPreviewResponseBytes) + return nil, encoderErr } + finish = encoder.Finish + return encoder.Visit, nil + }}) + if err != nil { + return result, err + } + var encoded []byte + if value.Rows != nil { + encoded, err = encodeExplorerPreviewResponse(value.Receipt, body.OutputId, value.Columns, value.Rows[body.OutputId], maxExplorerPreviewResponseBytes) + } else if finish != nil { + encoded, err = finish() } if err != nil { - return result, previewRouteError(err) + return result, err } - responseBytes = len(encoded) if err := json.Unmarshal(encoded, &result); err != nil { return result, err } - outcome = "success" return result, nil } @@ -404,104 +193,39 @@ func (h *explorerHTTPHandlers) publishAuthoringDirect(ctx context.Context, proje if err := h.authoringWriteDirect(ctx, project); err != nil { return result, err } - if body == nil || strings.TrimSpace(body.ReceiptId) == "" { + if body == nil { return result, malformedRouteError("publish", errors.New("receiptId is required")) } - config := h.lifecycleConfig - if (config.Materialize == nil && config.MaterializeReceipt == nil) || config.ActivateRelease == nil || config.ValidateReleaseGeneration == nil || (config.CapabilityToken == nil && config.AuthorizedCapabilityExecution == nil) { - return result, explorerUnavailable("publish", "PUBLICATION_UNAVAILABLE", "Explorer publication is not configured") - } - receipt, err := lookupV2Receipt(ctx, h.explorers, config, project, explorerID, body.ReceiptId) - if err != nil { - return result, receiptRouteError("publish", err) - } - if err := validateV2ReceiptRouteValues(receipt, config, project, explorerID); err != nil { - return result, err - } - var snapshot capability.Snapshot - var authorized AuthorizedCapability - if config.AuthorizedCapabilityExecution != nil { - authorized, err = config.AuthorizedCapabilityExecution(ctx, receipt.Project, receipt.SnapshotToken) - snapshot = authorized.Snapshot - } else { - snapshot, err = config.CapabilityToken(ctx, receipt.Project, receipt.SnapshotToken) - } - if err != nil || strings.TrimSpace(snapshot.Identity.Generation) != strings.TrimSpace(receipt.SourceGeneration) { - return result, explorerConflict("publish", "RECEIPT_STALE", "the receipt's capability snapshot is no longer authorized or retained", nil) - } - if config.CompileReceipt != nil { - if err := validateReceiptCapability(receipt, snapshot); err != nil { - return result, explorerConflict("publish", "RECEIPT_STALE", err.Error(), nil) - } - } - if len(receipt.EmittedColumns) == 0 || len(receipt.CompiledConfig) == 0 { - return result, authoringSemanticRoute("publish", "$.receiptId", "NO_SELECTED_COLUMNS", "select at least one output column before publishing", nil) - } - if config.CompileReceipt != nil { - workspace, decodeErr := authoringv2.DecodeWorkspace(receipt.NormalizedBundle) - if decodeErr != nil { - return result, explorerConflict("publish", "RECEIPT_RECOMPILE_REQUIRED", "the compilation receipt contains invalid authoring intent", nil) - } - if err := workspace.ValidateForPublication(); err != nil { - return result, authoringSemanticRoute("publish", "$.receiptId", "NO_SELECTED_COLUMNS", "select at least one visible output column for every visible table before publishing", nil) - } - } - if err := config.ValidateReleaseGeneration(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration); err != nil { - return result, 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 config.MaterializeReceipt != nil { - execution, err = config.MaterializeReceipt(ctx, receipt, bindings) - } else { - execution, err = config.Materialize(ctx, receipt.Bundle, bindings) - } - if err != nil { - var mismatch *receiptContractMismatch - if errors.As(err, &mismatch) { - return result, &explorer.AuthoringError{Status: http.StatusConflict, Diagnostic: explorer.AuthoringDiagnostic{Severity: "ERROR", Stage: "publish", Code: "RECEIPT_RECOMPILE_REQUIRED", Message: "receipt deterministic lowering no longer matches the stored artifact", Details: receiptMismatchDetails(receipt.ID, err)}, Cause: err} - } - return result, &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 result, &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 := config.ActivateRelease(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration, selectorsForBundle(receipt.Bundle)); err != nil { - return result, &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 := h.explorers.PublishAuthoring(ctx, *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: subjectFromContext(ctx), CreatedAt: now, ReadyAt: &now}) + 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(revision.Materializations)) - for _, materialization := range revision.Materializations { + 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: receipt.ID, RevisionId: revision.ID, State: string(revision.Status), Outputs: outputs, Diagnostics: []loomapi.Diagnostic{}}, nil + 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 } -// authoringErrorForOpenAPI preserves the generated ErrorResponse contract and -// keeps internal causes out of the response while retaining request IDs and -// structured diagnostics. func authoringErrorForOpenAPI(ctx context.Context, operation string, err error) (int, loomapi.ErrorResponse) { requestID := requestIDFromContext(ctx) - status := 500 + 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, &authoringErr) { + 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 = authoringErr.Diagnostic.Code, authoringErr.Diagnostic.Message, authoringErr.Diagnostic.Stage, authoringErr.Diagnostic.JSONPath - details = authoringErr.Diagnostic.Details + 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 = 404, "NOT_FOUND", "Explorer resource not found" + status, code, message = http.StatusNotFound, "NOT_FOUND", "Explorer resource not found" } if stage == "" { stage = operation @@ -515,8 +239,7 @@ func authoringErrorForOpenAPI(ctx context.Context, operation string, err 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)) } - severity := "error" - diagnostic := loomapi.Diagnostic{Severity: severity, Stage: stage, Code: code, Message: message} + diagnostic := loomapi.Diagnostic{Severity: "error", Stage: stage, Code: code, Message: message} if path != "" { diagnostic.JsonPath = &path } @@ -537,3 +260,22 @@ func authoringErrorForOpenAPI(ctx context.Context, operation string, err error) } 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_direct.go b/internal/server/openapi_explorer_direct.go index 2f48f457..c399d54d 100644 --- a/internal/server/openapi_explorer_direct.go +++ b/internal/server/openapi_explorer_direct.go @@ -4,26 +4,19 @@ import ( "context" "encoding/json" "errors" - "fmt" "net/http" "strings" - "time" loomapi "github.com/calypr/loom/generated/loomapi" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" - httpapi "github.com/calypr/loom/internal/api/http" "github.com/calypr/loom/internal/authscope" - "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/lifecycle" "github.com/calypr/loom/internal/projectid" ) -// explorerLegacyOperationError carries the response contract used by the -// legacy Explorer endpoints. The old Fiber handlers encoded either a -// structured {code,message} value or a plain error string depending on the -// route; keeping that distinction here lets generated OpenAPI responses use -// the same wire shape without executing a second handler. +// 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 @@ -54,6 +47,32 @@ 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) { @@ -73,7 +92,7 @@ func explorerLegacyResponse(err error) (int, loomapi.LegacyErrorResponse) { func (r *explorerHTTPHandlers) listExplorers(ctx context.Context, rawProject string) (loomapi.ListExplorers200JSONResponse, error) { var result loomapi.ListExplorers200JSONResponse - if r == nil || r.explorers == nil || r.authorizeRead == nil { + 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) @@ -81,19 +100,11 @@ func (r *explorerHTTPHandlers) listExplorers(ctx context.Context, rawProject str if err := r.authorizeRead(ctx, principal, project); err != nil { return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) } - values, err := r.explorers.List(ctx, project) + value, err := r.application.List(ctx, project) if err != nil { - return result, legacyOperationFailure(http.StatusInternalServerError, "EXPLORER_READ_FAILED", err.Error(), err) - } - 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 result, lifecycleLegacyFailure(err) } - raw, err := json.Marshal(summaries) + raw, err := json.Marshal(value.Summaries) if err != nil { return result, err } @@ -105,7 +116,7 @@ func (r *explorerHTTPHandlers) listExplorers(ctx context.Context, rawProject str func (r *explorerHTTPHandlers) createExplorer(ctx context.Context, rawProject string, body *loomapi.CreateExplorerJSONRequestBody) (loomapi.CreateExplorer201JSONResponse, error) { var result loomapi.CreateExplorer201JSONResponse - if r == nil || r.explorers == nil || r.authorizeRead == nil || r.authorizer == nil { + 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) @@ -113,35 +124,23 @@ func (r *explorerHTTPHandlers) createExplorer(ctx context.Context, rawProject st if err := r.authorizer.AuthorizeWrite(ctx, principal, project, ""); 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"` } - if body == nil { - return result, legacyOperationFailure(http.StatusBadRequest, "MALFORMED_REQUEST", "name is required", nil) - } raw, err := json.Marshal(*body) - if err != nil || decodeStrict(raw, &request) != nil || strings.TrimSpace(request.Name) == "" { + if err != nil || decodeStrict(raw, &request) != nil { return result, legacyOperationFailure(http.StatusBadRequest, "MALFORMED_REQUEST", "name is required", err) } - id := explorer.StableExplorerID(request.Name) - if id == "default" { - return result, legacyOperationFailure(http.StatusConflict, "EXPLORER_EXISTS", "the repository default already exists", nil) - } - title := strings.TrimSpace(request.Title) - if title == "" { - title = strings.TrimSpace(request.Name) - } - value, err := r.explorers.CreateInteractiveFrom(ctx, project, id, title, strings.TrimSpace(request.SourceExplorerID), subjectFromContext(ctx)) - if errors.Is(err, explorer.ErrDraftConflict) { - return result, legacyOperationFailure(http.StatusConflict, "EXPLORER_EXISTS", "an Explorer with this name already exists", 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, legacyOperationFailure(http.StatusUnprocessableEntity, "INVALID_EXPLORER", err.Error(), err) + return result, lifecycleLegacyFailure(err) } - summary := explorer.ExplorerSummaryV1{Project: project, ExplorerID: value.ExplorerID, Title: value.Title, Management: value.ManagementMode, UpdatedAt: value.UpdatedAt} - raw, err = json.Marshal(summary) + raw, err = json.Marshal(value) if err != nil { return result, err } @@ -153,23 +152,19 @@ func (r *explorerHTTPHandlers) createExplorer(ctx context.Context, rawProject st func (r *explorerHTTPHandlers) getExplorer(ctx context.Context, rawProject, rawID string) (loomapi.GetExplorer200JSONResponse, error) { var result loomapi.GetExplorer200JSONResponse - if r == nil || r.explorers == nil || r.authorizeRead == nil { + 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) - id := strings.TrimSpace(rawID) principal, _ := authscope.PrincipalFromContext(ctx) if err := r.authorizeRead(ctx, principal, project); err != nil { return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) } - state, err := r.explorers.LoadExplorerState(ctx, project, id) - if errors.Is(err, explorer.ErrNotFound) { - return result, legacyOperationFailure(http.StatusNotFound, "EXPLORER_NOT_FOUND", "Explorer not found", err) - } + value, err := r.application.Get(ctx, project, rawID) if err != nil { - return result, legacyOperationFailure(http.StatusInternalServerError, "EXPLORER_READ_FAILED", err.Error(), err) + return result, lifecycleLegacyFailure(err) } - raw, err := json.Marshal(state) + raw, err := json.Marshal(value) if err != nil { return result, err } @@ -179,17 +174,9 @@ func (r *explorerHTTPHandlers) getExplorer(ctx context.Context, rawProject, rawI return result, nil } -func subjectFromContext(ctx context.Context) string { - principal, _ := authscope.PrincipalFromContext(ctx) - if principal == nil { - return "" - } - return principal.Subject -} - func (r *explorerHTTPHandlers) publishRepositoryExplorerConfig(ctx context.Context, request loomapi.PublishRepositoryExplorerConfigRequestObject) (loomapi.PublishRepositoryExplorerConfig200JSONResponse, error) { var result loomapi.PublishRepositoryExplorerConfig200JSONResponse - if r == nil || r.explorers == nil || r.authorizer == nil { + 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)) @@ -216,94 +203,17 @@ func (r *explorerHTTPHandlers) publishRepositoryExplorerConfig(ctx context.Conte if commit == "" { return result, legacyStringFailure(http.StatusBadRequest, "X-Loom-Source-Commit is required", nil) } - config := r.lifecycleConfig - if config.Capability == nil || config.AuthorizedCapabilityCompile == nil || config.CompileReceipt == nil { - return result, legacyStringFailure(http.StatusServiceUnavailable, "repository V2 compilation is not configured", nil) - } - snapshot, err := config.Capability(ctx, 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 result, legacyStringFailure(http.StatusConflict, fmt.Sprintf("resolve repository V2 capability: %v", err), err) - } - authorized, err := config.AuthorizedCapabilityCompile(ctx, project, snapshot.Token) - if err != nil { - return result, legacyStringFailure(http.StatusForbidden, fmt.Sprintf("authorize repository V2 compilation: %v", err), err) - } - receipt, err := config.CompileReceipt(ctx, ExplorerV2ReceiptCompileRequest{Project: project, ExplorerID: "default", Workspace: workspace, SnapshotToken: snapshot.Token, RequestID: requestIDFromContext(ctx), Authorized: authorized}) - if err != nil || receipt == nil { - if err == nil { - err = fmt.Errorf("compiler returned no receipt") - } - return result, legacyStringFailure(http.StatusUnprocessableEntity, fmt.Sprintf("compile repository V2 workspace: %v", err), err) - } - if receipt.SourceGeneration != generation { - return result, legacyStringFailure(http.StatusConflict, "compiled receipt generation does not match deployment generation", nil) - } - if config.AuthorizedCapabilityExecution != nil { - authorized, err = config.AuthorizedCapabilityExecution(ctx, project, receipt.SnapshotToken) - if err != nil { - return result, legacyStringFailure(http.StatusForbidden, fmt.Sprintf("authorize repository V2 materialization: %v", err), 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(ctx, receipt, bindings) - } else if r.materialize != nil { - execution, err = r.materialize(ctx, receipt.Bundle, bindings) - } else { - err = fmt.Errorf("repository V2 materialization is not configured") - } + value, err := r.application.PublishRepository(ctx, lifecycle.RepositoryPublishRequest{Project: project, Generation: generation, Workspace: workspace, Commit: commit, Actor: subjectFromContext(ctx)}) if err != nil { - return result, legacyStringFailure(http.StatusUnprocessableEntity, fmt.Sprintf("materialize repository V2 workspace: %v", err), err) + status, legacy := explorerLegacyResponse(lifecycleLegacyFailure(err)) + return result, legacyStringFailure(status, legacyErrorMessage(legacy), err) } - if err := verifyQueryableOutputs(receipt.Bundle, execution); err != nil { - return result, legacyStringFailure(http.StatusUnprocessableEntity, err.Error(), err) - } - materializations := explorerMaterializations(receipt.Bundle, execution) - datasetMetadata := datasetMetadataFromExecution(receipt.Bundle, generation, receipt.ResolvedSchemaDigest, execution) - publication := explorer.PublicationMetadata{State: string(explorer.RevisionReady), Generation: generation, ExecutionID: execution.ID, UpdatedAt: nowUTC()} - owner, revision, err := r.explorers.UpsertRepositoryV2(ctx, *receipt, commit, subjectFromContext(ctx), materializations, datasetMetadata, publication) - if err != nil { - return result, legacyStringFailure(http.StatusInternalServerError, fmt.Sprintf("persist Explorer lifecycle V2: %v", err), err) - } - if owner.ManagementMode != explorer.ManagementRepository { - return result, legacyStringFailure(http.StatusConflict, "default Explorer has invalid management mode", nil) - } - if config.ActivateRelease == nil { - return result, legacyStringFailure(http.StatusServiceUnavailable, "release activation is not configured", nil) - } - if err := config.ActivateRelease(ctx, projectid.Legacy(project), generation, selectorsForBundle(receipt.Bundle)); err != nil { - _, _ = r.explorers.FailRevision(ctx, revision.ID, []explorer.Diagnostic{{Severity: "ERROR", Code: "RELEASE_ACTIVATION_FAILED", Message: err.Error(), Retryable: true}}) - return result, legacyStringFailure(http.StatusConflict, fmt.Sprintf("activate published ExplorerConfigV2: %v", err), err) - } - if err := r.explorers.ActivateRepositoryGeneration(ctx, project, generation, revision.ID); err != nil { - return result, legacyStringFailure(http.StatusConflict, fmt.Sprintf("activate ExplorerConfigV2: %v", err), err) - } - publication.State = string(explorer.RevisionActive) - publication.RevisionID = revision.ID - publication.UpdatedAt = nowUTC() - if _, err := r.explorers.SaveRepositoryConfig(ctx, 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: datasetMetadata, Publication: publication, - }); err != nil { - return result, legacyStringFailure(http.StatusInternalServerError, fmt.Sprintf("persist ExplorerConfigV2: %v", err), err) - } - raw := loomapi.RawJSON{"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} - result = loomapi.PublishRepositoryExplorerConfig200JSONResponse(raw) - return result, nil -} - -func requestIDFromContext(ctx context.Context) string { - return httpapi.RequestIDFromContext(ctx) + 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 nowUTC() (now time.Time) { - return time.Now().UTC() +func legacyErrorMessage(value loomapi.LegacyErrorResponse) string { + if message, err := value.Error.AsLegacyErrorResponseError0(); err == nil { + return message + } + return "Explorer lifecycle failed" } From 956a1d111dfd4da4b2048d28f1a9eb4b0e1f448d Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 15:21:32 -0700 Subject: [PATCH 05/17] refactor(dataframe): unify semantic projection model --- .../graph/resolver/recipe_control_graphql.go | 10 +- .../graph/resolver/recipe_control_test.go | 2 +- .../dataframe/compiler/capability/probe.go | 51 +++--- .../compiler/generic_physical_plan_test.go | 153 ++++++++---------- .../dataframe/compiler/lower/auth_scope.go | 4 +- .../dataframe/compiler/lower/child_sets.go | 18 +-- .../dataframe/compiler/lower/graph_plan.go | 52 +++--- .../compiler/lower/graph_plan_test.go | 8 +- .../compiler/lower/plan_orchestration.go | 83 ++++++---- .../compiler/lower/recipe_compile.go | 24 +-- .../lower/recipe_expression_lowering.go | 132 ++++++--------- .../compiler/lower/recipe_output_test.go | 51 ++++++ .../dataframe/compiler/lower/recipe_schema.go | 36 +---- .../compiler/lower/root_projection.go | 125 ++++++++++---- .../compiler/physical_diagnostics_test.go | 25 ++- .../compiler/physical_lowering_test.go | 40 ++--- .../compiler/physical_optimize_test.go | 45 +++--- .../compiler/physical_render_test.go | 98 ++++------- .../compiler/physical_required_match_test.go | 29 ++-- .../dataframe/compiler/physical_scope_test.go | 19 +-- .../compiler/plan_test_helpers_test.go | 24 ++- .../compiler/render/aql/lookup_test.go | 11 +- .../compiler/render/aql/unnest_test.go | 11 +- .../compiler/selection_semantics_test.go | 15 +- .../compiler/semantic_validation_test.go | 28 ++-- internal/dataframe/runtime/auth_scope_test.go | 11 +- .../dataframe/semantic/execution_context.go | 14 ++ .../dataframe/semantic/recipe_plan_output.go | 8 +- .../dataframe/semantic/recipe_plan_test.go | 26 +++ .../dataframe/semantic/recipe_plan_types.go | 12 +- .../dataframe/semantic/recipe_projection.go | 101 ++++++------ .../dataframe/semantic/recipe_rich_shaping.go | 4 +- .../semantic/recipe_rich_shaping_test.go | 3 +- .../dataframe/semantic/selection_semantics.go | 61 ++++++- .../{semantic_plan.go => semantic_nodes.go} | 60 ++----- .../dataframe/semantic/semantic_validation.go | 26 +-- 36 files changed, 720 insertions(+), 700 deletions(-) create mode 100644 internal/dataframe/semantic/execution_context.go rename internal/dataframe/semantic/{semantic_plan.go => semantic_nodes.go} (52%) diff --git a/internal/api/graphql/graph/resolver/recipe_control_graphql.go b/internal/api/graphql/graph/resolver/recipe_control_graphql.go index e5f1302a..1160b163 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_graphql.go +++ b/internal/api/graphql/graph/resolver/recipe_control_graphql.go @@ -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 { @@ -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", diff --git a/internal/api/graphql/graph/resolver/recipe_control_test.go b/internal/api/graphql/graph/resolver/recipe_control_test.go index 827c426e..4780c24f 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_test.go +++ b/internal/api/graphql/graph/resolver/recipe_control_test.go @@ -48,7 +48,7 @@ func (f fakeRecipeControl) Preview(context.Context, string, recipe.RuntimeBindin func testRecipeValidation() engine.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} } diff --git a/internal/dataframe/compiler/capability/probe.go b/internal/dataframe/compiler/capability/probe.go index ab7dc7e5..267797ed 100644 --- a/internal/dataframe/compiler/capability/probe.go +++ b/internal/dataframe/compiler/capability/probe.go @@ -186,7 +186,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 +211,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 } @@ -374,12 +375,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 +407,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 +422,23 @@ 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 { + if err := options.Cost.Allow(ctx, CostEstimate{TraversalCount: countSemanticTraversals(output.Root)}); err != nil { return ir.PhysicalPlan{}, Rendered{}, err } } - plan.Project = scope.Project - plan.DatasetGeneration = scope.DatasetGeneration - plan.AuthResourcePaths = cloneStrings(scope.AuthResourcePaths) - plan.AuthScopeMode = scope.AuthScopeMode - physical, err := lower.BuildGenericPhysicalPlanWithPolicy(plan, options.Policy) + context := semantic.ExecutionContext{ + Project: scope.Project, + DatasetGeneration: scope.DatasetGeneration, + AuthResourcePaths: cloneStrings(scope.AuthResourcePaths), + AuthScopeMode: scope.AuthScopeMode, + } + physical, err := lower.BuildGenericPhysicalPlanWithPolicy(output, context, options.Policy) if err != nil { return ir.PhysicalPlan{}, Rendered{}, err } @@ -606,19 +614,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 diff --git a/internal/dataframe/compiler/generic_physical_plan_test.go b/internal/dataframe/compiler/generic_physical_plan_test.go index 9980710a..113c22e8 100644 --- a/internal/dataframe/compiler/generic_physical_plan_test.go +++ b/internal/dataframe/compiler/generic_physical_plan_test.go @@ -7,15 +7,13 @@ 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/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 +22,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 +65,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 +76,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) @@ -111,19 +108,18 @@ func TestBuildAndRenderGenericPhysicalPlanOptionalChildFieldsAndFilters(t *testi 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) @@ -159,18 +155,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 +187,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 +219,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) @@ -250,17 +246,16 @@ func TestBuildAndRenderGenericPhysicalPlanAggregatePredicates(t *testing.T) { 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 +331,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 +373,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 +398,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 +448,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 +477,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/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_sets.go b/internal/dataframe/compiler/lower/child_sets.go index c4d466d0..b0cc5106 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,22 +76,12 @@ 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) 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 03efbee7..da87c8a7 100644 --- a/internal/dataframe/compiler/lower/plan_orchestration.go +++ b/internal/dataframe/compiler/lower/plan_orchestration.go @@ -10,60 +10,72 @@ import ( 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 { + if err := semanticpkg.ValidateSemanticGraph(output.Root); 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 !fhirschema.ResourceExists(output.Root.ResourceType) { + return ir.PhysicalPlan{}, fmt.Errorf("root resource type %q is not represented by the generated FHIR schema", output.Root.ResourceType) } - if err := validateGenericPhysicalNode(semantic.Root, true); err != nil { + if err := validateGenericPhysicalNode(output.Root, true); 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 @@ -73,16 +85,17 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic if physicalNodeNeedsMaterializedSet(child) { childSetIndex++ childProjectionPrefix := child.Alias - if semantic.TraversalColumnNaming != recipe.TraversalColumnNamingAlias && 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 } } @@ -114,7 +127,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 @@ -124,25 +138,26 @@ func BuildGenericPhysicalPlanWithPolicy(semantic semanticpkg.SemanticPlan, polic // filters and projections to be applied before materialization. childSetIndex++ childProjectionPrefix := child.Alias - if semantic.TraversalColumnNaming != recipe.TraversalColumnNamingAlias && 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 } @@ -151,7 +166,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 { diff --git a/internal/dataframe/compiler/lower/recipe_compile.go b/internal/dataframe/compiler/lower/recipe_compile.go index cda739af..1174aa32 100644 --- a/internal/dataframe/compiler/lower/recipe_compile.go +++ b/internal/dataframe/compiler/lower/recipe_compile.go @@ -125,32 +125,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, - Project: bindings.Project, - DatasetGeneration: bindings.DatasetGeneration, - AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), - AuthScopeMode: bindings.AuthScopeMode, - TraversalColumnNaming: output.TraversalColumnNaming, - Root: root, - RowIdentity: &identity, + context := semantic.ExecutionContext{ + Project: bindings.Project, + DatasetGeneration: bindings.DatasetGeneration, + AuthResourcePaths: append([]string(nil), bindings.AuthResourcePaths...), + AuthScopeMode: bindings.AuthScopeMode, } - 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 } 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 55e3c52c..f78840ca 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"}) diff --git a/internal/dataframe/compiler/lower/recipe_schema.go b/internal/dataframe/compiler/lower/recipe_schema.go index 07185f14..0b2378ac 100644 --- a/internal/dataframe/compiler/lower/recipe_schema.go +++ b/internal/dataframe/compiler/lower/recipe_schema.go @@ -12,39 +12,12 @@ import ( "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 @@ -83,18 +56,11 @@ 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) diff --git a/internal/dataframe/compiler/lower/root_projection.go b/internal/dataframe/compiler/lower/root_projection.go index b87010fe..de6ee62f 100644 --- a/internal/dataframe/compiler/lower/root_projection.go +++ b/internal/dataframe/compiler/lower/root_projection.go @@ -5,44 +5,99 @@ 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) @@ -190,10 +245,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/physical_diagnostics_test.go b/internal/dataframe/compiler/physical_diagnostics_test.go index f21d2e1b..62e2b3de 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) 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..cd00ea8c 100644 --- a/internal/dataframe/compiler/physical_optimize_test.go +++ b/internal/dataframe/compiler/physical_optimize_test.go @@ -49,7 +49,7 @@ 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"}}) + plan, err := buildGenericPhysicalPlan(semantic.OutputPlan{Root: semantic.SemanticNode{Alias: "root", ResourceType: "Patient"}}) if err != nil { t.Fatal(err) } @@ -232,18 +232,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 +261,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 +407,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_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_test.go b/internal/dataframe/compiler/physical_scope_test.go index 10065876..68eeb3b4 100644 --- a/internal/dataframe/compiler/physical_scope_test.go +++ b/internal/dataframe/compiler/physical_scope_test.go @@ -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/render/aql/lookup_test.go b/internal/dataframe/compiler/render/aql/lookup_test.go index 6809985e..9fb9caf3 100644 --- a/internal/dataframe/compiler/render/aql/lookup_test.go +++ b/internal/dataframe/compiler/render/aql/lookup_test.go @@ -26,12 +26,11 @@ func TestRenderPhysicalLookupExpression(t *testing.T) { 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()) + 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/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/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/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/runtime/auth_scope_test.go b/internal/dataframe/runtime/auth_scope_test.go index 7b5f8bb9..2e295b0b 100644 --- a/internal/dataframe/runtime/auth_scope_test.go +++ b/internal/dataframe/runtime/auth_scope_test.go @@ -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/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 7ae43b2c..29caca45 100644 --- a/internal/dataframe/semantic/recipe_plan_output.go +++ b/internal/dataframe/semantic/recipe_plan_output.go @@ -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 942a62d7..f6ae1f1a 100644 --- a/internal/dataframe/semantic/recipe_plan_types.go +++ b/internal/dataframe/semantic/recipe_plan_types.go @@ -40,11 +40,9 @@ type OutputPlan struct { // 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 @@ -57,14 +55,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 @@ -184,7 +174,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..8f89154d 100644 --- a/internal/dataframe/semantic/recipe_rich_shaping.go +++ b/internal/dataframe/semantic/recipe_rich_shaping.go @@ -341,10 +341,10 @@ 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) diff --git a/internal/dataframe/semantic/recipe_rich_shaping_test.go b/internal/dataframe/semantic/recipe_rich_shaping_test.go index eba28ef7..adcbcbf7 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) { @@ -113,7 +114,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 a733cfc1..440e2863 100644 --- a/internal/dataframe/semantic/semantic_plan.go +++ b/internal/dataframe/semantic/semantic_nodes.go @@ -1,27 +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/recipe" "github.com/calypr/loom/internal/dataframe/spec" ) -type SemanticPlan struct { - Version int - Project string - DatasetGeneration string - AuthResourcePaths []string - AuthScopeMode authscope.ReadScopeMode - RowIdentity *spec.RowIdentity - TraversalColumnNaming recipe.TraversalColumnNaming - 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 @@ -38,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 } @@ -88,26 +75,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 { From c3261e1140fdb71c89ed6dd1c2f3094db96cecc7 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 15:33:14 -0700 Subject: [PATCH 06/17] refactor(dataframe): consolidate execution package --- Makefile | 2 +- conformance/compiler/policy_ablation_test.go | 4 ++-- docs/DEVELOPER_ARCHITECTURE.md | 6 ++--- docs/EXPLORER_COMPILATION_ARCHITECTURE.md | 2 +- .../graphql/graph/http_integration_test.go | 6 ++--- .../graph/query/active_generation_test.go | 4 ++-- .../graphql/graph/query/auth_scope_test.go | 6 ++--- internal/api/graphql/graph/query/doc.go | 4 ++-- .../api/graphql/graph/query/fhir_service.go | 2 +- .../graphql/graph/query/fhir_service_test.go | 4 ++-- internal/api/graphql/graph/query/service.go | 20 ++++++++--------- .../graph/resolver/dataframe_diagnostics.go | 4 ++-- .../graphql/graph/resolver/graph_resolvers.go | 4 ++-- .../graph/resolver/recipe_control_graphql.go | 10 ++++----- .../graph/resolver/recipe_control_test.go | 22 +++++++++---------- .../api/graphql/graph/resolver/resolver.go | 10 ++++----- .../{runtime => execution}/auth_scope_test.go | 2 +- .../{recipe/engine => execution}/control.go | 7 +++--- .../control_explain_types.go | 2 +- .../engine => execution}/control_types.go | 2 +- .../{recipe/engine => execution}/engine.go | 4 ++-- .../engine => execution}/engine_test.go | 2 +- .../execution_service.go | 10 ++++----- .../{runtime => execution}/execution_test.go | 2 +- .../{runtime => execution}/explain.go | 2 +- .../{runtime => execution}/explain_test.go | 2 +- .../{runtime => execution}/helpers.go | 2 +- .../{runtime => execution}/helpers_test.go | 2 +- .../engine => execution}/identity_test.go | 2 +- .../engine => execution}/named_uuid.go | 4 ++-- .../engine => execution}/named_uuid_test.go | 2 +- .../{recipe/engine => execution}/postquery.go | 2 +- .../engine => execution}/postquery_test.go | 2 +- .../{runtime => execution}/service.go | 2 +- .../dataframe/{runtime => execution}/types.go | 10 ++++----- internal/dataframe/runtime/profile.go | 1 - internal/explorer/lifecycle/service.go | 6 ++--- internal/explorer/lifecycle/types.go | 6 ++--- internal/server/explain_wiring.go | 9 ++++---- .../server/explorer_authoring_v2_routes.go | 4 ++-- internal/server/explorer_lifecycle_adapter.go | 4 ++-- internal/server/explorer_lifecycle_helpers.go | 4 ++-- internal/server/explorer_lifecycle_types.go | 4 ++-- internal/server/explorer_preview_security.go | 4 ++-- .../server/explorer_preview_security_test.go | 4 ++-- internal/server/explorer_publication.go | 12 +++++----- internal/server/explorer_receipt_contract.go | 12 +++++----- .../server/explorer_receipt_contract_test.go | 14 ++++++------ .../openapi_explorer_authoring_direct.go | 4 ++-- internal/server/recipe_publication.go | 22 +++++++++---------- internal/server/server.go | 19 ++++++++-------- scripts/check_dataframe_package_boundaries.sh | 14 ++++++------ 52 files changed, 156 insertions(+), 160 deletions(-) rename internal/dataframe/{runtime => execution}/auth_scope_test.go (98%) rename internal/dataframe/{recipe/engine => execution}/control.go (96%) rename internal/dataframe/{recipe/engine => execution}/control_explain_types.go (99%) rename internal/dataframe/{recipe/engine => execution}/control_types.go (97%) rename internal/dataframe/{recipe/engine => execution}/engine.go (99%) rename internal/dataframe/{recipe/engine => execution}/engine_test.go (99%) rename internal/dataframe/{runtime => execution}/execution_service.go (94%) rename internal/dataframe/{runtime => execution}/execution_test.go (99%) rename internal/dataframe/{runtime => execution}/explain.go (98%) rename internal/dataframe/{runtime => execution}/explain_test.go (97%) rename internal/dataframe/{runtime => execution}/helpers.go (95%) rename internal/dataframe/{runtime => execution}/helpers_test.go (93%) rename internal/dataframe/{recipe/engine => execution}/identity_test.go (99%) rename internal/dataframe/{recipe/engine => execution}/named_uuid.go (93%) rename internal/dataframe/{recipe/engine => execution}/named_uuid_test.go (98%) rename internal/dataframe/{recipe/engine => execution}/postquery.go (99%) rename internal/dataframe/{recipe/engine => execution}/postquery_test.go (99%) rename internal/dataframe/{runtime => execution}/service.go (95%) rename internal/dataframe/{runtime => execution}/types.go (81%) delete mode 100644 internal/dataframe/runtime/profile.go diff --git a/Makefile b/Makefile index 96c8e554..413d02fd 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,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) 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/DEVELOPER_ARCHITECTURE.md b/docs/DEVELOPER_ARCHITECTURE.md index b8b8dc86..da65610a 100644 --- a/docs/DEVELOPER_ARCHITECTURE.md +++ b/docs/DEVELOPER_ARCHITECTURE.md @@ -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 @@ -239,7 +239,7 @@ 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 diff --git a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md index 730eaf40..c59b55d8 100644 --- a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md +++ b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md @@ -249,7 +249,7 @@ execution contract. | 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` | | Explorer list/create/authoring/receipt/preview/publication workflows | `internal/explorer/lifecycle` | -| Resolved-recipe execution boundary | `internal/dataframe/recipe/engine/engine.go` | +| 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/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/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/recipe_control_graphql.go b/internal/api/graphql/graph/resolver/recipe_control_graphql.go index 1160b163..32a75397 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_graphql.go +++ b/internal/api/graphql/graph/resolver/recipe_control_graphql.go @@ -14,8 +14,8 @@ import ( materializationapi "github.com/calypr/loom/internal/api/graphql/graph/materialization" "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()) @@ -113,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 @@ -147,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)), @@ -313,7 +313,7 @@ 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 { diff --git a/internal/api/graphql/graph/resolver/recipe_control_test.go b/internal/api/graphql/graph/resolver/recipe_control_test.go index 4780c24f..abefcad7 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_test.go +++ b/internal/api/graphql/graph/resolver/recipe_control_test.go @@ -5,30 +5,30 @@ 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 +40,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, 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) { diff --git a/internal/api/graphql/graph/resolver/resolver.go b/internal/api/graphql/graph/resolver/resolver.go index 3c04fdce..d542021f 100644 --- a/internal/api/graphql/graph/resolver/resolver.go +++ b/internal/api/graphql/graph/resolver/resolver.go @@ -7,10 +7,10 @@ import ( "github.com/calypr/loom/generated/graphql/graph/model" materializationapi "github.com/calypr/loom/internal/api/graphql/graph/materialization" 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. diff --git a/internal/dataframe/runtime/auth_scope_test.go b/internal/dataframe/execution/auth_scope_test.go similarity index 98% rename from internal/dataframe/runtime/auth_scope_test.go rename to internal/dataframe/execution/auth_scope_test.go index 2e295b0b..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" 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 99% rename from internal/dataframe/recipe/engine/engine.go rename to internal/dataframe/execution/engine.go index 81db9d9c..fb6d40d9 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" diff --git a/internal/dataframe/recipe/engine/engine_test.go b/internal/dataframe/execution/engine_test.go similarity index 99% rename from internal/dataframe/recipe/engine/engine_test.go rename to internal/dataframe/execution/engine_test.go index 7ad49f68..b54ad476 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" 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/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/explorer/lifecycle/service.go b/internal/explorer/lifecycle/service.go index d9d170b7..d4d40448 100644 --- a/internal/explorer/lifecycle/service.go +++ b/internal/explorer/lifecycle/service.go @@ -7,8 +7,8 @@ import ( "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" @@ -245,9 +245,9 @@ func (s *Service) Preview(ctx context.Context, request PreviewRequest) (PreviewR return PreviewResult{}, malformed("preview", "receiptId and outputId are required", nil) } if request.Limit == 0 { - request.Limit = engine.DefaultPreviewLimit + request.Limit = dataframeexecution.DefaultPreviewLimit } - if request.Limit > engine.MaxPreviewLimit { + 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 { diff --git a/internal/explorer/lifecycle/types.go b/internal/explorer/lifecycle/types.go index f754b10c..a88f9cb2 100644 --- a/internal/explorer/lifecycle/types.go +++ b/internal/explorer/lifecycle/types.go @@ -9,9 +9,9 @@ import ( "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/dataframe/recipe/engine" "github.com/calypr/loom/internal/dataset" "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/authoringv2" @@ -71,7 +71,7 @@ type CompileReceiptRequest struct { type ReceiptCompiler func(context.Context, CompileReceiptRequest) (*explorer.CompilationReceipt, error) type ReceiptReader func(context.Context, string, string, string) (*explorer.CompilationReceipt, error) type Previewer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (map[string][]map[string]any, error) -type ReceiptPreviewer func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (engine.PreviewSummary, 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. @@ -177,7 +177,7 @@ type PreviewRequest struct { type PreviewResult struct { Receipt *explorer.CompilationReceipt Columns []explorer.EmittedColumn - Summary engine.PreviewSummary + Summary dataframeexecution.PreviewSummary Rows map[string][]map[string]any } 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_v2_routes.go b/internal/server/explorer_authoring_v2_routes.go index f128755a..62675f41 100644 --- a/internal/server/explorer_authoring_v2_routes.go +++ b/internal/server/explorer_authoring_v2_routes.go @@ -6,7 +6,7 @@ import ( explorerv2api "github.com/calypr/loom/generated/loomapi" "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" "github.com/calypr/loom/internal/explorer/authoringv2" explorercompilation "github.com/calypr/loom/internal/explorer/compilation" @@ -166,7 +166,7 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead func previewLimit(value *int) int { if value == nil { - return engine.DefaultPreviewLimit + return dataframeexecution.DefaultPreviewLimit } if *value == 0 { return -1 diff --git a/internal/server/explorer_lifecycle_adapter.go b/internal/server/explorer_lifecycle_adapter.go index 09e63510..501e624d 100644 --- a/internal/server/explorer_lifecycle_adapter.go +++ b/internal/server/explorer_lifecycle_adapter.go @@ -4,8 +4,8 @@ import ( "context" graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" + 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/dataset" "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/authoringv2" @@ -48,7 +48,7 @@ func newExplorerLifecycleApplication(store *explorer.Service, config ExplorerV2L } } if config.PreviewReceipt != nil { - appConfig.PreviewReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (engine.PreviewSummary, error) { + appConfig.PreviewReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { return config.PreviewReceipt(ctx, receipt, bindings, visit) } } diff --git a/internal/server/explorer_lifecycle_helpers.go b/internal/server/explorer_lifecycle_helpers.go index 1056d87a..ed57e221 100644 --- a/internal/server/explorer_lifecycle_helpers.go +++ b/internal/server/explorer_lifecycle_helpers.go @@ -8,10 +8,10 @@ import ( "github.com/calypr/loom/internal/dataframe/compiler/ir" "github.com/calypr/loom/internal/dataframe/compiler/lower" - "github.com/calypr/loom/internal/dataframe/recipe/engine" + dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" ) -func resolvedOutputArtifacts(resolved engine.Resolved) (map[string]string, map[string]map[string]string, error) { +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 { diff --git a/internal/server/explorer_lifecycle_types.go b/internal/server/explorer_lifecycle_types.go index 22f5d754..50355a6d 100644 --- a/internal/server/explorer_lifecycle_types.go +++ b/internal/server/explorer_lifecycle_types.go @@ -5,8 +5,8 @@ import ( "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/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/lifecycle" @@ -18,7 +18,7 @@ type ExplorerV2Materializer = graphresolver.ExplorerBundleMaterializer type ExplorerV2ReceiptCompileRequest = lifecycle.CompileReceiptRequest type ExplorerV2ReceiptCompiler = lifecycle.ReceiptCompiler 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 ExplorerV2ReceiptPreviewer func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (dataframeexecution.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 diff --git a/internal/server/explorer_preview_security.go b/internal/server/explorer_preview_security.go index 75287a56..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" ) @@ -41,7 +41,7 @@ func validateAuthorizedReadScope(scope authscope.ReadScope, expectedDigest strin // 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 20b62786..3fc867d0 100644 --- a/internal/server/explorer_preview_security_test.go +++ b/internal/server/explorer_preview_security_test.go @@ -8,8 +8,8 @@ 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" ) @@ -30,7 +30,7 @@ func TestValidateAuthorizedReadScopePreservesRestrictedEmpty(t *testing.T) { 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 2ef94de6..453c42da 100644 --- a/internal/server/explorer_publication.go +++ b/internal/server/explorer_publication.go @@ -6,17 +6,17 @@ import ( "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" ) // 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 { +// 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) ExplorerV2ReceiptMaterializer { return func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (graphresolver.RecipeExecution, error) { if receipt == nil { return graphresolver.RecipeExecution{}, fmt.Errorf("compilation receipt is required") @@ -26,7 +26,7 @@ func explorerReceiptMaterializer(recipeEngine *engine.Engine, target publication } bindings.IncludeAuthResourcePath = true var identity publication.BundleIdentity - _, err := recipeEngine.MaterializeResolvedBundle(ctx, receipt.Bundle, bindings, func(run context.Context, full engine.Resolved) error { + _, err := recipeEngine.MaterializeResolvedBundle(ctx, receipt.Bundle, bindings, func(run context.Context, full dataframeexecution.Resolved) error { if validationErr := validateReceiptResolution(receipt, &full); validationErr != nil { return validationErr } @@ -53,14 +53,14 @@ func explorerReceiptMaterializer(recipeEngine *engine.Engine, target publication } } -func explorerBundleMaterializer(recipeEngine *engine.Engine, target publication.Target, registry *materializationarango.Registry, degradation error, logger *slog.Logger, batchRows, batchBytes int) graphresolver.ExplorerBundleMaterializer { +func explorerBundleMaterializer(recipeEngine *dataframeexecution.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 { + _, err := recipeEngine.MaterializeBundle(ctx, bundle, bindings, func(run context.Context, full dataframeexecution.Resolved) error { var publishErr error identity, publishErr = publishResolvedRecipe(run, recipeEngine, target, bundle.Name, bindings, full, batchRows, batchBytes) return publishErr diff --git a/internal/server/explorer_receipt_contract.go b/internal/server/explorer_receipt_contract.go index 51f002e5..14acc5d7 100644 --- a/internal/server/explorer_receipt_contract.go +++ b/internal/server/explorer_receipt_contract.go @@ -8,8 +8,8 @@ import ( "sort" "strings" + 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" explorercompilation "github.com/calypr/loom/internal/explorer/compilation" @@ -196,7 +196,7 @@ func firstNonEmptyWorkspaceTitle(values ...string) string { // 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") } @@ -248,18 +248,18 @@ func validateReceiptResolution(receipt *explorer.CompilationReceipt, resolved *e // 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 + return dataframeexecution.Resolved{}, err } if err := validateReceiptEnginePublicColumns(receipt, resolved); err != nil { - return engine.Resolved{}, contractMismatch("public_columns", "", "receipt public columns", err.Error()) + 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 510d4744..952cf3bf 100644 --- a/internal/server/explorer_receipt_contract_test.go +++ b/internal/server/explorer_receipt_contract_test.go @@ -12,8 +12,8 @@ import ( "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" @@ -22,7 +22,7 @@ import ( ) 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 @@ -86,7 +86,7 @@ func TestCompileValidatedReceiptResolutionChecksAllOutputsForScopedPreview(t *te } func TestCompileValidatedReceiptResolutionSurvivesCompilerMetadataJSONRoundTrip(t *testing.T) { - recipeEngine, err := engine.New(engine.Config{Registry: compilerTestRegistry{}, QueryRows: func(context.Context, string, int, map[string]any, func(map[string]any) error) error { return nil }}) + 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) } @@ -132,7 +132,7 @@ func TestCompileValidatedReceiptResolutionSurvivesCompilerMetadataJSONRoundTrip( } func TestResolvedOutputFingerprintExcludesOptimizerAndTransientProvenance(t *testing.T) { - recipeEngine, err := engine.New(engine.Config{Registry: compilerTestRegistry{}, QueryRows: func(context.Context, string, int, map[string]any, func(map[string]any) error) error { return nil }}) + 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) } @@ -243,15 +243,15 @@ func TestNativeV2RouteUsesAuthorizedPersistedReceipt(t *testing.T) { } 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() diff --git a/internal/server/openapi_explorer_authoring_direct.go b/internal/server/openapi_explorer_authoring_direct.go index a5427310..fbf76ede 100644 --- a/internal/server/openapi_explorer_authoring_direct.go +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -10,7 +10,7 @@ import ( loomapi "github.com/calypr/loom/generated/loomapi" "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" "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/explorer/lifecycle" @@ -154,7 +154,7 @@ func (h *explorerHTTPHandlers) previewAuthoringDirect(ctx context.Context, proje if body == nil { return result, malformedRouteError("preview", errors.New("receiptId and outputId are required")) } - limit := engine.DefaultPreviewLimit + limit := dataframeexecution.DefaultPreviewLimit if body.Limit != nil { limit = *body.Limit if limit == 0 { diff --git a/internal/server/recipe_publication.go b/internal/server/recipe_publication.go index ed567f8b..1ae4f0ec 100644 --- a/internal/server/recipe_publication.go +++ b/internal/server/recipe_publication.go @@ -13,11 +13,11 @@ import ( 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 +31,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 @@ -74,7 +74,7 @@ func recipeOutputLogicalColumns(plan engine.Resolved, outputName string) []publi 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 +83,7 @@ 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 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 @@ -129,14 +129,14 @@ 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 { +func recipeResolvedDynamicColumnCounts(plan dataframeexecution.Resolved) map[string]int { counts := make(map[string]int, len(plan.Semantic.ResolvedColumns)) for dynamicMap, columns := range plan.Semantic.ResolvedColumns { counts[dynamicMap] = len(columns) @@ -144,7 +144,7 @@ func recipeResolvedDynamicColumnCounts(plan engine.Resolved) map[string]int { 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) { +func recipeMaterializer(recipeEngine *dataframeexecution.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 @@ -155,7 +155,7 @@ func recipeMaterializer(recipeEngine *engine.Engine, bundleTarget publication.Ta } bindings.IncludeAuthResourcePath = true var identity publication.BundleIdentity - _, err := recipeEngine.Materialize(ctx, name, bindings, func(ctx context.Context, full engine.Resolved) error { + _, err := recipeEngine.Materialize(ctx, name, bindings, func(ctx context.Context, full dataframeexecution.Resolved) error { logger.Info("recipe materialization resolved", "name", name, "project", bindings.Project, @@ -167,7 +167,7 @@ func recipeMaterializer(recipeEngine *engine.Engine, bundleTarget publication.Ta return err }) if err != nil { - var dynamicDrift *engine.DynamicDriftError + var dynamicDrift *dataframeexecution.DynamicDriftError if errors.As(err, &dynamicDrift) { logger.Error("recipe materialization dynamic schema drift", "name", name, @@ -207,7 +207,7 @@ func recipeMaterializer(recipeEngine *engine.Engine, bundleTarget publication.Ta } } -func recipePublicationProcessor(recipeEngine *engine.Engine, logger *slog.Logger, batchRows, batchBytes int) publicationclickhouse.ExecutionProcessor { +func recipePublicationProcessor(recipeEngine *dataframeexecution.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. @@ -221,7 +221,7 @@ func recipePublicationProcessor(recipeEngine *engine.Engine, logger *slog.Logger 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 { + _, err := recipeEngine.MaterializeVersion(ctx, execution.Name, execution.TranslationVersion, bindings, func(ctx context.Context, full dataframeexecution.Resolved) error { _, publishErr := publishResolvedRecipe(ctx, recipeEngine, target, execution.Name, bindings, full, batchRows, batchBytes) return publishErr }) diff --git a/internal/server/server.go b/internal/server/server.go index b4d48fc0..5af3fe1a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -24,15 +24,14 @@ 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" @@ -141,7 +140,7 @@ 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 { + 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 lifecycleClient.QueryRows(ctx, query, batch, binds, visit) }}) // The lifecycle client already owns this Arango database. Reusing it avoids @@ -176,7 +175,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), @@ -314,14 +313,14 @@ 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}, @@ -470,19 +469,19 @@ func run(ctx context.Context, serverConfig Config) error { 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} + return dataframeexecution.PreviewSummary{}, &receiptPreviewResolutionError{Err: 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), 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" From 54056172090e1c79c45be1cdeba8633fbc5b3302 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 16:50:28 -0700 Subject: [PATCH 07/17] refactor(server): remove federation inventory wiring --- .../bulk/load/memory_lifecycle_store_test.go | 28 --- .../dataset/arango/snapshot_release_store.go | 23 --- internal/dataset/inventory.go | 71 ------- internal/dataset/memory_store_test.go | 20 -- internal/dataset/release.go | 1 - internal/dataset/release_test.go | 20 -- internal/dataset/snapshot.go | 1 - internal/server/federation_status.go | 179 ------------------ internal/server/federation_status_test.go | 112 ----------- internal/server/release_verifier.go | 31 --- internal/server/server.go | 17 +- 11 files changed, 1 insertion(+), 502 deletions(-) delete mode 100644 internal/dataset/inventory.go delete mode 100644 internal/server/federation_status.go delete mode 100644 internal/server/federation_status_test.go diff --git a/internal/api/bulk/load/memory_lifecycle_store_test.go b/internal/api/bulk/load/memory_lifecycle_store_test.go index ffacf30a..165548a4 100644 --- a/internal/api/bulk/load/memory_lifecycle_store_test.go +++ b/internal/api/bulk/load/memory_lifecycle_store_test.go @@ -134,16 +134,6 @@ func (s *memoryLifecycleStore) TransitionSnapshot(_ context.Context, ref dataset 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() @@ -169,16 +159,6 @@ func (s *memoryLifecycleStore) CompareAndSwapActivateRelease(_ context.Context, 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...) @@ -207,14 +187,6 @@ func includesResource(values []string, target string) bool { } 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/dataset/arango/snapshot_release_store.go b/internal/dataset/arango/snapshot_release_store.go index 495c19eb..cff91647 100644 --- a/internal/dataset/arango/snapshot_release_store.go +++ b/internal/dataset/arango/snapshot_release_store.go @@ -91,10 +91,6 @@ func (s *Store) TransitionSnapshot(ctx context.Context, ref dataset.Ref, expecte 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 { @@ -204,10 +200,6 @@ func (s *Store) CompareAndSwapActivateRelease(ctx context.Context, release datas 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{ @@ -296,18 +288,6 @@ func (s *Store) snapshotRows(ctx context.Context, query string, binds map[string 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 { @@ -380,9 +360,6 @@ FOR snapshot 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) 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..38142568 100644 --- a/internal/dataset/memory_store_test.go +++ b/internal/dataset/memory_store_test.go @@ -130,16 +130,6 @@ func (s *MemoryLifecycleStore) TransitionSnapshot(_ context.Context, ref Ref, ex 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,16 +158,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...) diff --git a/internal/dataset/release.go b/internal/dataset/release.go index 42d1249a..539b9ab6 100644 --- a/internal/dataset/release.go +++ b/internal/dataset/release.go @@ -94,7 +94,6 @@ type ReleaseRepository interface { 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 { diff --git a/internal/dataset/release_test.go b/internal/dataset/release_test.go index 4b7ef995..5ed490a1 100644 --- a/internal/dataset/release_test.go +++ b/internal/dataset/release_test.go @@ -277,26 +277,6 @@ func TestActivationRetryOfSameReleaseIsIdempotent(t *testing.T) { } } -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) diff --git a/internal/dataset/snapshot.go b/internal/dataset/snapshot.go index 258e0ac6..edb514a7 100644 --- a/internal/dataset/snapshot.go +++ b/internal/dataset/snapshot.go @@ -153,5 +153,4 @@ type SnapshotRepository interface { 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/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/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/server.go b/internal/server/server.go index 5af3fe1a..6a8ee6c8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -220,7 +220,7 @@ func run(ctx context.Context, serverConfig Config) error { } } } - verificationStore := publicationVerificationStore{executions: publishedRegistry, query: lifecycleClient} + verificationStore := publicationVerificationStore{executions: publishedRegistry} releaseService := &publicationcontract.ReleaseService{Snapshots: lifecycleStore, 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) @@ -239,20 +239,6 @@ func run(ctx context.Context, serverConfig Config) error { 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) { @@ -329,7 +315,6 @@ func run(ctx context.Context, serverConfig Config) error { RecipeMaterialize: recipeMaterializer(recipeEngine, bundleTarget, publishedRegistry, degradation, logger, serverConfig.Server.RecipeBatchRows, serverConfig.Server.RecipeBatchBytes), ExactMaterializationStarter: exactStarter, ProjectReleaseActivator: releaseActivator, - CandidateProjects: candidateProjects, }) ingestRunner := loadapi.IngestRunner{BaseOptions: ingest.LoadOptions{ ConnectionOptions: connOpts, From 4e15ead6f598163183b332cd60c18f21956aea2b Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 19:11:41 -0700 Subject: [PATCH 08/17] cleanup a bunch of old cruft --- README.md | 21 +- docs/CODE_GENERATION.md | 4 +- docs/DATAFRAME_FEDERATION.md | 254 --- docs/DEVELOPER_ARCHITECTURE.md | 9 +- docs/GRAPHQL_API.md | 4 +- docs/README.md | 2 - docs/SIMP.md | 359 ++++ docs/loom-reliability-contract.md | 24 +- .../graph/executor/prelude.generated.go | 18 - .../graphql/graph/executor/root_.generated.go | 332 +--- .../graph/executor/schema.generated.go | 1522 +++-------------- generated/graphql/graph/model/models.go | 266 +-- .../api/bulk/load/generation_route_test.go | 8 +- internal/api/bulk/load/resource_route_test.go | 6 +- internal/api/bulk/load/service.go | 30 +- internal/api/bulk/load/service_test.go | 20 +- internal/api/bulk/load/snapshot_route_test.go | 2 +- internal/api/bulk/load/snapshot_service.go | 4 +- .../api/bulk/load/snapshot_service_test.go | 4 + .../graph/aggregate_http_integration_test.go | 18 +- .../aggregates.go} | 137 +- .../aggregates_test.go} | 2 +- .../{materialization => dataframe}/export.go | 23 +- .../export_test.go | 2 +- .../filter_test.go | 2 +- .../output.go => dataframe/model.go} | 50 +- .../model_test.go} | 17 +- internal/api/graphql/graph/dataframe/rows.go | 139 ++ .../api/graphql/graph/dataframe/service.go | 172 ++ .../graphql/graph/dataframe/service_test.go | 71 + .../graph/materialization/operation_cache.go | 107 -- .../materialization/operation_cache_test.go | 45 - .../graphql/graph/materialization/service.go | 528 ------ .../graph/materialization/service_test.go | 69 - .../graph/resolver/operation_context.go | 4 +- .../graph/resolver/recipe_control_graphql.go | 8 +- .../graph/resolver/recipe_control_test.go | 43 - .../api/graphql/graph/resolver/resolver.go | 68 +- .../graph/resolver/schema.resolvers.go | 108 +- .../api/graphql/graph/schema/schema.graphqls | 66 +- internal/catalog/arango/codec.go | 153 ++ internal/catalog/arango/evidence.go | 114 ++ internal/catalog/arango/fields.go | 158 ++ internal/catalog/arango/relationships.go | 265 +++ internal/catalog/arango/resources.go | 117 ++ internal/catalog/arango/store.go | 767 +-------- internal/catalog/write_persist.go | 15 +- .../dataframe/compiler/capability/probe.go | 41 +- internal/dataframe/compiler/compiled_query.go | 2 - internal/dataframe/compiler/ir/clone.go | 39 +- internal/dataframe/compiler/ir/lookup_test.go | 56 - .../compiler/ir/physical_operations.go | 7 +- .../compiler/ir/physical_validation.go | 8 - .../compiler/ir/physical_validation_expr.go | 58 - .../dataframe/compiler/ir/physical_values.go | 33 +- internal/dataframe/compiler/ir/policy.go | 19 +- .../compiler/lower/plan_orchestration.go | 17 - .../dataframe/compiler/lower/traversal.go | 6 +- .../compiler/lower/traversal_test.go | 11 +- .../dataframe/compiler/optimize/keyed_map.go | 134 -- .../dataframe/compiler/optimize/optimize.go | 7 - internal/dataframe/compiler/optimizer.go | 7 +- .../compiler/physical_diagnostics_test.go | 5 - .../compiler/physical_optimize_test.go | 50 +- .../dataframe/compiler/physical_plan_test.go | 6 +- .../compiler/physical_scope_constants.go | 14 - .../dataframe/compiler/physical_scope_test.go | 6 +- .../compiler/render/aql/lookup_test.go | 53 - .../compiler/render/aql/validation.go | 2 +- .../dataframe/compiler/render/aql/values.go | 35 - internal/dataframe/errors/errors.go | 3 - internal/dataframe/errors/errors_test.go | 2 +- internal/dataframe/execution/engine.go | 19 +- internal/dataframe/execution/engine_test.go | 4 + .../publication/arango/bundle_registry.go | 78 +- .../arango/bundle_registry_test.go | 42 + .../dataframe/publication/arango/registry.go | 7 +- internal/dataframe/publication/bundle.go | 28 - .../publication/clickhouse/bundle_store.go | 227 ++- .../clickhouse/clickhouse_bundle_test.go | 117 +- .../publication/clickhouse/target.go | 206 --- .../publication/clickhouse/target_test.go | 39 - .../publication/clickhouse/worker.go | 275 --- .../publication/clickhouse/worker_test.go | 272 --- internal/dataframe/publication/runner.go | 41 +- internal/dataframe/publication/runner_test.go | 10 +- internal/dataframe/publication/types.go | 23 +- .../aggregate_clickhouse_integration_test.go | 33 +- .../dataframe/published/aggregate_execute.go | 12 +- .../published/aggregate_execute_test.go | 6 +- .../dataframe/published/aggregate_plan.go | 12 +- .../published/aggregate_plan_test.go | 18 +- internal/dataframe/published/aggregate_sql.go | 61 +- .../dataframe/published/aggregate_types.go | 16 +- internal/dataframe/published/current.go | 164 ++ internal/dataframe/published/current_test.go | 61 + internal/dataframe/published/errors.go | 12 + internal/dataframe/published/federation.go | 733 -------- .../dataframe/published/federation_query.go | 351 ---- .../dataframe/published/federation_test.go | 283 --- internal/dataframe/published/page.go | 291 ++++ internal/dataframe/published/page_test.go | 42 + internal/dataframe/published/query_helpers.go | 11 + internal/dataframe/published/read.go | 33 +- internal/dataframe/published/types.go | 42 +- .../dataframe/recipe/exec/arango/registry.go | 7 +- internal/dataframe/recipe/exec/persistent.go | 31 +- internal/dataframe/semantic/semantic_nodes.go | 2 - internal/dataset/arango/manifest_lifecycle.go | 10 +- .../dataset/arango/snapshot_release_store.go | 10 +- internal/dataset/arango/store_test.go | 2 +- internal/dataset/dataframe_selector.go | 2 +- internal/explorer/arango/activation.go | 220 +++ internal/explorer/arango/codec.go | 66 + internal/explorer/arango/drafts.go | 30 + internal/explorer/arango/explorers.go | 48 + internal/explorer/arango/publication.go | 77 + internal/explorer/arango/receipts.go | 80 + internal/explorer/arango/repository.go | 40 + internal/explorer/arango/revisions.go | 62 + internal/explorer/arango/store.go | 559 +----- internal/explorer/capability/builder.go | 87 +- internal/explorer/capability/builder_test.go | 78 +- internal/explorer/lifecycle/service.go | 57 +- internal/explorer/lifecycle/service_test.go | 77 +- internal/explorer/lifecycle/types.go | 9 +- internal/explorer/service.go | 9 +- internal/explorer/store.go | 6 +- internal/fhir/schema/compiler_semantics.go | 3 - internal/server/config_test.go | 4 +- .../server/explorer_authoring_v2_routes.go | 11 +- internal/server/explorer_capability.go | 60 +- internal/server/explorer_capability_test.go | 6 +- internal/server/explorer_config_route.go | 11 +- internal/server/explorer_config_route_test.go | 5 +- internal/server/explorer_lifecycle_adapter.go | 16 +- internal/server/explorer_lifecycle_types.go | 7 +- internal/server/explorer_preview_response.go | 77 - .../server/explorer_preview_response_test.go | 3 - internal/server/explorer_publication.go | 29 - .../explorer_route_test_helpers_test.go | 5 +- internal/server/http_test_helpers_test.go | 4 + .../openapi_explorer_authoring_direct.go | 10 +- internal/server/options.go | 24 - internal/server/recipe_publication.go | 104 -- internal/server/routes_test.go | 6 +- internal/server/server.go | 93 +- internal/store/arango/client.go | 26 +- 148 files changed, 4061 insertions(+), 8359 deletions(-) delete mode 100644 docs/DATAFRAME_FEDERATION.md create mode 100644 docs/SIMP.md rename internal/api/graphql/graph/{materialization/aggregate_batch.go => dataframe/aggregates.go} (70%) rename internal/api/graphql/graph/{materialization/aggregate_batch_test.go => dataframe/aggregates_test.go} (98%) rename internal/api/graphql/graph/{materialization => dataframe}/export.go (86%) rename internal/api/graphql/graph/{materialization => dataframe}/export_test.go (98%) rename internal/api/graphql/graph/{materialization => dataframe}/filter_test.go (97%) rename internal/api/graphql/graph/{materialization/output.go => dataframe/model.go} (69%) rename internal/api/graphql/graph/{materialization/output_test.go => dataframe/model_test.go} (60%) create mode 100644 internal/api/graphql/graph/dataframe/rows.go create mode 100644 internal/api/graphql/graph/dataframe/service.go create mode 100644 internal/api/graphql/graph/dataframe/service_test.go delete mode 100644 internal/api/graphql/graph/materialization/operation_cache.go delete mode 100644 internal/api/graphql/graph/materialization/operation_cache_test.go delete mode 100644 internal/api/graphql/graph/materialization/service.go delete mode 100644 internal/api/graphql/graph/materialization/service_test.go create mode 100644 internal/catalog/arango/codec.go create mode 100644 internal/catalog/arango/evidence.go create mode 100644 internal/catalog/arango/fields.go create mode 100644 internal/catalog/arango/relationships.go create mode 100644 internal/catalog/arango/resources.go delete mode 100644 internal/dataframe/compiler/ir/lookup_test.go delete mode 100644 internal/dataframe/compiler/optimize/keyed_map.go delete mode 100644 internal/dataframe/compiler/physical_scope_constants.go delete mode 100644 internal/dataframe/compiler/render/aql/lookup_test.go delete mode 100644 internal/dataframe/publication/clickhouse/target.go delete mode 100644 internal/dataframe/publication/clickhouse/target_test.go delete mode 100644 internal/dataframe/publication/clickhouse/worker.go delete mode 100644 internal/dataframe/publication/clickhouse/worker_test.go create mode 100644 internal/dataframe/published/current.go create mode 100644 internal/dataframe/published/current_test.go delete mode 100644 internal/dataframe/published/federation.go delete mode 100644 internal/dataframe/published/federation_query.go delete mode 100644 internal/dataframe/published/federation_test.go create mode 100644 internal/dataframe/published/page.go create mode 100644 internal/dataframe/published/page_test.go create mode 100644 internal/dataframe/published/query_helpers.go create mode 100644 internal/explorer/arango/activation.go create mode 100644 internal/explorer/arango/codec.go create mode 100644 internal/explorer/arango/drafts.go create mode 100644 internal/explorer/arango/explorers.go create mode 100644 internal/explorer/arango/publication.go create mode 100644 internal/explorer/arango/receipts.go create mode 100644 internal/explorer/arango/repository.go create mode 100644 internal/explorer/arango/revisions.go diff --git a/README.md b/README.md index e6f6927f..dacb23f5 100644 --- a/README.md +++ b/README.md @@ -239,17 +239,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 +305,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/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/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/DEVELOPER_ARCHITECTURE.md b/docs/DEVELOPER_ARCHITECTURE.md index da65610a..1884c44e 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: @@ -75,9 +75,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 @@ -209,7 +208,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 ``` 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/README.md b/docs/README.md index be1638f2..9ccad377 100644 --- a/docs/README.md +++ b/docs/README.md @@ -32,8 +32,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/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/loom-reliability-contract.md b/docs/loom-reliability-contract.md index fba7e040..2a1a120b 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,7 +10,7 @@ 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 @@ -26,9 +26,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,18 +65,10 @@ 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. - -Operator operations support: - -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. +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. ## Snapshot HTTP API @@ -101,7 +90,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 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/internal/api/bulk/load/generation_route_test.go b/internal/api/bulk/load/generation_route_test.go index 94caef31..e0453567 100644 --- a/internal/api/bulk/load/generation_route_test.go +++ b/internal/api/bulk/load/generation_route_test.go @@ -22,9 +22,13 @@ func (r *fakeGenerationRunner) RunGeneration(_ context.Context, req GenerationLo return r.summary, nil } +func (r *fakeGenerationRunner) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { + return ingest.LoadSummary{}, nil +} + func TestCreateGenerationStagesCompleteBundle(t *testing.T) { runner := &fakeGenerationRunner{summary: ingest.LoadSummary{Files: 2, VerticesInserted: 4}} - svc, err := NewService(ServiceConfig{GenerationRunner: runner}) + svc, err := NewService(ServiceConfig{Loader: runner}) if err != nil { t.Fatal(err) } @@ -51,7 +55,7 @@ func TestCreateGenerationStagesCompleteBundle(t *testing.T) { func TestCreateGenerationPropagatesDeferredActivation(t *testing.T) { runner := &fakeGenerationRunner{} - svc, err := NewService(ServiceConfig{GenerationRunner: runner}) + svc, err := NewService(ServiceConfig{Loader: runner}) if err != nil { t.Fatal(err) } diff --git a/internal/api/bulk/load/resource_route_test.go b/internal/api/bulk/load/resource_route_test.go index 792c0451..64fc99d7 100644 --- a/internal/api/bulk/load/resource_route_test.go +++ b/internal/api/bulk/load/resource_route_test.go @@ -32,9 +32,13 @@ func (r *capturingResourceRunner) Run(_ context.Context, req ImportRequest, _ in return r.summary, nil } +func (r *capturingResourceRunner) RunGeneration(context.Context, GenerationLoadRequest, ingest.EventSink) (ingest.LoadSummary, error) { + return ingest.LoadSummary{}, nil +} + func TestResourceRouteUsesPathIdentityAndMultipartFile(t *testing.T) { runner := &capturingResourceRunner{summary: ingest.LoadSummary{Files: 1, VerticesInserted: 2}} - service, err := NewService(ServiceConfig{Runner: runner}) + service, err := NewService(ServiceConfig{Loader: runner}) if err != nil { t.Fatal(err) } diff --git a/internal/api/bulk/load/service.go b/internal/api/bulk/load/service.go index 420fd8df..44e8e08e 100644 --- a/internal/api/bulk/load/service.go +++ b/internal/api/bulk/load/service.go @@ -61,12 +61,11 @@ 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 { +// Loader is the single ingest boundary. Resource and generation loads are two +// request shapes over the same configured backend. +type Loader interface { Run(ctx context.Context, req ImportRequest, sink ingest.EventSink) (ingest.LoadSummary, error) + RunGeneration(ctx context.Context, req GenerationLoadRequest, sink ingest.EventSink) (ingest.LoadSummary, error) } type IngestRunner struct { @@ -101,8 +100,7 @@ func (r IngestRunner) RunGeneration(ctx context.Context, req GenerationLoadReque } type ServiceConfig struct { - Runner Runner - GenerationRunner GenerationRunner + Loader Loader Logger *slog.Logger OnSuccess func(project string) GenerationActivator GenerationActivator @@ -110,8 +108,7 @@ type ServiceConfig struct { } type Service struct { - runner Runner - generationRunner GenerationRunner + loader Loader logger *slog.Logger onSuccess func(project string) generationActivator GenerationActivator @@ -119,15 +116,14 @@ type Service struct { } func NewService(cfg ServiceConfig) (*Service, error) { - if cfg.Runner == nil && cfg.GenerationRunner == nil { + if cfg.Loader == 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, + loader: cfg.Loader, logger: cfg.Logger, onSuccess: cfg.OnSuccess, generationActivator: cfg.GenerationActivator, @@ -136,9 +132,6 @@ func NewService(cfg ServiceConfig) (*Service, error) { } func (s *Service) Run(ctx context.Context, req ImportRequest) (*ImportResult, error) { - if s.runner == nil { - return nil, errors.New("resource runner is not configured") - } if req.Project == "" { return nil, errors.New("project is required") } @@ -149,7 +142,7 @@ func (s *Service) Run(ctx context.Context, req ImportRequest) (*ImportResult, er return nil, errors.New("staged file path is required") } - summary, err := s.runner.Run(ctx, req, nil) + summary, err := s.loader.Run(ctx, req, nil) if err != nil { s.logger.Error("resource load failed", "project", req.Project, "resource_type", req.ResourceType, "error", err.Error()) return nil, err @@ -170,9 +163,6 @@ func (s *Service) Run(ctx context.Context, req ImportRequest) (*ImportResult, er } func (s *Service) RunGeneration(ctx context.Context, req GenerationLoadRequest) (*GenerationLoadResult, error) { - if s.generationRunner == nil { - return nil, errors.New("generation runner is not configured") - } if req.Project == "" || req.Generation == "" || req.StagedDir == "" { return nil, errors.New("project, generation, and staged directory are required") } @@ -196,7 +186,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.loader.RunGeneration(ctx, req, nil) if err != nil { s.logger.Error("generation load failed", "project", req.Project, "generation", req.Generation, "error", err.Error()) return nil, err diff --git a/internal/api/bulk/load/service_test.go b/internal/api/bulk/load/service_test.go index bed03bf0..0a0be8e8 100644 --- a/internal/api/bulk/load/service_test.go +++ b/internal/api/bulk/load/service_test.go @@ -18,6 +18,10 @@ func (activationGenerationRunner) RunGeneration(context.Context, GenerationLoadR return ingest.LoadSummary{}, nil } +func (activationGenerationRunner) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { + return ingest.LoadSummary{}, nil +} + type activationManifestStore struct { manifest publication.Manifest readErr error @@ -64,7 +68,7 @@ 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{Loader: runner}) if err != nil { t.Fatal(err) } @@ -93,11 +97,15 @@ func (r *activationCapturingRunner) RunGeneration(_ context.Context, request Gen return ingest.LoadSummary{}, nil } +func (r *activationCapturingRunner) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { + return ingest.LoadSummary{}, nil +} + 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{Loader: runner, GenerationActivator: store}) if err != nil { t.Fatal(err) } @@ -118,7 +126,7 @@ func TestRunGenerationReusesReadyManifest(t *testing.T) { 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{Loader: runner, GenerationActivator: store}) if err != nil { t.Fatal(err) } @@ -136,7 +144,7 @@ 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{Loader: runner, GenerationActivator: store}) if err != nil { t.Fatal(err) } @@ -186,7 +194,7 @@ 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}) + svc, err := NewService(ServiceConfig{Loader: activationGenerationRunner{}, GenerationActivator: activator, DataframeReleases: releases}) if err != nil { t.Fatal(err) } @@ -213,7 +221,7 @@ func TestActivateGenerationActivatesPublishedAllOutputBundle(t *testing.T) { activator := &activationManifestStore{manifest: manifest} calledSuccess := false svc, err := NewService(ServiceConfig{ - GenerationRunner: activationGenerationRunner{}, + Loader: activationGenerationRunner{}, GenerationActivator: activator, DataframeReleases: &activationReleaseStore{execution: execution, pointer: dataframepublication.BundlePointer{Name: execution.PointerName(), ExecutionID: execution.ID}}, OnSuccess: func(string) { calledSuccess = true }, diff --git a/internal/api/bulk/load/snapshot_route_test.go b/internal/api/bulk/load/snapshot_route_test.go index ae3ac229..ea9f0ff7 100644 --- a/internal/api/bulk/load/snapshot_route_test.go +++ b/internal/api/bulk/load/snapshot_route_test.go @@ -31,7 +31,7 @@ func TestSnapshotHTTPContractCreateUploadFinalizeStatusAndAbort(t *testing.T) { 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}) + baseService, err := NewService(ServiceConfig{Loader: runner}) if err != nil { t.Fatal(err) } diff --git a/internal/api/bulk/load/snapshot_service.go b/internal/api/bulk/load/snapshot_service.go index 04ae0eb0..14df2d73 100644 --- a/internal/api/bulk/load/snapshot_service.go +++ b/internal/api/bulk/load/snapshot_service.go @@ -24,7 +24,7 @@ type SnapshotBlobStore interface { type SnapshotService struct { Repository dataset.SnapshotRepository Blobs SnapshotBlobStore - Runner GenerationRunner + Runner Loader Now func() time.Time locks sync.Map } @@ -173,7 +173,7 @@ func (s *SnapshotService) Abort(ctx context.Context, project, generation string) return aborted, nil } -func runGeneration(runner GenerationRunner, ctx context.Context, request GenerationLoadRequest) (*GenerationLoadResult, error) { +func runGeneration(runner Loader, ctx context.Context, request GenerationLoadRequest) (*GenerationLoadResult, error) { summary, err := runner.RunGeneration(ctx, request, nil) if err != nil { return nil, err diff --git a/internal/api/bulk/load/snapshot_service_test.go b/internal/api/bulk/load/snapshot_service_test.go index 63fef276..bd286ca3 100644 --- a/internal/api/bulk/load/snapshot_service_test.go +++ b/internal/api/bulk/load/snapshot_service_test.go @@ -35,6 +35,10 @@ func (f *snapshotRunnerFixture) RunGeneration(_ context.Context, request Generat return ingest.LoadSummary{Files: 1}, f.err } +func (f *snapshotRunnerFixture) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { + return ingest.LoadSummary{}, nil +} + func TestSnapshotServiceSafeRetryFinalizeAndNoImplicitActivation(t *testing.T) { store := newMemoryLifecycleStore() runner := &snapshotRunnerFixture{} diff --git a/internal/api/graphql/graph/aggregate_http_integration_test.go b/internal/api/graphql/graph/aggregate_http_integration_test.go index 26fd2925..27efcf25 100644 --- a/internal/api/graphql/graph/aggregate_http_integration_test.go +++ b/internal/api/graphql/graph/aggregate_http_integration_test.go @@ -71,7 +71,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 +89,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 +174,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 +208,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 70% rename from internal/api/graphql/graph/materialization/aggregate_batch.go rename to internal/api/graphql/graph/dataframe/aggregates.go index 14b4a5ad..80118939 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: 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: 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 := strings.TrimSpace(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 := 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 69% rename from internal/api/graphql/graph/materialization/output.go rename to internal/api/graphql/graph/dataframe/model.go index 6812c2e0..c4aa6a04 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" @@ -40,49 +40,10 @@ func Model(value materialization.Materialization) *model.DataframeMaterializatio 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 +81,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..fe794d0b --- /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 = strings.TrimSpace(projectID) + if _, err := s.authorizeProject(ctx, principal, projectID); err != nil { + return nil, err + } + values, err := s.reader.CurrentProjectDatasets(ctx, 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..4fadd191 --- /dev/null +++ b/internal/api/graphql/graph/dataframe/service.go @@ -0,0 +1,172 @@ +// 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" +) + +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 = strings.TrimSpace(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) { + if err := authscope.AuthorizeProject(principal, value.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, value.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 = strings.TrimSpace(project) + if _, err := s.authorizeProject(ctx, principal, project); err != nil { + return dfmaterialization.Materialization{}, projectAccess{}, err + } + value, err := s.reader.CurrentProjectDataset(ctx, 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)) +} 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/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/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 32a75397..0bab172e 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_graphql.go +++ b/internal/api/graphql/graph/resolver/recipe_control_graphql.go @@ -11,7 +11,7 @@ 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" @@ -319,11 +319,11 @@ type recipePreviewView struct { 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() { @@ -334,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 abefcad7..7f2de407 100644 --- a/internal/api/graphql/graph/resolver/recipe_control_test.go +++ b/internal/api/graphql/graph/resolver/recipe_control_test.go @@ -10,7 +10,6 @@ import ( "github.com/calypr/loom/internal/dataframe/recipe" "github.com/calypr/loom/internal/dataframe/semantic" "github.com/calypr/loom/internal/dataframe/spec" - dataset "github.com/calypr/loom/internal/dataset" ) type fakeRecipeControl struct { @@ -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 d542021f..08eee183 100644 --- a/internal/api/graphql/graph/resolver/resolver.go +++ b/internal/api/graphql/graph/resolver/resolver.go @@ -5,7 +5,7 @@ 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" @@ -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/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..629d18f7 --- /dev/null +++ b/internal/catalog/arango/fields.go @@ -0,0 +1,158 @@ +package arango + +import ( + "context" + "sort" + + "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 { + return catalog.WriteFieldCatalog(ctx, s.client, collection, docs, batchSize, overwrite, writeAPI, timings) +} + +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..12707ee7 --- /dev/null +++ b/internal/catalog/arango/relationships.go @@ -0,0 +1,265 @@ +package arango + +import ( + "context" + "fmt" + "sort" + "time" + + "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 { + 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 +} + +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 index ea45daaf..e1482427 100644 --- a/internal/catalog/write_persist.go +++ b/internal/catalog/write_persist.go @@ -6,15 +6,10 @@ import ( "time" "github.com/bytedance/sonic" + store "github.com/calypr/loom/internal/store/arango" ) -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 { +func WriteFieldCatalog(ctx context.Context, client store.BatchInserter, collection string, docs []FieldCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { if len(docs) == 0 { return nil } @@ -45,9 +40,7 @@ func WriteFieldCatalog(ctx context.Context, client interface { } // 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 { +func WriteRelationshipCatalog(ctx context.Context, client store.BatchInserter, docs []RelationshipCatalogDocument, batchSize int, overwrite bool, writeAPI string, timings map[string]float64) error { if len(docs) == 0 { return nil } @@ -84,7 +77,7 @@ func WriteRelationshipCatalog(ctx context.Context, client interface { // 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 { +func AccumulateRelationshipCatalog(ctx context.Context, client store.AQLExecutor, docs []RelationshipCatalogDocument, timings map[string]float64) error { if len(docs) == 0 { return nil } diff --git a/internal/dataframe/compiler/capability/probe.go b/internal/dataframe/compiler/capability/probe.go index 267797ed..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 { @@ -243,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} @@ -427,11 +405,6 @@ func compile(ctx context.Context, scope Scope, output semantic.OutputPlan, optio return ir.PhysicalPlan{}, Rendered{}, err } options = options.normalized() - if options.Cost != nil { - if err := options.Cost.Allow(ctx, CostEstimate{TraversalCount: countSemanticTraversals(output.Root)}); err != nil { - return ir.PhysicalPlan{}, Rendered{}, err - } - } context := semantic.ExecutionContext{ Project: scope.Project, DatasetGeneration: scope.DatasetGeneration, @@ -639,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/ir/clone.go b/internal/dataframe/compiler/ir/clone.go index 961a3e5b..33312f5f 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 { @@ -87,9 +83,6 @@ func canonicalizePhysicalOperations(operations []PhysicalOperation) { if operation.Filter != nil && operation.Filter.Expression != nil { canonicalizePhysicalPredicateExpression(operation.Filter.Expression) } - if operation.DerivedLet != nil && operation.DerivedLet.Expression != nil { - canonicalizePhysicalExpression(operation.DerivedLet.Expression) - } if operation.ExpressionLet != nil { canonicalizePhysicalExpression(&operation.ExpressionLet.Expression) } @@ -143,11 +136,6 @@ func canonicalizePhysicalExpression(expression *PhysicalExpression) { canonicalizePhysicalExpression(&expression.Slice.Projections[index].Expression) } } - if expression.Lookup != nil { - canonicalizePhysicalExpression(&expression.Lookup.Source) - canonicalizePhysicalExpression(&expression.Lookup.ItemKey) - canonicalizePhysicalExpression(&expression.Lookup.ItemValue) - } if expression.KeyedMap != nil { canonicalizePhysicalExpression(&expression.KeyedMap.Source) canonicalizePhysicalExpression(&expression.KeyedMap.ItemKey) @@ -233,10 +221,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 { @@ -379,13 +363,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 @@ -424,20 +401,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..3051b9d9 100644 --- a/internal/dataframe/compiler/ir/physical_operations.go +++ b/internal/dataframe/compiler/ir/physical_operations.go @@ -189,10 +189,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..0f0161d3 100644 --- a/internal/dataframe/compiler/ir/physical_validation.go +++ b/internal/dataframe/compiler/ir/physical_validation.go @@ -582,14 +582,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_values.go b/internal/dataframe/compiler/ir/physical_values.go index 9cdd7209..a9ba3cb3 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 @@ -185,23 +179,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/plan_orchestration.go b/internal/dataframe/compiler/lower/plan_orchestration.go index da87c8a7..e5b14dd9 100644 --- a/internal/dataframe/compiler/lower/plan_orchestration.go +++ b/internal/dataframe/compiler/lower/plan_orchestration.go @@ -7,7 +7,6 @@ import ( "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 lowers one canonical output with an @@ -28,13 +27,6 @@ func buildGenericPhysicalPlanWithPolicy(output semanticpkg.OutputPlan, context s if err := semanticpkg.ValidateSemanticGraph(output.Root); err != nil { return ir.PhysicalPlan{}, err } - if !fhirschema.ResourceExists(output.Root.ResourceType) { - return ir.PhysicalPlan{}, fmt.Errorf("root resource type %q is not represented by the generated FHIR schema", output.Root.ResourceType) - } - if err := validateGenericPhysicalNode(output.Root, true); err != nil { - return ir.PhysicalPlan{}, err - } - physical := ir.PhysicalPlan{ Version: 1, Source: ir.PhysicalSource{ @@ -244,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/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..96eb10e6 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) } @@ -301,10 +298,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 62e2b3de..ba1468a9 100644 --- a/internal/dataframe/compiler/physical_diagnostics_test.go +++ b/internal/dataframe/compiler/physical_diagnostics_test.go @@ -44,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_optimize_test.go b/internal/dataframe/compiler/physical_optimize_test.go index cd00ea8c..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.OutputPlan{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) 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_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 68eeb3b4..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) 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 9fb9caf3..00000000 --- a/internal/dataframe/compiler/render/aql/lookup_test.go +++ /dev/null @@ -1,53 +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.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) - } - 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/validation.go b/internal/dataframe/compiler/render/aql/validation.go index f74287c1..9053005a 100644 --- a/internal/dataframe/compiler/render/aql/validation.go +++ b/internal/dataframe/compiler/render/aql/validation.go @@ -176,7 +176,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/errors/errors.go b/internal/dataframe/errors/errors.go index 3a5ec898..066c38dd 100644 --- a/internal/dataframe/errors/errors.go +++ b/internal/dataframe/errors/errors.go @@ -59,7 +59,6 @@ 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" @@ -349,8 +348,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: diff --git a/internal/dataframe/errors/errors_test.go b/internal/dataframe/errors/errors_test.go index 57837618..458cc85f 100644 --- a/internal/dataframe/errors/errors_test.go +++ b/internal/dataframe/errors/errors_test.go @@ -21,7 +21,7 @@ func TestErrorCodesAreUniqueAndStable(t *testing.T) { CodeGenerationLoadIncomplete, CodeGenerationActivationUnknown, CodeInvalidGenerationFile, CodeDuplicateGenerationFile, CodePublicationInProgress, CodePublicationConflict, CodePublicationLeaseLost, CodePublicationFailed, CodeOutputEncodingFailed, - CodeDynamicSchemaDrift, CodeRecipeContractViolation, CodeFederationIncompatible, + CodeDynamicSchemaDrift, CodeRecipeContractViolation, CodeInvalidSelector, CodeReceiptStoreUnavailable, CodePreviewTimeout, CodePreviewResponseTooLarge, } diff --git a/internal/dataframe/execution/engine.go b/internal/dataframe/execution/engine.go index fb6d40d9..7e64e2cc 100644 --- a/internal/dataframe/execution/engine.go +++ b/internal/dataframe/execution/engine.go @@ -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,7 +58,7 @@ 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 @@ -76,7 +67,7 @@ type Config struct { } type Engine struct { - registry Registry + registry exec.Reader revisions recipe.RevisionStore resolveBundle func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (recipe.Bundle, error) queryRows QueryRows @@ -177,11 +168,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 } diff --git a/internal/dataframe/execution/engine_test.go b/internal/dataframe/execution/engine_test.go index b54ad476..60266b06 100644 --- a/internal/dataframe/execution/engine_test.go +++ b/internal/dataframe/execution/engine_test.go @@ -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{}, 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..faed92ef 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,7 +108,7 @@ 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)) } @@ -105,7 +129,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 +148,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 +170,7 @@ type clickHouseBundleTx struct { store *ClickHouseBundleStore execution publication.BundleExecution expectedPointer string + columns map[string][]clickhouse.Column idempotent bool closed bool leaseLost bool @@ -188,6 +192,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 +312,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 +331,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 +420,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 +519,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 +563,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 +613,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 +714,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 +781,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 +812,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..57a62a7d 100644 --- a/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go +++ b/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go @@ -154,6 +154,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 +163,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 +189,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 +223,45 @@ 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 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 +285,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 +333,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 +363,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 +374,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 +386,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 +400,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 +411,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 +443,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 +460,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 +476,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 +492,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 +527,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..3b7746d7 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 { diff --git a/internal/dataframe/publication/runner_test.go b/internal/dataframe/publication/runner_test.go index 8c984aa2..41545152 100644 --- a/internal/dataframe/publication/runner_test.go +++ b/internal/dataframe/publication/runner_test.go @@ -21,13 +21,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 +52,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 diff --git a/internal/dataframe/publication/types.go b/internal/dataframe/publication/types.go index 76663d4d..70012766 100644 --- a/internal/dataframe/publication/types.go +++ b/internal/dataframe/publication/types.go @@ -75,30 +75,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..a1ed4b9b 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 { @@ -140,7 +140,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 +154,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 +205,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 +260,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..c3ec87b3 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) @@ -157,7 +151,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 +174,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/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/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/semantic/semantic_nodes.go b/internal/dataframe/semantic/semantic_nodes.go index 440e2863..f946a379 100644 --- a/internal/dataframe/semantic/semantic_nodes.go +++ b/internal/dataframe/semantic/semantic_nodes.go @@ -56,7 +56,6 @@ type SemanticAggregate struct { Operation string FieldRef string Selector *spec.Selector - PredicateField string Predicate *spec.Selector PredicateEquals string ValueMode string @@ -65,7 +64,6 @@ type SemanticAggregate struct { type SemanticSlice struct { Name string Limit int - PredicateField string Predicate *spec.Selector PredicateEquals string Fields []SemanticField 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/snapshot_release_store.go b/internal/dataset/arango/snapshot_release_store.go index cff91647..819fcfa3 100644 --- a/internal/dataset/arango/snapshot_release_store.go +++ b/internal/dataset/arango/snapshot_release_store.go @@ -8,6 +8,7 @@ import ( "time" "github.com/calypr/loom/internal/dataset" + arangostore "github.com/calypr/loom/internal/store/arango" ) func (s *Store) CreateOrResumeSnapshot(ctx context.Context, candidate dataset.SnapshotGeneration) (dataset.SnapshotGeneration, error) { @@ -164,6 +165,13 @@ func (s *Store) ReadActiveRelease(ctx context.Context, project string) (dataset. } func (s *Store) CompareAndSwapActivateRelease(ctx context.Context, release dataset.ProjectRelease, expectedRevision int64) (dataset.ActiveRelease, error) { + return CompareAndSwapActivateRelease(ctx, s.client, release, expectedRevision) +} + +// CompareAndSwapActivateRelease performs the release visibility switch using +// the supplied transaction-scoped queryer. Explorer publication uses this to +// commit its active revision and 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 @@ -183,7 +191,7 @@ func (s *Store) CompareAndSwapActivateRelease(ctx context.Context, release datas 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 { + err = client.QueryRows(ctx, activateReleaseAQL, metadataBatchSize, binds, func(row map[string]any) error { decoded, err := decodeValue[dataset.ActiveRelease](row) if err != nil { return err diff --git a/internal/dataset/arango/store_test.go b/internal/dataset/arango/store_test.go index 057f36db..90c2f176 100644 --- a/internal/dataset/arango/store_test.go +++ b/internal/dataset/arango/store_test.go @@ -277,7 +277,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/explorer/arango/activation.go b/internal/explorer/arango/activation.go new file mode 100644 index 00000000..d706d23c --- /dev/null +++ b/internal/explorer/arango/activation.go @@ -0,0 +1,220 @@ +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` + +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 + } + 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.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/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/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..41d5622d --- /dev/null +++ b/internal/explorer/arango/explorers.go @@ -0,0 +1,48 @@ +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) 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) +} 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/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..451c221d --- /dev/null +++ b/internal/explorer/arango/repository.go @@ -0,0 +1,40 @@ +package arango + +import ( + "context" + "time" + + "github.com/calypr/loom/internal/explorer" +) + +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 +} diff --git a/internal/explorer/arango/revisions.go b/internal/explorer/arango/revisions.go new file mode 100644 index 00000000..7b6725b8 --- /dev/null +++ b/internal/explorer/arango/revisions.go @@ -0,0 +1,62 @@ +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 +} + +// 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 +} 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/capability/builder.go b/internal/explorer/capability/builder.go index 4cdc4c37..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,20 +91,18 @@ type Builder struct { Complete bool Truncated bool - Resources ResourceInventory - Relationships RelationshipEvidence - Fields FieldEvidence - Compiler CompilerProbe + Evidence Evidence + Compiler CompilerCallbacks } 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) @@ -125,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 @@ -204,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 { @@ -246,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 { @@ -290,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 { @@ -316,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/lifecycle/service.go b/internal/explorer/lifecycle/service.go index d4d40448..8f8cd7c9 100644 --- a/internal/explorer/lifecycle/service.go +++ b/internal/explorer/lifecycle/service.go @@ -253,7 +253,7 @@ func (s *Service) Preview(ctx context.Context, request PreviewRequest) (PreviewR if s.config.Capability.ForExecution == nil { return PreviewResult{}, unavailable("preview", "PREVIEW_UNAVAILABLE", "authorized receipt execution is not configured", nil) } - if s.config.Preview == nil && s.config.PreviewReceipt == 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) @@ -267,33 +267,24 @@ func (s *Service) Preview(ctx context.Context, request PreviewRequest) (PreviewR 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 s.config.PreviewReceipt != nil { - 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 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) || (s.config.PreviewReceipt != nil && validateReceiptOutputContract(receipt, request.OutputID) != nil) { + 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 s.config.PreviewReceipt != nil { - 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 + 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.Rows, err = s.config.Preview(ctx, receipt.Bundle, bindings) + result.Summary, err = s.config.PreviewReceipt(ctx, receipt, bindings, sink) if err != nil { return PreviewResult{}, err } @@ -304,10 +295,10 @@ func (s *Service) Publish(ctx context.Context, request PublishRequest) (PublishR if strings.TrimSpace(request.ReceiptID) == "" { return PublishResult{}, malformed("publish", "receiptId is required", nil) } - if s.config.Materialize == nil && s.config.MaterializeReceipt == nil { + if s.config.MaterializeReceipt == nil { return PublishResult{}, unavailable("publish", "PUBLICATION_UNAVAILABLE", "Explorer publication is not configured", nil) } - if s.config.ActivateRelease == nil || s.config.ValidateReleaseGeneration == nil || (s.config.Capability.Token == nil && s.config.Capability.ForExecution == nil) { + if 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) @@ -344,23 +335,21 @@ func (s *Service) Publish(ctx context.Context, request PublishRequest) (PublishR bindings := recipe.RuntimeBindings{Project: projectid.Legacy(receipt.Project), DatasetGeneration: receipt.SourceGeneration} applyAuthorizedScope(&bindings, authorized, true) var execution Execution - if s.config.MaterializeReceipt != nil { - execution, err = s.config.MaterializeReceipt(ctx, receipt, bindings) - } else { - execution, err = s.config.Materialize(ctx, receipt.Bundle, bindings) - } + execution, err = s.config.MaterializeReceipt(ctx, receipt, bindings) if err != nil { 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) } - if err := s.config.ActivateRelease(ctx, projectid.Legacy(receipt.Project), receipt.SourceGeneration, selectorsForBundle(receipt.Bundle)); err != nil { - return PublishResult{}, unavailable("activation", "MATERIALIZATION_ACTIVATION_FAILED", "dataset release activation failed; the prior Explorer revision was retained", 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_") - revision, err := s.store.PublishAuthoring(ctx, *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: 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}) + revisionValue := 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: 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 } @@ -401,12 +390,10 @@ func (s *Service) PublishRepository(ctx context.Context, request RepositoryPubli bindings := recipe.RuntimeBindings{Project: projectid.Legacy(request.Project), DatasetGeneration: request.Generation} applyAuthorizedScope(&bindings, authorized, true) var execution Execution - if s.config.MaterializeReceipt != nil { - execution, err = s.config.MaterializeReceipt(ctx, receipt, bindings) - } else if s.config.Materialize != nil { - execution, err = s.config.Materialize(ctx, receipt.Bundle, bindings) - } else { + 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 { return RepositoryPublishResult{}, unprocessable("repository_publish", "MATERIALIZATION_FAILED", fmt.Sprintf("materialize repository V2 workspace: %v", err), err) diff --git a/internal/explorer/lifecycle/service_test.go b/internal/explorer/lifecycle/service_test.go index 8d8dc4f6..e1a9f340 100644 --- a/internal/explorer/lifecycle/service_test.go +++ b/internal/explorer/lifecycle/service_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "encoding/json" "errors" "fmt" "sort" @@ -12,6 +13,7 @@ import ( "time" "github.com/calypr/loom/internal/authscope" + 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" @@ -28,6 +30,8 @@ type fakeStore struct { receipt *explorer.CompilationReceipt publishErr error published bool + release dataset.ProjectRelease + revision int64 order *[]string } @@ -70,13 +74,15 @@ func (f *fakeStore) ActiveRevision(context.Context, string, string) (*explorer.R return nil, explorer.ErrNotFound } -func (f *fakeStore) PublishAuthoring(_ context.Context, _ explorer.CompilationReceipt, revision explorer.Revision) (*explorer.Revision, error) { +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 } @@ -122,6 +128,41 @@ func testConfig(snapshot capability.Snapshot) Config { }} } +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, err := New(store, Config{}) @@ -167,7 +208,7 @@ func TestApplyCommandsMapsCASFailures(t *testing.T) { } func TestPreviewRejectsStaleGenerationAndScope(t *testing.T) { - receipt := &explorer.CompilationReceipt{ID: "receipt-a", Project: "project-a", ExplorerID: "patients", SnapshotToken: "token", SourceGeneration: "generation-a", RecipeDigest: "recipe", Bundle: recipe.Bundle{Outputs: []recipe.Output{{Name: "patients"}}}} + receipt := nativeReceipt(readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted})) for _, test := range []struct { name string snapshot capability.Snapshot @@ -179,8 +220,8 @@ func TestPreviewRejectsStaleGenerationAndScope(t *testing.T) { t.Run(test.name, func(t *testing.T) { store := &fakeStore{receipt: receipt} config := testConfig(test.snapshot) - config.Preview = func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (map[string][]map[string]any, error) { - return nil, nil + config.PreviewReceipt = func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { + return dataframeexecution.PreviewSummary{}, nil } service, _ := New(store, config) _, err := service.Preview(context.Background(), PreviewRequest{Project: "project-a", ExplorerID: "patients", ReceiptID: receipt.ID, OutputID: "patients", Limit: 10}) @@ -204,18 +245,18 @@ func TestAuthorizedReceiptExecutionPreservesRestrictedEmptyScope(t *testing.T) { } } -func TestPublishPersistsOnlyAfterActivationAndRetainsRevisionOnFailure(t *testing.T) { +func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { snapshot := readySnapshot("project-a", "generation-a", "token", authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}) - receipt := &explorer.CompilationReceipt{ID: "receipt-a", Project: "project-a", ExplorerID: "patients", SnapshotToken: "token", SourceGeneration: "generation-a", RecipeDigest: "recipe", CompiledConfig: []byte(`{}`), EmittedColumns: []explorer.EmittedColumn{{OutputID: "patients", PublicColumn: "patient_id"}}, Bundle: recipe.Bundle{Outputs: []recipe.Output{{Name: "patients"}}}} + 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.ActivateRelease = func(context.Context, string, string, []dataset.DataframeSelector) error { - order = append(order, "activate-release") - 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.Materialize = func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (Execution, error) { + 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 } @@ -224,15 +265,29 @@ func TestPublishPersistsOnlyAfterActivationAndRetainsRevisionOnFailure(t *testin 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 activate-release persist]"; got != want { + if got, want := fmt.Sprint(order), "[validate-generation materialize prepare-release persist]"; 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.PrepareRelease = func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { + return dataset.ProjectRelease{}, 0, errors.New("release preparation failed") + } + service, _ = New(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 TestServiceUsesConfiguredClock(t *testing.T) { diff --git a/internal/explorer/lifecycle/types.go b/internal/explorer/lifecycle/types.go index a88f9cb2..1adec99c 100644 --- a/internal/explorer/lifecycle/types.go +++ b/internal/explorer/lifecycle/types.go @@ -29,7 +29,7 @@ type Store interface { ApplyWorkspaceCommands(context.Context, string, string, authoringv2.CatalogSnapshot, authoringv2.ApplyCommandsRequest, string) (*authoringv2.ApplyCommandsResponse, error) ActiveRevision(context.Context, string, string) (*explorer.Revision, error) CompilationReceiptForExplorer(context.Context, string, string, string) (*explorer.CompilationReceipt, error) - PublishAuthoring(context.Context, explorer.CompilationReceipt, explorer.Revision) (*explorer.Revision, error) + PublishAuthoring(context.Context, explorer.CompilationReceipt, explorer.Revision, dataset.ProjectRelease, int64) (*explorer.Revision, error) UpsertRepositoryV2(context.Context, explorer.CompilationReceipt, string, string, []explorer.Materialization, explorer.DatasetMetadata, explorer.PublicationMetadata) (*explorer.Explorer, *explorer.Revision, error) FailRevision(context.Context, string, []explorer.Diagnostic) (*explorer.Revision, error) ActivateRepositoryGeneration(context.Context, string, string, string) error @@ -70,7 +70,6 @@ type CompileReceiptRequest struct { type ReceiptCompiler func(context.Context, CompileReceiptRequest) (*explorer.CompilationReceipt, error) type ReceiptReader func(context.Context, string, string, string) (*explorer.CompilationReceipt, error) -type Previewer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (map[string][]map[string]any, 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 @@ -92,10 +91,10 @@ type ExecutionOutput struct { Columns []publication.PhysicalColumn } -type Materializer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (Execution, error) 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. @@ -103,14 +102,13 @@ type Config struct { Capability CapabilityResolver CompileReceipt ReceiptCompiler - Preview Previewer PreviewReceipt ReceiptPreviewer - Materialize Materializer MaterializeReceipt ReceiptMaterializer ReceiptLookup ReceiptReader ValidateReleaseGeneration GenerationValidator ActivateRelease ReleaseActivator + PrepareRelease ReleasePreparer Now func() time.Time } @@ -178,7 +176,6 @@ type PreviewResult struct { Receipt *explorer.CompilationReceipt Columns []explorer.EmittedColumn Summary dataframeexecution.PreviewSummary - Rows map[string][]map[string]any } type PublishRequest struct { diff --git a/internal/explorer/service.go b/internal/explorer/service.go index fbe6cb69..5f952fb9 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" ) @@ -325,15 +326,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 diff --git a/internal/explorer/store.go b/internal/explorer/store.go index 220f42a0..d476332e 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,7 +20,7 @@ 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 { @@ -33,7 +35,7 @@ type Store interface { 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) 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/server/config_test.go b/internal/server/config_test.go index ab4dce57..145075a6 100644 --- a/internal/server/config_test.go +++ b/internal/server/config_test.go @@ -125,13 +125,11 @@ func TestRequiredDataframeSelectorsAndSnapshotRetentionFromEnvironment(t *testin 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" || cfg.Server.SnapshotRetention != 48*time.Hour { t.Fatalf("environment config = %#v", cfg.Server) } if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "dataframer.recipe") { diff --git a/internal/server/explorer_authoring_v2_routes.go b/internal/server/explorer_authoring_v2_routes.go index 62675f41..c7476a7f 100644 --- a/internal/server/explorer_authoring_v2_routes.go +++ b/internal/server/explorer_authoring_v2_routes.go @@ -2,6 +2,7 @@ package server import ( "context" + "errors" "strings" explorerv2api "github.com/calypr/loom/generated/loomapi" @@ -120,7 +121,7 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead return authoringHTTPError(c, malformedRouteError("preview", err)) } var finish func() ([]byte, error) - value, err := application.Preview(previewCtx, lifecycle.PreviewRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), ReceiptID: request.ReceiptId, OutputID: request.OutputId, Limit: previewLimit(request.Limit), SinkFactory: func(receipt *explorer.CompilationReceipt, columns []explorer.EmittedColumn) (func(map[string]any) error, error) { + _, err := application.Preview(previewCtx, lifecycle.PreviewRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), ReceiptID: request.ReceiptId, OutputID: request.OutputId, Limit: previewLimit(request.Limit), SinkFactory: func(receipt *explorer.CompilationReceipt, columns []explorer.EmittedColumn) (func(map[string]any) error, error) { encoder, encoderErr := newPreviewResponseEncoder(receipt, request.OutputId, columns, maxExplorerPreviewResponseBytes) if encoderErr != nil { return nil, encoderErr @@ -131,12 +132,10 @@ func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead if err != nil { return authoringHTTPError(c, err) } - var encoded []byte - if value.Rows != nil { - encoded, err = encodeExplorerPreviewResponse(value.Receipt, request.OutputId, value.Columns, value.Rows[request.OutputId], maxExplorerPreviewResponseBytes) - } else if finish != nil { - encoded, err = finish() + if finish == nil { + return previewRouteFailure(c, errors.New("native preview response sink was not configured")) } + encoded, err := finish() if err != nil { return previewRouteFailure(c, err) } diff --git a/internal/server/explorer_capability.go b/internal/server/explorer_capability.go index 7ef10f08..3e8f2fc1 100644 --- a/internal/server/explorer_capability.go +++ b/internal/server/explorer_capability.go @@ -68,18 +68,6 @@ type ExplorerAuthorizedCapabilityCompilationReader func(context.Context, string, // 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 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") @@ -143,11 +131,13 @@ func (r *explorerCapabilityResolver) Resolve(ctx context.Context, project, reque } 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}, @@ -283,30 +273,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 { @@ -333,10 +307,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 { @@ -366,10 +340,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 9a3d844d..1508711a 100644 --- a/internal/server/explorer_config_route.go +++ b/internal/server/explorer_config_route.go @@ -6,7 +6,6 @@ import ( "net/http" "strings" - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/explorer" "github.com/calypr/loom/internal/explorer/authoringv2" @@ -23,7 +22,6 @@ type explorerHTTPHandlers struct { authorizer authscope.Authorizer authorizeRead explorerConfigReadAuthorizer explorers *explorer.Service - materialize graphresolver.ExplorerBundleMaterializer lifecycleConfig ExplorerV2LifecycleConfig application *lifecycle.Service } @@ -31,14 +29,11 @@ type explorerHTTPHandlers struct { // newExplorerHTTPHandlers wires transport adapters to the single Explorer // application service. HTTP response conversion and deployment adapters stay // in server; lifecycle policy does not. -func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, materialize graphresolver.ExplorerBundleMaterializer, configs ...ExplorerV2LifecycleConfig) *explorerHTTPHandlers { - handlers := &explorerHTTPHandlers{authorizer: authorizer, authorizeRead: authorizeRead, explorers: explorers, materialize: materialize} - config := ExplorerV2LifecycleConfig{Materialize: materialize} +func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, configs ...ExplorerV2LifecycleConfig) *explorerHTTPHandlers { + handlers := &explorerHTTPHandlers{authorizer: authorizer, authorizeRead: authorizeRead, explorers: explorers} + config := ExplorerV2LifecycleConfig{} if len(configs) > 0 { config = configs[0] - if config.Materialize == nil { - config.Materialize = materialize - } } handlers.lifecycleConfig = config handlers.application = newExplorerLifecycleApplication(explorers, config) diff --git a/internal/server/explorer_config_route_test.go b/internal/server/explorer_config_route_test.go index 9f60c80c..a57734f1 100644 --- a/internal/server/explorer_config_route_test.go +++ b/internal/server/explorer_config_route_test.go @@ -49,9 +49,12 @@ func TestRepositoryDeploymentPersistsExecutableDataframeSelectors(t *testing.T) CompileReceipt: func(ctx context.Context, request ExplorerV2ReceiptCompileRequest) (*explorer.CompilationReceipt, error) { return persistTestNativeReceipt(ctx, t, service, request, snapshot) }, + MaterializeReceipt: func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (graphresolver.RecipeExecution, 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) + registerTestExplorerRoutes(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 { diff --git a/internal/server/explorer_lifecycle_adapter.go b/internal/server/explorer_lifecycle_adapter.go index 501e624d..b18d2447 100644 --- a/internal/server/explorer_lifecycle_adapter.go +++ b/internal/server/explorer_lifecycle_adapter.go @@ -42,11 +42,6 @@ func newExplorerLifecycleApplication(store *explorer.Service, config ExplorerV2L return config.CompileReceipt(ctx, request) } } - if config.Preview != nil { - appConfig.Preview = func(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (map[string][]map[string]any, error) { - return config.Preview(ctx, bundle, bindings) - } - } if config.PreviewReceipt != nil { appConfig.PreviewReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { return config.PreviewReceipt(ctx, receipt, bindings, visit) @@ -62,18 +57,17 @@ func newExplorerLifecycleApplication(store *explorer.Service, config ExplorerV2L return config.ActivateRelease(ctx, project, generation, selectors) } } + if config.PrepareRelease != nil { + appConfig.PrepareRelease = func(ctx context.Context, project, generation string, selectors []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { + return config.PrepareRelease(ctx, project, generation, selectors) + } + } if config.AuthorizedCapabilityCompile != nil { appConfig.Capability.ForCompilation = config.AuthorizedCapabilityCompile } if config.AuthorizedCapabilityExecution != nil { appConfig.Capability.ForExecution = config.AuthorizedCapabilityExecution } - if config.Materialize != nil { - appConfig.Materialize = func(ctx context.Context, bundle recipe.Bundle, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { - value, err := config.Materialize(ctx, bundle, bindings) - return toExecution(value), err - } - } if config.MaterializeReceipt != nil { appConfig.MaterializeReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { value, err := config.MaterializeReceipt(ctx, receipt, bindings) diff --git a/internal/server/explorer_lifecycle_types.go b/internal/server/explorer_lifecycle_types.go index 50355a6d..3528b529 100644 --- a/internal/server/explorer_lifecycle_types.go +++ b/internal/server/explorer_lifecycle_types.go @@ -12,9 +12,6 @@ import ( "github.com/calypr/loom/internal/explorer/lifecycle" ) -type ExplorerV2Previewer func(context.Context, recipe.Bundle, recipe.RuntimeBindings) (map[string][]map[string]any, error) -type ExplorerV2Materializer = graphresolver.ExplorerBundleMaterializer - type ExplorerV2ReceiptCompileRequest = lifecycle.CompileReceiptRequest type ExplorerV2ReceiptCompiler = lifecycle.ReceiptCompiler type ExplorerV2ReceiptReader func(context.Context, string, string, string) (*explorer.CompilationReceipt, error) @@ -22,6 +19,7 @@ type ExplorerV2ReceiptPreviewer func(context.Context, *explorer.CompilationRecei 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 ExplorerV2ReleasePreparer func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) type ExplorerV2LifecycleConfig struct { CompileReceipt ExplorerV2ReceiptCompiler @@ -29,12 +27,11 @@ type ExplorerV2LifecycleConfig struct { CapabilityToken ExplorerCapabilityTokenReader AuthorizedCapabilityCompile ExplorerAuthorizedCapabilityCompilationReader AuthorizedCapabilityExecution ExplorerAuthorizedCapabilityExecutionReader - Preview ExplorerV2Previewer PreviewReceipt ExplorerV2ReceiptPreviewer - Materialize ExplorerV2Materializer MaterializeReceipt ExplorerV2ReceiptMaterializer ReceiptLookup ExplorerV2ReceiptReader Logger *slog.Logger ValidateReleaseGeneration ExplorerV2GenerationValidator ActivateRelease ExplorerV2ReleaseActivator + PrepareRelease ExplorerV2ReleasePreparer } diff --git a/internal/server/explorer_preview_response.go b/internal/server/explorer_preview_response.go index cdad9cdc..eab15d09 100644 --- a/internal/server/explorer_preview_response.go +++ b/internal/server/explorer_preview_response.go @@ -113,83 +113,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 242a878a..f205bb07 100644 --- a/internal/server/explorer_preview_response_test.go +++ b/internal/server/explorer_preview_response_test.go @@ -51,9 +51,6 @@ func TestPreviewResponseEncoderRejectsOverflowWithoutResult(t *testing.T) { if !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) { diff --git a/internal/server/explorer_publication.go b/internal/server/explorer_publication.go index 453c42da..18d95224 100644 --- a/internal/server/explorer_publication.go +++ b/internal/server/explorer_publication.go @@ -52,32 +52,3 @@ func explorerReceiptMaterializer(recipeEngine *dataframeexecution.Engine, target 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 *dataframeexecution.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 dataframeexecution.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 - } -} diff --git a/internal/server/explorer_route_test_helpers_test.go b/internal/server/explorer_route_test_helpers_test.go index 285e27fd..c2169b4a 100644 --- a/internal/server/explorer_route_test_helpers_test.go +++ b/internal/server/explorer_route_test_helpers_test.go @@ -1,7 +1,6 @@ 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" @@ -27,8 +26,8 @@ func registerTestExplorerAuthoringRoutes(router fiber.Router, authorizer authsco 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) +func registerTestExplorerRoutes(router fiber.Router, authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, service *explorer.Service, config ExplorerV2LifecycleConfig) { + h := newExplorerHTTPHandlers(authorizer, authorizeRead, service, config) router.Post("/api/v1/projects/:project/generations/:generation/explorer-config", h.publishRepositoryConfig) registerTestExplorerLifecycleRoutes(router, authorizer, authorizeRead, service, config) registerTestExplorerAuthoringRoutes(router, authorizer, authorizeRead, service, config) diff --git a/internal/server/http_test_helpers_test.go b/internal/server/http_test_helpers_test.go index 75bc25e8..7645c77e 100644 --- a/internal/server/http_test_helpers_test.go +++ b/internal/server/http_test_helpers_test.go @@ -57,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 diff --git a/internal/server/openapi_explorer_authoring_direct.go b/internal/server/openapi_explorer_authoring_direct.go index fbf76ede..6308b4cc 100644 --- a/internal/server/openapi_explorer_authoring_direct.go +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -162,7 +162,7 @@ func (h *explorerHTTPHandlers) previewAuthoringDirect(ctx context.Context, proje } } var finish func() ([]byte, error) - value, 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) { + _, 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 @@ -173,12 +173,10 @@ func (h *explorerHTTPHandlers) previewAuthoringDirect(ctx context.Context, proje if err != nil { return result, err } - var encoded []byte - if value.Rows != nil { - encoded, err = encodeExplorerPreviewResponse(value.Receipt, body.OutputId, value.Columns, value.Rows[body.OutputId], maxExplorerPreviewResponseBytes) - } else if finish != nil { - encoded, err = finish() + if finish == nil { + return result, errors.New("native preview response sink was not configured") } + encoded, err := finish() if err != nil { return result, err } diff --git a/internal/server/options.go b/internal/server/options.go index d561f8c5..055618bc 100644 --- a/internal/server/options.go +++ b/internal/server/options.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "os" - "strconv" "strings" "time" @@ -32,8 +31,6 @@ type ServerConfig struct { 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"` } type ClickHouseConfig struct { @@ -72,7 +69,6 @@ func DefaultConfig() Config { 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, }, Auth: AuthConfig{Mode: "basic", Calypr: CalyprAuthConfig{RequestTimeout: 5 * time.Second, CacheTTL: 30 * time.Second}}, } @@ -195,20 +191,6 @@ func applyEnvironment(cfg Config) (Config, error) { 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 } @@ -223,12 +205,6 @@ func (c Config) Validate() error { if cfg.Server.SnapshotRetention <= 0 { return fmt.Errorf("server.snapshot_retention must be positive") } - if cfg.Server.PublicationWorkerLease <= 0 { - return fmt.Errorf("server.publication_worker_lease must be positive") - } - if cfg.Server.PublicationMaxAttempts <= 0 { - return fmt.Errorf("server.publication_max_attempts must be positive") - } for index, selector := range cfg.Server.RequiredDataframeSelectors { if err := selector.Validate(); err != nil { return fmt.Errorf("server.required_dataframe_selectors[%d]: %w", index, err) diff --git a/internal/server/recipe_publication.go b/internal/server/recipe_publication.go index 1ae4f0ec..50b1fcd6 100644 --- a/internal/server/recipe_publication.go +++ b/internal/server/recipe_publication.go @@ -4,19 +4,11 @@ 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" ) @@ -135,99 +127,3 @@ func incrementalPublicationOutput(bindings recipe.RuntimeBindings, streams []dat } return streams[0].Name } - -func recipeResolvedDynamicColumnCounts(plan dataframeexecution.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 *dataframeexecution.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 dataframeexecution.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 *dataframeexecution.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 *dataframeexecution.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 dataframeexecution.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/routes_test.go b/internal/server/routes_test.go index 0255aa25..7d24d38e 100644 --- a/internal/server/routes_test.go +++ b/internal/server/routes_test.go @@ -20,6 +20,10 @@ func (routeRunner) Run(context.Context, loadapi.ImportRequest, ingest.EventSink) return ingest.LoadSummary{}, nil } +func (routeRunner) RunGeneration(context.Context, loadapi.GenerationLoadRequest, ingest.EventSink) (ingest.LoadSummary, error) { + return ingest.LoadSummary{}, nil +} + func TestRegisterRoutesExposesGenerationReleaseWorkflow(t *testing.T) { server, err := api.NewHTTPServer(api.HTTPConfig{ Authenticator: authscope.StaticAuthenticator{}, @@ -28,7 +32,7 @@ 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{Loader: routeRunner{}}) if err != nil { t.Fatal(err) } diff --git a/internal/server/server.go b/internal/server/server.go index 6a8ee6c8..a2ff5f2c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -11,7 +11,6 @@ import ( "log/slog" "os" "os/signal" - "strconv" "strings" "syscall" "time" @@ -23,7 +22,6 @@ import ( "github.com/calypr/loom/internal/authscope" "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" @@ -199,7 +197,6 @@ func run(ctx context.Context, serverConfig Config) error { 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 { @@ -210,14 +207,7 @@ 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} @@ -236,45 +226,23 @@ func run(ctx context.Context, serverConfig Config) error { }) return err } + 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 + } validateExplorerReleaseGeneration := func(ctx context.Context, project, generation string) error { return releaseService.ValidateGeneration(ctx, project, generation) } - 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 - } - } - 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 - } explorerStore, err := explorerarango.New(lifecycleClient) if err != nil { return fmt.Errorf("create Explorer store: %w", err) @@ -291,7 +259,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, @@ -309,20 +276,16 @@ func run(ctx context.Context, serverConfig Config) 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, + 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, + Loader: ingestRunner, GenerationActivator: lifecycleStore, DataframeReleases: publishedRegistry, Logger: logger, @@ -451,9 +414,6 @@ func run(ctx context.Context, serverConfig Config) error { 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) (dataframeexecution.PreviewSummary, error) { if receipt == nil { return dataframeexecution.PreviewSummary{}, fmt.Errorf("compilation receipt is required") @@ -468,10 +428,10 @@ func run(ctx context.Context, serverConfig Config) error { } 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, } server, err := httpapi.NewHTTPServer(httpapi.HTTPConfig{Authenticator: authenticator, Authorizer: authorizer, Logger: logger, CoreReadyCheck: func(ctx context.Context) error { @@ -494,21 +454,10 @@ func run(ctx context.Context, serverConfig Config) error { return nil } return scopeResolver.AuthorizeReadProject(ctx, principal, project) - }, explorerService, explorerMaterializer, lifecycleConfig) + }, explorerService, lifecycleConfig) if err := registerRoutes(server, resourceService, snapshotService, releaseService, 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..e0ba2eb6 100644 --- a/internal/store/arango/client.go +++ b/internal/store/arango/client.go @@ -26,18 +26,36 @@ 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 +} + 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 { From 5d94a87d4fb5a2f0c9cae3b040a4295b6b77c920 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 20:24:32 -0700 Subject: [PATCH 09/17] remove legacy code --- README.md | 8 +- docs/DEVELOPER_ARCHITECTURE.md | 5 +- docs/EXPLORER_COMPILATION_ARCHITECTURE.md | 3 +- docs/TODO.md | 27 + docs/loom-reliability-contract.md | 27 +- generated/loomapi/api.gen.go | 2179 ++--------------- internal/api/README.md | 4 +- internal/api/bulk/load/errors.go | 54 + internal/api/bulk/load/errors_test.go | 32 + internal/api/bulk/load/generation_route.go | 126 - .../api/bulk/load/generation_route_test.go | 110 - internal/api/bulk/load/generation_staging.go | 49 + internal/api/bulk/load/handler.go | 12 +- .../bulk/load/memory_lifecycle_store_test.go | 192 -- internal/api/bulk/load/operations.go | 266 +- internal/api/bulk/load/operations_test.go | 63 + internal/api/bulk/load/resource_route.go | 95 - internal/api/bulk/load/resource_route_test.go | 112 - internal/api/bulk/load/service.go | 78 +- internal/api/bulk/load/service_test.go | 21 +- internal/api/bulk/load/snapshot_route.go | 231 -- internal/api/bulk/load/snapshot_route_test.go | 135 - internal/api/bulk/load/snapshot_service.go | 267 -- .../api/bulk/load/snapshot_service_test.go | 151 -- .../api/bulk/load/test_server_helpers_test.go | 52 - internal/api/http/errors.go | 44 - internal/api/http/errors_test.go | 27 - internal/api/http/http_test.go | 139 +- internal/api/http/routes.go | 15 - internal/catalog/arango/fields.go | 33 +- internal/catalog/arango/relationships.go | 55 +- internal/catalog/write_persist.go | 119 - internal/dataset/arango/release_store.go | 178 ++ .../dataset/arango/snapshot_release_store.go | 491 ---- internal/dataset/arango/store_test.go | 29 - internal/dataset/memory_store_test.go | 135 +- internal/dataset/release.go | 41 +- internal/dataset/release_test.go | 124 +- internal/dataset/retention.go | 65 - internal/dataset/retention_test.go | 54 - internal/dataset/snapshot.go | 156 -- internal/dataset/snapshot_test.go | 73 - internal/ingest/single_resource_load.go | 58 - internal/server/config_test.go | 6 +- .../explorer_authoring_error_logging_test.go | 71 - internal/server/explorer_authoring_http.go | 153 +- .../server/explorer_authoring_v2_routes.go | 207 -- internal/server/explorer_builder_lifecycle.go | 71 - internal/server/explorer_config_route.go | 65 +- internal/server/explorer_config_route_test.go | 2 +- internal/server/explorer_preview_response.go | 8 +- .../server/explorer_preview_response_test.go | 20 +- internal/server/explorer_receipt_contract.go | 97 + .../server/explorer_receipt_contract_test.go | 6 +- .../explorer_route_test_helpers_test.go | 34 - internal/server/explorer_state_route_test.go | 4 +- internal/server/openapi_data_routes.go | 371 +-- .../openapi_explorer_authoring_direct.go | 6 +- .../server/openapi_explorer_conversion.go | 63 + .../openapi_recipe_execution.go} | 48 +- .../openapi_recipe_execution_test.go} | 2 +- .../server/openapi_route_inventory_test.go | 5 +- internal/server/openapi_routes.go | 5 +- internal/server/openapi_test_helpers_test.go | 17 + internal/server/options.go | 19 - internal/server/project_id.go | 20 - internal/server/project_id_test.go | 35 - internal/server/routes.go | 12 +- internal/server/routes_test.go | 12 +- internal/server/server.go | 118 +- openapi/README.md | 2 +- openapi/openapi.yaml | 257 -- 72 files changed, 1039 insertions(+), 6832 deletions(-) create mode 100644 docs/TODO.md create mode 100644 internal/api/bulk/load/errors.go create mode 100644 internal/api/bulk/load/errors_test.go delete mode 100644 internal/api/bulk/load/generation_route.go delete mode 100644 internal/api/bulk/load/generation_route_test.go create mode 100644 internal/api/bulk/load/generation_staging.go delete mode 100644 internal/api/bulk/load/memory_lifecycle_store_test.go create mode 100644 internal/api/bulk/load/operations_test.go delete mode 100644 internal/api/bulk/load/resource_route.go delete mode 100644 internal/api/bulk/load/resource_route_test.go delete mode 100644 internal/api/bulk/load/snapshot_route.go delete mode 100644 internal/api/bulk/load/snapshot_route_test.go delete mode 100644 internal/api/bulk/load/snapshot_service.go delete mode 100644 internal/api/bulk/load/snapshot_service_test.go delete mode 100644 internal/api/bulk/load/test_server_helpers_test.go delete mode 100644 internal/catalog/write_persist.go create mode 100644 internal/dataset/arango/release_store.go delete mode 100644 internal/dataset/arango/snapshot_release_store.go delete mode 100644 internal/dataset/retention.go delete mode 100644 internal/dataset/retention_test.go delete mode 100644 internal/dataset/snapshot.go delete mode 100644 internal/dataset/snapshot_test.go delete mode 100644 internal/ingest/single_resource_load.go delete mode 100644 internal/server/explorer_authoring_error_logging_test.go delete mode 100644 internal/server/explorer_authoring_v2_routes.go delete mode 100644 internal/server/explorer_builder_lifecycle.go delete mode 100644 internal/server/explorer_route_test_helpers_test.go create mode 100644 internal/server/openapi_explorer_conversion.go rename internal/{api/http/recipe_execution.go => server/openapi_recipe_execution.go} (65%) rename internal/{api/http/recipe_execution_test.go => server/openapi_recipe_execution_test.go} (96%) create mode 100644 internal/server/openapi_test_helpers_test.go delete mode 100644 internal/server/project_id.go delete mode 100644 internal/server/project_id_test.go diff --git a/README.md b/README.md index dacb23f5..9b28a184 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`). diff --git a/docs/DEVELOPER_ARCHITECTURE.md b/docs/DEVELOPER_ARCHITECTURE.md index 1884c44e..5940a570 100644 --- a/docs/DEVELOPER_ARCHITECTURE.md +++ b/docs/DEVELOPER_ARCHITECTURE.md @@ -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 diff --git a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md index c59b55d8..c009ba11 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 diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 00000000..aab09675 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,27 @@ +# Architectural TODOs + +## Consolidate Explorer persistence boundaries + +Revisit the two broad, overlapping Explorer interfaces during the Explorer +package audit: + +- `internal/explorer/store.go` defines a 14-method `Store` covering repository + configuration, explorers, drafts, compilation receipts, revisions, + publication, and activation. +- `internal/explorer/lifecycle/types.go` defines a 12-method `Store` that + largely mirrors `explorer.Service` to support lifecycle orchestration and + tests. + +Do not mechanically split these into more interfaces. First decide which +Explorer workflows and compatibility tracks remain, assign persistence and +workflow ownership to their packages, and then remove or narrow the interfaces +that no longer represent real external boundaries. + +Completion criteria: + +- Explorer persistence responsibilities have explicit owners. +- Lifecycle orchestration does not mirror the entire Explorer service solely + for tests. +- Remaining interfaces are consumer-owned and limited to the workflows their + consumers actually execute. +- Obsolete workflow methods, adapters, fakes, and tests are deleted together. diff --git a/docs/loom-reliability-contract.md b/docs/loom-reliability-contract.md index 2a1a120b..fbf17e73 100644 --- a/docs/loom-reliability-contract.md +++ b/docs/loom-reliability-contract.md @@ -16,9 +16,7 @@ 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 @@ -70,18 +68,14 @@ 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. -## Snapshot HTTP API +## Ingest 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 @@ -90,8 +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; -- `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 @@ -108,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/loomapi/api.gen.go b/generated/loomapi/api.gen.go index 2eb325c9..937d55d2 100644 --- a/generated/loomapi/api.gen.go +++ b/generated/loomapi/api.gen.go @@ -858,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 @@ -975,17 +969,6 @@ type PublishRepositoryExplorerConfigParams struct { XLoomSourceCommit string `json:"X-Loom-Source-Commit"` } -// UploadSnapshotResourceParams defines parameters for UploadSnapshotResource. -type UploadSnapshotResourceParams struct { - XContentSHA256 *string `json:"X-Content-SHA256,omitempty"` -} - -// UploadProjectResourceMultipartBody defines parameters for UploadProjectResource. -type UploadProjectResourceMultipartBody struct { - AuthResourcePath *string `json:"auth_resource_path,omitempty"` - File openapi_types.File `json:"file"` -} - // CreateDatasetGenerationMultipartRequestBody defines body for CreateDatasetGeneration for multipart/form-data ContentType. type CreateDatasetGenerationMultipartRequestBody CreateDatasetGenerationMultipartBody @@ -1010,26 +993,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 @@ -1396,44 +1362,9 @@ type ServerInterface interface { // (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 // GetApolloSandbox Render the Apollo GraphQL sandbox. // (GET /apollo) GetApolloSandbox(c fiber.Ctx) error @@ -1983,117 +1914,6 @@ 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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.AbortSnapshot(c, project, generation) - } - - 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) -} - -// 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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetSnapshot(c, project, generation) - } - - 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) -} - -// 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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.CreateSnapshot(c, project, generation) - } - - 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) -} - // PublishRepositoryExplorerConfig operation middleware func (siw *ServerInterfaceWrapper) PublishRepositoryExplorerConfig(c fiber.Ctx) error { @@ -2168,30 +1988,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()) - } +// GetApolloSandbox operation middleware +func (siw *ServerInterfaceWrapper) GetApolloSandbox(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.FinalizeSnapshot(c, project, generation) + return siw.Handler.GetApolloSandbox(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2205,60 +2006,11 @@ 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 - - // ------------- 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()) - } - - // ------------- 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 - - headers := c.GetReqHeaders() - - // ------------- 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)) - } - - 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()) - } - - params.XContentSHA256 = &XContentSHA256 - - } +// ExecuteDataframeGraphQL operation middleware +func (siw *ServerInterfaceWrapper) ExecuteDataframeGraphQL(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.UploadSnapshotResource(c, project, generation, resourceType, params) + return siw.Handler.ExecuteDataframeGraphQL(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2272,22 +2024,11 @@ func (siw *ServerInterfaceWrapper) UploadSnapshotResource(c fiber.Ctx) error { return handler(c) } -// CreateRelease operation middleware -func (siw *ServerInterfaceWrapper) CreateRelease(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()) - } +// GetGraphQLPlayground operation middleware +func (siw *ServerInterfaceWrapper) GetGraphQLPlayground(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.CreateRelease(c, project) + return siw.Handler.GetGraphQLPlayground(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2301,22 +2042,11 @@ func (siw *ServerInterfaceWrapper) CreateRelease(c fiber.Ctx) error { return handler(c) } -// ActivateReleaseCompatibility operation middleware -func (siw *ServerInterfaceWrapper) ActivateReleaseCompatibility(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()) - } +// ExecuteGraphQL operation middleware +func (siw *ServerInterfaceWrapper) ExecuteGraphQL(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.ActivateReleaseCompatibility(c, project) + return siw.Handler.ExecuteGraphQL(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2330,22 +2060,11 @@ func (siw *ServerInterfaceWrapper) ActivateReleaseCompatibility(c fiber.Ctx) err return handler(c) } -// GetActiveRelease operation middleware -func (siw *ServerInterfaceWrapper) GetActiveRelease(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()) - } +// GetHealth operation middleware +func (siw *ServerInterfaceWrapper) GetHealth(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.GetActiveRelease(c, project) + return siw.Handler.GetHealth(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2359,30 +2078,11 @@ func (siw *ServerInterfaceWrapper) GetActiveRelease(c fiber.Ctx) error { return handler(c) } -// GetRelease operation middleware -func (siw *ServerInterfaceWrapper) GetRelease(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 "release" ------------- - var release Release - - 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()) - } +// GetLiveness operation middleware +func (siw *ServerInterfaceWrapper) GetLiveness(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.GetRelease(c, project, release) + return siw.Handler.GetLiveness(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2396,193 +2096,11 @@ func (siw *ServerInterfaceWrapper) GetRelease(c fiber.Ctx) error { return handler(c) } -// ActivateRelease operation middleware -func (siw *ServerInterfaceWrapper) ActivateRelease(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 "release" ------------- - var release Release - - 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()) - } +// GetReadiness operation middleware +func (siw *ServerInterfaceWrapper) GetReadiness(c fiber.Ctx) error { handler := func(c fiber.Ctx) error { - return siw.Handler.ActivateRelease(c, project, release) - } - - 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) -} - -// UploadProjectResource operation middleware -func (siw *ServerInterfaceWrapper) UploadProjectResource(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 "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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.UploadProjectResource(c, project, resourceType) - } - - 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) -} - -// GetApolloSandbox operation middleware -func (siw *ServerInterfaceWrapper) GetApolloSandbox(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetApolloSandbox(c) - } - - 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) -} - -// ExecuteDataframeGraphQL operation middleware -func (siw *ServerInterfaceWrapper) ExecuteDataframeGraphQL(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.ExecuteDataframeGraphQL(c) - } - - 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) -} - -// GetGraphQLPlayground operation middleware -func (siw *ServerInterfaceWrapper) GetGraphQLPlayground(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetGraphQLPlayground(c) - } - - 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) -} - -// ExecuteGraphQL operation middleware -func (siw *ServerInterfaceWrapper) ExecuteGraphQL(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.ExecuteGraphQL(c) - } - - 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) -} - -// GetHealth operation middleware -func (siw *ServerInterfaceWrapper) GetHealth(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetHealth(c) - } - - 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) -} - -// GetLiveness operation middleware -func (siw *ServerInterfaceWrapper) GetLiveness(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetLiveness(c) - } - - 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) -} - -// GetReadiness operation middleware -func (siw *ServerInterfaceWrapper) GetReadiness(c fiber.Ctx) error { - - handler := func(c fiber.Ctx) error { - return siw.Handler.GetReadiness(c) + return siw.Handler.GetReadiness(c) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2641,32 +2159,10 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Get(options.BaseURL+"/readyz", wrapper.GetReadiness) - router.Put(options.BaseURL+"/api/v1/projects/:project/resources/:resourceType", wrapper.UploadProjectResource) - router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation", wrapper.CreateDatasetGeneration) router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation/activate", wrapper.ActivateDatasetGeneration) - router.Delete(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.AbortSnapshot) - - router.Get(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.GetSnapshot) - - router.Post(options.BaseURL+"/api/v1/projects/:project/generations/:generation", wrapper.CreateSnapshot) - - router.Put(options.BaseURL+"/api/v1/projects/:project/generations/:generation/resources/:resourceType", wrapper.UploadSnapshotResource) - - router.Post(options.BaseURL+"/api/v1/projects/:project/generations/:generation/finalize", wrapper.FinalizeSnapshot) - - router.Post(options.BaseURL+"/api/v1/projects/:project/releases", wrapper.CreateRelease) - - router.Get(options.BaseURL+"/api/v1/projects/:project/releases/active", wrapper.GetActiveRelease) - - router.Get(options.BaseURL+"/api/v1/projects/:project/releases/:release", wrapper.GetRelease) - - router.Post(options.BaseURL+"/api/v1/projects/:project/releases/:release/activate", wrapper.ActivateRelease) - - router.Post(options.BaseURL+"/api/v1/projects/:project/releases/activate", wrapper.ActivateReleaseCompatibility) - router.Get(options.BaseURL+"/api/v1/dataframe/recipe-executions/:id", wrapper.GetRecipeExecution) router.Get(options.BaseURL+"/graphql/graph", wrapper.GetGraphQLPlayground) @@ -3896,1098 +3392,171 @@ func (response SearchExplorerCandidates503JSONResponse) VisitSearchExplorerCandi return ctx.JSON(&response) } -type AbortSnapshotRequestObject struct { +type PublishRepositoryExplorerConfigRequestObject struct { Project Project `json:"project"` Generation Generation `json:"generation"` + Params PublishRepositoryExplorerConfigParams + Body *PublishRepositoryExplorerConfigJSONRequestBody } -type AbortSnapshotResponseObject interface { - VisitAbortSnapshotResponse(ctx fiber.Ctx) error +type PublishRepositoryExplorerConfigResponseObject interface { + VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error } -type AbortSnapshot200JSONResponse RawJSON +type PublishRepositoryExplorerConfig200JSONResponse RawJSON -func (response AbortSnapshot200JSONResponse) VisitAbortSnapshotResponse(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 AbortSnapshot401JSONResponse struct { +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 AbortSnapshot401JSONResponse) VisitAbortSnapshotResponse(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 AbortSnapshot403JSONResponse struct{ ServiceForbiddenJSONResponse } +type PublishRepositoryExplorerConfig403JSONResponse struct{ LegacyForbiddenJSONResponse } -func (response AbortSnapshot403JSONResponse) VisitAbortSnapshotResponse(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 AbortSnapshot404JSONResponse struct{ ServiceNotFoundJSONResponse } +type PublishRepositoryExplorerConfig409JSONResponse struct{ LegacyConflictJSONResponse } -func (response AbortSnapshot404JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig409JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(404) + ctx.Status(409) return ctx.JSON(&response) } -type AbortSnapshot409JSONResponse struct{ ServiceConflictJSONResponse } +type PublishRepositoryExplorerConfig422JSONResponse struct { + LegacyUnprocessableJSONResponse +} -func (response AbortSnapshot409JSONResponse) VisitAbortSnapshotResponse(ctx fiber.Ctx) error { +func (response PublishRepositoryExplorerConfig422JSONResponse) VisitPublishRepositoryExplorerConfigResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) + ctx.Status(422) return ctx.JSON(&response) } -type AbortSnapshot500JSONResponse struct { - ServiceInternalErrorJSONResponse +type PublishRepositoryExplorerConfig500JSONResponse struct { + LegacyInternalErrorJSONResponse } -func (response AbortSnapshot500JSONResponse) VisitAbortSnapshotResponse(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 AbortSnapshot503JSONResponse struct{ ServiceUnavailableJSONResponse } +type PublishRepositoryExplorerConfig503JSONResponse struct{ LegacyUnavailableJSONResponse } -func (response AbortSnapshot503JSONResponse) VisitAbortSnapshotResponse(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 GetSnapshotRequestObject struct { - Project Project `json:"project"` - Generation Generation `json:"generation"` +type GetApolloSandboxRequestObject struct { } -type GetSnapshotResponseObject interface { - VisitGetSnapshotResponse(ctx fiber.Ctx) error +type GetApolloSandboxResponseObject interface { + VisitGetApolloSandboxResponse(ctx fiber.Ctx) error } -type GetSnapshot200JSONResponse RawJSON +type GetApolloSandbox200TexthtmlResponse struct{ HTMLTexthtmlResponse } -func (response GetSnapshot200JSONResponse) VisitGetSnapshotResponse(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 GetSnapshot401JSONResponse struct { +type GetApolloSandbox401JSONResponse struct { ServiceUnauthorizedJSONResponse } -func (response GetSnapshot401JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { +func (response GetApolloSandbox401JSONResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") ctx.Status(401) return ctx.JSON(&response) } -type GetSnapshot403JSONResponse struct{ ServiceForbiddenJSONResponse } +type GetApolloSandbox404JSONResponse struct{ ServiceNotFoundJSONResponse } -func (response GetSnapshot403JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { +func (response GetApolloSandbox404JSONResponse) VisitGetApolloSandboxResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) + ctx.Status(404) 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 ExecuteDataframeGraphQLRequestObject struct { + Body *ExecuteDataframeGraphQLJSONRequestBody } -type GetSnapshot500JSONResponse struct { - ServiceInternalErrorJSONResponse +type ExecuteDataframeGraphQLResponseObject interface { + VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error } -func (response GetSnapshot500JSONResponse) VisitGetSnapshotResponse(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(500) + ctx.Status(200) return ctx.JSON(&response) } -type GetSnapshot503JSONResponse struct{ ServiceUnavailableJSONResponse } +type ExecuteDataframeGraphQL400JSONResponse struct{ GraphQLBadRequestJSONResponse } -func (response GetSnapshot503JSONResponse) VisitGetSnapshotResponse(ctx fiber.Ctx) error { +func (response ExecuteDataframeGraphQL400JSONResponse) VisitExecuteDataframeGraphQLResponse(ctx fiber.Ctx) error { ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(503) + ctx.Status(400) 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 ExecuteDataframeGraphQL401JSONResponse struct { + ServiceUnauthorizedJSONResponse } -type CreateSnapshot200JSONResponse RawJSON - -func (response CreateSnapshot200JSONResponse) VisitCreateSnapshotResponse(ctx fiber.Ctx) error { +func (response ExecuteDataframeGraphQL401JSONResponse) VisitExecuteDataframeGraphQLResponse(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 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) + ctx.Status(401) return ctx.JSON(&response) } @@ -5226,44 +3795,9 @@ type StrictServerInterface interface { // (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) // GetApolloSandbox Render the Apollo GraphQL sandbox. // (GET /apollo) GetApolloSandbox(ctx context.Context, request GetApolloSandboxRequestObject) (GetApolloSandboxResponseObject, error) @@ -5734,96 +4268,6 @@ 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 - - request.Project = project - request.Generation = generation - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.AbortSnapshot(ctx.Context(), request.(AbortSnapshotRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AbortSnapshot") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(AbortSnapshotResponseObject); ok { - if err := validResponse.VisitAbortSnapshotResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetSnapshot operation middleware -func (sh *strictHandler) GetSnapshot(ctx fiber.Ctx, project Project, generation Generation) error { - var request GetSnapshotRequestObject - - request.Project = project - request.Generation = generation - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetSnapshot(ctx.Context(), request.(GetSnapshotRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetSnapshot") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetSnapshotResponseObject); ok { - if err := validResponse.VisitGetSnapshotResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// CreateSnapshot operation middleware -func (sh *strictHandler) CreateSnapshot(ctx fiber.Ctx, project Project, generation Generation) error { - var request CreateSnapshotRequestObject - - request.Project = project - request.Generation = generation - - var body CreateSnapshotJSONRequestBody - 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)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "CreateSnapshot") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(CreateSnapshotResponseObject); ok { - if err := validResponse.VisitCreateSnapshotResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - // PublishRepositoryExplorerConfig operation middleware func (sh *strictHandler) PublishRepositoryExplorerConfig(ctx fiber.Ctx, project Project, generation Generation, params PublishRepositoryExplorerConfigParams) error { var request PublishRepositoryExplorerConfigRequestObject @@ -5859,251 +4303,6 @@ 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 - - request.Project = project - request.Generation = generation - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.FinalizeSnapshot(ctx.Context(), request.(FinalizeSnapshotRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "FinalizeSnapshot") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(FinalizeSnapshotResponseObject); ok { - if err := validResponse.VisitFinalizeSnapshotResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// UploadSnapshotResource operation middleware -func (sh *strictHandler) UploadSnapshotResource(ctx fiber.Ctx, project Project, generation Generation, resourceType ResourceType, params UploadSnapshotResourceParams) error { - var request UploadSnapshotResourceRequestObject - - request.Project = project - request.Generation = generation - request.ResourceType = resourceType - request.Params = params - - request.Body = bytes.NewReader(ctx.Request().Body()) - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.UploadSnapshotResource(ctx.Context(), request.(UploadSnapshotResourceRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UploadSnapshotResource") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(UploadSnapshotResourceResponseObject); ok { - if err := validResponse.VisitUploadSnapshotResourceResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// CreateRelease operation middleware -func (sh *strictHandler) CreateRelease(ctx fiber.Ctx, project Project) error { - var request CreateReleaseRequestObject - - request.Project = project - - var body CreateReleaseJSONRequestBody - 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)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "CreateRelease") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(CreateReleaseResponseObject); ok { - if err := validResponse.VisitCreateReleaseResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// ActivateReleaseCompatibility operation middleware -func (sh *strictHandler) ActivateReleaseCompatibility(ctx fiber.Ctx, project Project) error { - var request ActivateReleaseCompatibilityRequestObject - - request.Project = project - - var body ActivateReleaseCompatibilityJSONRequestBody - 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)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "ActivateReleaseCompatibility") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(ActivateReleaseCompatibilityResponseObject); ok { - if err := validResponse.VisitActivateReleaseCompatibilityResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// GetActiveRelease operation middleware -func (sh *strictHandler) GetActiveRelease(ctx fiber.Ctx, project Project) error { - var request GetActiveReleaseRequestObject - - request.Project = project - - 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) - } - 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 - - handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { - return sh.ssi.GetRelease(ctx.Context(), request.(GetReleaseRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetRelease") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(GetReleaseResponseObject); ok { - if err := validResponse.VisitGetReleaseResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// ActivateRelease operation middleware -func (sh *strictHandler) ActivateRelease(ctx fiber.Ctx, project Project, release Release) error { - var request ActivateReleaseRequestObject - - request.Project = project - request.Release = release - - var body ActivateReleaseJSONRequestBody - 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)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "ActivateRelease") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(ActivateReleaseResponseObject); ok { - if err := validResponse.VisitActivateReleaseResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - -// UploadProjectResource operation middleware -func (sh *strictHandler) UploadProjectResource(ctx fiber.Ctx, project Project, resourceType ResourceType) error { - var request UploadProjectResourceRequestObject - - request.Project = project - request.ResourceType = resourceType - - 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.UploadProjectResource(ctx.Context(), request.(UploadProjectResourceRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UploadProjectResource") - } - - response, err := handler(ctx, request) - - if err != nil { - return err - } else if validResponse, ok := response.(UploadProjectResourceResponseObject); ok { - if err := validResponse.VisitUploadProjectResourceResponse(ctx); err != nil { - return err - } - } else if response != nil { - return fmt.Errorf("unexpected response type: %T", response) - } - return nil -} - // GetApolloSandbox operation middleware func (sh *strictHandler) GetApolloSandbox(ctx fiber.Ctx) error { var request GetApolloSandboxRequestObject @@ -6296,111 +4495,103 @@ 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{ - "7D39b9u4kv8KoXvAAgcnTrvdB2zupzRJ2xzSNM9Jd+/dolfQ0tjmViK1JJXULfK/H/glUbI+HdvJ7vqn", - "VjElDofzzZnh9yBkScooUCmC4+9BijlOQALXT+df05hx4BeReiI0OA5SLBfBKKA4geA4gGLAKODwR0Y4", - "RMGx5BmMAhEuIMHqzYTQS6BzuQiOX4wCuUzVu0JyQufBw8MoeAsUOJaE0YZ55sWAx8xzzdnvEMqGSVL7", - "62NmmEAMWEDDDNz++rgZBMt4CLf6l4ZpvCHrz/WgXhUpowI0NZxkcsHUb69xNIE/MhAakyGjEqj+L07T", - "mIR6n8a/C7ObxWz/4DALjoP/GBckNza/ivE554xP7Gxm7ghEyElqqCK4XQD675sPVyhiYZYAlYgIlOB4", - "xngC0QgpKDChAmX0C2X3FM0IxJEYIcYRS4gUyKEBYbcORGiaycPgYVSs7TQmQOVpzAQ8zSJDDQAKMQ0h", - "hgilHO4I3CP4CmGmxqEpzBgHpCaIQf2lsgJGZzEJdwj2RZJkEk9jQCFO8ZTERC4V3jmEQFKJhMQS1H4J", - "iWMwW5JKkhAhSYgijmdynGZTB54dHy4wnUNUXtwbxqckioDudlMUyQCVaga9JYSGJMWx2iXKJOKAI7Wq", - "hEVktkRyQQRyshNZsVJex1ss4R4vb0kCLNvhVl2vUBN8DQEiiJDiEQH8DviBIBGgCHAUEwplwC+oBE5x", - "rGfaHdwnFGUUvqYQSp+DRygFLoiQQENDWI5dZpjEGQfEwjDjvEpGV0y+YRmNdktFjiRGim4iEmEJI83G", - "JDaEbxnGcIhcgOITq/cORAohmZHQE19OzqOIgUCKEOErERVKu8bLmOHolrFLzOew2xVPFZI9IcZBZLG0", - "NCeQXABK8FeSZAlyqgYJ8q1Cc7eMvcd0acWx2O0SuJnVW0QaY1paguGaH0TBVQchExKlLCbhsryWjxTf", - "YRIrablD7imUXyGhR558ZhxGKMESOMEx+aZIFIeS3Fl5DPyOWP6a4vAL0EhJ86xYSnWNKWchCLHbVXq7", - "pcC7wzGJjM0wzaQWb/eMfxEpDg3bJZhGI8QymWaG5XxWJBpirbQgwVr0x/ESEao/q9f7luN08a/LLVhD", - "E3yv4G5apJ0Yvbu9vc5XDPQOYpaCWojkmIqUcYkKex5hDoXB5C9gOyK9ZQ0O/kIJKWkNkbNsMKKMJzjO", - "F5rLhpBlsRqmlGqUheV1bIfseu5FbpYaEJUwVmBiLkCbBppstPUwY7xYuYb/3e37ywq8Er7K8UImcRnQ", - "GhO9DNK5UgBKN0w5uxfA0ceLHDQ91yXMcbjcAs2aD/di0lgPLSwkj2lzAtXmFBHC6DkrvAp73cy2cVN3", - "wCJy6HGszL+l0b3C2L0pE0QyvkQRpDFbasIoWbVqGBGI0JBRY7/469q8lTtgYR22Lg4Vi7XYuGaq7QiV", - "nsuYAI4U4Sh70NiH6kn6u2YlTmFUxksP+o3bh+sQVomSDtg9haj4MWR0RuaZMQ7RPTb230wB7a1jG9ZG", - "7z3ImcDTq76VYf+gl6njIr7NQcyC3DINx1QtDrfIbcj9dXYsty8UsB7rKytiSaVanjEjjG2izBLLVVOw", - "WIKoZIhFxqlRzrFYWHV3Y8yxLchw++Uh9mSK5WKE/siAL0coyWJJUszlSNtdGvarM22C3eE40zEAJ9SV", - "APSMKTv1xiV63yV5MQwzbdkClsZXy7hg3D25jSlHLewCBLoncqFljlVv/jI3L+D7rrNNwsc4/CJyAc98", - "4PU4LeMNv1rHU4QsBX9h2xH7vemyFCUwbhkC9VYeB/gvFIHEJDb2MIcIm4gCjVDIOIdY42S6dAu/KJHn", - "xvXCkG0rtiLCEguQI1TExUdOiGq3jqRQ2JhGxLJMQrFxqwrDQvKR2vjCN9j9Kk8KwlQcFXKI1CO2u4Wn", - "QknTesmxDVW3hjx02LObolxrPIcRInQOwvzNExp6a7TwbHOs8xVuQ8+tQYFt+ky52Yo8y7Tp6XiLKaUX", - "haf97NKgsqciS5UfC9F7iAh2Zx47XfipmexATe7MkhwsJSm0KZylMcNREa871Ic1di59enJ98YuyRQ2I", - "QLMkOP4tiBlLDkMcL1N+yPh87A7UDvIo3/juZfBp5XBmFJyE7rwMRxFR/8fxNVfzSwIiOJ7hWMAoSL0/", - "KcTFWUL1f4mERHThyExyqt9Sk1ooMOd4qZ5nJIYrfej0fRVCSaSh09ZjJveH7pOv4hzrN/Orm6JAD5vq", - "E7wcPRbydZDUA3D4qojgHeAIeJ13XgbZfrYW2DSNl6cmIiU8o24Q0Pptc1LbAbcdO4AMirMl/ab6SELo", - "hXn3xSpdOB18xvFMnpG5XU8dBotxHnvMGE+wDI4DQuU/XwV6NpIoljnKJyNUwhy4+oygOBULJm/ZF6BD", - "SanAXPVDDfB5COyxmVawbG83I4LnlAlJwv4bepa/U8fVUXnXuubvvXcv6vbOHAn0B91idqJfq4M+d8K6", - "vvRrPrCNJIrPVZZaxlOxkPKO1FJIzk55MH4gfeCUeBhvZd1C7Sh5DVhmHPoz/Bv3wsMo+EKM4et0l3N7", - "V5ejoKzTWbl2LG+3++A0I7ESpaNAZHNrLil82oMX9T/jCPssqBAfMhqSkiKoapeCPOzHLklCKlRnT6KC", - "4xdHR0ddZFv+bIV+vO2xaCutvQqEty/t5GKl70BZ4s4ae+qGnsrPDPxFufXdLOvsB4jm/cAglCi7/5qD", - "Mvnz1CBHKbcnry/Pg1Hw5uLy9nwSjILTdyeT23qaM44fDftNbI6DBg2tUFEfY8cjR8yByg/DgLSuOGH0", - "PYs60X9dHq1olTF5xaJ+kxmP8UN/vAy1+9ymnk7OT27PP7u9Pft4fXlxWvrL+eW59zg5vzp57z9+mJyd", - "T8zzTTAKbs5vzcPnyYcPt8EoODk7+zz58PHWjH7/4Zfz/FH9dPrh8uP7q2AUfLw+U9Pmz3asff7UyzZt", - "ZeQ3nhgewMkRxCDBiV3PoJoyFgM2HKbja4yeLjA38q15zBsSu0y/1UHKtDdGdOOArxC1fkIsMG8dUkFc", - "Fa6V1VQ/WQGiDPSoiq+6LXltVM6NxBJ2pINDLHHM5p1C0w5btcceYYHVWs9Ner2EmzrRGpMZhMswhhx9", - "7hNX579qvjk5+3ftmyUbDcfxh1lw/Ftva+3TKKBZbEM9kmfQR/9WgO0w5nyrz21YHf2cOuV6A5iHi/V8", - "N9pXFOtIUS0JPMoDqno9Fp5e613LvVmfdezsA9wFs3k53LX+zsY9qA6WKrDo2bl1fNKbMh61/3Xs0kAT", - "pT3o9nROC1k3hDz8GOpNyFLo7YxuiUJsri006NNoPny+82heO9W8lAvfsVy1K4NnvrJWYHVmDoLFdxDd", - "6OEt+kafJVzr5LbO7Blv6GPJtFQE0Egjq6Sbb57Dl9uxCjH7y2oh5oJQtuqHKYvHBfpXSS6CGc5ief1I", - "R0AnyV9jBUQnRDNtZTWDFOMpxD2+E7M5CXF82yfqO0ACln2i/kyxipRVvkgBS3Mc1mHB+nvsycwCzQ5N", - "ZTR4c5Tw7JPB6gqbqKCFdrXQGUa2vV32GWdJb6eyL7GkLM3iJuSPAsl6zlj1NMyiSkB7Xyt2qZi/BatX", - "lveGuHI2J++UZeYca6DN3p8t2hHIK9VEHV/j7P4tx4Q2qIX7CWPyPCZz0iAlKtuQM0i1Yqnypc59UFxS", - "jRUN2A3G7QlOO9LzwMbmjrBqV7PWoZVCQacBsIKnUsQvxVICp8Fx8H+/nRz8Lz749vmT/c/Rwc+fP/3n", - "P4JGvdA1tfHRq3OvqTMeG+QzxNYvbHljxqpNdrqv7aVbNai8zPpzwELGlIDPgWumjJsc+gH0UdL0Kwip", - "+it6dDAKiM6/mBHgr5c3SyEh0YdiEqjyFl4vP/JYG1gRoXNvBJsK4CZ16tSh6PXyVInJXI2VHMwCkgTL", - "sB7G1aCng/aXk8uPJho8udFxvsvLYBScXdzcXlyd3nYH7PTq6/HtnzWte4rcSa2tQehVWYOnTb9UAqom", - "+GnCqmfByD2/O7l6q5918PPzydmZfjLhTfv46RHSS6cxrhcJGeYfbOSsz4/0lKdvXd1O4x7uYKz/Io3X", - "Q2Lg3oQ7P0ge5Muaipvenn5HiGViapvqj4Q0e/VfpP2WOQKp9xD0gKeJ1BSTr7q+JZe5hOBVAhl5568O", - "Qz0iPIxKjkO5vtHS7FH2Povs7Zo+yhnt0uNlj67BjavDocdEQ3WMUYQVw00bbS0Wm82AbZ7M9AlYgfN3", - "wWijCZGAEHjey4lwmbX1kWy4A24zIrpYSfaZsBrsdt93748MGosV1G6R9deGCvxwNduhO9+uPgI5LHOv", - "LWevfG7X63NvipfWiHfn2GuWxl0AFMKXMyYnQ93WTEKvUKWJjdbZhzmgNRC4GYptqqMhnWn6mkXLLsar", - "Z/Le2nuYzt4M367Ixk6OKufdDoyebNxSAVev0FmBrDdwJZ6kX69dpuWE9yBxhCUefOTvpSKvnWNbtd0b", - "s2bflA/915N09W81+ive9Lln2fqdfLs7k2sqE1Q3rZhtlMPfgJVq7GIYcprtj16Rp4damAqBvKX0Zl2y", - "1YHxlizgJnPJfrYO0V7N3fpyss0yebTUqqsK7ICyUqCtq5HyYn2uTDFq6giqZdGYRraW56ColNeKRhwG", - "K2F6J8Aq0xHdRgS7j9u1IYMBxNRPQvIslLrGUmFg7MaYVY9QBClQU09LTU2YgkGBwCjYzI0q0nvXVhpx", - "+qm/QP2QmwsDaJ5E/YLbl42MupagJVFrjYJtxrNeqCQmCZGD01YHpDf2d2orq/Yd0ny+1vXvNolksCVd", - "8nCfQ/6IRVyLOb3hHdbMkZ9VtZ+ScHZfRsMgH3NwXnUttRW7bOHxFtAd07hujDTfnJ5cnkz8UPNkcvJv", - "L9j82f3BxaQ7Y9Gj4LqoB1xLuFUq6jed5ixcct8QGeDthLD5dqtg1uLeZPivJxM3ILJaYdqpnNq9VCno", - "cCNh01W6fmToVEk90ZfC1yLbLgHjQVAQdv9gqeslNMhi1O0T7hdMgGkxowwxscCmGDVVTgm/gwjFTIgY", - "hIiXKMpMezhd8h1mkt0BPwzq4CkFtrdbStqtRodorrY8hLVMNU9gmfe9SdrDOhNXfrSezNppsd1G04Rb", - "Eqhr8ZTH14YeEpA44gbcfsc1RSCvhsiGZQrohhCEUbEgaX3Sy5BIZJXmygf/pU81YrDIuxyig+KY3U9s", - "jtm5S1ddPSjR424gnl0yljaMSfDXGwmp6BG1KInWVRBW5qtbtF+m3x4UaD0iWfcQpFxoUANfc3Sh66SD", - "g+RL3C9TqkdoorafQReyyqrmxnTAgXKMAhIibauDS8aSHwQyVTim/6AZnOA0tSELJya6QxV9uzMMDLne", - "eDVCr4mOXGwrRNZbZTVrm5a2ALd4ulacY708rp5JcGzzhYDKpurHBsTXzx4SDezFlxqwWcnMGkgUEMcT", - "oBHY2rsiV6qImX9aH68pobQpS7MdQSsr/bVUXbUDT8Ue7w3wU9yB4ENt74i8vrH1NKZ6srJatNiRkJgP", - "7faNCpTWbfFKoWPn0UWr6KuRX92HGzoxrP8kSrasE27Jd8ffdjv3CteNgq8Hc3bgJnHlr3cvD3/1sq7y", - "MQckSZnJYrU3GXivKDWicw+COZGLbHoYsmRsOueMY8aSMbGtx/IeOmP/7Qfd2YfQGdNcZCRToPSZUWMn", - "1xdKfjjyD14cHh0e2Z4FFKckOA5+PDw6/NGCofE7xikZ370YK+qbcZzAuBquF+PvJHpQY+eg15V3AVAC", - "NHgLcqLfOHcvBJU7F14eHe2ik+1Nppu+zbI477Wr+zC9Onqxi+krfb+KLoEj1+nL9GfOu0W5UiXdTFcu", - "vGZnPwjXHc1rTvfq6Mcm6HJsj6utV/V7r/q+l3eG022fsiTBfGk7kdqGXyixgkoDjVFONSsN2w5tY4D8", - "Jpbfaq/5IO2XrlQtkE8PoxLFCpBi/N1m1T6Mi1QxMf5ePDxUb4VpKNMthriKmsZTIG+od/HLw6dRkDJR", - "wyWnXPkMZwbkt9WbYEBI5xZ4ZJp3ohwrD/nAnbwX2FmtOPzs/K/PaVOOUwQz4J+LvpDNlfMlOZw76VNC", - "FVn0aAtRzpxsyoXuPtLUsIy8q228D68aSeW3dWX1c5NHR90MudoftZBkvd4s9UDsKz5WOnvqF3/u/WLe", - "+VS99+KnAcDW9MhT33j5csA3/M6CD6PgpwF4Lrf81C//OATVedfGsuT8aBrq4aKpbC7YCxo2XfbWkGpj", - "y8SwM/E2sjLc1PDnQjzXAp9z8f95oGBv+HKNTGtVD03i98Riql4A72XDjmXDM2BNRxEIe6yI8EwC96wa", - "r81pmU0tc/ps6kx20WgqXxIhz/NRj6S8jRxNDzWit0deNZZrLyqp69Rf3mmF9SIhyfydgLAWrN/1f00Z", - "2mH0nRfeZrOttyEp02X4vHj2wm3lbo8nIr5eoq1yg0dfi6Wu5f/m6N1QHcLm/h+ujYSiv39/MTb+XtyM", - "2er+lwj82fn9O6aaNZztje28dtQxzff6B4Hs7vOMSpIAKupPHyPwuo1G79LVks/em97Gfu/osVc72EWE", - "r/P6r63RYqmnWF2zbX0gKr2bNDA1VxMS75JJ3XUhP2jaDcXWXEHZl2hXrx3sKyRXL/XsS/ENFzb2tQFr", - "76vT+7Ubum80C0zRYh3Fbt48qJQy7zg8Ui017ri0ZOUWyf52Q93Fvk/JUusyRi8DouGWwifmq8fL+bDU", - "S7pL1Ne1oN4iLddNV2uOuGsViktOi77JWhV8vECuWfJTyv1H0crDn8h88O8KeFrRr/vqVxsp+l3AN68A", - "au9l2LEaqL9OoM50wpRREuLYXSVj7xCByLemdJAGU4QlS0jorkFFUyzDxV5j/K00hmur/+ScbctZthzs", - "qVRb7ZiNq7VONQz8unJZNpshRiG/pdmkO/15eXT3jtKrF0PArV6U/njx8OrlEMirt5yrD/w8aOkxASpP", - "YybA3/CnE1Lq5SG7/hZLuMfLW5IAy+SGhJy9MeTphZwBZNtCrlw+tWshVymUqkv1sWdHkX/9symq2Yu2", - "veHSi6eLC3+enKvz4p/dRKRWao2ec0zKGS7uYmM04yyx2Xo4lO5qboiQLh3as/+e/Xuxv3852JMLAHMP", - "hht6Wuonv42QdP19I7sWAw23gNRHJcxQFMFM3+rFqEC6yyuhc4QRZZE55NHpS3+/EMTz5cW2jGBztVJN", - "kG7KuLyxB3N/t0Pt+jSvV71fHKwbnml6mKKBcm5Y6ai26WxiTzaPJ5tnsP06k6Jm75GQWGZiu8kTAwoK", - "SuT2xLll+8TZv0XirE0v07fhiywxiWa5v1QvL9fT0cVF8iGjMzJ/+iqePDaUMkEk4/lR3qkBsL7i6XE5", - "7S4zfmGuaM8/8z8Hl4wlB+YihYNTliREDqul2o7M8LvU/yWkxj4jdY28xJ7Sxs3bKGxM2EX7VjYKjjDi", - "Of8dsHvqh2HzQ+L1hc6MUByTb/D00uaNheTZ2pR/MpW7Wyfmz1679guOTcxD8Z5jii0qe6cRxfi730To", - "YZfFbR2jS/3SNd9mNWxriv4c07p3GmyDVa1+atj54Obdycuf/hlsRIOzUII8EJIDTsoyobOsd2/578XQ", - "U4qhG020NonEsBJSJIoIlawsjnrLIA4xYEu926n5mpgZ9m75njn/yszpSs1QlOlbglx9EbIchvK7ZHvz", - "5CaK2j3mjCDlEJq7Q41DXF8YbhlW+RtYEi+ZfM+9e+79i3MvjRAuquEd6xKqla69cyHnmoGMDG2FHJr5", - "Srpyf2Lypz4xkQtwFacVTfCo+vqeBPfd/q+jV9me2v4i1IZXzI3pEl2cbfdwztHPcKrcTbueAr6uXjh7", - "H2Vv5fwNrJyi0Q8t7h5w/kqhnbq4+QmCk0PCjfazXrRx130FXcfA7ohiTXe/fRu/rYuNfTs+145PuTVF", - "Q76rM305SR5cVPSYywMWx6zVg9EjbjCNpuxrg03ZDvS72/eXjyeKdVRCxaCiEZg2tGZN6C3H6eJfl0iY", - "xVmUzNVf/4iLdsFaXtTaGaYfsG65p0faD+7tjT5kYZG1QcHRh3ftrOvxrn25rUGRJYlS42BHZ9UIg6M0", - "/W8bD9oPXMd4OeeatP8afOgQk+YLM55NG7PteWzPYzmPac5BjKM37y4mjXy2ABzLVgZ7Z0Y8rx22FsHW", - "u7ojR+fIIMreJAs0XCIiEGUSccDR8nCFkZUBhUIcLvSdtNwEWgu5Zz5nNyEmd/CtbQ8uyR1QEOJZ7ULd", - "gi1Vo9gCjO6JXLBMonAB4RftezkEEt3+Ri1fY/Bbe9gOR+TZIWBnZHjm4QxhDp10d78AfWlyTr1R9Qv5", - "2+p14HfOf814HBwH4+Dh08P/BwAA//8=", + "7D1rb9w4kn+F0C0wwEF2O5nsAeP75NhO4oPjeNvOzO0NcgYtVXdzIpEakrLdCfzfF3xJlFrPdnfbu5tP", + "idp8VBWrilXFYvF7ELE0YxSoFMHh9yDDHKcggeuv04csYRz4Way+CA0OgwzLRRAGFKcQHAZQNggDDn/m", + "hEMcHEqeQxiIaAEpVj1TQs+BzuUiOHwVBnKZqb5CckLnweNjGLwHChxLwmjLPPOywVPmueTsD4hkyySZ", + "/ev6MzyqriJjVICm31EuF0z97S2Op/BnDkLPHTEqger/4ixLSKQxm/whDP7lbH/hMAsOg/+YlIs0MX8V", + "k1POGZ/a2czcMYiIk8zQMbheAPqfq08XKGZRngKViAiU4mTGeApxiBQUmFCBcvqVsnuKZgSSWISIccRS", + "IgVyZEDY4YEIzXK5HzyGJW7HCQEqjxMm4HmQjDQAKMI0ggRilHG4I3CP4AGiXLVDtzBjHJCaIAH1Sw0D", + "RmcJiXYI9lma5hLfJoAinOFbkhC5VHTnEAHJJBISS1DrJSROwCxJJklKhCQRijmeyUmW3zrwbPtogekc", + "4ipy7xi/JXEMdLeLolgGqFQz6CUhNCIZTtQqUSYRBxwrrFIWk9kSyQURyGkbZAWxisd7LOEeL69JCizf", + "4VJdrnATPEQAMcRIyYgAfgd8T5AYUAw4TgiFKuBnVAKnONEz7Q7uI4pyCg8ZRNKX4BBlwAUREmhkGMuJ", + "ywyTJOeAWBTlnNfZ6ILJdyyn8W65yLFEqPgmJjGWEGoxJolhfCswRkLkApSc2J1iT2QQkRmJPPXFQbCc", + "R4BiBgIpRoQHImqcdomXCcPxNWPnmM9htxjfKiJ7SoyDyBNpeU4guQCU4geS5ilyWw0S5FuN564Z+4jp", + "0qpjsVsUuJnVQyJLMK2gYKTmJ1FK1V7EhEQZS0i0rOLymeI7TBKlLXcoPeXmV2ro0NPPjEOIUiyBE5yQ", + "b4pFcSTJndXHwO+Ila9bHH0FGittnpeo1HHMOItAiN1i6a2WAu8OJyQ2NsNtLrV6u2f8q8hwZMQuxTQO", + "EctllhuR80WRaIj1pgUp1qo/SZaIUD2sxvc9x9nib+dbsIam+F7B3YaknRh9uL6+LDAGegcJy0AhIjmm", + "ImNcotICRphDaTD5CGxHpXfg4OAvNyGlrSF2lg1GlPEUJwWihW6IWJ6oZmpTjfOoisd22G7gWhRmqQFR", + "KWMFJuYCtGmg2UZbDzPGS8w1/B+uP57X4JXwICcLmSZVQBtM9CpIp2oDUHvDLWf3Ajj6fFaApuc6hzmO", + "llvgWTPwICFNdNPSQvKEtmBQbU4RIcw+Z5VXaa+b2TZu6o5AooAeJ8r8W5q9Vxi7N2OCSMaXKIYsYUvN", + "GBWrVjUjAhEaMWrsFx+vzVu5IxDrsXVxpESsw8Y1U21HqQxEYwo4Voyj7EFjH6ov6a+a1TilUZksPeg3", + "bh+uw1gVTtpj9xTi8o8RozMyz41xiO6xsf9mCmgPj21YG4PXoBACb1/1rQz7g0YzASzAtzmIQcihaSSm", + "bnE4JLeh99dZscK+UMB6oq+siCWVCj1jRhjbRJklVqpuwVIJ4oohFhunRjnHYmG3uytjjm1Bh9uRx9iT", + "GZaLEP2ZA1+GKM0TSTLMZajtLg37xYk2we5wkusYgFPqSgF6xpSdeuMafShKXgzDTFu1gKXx1XIuGHdf", + "bmGqUQuLgED3RC60zrHbm4/m5hX8UDy7NHyCo6+iUPDMB1630zreyKt1PEXEMvAR247aH8yXlSiBccsQ", + "qF5FHOC/UQwSk8TYwxxibCIKNEYR4xwSTZPbpUP8rMKeG98XxixbuRQxlliADFEZSQ6dEtVuHcmgtDGN", + "imW5hHLhVjcMC8lnauML32D3WB6VjKkkKuIQq09sVwvfCqVNmzXHNra6NfSho55dFOVa4zmEiNA5CPOb", + "pzT00mjl2eVYFxhuY59bgwO79jPlZiv2rPKmt8dbSql9UXi7n0UNamsq8kz5sRB/hJjga+3+7BjxYzPZ", + "nprcmSUFWEpTaFM4zxKG4zJet6+PaOxc+vTk8uxXZYsaEIHmaXD4e5Awlu5HOFlmfJ/x+cQdQe0VUb7J", + "3evgy8rhTBgcRe6ECccxUf/HySVX80sCIjic4URAGGTeT4pwSZ5S/V8iIRV9NDKTHOtealILBeYcL9X3", + "jCRwoc+bvq9CKIk0fNp5zOR+6D/vKs+xfjd/dVOU5GG3+syrII+FfB0iDQAcHhQTfAAcA2/yzqsg22Eb", + "gc2yZHlsIlLCM+pGAa17m7PNHrht2xFsUJ4t6Z5qkJTQM9P31SpfuD34hOOZPCFzi08TBct2nnjMGE+x", + "DA4DQuV/vQn0bCRVInNQTEaohDlwNYygOBMLJq/ZV6BjWamkXH2gFvg8Ag5YTKtYtreaMcFzyoQk0fAF", + "PSn6NEl1XF21vvkHr92rprUzRwLDQbeUnepuTdAXTljfSL8VDbtYohyuhmqVTiUi1RVp5JBCnIpg/Ej+", + "wBnxKN4puuW2o/Q1YJlzGC7w71yHxzD4Sozh6/Yu5/auoqOgbNqzit2xutxuwNucJEqVhoHI59ZcUvS0", + "By/qf8YR9kVQET5iNCKVjaC+u5TsYQc7JympcZ09iQoOXx0cHPSxbXXYGv94y2PJVsG9DoS3Lt3sYrXv", + "SF3izhoH7g0DNz/T8Ffl1veLrLMfIJ4PA4NQouz+Sw7K5C+SaRynXB+9PT8NwuDd2fn16TQIg+MPR9Pr", + "Zp4zjh+Nhk1sjoNGNa1x0RBjx2NHzIHKT+OAtK44YfQji3vJf1ltrXiVMXnB4mGTGY/x03C6jLX73KIe", + "T0+Prk9v3NqefL48Pzuu/HJ6fup9Tk8vjj76n5+mJ6dT830VhMHV6bX5uJl++nQdhMHRycnN9NPna9P6", + "46dfT4tP9afjT+efP14EYfD58kRNW3zbtvb7yyDbtFOQ33lqeIQkx5CABKd2PYPqlrEEsJEwHV9j9HiB", + "udFv7W3ekcTlxq02Uqa9MaJbGzxA3DmEWGDe2aRGuDpcK9jUh6wBUQU6rNOraUnemi3nSmIJO9qDIyxx", + "wua9StM2W7XHnmCBNVrPbft6hTZNqjUhM4iWUQIF+dwQF6e/abk5Ovl7Y8+KjYaT5NMsOPx9sLX2JQxo", + "nthQj+Q5DNl/a8D2GHO+1ecWrIl/jt3megWYR4v1fDc6VBXrSFEjCzzJA6p7PRaeQfiu5d6sLzp29hHu", + "glm8Au5Gf2fjHlSPSJVU9OzcJjkZzBlPWv8mcWnhicoa9Hs6x6WuG8Mefgz1KmIZDHZGt8QhNtcWWvbT", + "eD5+vtN43jjVvJI93oOuWpXRM19YK7A+MwfBkjuIr3Tzjv1GnyVc6uS23uwZr+lT2bSSNt/KI6usWyye", + "o5dbsRoz+2h1MHPJKFv1w5TF4wL9qywXwwznibx8oiOgk+QvsQKiF6KZtrLaQUrwLSQDxknYnEQ4uR4S", + "9R2hAas+0XChWCXKqlxkgKU5DuuxYP019nRmSWZHpioZvDkqdPbZYBXDNi7o4F2tdMax7WCXfcZZOtip", + "HMosGcvypI34YSDZwBnrnoZBqgK0N1q5SuX8HVS9sLI3xpWzOXnHLDfnWCNt9uFi0U1AdyA8UBo5u3/P", + "MaEt28L9lDF5mpA5adEStWUoBKQCxupIveugpKQeKxqxGozbE5xuoheBjc0dYTVis9ahlSJBrwGwQqdK", + "xC/DUgKnwWHw/78f7f0f3vt288X+52Dvl5sv//mXoHVf6Jva+Oj1udfcM54a5DPMNixseWXaqkV2e19X", + "p2vVqIpm8zlgqWMqwBfAtXPGVQH9CP6o7PQrBKn7K7p1EAZE51/MCPC3y6ulkJDqQzEJVHkLb5efeaIN", + "rJjQudeC3QrgJnXq2JHo7fJYqcliG6s4mCUkKZZRM4yrQU8H7a9H559NNHh6peN85+dBGJycXV2fXRxf", + "9wfsNPbN9PbPmtY9Re7l1s4g9Kquwbdtf6kFVE3w04RVT4LQfX84univv3Xw8+bo5ER/mfCm/fzyBO2l", + "0xjXi4SM8w82ctbnR3qq03dit9O4hzsYG46k8XpIAtybcOcHyaN8WXPjZrCn3xNimZq7Tc1HQlq8hiNp", + "xzJHIM0egm7wPJGacvJV17fiMlcIvMogoXf+6ig0IMLDqOQ4kusbLe0e5eCzyMGu6ZOc0b59vOrRtbhx", + "TTT0hGjsHmM2wprhpo22DovNZsC2T2bqBKzA+YdgtNWESEEIPB/kRLjM2uZINtwBtxkRfaIkh0xYD3a7", + "8V3/0JCxxKBxiay/NlbhR6vZDv35ds0RyHGZe105e9Vzu0HDvSs7rRHvLqjXro37ACiVL2dMTse6rbmE", + "QaFKExttsg8LQBsgcDOUy9TEQzrT9C2Ll32C1yzkg3fvcXv2ZuR2RTf2SlQ173Zk9GTjlgq4+wq9N5D1", + "Aq7Ek3T3RjStJHwEiWMs8egjfy8Vee0c27rt3po1+6566L+epmvu1eqveNMXnmXnOMVy9ybX1CaoL1o5", + "W1jA30KVeuxiHHHa7Y9BkafHRphKhbyl9GZ9ZauH4h1ZwG3mkh22idDenbv19WSXZfJkrdV0K7AHytoF", + "bX0bqbisz5UpRs09gvq1aExje5dnr7wprzcasR+shOmdAqtNR3QZEewGt7ghQwHE1J+E5Hkk9R1LRYGJ", + "a2OwDlEMGVBzn5aaO2EKBgUCo2AzN+pEH3y30qjTL8MV6qfCXBjB8yQeFtw+bxXUtRQtiTvvKNhiPOuF", + "ShKSEjk6bXVEeuNwp7aGte+QFvN14r/bJJLRlnTFw30J+SOWcB3m9IZXWAtHcVbVfUrC2X2VDKN8zNF5", + "1Y3cVq6yhcdDoD+mcdkaab46Pjo/mvqh5un06O9esPnG/eBi0r2x6DC4LO8DrqXcajfqN53mLFxy3xgd", + "4K2EsPl2q2A20t5k+K+nEzegsjph2qme2r1WKflwI2HTVb5+YuhUaT0xlMPXYts+BeNBUDL28GCpqyU0", + "ymLU5RPuF0yAKTGjDDGxwOYyaqacEn4HMUqYEAkIkSxRnJvycPrKd5RLdgd8P2iCpxLY3u5V0v5tdMzO", + "1ZWHsJap5iks09+bpDusM3XXj9bTWTu9bLfRNOGOBOpGOhXxtbGHBCSJuQF32HFNGchrYLJxmQK6IARh", + "VCxI1pz0MiYSWee56sF/ZahWCpZ5l2P2oCRh91ObY3bq0lVXD0p0uytIZueMZS1tUvxwJSETA6IWFdW6", + "CsLKfE1I+9f0u4MCnUck6x6CVC8aNMDXHl3oO+ngIPkSD8uUGhCaaKxn0Ees6lZzZSrgQDVGASmRttTB", + "OWPpTwKZWzim/qBpnOIssyELpyb6QxVDqzOMDLleeXeE3hIdudhWiGzwltW+23SUBbjGt2vFOdbL4xqY", + "BMc2fxFQ2VTDxID4+7NHRAN7OVILNWuZWSOZApJkCjQGe/euzJUqY+Zf1qdrRihty9LsJtAKpr9Vblft", + "wFOxx3sj/BR3IPjYWDuiuN/YeRpTP1lZvbTYk5BYNO33jUqSNi3xykXH3qOLTtXXoL/6Dzd0YtjwSZRu", + "WSfcUqyOv+x27hWpC4OHvTnbc5O46693r/d/87KuijZ7JM2YyWK1jxh4XdQ2onMPgjmRi/x2P2LpxFTO", + "mSSMpRNiS48VNXQmfu9HXdmH0BnTUmQ0U6D2M7ONHV2eKf3h2D94tX+wf2BrFlCckeAw+Hn/YP9nC4am", + "7wRnZHL3aqK4b8ZxCpN6uF5MvpP4UbWdg8arqAKgFGjwHuRU9zh1HYLamwuvDw52Ucn2KtdF32Z5UtTa", + "1XWY3hy82sX0tbpfZZXA0FX6MvWZi2pR7qqSLqYrF16xs5+Eq47mFad7c/BzG3QFtSf10qu635uh/YrK", + "cLrsU56mmC9tJVJb8AulVlFpoDEquGalYNu+LQxQvF3ye+MLH6T7mZK6BfLlMaxwrAApJt9tVu3jpEwV", + "E5Pv5cdj/R2Vlmu6ZRN3o6b1FMhr6j2V8vglDDImGqTkmCuf4cSA/L7+dgoI6dwCj02LSpQT5SHvuZP3", + "kjqrNw5vnP91k7XlOMUwA35T1oVsvzlf0cOFk35LqGKLAWUhqpmTbbnQ/UeaGpbQewzGG3jVSKr21jer", + "X5o+OugXyNX6qKUmG9SzUgNxqPpYqeypO/4yuGNR+VT1e/XXEcA21MhTY7x+PWIMv7LgYxj8dQSdqyU/", + "deefx5C6qNpY1ZyfTUE9XBaVLRR7ycOmyt4aWm1ihRh2pt5Cq8PNHf5CiRe7wE2h/m9GKvaWkRt0Wuf2", + "0KZ+jyylmhXwD92wY93wAkTTcQTCnigiPJPAPavGK3NaFVMrnL6YOpNdtJrK50TI06LVEzlvI0fTY43o", + "7bFXg+U6iEuaKvVXV1pRvUxIMr8TENaC9av+r6lDe4y+09LbbLf1NqRl+gyfVy9eua287fFMzDdItdVe", + "8BhqsTSV/N8cvxuuQ9i8/8O1kVDW9x+uxibfy7ckO93/CoO/OL9/x1yzhrO9sZXXjjqmxVr/JJBdfZ5T", + "SVJA5f3Tpyi8fqPRe6a04rMP5reJXzt64t0d7GPCt8X9r63xYqWmWFOxbX0gKr2XNDA1TxMS75FJXXWh", + "OGjaDcc2PEE5lGlXnx0cqiRXH/UcyvEtDzYOtQEb36vT67Ubvm81C8ylxSaO3bx5ULvKvOPwSP2qcc+j", + "JSuvSA63G5oe9n1OkVpXMAYZEC2vFD6zXD1dz0eVWtJ9qr6pBPUWeblpukZzxD2rUD5yWtZN1lvB5zPk", + "iiU/p95/Eq88/hOZD/5bAc+r+nVd/XohRb8K+OY3gMZ3GXa8DTQ/J9BkOmHKKIlw4p6SsW+IQOxbUzpI", + "gynCkqUkcs+golsso8WPHePfasdwZfWfXbLtdZYtB3tqt612LMb1u04NAvy29lg2myFGoXil2aQ7/fPK", + "6O4dpTevxoBbfyj96erhzesxkNdfOVcD/DIK9YQAlccJE+Av+PMpKdV5zKq/xxLu8fKapMByuSElZ18M", + "eX4lZwDZtpKrXp/atZKrXZRqSvWxZ0ex//yzuVTzQ7X9MFwGyXT54M+zS3Vx+Wc3EamVu0YvOSblDBf3", + "sDGacZbabD0cSfc0N8RIXx36If4/xH+Q+PuPgz27AjDvYLimx5V68tsISTe/N7JrNdDyCkhzVMI0RTHM", + "9KtejAqkq7wSOkcYURabQx6dvvTvF4J4ubLYmjtXPFJrXud//gzhwu7MmCCS8SJMeGwAbM6mflq+nMu6", + "W5jnX4th/nfvnLF0zxRp3jtmaUrkuDzt7WgOvwLuv0Sa749slzVyHgYqDDdvWwagtQO13rYeNsKIF/K3", + "x+6p7+IVAegihYYlCes6qDrSLa4wjW/ZQ8vRVDcOH64/nj+dJ94M7tt6A4TGYC6pGJzQe46zxd/OkTDI", + "WZLM1a9/JuVlIq1WG9WduS2kE3J1SzvgS0iSe/lqwxJrg3pjiCDbWdeTZNu5K33JskTlWpHjs9qb9AWn", + "6X+7ZNAOcJng5Zxr1v7XkENHmKxAzGR0dQnbDxn7IWOFjGnJQYyjdx/Opq1ytgCcyE4B+2BavKwVtgbC", + "1u98IsfnyBDK1pkEGi0REYgyiTjgeLm/IsgZ4xJFOFroipXcmCGl3jPD2UVIyB1861qDc3IHFIR4UavQ", + "hLDlapRYgNE9kQuWSxQtIPqqnOmCgEQnxyj0NQW/dV9AxjF5cQTYGRueeDRDmEMv390vQJdULbg3ro9Q", + "9NbV7PmdczpzngSHwSR4/PL4jwAAAP//", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, 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..ccab32ad --- /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..0cf1d276 --- /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 e0453567..00000000 --- a/internal/api/bulk/load/generation_route_test.go +++ /dev/null @@ -1,110 +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 (r *fakeGenerationRunner) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { - return ingest.LoadSummary{}, nil -} - -func TestCreateGenerationStagesCompleteBundle(t *testing.T) { - runner := &fakeGenerationRunner{summary: ingest.LoadSummary{Files: 2, VerticesInserted: 4}} - svc, err := NewService(ServiceConfig{Loader: 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{Loader: 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 165548a4..00000000 --- a/internal/api/bulk/load/memory_lifecycle_store_test.go +++ /dev/null @@ -1,192 +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) 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 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 -} - -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 index 5cda4061..ca274682 100644 --- a/internal/api/bulk/load/operations.go +++ b/internal/api/bulk/load/operations.go @@ -2,71 +2,17 @@ package load import ( "context" - "encoding/json" "errors" - "io" "mime/multipart" - "net/http" "os" "strconv" "strings" - httpapi "github.com/calypr/loom/internal/api/http" "github.com/calypr/loom/internal/authscope" dataframeerrors "github.com/calypr/loom/internal/dataframe/errors" - "github.com/calypr/loom/internal/dataset" "github.com/gofiber/fiber/v3" ) -// UploadProjectResource parses the generated multipart request and performs -// the resource import without constructing a Fiber context. The generated -// OpenAPI strict handler owns multipart framing; this method owns the load -// operation and its authorization policy. -func (h *Handler) UploadProjectResource(ctx context.Context, project, resourceType string, body *multipart.Reader, principal *authscope.Principal) (*ImportResult, error) { - if body == nil { - return nil, fiber.NewError(fiber.StatusUnsupportedMediaType) - } - project = strings.TrimSpace(project) - resourceType = strings.TrimSpace(resourceType) - if project == "" { - return nil, dataframeerrors.NewError(dataframeerrors.CodeProjectRequired, "") - } - if resourceType == "" { - return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidResourceType, "") - } - 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.NewError(dataframeerrors.CodeInvalidRequest, "") - } - if form == nil || len(form.File["file"]) == 0 { - return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - authResourcePath := strings.TrimSpace(firstFormValue(form.Value, "auth_resource_path")) - if err := h.authz.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { - return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") - } - authResourcePath = authscope.NormalizeAuthResourcePath(authResourcePath) - fileHeader := form.File["file"][0] - stagedPath, err := stageUploadedFile(fileHeader) - if err != nil { - return nil, 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(ctx, req) - if err != nil { - return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidData, "") - } - return result, 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) { @@ -81,11 +27,7 @@ func (h *Handler) CreateDatasetGeneration(ctx context.Context, project, generati return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidRequest, "") } project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) - if project == "" || generation == "" { - return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") - } - files := form.File["file"] - if len(files) == 0 { + if project == "" || generation == "" || len(form.File["file"]) == 0 { return nil, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") } authResourcePath := strings.TrimSpace(firstFormValue(form.Value, "auth_resource_path")) @@ -100,22 +42,24 @@ func (h *Handler) CreateDatasetGeneration(ctx context.Context, project, generati return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") } authResourcePath = authscope.NormalizeAuthResourcePath(authResourcePath) - stagedDir, err := stageGenerationFiles(files) + stagedDir, err := stageGenerationFiles(form.File["file"]) if err != nil { - if errors.Is(err, os.ErrInvalid) { + switch { + case errors.Is(err, os.ErrInvalid): return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeInvalidGenerationFile, "") - } - if errors.Is(err, os.ErrExist) { + case errors.Is(err, os.ErrExist): return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeDuplicateGenerationFile, "") + default: + return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeBackendUnavailable, "", dataframeerrors.WithRetryable(true)) } - 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 } - return h.service.RunGeneration(ctx, req) + 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) { @@ -127,204 +71,14 @@ func (h *Handler) ActivateDatasetGeneration(ctx context.Context, project, genera return nil, dataframeerrors.Wrap(err, dataframeerrors.CodeForbidden, "") } if err := h.service.ActivateGeneration(ctx, project, generation, executionID); err != nil { - return nil, dataframeerrors.Wrap(err, dataframeerrors.CodePublicationConflict, "") + return nil, NormalizeError(err) } return map[string]any{"project": project, "generation": generation, "dataframeExecutionId": executionID, "activated": true}, nil } -type SnapshotCreateRequest struct { - GitCommit string `json:"gitCommit"` - ExpectedResourceTypes []string `json:"expectedResourceTypes"` - AuthResourcePath string `json:"authResourcePath,omitempty"` -} - -func (h *Handler) CreateSnapshot(ctx context.Context, project, generation string, body []byte, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { - var request SnapshotCreateRequest - if err := json.Unmarshal(body, &request); err != nil { - return dataset.SnapshotGeneration{}, err - } - project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) - if request.GitCommit == "" { - request.GitCommit = generation - } - if request.GitCommit != generation { - return dataset.SnapshotGeneration{}, dataset.ErrSnapshotConflict - } - if err := h.authorizeSnapshotContext(ctx, principal, project, request.AuthResourcePath); err != nil { - return dataset.SnapshotGeneration{}, err - } - if h.snapshots == nil { - return dataset.SnapshotGeneration{}, errors.New("snapshot service dependencies are required") - } - return h.snapshots.CreateOrResume(ctx, project, generation, request.AuthResourcePath, request.ExpectedResourceTypes) -} - -func (h *Handler) SnapshotStatus(ctx context.Context, project, generation string, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { - project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) - result, err := h.snapshots.Status(ctx, project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - if err := h.authorizeSnapshotContext(ctx, principal, project, result.AuthResourcePath); err != nil { - return dataset.SnapshotGeneration{}, err - } - return result, nil -} - -func (h *Handler) UploadSnapshotResource(ctx context.Context, project, generation, resourceType, checksum string, body io.Reader, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { - project, generation, resourceType = strings.TrimSpace(project), strings.TrimSpace(generation), strings.TrimSpace(resourceType) - status, err := h.snapshots.Status(ctx, project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - if err := h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath); err != nil { - return dataset.SnapshotGeneration{}, err - } - content, err := io.ReadAll(body) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - return h.snapshots.Upload(ctx, project, generation, resourceType, checksum, content) -} - -func (h *Handler) FinalizeSnapshot(ctx context.Context, project, generation string, principal *authscope.Principal) (dataset.SnapshotGeneration, *GenerationLoadResult, error) { - project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) - status, err := h.snapshots.Status(ctx, project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, nil, err - } - if err := h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath); err != nil { - return dataset.SnapshotGeneration{}, nil, err - } - submittedBy := "" - if principal != nil { - submittedBy = principal.Subject - } - return h.snapshots.Finalize(ctx, project, generation, submittedBy) -} - -func (h *Handler) AbortSnapshot(ctx context.Context, project, generation string, principal *authscope.Principal) (dataset.SnapshotGeneration, error) { - project, generation = strings.TrimSpace(project), strings.TrimSpace(generation) - status, err := h.snapshots.Status(ctx, project, generation) - if err != nil { - return dataset.SnapshotGeneration{}, err - } - if err := h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath); err != nil { - return dataset.SnapshotGeneration{}, err - } - return h.snapshots.Abort(ctx, project, generation) -} - -func (h *Handler) CreateRelease(ctx context.Context, project string, body []byte, principal *authscope.Principal) (dataset.ProjectRelease, error) { - var request dataset.ActivationRequest - if err := json.Unmarshal(body, &request); err != nil { - return dataset.ProjectRelease{}, err - } - request.Project = strings.TrimSpace(project) - if err := h.authorizeReleaseGenerationContext(ctx, request.Project, request.Generation, principal); err != nil { - return dataset.ProjectRelease{}, err - } - return h.releases.Create(ctx, request) -} - -func (h *Handler) ActivateReleaseCompatibility(ctx context.Context, project string, body []byte, principal *authscope.Principal) (dataset.ActiveRelease, error) { - var request dataset.ActivationRequest - if err := json.Unmarshal(body, &request); err != nil { - return dataset.ActiveRelease{}, err - } - request.Project = strings.TrimSpace(project) - if err := h.authorizeReleaseGenerationContext(ctx, request.Project, request.Generation, principal); err != nil { - return dataset.ActiveRelease{}, err - } - return h.releases.Activate(ctx, request) -} - -func (h *Handler) ActiveRelease(ctx context.Context, project string, principal *authscope.Principal) (dataset.ActiveRelease, error) { - project = strings.TrimSpace(project) - active, err := h.releases.Active(ctx, project) - if err != nil { - return dataset.ActiveRelease{}, err - } - if err := h.authorizeReleaseGenerationContext(ctx, project, active.Release.Generation, principal); err != nil { - return dataset.ActiveRelease{}, err - } - return active, nil -} - -func (h *Handler) ReleaseStatus(ctx context.Context, project, release string, principal *authscope.Principal) (dataset.ProjectRelease, error) { - project, release = strings.TrimSpace(project), strings.TrimSpace(release) - value, err := h.releases.Releases.ReadRelease(ctx, project, release) - if err != nil { - return dataset.ProjectRelease{}, err - } - if err := h.authorizeReleaseGenerationContext(ctx, project, value.Generation, principal); err != nil { - return dataset.ProjectRelease{}, err - } - return value, nil -} - -func (h *Handler) ActivateRelease(ctx context.Context, project, release string, body []byte, principal *authscope.Principal) (dataset.ActiveRelease, error) { - project, release = strings.TrimSpace(project), strings.TrimSpace(release) - value, err := h.releases.Releases.ReadRelease(ctx, project, release) - if err != nil { - return dataset.ActiveRelease{}, err - } - if err := h.authorizeReleaseGenerationContext(ctx, project, value.Generation, principal); err != nil { - return dataset.ActiveRelease{}, err - } - var request struct { - ExpectedRevision int64 `json:"expectedRevision"` - } - if err := json.Unmarshal(body, &request); err != nil { - return dataset.ActiveRelease{}, err - } - return h.releases.ActivateExisting(ctx, project, release, request.ExpectedRevision) -} - -func (h *Handler) authorizeSnapshotContext(ctx context.Context, principal *authscope.Principal, project, authResourcePath string) error { - return h.authz.AuthorizeWrite(ctx, principal, project, authResourcePath) -} - -func (h *Handler) authorizeReleaseGenerationContext(ctx context.Context, project, generation string, principal *authscope.Principal) error { - status, err := h.snapshots.Status(ctx, project, generation) - if err != nil { - return err - } - return h.authorizeSnapshotContext(ctx, principal, project, status.AuthResourcePath) -} - func firstFormValue(values map[string][]string, key string) string { if items := values[key]; len(items) != 0 { return items[0] } return "" } - -// MapSnapshotError preserves the status and safe snapshot error envelope used -// by the legacy Fiber routes for generated OpenAPI callers. -func MapSnapshotError(err error, requestID string) (int, httpapi.ErrorResponse) { - 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: - mapped := httpapi.MapDataframeError(err, requestID) - return mapped.Status, mapped.Body - } - details := map[string]any(nil) - var requirements *dataset.ReleaseRequirementsError - if errors.As(err, &requirements) { - details = map[string]any{"verifications": requirements.Verifications} - } - return status, httpapi.ErrorResponse{Error: httpapi.HTTPErrorBody{Code: code, Message: message, Details: details, Retryable: retryable, RequestID: requestID}} -} diff --git a/internal/api/bulk/load/operations_test.go b/internal/api/bulk/load/operations_test.go new file mode 100644 index 00000000..a0da8425 --- /dev/null +++ b/internal/api/bulk/load/operations_test.go @@ -0,0 +1,63 @@ +package load + +import ( + "bytes" + "context" + "mime/multipart" + "testing" + + "github.com/calypr/loom/internal/authscope" + "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 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/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 64fc99d7..00000000 --- a/internal/api/bulk/load/resource_route_test.go +++ /dev/null @@ -1,112 +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 (r *capturingResourceRunner) RunGeneration(context.Context, GenerationLoadRequest, ingest.EventSink) (ingest.LoadSummary, error) { - return ingest.LoadSummary{}, nil -} - -func TestResourceRouteUsesPathIdentityAndMultipartFile(t *testing.T) { - runner := &capturingResourceRunner{summary: ingest.LoadSummary{Files: 1, VerticesInserted: 2}} - service, err := NewService(ServiceConfig{Loader: 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 44e8e08e..e316a9bf 100644 --- a/internal/api/bulk/load/service.go +++ b/internal/api/bulk/load/service.go @@ -21,26 +21,6 @@ type GenerationLoadRequest struct { 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"` @@ -61,27 +41,12 @@ type DataframeReleaseStore interface { GetPointer(context.Context, string) (dataframepublication.BundlePointer, error) } -// Loader is the single ingest boundary. Resource and generation loads are two -// request shapes over the same configured backend. -type Loader interface { - Run(ctx context.Context, req ImportRequest, sink ingest.EventSink) (ingest.LoadSummary, error) - RunGeneration(ctx context.Context, req GenerationLoadRequest, 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 { @@ -100,7 +65,7 @@ func (r IngestRunner) RunGeneration(ctx context.Context, req GenerationLoadReque } type ServiceConfig struct { - Loader Loader + LoadGeneration GenerationLoader Logger *slog.Logger OnSuccess func(project string) GenerationActivator GenerationActivator @@ -108,7 +73,7 @@ type ServiceConfig struct { } type Service struct { - loader Loader + loadGeneration GenerationLoader logger *slog.Logger onSuccess func(project string) generationActivator GenerationActivator @@ -116,14 +81,14 @@ type Service struct { } func NewService(cfg ServiceConfig) (*Service, error) { - if cfg.Loader == nil { + if cfg.LoadGeneration == nil { return nil, errors.New("load runner is required") } if cfg.Logger == nil { cfg.Logger = slog.Default() } return &Service{ - loader: cfg.Loader, + loadGeneration: cfg.LoadGeneration, logger: cfg.Logger, onSuccess: cfg.OnSuccess, generationActivator: cfg.GenerationActivator, @@ -131,37 +96,6 @@ func NewService(cfg ServiceConfig) (*Service, error) { }, nil } -func (s *Service) Run(ctx context.Context, req ImportRequest) (*ImportResult, error) { - if req.Project == "" { - return nil, errors.New("project is required") - } - 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.loader.Run(ctx, req, nil) - 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) - } - 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, - }, nil -} - 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") @@ -186,7 +120,7 @@ func (s *Service) RunGeneration(ctx context.Context, req GenerationLoadRequest) return nil, fmt.Errorf("inspect existing generation: %w", err) } } - summary, err := s.loader.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 diff --git a/internal/api/bulk/load/service_test.go b/internal/api/bulk/load/service_test.go index 0a0be8e8..0640c461 100644 --- a/internal/api/bulk/load/service_test.go +++ b/internal/api/bulk/load/service_test.go @@ -18,10 +18,6 @@ func (activationGenerationRunner) RunGeneration(context.Context, GenerationLoadR return ingest.LoadSummary{}, nil } -func (activationGenerationRunner) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { - return ingest.LoadSummary{}, nil -} - type activationManifestStore struct { manifest publication.Manifest readErr error @@ -68,7 +64,7 @@ func (s *activationReleaseStore) GetPointer(context.Context, string) (dataframep func TestRunGenerationPropagatesDeferActivationAndResult(t *testing.T) { runner := &activationCapturingRunner{} - svc, err := NewService(ServiceConfig{Loader: runner}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration}) if err != nil { t.Fatal(err) } @@ -97,15 +93,11 @@ func (r *activationCapturingRunner) RunGeneration(_ context.Context, request Gen return ingest.LoadSummary{}, nil } -func (r *activationCapturingRunner) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { - return ingest.LoadSummary{}, nil -} - func TestRunGenerationReusesReadyManifest(t *testing.T) { runner := &activationCapturingRunner{} manifest := activationManifest(t, "project-a", "generation-a") store := &activationManifestStore{manifest: manifest} - svc, err := NewService(ServiceConfig{Loader: runner, GenerationActivator: store}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: store}) if err != nil { t.Fatal(err) } @@ -126,7 +118,7 @@ func TestRunGenerationReusesReadyManifest(t *testing.T) { func TestRunGenerationLoadsWhenManifestDoesNotExist(t *testing.T) { runner := &activationCapturingRunner{} store := &activationManifestStore{readErr: fmt.Errorf("lookup: %w", publication.ErrManifestNotFound)} - svc, err := NewService(ServiceConfig{Loader: runner, GenerationActivator: store}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: store}) if err != nil { t.Fatal(err) } @@ -144,7 +136,7 @@ func TestRunGenerationLoadsWhenManifestDoesNotExist(t *testing.T) { func TestRunGenerationRejectsManifestLookupFailure(t *testing.T) { runner := &activationCapturingRunner{} store := &activationManifestStore{readErr: errors.New("database unavailable")} - svc, err := NewService(ServiceConfig{Loader: runner, GenerationActivator: store}) + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: store}) if err != nil { t.Fatal(err) } @@ -194,7 +186,8 @@ func TestActivateGenerationRejectsInvalidDataframeRelease(t *testing.T) { test.mutate(&execution, &pointer) activator := &activationManifestStore{manifest: readyManifest} releases := &activationReleaseStore{execution: execution, pointer: pointer} - svc, err := NewService(ServiceConfig{Loader: activationGenerationRunner{}, GenerationActivator: activator, DataframeReleases: releases}) + runner := activationGenerationRunner{} + svc, err := NewService(ServiceConfig{LoadGeneration: runner.RunGeneration, GenerationActivator: activator, DataframeReleases: releases}) if err != nil { t.Fatal(err) } @@ -221,7 +214,7 @@ func TestActivateGenerationActivatesPublishedAllOutputBundle(t *testing.T) { activator := &activationManifestStore{manifest: manifest} calledSuccess := false svc, err := NewService(ServiceConfig{ - Loader: activationGenerationRunner{}, + LoadGeneration: activationGenerationRunner{}.RunGeneration, GenerationActivator: activator, DataframeReleases: &activationReleaseStore{execution: execution, pointer: dataframepublication.BundlePointer{Name: execution.PointerName(), ExecutionID: execution.ID}}, OnSuccess: func(string) { calledSuccess = true }, 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 ea9f0ff7..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{Loader: 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 14df2d73..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 Loader - 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 Loader, 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 bd286ca3..00000000 --- a/internal/api/bulk/load/snapshot_service_test.go +++ /dev/null @@ -1,151 +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 (f *snapshotRunnerFixture) Run(context.Context, ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { - return ingest.LoadSummary{}, nil -} - -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 bb7c62dd..00000000 --- a/internal/api/bulk/load/test_server_helpers_test.go +++ /dev/null @@ -1,52 +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) - 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/http/errors.go b/internal/api/http/errors.go index 12872b5f..934ed3c0 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,27 +106,6 @@ 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, "-", "_") diff --git a/internal/api/http/errors_test.go b/internal/api/http/errors_test.go index dffa6659..2f3e4465 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") 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/routes.go b/internal/api/http/routes.go index 3cb321b2..b4f47b11 100644 --- a/internal/api/http/routes.go +++ b/internal/api/http/routes.go @@ -11,11 +11,6 @@ func (s *HTTPServer) register() { s.app.Use(s.requestIDMiddleware, s.recoveryMiddleware, s.loggingMiddleware, s.authenticationMiddleware) } -func (s *HTTPServer) HandleHealth(c fiber.Ctx) error { - body, status := s.Health(c.Context()) - return c.Status(status).JSON(body) -} - // 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) { @@ -31,20 +26,10 @@ func (s *HTTPServer) Health(parent context.Context) (map[string]any, int) { return healthBody(result), result.httpStatus } -func (s *HTTPServer) HandleLiveness(c fiber.Ctx) error { - body, status := s.Liveness(c.Context()) - return c.Status(status).JSON(body) -} - 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 { - body, status := s.Readiness(c.Context()) - return c.Status(status).JSON(body) -} - func (s *HTTPServer) Readiness(parent context.Context) (map[string]any, int) { ctx, cancel := context.WithTimeout(parent, 2*time.Second) defer cancel() diff --git a/internal/catalog/arango/fields.go b/internal/catalog/arango/fields.go index 629d18f7..952b575a 100644 --- a/internal/catalog/arango/fields.go +++ b/internal/catalog/arango/fields.go @@ -2,8 +2,11 @@ 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" ) @@ -91,7 +94,35 @@ func (s *Store) DiscoverFieldEnrichment(ctx context.Context, opts catalog.FieldE } 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) + 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 = ` diff --git a/internal/catalog/arango/relationships.go b/internal/catalog/arango/relationships.go index 12707ee7..5b3d4831 100644 --- a/internal/catalog/arango/relationships.go +++ b/internal/catalog/arango/relationships.go @@ -2,10 +2,12 @@ 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" @@ -147,10 +149,59 @@ func (s *Store) DiscoverRelationshipObservations(ctx context.Context, opts catal } 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) + 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 { - return catalog.AccumulateRelationshipCatalog(ctx, s.client, docs, timings) + 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 { diff --git a/internal/catalog/write_persist.go b/internal/catalog/write_persist.go deleted file mode 100644 index e1482427..00000000 --- a/internal/catalog/write_persist.go +++ /dev/null @@ -1,119 +0,0 @@ -package catalog - -import ( - "context" - "encoding/json" - "time" - - "github.com/bytedance/sonic" - store "github.com/calypr/loom/internal/store/arango" -) - -func WriteFieldCatalog(ctx context.Context, client store.BatchInserter, 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 store.BatchInserter, 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 store.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/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 819fcfa3..00000000 --- a/internal/dataset/arango/snapshot_release_store.go +++ /dev/null @@ -1,491 +0,0 @@ -package arango - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/calypr/loom/internal/dataset" - arangostore "github.com/calypr/loom/internal/store/arango" -) - -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) 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) { - return CompareAndSwapActivateRelease(ctx, s.client, release, expectedRevision) -} - -// CompareAndSwapActivateRelease performs the release visibility switch using -// the supplied transaction-scoped queryer. Explorer publication uses this to -// commit its active revision and 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 (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 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 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 90c2f176..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) diff --git a/internal/dataset/memory_store_test.go b/internal/dataset/memory_store_test.go index 38142568..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,101 +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) ReadActiveRelease(_ context.Context, project string) (ActiveRelease, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -158,16 +70,6 @@ func (s *MemoryLifecycleStore) CompareAndSwapActivateRelease(_ context.Context, return cloneActiveRelease(active), 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...) @@ -179,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 539b9ab6..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,16 +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) } 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 @@ -113,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 { @@ -124,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() { @@ -149,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)) @@ -247,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 5ed490a1..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,85 +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 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 edb514a7..00000000 --- a/internal/dataset/snapshot.go +++ /dev/null @@ -1,156 +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) -} 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/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/server/config_test.go b/internal/server/config_test.go index 145075a6..2f4098b7 100644 --- a/internal/server/config_test.go +++ b/internal/server/config_test.go @@ -121,15 +121,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()) 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 { + 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/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 0c2fd383..0f2e97bc 100644 --- a/internal/server/explorer_authoring_http.go +++ b/internal/server/explorer_authoring_http.go @@ -1,56 +1,6 @@ package server -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "log/slog" - "strings" - - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/lifecycle" - "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} @@ -63,104 +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 authoringHTTPError(c fiber.Ctx, err error) error { - requestID := requestIDFromFiber(c) - var applicationErr *lifecycle.Error - if errors.As(err, &applicationErr) { - status := lifecycleErrorStatus(applicationErr.Class) - details := applicationErr.Details - if details == nil { - details = map[string]any{} - } - logAuthoringRequestFailure(c, requestID, status, applicationErr.Stage, applicationErr.Code, applicationErr.Path, applicationErr.Message, applicationErr.Details, applicationErr.Cause) - diagnostic := fiber.Map{"severity": "error", "stage": applicationErr.Stage, "code": applicationErr.Code, "path": applicationErr.Path, "message": applicationErr.Message, "requestId": requestID} - return c.Status(status).JSON(fiber.Map{"code": applicationErr.Code, "message": applicationErr.Message, "diagnostics": []fiber.Map{diagnostic}, "requestId": requestID, "details": details}) - } - 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 c7476a7f..00000000 --- a/internal/server/explorer_authoring_v2_routes.go +++ /dev/null @@ -1,207 +0,0 @@ -package server - -import ( - "context" - "errors" - "strings" - - explorerv2api "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" - explorercompilation "github.com/calypr/loom/internal/explorer/compilation" - "github.com/calypr/loom/internal/explorer/lifecycle" - "github.com/gofiber/fiber/v3" -) - -type explorerAuthoringHandlers struct { - getCapability fiber.Handler - searchSuggestions fiber.Handler - getBuilder fiber.Handler - applyCommands fiber.Handler - compileBuilder fiber.Handler - reconcile fiber.Handler - preview fiber.Handler - publish fiber.Handler -} - -// newExplorerAuthoringHandlers is a transport adapter. All Builder, receipt, -// preview, and publication policy is implemented by lifecycle.Service. -func newExplorerAuthoringHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, capabilities ExplorerV2LifecycleConfig) *explorerAuthoringHandlers { - handlers := &explorerAuthoringHandlers{} - if authorizer == nil || authorizeRead == nil || explorers == nil { - return handlers - } - application := newExplorerLifecycleApplication(explorers, capabilities) - 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.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 { - return authoringHTTPError(c, malformedRouteError("suggestions", err)) - } - query := "" - if request.Query != nil { - query = *request.Query - } - value, err := application.Suggestions(c.Context(), lifecycle.SuggestionsRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), SnapshotToken: request.SnapshotToken, NodeID: request.NodeId, Query: query}) - if err != nil { - return authoringHTTPError(c, err) - } - return c.JSON(fiber.Map{"apiVersion": authoringv2.APIVersion, "kind": "ExplorerBuilderCandidateSuggestions", "snapshotToken": value.SnapshotToken, "nodeId": value.NodeID, "candidates": value.Candidates, "diagnostics": []any{}}) - } - handlers.getBuilder = func(c fiber.Ctx) error { - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - value, err := application.Builder(c.Context(), lifecycle.BuilderRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId"))}) - if err != nil { - return authoringHTTPError(c, err) - } - return c.JSON(value) - } - 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)) - } - value, err := application.ApplyCommands(c.Context(), explorerProjectParam(c), strings.TrimSpace(c.Params("explorerId")), request, subjectFromFiber(c)) - if err != nil { - return authoringHTTPError(c, err) - } - return c.JSON(value) - } - handlers.compileBuilder = 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)) - } - receipt, err := application.Compile(c.Context(), lifecycle.CompileRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), Workspace: request.Workspace, SnapshotToken: request.SnapshotToken, RequestID: requestIDFromFiber(c)}) - if err != nil { - return authoringHTTPError(c, err) - } - return c.JSON(v2ReceiptResponse(receipt, request.Workspace)) - } - handlers.reconcile = func(c fiber.Ctx) error { - if err := authoringWrite(c, authorizer); err != nil { - return err - } - var request explorerv2api.ReconcileRequest - if err := decodeAuthoringStrict(c.Body(), &request); err != nil { - return authoringHTTPError(c, malformedRouteError("reconcile", err)) - } - receipt, err := application.Reconcile(c.Context(), lifecycle.ReconcileRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), SnapshotToken: request.SnapshotToken, DraftVersion: request.DraftVersion, DraftDigest: request.DraftDigest}) - if err != nil { - return authoringHTTPError(c, err) - } - return c.JSON(v2ReceiptResponse(receipt, authoringv2.Workspace{})) - } - handlers.preview = func(c fiber.Ctx) error { - if err := authoringRead(c, authorizeRead); err != nil { - return err - } - previewCtx, cancel := context.WithTimeout(c.Context(), explorerPreviewTimeout) - defer cancel() - var request explorerv2api.PreviewRequest - if err := decodeAuthoringStrict(c.Body(), &request); err != nil { - return authoringHTTPError(c, malformedRouteError("preview", err)) - } - var finish func() ([]byte, error) - _, err := application.Preview(previewCtx, lifecycle.PreviewRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), ReceiptID: request.ReceiptId, OutputID: request.OutputId, Limit: previewLimit(request.Limit), SinkFactory: func(receipt *explorer.CompilationReceipt, columns []explorer.EmittedColumn) (func(map[string]any) error, error) { - encoder, encoderErr := newPreviewResponseEncoder(receipt, request.OutputId, columns, maxExplorerPreviewResponseBytes) - if encoderErr != nil { - return nil, encoderErr - } - finish = encoder.Finish - return encoder.Visit, nil - }}) - if err != nil { - return authoringHTTPError(c, err) - } - if finish == nil { - return previewRouteFailure(c, errors.New("native preview response sink was not configured")) - } - encoded, err := finish() - if err != nil { - return previewRouteFailure(c, err) - } - 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 { - return authoringHTTPError(c, malformedRouteError("publish", err)) - } - value, err := application.Publish(c.Context(), lifecycle.PublishRequest{Project: explorerProjectParam(c), ExplorerID: strings.TrimSpace(c.Params("explorerId")), ReceiptID: request.ReceiptId, Actor: subjectFromFiber(c)}) - if err != nil { - return authoringHTTPError(c, err) - } - outputs := make([]explorerv2api.PublicationOutput, 0, len(value.Revision.Materializations)) - for _, materialization := range value.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: value.Receipt.ID, RevisionId: value.Revision.ID, State: string(value.Revision.Status), Outputs: outputs, Diagnostics: []explorerv2api.Diagnostic{}}) - } - return handlers -} - -func previewLimit(value *int) int { - if value == nil { - return dataframeexecution.DefaultPreviewLimit - } - if *value == 0 { - return -1 - } - return *value -} - -// v2ReceiptResponse is transport conversion only; receipt construction and -// validation belong to lifecycle.Service. -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 bc8d274a..00000000 --- a/internal/server/explorer_builder_lifecycle.go +++ /dev/null @@ -1,71 +0,0 @@ -package server - -import ( - "net/http" - "strings" - - "github.com/calypr/loom/internal/authscope" - "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/lifecycle" - "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 - } - application := newExplorerLifecycleApplication(explorers, capabilities) - 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") - } - value, err := application.List(c.Context(), project) - if err != nil { - return authoringHTTPError(c, err) - } - return c.JSON(value.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 := application.Get(c.Context(), project, id) - if err != nil { - return authoringHTTPError(c, err) - } - 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") - } - value, err := application.Create(c.Context(), lifecycle.CreateRequest{Project: project, Name: request.Name, Title: request.Title, SourceExplorerID: request.SourceExplorerID, Actor: subjectFromFiber(c)}) - if err != nil { - return authoringHTTPError(c, err) - } - return c.Status(http.StatusCreated).JSON(value) - } - return handlers -} diff --git a/internal/server/explorer_config_route.go b/internal/server/explorer_config_route.go index 1508711a..d1963e99 100644 --- a/internal/server/explorer_config_route.go +++ b/internal/server/explorer_config_route.go @@ -2,76 +2,31 @@ package server import ( "context" - "errors" - "net/http" - "strings" "github.com/calypr/loom/internal/authscope" "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/authoringv2" "github.com/calypr/loom/internal/explorer/lifecycle" - "github.com/gofiber/fiber/v3" ) 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 - lifecycle *explorerLifecycleHandlers - authoring *explorerAuthoringHandlers - authorizer authscope.Authorizer - authorizeRead explorerConfigReadAuthorizer - explorers *explorer.Service - lifecycleConfig ExplorerV2LifecycleConfig - application *lifecycle.Service + authorizer authscope.Authorizer + authorizeRead explorerConfigReadAuthorizer + application *lifecycle.Service } -// newExplorerHTTPHandlers wires transport adapters to the single Explorer -// application service. HTTP response conversion and deployment adapters stay -// in server; lifecycle policy does not. func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, configs ...ExplorerV2LifecycleConfig) *explorerHTTPHandlers { - handlers := &explorerHTTPHandlers{authorizer: authorizer, authorizeRead: authorizeRead, explorers: explorers} config := ExplorerV2LifecycleConfig{} if len(configs) > 0 { config = configs[0] } - handlers.lifecycleConfig = config - handlers.application = newExplorerLifecycleApplication(explorers, config) - if authorizer == nil || authorizeRead == nil || explorers == nil || handlers.application == nil { - return handlers + return &explorerHTTPHandlers{ + authorizer: authorizer, + authorizeRead: authorizeRead, + application: newExplorerLifecycleApplication(explorers, config), } - 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"}) - } - path := authscope.NormalizeAuthResourcePath(strings.TrimSpace(c.Query("auth_resource_path"))) - if err := authorizer.AuthorizeWrite(c.Context(), principalFromFiber(c), project, path); 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"}) - } - value, err := handlers.application.PublishRepository(c.Context(), lifecycle.RepositoryPublishRequest{Project: project, Generation: generation, Workspace: workspace, Commit: commit, Actor: subjectFromFiber(c)}) - if err != nil { - status := http.StatusInternalServerError - var applicationErr *lifecycle.Error - if errors.As(err, &applicationErr) { - status = lifecycleErrorStatus(applicationErr.Class) - } - return c.Status(status).JSON(fiber.Map{"error": err.Error()}) - } - return c.Status(http.StatusOK).JSON(fiber.Map{"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}) - } - handlers.lifecycle = newExplorerLifecycleHandlers(authorizer, authorizeRead, explorers, config) - handlers.authoring = newExplorerAuthoringHandlers(authorizer, authorizeRead, explorers, config) - return handlers } diff --git a/internal/server/explorer_config_route_test.go b/internal/server/explorer_config_route_test.go index a57734f1..639a3e4d 100644 --- a/internal/server/explorer_config_route_test.go +++ b/internal/server/explorer_config_route_test.go @@ -54,7 +54,7 @@ func TestRepositoryDeploymentPersistsExecutableDataframeSelectors(t *testing.T) }, ActivateRelease: activateRelease, } - registerTestExplorerRoutes(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) 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 { diff --git a/internal/server/explorer_preview_response.go b/internal/server/explorer_preview_response.go index eab15d09..16aa8bc4 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 ( @@ -27,14 +26,9 @@ type receiptPreviewResolutionError struct{ 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 { - return authoringHTTPError(c, previewRouteError(err)) -} - // previewRouteError maps execution failures to the stable authoring error // contract without requiring a Fiber context. The generated OpenAPI adapter -// uses this form directly, while the legacy Fiber route above only adds the -// transport response encoding. +// uses this form directly. func previewRouteError(err error) error { if errors.Is(err, ErrPreviewResponseTooLarge) { 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} diff --git a/internal/server/explorer_preview_response_test.go b/internal/server/explorer_preview_response_test.go index f205bb07..6e1c3893 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,13 +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) } } -func TestPreviewRouteFailurePreservesStableClassifications(t *testing.T) { +func TestPreviewErrorPreservesStableClassifications(t *testing.T) { tests := []struct { name string err error @@ -70,17 +68,9 @@ func TestPreviewRouteFailurePreservesStableClassifications(t *testing.T) { } 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) - } - if test.name == "receipt" && (!strings.Contains(response.Body, `"component":"output_execution"`) || !strings.Contains(response.Body, `"outputId":"patients"`)) { - t.Fatalf("receipt mismatch details missing: %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) } }) } diff --git a/internal/server/explorer_receipt_contract.go b/internal/server/explorer_receipt_contract.go index 14acc5d7..3afa274b 100644 --- a/internal/server/explorer_receipt_contract.go +++ b/internal/server/explorer_receipt_contract.go @@ -5,17 +5,114 @@ import ( "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/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" ) +func compileExplorerReceipt(ctx context.Context, request ExplorerV2ReceiptCompileRequest, 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 + } + presentationByEmission := make(map[string]explorercompilation.PresentationColumn) + for _, presentation := range translated.Presentations { + for _, column := range presentation.Columns { + presentationByEmission[column.EmissionID] = column + } + } + emitted := make([]explorer.EmittedColumn, 0, len(translated.EmittedColumns)) + 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, 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: mappings, EmittedColumns: emitted, 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 diff --git a/internal/server/explorer_receipt_contract_test.go b/internal/server/explorer_receipt_contract_test.go index 952cf3bf..6b79616d 100644 --- a/internal/server/explorer_receipt_contract_test.go +++ b/internal/server/explorer_receipt_contract_test.go @@ -255,7 +255,7 @@ func TestNativeV2RouteUsesAuthorizedPersistedReceipt(t *testing.T) { }, } 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) body := `{"workspace":` + string(baselineExplorerWorkspaceV2()) + `,"snapshotToken":"` + snapshot.Token + `"}` compiled := requestJSON(t, app, http.MethodPost, "/api/v1/projects/project-a/explorers/custom/authoring/v2/builder", body) if compiled.StatusCode != http.StatusOK { @@ -333,7 +333,7 @@ func TestBuilderCommandsCreateBackendOwnedDraftAndReconcileIt(t *testing.T) { }, } 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) @@ -388,7 +388,7 @@ func TestCreateExplorerFromCurrentClonesWorkspaceOnServer(t *testing.T) { 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, ExplorerV2LifecycleConfig{}) 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) 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 c2169b4a..00000000 --- a/internal/server/explorer_route_test_helpers_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package server - -import ( - "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.Post(base+"/suggestions", h.searchSuggestions) - router.Get(base+"/builder", h.getBuilder) - router.Post(base+"/builder", h.compileBuilder) - router.Post(base+"/commands", h.applyCommands) - router.Post(base+"/reconcile", h.reconcile) - 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, config ExplorerV2LifecycleConfig) { - h := newExplorerHTTPHandlers(authorizer, authorizeRead, service, config) - router.Post("/api/v1/projects/:project/generations/:generation/explorer-config", h.publishRepositoryConfig) - 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..1c93ade2 100644 --- a/internal/server/explorer_state_route_test.go +++ b/internal/server/explorer_state_route_test.go @@ -47,7 +47,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, ExplorerV2LifecycleConfig{}) 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) @@ -147,7 +147,7 @@ func TestExplorerStateRouteReturnsExplicitUnpublishedProjection(t *testing.T) { } 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, ExplorerV2LifecycleConfig{}) 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/openapi_data_routes.go b/internal/server/openapi_data_routes.go index 1cb07852..99cf579c 100644 --- a/internal/server/openapi_data_routes.go +++ b/internal/server/openapi_data_routes.go @@ -3,11 +3,9 @@ package server import ( "context" "encoding/json" - "errors" "net/http" loomapi "github.com/calypr/loom/generated/loomapi" - loadapi "github.com/calypr/loom/internal/api/bulk/load" httpapi "github.com/calypr/loom/internal/api/http" "github.com/calypr/loom/internal/authscope" ) @@ -54,37 +52,6 @@ func (r *HTTPRoutes) GetReadiness(ctx context.Context, _ loomapi.GetReadinessReq return nil, unexpectedResponseStatus("getReadiness", status) } -func (r *HTTPRoutes) UploadProjectResource(ctx context.Context, request loomapi.UploadProjectResourceRequestObject) (loomapi.UploadProjectResourceResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - result, err := r.load.UploadProjectResource(ctx, request.Project, request.ResourceType, request.Body, principal) - if err == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.UploadProjectResource200JSONResponse(value), nil - } - status, body := mapServiceError(err, ctx) - switch status { - case http.StatusBadRequest: - return loomapi.UploadProjectResource400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(body)}, nil - case http.StatusUnauthorized: - return loomapi.UploadProjectResource401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(body)}, nil - case http.StatusForbidden: - return loomapi.UploadProjectResource403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(body)}, nil - case http.StatusUnsupportedMediaType: - return loomapi.UploadProjectResource415JSONResponse{ServiceUnsupportedMediaTypeJSONResponse: loomapi.ServiceUnsupportedMediaTypeJSONResponse(body)}, nil - case http.StatusUnprocessableEntity: - return loomapi.UploadProjectResource422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(body)}, nil - case http.StatusInternalServerError: - return loomapi.UploadProjectResource500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(body)}, nil - case http.StatusServiceUnavailable: - return loomapi.UploadProjectResource503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(body)}, nil - default: - return nil, unexpectedResponseStatus("uploadProjectResource", status) - } -} - 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) @@ -147,332 +114,8 @@ func (r *HTTPRoutes) ActivateDatasetGeneration(ctx context.Context, request loom } } -func (r *HTTPRoutes) CreateSnapshot(ctx context.Context, request loomapi.CreateSnapshotRequestObject) (loomapi.CreateSnapshotResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - body, err := marshalRequestBody(request.Body) - if err == nil { - result, operationErr := r.load.CreateSnapshot(ctx, request.Project, request.Generation, body, principal) - if operationErr == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.CreateSnapshot200JSONResponse(value), nil - } - err = operationErr - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusBadRequest: - return loomapi.CreateSnapshot400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil - case http.StatusUnauthorized: - return loomapi.CreateSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.CreateSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusConflict: - return loomapi.CreateSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.CreateSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.CreateSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("createSnapshot", status) - } -} - -func (r *HTTPRoutes) GetSnapshot(ctx context.Context, request loomapi.GetSnapshotRequestObject) (loomapi.GetSnapshotResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - result, err := r.load.SnapshotStatus(ctx, request.Project, request.Generation, principal) - if err == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.GetSnapshot200JSONResponse(value), nil - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusUnauthorized: - return loomapi.GetSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.GetSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.GetSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.GetSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.GetSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("getSnapshot", status) - } -} - -func (r *HTTPRoutes) AbortSnapshot(ctx context.Context, request loomapi.AbortSnapshotRequestObject) (loomapi.AbortSnapshotResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - result, err := r.load.AbortSnapshot(ctx, request.Project, request.Generation, principal) - if err == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.AbortSnapshot200JSONResponse(value), nil - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusUnauthorized: - return loomapi.AbortSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.AbortSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.AbortSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusConflict: - return loomapi.AbortSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.AbortSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.AbortSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("abortSnapshot", status) - } -} - -func (r *HTTPRoutes) UploadSnapshotResource(ctx context.Context, request loomapi.UploadSnapshotResourceRequestObject) (loomapi.UploadSnapshotResourceResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - result, err := r.load.UploadSnapshotResource(ctx, request.Project, request.Generation, request.ResourceType, optionalString(request.Params.XContentSHA256), request.Body, principal) - if err == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.UploadSnapshotResource200JSONResponse(value), nil - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusBadRequest: - return loomapi.UploadSnapshotResource400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil - case http.StatusUnauthorized: - return loomapi.UploadSnapshotResource401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.UploadSnapshotResource403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.UploadSnapshotResource404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusConflict: - return loomapi.UploadSnapshotResource409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil - case http.StatusUnprocessableEntity: - return loomapi.UploadSnapshotResource422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.UploadSnapshotResource500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.UploadSnapshotResource503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("uploadSnapshotResource", status) - } -} - -func (r *HTTPRoutes) FinalizeSnapshot(ctx context.Context, request loomapi.FinalizeSnapshotRequestObject) (loomapi.FinalizeSnapshotResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - generation, result, err := r.load.FinalizeSnapshot(ctx, request.Project, request.Generation, principal) - if err == nil { - value, conversionErr := rawJSON(map[string]any{"generation": generation, "load": result}) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.FinalizeSnapshot200JSONResponse(value), nil - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusBadRequest: - return loomapi.FinalizeSnapshot400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil - case http.StatusUnauthorized: - return loomapi.FinalizeSnapshot401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.FinalizeSnapshot403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.FinalizeSnapshot404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusConflict: - return loomapi.FinalizeSnapshot409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil - case http.StatusUnprocessableEntity: - return loomapi.FinalizeSnapshot422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.FinalizeSnapshot500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.FinalizeSnapshot503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("finalizeSnapshot", status) - } -} - -func (r *HTTPRoutes) CreateRelease(ctx context.Context, request loomapi.CreateReleaseRequestObject) (loomapi.CreateReleaseResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - body, err := marshalRequestBody(request.Body) - if err == nil { - result, operationErr := r.load.CreateRelease(ctx, request.Project, body, principal) - if operationErr == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.CreateRelease200JSONResponse(value), nil - } - err = operationErr - } - return r.createReleaseError(err, ctx) -} - -func (r *HTTPRoutes) createReleaseError(err error, ctx context.Context) (loomapi.CreateReleaseResponseObject, error) { - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusBadRequest: - return loomapi.CreateRelease400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil - case http.StatusUnauthorized: - return loomapi.CreateRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.CreateRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.CreateRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusConflict: - return loomapi.CreateRelease409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil - case http.StatusUnprocessableEntity: - return loomapi.CreateRelease422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.CreateRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.CreateRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("createRelease", status) - } -} - -func (r *HTTPRoutes) GetActiveRelease(ctx context.Context, request loomapi.GetActiveReleaseRequestObject) (loomapi.GetActiveReleaseResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - result, err := r.load.ActiveRelease(ctx, request.Project, principal) - if err == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.GetActiveRelease200JSONResponse(value), nil - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusUnauthorized: - return loomapi.GetActiveRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.GetActiveRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.GetActiveRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.GetActiveRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.GetActiveRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("getActiveRelease", status) - } -} - -func (r *HTTPRoutes) GetRelease(ctx context.Context, request loomapi.GetReleaseRequestObject) (loomapi.GetReleaseResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - result, err := r.load.ReleaseStatus(ctx, request.Project, request.Release, principal) - if err == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.GetRelease200JSONResponse(value), nil - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusUnauthorized: - return loomapi.GetRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.GetRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.GetRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.GetRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.GetRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("getRelease", status) - } -} - -func (r *HTTPRoutes) ActivateRelease(ctx context.Context, request loomapi.ActivateReleaseRequestObject) (loomapi.ActivateReleaseResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - body, err := marshalRequestBody(request.Body) - if err == nil { - result, operationErr := r.load.ActivateRelease(ctx, request.Project, request.Release, body, principal) - if operationErr == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.ActivateRelease200JSONResponse(value), nil - } - err = operationErr - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusBadRequest: - return loomapi.ActivateRelease400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil - case http.StatusUnauthorized: - return loomapi.ActivateRelease401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.ActivateRelease403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.ActivateRelease404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusConflict: - return loomapi.ActivateRelease409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil - case http.StatusUnprocessableEntity: - return loomapi.ActivateRelease422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.ActivateRelease500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.ActivateRelease503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("activateRelease", status) - } -} - -func (r *HTTPRoutes) ActivateReleaseCompatibility(ctx context.Context, request loomapi.ActivateReleaseCompatibilityRequestObject) (loomapi.ActivateReleaseCompatibilityResponseObject, error) { - principal, _ := authscope.PrincipalFromContext(ctx) - body, err := marshalRequestBody(request.Body) - if err == nil { - result, operationErr := r.load.ActivateReleaseCompatibility(ctx, request.Project, body, principal) - if operationErr == nil { - value, conversionErr := rawJSON(result) - if conversionErr != nil { - return nil, conversionErr - } - return loomapi.ActivateReleaseCompatibility200JSONResponse(value), nil - } - err = operationErr - } - status, mapped := snapshotError(err, ctx) - switch status { - case http.StatusBadRequest: - return loomapi.ActivateReleaseCompatibility400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(mapped)}, nil - case http.StatusUnauthorized: - return loomapi.ActivateReleaseCompatibility401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(mapped)}, nil - case http.StatusForbidden: - return loomapi.ActivateReleaseCompatibility403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(mapped)}, nil - case http.StatusNotFound: - return loomapi.ActivateReleaseCompatibility404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(mapped)}, nil - case http.StatusConflict: - return loomapi.ActivateReleaseCompatibility409JSONResponse{ServiceConflictJSONResponse: loomapi.ServiceConflictJSONResponse(mapped)}, nil - case http.StatusUnprocessableEntity: - return loomapi.ActivateReleaseCompatibility422JSONResponse{ServiceUnprocessableJSONResponse: loomapi.ServiceUnprocessableJSONResponse(mapped)}, nil - case http.StatusInternalServerError: - return loomapi.ActivateReleaseCompatibility500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(mapped)}, nil - case http.StatusServiceUnavailable: - return loomapi.ActivateReleaseCompatibility503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(mapped)}, nil - default: - return nil, unexpectedResponseStatus("activateReleaseCompatibility", status) - } -} - func (r *HTTPRoutes) GetRecipeExecution(ctx context.Context, request loomapi.GetRecipeExecutionRequestObject) (loomapi.GetRecipeExecutionResponseObject, error) { - body, status := r.recipe.Execute(ctx, request.Id) + body, status := r.recipeExecution(ctx, request.Id) value, err := rawJSON(body) if err != nil { return nil, err @@ -507,23 +150,11 @@ func optionalString(value *string) string { return *value } -func marshalRequestBody(body any) ([]byte, error) { - if body == nil { - return nil, errors.New("request body is required") - } - return json.Marshal(body) -} - func mapServiceError(err error, ctx context.Context) (int, loomapi.ServiceErrorResponse) { mapped := httpapi.MapDataframeError(err, httpapi.RequestIDFromContext(ctx)) return mapped.Status, serviceErrorResponse(mapped.Body) } -func snapshotError(err error, ctx context.Context) (int, loomapi.ServiceErrorResponse) { - status, body := loadapi.MapSnapshotError(err, httpapi.RequestIDFromContext(ctx)) - return status, serviceErrorResponse(body) -} - func rawJSON(value any) (loomapi.RawJSON, error) { if value == nil { return loomapi.RawJSON{}, nil diff --git a/internal/server/openapi_explorer_authoring_direct.go b/internal/server/openapi_explorer_authoring_direct.go index 6308b4cc..cd34a7cc 100644 --- a/internal/server/openapi_explorer_authoring_direct.go +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -171,14 +171,14 @@ func (h *explorerHTTPHandlers) previewAuthoringDirect(ctx context.Context, proje return encoder.Visit, nil }}) if err != nil { - return result, err + return result, previewRouteError(err) } if finish == nil { - return result, errors.New("native preview response sink was not configured") + return result, previewRouteError(errors.New("native preview response sink was not configured")) } encoded, err := finish() if err != nil { - return result, err + return result, previewRouteError(err) } if err := json.Unmarshal(encoded, &result); err != nil { return result, err 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/api/http/recipe_execution.go b/internal/server/openapi_recipe_execution.go similarity index 65% rename from internal/api/http/recipe_execution.go rename to internal/server/openapi_recipe_execution.go index 493692e6..f9dcf4b5 100644 --- a/internal/api/http/recipe_execution.go +++ b/internal/server/openapi_recipe_execution.go @@ -1,4 +1,4 @@ -package httpapi +package server import ( "context" @@ -8,36 +8,22 @@ import ( "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 { - body, status := h.Execute(c.Context(), c.Params("id")) - return c.Status(status).JSON(body) -} - -// Execute returns the legacy recipe execution document for the generated -// OpenAPI adapter. It deliberately keeps the historical not-found response -// indistinguishable for missing catalog entries and denied scopes. -func (h RecipeExecutionHandler) Execute(ctx context.Context, id string) (map[string]any, int) { - if h.Catalog == nil { +// 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 := h.Catalog.GetExecution(ctx, 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 h.Scopes != nil { + if r.scopes != nil { principal, _ := authscope.PrincipalFromContext(ctx) - if _, scopeErr := h.Scopes.ResolveReadScopeForGeneration(ctx, principal, execution.Project, execution.DatasetGeneration, execution.AuthResourcePaths); scopeErr != nil { + 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 @@ -53,12 +39,8 @@ func (h RecipeExecutionHandler) Execute(ctx context.Context, id string) (map[str if column.LogicalType != "" { logical = column.LogicalType } - if column.Nullable { - nullable = true - } - if column.Repeated { - repeated = true - } + 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}) @@ -66,10 +48,6 @@ func (h RecipeExecutionHandler) Execute(ctx context.Context, id string) (map[str 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 } -// 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) @@ -90,13 +68,13 @@ func columnCapabilities(clickHouseType string) (logical string, nullable, repeat switch { case strings.HasPrefix(typ, "Bool"): logical = "boolean" - case strings.HasPrefix(typ, "Int") || strings.HasPrefix(typ, "UInt"): + case strings.HasPrefix(typ, "Int"), strings.HasPrefix(typ, "UInt"): logical = "integer" - case strings.HasPrefix(typ, "Float") || strings.HasPrefix(typ, "Decimal"): + 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"): + case strings.HasPrefix(typ, "String"), strings.HasPrefix(typ, "FixedString"), strings.HasPrefix(typ, "UUID"): logical = "string" default: logical = "json" 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 9dea473f..d9455529 100644 --- a/internal/server/openapi_route_inventory_test.go +++ b/internal/server/openapi_route_inventory_test.go @@ -47,8 +47,8 @@ func TestGeneratedRoutesExactlyMatchOpenAPISpec(t *testing.T) { t.Errorf("registered route is absent from OpenAPI: %s", route) } } - if len(got) != 33 || len(want) != 33 { - t.Errorf("route count got=%d spec=%d, want 33", 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,7 +117,6 @@ 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("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 950aff1c..4e766d02 100644 --- a/internal/server/openapi_routes.go +++ b/internal/server/openapi_routes.go @@ -7,12 +7,15 @@ import ( 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/calypr/loom/internal/authscope" + "github.com/calypr/loom/internal/dataframe/publication" ) type HTTPRoutes struct { server *httpapi.HTTPServer load *loadapi.Handler - recipe httpapi.RecipeExecutionHandler + releases publication.BundleCatalog + scopes *authscope.ScopeResolver graphql graphapi.RouteConfig explorer *explorerHTTPHandlers } diff --git a/internal/server/openapi_test_helpers_test.go b/internal/server/openapi_test_helpers_test.go new file mode 100644 index 00000000..29018cc9 --- /dev/null +++ b/internal/server/openapi_test_helpers_test.go @@ -0,0 +1,17 @@ +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/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 ExplorerV2LifecycleConfig) { + 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 055618bc..224e2eb1 100644 --- a/internal/server/options.go +++ b/internal/server/options.go @@ -29,8 +29,6 @@ type ServerConfig struct { RecipeBatchBytes int `yaml:"recipe_batch_bytes"` AllowUnauthenticated bool `yaml:"allow_unauthenticated"` RequiredDataframeSelectors []dataset.DataframeSelector `yaml:"required_dataframe_selectors"` - SnapshotDirectory string `yaml:"snapshot_directory"` - SnapshotRetention time.Duration `yaml:"snapshot_retention"` } type ClickHouseConfig struct { @@ -68,7 +66,6 @@ func DefaultConfig() Config { 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, }, Auth: AuthConfig{Mode: "basic", Calypr: CalyprAuthConfig{RequestTimeout: 5 * time.Second, CacheTTL: 30 * time.Second}}, } @@ -181,16 +178,6 @@ 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 - } return cfg, nil } @@ -199,12 +186,6 @@ func (c Config) Validate() error { 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.SnapshotRetention <= 0 { - return fmt.Errorf("server.snapshot_retention must be positive") - } for index, selector := range cfg.Server.RequiredDataframeSelectors { if err := selector.Validate(); err != nil { return fmt.Errorf("server.required_dataframe_selectors[%d]: %w", index, err) 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/routes.go b/internal/server/routes.go index 93f6d20a..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,8 +29,9 @@ 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, } diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go index 7d24d38e..014899f4 100644 --- a/internal/server/routes_test.go +++ b/internal/server/routes_test.go @@ -16,10 +16,6 @@ import ( type routeRunner struct{} -func (routeRunner) Run(context.Context, loadapi.ImportRequest, ingest.EventSink) (ingest.LoadSummary, error) { - return ingest.LoadSummary{}, nil -} - func (routeRunner) RunGeneration(context.Context, loadapi.GenerationLoadRequest, ingest.EventSink) (ingest.LoadSummary, error) { return ingest.LoadSummary{}, nil } @@ -32,11 +28,11 @@ func TestRegisterRoutesExposesGenerationReleaseWorkflow(t *testing.T) { if err != nil { t.Fatal(err) } - resourceService, err := loadapi.NewService(loadapi.ServiceConfig{Loader: 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) } @@ -92,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 a2ff5f2c..b313f8fb 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,14 +4,12 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "errors" "flag" "fmt" "log/slog" "os" "os/signal" - "strings" "syscall" "time" @@ -35,9 +33,7 @@ import ( "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/ingest" - "github.com/calypr/loom/internal/projectid" arangostore "github.com/calypr/loom/internal/store/arango" clickhousestore "github.com/calypr/loom/internal/store/clickhouse" ) @@ -211,7 +207,7 @@ func run(ctx context.Context, serverConfig Config) error { } } verificationStore := publicationVerificationStore{executions: publishedRegistry} - releaseService := &publicationcontract.ReleaseService{Snapshots: lifecycleStore, Manifests: lifecycleStore, Releases: lifecycleStore, Verifier: verificationStore, Required: serverConfig.Server.RequiredDataframeSelectors} + 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) @@ -284,8 +280,8 @@ func run(ctx context.Context, serverConfig Config) error { ConnectionOptions: connOpts, Schema: serverConfig.Server.Schema, }} - resourceService, err := loadapi.NewService(loadapi.ServiceConfig{ - Loader: ingestRunner, + generationService, err := loadapi.NewService(loadapi.ServiceConfig{ + LoadGeneration: ingestRunner.RunGeneration, GenerationActivator: lifecycleStore, DataframeReleases: publishedRegistry, Logger: logger, @@ -297,110 +293,10 @@ 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. + return fmt.Errorf("create generation load service: %w", err) + } 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, 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: mappings, EmittedColumns: emitted, 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 - } - 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, receiptCompilationConflict(stored.ID, err) - } - 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 compileExplorerReceipt(ctx, request, capabilityResolver, recipeEngine, explorerService, logger) } lifecycleConfig := ExplorerV2LifecycleConfig{ CompileReceipt: compileReceipt, @@ -455,7 +351,7 @@ func run(ctx context.Context, serverConfig Config) error { } return scopeResolver.AuthorizeReadProject(ctx, principal, project) }, explorerService, lifecycleConfig) - if err := registerRoutes(server, resourceService, snapshotService, releaseService, authorizer, resolver, explorerHandlers, publishedRegistry, scopeResolver); err != nil { + if err := registerRoutes(server, generationService, authorizer, resolver, explorerHandlers, publishedRegistry, scopeResolver); err != nil { return fmt.Errorf("register HTTP routes: %w", err) } errCh := make(chan error, 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 298706f8..8d8cff2d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -238,36 +238,6 @@ paths: content: application/json: schema: {$ref: '#/components/schemas/RawJSON'} - /api/v1/projects/{project}/resources/{resourceType}: - 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} - 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/datasets/{project}/generations/{generation}: parameters: - $ref: '#/components/parameters/Project' @@ -330,223 +300,6 @@ paths: '409': {$ref: '#/components/responses/ServiceConflict'} '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 @@ -728,16 +481,6 @@ components: 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. From 0aaee4b1910141f27baab980663c58dad8fb084d Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sat, 29 Aug 2026 21:20:25 -0700 Subject: [PATCH 10/17] audit explorer package --- docs/EXPLORER_COMPILATION_ARCHITECTURE.md | 1 - docs/TODO.md | 37 +- generated/loomapi/api.gen.go | 373 +++-------- internal/explorer/arango/activation.go | 38 +- internal/explorer/arango/collections.go | 9 +- internal/explorer/arango/explorers.go | 5 +- internal/explorer/arango/repository.go | 87 ++- internal/explorer/arango/repository_test.go | 66 ++ internal/explorer/arango/revisions.go | 14 +- internal/explorer/arango/store_test.go | 7 +- internal/explorer/authoringv2/canonical.go | 48 -- .../explorer/authoringv2/semantic_types.go | 9 - internal/explorer/authoringv2/types.go | 69 +- internal/explorer/authoringv2/types_test.go | 74 --- internal/explorer/authoringv2/validation.go | 285 +------- .../explorer/authoringv2/workspace_test.go | 44 +- internal/explorer/compilation.go | 13 - internal/explorer/compilation/compile.go | 323 ++++++++++ .../explorer/compilation/semantic_compile.go | 13 +- internal/explorer/compilation/translate.go | 609 ------------------ .../explorer/compilation/translate_test.go | 110 ---- .../explorer/compilation/workspace_test.go | 5 +- internal/explorer/lifecycle/authoring.go | 96 +++ internal/explorer/lifecycle/preview.go | 61 ++ internal/explorer/lifecycle/publish.go | 82 +++ internal/explorer/lifecycle/query.go | 136 ++++ .../explorer/lifecycle/repository_publish.go | 80 +++ internal/explorer/lifecycle/service.go | 430 +------------ internal/explorer/lifecycle/service_test.go | 65 +- internal/explorer/lifecycle/types.go | 20 +- internal/explorer/service.go | 84 +-- internal/explorer/store.go | 7 +- internal/explorer/types.go | 111 +--- internal/explorer/viewer_projection.go | 4 +- internal/server/explorer_capability.go | 44 +- internal/server/explorer_config_route.go | 7 +- internal/server/explorer_config_route_test.go | 39 +- internal/server/explorer_lifecycle_adapter.go | 91 --- internal/server/explorer_lifecycle_types.go | 37 -- internal/server/explorer_publication.go | 20 +- internal/server/explorer_receipt_contract.go | 19 +- .../server/explorer_receipt_contract_test.go | 89 +-- internal/server/explorer_state_route_test.go | 21 +- internal/server/http_test_helpers_test.go | 35 +- .../openapi_explorer_authoring_direct.go | 15 - internal/server/openapi_explorer_routes.go | 30 - .../server/openapi_route_inventory_test.go | 4 +- internal/server/openapi_test_helpers_test.go | 3 +- internal/server/server.go | 22 +- openapi/openapi.yaml | 29 - 50 files changed, 1274 insertions(+), 2646 deletions(-) create mode 100644 internal/explorer/arango/repository_test.go create mode 100644 internal/explorer/compilation/compile.go delete mode 100644 internal/explorer/compilation/translate.go create mode 100644 internal/explorer/lifecycle/authoring.go create mode 100644 internal/explorer/lifecycle/preview.go create mode 100644 internal/explorer/lifecycle/publish.go create mode 100644 internal/explorer/lifecycle/query.go create mode 100644 internal/explorer/lifecycle/repository_publish.go delete mode 100644 internal/server/explorer_lifecycle_adapter.go delete mode 100644 internal/server/explorer_lifecycle_types.go diff --git a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md index c009ba11..3fcdc9e7 100644 --- a/docs/EXPLORER_COMPILATION_ARCHITECTURE.md +++ b/docs/EXPLORER_COMPILATION_ARCHITECTURE.md @@ -43,7 +43,6 @@ scoped Arango query ClickHouse outputs The HTTP endpoints are: - `GET .../authoring/v2/capability` -- `POST .../authoring/v2/builder` for direct workspace compilation - `POST .../authoring/v2/reconcile` for compilation of the exact persisted draft - `POST .../authoring/v2/preview` with `receiptId`, `outputId`, and an optional limit diff --git a/docs/TODO.md b/docs/TODO.md index aab09675..2f6e49e1 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,27 +1,20 @@ # Architectural TODOs -## Consolidate Explorer persistence boundaries +## Explorer persistence follow-up -Revisit the two broad, overlapping Explorer interfaces during the Explorer -package audit: +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. -- `internal/explorer/store.go` defines a 14-method `Store` covering repository - configuration, explorers, drafts, compilation receipts, revisions, - publication, and activation. -- `internal/explorer/lifecycle/types.go` defines a 12-method `Store` that - largely mirrors `explorer.Service` to support lifecycle orchestration and - tests. +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. -Do not mechanically split these into more interfaces. First decide which -Explorer workflows and compatibility tracks remain, assign persistence and -workflow ownership to their packages, and then remove or narrow the interfaces -that no longer represent real external boundaries. - -Completion criteria: - -- Explorer persistence responsibilities have explicit owners. -- Lifecycle orchestration does not mirror the entire Explorer service solely - for tests. -- Remaining interfaces are consumer-owned and limited to the workflows their - consumers actually execute. -- Obsolete workflow methods, adapters, fakes, and tests are deleted together. +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/generated/loomapi/api.gen.go b/generated/loomapi/api.gen.go index 937d55d2..00b9cf3b 100644 --- a/generated/loomapi/api.gen.go +++ b/generated/loomapi/api.gen.go @@ -597,12 +597,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"` @@ -975,9 +969,6 @@ 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 @@ -1342,9 +1333,6 @@ 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/capability) GetExplorerAuthoringCapability(c fiber.Ctx, project Project, explorerId ExplorerId) error @@ -1655,43 +1643,6 @@ func (siw *ServerInterfaceWrapper) GetExplorerBuilder(c fiber.Ctx) error { return handler(c) } -// CompileExplorerBuilder operation middleware -func (siw *ServerInterfaceWrapper) CompileExplorerBuilder(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()) - } - - handler := func(c fiber.Ctx) error { - return siw.Handler.CompileExplorerBuilder(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) -} - // GetExplorerAuthoringCapability operation middleware func (siw *ServerInterfaceWrapper) GetExplorerAuthoringCapability(c fiber.Ctx) error { @@ -2143,8 +2094,6 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Get(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/builder", wrapper.GetExplorerBuilder) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/builder", wrapper.CompileExplorerBuilder) - router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/commands", wrapper.ApplyExplorerBuilderCommands) router.Post(options.BaseURL+"/api/v1/projects/:project/explorers/:explorerId/authoring/v2/reconcile", wrapper.ReconcileExplorerBuilder) @@ -2731,98 +2680,6 @@ func (response GetExplorerBuilder503JSONResponse) VisitGetExplorerBuilderRespons return ctx.JSON(&response) } -type CompileExplorerBuilderRequestObject struct { - Project Project `json:"project"` - ExplorerId ExplorerId `json:"explorerId"` - Body *CompileExplorerBuilderJSONRequestBody -} - -type CompileExplorerBuilderResponseObject interface { - VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error -} - -type CompileExplorerBuilder200JSONResponse CompileResponse - -func (response CompileExplorerBuilder200JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(200) - - return ctx.JSON(&response) -} - -type CompileExplorerBuilder400JSONResponse struct { - AuthoringBadRequestJSONResponse -} - -func (response CompileExplorerBuilder400JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(400) - - return ctx.JSON(&response) -} - -type CompileExplorerBuilder401JSONResponse struct { - ServiceUnauthorizedJSONResponse -} - -func (response CompileExplorerBuilder401JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(401) - - return ctx.JSON(&response) -} - -type CompileExplorerBuilder403JSONResponse struct{ AuthoringForbiddenJSONResponse } - -func (response CompileExplorerBuilder403JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(403) - - return ctx.JSON(&response) -} - -type CompileExplorerBuilder409JSONResponse struct{ AuthoringConflictJSONResponse } - -func (response CompileExplorerBuilder409JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(409) - - return ctx.JSON(&response) -} - -type CompileExplorerBuilder422JSONResponse struct { - AuthoringUnprocessableJSONResponse -} - -func (response CompileExplorerBuilder422JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(422) - - return ctx.JSON(&response) -} - -type CompileExplorerBuilder500JSONResponse struct { - AuthoringInternalErrorJSONResponse -} - -func (response CompileExplorerBuilder500JSONResponse) VisitCompileExplorerBuilderResponse(ctx fiber.Ctx) error { - ctx.Response().Header.Set("Content-Type", "application/json") - ctx.Status(500) - - return ctx.JSON(&response) -} - -type CompileExplorerBuilder503JSONResponse struct { - AuthoringUnavailableJSONResponse -} - -func (response CompileExplorerBuilder503JSONResponse) VisitCompileExplorerBuilderResponse(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"` @@ -3775,9 +3632,6 @@ type StrictServerInterface interface { // (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/capability) GetExplorerAuthoringCapability(ctx context.Context, request GetExplorerAuthoringCapabilityRequestObject) (GetExplorerAuthoringCapabilityResponseObject, error) @@ -4036,40 +3890,6 @@ func (sh *strictHandler) GetExplorerBuilder(ctx fiber.Ctx, project Project, expl 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 -} - // GetExplorerAuthoringCapability operation middleware func (sh *strictHandler) GetExplorerAuthoringCapability(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { var request GetExplorerAuthoringCapabilityRequestObject @@ -4495,103 +4315,102 @@ 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{ - "7D1rb9w4kn+F0C0wwEF2O5nsAeP75NhO4oPjeNvOzO0NcgYtVXdzIpEakrLdCfzfF3xJlFrPdnfbu5tP", - "idp8VBWrilXFYvF7ELE0YxSoFMHh9yDDHKcggeuv04csYRz4Way+CA0OgwzLRRAGFKcQHAZQNggDDn/m", - "hEMcHEqeQxiIaAEpVj1TQs+BzuUiOHwVBnKZqb5CckLnweNjGLwHChxLwmjLPPOywVPmueTsD4hkyySZ", - "/ev6MzyqriJjVICm31EuF0z97S2Op/BnDkLPHTEqger/4ixLSKQxm/whDP7lbH/hMAsOg/+YlIs0MX8V", - "k1POGZ/a2czcMYiIk8zQMbheAPqfq08XKGZRngKViAiU4mTGeApxiBQUmFCBcvqVsnuKZgSSWISIccRS", - "IgVyZEDY4YEIzXK5HzyGJW7HCQEqjxMm4HmQjDQAKMI0ggRilHG4I3CP4AGiXLVDtzBjHJCaIAH1Sw0D", - "RmcJiXYI9lma5hLfJoAinOFbkhC5VHTnEAHJJBISS1DrJSROwCxJJklKhCQRijmeyUmW3zrwbPtogekc", - "4ipy7xi/JXEMdLeLolgGqFQz6CUhNCIZTtQqUSYRBxwrrFIWk9kSyQURyGkbZAWxisd7LOEeL69JCizf", - "4VJdrnATPEQAMcRIyYgAfgd8T5AYUAw4TgiFKuBnVAKnONEz7Q7uI4pyCg8ZRNKX4BBlwAUREmhkGMuJ", - "ywyTJOeAWBTlnNfZ6ILJdyyn8W65yLFEqPgmJjGWEGoxJolhfCswRkLkApSc2J1iT2QQkRmJPPXFQbCc", - "R4BiBgIpRoQHImqcdomXCcPxNWPnmM9htxjfKiJ7SoyDyBNpeU4guQCU4geS5ilyWw0S5FuN564Z+4jp", - "0qpjsVsUuJnVQyJLMK2gYKTmJ1FK1V7EhEQZS0i0rOLymeI7TBKlLXcoPeXmV2ro0NPPjEOIUiyBE5yQ", - "b4pFcSTJndXHwO+Ila9bHH0FGittnpeo1HHMOItAiN1i6a2WAu8OJyQ2NsNtLrV6u2f8q8hwZMQuxTQO", - "EctllhuR80WRaIj1pgUp1qo/SZaIUD2sxvc9x9nib+dbsIam+F7B3YaknRh9uL6+LDAGegcJy0AhIjmm", - "ImNcotICRphDaTD5CGxHpXfg4OAvNyGlrSF2lg1GlPEUJwWihW6IWJ6oZmpTjfOoisd22G7gWhRmqQFR", - "KWMFJuYCtGmg2UZbDzPGS8w1/B+uP57X4JXwICcLmSZVQBtM9CpIp2oDUHvDLWf3Ajj6fFaApuc6hzmO", - "llvgWTPwICFNdNPSQvKEtmBQbU4RIcw+Z5VXaa+b2TZu6o5AooAeJ8r8W5q9Vxi7N2OCSMaXKIYsYUvN", - "GBWrVjUjAhEaMWrsFx+vzVu5IxDrsXVxpESsw8Y1U21HqQxEYwo4Voyj7EFjH6ov6a+a1TilUZksPeg3", - "bh+uw1gVTtpj9xTi8o8RozMyz41xiO6xsf9mCmgPj21YG4PXoBACb1/1rQz7g0YzASzAtzmIQcihaSSm", - "bnE4JLeh99dZscK+UMB6oq+siCWVCj1jRhjbRJklVqpuwVIJ4oohFhunRjnHYmG3uytjjm1Bh9uRx9iT", - "GZaLEP2ZA1+GKM0TSTLMZajtLg37xYk2we5wkusYgFPqSgF6xpSdeuMafShKXgzDTFu1gKXx1XIuGHdf", - "bmGqUQuLgED3RC60zrHbm4/m5hX8UDy7NHyCo6+iUPDMB1630zreyKt1PEXEMvAR247aH8yXlSiBccsQ", - "qF5FHOC/UQwSk8TYwxxibCIKNEYR4xwSTZPbpUP8rMKeG98XxixbuRQxlliADFEZSQ6dEtVuHcmgtDGN", - "imW5hHLhVjcMC8lnauML32D3WB6VjKkkKuIQq09sVwvfCqVNmzXHNra6NfSho55dFOVa4zmEiNA5CPOb", - "pzT00mjl2eVYFxhuY59bgwO79jPlZiv2rPKmt8dbSql9UXi7n0UNamsq8kz5sRB/hJjga+3+7BjxYzPZ", - "nprcmSUFWEpTaFM4zxKG4zJet6+PaOxc+vTk8uxXZYsaEIHmaXD4e5Awlu5HOFlmfJ/x+cQdQe0VUb7J", - "3evgy8rhTBgcRe6ECccxUf/HySVX80sCIjic4URAGGTeT4pwSZ5S/V8iIRV9NDKTHOtealILBeYcL9X3", - "jCRwoc+bvq9CKIk0fNp5zOR+6D/vKs+xfjd/dVOU5GG3+syrII+FfB0iDQAcHhQTfAAcA2/yzqsg22Eb", - "gc2yZHlsIlLCM+pGAa17m7PNHrht2xFsUJ4t6Z5qkJTQM9P31SpfuD34hOOZPCFzi08TBct2nnjMGE+x", - "DA4DQuV/vQn0bCRVInNQTEaohDlwNYygOBMLJq/ZV6BjWamkXH2gFvg8Ag5YTKtYtreaMcFzyoQk0fAF", - "PSn6NEl1XF21vvkHr92rprUzRwLDQbeUnepuTdAXTljfSL8VDbtYohyuhmqVTiUi1RVp5JBCnIpg/Ej+", - "wBnxKN4puuW2o/Q1YJlzGC7w71yHxzD4Sozh6/Yu5/auoqOgbNqzit2xutxuwNucJEqVhoHI59ZcUvS0", - "By/qf8YR9kVQET5iNCKVjaC+u5TsYQc7JympcZ09iQoOXx0cHPSxbXXYGv94y2PJVsG9DoS3Lt3sYrXv", - "SF3izhoH7g0DNz/T8Ffl1veLrLMfIJ4PA4NQouz+Sw7K5C+SaRynXB+9PT8NwuDd2fn16TQIg+MPR9Pr", - "Zp4zjh+Nhk1sjoNGNa1x0RBjx2NHzIHKT+OAtK44YfQji3vJf1ltrXiVMXnB4mGTGY/x03C6jLX73KIe", - "T0+Prk9v3NqefL48Pzuu/HJ6fup9Tk8vjj76n5+mJ6dT830VhMHV6bX5uJl++nQdhMHRycnN9NPna9P6", - "46dfT4tP9afjT+efP14EYfD58kRNW3zbtvb7yyDbtFOQ33lqeIQkx5CABKd2PYPqlrEEsJEwHV9j9HiB", - "udFv7W3ekcTlxq02Uqa9MaJbGzxA3DmEWGDe2aRGuDpcK9jUh6wBUQU6rNOraUnemi3nSmIJO9qDIyxx", - "wua9StM2W7XHnmCBNVrPbft6hTZNqjUhM4iWUQIF+dwQF6e/abk5Ovl7Y8+KjYaT5NMsOPx9sLX2JQxo", - "nthQj+Q5DNl/a8D2GHO+1ecWrIl/jt3megWYR4v1fDc6VBXrSFEjCzzJA6p7PRaeQfiu5d6sLzp29hHu", - "glm8Au5Gf2fjHlSPSJVU9OzcJjkZzBlPWv8mcWnhicoa9Hs6x6WuG8Mefgz1KmIZDHZGt8QhNtcWWvbT", - "eD5+vtN43jjVvJI93oOuWpXRM19YK7A+MwfBkjuIr3Tzjv1GnyVc6uS23uwZr+lT2bSSNt/KI6usWyye", - "o5dbsRoz+2h1MHPJKFv1w5TF4wL9qywXwwznibx8oiOgk+QvsQKiF6KZtrLaQUrwLSQDxknYnEQ4uR4S", - "9R2hAas+0XChWCXKqlxkgKU5DuuxYP019nRmSWZHpioZvDkqdPbZYBXDNi7o4F2tdMax7WCXfcZZOtip", - "HMosGcvypI34YSDZwBnrnoZBqgK0N1q5SuX8HVS9sLI3xpWzOXnHLDfnWCNt9uFi0U1AdyA8UBo5u3/P", - "MaEt28L9lDF5mpA5adEStWUoBKQCxupIveugpKQeKxqxGozbE5xuoheBjc0dYTVis9ahlSJBrwGwQqdK", - "xC/DUgKnwWHw/78f7f0f3vt288X+52Dvl5sv//mXoHVf6Jva+Oj1udfcM54a5DPMNixseWXaqkV2e19X", - "p2vVqIpm8zlgqWMqwBfAtXPGVQH9CP6o7PQrBKn7K7p1EAZE51/MCPC3y6ulkJDqQzEJVHkLb5efeaIN", - "rJjQudeC3QrgJnXq2JHo7fJYqcliG6s4mCUkKZZRM4yrQU8H7a9H559NNHh6peN85+dBGJycXV2fXRxf", - "9wfsNPbN9PbPmtY9Re7l1s4g9Kquwbdtf6kFVE3w04RVT4LQfX84univv3Xw8+bo5ER/mfCm/fzyBO2l", - "0xjXi4SM8w82ctbnR3qq03dit9O4hzsYG46k8XpIAtybcOcHyaN8WXPjZrCn3xNimZq7Tc1HQlq8hiNp", - "xzJHIM0egm7wPJGacvJV17fiMlcIvMogoXf+6ig0IMLDqOQ4kusbLe0e5eCzyMGu6ZOc0b59vOrRtbhx", - "TTT0hGjsHmM2wprhpo22DovNZsC2T2bqBKzA+YdgtNWESEEIPB/kRLjM2uZINtwBtxkRfaIkh0xYD3a7", - "8V3/0JCxxKBxiay/NlbhR6vZDv35ds0RyHGZe105e9Vzu0HDvSs7rRHvLqjXro37ACiVL2dMTse6rbmE", - "QaFKExttsg8LQBsgcDOUy9TEQzrT9C2Ll32C1yzkg3fvcXv2ZuR2RTf2SlQ173Zk9GTjlgq4+wq9N5D1", - "Aq7Ek3T3RjStJHwEiWMs8egjfy8Vee0c27rt3po1+6566L+epmvu1eqveNMXnmXnOMVy9ybX1CaoL1o5", - "W1jA30KVeuxiHHHa7Y9BkafHRphKhbyl9GZ9ZauH4h1ZwG3mkh22idDenbv19WSXZfJkrdV0K7AHytoF", - "bX0bqbisz5UpRs09gvq1aExje5dnr7wprzcasR+shOmdAqtNR3QZEewGt7ghQwHE1J+E5Hkk9R1LRYGJ", - "a2OwDlEMGVBzn5aaO2EKBgUCo2AzN+pEH3y30qjTL8MV6qfCXBjB8yQeFtw+bxXUtRQtiTvvKNhiPOuF", - "ShKSEjk6bXVEeuNwp7aGte+QFvN14r/bJJLRlnTFw30J+SOWcB3m9IZXWAtHcVbVfUrC2X2VDKN8zNF5", - "1Y3cVq6yhcdDoD+mcdkaab46Pjo/mvqh5un06O9esPnG/eBi0r2x6DC4LO8DrqXcajfqN53mLFxy3xgd", - "4K2EsPl2q2A20t5k+K+nEzegsjph2qme2r1WKflwI2HTVb5+YuhUaT0xlMPXYts+BeNBUDL28GCpqyU0", - "ymLU5RPuF0yAKTGjDDGxwOYyaqacEn4HMUqYEAkIkSxRnJvycPrKd5RLdgd8P2iCpxLY3u5V0v5tdMzO", - "1ZWHsJap5iks09+bpDusM3XXj9bTWTu9bLfRNOGOBOpGOhXxtbGHBCSJuQF32HFNGchrYLJxmQK6IARh", - "VCxI1pz0MiYSWee56sF/ZahWCpZ5l2P2oCRh91ObY3bq0lVXD0p0uytIZueMZS1tUvxwJSETA6IWFdW6", - "CsLKfE1I+9f0u4MCnUck6x6CVC8aNMDXHl3oO+ngIPkSD8uUGhCaaKxn0Ees6lZzZSrgQDVGASmRttTB", - "OWPpTwKZWzim/qBpnOIssyELpyb6QxVDqzOMDLleeXeE3hIdudhWiGzwltW+23SUBbjGt2vFOdbL4xqY", - "BMc2fxFQ2VTDxID4+7NHRAN7OVILNWuZWSOZApJkCjQGe/euzJUqY+Zf1qdrRihty9LsJtAKpr9Vblft", - "wFOxx3sj/BR3IPjYWDuiuN/YeRpTP1lZvbTYk5BYNO33jUqSNi3xykXH3qOLTtXXoL/6Dzd0YtjwSZRu", - "WSfcUqyOv+x27hWpC4OHvTnbc5O46693r/d/87KuijZ7JM2YyWK1jxh4XdQ2onMPgjmRi/x2P2LpxFTO", - "mSSMpRNiS48VNXQmfu9HXdmH0BnTUmQ0U6D2M7ONHV2eKf3h2D94tX+wf2BrFlCckeAw+Hn/YP9nC4am", - "7wRnZHL3aqK4b8ZxCpN6uF5MvpP4UbWdg8arqAKgFGjwHuRU9zh1HYLamwuvDw52Ucn2KtdF32Z5UtTa", - "1XWY3hy82sX0tbpfZZXA0FX6MvWZi2pR7qqSLqYrF16xs5+Eq47mFad7c/BzG3QFtSf10qu635uh/YrK", - "cLrsU56mmC9tJVJb8AulVlFpoDEquGalYNu+LQxQvF3ye+MLH6T7mZK6BfLlMaxwrAApJt9tVu3jpEwV", - "E5Pv5cdj/R2Vlmu6ZRN3o6b1FMhr6j2V8vglDDImGqTkmCuf4cSA/L7+dgoI6dwCj02LSpQT5SHvuZP3", - "kjqrNw5vnP91k7XlOMUwA35T1oVsvzlf0cOFk35LqGKLAWUhqpmTbbnQ/UeaGpbQewzGG3jVSKr21jer", - "X5o+OugXyNX6qKUmG9SzUgNxqPpYqeypO/4yuGNR+VT1e/XXEcA21MhTY7x+PWIMv7LgYxj8dQSdqyU/", - "deefx5C6qNpY1ZyfTUE9XBaVLRR7ycOmyt4aWm1ihRh2pt5Cq8PNHf5CiRe7wE2h/m9GKvaWkRt0Wuf2", - "0KZ+jyylmhXwD92wY93wAkTTcQTCnigiPJPAPavGK3NaFVMrnL6YOpNdtJrK50TI06LVEzlvI0fTY43o", - "7bFXg+U6iEuaKvVXV1pRvUxIMr8TENaC9av+r6lDe4y+09LbbLf1NqRl+gyfVy9eua287fFMzDdItdVe", - "8BhqsTSV/N8cvxuuQ9i8/8O1kVDW9x+uxibfy7ckO93/CoO/OL9/x1yzhrO9sZXXjjqmxVr/JJBdfZ5T", - "SVJA5f3Tpyi8fqPRe6a04rMP5reJXzt64t0d7GPCt8X9r63xYqWmWFOxbX0gKr2XNDA1TxMS75FJXXWh", - "OGjaDcc2PEE5lGlXnx0cqiRXH/UcyvEtDzYOtQEb36vT67Ubvm81C8ylxSaO3bx5ULvKvOPwSP2qcc+j", - "JSuvSA63G5oe9n1OkVpXMAYZEC2vFD6zXD1dz0eVWtJ9qr6pBPUWeblpukZzxD2rUD5yWtZN1lvB5zPk", - "iiU/p95/Eq88/hOZD/5bAc+r+nVd/XohRb8K+OY3gMZ3GXa8DTQ/J9BkOmHKKIlw4p6SsW+IQOxbUzpI", - "gynCkqUkcs+golsso8WPHePfasdwZfWfXbLtdZYtB3tqt612LMb1u04NAvy29lg2myFGoXil2aQ7/fPK", - "6O4dpTevxoBbfyj96erhzesxkNdfOVcD/DIK9YQAlccJE+Av+PMpKdV5zKq/xxLu8fKapMByuSElZ18M", - "eX4lZwDZtpKrXp/atZKrXZRqSvWxZ0ex//yzuVTzQ7X9MFwGyXT54M+zS3Vx+Wc3EamVu0YvOSblDBf3", - "sDGacZbabD0cSfc0N8RIXx36If4/xH+Q+PuPgz27AjDvYLimx5V68tsISTe/N7JrNdDyCkhzVMI0RTHM", - "9KtejAqkq7wSOkcYURabQx6dvvTvF4J4ubLYmjtXPFJrXud//gzhwu7MmCCS8SJMeGwAbM6mflq+nMu6", - "W5jnX4th/nfvnLF0zxRp3jtmaUrkuDzt7WgOvwLuv0Sa749slzVyHgYqDDdvWwagtQO13rYeNsKIF/K3", - "x+6p7+IVAegihYYlCes6qDrSLa4wjW/ZQ8vRVDcOH64/nj+dJ94M7tt6A4TGYC6pGJzQe46zxd/OkTDI", - "WZLM1a9/JuVlIq1WG9WduS2kE3J1SzvgS0iSe/lqwxJrg3pjiCDbWdeTZNu5K33JskTlWpHjs9qb9AWn", - "6X+7ZNAOcJng5Zxr1v7XkENHmKxAzGR0dQnbDxn7IWOFjGnJQYyjdx/Opq1ytgCcyE4B+2BavKwVtgbC", - "1u98IsfnyBDK1pkEGi0REYgyiTjgeLm/IsgZ4xJFOFroipXcmCGl3jPD2UVIyB1861qDc3IHFIR4UavQ", - "hLDlapRYgNE9kQuWSxQtIPqqnOmCgEQnxyj0NQW/dV9AxjF5cQTYGRueeDRDmEMv390vQJdULbg3ro9Q", - "9NbV7PmdczpzngSHwSR4/PL4jwAAAP//", + "7D1rc9s4kn8FxduqqbqiLWcme1Xr++TYTuIrx/HKzsztTeVcENmSsCEBDgDaVlL+71d4kSDFpyzJmb18", + "Sijj0d3obnQ3Go1vQcTSjFGgUgTH34IMc5yCBK6/zh+zhHHgF7H6IjQ4DjIsl0EYUJxCcBxA2SAMOPyR", + "Ew5xcCx5DmEgoiWkWPVMCb0EupDL4PhVGMhVpvoKyQldBE9PYfAOKHAsCaMt8yzKBs+Z55qzf0IkWybJ", + "7F83n+FJdRUZowI0/U5yuWTqb29wPIU/chB67ohRCVT/F2dZQiKN2eSfwuBfzvYXDvPgOPi3SblIE/NX", + "MTnnnPGpnc3MHYOIOMkMHYPbJaD/uvl4hWIW5SlQiYhAKU7mjKcQh0hBgQkVKKdfKHugaE4giUWIGEcs", + "JVIgRwaEHR6I0CyXh8FTWOJ2mhCg8jRhAl4GyUgDgCJMI0ggRhmHewIPCB4hylU7NIM544DUBAmoX2oY", + "MDpPSLRHsC/SNJd4lgCKcIZnJCFypejOIQKSSSQklqDWS0icgFmSTJKUCEkiFHM8l5MsnznwbPtoiekC", + "4ipybxmfkTgGut9FUSwDVKoZ9JIQGpEMJ2qVKJOIA44VVimLyXyF5JII5LQNsoJYxeMdlvCAV7ckBZbv", + "camu17gJHiOAGGKkZEQAvwd+IEgMKAYcJ4RCFfALKoFTnOiZ9gf3CUU5hccMIulLcIgy4IIICTQyjOXE", + "ZY5JknNALIpyzutsdMXkW5bTeL9c5FgiVHwTkxhLCLUYk8QwvhUYIyFyCUpO7E5xIDKIyJxEnvriIFjO", + "I0AxA4EUI8IjETVOu8arhOH4lrFLzBewX4xnisieEuMg8kRanhNILgGl+JGkeYrcVoME+VrjuVvGPmC6", + "supY7BcFbmb1kMgSTCsoGKn5SZRSdRAxIVHGEhKtqrh8ovgek0Rpyz1KT7n5lRo69PQz4xCiFEvgBCfk", + "q2JRHElyb/Ux8Hti5WuGoy9AY6XN8xKVOo4ZZxEIsV8svdVS4N3jhMTGZpjlUqu3B8a/iAxHRuxSTOMQ", + "sVxmuRE5XxSJhlhvWpBirfqTZIUI1cNqfN9xnC3/frkDa2iKHxTcbUjaidH729vrAmOg95CwDBQikmMq", + "MsYlKi1ghDmUBpOPwG5UegcODv5yE1LaGmJn2WBEGU9xUiBa6IaI5YlqpjbVOI+qeOyG7QauRWGWGhCV", + "MlZgYi5AmwaabbT1MGe8xFzD//72w2UNXgmPcrKUaVIFtMFEr4J0rjYAtTfMOHsQwNGniwI0PdclLHC0", + "2gHPmoEHCWmim5YWkie0BYNqc4oIYfY5q7xKe93MtnVTdwQSBfQ4Uebfyuy9wti9GRNEMr5CMWQJW2nG", + "qFi1qhkRiNCIUWO/+Hht38odgViPrYsjJWIdNq6ZajdKZSAaU8CxYhxlDxr7UH1Jf9WsximNymTlQb91", + "+3ATxqpw0gF7oBCXf4wYnZNFboxD9ICN/TdXQHt47MLaGLwGhRB4+6pvZdgfNJoJYAG+zUEMQg5NIzF1", + "i8MhuQu9v8mKFfaFAtYTfWVFrKhU6BkzwtgmyiyxUjUDSyWIK4ZYbJwa5RyLpd3ubow5tgMdbkceY09m", + "WC5D9EcOfBWiNE8kyTCXoba7NOxXZ9oEu8dJrmMATqkrBegZU3bqrWv0oSh5MQwzbdUClsZXy7lg3H25", + "halGLSwCAj0QudQ6x25vPprbV/BD8ezS8AmOvohCwTMfeN1O63gjr9bxFBHLwEdsN2p/MF9WogTGLUOg", + "ehVxgP9EMUhMEmMPc4ixiSjQGEWMc0g0TWYrh/hFhT23vi+MWbZyKWIssQAZojKSHDolqt06kkFpYxoV", + "y3IJ5cKtbxgWkk/Uxhe+wv6xPCkZU0lUxCFWn9iuFp4JpU2bNccutroN9KGjnl0U5VrjBYSI0AUI85un", + "NPTSaOXZ5VgXGO5in9uAA7v2M+VmK/as8qa3x1tKqX1ReLufRQ1qayryTPmxEH+AmOBb7f7sGfFTM9mB", + "mtyZJQVYSlNoUzjPEobjMl53qI9o7Fz69OT64ldlixoQgeZpcPx7kDCWHkY4WWX8kPHFxB1BHRRRvsn9", + "z8HntcOZMDiJ3AkTjmOi/o+Ta67mlwREcDzHiYAwyLyfFOGSPKX6v0RCKvpoZCY51b3UpBYKzDleqe85", + "SeBKnzd9W4dQEmn4tPOYyf3Qf95VnmP9bv7qpijJw2b6zKsgj4V8EyINABweFRO8BxwDb/LOqyDbYRuB", + "zbJkdWoiUsIz6kYBrXubs80euG3bEWxQni3pnmqQlNAL0/fVOl+4PfiM47k8IwuLTxMFy3aeeMwZT7EM", + "jgNC5X+8DvRsJFUic1RMRqiEBXA1jKA4E0smb9kXoGNZqaRcfaAW+DwCDlhMq1h2t5oxwQvKhCTR8AU9", + "K/o0SXVcXbW++Qev3aumtTNHAsNBt5Sd6m5N0BdOWN9IvxUNu1iiHK6GapVOJSLVFWnkkEKcimD8SP7A", + "GfEo3im65baj9DVgmXMYLvBvXYenMPhCjOHr9i7n9q6jo6Bs2rOK3bG63G7AWU4SpUrDQOQLay4petqD", + "F/U/4wj7IqgIHzEakcpGUN9dSvawg12SlNS4zp5EBcevjo6O+ti2OmyNf7zlsWSr4F4HwluXbnax2nek", + "LnFnjQP3hoGbn2n4q3Lr+0XW2Q8QL4aBQShRdv81B2XyF8k0jlNuT95cngdh8Pbi8vZ8GoTB6fuT6W0z", + "zxnHj0bDJjbHQaOa1rhoiLHjsSPmQOXHcUBaV5ww+oHFveS/rrZWvMqYvGLxsMmMx/hxOF3G2n1uUU+n", + "5ye353dubc8+XV9enFZ+Ob889z6n51cnH/zPj9Oz86n5vgnC4Ob81nzcTT9+vA3C4OTs7G768dOtaf3h", + "46/nxaf60+nHy08froIw+HR9pqYtvm1b+/15kG3aKchvPTU8QpJjSECCU7ueQTVjLAFsJEzH1xg9XWJu", + "9Ft7m7ckcblx642UaW+M6NYGjxB3DiGWmHc2qRGuDtcaNvUha0BUgQ7r9Gpakjdmy7mRWMKe9uAIS5yw", + "Ra/StM3W7bFnWGCN1nPbvl6hTZNqTcgcolWUQEE+N8TV+W9abk7O/tHYs2Kj4ST5OA+Ofx9srX0OA5on", + "NtQjeQ5D9t8asD3GnG/1uQVr4p9Tt7neAObRcjPfjQ5VxTpS1MgCz/KA6l6PhWcQvhu5N5uLjp19hLtg", + "Fq+Au9Hf2boH1SNSJRU9O7dJTgZzxrPWv0lcWniisgb9ns5pqevGsIcfQ72JWAaDndEdcYjNtYWW/TRe", + "jJ/vPF40TrWoZI/3oKtWZfTMV9YKrM/MQbDkHuIb3bxjv9FnCdc6ua03e8Zr+lw2raTNt/LIOusWi+fo", + "5Vasxsw+Wh3MXDLKTv0wZfG4QP86y8Uwx3kir5/pCOgk+WusgOiFaK6trHaQEjyDZMA4CVuQCCe3Q6K+", + "IzRg1ScaLhTrRFmXiwywNMdhPRasv8aezizJ7MhUJYM3R4XOPhusY9jGBR28q5XOOLYd7LLPOUsHO5VD", + "mSVjWZ60ET8MJBs4Y93TMEhVgPZGK1epnL+DqldW9sa4cjYn75Tl5hxrpM0+XCy6CegOhAdKI2cP7zgm", + "tGVbeJgyJs8TsiAtWqK2DIWAVMBYH6l3HZSU1GNFI1aDcXuC0030IrCxvSOsRmw2OrRSJOg1ANboVIn4", + "ZVhK4DQ4Dv7395OD/8EHX+8+2/8cHfzt7vO//yVo3Rf6pjY+en3uDfeM5wb5DLMNC1vemLZqkd3e19Xp", + "VjWqotl8DljqmArwBXDtnHFTQD+CPyo7/RpB6v6Kbh2EAdH5F3MC/M3qZiUkpPpQTAJV3sKb1SeeaAMr", + "JnThtWAzAdykTp06Er1ZnSo1WWxjFQezhCTFMmqGcT3o6aD99eTyk4kGT290nO/yMgiDs4ub24ur09v+", + "gJ3Gvpne/lnTpqfIvdzaGYRe1zV41vaXWkDVBD9NWPUsCN33+5Ord/pbBz/vTs7O9JcJb9rPz8/QXjqN", + "cc+RAXd0NPzIz/gFJAHuTbj3o9ZR3p65kzLYF+4JQkzN7Z/mQxPNgMORtGOZQ4JmG1o3eJlYRjn5unNY", + "cSorBF5nkNA7oXQUGhADYVRyHMnNt/V2n2vwad1g5+1Z7lrfTlf1eVocnSYaekI0VgubraJm2mizpsOm", + "sTmi7ZOZm/RrcP5TMNq6yaYgBF4MMrNd7mlzrBfugducgT5RkkMmrIeD3fiuf2jIWGLQuETWoxmr8KP1", + "fID+jLTmGN243LaurLbqydag4d6WnTaICBfUa9fGfQCUypczJqdjHbtcwqBgnokeNllQBaANELgZymVq", + "4iGdi/mGxas+wWsW8sG797g9eztyu6YbeyWqmpk6Mr6wdUsFXEZ/7x1dvYBrERfdvRFNKwkfQOIYSzz6", + "UNxL1t04C7Vu3bbmlb6tHotvpumae7Va9N70he/VOU6x3L3pJ7UJ6otWzhYW8LdQpe7djyNOu/0xKDbz", + "1AhTqZB3lACsLzX1ULwjT7bNXLLDNhHau5W2uZ7sskyerbWa7s31QFm7wqzv6xTX2bkyxajJtK9fHMY0", + "trddDsq75HqjEYfBWiDbKbDadEQX2sBucIsbMhRATP1JSJ5HUt9CVBSYuDYG6xDFkAE1N06puTWlYFAg", + "MAo2t6FO9MG3D406/TxcoX4szIURPE/iYeHfy1ZB3UjRkrgzi9+Wq9ksrSIhKZGjEztHJAAOd2prWPsO", + "aTFfJ/77TbMYbUlXPNzvIcPCEq7DnN7yCmvhKE5zus8ROHuokmGUjzk687iR28pVtvB4CPTHNK5bY7E3", + "pyeXJ1M/GDudnvzDC8feuR9c1LY3WhsG1+WNuY2UW+3O+bYTgYVLfxujA7yVEDYjbR3MRtqbHPjNdOIW", + "VFYnTHvVU/vXKiUfbiVsus7XzwydKq0nhnL4Rmzbp2A8CErGHh4sddV2RlmMusDAw5IJMEVYlCEmlthc", + "18yUU8LvIUYJEyIBIZIVinNTQE1fio5yye6BHwZN8FQC27u9bNm/jY7ZubpO6jcy1TyFZfp7k3SHdabu", + "gs5mOmuv19G2mkjbkWLcSKcivjb2kIAkMTfgDjuuKQN5DUw27ixdl0wgjIolyZrTQsZEIus8Vz0arwzV", + "SsEyM3HMHpQk7GFqs7DOXULn+kGJbncDyfySsaylTYofbyRkYkDUoqJa10FYm68Jaf8ie3dQoPOIZNND", + "kGoqfgN87dGFvpMODpKv8LBcogGhicYb/33Eqm41N6ZGDFRjFJASaYsBXDKW/iSQuadiKvSZxinOMhuy", + "cGqiP1QxtH7ByJDrjXeL5g3RkYtdhcgGb1ntu03HxflbPNsozrFZptPANDG2/atyyqYaJgbE3589IhrY", + "y5FaqFnLXRrJFJAkU6Ax2NtpZTZRGTP/vDldM0JpWx5jN4HWMP2tcv9oD56KPd4b4ae4A8GnxuoKxQ3A", + "ztOY+snK+rW+npS9omm/b1SStGmJ164C9h5ddKq+Bv3Vf7ihU6eGT6J0yybhlmJ1/GW3c69JXRg8HizY", + "gZvEXRC9//nwN+8GWtHmgKQZM3metsy/10VtIzr3IFgQucxnhxFLJ6a2zCRhLJ0QW5yrqDIz8Xs/6do3", + "hM6ZliKjmQK1n5lt7OT6QukPx/7Bq8OjwyN7q5/ijATHwS+HR4e/WDA0fSc4I5P7VxPFfXOOU5jUw/Vi", + "8o3ET6rtAjRexT15pUCDdyCnuse56xDUXiX4+ehoH7Veb3JdFm2eJ0U1Wl2p6PXRq31MX6uMVdbRC10t", + "LFPBuKin5C7z6HKzcumVA/tJuPphXvm210e/tEFXUHtSL06q+70e2q+onaYLI+VpivnK1uq0JbFQahWV", + "BhqjgmvWSpod2qvzxesevze+gUG6H/KoWyCfn8IKxwqQYvLN5p0+TcpUMTH5Vn481V8aabnIWjZxd05a", + "T4G8pt5jIk+fwyBjokFKTrnyGc4MyO/qr4uAkM4t8Ni0qNU4UR7ygTt5L6mzfifvzvlfd1lbjlMMc+B3", + "ZeXE9rvlFT1cOOkzQhVbDCicUM2cbMsW7j/S1LCE3nMp3sDrRlK1t757/L3po6N+gVyvIFpqskE9K1UC", + "h6qPtdqXuuPfBncsaoOqfq/+OgLYhipyaoyffx4xhl977ykM/jqCztWimLrzL2NIXdQ1rGrOT6bkHC7L", + "rhaKveRhU4duA602sUIMe1NvodXh5pZ7ocSLXeCuUP93IxV7y8gNOq1ze2hTvyeWUs0K+Idu2LNu+A5E", + "03EEwp4oIjyXwD2rxisEWhVTK5y+mDqTXbSaypdEyPOi1TM5bytH02ON6N2xV4PlOohLmmrZV1daUb1M", + "SDK/ExDWgvXr4m+oQ3uMvvPS22y39bakZfoMn1ffvXJbe/3ihZhvkGqrvXEx1GJpKoq/PX43XIeweSGH", + "ayOhrIA/XI1NvpWvLXa6/xUG/+78/j1zzQbO9tZWXjvqmBZr/ZNAdvV5TiVJAZU3NJ+j8PqNRu8hz4rP", + "PpjfJn515Yl3d7CPCd8U9792xouVqltN5aj1gaj03prA1DzeR7xnGHVdguKgaT8c2/BI41CmXX+Yb6iS", + "XH/2cijHtzxpONQGbHzRTa/Xn4Xvo0r12T7Wbypau0MxaJquUT27Quzls4hlpVUtGp8ukCuv+pJy8Cye", + "/FOxlVddfD8wt/rlWZas6qXX/LrB27eXGyu57zlq2FyAvGkrwZRREuHEPT5hXx2A2N9dtNOKKcKSpSRy", + "DyeiGZbRcrjl3fR49EtuSptuLYNM8JaXMF94Z3q+aLtC3C8u2Ta9f8fOb+32yZ7FuH73o0GA39Se12Vz", + "xCgU77qa9I8/r4zu33B8/WoMuPWnlZ+vHl7/PAby+rvIaoC/jUK94bH7F1VSqvOYVa89o74dJWffGHh5", + "JWcA2bWSq14n2beSq10caUp9sLH02H8w1lwy+KHafhgug2S6fCLkxaW6uAzRFFPaQQC/fvdizwJeLwfW", + "+fSmM1zcU6hozllqs5dwJN1jvhAjfZXih/j/EP9B4u8/J/TiCsBUzndNTysVqHegAFpeKNi3Gmh5N6A5", + "KmGaohjm+h0gRgXSdSEJXSCMKItN0Func/z/C0F8v7LYmktUPGtp3vN++YzJwu50j5QX8mgAbM4ufV7+", + "kMtCWpoHI4th/vvgkrH0wJR1PThlaUrkuLzV3WgO/328f4m0xx+n/xucAQ9UGG7etowoawdqvW09bIQR", + "L+TvgD1Q38UrAtBFSgFLEtZ1UHWiW9xgGs/YY8vRVDcO728/XD6fJ14P7tuaEU9jMEn7Bif0juNs+fdL", + "JAxyliQL9esfSXm5QqvVRnVnbk/oBEXd0g74PSQNff9qwxJri3pjiCDbWTeTZNu5K53DskTlmoXjs9or", + "1gWn6X+7ZNAOcJ3g1YJr1v7XkENHmKxAzGS4dAnbDxn7IWOFjGnJQYyjt+8vpq1ytgScyE4Be29afF8r", + "bA2End+BQ47PkSGUrbsHNFq5h/k54Hh1uCbIGeMSRTha6gp+3Jghpd4zw9lFSMg9fO1ag0tyDxSE+K5W", + "oQlhy9UosQCjByKXLJcoWkL0RTnTBQGJTo5R6GsKfu2+kIlj8t0RYG9seObRDGEOvXz3sARdYrLg3rg+", + "QtFbV/fm987pzHkSHAeT4Onz0/8FAAD//w==", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/explorer/arango/activation.go b/internal/explorer/arango/activation.go index d706d23c..563af198 100644 --- a/internal/explorer/arango/activation.go +++ b/internal/explorer/arango/activation.go @@ -38,6 +38,11 @@ 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 @@ -161,10 +166,6 @@ func activateRevisionAndOwner(ctx context.Context, tx store.RowQueryer, state ac 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 { @@ -183,22 +184,25 @@ func activateRevisionAndOwner(ctx context.Context, tx store.RowQueryer, state ac if err != nil { return err } - return updateActivationDocument(ctx, tx, ExplorersCollection, ownerKey, activeExplorerPatch(candidate, candidateKey, now)) + return updateActiveExplorerDocument(ctx, tx, ownerKey, candidateKey) } -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 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 { 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/explorers.go b/internal/explorer/arango/explorers.go index 41d5622d..ac88f10b 100644 --- a/internal/explorer/arango/explorers.go +++ b/internal/explorer/arango/explorers.go @@ -28,7 +28,7 @@ func (s *Store) Get(ctx context.Context, project, id string) (*explorer.Explorer } return out, nil } -func (s *Store) CreateInteractive(ctx context.Context, e explorer.Explorer) (*explorer.Explorer, error) { +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 @@ -43,6 +43,3 @@ func (s *Store) CreateInteractive(ctx context.Context, e explorer.Explorer) (*ex } return out, nil } -func (s *Store) CreateRepository(ctx context.Context, e explorer.Explorer) (*explorer.Explorer, error) { - return s.CreateInteractive(ctx, e) -} diff --git a/internal/explorer/arango/repository.go b/internal/explorer/arango/repository.go index 451c221d..69246b87 100644 --- a/internal/explorer/arango/repository.go +++ b/internal/explorer/arango/repository.go @@ -2,39 +2,74 @@ 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" ) -func repositoryConfigKey(project string) string { - return "repository_config_" + key(project, "default") +// 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"` } -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)) + +// 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 nil, err + return fmt.Errorf("read legacy Explorer repository configs: %w", 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 + 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 out, nil + 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 index 7b6725b8..16b81ef1 100644 --- a/internal/explorer/arango/revisions.go +++ b/internal/explorer/arango/revisions.go @@ -39,17 +39,11 @@ func (s *Store) GetRevision(ctx context.Context, id string) (*explorer.Revision, 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) { +// 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": status, "diagnostics": diagnostics} - if status == explorer.RevisionReady { - patch["readyAt"] = now - } - if status == explorer.RevisionFailed { - patch["failedAt"] = now - } + 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 { 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/semantic_types.go b/internal/explorer/authoringv2/semantic_types.go index 42ea285e..5eb24175 100644 --- a/internal/explorer/authoringv2/semantic_types.go +++ b/internal/explorer/authoringv2/semantic_types.go @@ -82,16 +82,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") } 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..f8ee0954 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) 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..732f3b30 --- /dev/null +++ b/internal/explorer/compilation/compile.go @@ -0,0 +1,323 @@ +// 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" +) + +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) { + 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 = 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 + } + 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 ec939d54..a67edbc1 100644 --- a/internal/explorer/compilation/semantic_compile.go +++ b/internal/explorer/compilation/semantic_compile.go @@ -9,6 +9,7 @@ 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" ) @@ -43,10 +44,10 @@ 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] @@ -104,9 +105,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,7 +124,7 @@ 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), TraversalColumnNaming: recipe.TraversalColumnNamingAlias, Fields: nodes[authoringv2.RootOccurrenceID].fields, Pivots: nodes[authoringv2.RootOccurrenceID].pivots, DynamicColumns: nodes[authoringv2.RootOccurrenceID].dynamics, CollisionPolicy: "error"} diff --git a/internal/explorer/compilation/translate.go b/internal/explorer/compilation/translate.go deleted file mode 100644 index 827cdec7..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-5" - -// 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 f53123a1..0692e1aa 100644 --- a/internal/explorer/compilation/translate_test.go +++ b/internal/explorer/compilation/translate_test.go @@ -2,9 +2,6 @@ package compilation import ( "context" - "errors" - "fmt" - "strings" "testing" "github.com/calypr/loom/internal/dataframe/recipe" @@ -22,86 +19,6 @@ 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 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) - } -} - -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) - } -} - func TestCompileSemanticWorkspacePreservesAuthoredColumnsAndTypedSources(t *testing.T) { visible, order := true, 0 document := authoringv2.Document{ @@ -178,33 +95,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", diff --git a/internal/explorer/compilation/workspace_test.go b/internal/explorer/compilation/workspace_test.go index 60a3fc2f..e9c98492 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()) 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/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..cadaf903 --- /dev/null +++ b/internal/explorer/lifecycle/publish.go @@ -0,0 +1,82 @@ +package lifecycle + +import ( + "context" + "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" +) + +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 { + 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 + } + return PublishResult{Receipt: receipt, Revision: revision, Execution: execution}, nil +} + +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..3f4cb34d --- /dev/null +++ b/internal/explorer/lifecycle/repository_publish.go @@ -0,0 +1,80 @@ +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 { + 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 index 8f8cd7c9..fe424786 100644 --- a/internal/explorer/lifecycle/service.go +++ b/internal/explorer/lifecycle/service.go @@ -1,30 +1,21 @@ package lifecycle import ( - "context" - "errors" "fmt" - "strings" "time" - dataframeexecution "github.com/calypr/loom/internal/dataframe/execution" - "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" - explorercompilation "github.com/calypr/loom/internal/explorer/compilation" - "github.com/calypr/loom/internal/projectid" ) // Service is the Explorer application layer. It coordinates persistence and // deployment adapters while leaving all wire representation to transports. type Service struct { - store Store + store *explorer.Service config Config now func() time.Time } -func New(store Store, config Config) (*Service, error) { +func New(store *explorer.Service, config Config) (*Service, error) { if store == nil { return nil, fmt.Errorf("Explorer lifecycle store is required") } @@ -35,420 +26,3 @@ func New(store Store, config Config) (*Service, error) { config.Now = now return &Service{store: store, config: config, now: now}, nil } - -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 { - continue - } - if query != "" && !strings.Contains(strings.ToLower(candidate.Label), query) && !strings.Contains(strings.ToLower(candidate.ID), query) { - continue - } - 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 = &workspace - state.LifecycleState = authoringv2.LifecycleReady - } else { - state.Workspace = activeWorkspace - if activeWorkspace != nil { - state.LifecycleState = authoringv2.LifecycleReady - } - } - if err := state.Validate(); err != nil { - return authoringv2.BuilderState{}, unavailable("capability", "CAPABILITY_UNAVAILABLE", err.Error(), err) - } - return state, nil -} - -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) 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 -} - -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 { - return PublishResult{}, unavailable("publish", "PUBLICATION_UNAVAILABLE", "Explorer publication is not configured", nil) - } - if 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) - var execution Execution - execution, err = s.config.MaterializeReceipt(ctx, receipt, bindings) - if err != nil { - 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, 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: 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 - } - return PublishResult{Receipt: receipt, Revision: revision, Execution: execution}, nil -} - -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 { - 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() - if _, err := s.store.SaveRepositoryConfig(ctx, explorer.RepositoryConfig{Project: request.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: request.Generation, SourceCommit: request.Commit, ExecutionID: execution.ID, Materializations: materialized, Dataset: datasetMetadata, Publication: publication}); err != nil { - return RepositoryPublishResult{}, internal("repository_publish", "PERSISTENCE_FAILED", fmt.Sprintf("persist ExplorerConfigV2: %v", err), err) - } - return RepositoryPublishResult{Receipt: receipt, Owner: owner, Revision: revision, Execution: execution, Materializations: materialized, Dataset: datasetMetadata, Publication: publication}, nil -} - -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 -} - -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/service_test.go b/internal/explorer/lifecycle/service_test.go index e1a9f340..72edc184 100644 --- a/internal/explorer/lifecycle/service_test.go +++ b/internal/explorer/lifecycle/service_test.go @@ -22,7 +22,7 @@ import ( ) type fakeStore struct { - Store + explorer.Store listValues []explorer.Explorer state explorer.ExplorerStateV1 created *explorer.Explorer @@ -39,38 +39,38 @@ func (f *fakeStore) List(context.Context, string) ([]explorer.Explorer, error) { return append([]explorer.Explorer(nil), f.listValues...), nil } -func (f *fakeStore) LoadExplorerState(context.Context, string, string) (explorer.ExplorerStateV1, error) { - return f.state, nil -} - -func (f *fakeStore) CreateInteractiveFrom(_ context.Context, project, id, title, source, actor string) (*explorer.Explorer, error) { - value := &explorer.Explorer{Project: project, ExplorerID: id, Title: title, ManagementMode: explorer.ManagementInteractive, UpdatedBy: actor} - f.created = value - return value, 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) ApplyWorkspaceCommands(context.Context, string, string, authoringv2.CatalogSnapshot, authoringv2.ApplyCommandsRequest, string) (*authoringv2.ApplyCommandsResponse, error) { +func (f *fakeStore) SaveDraft(_ context.Context, value explorer.Explorer, _ int64, _ ...string) (*explorer.Explorer, error) { if f.applyErr != nil { return nil, f.applyErr } - return &authoringv2.ApplyCommandsResponse{}, nil + value.DraftVersion++ + f.created = &value + return &value, nil } -func (f *fakeStore) CompilationReceiptForExplorer(context.Context, string, string, string) (*explorer.CompilationReceipt, error) { +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) ActiveRevision(context.Context, string, string) (*explorer.Revision, error) { +func (f *fakeStore) GetRevision(context.Context, string) (*explorer.Revision, error) { return nil, explorer.ErrNotFound } @@ -87,10 +87,6 @@ func (f *fakeStore) PublishAuthoring(_ context.Context, _ explorer.CompilationRe return &revision, nil } -func (f *fakeStore) UpsertRepositoryV2(context.Context, explorer.CompilationReceipt, string, string, []explorer.Materialization, explorer.DatasetMetadata, explorer.PublicationMetadata) (*explorer.Explorer, *explorer.Revision, error) { - return nil, nil, errors.New("not used") -} - func (f *fakeStore) FailRevision(context.Context, string, []explorer.Diagnostic) (*explorer.Revision, error) { return nil, nil } @@ -102,11 +98,17 @@ func (f *fakeStore) ActivateRepositoryGeneration(context.Context, string, string return nil } -func (f *fakeStore) SaveRepositoryConfig(context.Context, explorer.RepositoryConfig) (*explorer.RepositoryConfig, error) { - if f.order != nil { - *f.order = append(*f.order, "save-config") +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 &explorer.RepositoryConfig{}, nil + return service } func readySnapshot(project, generation, token string, scope authscope.ReadScope) capability.Snapshot { @@ -123,7 +125,7 @@ func testConfig(snapshot capability.Snapshot) Config { return AuthorizedCapability{Snapshot: snapshot, Scope: authscope.ReadScope{Mode: authscope.ReadScopeUnrestricted}}, nil }, Catalog: func(capability.Snapshot, string) authoringv2.CatalogSnapshot { - return authoringv2.CatalogSnapshot{SnapshotToken: snapshot.Token, Complete: true, Nodes: []authoringv2.CatalogNode{{ID: "node", ResourceType: "Patient", RowRootEligible: true}}} + 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}}} }, }} } @@ -165,10 +167,7 @@ func nativeReceipt(snapshot capability.Snapshot) *explorer.CompilationReceipt { func TestListGetCreateUseApplicationStore(t *testing.T) { store := &fakeStore{listValues: []explorer.Explorer{{ExplorerID: "patients", Title: "Patients", ManagementMode: explorer.ManagementInteractive}}} - service, err := New(store, Config{}) - if err != nil { - t.Fatal(err) - } + 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) @@ -197,7 +196,7 @@ func TestApplyCommandsMapsCASFailures(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { store := &fakeStore{applyErr: test.err} - service, _ := New(store, testConfig(snapshot)) + 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 { @@ -223,7 +222,7 @@ func TestPreviewRejectsStaleGenerationAndScope(t *testing.T) { config.PreviewReceipt = func(context.Context, *explorer.CompilationReceipt, recipe.RuntimeBindings, func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { return dataframeexecution.PreviewSummary{}, nil } - service, _ := New(store, config) + 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 { @@ -260,7 +259,7 @@ func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { order = append(order, "materialize") return Execution{ID: "execution-a", Outputs: []ExecutionOutput{{Name: "patients", State: "READY"}}}, nil } - service, _ := New(store, config) + 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) @@ -283,7 +282,7 @@ func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { config.PrepareRelease = func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { return dataset.ProjectRelease{}, 0, errors.New("release preparation failed") } - service, _ = New(store, config) + 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) @@ -292,8 +291,8 @@ func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { func TestServiceUsesConfiguredClock(t *testing.T) { clock := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC) - service, err := New(&fakeStore{}, Config{Now: func() time.Time { return clock }}) - if err != nil || service.now() != clock { - t.Fatalf("clock=%v, err=%v", service.now(), err) + 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 index 1adec99c..85bf6281 100644 --- a/internal/explorer/lifecycle/types.go +++ b/internal/explorer/lifecycle/types.go @@ -18,24 +18,6 @@ import ( "github.com/calypr/loom/internal/explorer/capability" ) -// Store is the persistence surface required by Explorer workflows. The -// concrete Explorer service implements this interface; tests can provide a -// small fake without constructing a database-backed service. -type Store interface { - List(context.Context, string) ([]explorer.Explorer, error) - Get(context.Context, string, string) (*explorer.Explorer, error) - LoadExplorerState(context.Context, string, string) (explorer.ExplorerStateV1, error) - CreateInteractiveFrom(context.Context, string, string, string, string, string) (*explorer.Explorer, error) - ApplyWorkspaceCommands(context.Context, string, string, authoringv2.CatalogSnapshot, authoringv2.ApplyCommandsRequest, string) (*authoringv2.ApplyCommandsResponse, error) - ActiveRevision(context.Context, string, string) (*explorer.Revision, error) - CompilationReceiptForExplorer(context.Context, string, string, string) (*explorer.CompilationReceipt, error) - PublishAuthoring(context.Context, explorer.CompilationReceipt, explorer.Revision, dataset.ProjectRelease, int64) (*explorer.Revision, error) - UpsertRepositoryV2(context.Context, explorer.CompilationReceipt, string, string, []explorer.Materialization, explorer.DatasetMetadata, explorer.PublicationMetadata) (*explorer.Explorer, *explorer.Revision, error) - FailRevision(context.Context, string, []explorer.Diagnostic) (*explorer.Revision, error) - ActivateRepositoryGeneration(context.Context, string, string, string) error - SaveRepositoryConfig(context.Context, explorer.RepositoryConfig) (*explorer.RepositoryConfig, error) -} - // 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. @@ -144,7 +126,7 @@ type BuilderRequest struct { ExplorerID string } -type CompileRequest struct { +type compileRequest struct { Project string ExplorerID string Workspace authoringv2.Workspace diff --git a/internal/explorer/service.go b/internal/explorer/service.go index 5f952fb9..818fdb19 100644 --- a/internal/explorer/service.go +++ b/internal/explorer/service.go @@ -32,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) } @@ -52,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 } @@ -83,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) } @@ -95,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 { @@ -111,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{ @@ -133,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, @@ -155,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, @@ -182,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") @@ -200,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 { @@ -215,7 +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()}) + 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 @@ -298,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 { @@ -312,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 @@ -365,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 { @@ -375,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 d476332e..b0217df0 100644 --- a/internal/explorer/store.go +++ b/internal/explorer/store.go @@ -24,12 +24,9 @@ var ( // 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) @@ -38,6 +35,6 @@ type Store interface { 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/server/explorer_capability.go b/internal/server/explorer_capability.go index 3e8f2fc1..49cb3548 100644 --- a/internal/server/explorer_capability.go +++ b/internal/server/explorer_capability.go @@ -48,26 +48,6 @@ type explorerCapabilityResolver struct { 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 = lifecycle.AuthorizedCapability - -// 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) - 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") @@ -179,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 } diff --git a/internal/server/explorer_config_route.go b/internal/server/explorer_config_route.go index d1963e99..806d3b0f 100644 --- a/internal/server/explorer_config_route.go +++ b/internal/server/explorer_config_route.go @@ -19,14 +19,15 @@ type explorerHTTPHandlers struct { application *lifecycle.Service } -func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, configs ...ExplorerV2LifecycleConfig) *explorerHTTPHandlers { - config := ExplorerV2LifecycleConfig{} +func newExplorerHTTPHandlers(authorizer authscope.Authorizer, authorizeRead explorerConfigReadAuthorizer, explorers *explorer.Service, configs ...lifecycle.Config) *explorerHTTPHandlers { + config := lifecycle.Config{} if len(configs) > 0 { config = configs[0] } + application, _ := lifecycle.New(explorers, config) return &explorerHTTPHandlers{ authorizer: authorizer, authorizeRead: authorizeRead, - application: newExplorerLifecycleApplication(explorers, config), + application: application, } } diff --git a/internal/server/explorer_config_route_test.go b/internal/server/explorer_config_route_test.go index 639a3e4d..eb96b5f0 100644 --- a/internal/server/explorer_config_route_test.go +++ b/internal/server/explorer_config_route_test.go @@ -8,13 +8,13 @@ import ( "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" ) @@ -25,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 { @@ -40,16 +40,19 @@ 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) (graphresolver.RecipeExecution, error) { + MaterializeReceipt: func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { return materialize(ctx, receipt.Bundle, bindings) }, ActivateRelease: activateRelease, @@ -67,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) diff --git a/internal/server/explorer_lifecycle_adapter.go b/internal/server/explorer_lifecycle_adapter.go deleted file mode 100644 index b18d2447..00000000 --- a/internal/server/explorer_lifecycle_adapter.go +++ /dev/null @@ -1,91 +0,0 @@ -package server - -import ( - "context" - - graphresolver "github.com/calypr/loom/internal/api/graphql/graph/resolver" - 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" - "github.com/calypr/loom/internal/explorer/lifecycle" -) - -// newExplorerLifecycleApplication is the only server-to-application wiring -// seam. GraphQL execution values are converted here so the lifecycle package -// never depends on resolver transport types. -func newExplorerLifecycleApplication(store *explorer.Service, config ExplorerV2LifecycleConfig) *lifecycle.Service { - if store == nil { - return nil - } - toExecution := func(value graphresolver.RecipeExecution) lifecycle.Execution { - result := lifecycle.Execution{ID: value.ID, Name: value.Name, RecipeDigest: value.RecipeDigest, ResolvedSchemaDigest: value.ResolvedSchemaDigest, SourceGeneration: value.SourceGeneration, State: value.State} - result.Outputs = make([]lifecycle.ExecutionOutput, 0, len(value.Outputs)) - for _, output := range value.Outputs { - result.Outputs = append(result.Outputs, lifecycle.ExecutionOutput{Name: output.Name, State: output.State, RowCount: output.RowCount, Columns: output.Columns}) - } - return result - } - appConfig := lifecycle.Config{ - Capability: lifecycle.CapabilityResolver{ - Current: config.Capability, - Token: config.CapabilityToken, - Catalog: func(snapshot capability.Snapshot, explorerID string) authoringv2.CatalogSnapshot { - return authoringV2Catalog(snapshot, explorerID) - }, - }, - } - if config.CompileReceipt != nil { - appConfig.CompileReceipt = func(ctx context.Context, request lifecycle.CompileReceiptRequest) (*explorer.CompilationReceipt, error) { - return config.CompileReceipt(ctx, request) - } - } - if config.PreviewReceipt != nil { - appConfig.PreviewReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings, visit func(map[string]any) error) (dataframeexecution.PreviewSummary, error) { - return config.PreviewReceipt(ctx, receipt, bindings, visit) - } - } - if config.ValidateReleaseGeneration != nil { - appConfig.ValidateReleaseGeneration = func(ctx context.Context, project, generation string) error { - return config.ValidateReleaseGeneration(ctx, project, generation) - } - } - if config.ActivateRelease != nil { - appConfig.ActivateRelease = func(ctx context.Context, project, generation string, selectors []dataset.DataframeSelector) error { - return config.ActivateRelease(ctx, project, generation, selectors) - } - } - if config.PrepareRelease != nil { - appConfig.PrepareRelease = func(ctx context.Context, project, generation string, selectors []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) { - return config.PrepareRelease(ctx, project, generation, selectors) - } - } - if config.AuthorizedCapabilityCompile != nil { - appConfig.Capability.ForCompilation = config.AuthorizedCapabilityCompile - } - if config.AuthorizedCapabilityExecution != nil { - appConfig.Capability.ForExecution = config.AuthorizedCapabilityExecution - } - if config.MaterializeReceipt != nil { - appConfig.MaterializeReceipt = func(ctx context.Context, receipt *explorer.CompilationReceipt, bindings recipe.RuntimeBindings) (lifecycle.Execution, error) { - value, err := config.MaterializeReceipt(ctx, receipt, bindings) - return toExecution(value), err - } - } - if config.ReceiptLookup != nil { - appConfig.ReceiptLookup = func(ctx context.Context, project, explorerID, receiptID string) (*explorer.CompilationReceipt, error) { - return config.ReceiptLookup(ctx, project, explorerID, receiptID) - } - } - if config.Logger != nil { - // Logging remains a transport/deployment concern. Preview metrics are - // emitted by the existing server adapter around the sink callback. - } - application, err := lifecycle.New(store, appConfig) - if err != nil { - return nil - } - return application -} diff --git a/internal/server/explorer_lifecycle_types.go b/internal/server/explorer_lifecycle_types.go deleted file mode 100644 index 3528b529..00000000 --- a/internal/server/explorer_lifecycle_types.go +++ /dev/null @@ -1,37 +0,0 @@ -package server - -import ( - "context" - "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/recipe" - "github.com/calypr/loom/internal/dataset" - "github.com/calypr/loom/internal/explorer" - "github.com/calypr/loom/internal/explorer/lifecycle" -) - -type ExplorerV2ReceiptCompileRequest = lifecycle.CompileReceiptRequest -type ExplorerV2ReceiptCompiler = lifecycle.ReceiptCompiler -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) (dataframeexecution.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 ExplorerV2ReleasePreparer func(context.Context, string, string, []dataset.DataframeSelector) (dataset.ProjectRelease, int64, error) - -type ExplorerV2LifecycleConfig struct { - CompileReceipt ExplorerV2ReceiptCompiler - Capability ExplorerCapabilityReader - CapabilityToken ExplorerCapabilityTokenReader - AuthorizedCapabilityCompile ExplorerAuthorizedCapabilityCompilationReader - AuthorizedCapabilityExecution ExplorerAuthorizedCapabilityExecutionReader - PreviewReceipt ExplorerV2ReceiptPreviewer - MaterializeReceipt ExplorerV2ReceiptMaterializer - ReceiptLookup ExplorerV2ReceiptReader - Logger *slog.Logger - ValidateReleaseGeneration ExplorerV2GenerationValidator - ActivateRelease ExplorerV2ReleaseActivator - PrepareRelease ExplorerV2ReleasePreparer -} diff --git a/internal/server/explorer_publication.go b/internal/server/explorer_publication.go index 18d95224..facc57f3 100644 --- a/internal/server/explorer_publication.go +++ b/internal/server/explorer_publication.go @@ -5,24 +5,24 @@ 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/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 execution package. -func explorerReceiptMaterializer(recipeEngine *dataframeexecution.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) { +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 @@ -35,20 +35,20 @@ func explorerReceiptMaterializer(recipeEngine *dataframeexecution.Engine, target 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 + 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 3afa274b..92c3caa5 100644 --- a/internal/server/explorer_receipt_contract.go +++ b/internal/server/explorer_receipt_contract.go @@ -16,10 +16,11 @@ import ( "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 ExplorerV2ReceiptCompileRequest, capabilityResolver *explorerCapabilityResolver, recipeEngine *dataframeexecution.Engine, explorerService *explorer.Service, logger *slog.Logger) (*explorer.CompilationReceipt, error) { +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) == "" { @@ -53,20 +54,6 @@ func compileExplorerReceipt(ctx context.Context, request ExplorerV2ReceiptCompil if err != nil { return nil, err } - presentationByEmission := make(map[string]explorercompilation.PresentationColumn) - for _, presentation := range translated.Presentations { - for _, column := range presentation.Columns { - presentationByEmission[column.EmissionID] = column - } - } - emitted := make([]explorer.EmittedColumn, 0, len(translated.EmittedColumns)) - 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 @@ -87,7 +74,7 @@ func compileExplorerReceipt(ctx context.Context, request ExplorerV2ReceiptCompil 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: mappings, EmittedColumns: emitted, OutputFingerprints: fingerprints, OutputColumnProvenance: columnProvenance, RequestID: request.RequestID, CreatedAt: time.Now().UTC()} + 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 diff --git a/internal/server/explorer_receipt_contract_test.go b/internal/server/explorer_receipt_contract_test.go index 6b79616d..28d23a9d 100644 --- a/internal/server/explorer_receipt_contract_test.go +++ b/internal/server/explorer_receipt_contract_test.go @@ -18,6 +18,7 @@ import ( "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" ) @@ -178,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"}, }, @@ -218,31 +219,17 @@ 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) (dataframeexecution.PreviewSummary, error) { previewCalls++ if receipt == nil || bindings.AuthScopeMode != authscope.ReadScopeUnrestricted || bindings.IncludeAuthResourcePath { @@ -254,32 +241,17 @@ func TestNativeV2RouteUsesAuthorizedPersistedReceipt(t *testing.T) { 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() - registerGeneratedExplorerTestRoutes(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/builder", 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) } @@ -290,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) } @@ -301,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) } @@ -319,16 +291,19 @@ 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) }, } @@ -380,7 +355,7 @@ func TestCreateExplorerFromCurrentClonesWorkspaceOnServer(t *testing.T) { t.Fatal(err) } store := newTestExplorerStore() - if _, err := store.CreateInteractive(context.Background(), explorer.Explorer{Project: "project-a", ExplorerID: "source", Title: "Source", ManagementMode: explorer.ManagementInteractive, DraftConfig: canonical, DraftVersion: 1, DraftDigest: digest}); err != nil { + 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) @@ -388,7 +363,7 @@ func TestCreateExplorerFromCurrentClonesWorkspaceOnServer(t *testing.T) { t.Fatal(err) } app := fiber.New() - registerGeneratedExplorerTestRoutes(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) @@ -406,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() diff --git a/internal/server/explorer_state_route_test.go b/internal/server/explorer_state_route_test.go index 1c93ade2..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() - registerGeneratedExplorerTestRoutes(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() - registerGeneratedExplorerTestRoutes(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/http_test_helpers_test.go b/internal/server/http_test_helpers_test.go index 7645c77e..1eadd680 100644 --- a/internal/server/http_test_helpers_test.go +++ b/internal/server/http_test_helpers_test.go @@ -87,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() @@ -139,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) } @@ -222,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 } @@ -250,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/openapi_explorer_authoring_direct.go b/internal/server/openapi_explorer_authoring_direct.go index cd34a7cc..5c7ad4e6 100644 --- a/internal/server/openapi_explorer_authoring_direct.go +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -116,21 +116,6 @@ func (h *explorerHTTPHandlers) applyAuthoringCommandsDirect(ctx context.Context, return directAuthoringJSON[loomapi.ApplyCommandsResponse](value) } -func (h *explorerHTTPHandlers) compileAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.CompileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { - var result loomapi.CompileResponse - if err := h.authoringWriteDirect(ctx, project); err != nil { - return result, err - } - if body == nil { - return result, malformedRouteError("intent", errors.New("request body is required")) - } - receipt, err := h.application.Compile(ctx, lifecycle.CompileRequest{Project: project, ExplorerID: explorerID, Workspace: body.Workspace, SnapshotToken: body.SnapshotToken, RequestID: requestIDFromContext(ctx)}) - if err != nil { - return result, err - } - return v2ReceiptResponse(receipt, body.Workspace), nil -} - func (h *explorerHTTPHandlers) reconcileAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.ReconcileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { var result loomapi.CompileResponse if err := h.authoringWriteDirect(ctx, project); err != nil { diff --git a/internal/server/openapi_explorer_routes.go b/internal/server/openapi_explorer_routes.go index 846640e1..c25d41fe 100644 --- a/internal/server/openapi_explorer_routes.go +++ b/internal/server/openapi_explorer_routes.go @@ -197,36 +197,6 @@ func (r *HTTPRoutes) ApplyExplorerBuilderCommands(ctx context.Context, request l } } -func (r *HTTPRoutes) CompileExplorerBuilder(ctx context.Context, request loomapi.CompileExplorerBuilderRequestObject) (loomapi.CompileExplorerBuilderResponseObject, error) { - project := string(request.Project) - explorerID := string(request.ExplorerId) - if r == nil || r.explorer == nil { - _, failure := authoringErrorForOpenAPI(ctx, "compileExplorerBuilder", explorerUnavailable("compile", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) - return loomapi.CompileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil - } - value, err := r.explorer.compileAuthoringDirect(ctx, project, explorerID, request.Body) - if err == nil { - return loomapi.CompileExplorerBuilder200JSONResponse(value), nil - } - status, failure := authoringErrorForOpenAPI(ctx, "compileExplorerBuilder", err) - switch status { - case http.StatusBadRequest: - return loomapi.CompileExplorerBuilder400JSONResponse{AuthoringBadRequestJSONResponse: loomapi.AuthoringBadRequestJSONResponse(failure)}, nil - case http.StatusForbidden: - return loomapi.CompileExplorerBuilder403JSONResponse{AuthoringForbiddenJSONResponse: loomapi.AuthoringForbiddenJSONResponse(failure)}, nil - case http.StatusConflict: - return loomapi.CompileExplorerBuilder409JSONResponse{AuthoringConflictJSONResponse: loomapi.AuthoringConflictJSONResponse(failure)}, nil - case http.StatusUnprocessableEntity: - return loomapi.CompileExplorerBuilder422JSONResponse{AuthoringUnprocessableJSONResponse: loomapi.AuthoringUnprocessableJSONResponse(failure)}, nil - case http.StatusInternalServerError: - return loomapi.CompileExplorerBuilder500JSONResponse{AuthoringInternalErrorJSONResponse: loomapi.AuthoringInternalErrorJSONResponse(failure)}, nil - case http.StatusServiceUnavailable: - return loomapi.CompileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil - default: - return nil, unexpectedResponseStatus("compileExplorerBuilder", status) - } -} - func (r *HTTPRoutes) ReconcileExplorerBuilder(ctx context.Context, request loomapi.ReconcileExplorerBuilderRequestObject) (loomapi.ReconcileExplorerBuilderResponseObject, error) { project := string(request.Project) explorerID := string(request.ExplorerId) diff --git a/internal/server/openapi_route_inventory_test.go b/internal/server/openapi_route_inventory_test.go index d9455529..8756c381 100644 --- a/internal/server/openapi_route_inventory_test.go +++ b/internal/server/openapi_route_inventory_test.go @@ -47,8 +47,8 @@ func TestGeneratedRoutesExactlyMatchOpenAPISpec(t *testing.T) { t.Errorf("registered route is absent from OpenAPI: %s", route) } } - if len(got) != 22 || len(want) != 22 { - t.Errorf("route count got=%d spec=%d, want 22", len(got), len(want)) + if len(got) != 21 || len(want) != 21 { + t.Errorf("route count got=%d spec=%d, want 21", len(got), len(want)) } } diff --git a/internal/server/openapi_test_helpers_test.go b/internal/server/openapi_test_helpers_test.go index 29018cc9..c73178bf 100644 --- a/internal/server/openapi_test_helpers_test.go +++ b/internal/server/openapi_test_helpers_test.go @@ -6,12 +6,13 @@ import ( 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 ExplorerV2LifecycleConfig) { +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/server.go b/internal/server/server.go index b313f8fb..7a2163c1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -33,6 +33,7 @@ import ( "github.com/calypr/loom/internal/explorer" explorerarango "github.com/calypr/loom/internal/explorer/arango" "github.com/calypr/loom/internal/explorer/capability" + "github.com/calypr/loom/internal/explorer/lifecycle" "github.com/calypr/loom/internal/ingest" arangostore "github.com/calypr/loom/internal/store/arango" clickhousestore "github.com/calypr/loom/internal/store/clickhouse" @@ -243,6 +244,9 @@ func run(ctx context.Context, serverConfig Config) error { 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) @@ -295,18 +299,20 @@ func run(ctx context.Context, serverConfig Config) error { if err != nil { return fmt.Errorf("create generation load service: %w", err) } - compileReceipt := func(ctx context.Context, request ExplorerV2ReceiptCompileRequest) (*explorer.CompilationReceipt, error) { + compileReceipt := func(ctx context.Context, request lifecycle.CompileReceiptRequest) (*explorer.CompilationReceipt, error) { return compileExplorerReceipt(ctx, request, capabilityResolver, recipeEngine, explorerService, logger) } - lifecycleConfig := ExplorerV2LifecycleConfig{ + 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) }, diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 8d8cff2d..f98a39b3 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -65,28 +65,6 @@ 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/commands: parameters: - $ref: '#/components/parameters/Project' @@ -896,13 +874,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 From 6bfefa0d76a1a94132d7ce4311cdb6c877225405 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sun, 30 Aug 2026 20:58:20 -0700 Subject: [PATCH 11/17] add ci job for dataframer to use open access fhir aggregator data --- .github/workflows/acceptance.yaml | 66 + .gitignore | 2 + Makefile | 10 +- cmd/loom-acceptance/main.go | 119 + docs/DEVELOPER_ARCHITECTURE.md | 7 +- docs/PACKAGE_AUDIT.csv | 57 + docs/PACKAGE_AUDIT.md | 116 + docs/PACKAGE_AUDIT_DECISIONS.csv | 4 + docs/README.md | 2 + docs/REAL_DATA_ACCEPTANCE.md | 155 + internal/acceptance/fixture.go | 762 + internal/acceptance/fixture_test.go | 341 + internal/acceptance/performance.go | 228 + internal/acceptance/performance_test.go | 152 + internal/acceptance/runner.go | 247 + internal/acceptance/scenario.go | 732 + internal/acceptance/scenario_test.go | 152 + internal/api/bulk/load/errors.go | 4 +- internal/api/bulk/load/errors_test.go | 6 +- internal/api/bulk/load/operations.go | 8 +- internal/api/bulk/load/service.go | 12 +- internal/api/bulk/load/service_test.go | 12 +- internal/api/graphql/errors_test.go | 73 - internal/api/graphql/graph/doc.go | 6 +- internal/api/graphql/{ => graph}/errors.go | 24 +- internal/api/graphql/graph/errors_test.go | 189 + internal/api/graphql/graph/handler.go | 3 +- internal/api/http/errors.go | 4 +- internal/api/http/errors_test.go | 16 + .../compiler/generic_physical_plan_test.go | 31 + .../compiler/ir/physical_validation_refs.go | 23 +- .../dataframe/compiler/ir/physical_values.go | 10 +- .../dataframe/compiler/lower/child_sets.go | 3 +- .../compiler/lower/recipe_compile.go | 3 +- .../dataframe/compiler/lower/recipe_schema.go | 19 +- .../compiler/lower/root_projection.go | 29 +- .../compiler/render/aql/collections.go | 14 + .../expression/expression_checking.go | 12 - .../dataframe/expression/recipe_adapter.go | 86 +- .../expression/recipe_adapter_test.go | 89 + internal/dataframe/publication/runner.go | 12 + internal/dataframe/publication/runner_test.go | 21 + internal/dataframe/publication/types.go | 19 +- internal/dataframe/recipe/document_types.go | 36 +- .../dataframe/recipe/document_validation.go | 3 + .../recipe/exec/arango/registry_test.go | 220 + .../dataframe/recipe/exec/arango/revisions.go | 17 +- .../recipe/exec/arango/revisions_test.go | 47 + internal/dataframe/recipe/fragments.go | 211 +- internal/dataframe/recipe/fragments_test.go | 220 +- internal/dataframe/recipe/types_test.go | 11 + internal/dataframe/recipe/validation_rich.go | 34 +- .../dataframe/semantic/recipe_plan_output.go | 4 +- .../dataframe/semantic/recipe_plan_types.go | 1 + .../dataframe/semantic/recipe_rich_shaping.go | 73 +- .../semantic/recipe_rich_shaping_test.go | 29 + internal/dataframe/semantic/semantic_nodes.go | 5 + .../explorer/authoringv2/semantic_types.go | 44 +- .../explorer/authoringv2/workspace_test.go | 19 + .../explorer/compilation/semantic_compile.go | 96 +- .../explorer/compilation/translate_test.go | 28 + internal/server/openapi_explorer_direct.go | 3 + .../server/openapi_explorer_direct_test.go | 17 + internal/server/recipe_publication.go | 19 +- scripts/acceptance-performance.sh | 97 + scripts/acceptance-real.sh | 197 + scripts/package_audit.py | 565 + scripts/package_audit_verify.py | 110 + scripts/tests/__init__.py | 1 + scripts/tests/test_acceptance_real.py | 61 + scripts/tests/test_package_audit.py | 123 + .../ncpi-tcga-brca/fixture.lock.json | 34328 ++++++++++++++++ .../acceptance/ncpi-tcga-brca/oracle.json | 46 + .../acceptance/ncpi-tcga-brca/recipe.json | 14 + .../acceptance/ncpi-tcga-brca/workspace.json | 48 + 75 files changed, 40358 insertions(+), 249 deletions(-) create mode 100644 .github/workflows/acceptance.yaml create mode 100644 cmd/loom-acceptance/main.go create mode 100644 docs/PACKAGE_AUDIT.csv create mode 100644 docs/PACKAGE_AUDIT.md create mode 100644 docs/PACKAGE_AUDIT_DECISIONS.csv create mode 100644 docs/REAL_DATA_ACCEPTANCE.md create mode 100644 internal/acceptance/fixture.go create mode 100644 internal/acceptance/fixture_test.go create mode 100644 internal/acceptance/performance.go create mode 100644 internal/acceptance/performance_test.go create mode 100644 internal/acceptance/runner.go create mode 100644 internal/acceptance/scenario.go create mode 100644 internal/acceptance/scenario_test.go delete mode 100644 internal/api/graphql/errors_test.go rename internal/api/graphql/{ => graph}/errors.go (72%) create mode 100644 internal/api/graphql/graph/errors_test.go create mode 100644 internal/dataframe/recipe/exec/arango/registry_test.go create mode 100644 internal/server/openapi_explorer_direct_test.go create mode 100755 scripts/acceptance-performance.sh create mode 100755 scripts/acceptance-real.sh create mode 100644 scripts/package_audit.py create mode 100644 scripts/package_audit_verify.py create mode 100644 scripts/tests/__init__.py create mode 100644 scripts/tests/test_acceptance_real.py create mode 100644 scripts/tests/test_package_audit.py create mode 100644 testdata/acceptance/ncpi-tcga-brca/fixture.lock.json create mode 100644 testdata/acceptance/ncpi-tcga-brca/oracle.json create mode 100644 testdata/acceptance/ncpi-tcga-brca/recipe.json create mode 100644 testdata/acceptance/ncpi-tcga-brca/workspace.json diff --git a/.github/workflows/acceptance.yaml b/.github/workflows/acceptance.yaml new file mode 100644 index 00000000..d86b983a --- /dev/null +++ b/.github/workflows/acceptance.yaml @@ -0,0 +1,66 @@ +name: real-data acceptance + +on: + push: + 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://localhost: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: 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..810ef510 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .DS_Store .gocache/ +.cache/ +.artifacts/ bin/ data/ META/ diff --git a/Makefile b/Makefile index 413d02fd..4c5ada5d 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 GO ?= go GO_VERSION ?= 1.26.5 @@ -90,5 +90,13 @@ 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 + clean: rm -rf bin diff --git a/cmd/loom-acceptance/main.go b/cmd/loom-acceptance/main.go new file mode 100644 index 00000000..d5ae09be --- /dev/null +++ b/cmd/loom-acceptance/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/calypr/loom/internal/acceptance" +) + +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") + ) + flag.Parse() + 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 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/docs/DEVELOPER_ARCHITECTURE.md b/docs/DEVELOPER_ARCHITECTURE.md index 5940a570..a8d8c3e4 100644 --- a/docs/DEVELOPER_ARCHITECTURE.md +++ b/docs/DEVELOPER_ARCHITECTURE.md @@ -241,9 +241,10 @@ When adding code, use this lookup table: | AQL text or bind emission | `internal/dataframe/compiler/render/aql` | | 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/PACKAGE_AUDIT.csv b/docs/PACKAGE_AUDIT.csv new file mode 100644 index 00000000..36707c89 --- /dev/null +++ b/docs/PACKAGE_AUDIT.csv @@ -0,0 +1,57 @@ +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 +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/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,—,—,538,245,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,2416,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,—,—,3716,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,—,—,2926,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,—,—,2527,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,—,—,808,145,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,—,—,765,290,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/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/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,—,—,1882,268,50,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; 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,revisions_test.go,—,—,286,83,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/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,—,—,2344,373,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,—,—,1525,322,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,—,—,—,—,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,—,—,638,164,8,Compile; CompileWorkspace; Error; PresentationColumn; PresentationConfig; Result; Unwrap; WorkspaceResult,internal/dataframe/recipe; internal/dataframe/spec; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability,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/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_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,—,—,3653,1960,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/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/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..ff1e9139 --- /dev/null +++ b/docs/PACKAGE_AUDIT.md @@ -0,0 +1,116 @@ +# 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: 56 +- Production Go files: 335 +- Test Go files: 179 +- Production Go lines: 338,307 +- Test Go lines: 21,488 +- Package kinds: conformance=1, domain/library=28, executable=8, 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/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 | +| `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/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: — | 538/245 | 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,416 | 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,716/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,926/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,527/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: — | 808/145 | 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: — | 765/290 | 34: BundleCatalog; BundleExecution; BundleIdentity; BundleOutputRecord; BundlePointer; BundleState; Canonical; CanonicalizeLegacy; ColumnProvenance; DataframeSelector; Error; ExactExecutionCatalog; … (+22) | internal/dataframe/errors; internal/dataset | 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/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: — | 1,882/268 | 50: Aggregate; AggregateOperation; ArrayQuantifier; Bundle; CanonicalJSON; CatalogProjection; Clone; CodeValue; ColumnNaming; Digest; DocumentRef; DynamicColumn; … (+38) | 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: revisions_test.go; Other: — | 286/83 | 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,344/373 | 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,525/322 | 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: — | 638/164 | 8: Compile; CompileWorkspace; Error; PresentationColumn; PresentationConfig; Result; Unwrap; WorkspaceResult | internal/dataframe/recipe; internal/dataframe/spec; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability | 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/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_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,653/1,960 | 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/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/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/README.md b/docs/README.md index 9ccad377..e164f3b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,6 +24,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 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/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..43acc5de --- /dev/null +++ b/internal/acceptance/scenario.go @@ -0,0 +1,732 @@ +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 { + 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..cb229ddb --- /dev/null +++ b/internal/acceptance/scenario_test.go @@ -0,0 +1,152 @@ +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 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/bulk/load/errors.go b/internal/api/bulk/load/errors.go index ccab32ad..66bf52aa 100644 --- a/internal/api/bulk/load/errors.go +++ b/internal/api/bulk/load/errors.go @@ -7,9 +7,9 @@ import ( "github.com/calypr/loom/internal/ingest" ) -// NormalizeError translates ingest-owned failures at the ingest HTTP boundary +// 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 { +func normalizeError(err error) error { if err == nil { return nil } diff --git a/internal/api/bulk/load/errors_test.go b/internal/api/bulk/load/errors_test.go index 0cf1d276..ecfaaf5e 100644 --- a/internal/api/bulk/load/errors_test.go +++ b/internal/api/bulk/load/errors_test.go @@ -10,7 +10,7 @@ import ( 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)) + userErr, ok := dataframeerrors.AsUserError(normalizeError(preflight)) if !ok || userErr.Code() != "INGEST_PREFLIGHT_FAILED" || userErr.Retryable() { t.Fatalf("preflight normalized = %#v", userErr) } @@ -19,13 +19,13 @@ func TestNormalizeIngestionFailures(t *testing.T) { } incomplete := &ingest.GenerationLoadIncompleteError{ValidationErrors: 2, GenerationErrors: 1, EdgeErrors: 3} - userErr, ok = dataframeerrors.AsUserError(NormalizeError(incomplete)) + 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)) + 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/operations.go b/internal/api/bulk/load/operations.go index ca274682..80d43c45 100644 --- a/internal/api/bulk/load/operations.go +++ b/internal/api/bulk/load/operations.go @@ -58,8 +58,8 @@ func (h *Handler) CreateDatasetGeneration(ctx context.Context, project, generati if principal != nil { req.SubmittedBy = principal.Subject } - result, err := h.service.RunGeneration(ctx, req) - return result, NormalizeError(err) + 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) { @@ -70,8 +70,8 @@ func (h *Handler) ActivateDatasetGeneration(ctx context.Context, project, genera 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) + 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 } diff --git a/internal/api/bulk/load/service.go b/internal/api/bulk/load/service.go index e316a9bf..bbcd26f5 100644 --- a/internal/api/bulk/load/service.go +++ b/internal/api/bulk/load/service.go @@ -17,7 +17,6 @@ type GenerationLoadRequest struct { AuthResourcePath string StagedDir string SubmittedBy string - StageOnly bool DeferActivation bool } @@ -60,7 +59,8 @@ 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) } @@ -96,7 +96,7 @@ func NewService(cfg ServiceConfig) (*Service, error) { }, nil } -func (s *Service) RunGeneration(ctx context.Context, req GenerationLoadRequest) (*GenerationLoadResult, error) { +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") } @@ -129,13 +129,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 0640c461..a5813a72 100644 --- a/internal/api/bulk/load/service_test.go +++ b/internal/api/bulk/load/service_test.go @@ -69,7 +69,7 @@ func TestRunGenerationPropagatesDeferActivationAndResult(t *testing.T) { 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) } @@ -101,7 +101,7 @@ func TestRunGenerationReusesReadyManifest(t *testing.T) { 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 { @@ -122,7 +122,7 @@ func TestRunGenerationLoadsWhenManifestDoesNotExist(t *testing.T) { 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 { @@ -140,7 +140,7 @@ func TestRunGenerationRejectsManifestLookupFailure(t *testing.T) { 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 { @@ -191,7 +191,7 @@ func TestActivateGenerationRejectsInvalidDataframeRelease(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("ActivateGeneration() unexpectedly succeeded") } if activator.readCalls != 0 || activator.activateCalls != 0 { @@ -222,7 +222,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/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/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/http/errors.go b/internal/api/http/errors.go index 934ed3c0..a1ac0ab7 100644 --- a/internal/api/http/errors.go +++ b/internal/api/http/errors.go @@ -110,7 +110,7 @@ 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", "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" @@ -137,6 +137,8 @@ func statusForCode(code string, fallback int) int { return http.StatusConflict case "PUBLICATION_LEASE_LOST": return http.StatusServiceUnavailable + case "PUBLICATION_FAILED": + return http.StatusServiceUnavailable case "OUTPUT_ENCODING_FAILED": return http.StatusInternalServerError case "EXPORT_LIMIT_EXCEEDED": diff --git a/internal/api/http/errors_test.go b/internal/api/http/errors_test.go index 2f3e4465..178899ba 100644 --- a/internal/api/http/errors_test.go +++ b/internal/api/http/errors_test.go @@ -87,3 +87,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/dataframe/compiler/generic_physical_plan_test.go b/internal/dataframe/compiler/generic_physical_plan_test.go index 113c22e8..5ca7f275 100644 --- a/internal/dataframe/compiler/generic_physical_plan_test.go +++ b/internal/dataframe/compiler/generic_physical_plan_test.go @@ -243,6 +243,37 @@ 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) 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 a9ba3cb3..6ead73af 100644 --- a/internal/dataframe/compiler/ir/physical_values.go +++ b/internal/dataframe/compiler/ir/physical_values.go @@ -150,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 { diff --git a/internal/dataframe/compiler/lower/child_sets.go b/internal/dataframe/compiler/lower/child_sets.go index b0cc5106..59713040 100644 --- a/internal/dataframe/compiler/lower/child_sets.go +++ b/internal/dataframe/compiler/lower/child_sets.go @@ -88,7 +88,8 @@ func buildOptionalChildPhysicalSet(physical *ir.PhysicalPlan, setIndex int, pare 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+"__") diff --git a/internal/dataframe/compiler/lower/recipe_compile.go b/internal/dataframe/compiler/lower/recipe_compile.go index 1174aa32..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 @@ -160,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_schema.go b/internal/dataframe/compiler/lower/recipe_schema.go index 0b2378ac..c9f4275f 100644 --- a/internal/dataframe/compiler/lower/recipe_schema.go +++ b/internal/dataframe/compiler/lower/recipe_schema.go @@ -63,7 +63,24 @@ func recipeOutputSchema(plan ir.PhysicalPlan, output semantic.OutputPlan, dynami 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 diff --git a/internal/dataframe/compiler/lower/root_projection.go b/internal/dataframe/compiler/lower/root_projection.go index de6ee62f..886c83d3 100644 --- a/internal/dataframe/compiler/lower/root_projection.go +++ b/internal/dataframe/compiler/lower/root_projection.go @@ -104,7 +104,7 @@ func rootPhysicalProjections(physical *ir.PhysicalPlan, root semantic.SemanticNo 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, "") @@ -168,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 { @@ -192,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} @@ -204,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; @@ -235,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} diff --git a/internal/dataframe/compiler/render/aql/collections.go b/internal/dataframe/compiler/render/aql/collections.go index 4e5885ba..a7bbbaf7 100644 --- a/internal/dataframe/compiler/render/aql/collections.go +++ b/internal/dataframe/compiler/render/aql/collections.go @@ -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/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/runner.go b/internal/dataframe/publication/runner.go index 3b7746d7..88509c74 100644 --- a/internal/dataframe/publication/runner.go +++ b/internal/dataframe/publication/runner.go @@ -211,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) } @@ -282,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 41545152..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 { @@ -167,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 70012766..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 { diff --git a/internal/dataframe/recipe/document_types.go b/internal/dataframe/recipe/document_types.go index c931e7a2..1e4735b6 100644 --- a/internal/dataframe/recipe/document_types.go +++ b/internal/dataframe/recipe/document_types.go @@ -34,6 +34,7 @@ type Output struct { 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"` @@ -49,6 +50,24 @@ type Output struct { 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 @@ -261,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 @@ -273,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 e37547d9..4788c14a 100644 --- a/internal/dataframe/recipe/document_validation.go +++ b/internal/dataframe/recipe/document_validation.go @@ -48,6 +48,9 @@ func (b Bundle) Validate() error { 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") } 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/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 1f9e6c63..8f6f7387 100644 --- a/internal/dataframe/recipe/types_test.go +++ b/internal/dataframe/recipe/types_test.go @@ -133,6 +133,17 @@ func TestTraversalColumnNamingValidation(t *testing.T) { } } +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/semantic/recipe_plan_output.go b/internal/dataframe/semantic/recipe_plan_output.go index 29caca45..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, TraversalColumnNaming: output.TraversalColumnNaming.Normalized(), 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, TraversalColumnNaming: output.TraversalColumnNaming.Normalized(), 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) } diff --git a/internal/dataframe/semantic/recipe_plan_types.go b/internal/dataframe/semantic/recipe_plan_types.go index f6ae1f1a..4cb28228 100644 --- a/internal/dataframe/semantic/recipe_plan_types.go +++ b/internal/dataframe/semantic/recipe_plan_types.go @@ -33,6 +33,7 @@ type OutputPlan struct { 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. diff --git a/internal/dataframe/semantic/recipe_rich_shaping.go b/internal/dataframe/semantic/recipe_rich_shaping.go index 8f89154d..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) } @@ -350,12 +371,13 @@ func lowerRecipeSlices(resourceType, alias string, scope scopeFrame, slices []re 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 adcbcbf7..21e64726 100644 --- a/internal/dataframe/semantic/recipe_rich_shaping_test.go +++ b/internal/dataframe/semantic/recipe_rich_shaping_test.go @@ -96,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" diff --git a/internal/dataframe/semantic/semantic_nodes.go b/internal/dataframe/semantic/semantic_nodes.go index f946a379..d1c7352b 100644 --- a/internal/dataframe/semantic/semantic_nodes.go +++ b/internal/dataframe/semantic/semantic_nodes.go @@ -53,12 +53,16 @@ type SemanticPivot struct { type SemanticAggregate struct { Name string + OutputName string Operation string FieldRef string Selector *spec.Selector Predicate *spec.Selector PredicateEquals string + PredicateKind spec.FilterValueKind ValueMode string + RequiredValues []string + ValueKind expression.ValueKind } type SemanticSlice struct { @@ -66,6 +70,7 @@ type SemanticSlice struct { Limit int Predicate *spec.Selector PredicateEquals string + PredicateKind spec.FilterValueKind Fields []SemanticField } diff --git a/internal/explorer/authoringv2/semantic_types.go b/internal/explorer/authoringv2/semantic_types.go index 5eb24175..91db29a2 100644 --- a/internal/explorer/authoringv2/semantic_types.go +++ b/internal/explorer/authoringv2/semantic_types.go @@ -31,6 +31,10 @@ type ColumnSource struct { 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 { @@ -133,9 +138,46 @@ 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) != "" { + 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 || strings.TrimSpace(s.ProjectionMode) != "" { return fmt.Errorf("%s projectId source has no 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/workspace_test.go b/internal/explorer/authoringv2/workspace_test.go index f8ee0954..8f6b938c 100644 --- a/internal/explorer/authoringv2/workspace_test.go +++ b/internal/explorer/authoringv2/workspace_test.go @@ -156,3 +156,22 @@ func TestDecodeWorkspaceRepairsPreviouslyOmittedEmptyColumns(t *testing.T) { t.Fatalf("decoded legacy draft did not restore required columns array: %s", encoded) } } + +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/compilation/semantic_compile.go b/internal/explorer/compilation/semantic_compile.go index a67edbc1..80c693c7 100644 --- a/internal/explorer/compilation/semantic_compile.go +++ b/internal/explorer/compilation/semantic_compile.go @@ -12,6 +12,7 @@ import ( "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 { @@ -21,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) { @@ -52,13 +54,17 @@ func compileSemanticDocument(ctx context.Context, project, explorerID string, do 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 { @@ -82,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 { @@ -127,7 +141,7 @@ func compileSemanticDocument(ctx context.Context, project, explorerID string, do 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), TraversalColumnNaming: recipe.TraversalColumnNamingAlias, 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 { @@ -215,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 } @@ -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_test.go b/internal/explorer/compilation/translate_test.go index 0692e1aa..d848ca00 100644 --- a/internal/explorer/compilation/translate_test.go +++ b/internal/explorer/compilation/translate_test.go @@ -48,6 +48,9 @@ func TestCompileSemanticWorkspacePreservesAuthoredColumnsAndTypedSources(t *test 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) } @@ -127,3 +130,28 @@ func TestSemanticNestedOccurrenceUsesGloballyScopedAlias(t *testing.T) { 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/server/openapi_explorer_direct.go b/internal/server/openapi_explorer_direct.go index c399d54d..fac3fefa 100644 --- a/internal/server/openapi_explorer_direct.go +++ b/internal/server/openapi_explorer_direct.go @@ -215,5 +215,8 @@ 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..747c2b33 --- /dev/null +++ b/internal/server/openapi_explorer_direct_test.go @@ -0,0 +1,17 @@ +package server + +import ( + "testing" + + loomapi "github.com/calypr/loom/generated/loomapi" +) + +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) + } +} diff --git a/internal/server/recipe_publication.go b/internal/server/recipe_publication.go index 50b1fcd6..830b23cd 100644 --- a/internal/server/recipe_publication.go +++ b/internal/server/recipe_publication.go @@ -59,7 +59,11 @@ func recipeOutputLogicalColumns(plan dataframeexecution.Resolved, outputName str 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 } @@ -75,6 +79,15 @@ func recipeOutputRootResourceType(plan dataframeexecution.Resolved, outputName s return "" } +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 { @@ -94,10 +107,14 @@ func publishResolvedRecipe(ctx context.Context, recipeEngine *dataframeexecution 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 diff --git a/scripts/acceptance-performance.sh b/scripts/acceptance-performance.sh new file mode 100755 index 00000000..2fdf1218 --- /dev/null +++ b/scripts/acceptance-performance.sh @@ -0,0 +1,97 @@ +#!/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" +} + +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/package_audit.py b/scripts/package_audit.py new file mode 100644 index 00000000..bb0fd90a --- /dev/null +++ b/scripts/package_audit.py @@ -0,0 +1,565 @@ +#!/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.", + "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/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/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..b87d83d2 --- /dev/null +++ b/scripts/tests/test_acceptance_real.py @@ -0,0 +1,61 @@ +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("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.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("--performance-repeat-base-report", performance) + self.assertIn("--performance-repeat-current-report", performance) + + +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/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/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/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}] +} From 9da7f2d6d30ec87f11ccb5de4d5d7f2a25d67236 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sun, 30 Aug 2026 21:17:56 -0700 Subject: [PATCH 12/17] fix ci --- .github/workflows/acceptance.yaml | 3 +-- scripts/acceptance-performance.sh | 6 ++++++ scripts/tests/test_acceptance_real.py | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/acceptance.yaml b/.github/workflows/acceptance.yaml index d86b983a..edcd53bd 100644 --- a/.github/workflows/acceptance.yaml +++ b/.github/workflows/acceptance.yaml @@ -1,7 +1,6 @@ name: real-data acceptance on: - push: pull_request: workflow_dispatch: @@ -19,7 +18,7 @@ jobs: ports: - 8529:8529 options: >- - --health-cmd="wget --quiet --tries=1 --spider http://localhost:8529/_api/version || exit 1" + --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 diff --git a/scripts/acceptance-performance.sh b/scripts/acceptance-performance.sh index 2fdf1218..821900c1 100755 --- a/scripts/acceptance-performance.sh +++ b/scripts/acceptance-performance.sh @@ -56,6 +56,12 @@ run_variant() { "$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 diff --git a/scripts/tests/test_acceptance_real.py b/scripts/tests/test_acceptance_real.py index b87d83d2..ca07e330 100644 --- a/scripts/tests/test_acceptance_real.py +++ b/scripts/tests/test_acceptance_real.py @@ -43,6 +43,10 @@ def test_local_and_github_wiring_contract(self) -> None: 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) @@ -53,6 +57,8 @@ def test_local_and_github_wiring_contract(self) -> None: 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) From db2f58fe88127e33f8a4960662e1ca49b95edf14 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Sun, 30 Aug 2026 21:28:09 -0700 Subject: [PATCH 13/17] expand acceptance to report results --- .github/workflows/acceptance.yaml | 49 +++++++++++++++++++++++++++ scripts/tests/test_acceptance_real.py | 6 ++++ 2 files changed, 55 insertions(+) diff --git a/.github/workflows/acceptance.yaml b/.github/workflows/acceptance.yaml index edcd53bd..e4888edd 100644 --- a/.github/workflows/acceptance.yaml +++ b/.github/workflows/acceptance.yaml @@ -56,6 +56,55 @@ jobs: 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 diff --git a/scripts/tests/test_acceptance_real.py b/scripts/tests/test_acceptance_real.py index ca07e330..862f2582 100644 --- a/scripts/tests/test_acceptance_real.py +++ b/scripts/tests/test_acceptance_real.py @@ -52,6 +52,12 @@ def test_local_and_github_wiring_contract(self) -> None: 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") From 66671f895464bc74ee63c41e377355ffac9ff2e5 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Tue, 1 Sep 2026 11:14:23 -0700 Subject: [PATCH 14/17] factor frontend ui into this repo + begin full dockerized demo --- .gitignore | 4 + Dockerfile | 12 +- Makefile | 14 +- README.md | 13 +- compose.yaml | 123 + docs/DEMO_QUICKSTART.md | 87 + docs/PACKAGE_AUDIT.csv | 36 +- docs/PACKAGE_AUDIT.md | 50 +- docs/README.md | 1 + generated/loomapi/api.gen.go | 316 +- internal/acceptance/scenario.go | 3 + internal/acceptance/scenario_test.go | 10 + .../graph/aggregate_http_integration_test.go | 66 + .../api/graphql/graph/dataframe/aggregates.go | 8 +- internal/api/graphql/graph/dataframe/model.go | 3 +- internal/api/graphql/graph/dataframe/rows.go | 4 +- .../api/graphql/graph/dataframe/service.go | 20 +- .../explorer/authoringv2/semantic_types.go | 12 +- .../explorer/authoringv2/workspace_test.go | 37 + internal/explorer/compilation/compile.go | 6 +- .../explorer/compilation/translate_test.go | 41 +- .../explorer/compilation/workspace_test.go | 41 + internal/explorer/lifecycle/policy.go | 3 +- internal/explorer/lifecycle/service_test.go | 17 + .../openapi_explorer_authoring_direct.go | 23 +- internal/server/openapi_explorer_direct.go | 4 +- internal/server/openapi_explorer_routes.go | 8 +- .../openapi_explorer_write_scope_test.go | 84 + openapi/openapi.yaml | 14 + scripts/demo-browser-smoke.sh | 108 + scripts/demo-down.sh | 7 + scripts/demo-smoke.sh | 11 + scripts/demo-up.sh | 31 + scripts/package_audit.py | 2 + .../acceptance/ncpi-tcga-brca/graphql.graphql | 6 + .../acceptance/ncpi-tcga-brca/provenance.json | 8 + ui/.dockerignore | 3 + ui/README.md | 30 + ui/apps/demo/Dockerfile | 14 + ui/apps/demo/index.html | 13 + ui/apps/demo/nginx.conf | 31 + ui/apps/demo/package.json | 21 + ui/apps/demo/src/main.tsx | 38 + ui/apps/demo/src/styles.css | 11 + ui/apps/demo/tsconfig.json | 13 + ui/apps/demo/vite.config.ts | 21 + ui/package-lock.json | 4152 +++++++++++++++++ ui/package.json | 30 + ui/packages/loom-ui/package.json | 44 + ui/packages/loom-ui/postcss.config.cjs | 6 + .../loom-ui/scripts/check-boundaries.mjs | 26 + ui/packages/loom-ui/src/Builder.tsx | 39 + ui/packages/loom-ui/src/Viewer.tsx | 105 + ui/packages/loom-ui/src/api.ts | 435 ++ ui/packages/loom-ui/src/api.unit.test.ts | 232 + ...lderWorkspace.reconciliation.unit.test.tsx | 348 ++ .../BuilderWorkspace.route.unit.test.tsx | 236 + .../ExplorerBuilder/BuilderWorkspace.tsx | 1121 +++++ .../ExplorerBuilder/authoring/model.ts | 328 ++ .../authoring/previewRecovery.ts | 46 + .../authoring/previewRecovery.unit.test.ts | 91 + .../ExplorerBuilder/authoring/reducer.ts | 513 ++ .../authoring/reducer.unit.test.ts | 614 +++ .../ExplorerBuilder/authoring/routeActions.ts | 62 + .../authoring/routeActions.unit.test.ts | 103 + .../components/BuilderToolbar.tsx | 382 ++ .../components/BuilderToolbar.unit.test.tsx | 136 + .../components/ColumnSelector.tsx | 565 +++ .../components/ColumnSelector.unit.test.tsx | 305 ++ .../components/GuidedGraphWorkspace.tsx | 547 +++ .../GuidedGraphWorkspace.unit.test.tsx | 479 ++ .../components/PresentationPanels.tsx | 52 + .../components/PreviewTable.tsx | 348 ++ .../components/PreviewTable.unit.test.ts | 193 + .../components/RouteExtensionPanel.tsx | 145 + .../RouteExtensionPanel.unit.test.tsx | 194 + .../ExplorerBuilder/components/icons.tsx | 50 + .../components/useDismissibleLayer.ts | 1 + .../features/ExplorerBuilder/graphLayout.ts | 112 + .../ExplorerBuilder/graphLayout.unit.test.ts | 37 + .../hooks/useDirtyBeforeUnload.ts | 11 + .../hooks/useDismissibleLayer.ts | 26 + .../ExplorerBuilder/hooks/useEscapeToClose.ts | 15 + .../ExplorerBuilder/hooks/useGraphLayout.ts | 39 + .../hooks/useGraphViewportFitter.ts | 61 + .../ExplorerBuilder/hooks/usePortalHost.ts | 20 + .../hooks/useTraversalOverflow.ts | 36 + .../src/features/ExplorerBuilder/index.ts | 5 + ui/packages/loom-ui/src/index.ts | 11 + ui/packages/loom-ui/src/react.tsx | 219 + ui/packages/loom-ui/src/react.unit.test.tsx | 97 + ui/packages/loom-ui/src/styles.css | 34 + ui/packages/loom-ui/src/test-matchers.d.ts | 13 + ui/packages/loom-ui/src/types.ts | 799 ++++ ui/packages/loom-ui/src/types.unit.test.ts | 30 + ui/packages/loom-ui/tailwind.config.cjs | 5 + ui/packages/loom-ui/tsconfig.json | 21 + ui/packages/loom-ui/vite.config.ts | 25 + ui/packages/loom-ui/vitest.config.ts | 9 + ui/packages/loom-ui/vitest.setup.ts | 70 + 100 files changed, 14863 insertions(+), 188 deletions(-) create mode 100644 compose.yaml create mode 100644 docs/DEMO_QUICKSTART.md create mode 100644 internal/server/openapi_explorer_write_scope_test.go create mode 100755 scripts/demo-browser-smoke.sh create mode 100755 scripts/demo-down.sh create mode 100755 scripts/demo-smoke.sh create mode 100755 scripts/demo-up.sh create mode 100644 testdata/acceptance/ncpi-tcga-brca/graphql.graphql create mode 100644 testdata/acceptance/ncpi-tcga-brca/provenance.json create mode 100644 ui/.dockerignore create mode 100644 ui/README.md create mode 100644 ui/apps/demo/Dockerfile create mode 100644 ui/apps/demo/index.html create mode 100644 ui/apps/demo/nginx.conf create mode 100644 ui/apps/demo/package.json create mode 100644 ui/apps/demo/src/main.tsx create mode 100644 ui/apps/demo/src/styles.css create mode 100644 ui/apps/demo/tsconfig.json create mode 100644 ui/apps/demo/vite.config.ts create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/packages/loom-ui/package.json create mode 100644 ui/packages/loom-ui/postcss.config.cjs create mode 100644 ui/packages/loom-ui/scripts/check-boundaries.mjs create mode 100644 ui/packages/loom-ui/src/Builder.tsx create mode 100644 ui/packages/loom-ui/src/Viewer.tsx create mode 100644 ui/packages/loom-ui/src/api.ts create mode 100644 ui/packages/loom-ui/src/api.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.reconciliation.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.route.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/previewRecovery.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/PresentationPanels.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/icons.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/components/useDismissibleLayer.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/graphLayout.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDirtyBeforeUnload.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useDismissibleLayer.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useEscapeToClose.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphLayout.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useGraphViewportFitter.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/usePortalHost.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/hooks/useTraversalOverflow.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/index.ts create mode 100644 ui/packages/loom-ui/src/index.ts create mode 100644 ui/packages/loom-ui/src/react.tsx create mode 100644 ui/packages/loom-ui/src/react.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/styles.css create mode 100644 ui/packages/loom-ui/src/test-matchers.d.ts create mode 100644 ui/packages/loom-ui/src/types.ts create mode 100644 ui/packages/loom-ui/src/types.unit.test.ts create mode 100644 ui/packages/loom-ui/tailwind.config.cjs create mode 100644 ui/packages/loom-ui/tsconfig.json create mode 100644 ui/packages/loom-ui/vite.config.ts create mode 100644 ui/packages/loom-ui/vitest.config.ts create mode 100644 ui/packages/loom-ui/vitest.setup.ts diff --git a/.gitignore b/.gitignore index 810ef510..e1c7ce41 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ 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..6021bd50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,16 +34,26 @@ 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 4c5ada5d..39136799 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 acceptance-real acceptance-performance +.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 GO ?= go GO_VERSION ?= 1.26.5 @@ -98,5 +98,17 @@ acceptance-real: 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 + clean: rm -rf bin diff --git a/README.md b/README.md index 9b28a184..9791ac0d 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,18 @@ 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. + +For backend-only development, the lightweight Compose stack starts ArangoDB and ClickHouse: ```bash rtk docker compose -f experimental/docker-compose.yml up -d diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 00000000..a9fd468c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,123 @@ +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-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 + 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: + - "127.0.0.1:8080:8080" + volumes: + - ./testdata/acceptance/ncpi-tcga-brca/recipe.json:/app/testdata/recipe.json:ro + + demo-seed: + 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 + - NCPI_ACCEPTANCE + - --generation + - tcga-brca-locked + depends_on: + loom-api: + condition: service_healthy + volumes: + - ./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: + - "127.0.0.1:3080:8080" + +volumes: + arangodb_data: + clickhouse_data: + fixture_cache: + demo_artifacts: diff --git a/docs/DEMO_QUICKSTART.md b/docs/DEMO_QUICKSTART.md new file mode 100644 index 00000000..e2e4d556 --- /dev/null +++ b/docs/DEMO_QUICKSTART.md @@ -0,0 +1,87 @@ +# 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. + +## Check the running demo + +Run the smoke check: + +```bash +make demo-smoke +``` + +This checks Loom readiness, the seeded Explorer state, and the UI document. + +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, 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. + +## 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/PACKAGE_AUDIT.csv b/docs/PACKAGE_AUDIT.csv index 36707c89..b416f9e8 100644 --- a/docs/PACKAGE_AUDIT.csv +++ b/docs/PACKAGE_AUDIT.csv @@ -7,51 +7,53 @@ cmd/dataframe-query,github.com/calypr/loom/cmd/dataframe-query,main,executable,E 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,—,—,538,245,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/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,2416,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,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,—,—,3716,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,—,—,2926,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/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,—,—,2527,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/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,—,—,808,145,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,—,—,765,290,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/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/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/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,—,—,1882,268,50,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; 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,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,revisions_test.go,—,—,286,83,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/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/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,—,—,2344,373,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/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,—,—,1525,322,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,—,—,—,—,no automatic consolidation signal; perform semantic review,go test ./internal/explorer/authoringv2,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,—,—,638,164,8,Compile; CompileWorkspace; Error; PresentationColumn; PresentationConfig; Result; Unwrap; WorkspaceResult,internal/dataframe/recipe; internal/dataframe/spec; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability,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/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/ingest,conformance/compiler,—,—,compatibility=1,no automatic consolidation signal; perform semantic review,go test ./internal/fhir/schema,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_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,—,—,3653,1960,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/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/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 +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 index ff1e9139..c9088815 100644 --- a/docs/PACKAGE_AUDIT.md +++ b/docs/PACKAGE_AUDIT.md @@ -6,14 +6,14 @@ A row marked as a consolidation candidate is **not** deletion authorization. Bef ## Snapshot -- Packages: 56 -- Production Go files: 335 -- Test Go files: 179 -- Production Go lines: 338,307 -- Test Go lines: 21,488 -- Package kinds: conformance=1, domain/library=28, executable=8, generated=5, storage adapter=8, transport adapter=6 +- 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/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 +- 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 @@ -57,54 +57,56 @@ These records preserve verified package-boundary decisions for both live package | `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: — | 538/245 | 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/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,416 | 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` | 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,716/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,926/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/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,527/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/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: — | 808/145 | 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: — | 765/290 | 34: BundleCatalog; BundleExecution; BundleIdentity; BundleOutputRecord; BundlePointer; BundleState; Canonical; CanonicalizeLegacy; ColumnProvenance; DataframeSelector; Error; ExactExecutionCatalog; … (+22) | internal/dataframe/errors; internal/dataset | 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/explorer/arango; internal/server | storage adapter; preserve interface and persisted-data contracts | +| `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: — | 1,882/268 | 50: Aggregate; AggregateOperation; ArrayQuantifier; Bundle; CanonicalJSON; CatalogProjection; Clone; CodeValue; ColumnNaming; Digest; DocumentRef; DynamicColumn; … (+38) | 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` | 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: revisions_test.go; Other: — | 286/83 | 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/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,344/373 | 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/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,525/322 | 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/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: — | 638/164 | 8: Compile; CompileWorkspace; Error; PresentationColumn; PresentationConfig; Result; Unwrap; WorkspaceResult | internal/dataframe/recipe; internal/dataframe/spec; internal/explorer; internal/explorer/authoringv2; internal/explorer/capability | internal/explorer/lifecycle; internal/server | no automatic consolidation signal; perform semantic review | +| `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/ingest | no automatic consolidation signal; perform semantic review | +| `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_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,653/1,960 | 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/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/dataframe/publication/clickhouse; internal/server | storage adapter; preserve interface and persisted-data contracts | +| `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 diff --git a/docs/README.md b/docs/README.md index e164f3b3..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, diff --git a/generated/loomapi/api.gen.go b/generated/loomapi/api.gen.go index 00b9cf3b..1d8f10de 100644 --- a/generated/loomapi/api.gen.go +++ b/generated/loomapi/api.gen.go @@ -843,6 +843,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 @@ -957,6 +960,30 @@ type ActivateDatasetGenerationParams struct { AuthResourcePath *string `form:"auth_resource_path,omitempty" json:"auth_resource_path,omitempty"` } +// 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"` +} + +// 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"` +} + +// 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"` +} + +// 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"` @@ -1325,7 +1352,7 @@ type ServerInterface interface { 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 @@ -1337,16 +1364,16 @@ type ServerInterface interface { 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 + 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 @@ -1554,8 +1581,24 @@ func (siw *ServerInterfaceWrapper) CreateExplorer(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter project: %w", err).Error()) } + // Parameter object where we will unmarshal all parameters from the context + var params CreateExplorerParams + + 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.CreateExplorer(c, project) + return siw.Handler.CreateExplorer(c, project, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1702,8 +1745,24 @@ func (siw *ServerInterfaceWrapper) ApplyExplorerBuilderCommands(c fiber.Ctx) err return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) } + // Parameter object where we will unmarshal all parameters from the context + var params ApplyExplorerBuilderCommandsParams + + 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.ApplyExplorerBuilderCommands(c, project, explorerId) + return siw.Handler.ApplyExplorerBuilderCommands(c, project, explorerId, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1776,8 +1835,24 @@ func (siw *ServerInterfaceWrapper) PublishExplorer(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) } + // Parameter object where we will unmarshal all parameters from the context + var params PublishExplorerParams + + 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.PublishExplorer(c, project, explorerId) + return siw.Handler.PublishExplorer(c, project, explorerId, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -1813,8 +1888,24 @@ func (siw *ServerInterfaceWrapper) ReconcileExplorerBuilder(c fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter explorerId: %w", err).Error()) } + // Parameter object where we will unmarshal all parameters from the context + var params ReconcileExplorerBuilderParams + + 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.ReconcileExplorerBuilder(c, project, explorerId) + return siw.Handler.ReconcileExplorerBuilder(c, project, explorerId, params) } for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { @@ -2468,6 +2559,7 @@ func (response ListExplorers500JSONResponse) VisitListExplorersResponse(ctx fibe type CreateExplorerRequestObject struct { Project Project `json:"project"` + Params CreateExplorerParams Body *CreateExplorerJSONRequestBody } @@ -2732,6 +2824,7 @@ func (response GetExplorerAuthoringCapability500JSONResponse) VisitGetExplorerAu type ApplyExplorerBuilderCommandsRequestObject struct { Project Project `json:"project"` ExplorerId ExplorerId `json:"explorerId"` + Params ApplyExplorerBuilderCommandsParams Body *ApplyExplorerBuilderCommandsJSONRequestBody } @@ -2969,6 +3062,7 @@ func (response PreviewExplorer504JSONResponse) VisitPreviewExplorerResponse(ctx type PublishExplorerRequestObject struct { Project Project `json:"project"` ExplorerId ExplorerId `json:"explorerId"` + Params PublishExplorerParams Body *PublishExplorerJSONRequestBody } @@ -3070,6 +3164,7 @@ func (response PublishExplorer503JSONResponse) VisitPublishExplorerResponse(ctx type ReconcileExplorerBuilderRequestObject struct { Project Project `json:"project"` ExplorerId ExplorerId `json:"explorerId"` + Params ReconcileExplorerBuilderParams Body *ReconcileExplorerBuilderJSONRequestBody } @@ -3802,10 +3897,11 @@ func (sh *strictHandler) ListExplorers(ctx fiber.Ctx, project Project) error { } // CreateExplorer operation middleware -func (sh *strictHandler) CreateExplorer(ctx fiber.Ctx, project Project) error { +func (sh *strictHandler) CreateExplorer(ctx fiber.Ctx, project Project, params CreateExplorerParams) error { var request CreateExplorerRequestObject request.Project = project + request.Params = params var body CreateExplorerJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { @@ -3919,11 +4015,12 @@ func (sh *strictHandler) GetExplorerAuthoringCapability(ctx fiber.Ctx, project P } // ApplyExplorerBuilderCommands operation middleware -func (sh *strictHandler) ApplyExplorerBuilderCommands(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { +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 ApplyExplorerBuilderCommandsJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { @@ -3987,11 +4084,12 @@ func (sh *strictHandler) PreviewExplorer(ctx fiber.Ctx, project Project, explore } // PublishExplorer operation middleware -func (sh *strictHandler) PublishExplorer(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { +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 var body PublishExplorerJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { @@ -4021,11 +4119,12 @@ func (sh *strictHandler) PublishExplorer(ctx fiber.Ctx, project Project, explore } // ReconcileExplorerBuilder operation middleware -func (sh *strictHandler) ReconcileExplorerBuilder(ctx fiber.Ctx, project Project, explorerId ExplorerId) error { +func (sh *strictHandler) ReconcileExplorerBuilder(ctx fiber.Ctx, project Project, explorerId ExplorerId, params ReconcileExplorerBuilderParams) error { var request ReconcileExplorerBuilderRequestObject request.Project = project request.ExplorerId = explorerId + request.Params = params var body ReconcileExplorerBuilderJSONRequestBody if err := ctx.Bind().Body(&body); err != nil { @@ -4315,102 +4414,103 @@ 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{ - "7D1rc9s4kn8FxduqqbqiLWcme1Xr++TYTuIrx/HKzsztTeVcENmSsCEBDgDaVlL+71d4kSDFpyzJmb18", - "Sijj0d3obnQ3Go1vQcTSjFGgUgTH34IMc5yCBK6/zh+zhHHgF7H6IjQ4DjIsl0EYUJxCcBxA2SAMOPyR", - "Ew5xcCx5DmEgoiWkWPVMCb0EupDL4PhVGMhVpvoKyQldBE9PYfAOKHAsCaMt8yzKBs+Z55qzf0IkWybJ", - "7F83n+FJdRUZowI0/U5yuWTqb29wPIU/chB67ohRCVT/F2dZQiKN2eSfwuBfzvYXDvPgOPi3SblIE/NX", - "MTnnnPGpnc3MHYOIOMkMHYPbJaD/uvl4hWIW5SlQiYhAKU7mjKcQh0hBgQkVKKdfKHugaE4giUWIGEcs", - "JVIgRwaEHR6I0CyXh8FTWOJ2mhCg8jRhAl4GyUgDgCJMI0ggRhmHewIPCB4hylU7NIM544DUBAmoX2oY", - "MDpPSLRHsC/SNJd4lgCKcIZnJCFypejOIQKSSSQklqDWS0icgFmSTJKUCEkiFHM8l5MsnznwbPtoiekC", - "4ipybxmfkTgGut9FUSwDVKoZ9JIQGpEMJ2qVKJOIA44VVimLyXyF5JII5LQNsoJYxeMdlvCAV7ckBZbv", - "camu17gJHiOAGGKkZEQAvwd+IEgMKAYcJ4RCFfALKoFTnOiZ9gf3CUU5hccMIulLcIgy4IIICTQyjOXE", - "ZY5JknNALIpyzutsdMXkW5bTeL9c5FgiVHwTkxhLCLUYk8QwvhUYIyFyCUpO7E5xIDKIyJxEnvriIFjO", - "I0AxA4EUI8IjETVOu8arhOH4lrFLzBewX4xnisieEuMg8kRanhNILgGl+JGkeYrcVoME+VrjuVvGPmC6", - "supY7BcFbmb1kMgSTCsoGKn5SZRSdRAxIVHGEhKtqrh8ovgek0Rpyz1KT7n5lRo69PQz4xCiFEvgBCfk", - "q2JRHElyb/Ux8Hti5WuGoy9AY6XN8xKVOo4ZZxEIsV8svdVS4N3jhMTGZpjlUqu3B8a/iAxHRuxSTOMQ", - "sVxmuRE5XxSJhlhvWpBirfqTZIUI1cNqfN9xnC3/frkDa2iKHxTcbUjaidH729vrAmOg95CwDBQikmMq", - "MsYlKi1ghDmUBpOPwG5UegcODv5yE1LaGmJn2WBEGU9xUiBa6IaI5YlqpjbVOI+qeOyG7QauRWGWGhCV", - "MlZgYi5AmwaabbT1MGe8xFzD//72w2UNXgmPcrKUaVIFtMFEr4J0rjYAtTfMOHsQwNGniwI0PdclLHC0", - "2gHPmoEHCWmim5YWkie0BYNqc4oIYfY5q7xKe93MtnVTdwQSBfQ4Uebfyuy9wti9GRNEMr5CMWQJW2nG", - "qFi1qhkRiNCIUWO/+Hht38odgViPrYsjJWIdNq6ZajdKZSAaU8CxYhxlDxr7UH1Jf9WsximNymTlQb91", - "+3ATxqpw0gF7oBCXf4wYnZNFboxD9ICN/TdXQHt47MLaGLwGhRB4+6pvZdgfNJoJYAG+zUEMQg5NIzF1", - "i8MhuQu9v8mKFfaFAtYTfWVFrKhU6BkzwtgmyiyxUjUDSyWIK4ZYbJwa5RyLpd3ubow5tgMdbkceY09m", - "WC5D9EcOfBWiNE8kyTCXoba7NOxXZ9oEu8dJrmMATqkrBegZU3bqrWv0oSh5MQwzbdUClsZXy7lg3H25", - "halGLSwCAj0QudQ6x25vPprbV/BD8ezS8AmOvohCwTMfeN1O63gjr9bxFBHLwEdsN2p/MF9WogTGLUOg", - "ehVxgP9EMUhMEmMPc4ixiSjQGEWMc0g0TWYrh/hFhT23vi+MWbZyKWIssQAZojKSHDolqt06kkFpYxoV", - "y3IJ5cKtbxgWkk/Uxhe+wv6xPCkZU0lUxCFWn9iuFp4JpU2bNccutroN9KGjnl0U5VrjBYSI0AUI85un", - "NPTSaOXZ5VgXGO5in9uAA7v2M+VmK/as8qa3x1tKqX1ReLufRQ1qayryTPmxEH+AmOBb7f7sGfFTM9mB", - "mtyZJQVYSlNoUzjPEobjMl53qI9o7Fz69OT64ldlixoQgeZpcPx7kDCWHkY4WWX8kPHFxB1BHRRRvsn9", - "z8HntcOZMDiJ3AkTjmOi/o+Ta67mlwREcDzHiYAwyLyfFOGSPKX6v0RCKvpoZCY51b3UpBYKzDleqe85", - "SeBKnzd9W4dQEmn4tPOYyf3Qf95VnmP9bv7qpijJw2b6zKsgj4V8EyINABweFRO8BxwDb/LOqyDbYRuB", - "zbJkdWoiUsIz6kYBrXubs80euG3bEWxQni3pnmqQlNAL0/fVOl+4PfiM47k8IwuLTxMFy3aeeMwZT7EM", - "jgNC5X+8DvRsJFUic1RMRqiEBXA1jKA4E0smb9kXoGNZqaRcfaAW+DwCDlhMq1h2t5oxwQvKhCTR8AU9", - "K/o0SXVcXbW++Qev3aumtTNHAsNBt5Sd6m5N0BdOWN9IvxUNu1iiHK6GapVOJSLVFWnkkEKcimD8SP7A", - "GfEo3im65baj9DVgmXMYLvBvXYenMPhCjOHr9i7n9q6jo6Bs2rOK3bG63G7AWU4SpUrDQOQLay4petqD", - "F/U/4wj7IqgIHzEakcpGUN9dSvawg12SlNS4zp5EBcevjo6O+ti2OmyNf7zlsWSr4F4HwluXbnax2nek", - "LnFnjQP3hoGbn2n4q3Lr+0XW2Q8QL4aBQShRdv81B2XyF8k0jlNuT95cngdh8Pbi8vZ8GoTB6fuT6W0z", - "zxnHj0bDJjbHQaOa1rhoiLHjsSPmQOXHcUBaV5ww+oHFveS/rrZWvMqYvGLxsMmMx/hxOF3G2n1uUU+n", - "5ye353dubc8+XV9enFZ+Ob889z6n51cnH/zPj9Oz86n5vgnC4Ob81nzcTT9+vA3C4OTs7G768dOtaf3h", - "46/nxaf60+nHy08froIw+HR9pqYtvm1b+/15kG3aKchvPTU8QpJjSECCU7ueQTVjLAFsJEzH1xg9XWJu", - "9Ft7m7ckcblx642UaW+M6NYGjxB3DiGWmHc2qRGuDtcaNvUha0BUgQ7r9Gpakjdmy7mRWMKe9uAIS5yw", - "Ra/StM3W7bFnWGCN1nPbvl6hTZNqTcgcolWUQEE+N8TV+W9abk7O/tHYs2Kj4ST5OA+Ofx9srX0OA5on", - "NtQjeQ5D9t8asD3GnG/1uQVr4p9Tt7neAObRcjPfjQ5VxTpS1MgCz/KA6l6PhWcQvhu5N5uLjp19hLtg", - "Fq+Au9Hf2boH1SNSJRU9O7dJTgZzxrPWv0lcWniisgb9ns5pqevGsIcfQ72JWAaDndEdcYjNtYWW/TRe", - "jJ/vPF40TrWoZI/3oKtWZfTMV9YKrM/MQbDkHuIb3bxjv9FnCdc6ua03e8Zr+lw2raTNt/LIOusWi+fo", - "5Vasxsw+Wh3MXDLKTv0wZfG4QP86y8Uwx3kir5/pCOgk+WusgOiFaK6trHaQEjyDZMA4CVuQCCe3Q6K+", - "IzRg1ScaLhTrRFmXiwywNMdhPRasv8aezizJ7MhUJYM3R4XOPhusY9jGBR28q5XOOLYd7LLPOUsHO5VD", - "mSVjWZ60ET8MJBs4Y93TMEhVgPZGK1epnL+DqldW9sa4cjYn75Tl5hxrpM0+XCy6CegOhAdKI2cP7zgm", - "tGVbeJgyJs8TsiAtWqK2DIWAVMBYH6l3HZSU1GNFI1aDcXuC0030IrCxvSOsRmw2OrRSJOg1ANboVIn4", - "ZVhK4DQ4Dv7395OD/8EHX+8+2/8cHfzt7vO//yVo3Rf6pjY+en3uDfeM5wb5DLMNC1vemLZqkd3e19Xp", - "VjWqotl8DljqmArwBXDtnHFTQD+CPyo7/RpB6v6Kbh2EAdH5F3MC/M3qZiUkpPpQTAJV3sKb1SeeaAMr", - "JnThtWAzAdykTp06Er1ZnSo1WWxjFQezhCTFMmqGcT3o6aD99eTyk4kGT290nO/yMgiDs4ub24ur09v+", - "gJ3Gvpne/lnTpqfIvdzaGYRe1zV41vaXWkDVBD9NWPUsCN33+5Ord/pbBz/vTs7O9JcJb9rPz8/QXjqN", - "cc+RAXd0NPzIz/gFJAHuTbj3o9ZR3p65kzLYF+4JQkzN7Z/mQxPNgMORtGOZQ4JmG1o3eJlYRjn5unNY", - "cSorBF5nkNA7oXQUGhADYVRyHMnNt/V2n2vwad1g5+1Z7lrfTlf1eVocnSYaekI0VgubraJm2mizpsOm", - "sTmi7ZOZm/RrcP5TMNq6yaYgBF4MMrNd7mlzrBfugducgT5RkkMmrIeD3fiuf2jIWGLQuETWoxmr8KP1", - "fID+jLTmGN243LaurLbqydag4d6WnTaICBfUa9fGfQCUypczJqdjHbtcwqBgnokeNllQBaANELgZymVq", - "4iGdi/mGxas+wWsW8sG797g9eztyu6YbeyWqmpk6Mr6wdUsFXEZ/7x1dvYBrERfdvRFNKwkfQOIYSzz6", - "UNxL1t04C7Vu3bbmlb6tHotvpumae7Va9N70he/VOU6x3L3pJ7UJ6otWzhYW8LdQpe7djyNOu/0xKDbz", - "1AhTqZB3lACsLzX1ULwjT7bNXLLDNhHau5W2uZ7sskyerbWa7s31QFm7wqzv6xTX2bkyxajJtK9fHMY0", - "trddDsq75HqjEYfBWiDbKbDadEQX2sBucIsbMhRATP1JSJ5HUt9CVBSYuDYG6xDFkAE1N06puTWlYFAg", - "MAo2t6FO9MG3D406/TxcoX4szIURPE/iYeHfy1ZB3UjRkrgzi9+Wq9ksrSIhKZGjEztHJAAOd2prWPsO", - "aTFfJ/77TbMYbUlXPNzvIcPCEq7DnN7yCmvhKE5zus8ROHuokmGUjzk687iR28pVtvB4CPTHNK5bY7E3", - "pyeXJ1M/GDudnvzDC8feuR9c1LY3WhsG1+WNuY2UW+3O+bYTgYVLfxujA7yVEDYjbR3MRtqbHPjNdOIW", - "VFYnTHvVU/vXKiUfbiVsus7XzwydKq0nhnL4Rmzbp2A8CErGHh4sddV2RlmMusDAw5IJMEVYlCEmlthc", - "18yUU8LvIUYJEyIBIZIVinNTQE1fio5yye6BHwZN8FQC27u9bNm/jY7ZubpO6jcy1TyFZfp7k3SHdabu", - "gs5mOmuv19G2mkjbkWLcSKcivjb2kIAkMTfgDjuuKQN5DUw27ixdl0wgjIolyZrTQsZEIus8Vz0arwzV", - "SsEyM3HMHpQk7GFqs7DOXULn+kGJbncDyfySsaylTYofbyRkYkDUoqJa10FYm68Jaf8ie3dQoPOIZNND", - "kGoqfgN87dGFvpMODpKv8LBcogGhicYb/33Eqm41N6ZGDFRjFJASaYsBXDKW/iSQuadiKvSZxinOMhuy", - "cGqiP1QxtH7ByJDrjXeL5g3RkYtdhcgGb1ntu03HxflbPNsozrFZptPANDG2/atyyqYaJgbE3589IhrY", - "y5FaqFnLXRrJFJAkU6Ax2NtpZTZRGTP/vDldM0JpWx5jN4HWMP2tcv9oD56KPd4b4ae4A8GnxuoKxQ3A", - "ztOY+snK+rW+npS9omm/b1SStGmJ164C9h5ddKq+Bv3Vf7ihU6eGT6J0yybhlmJ1/GW3c69JXRg8HizY", - "gZvEXRC9//nwN+8GWtHmgKQZM3metsy/10VtIzr3IFgQucxnhxFLJ6a2zCRhLJ0QW5yrqDIz8Xs/6do3", - "hM6ZliKjmQK1n5lt7OT6QukPx/7Bq8OjwyN7q5/ijATHwS+HR4e/WDA0fSc4I5P7VxPFfXOOU5jUw/Vi", - "8o3ET6rtAjRexT15pUCDdyCnuse56xDUXiX4+ehoH7Veb3JdFm2eJ0U1Wl2p6PXRq31MX6uMVdbRC10t", - "LFPBuKin5C7z6HKzcumVA/tJuPphXvm210e/tEFXUHtSL06q+70e2q+onaYLI+VpivnK1uq0JbFQahWV", - "BhqjgmvWSpod2qvzxesevze+gUG6H/KoWyCfn8IKxwqQYvLN5p0+TcpUMTH5Vn481V8aabnIWjZxd05a", - "T4G8pt5jIk+fwyBjokFKTrnyGc4MyO/qr4uAkM4t8Ni0qNU4UR7ygTt5L6mzfifvzvlfd1lbjlMMc+B3", - "ZeXE9rvlFT1cOOkzQhVbDCicUM2cbMsW7j/S1LCE3nMp3sDrRlK1t757/L3po6N+gVyvIFpqskE9K1UC", - "h6qPtdqXuuPfBncsaoOqfq/+OgLYhipyaoyffx4xhl977ykM/jqCztWimLrzL2NIXdQ1rGrOT6bkHC7L", - "rhaKveRhU4duA602sUIMe1NvodXh5pZ7ocSLXeCuUP93IxV7y8gNOq1ze2hTvyeWUs0K+Idu2LNu+A5E", - "03EEwp4oIjyXwD2rxisEWhVTK5y+mDqTXbSaypdEyPOi1TM5bytH02ON6N2xV4PlOohLmmrZV1daUb1M", - "SDK/ExDWgvXr4m+oQ3uMvvPS22y39bakZfoMn1ffvXJbe/3ihZhvkGqrvXEx1GJpKoq/PX43XIeweSGH", - "ayOhrIA/XI1NvpWvLXa6/xUG/+78/j1zzQbO9tZWXjvqmBZr/ZNAdvV5TiVJAZU3NJ+j8PqNRu8hz4rP", - "PpjfJn515Yl3d7CPCd8U9792xouVqltN5aj1gaj03prA1DzeR7xnGHVdguKgaT8c2/BI41CmXX+Yb6iS", - "XH/2cijHtzxpONQGbHzRTa/Xn4Xvo0r12T7Wbypau0MxaJquUT27Quzls4hlpVUtGp8ukCuv+pJy8Cye", - "/FOxlVddfD8wt/rlWZas6qXX/LrB27eXGyu57zlq2FyAvGkrwZRREuHEPT5hXx2A2N9dtNOKKcKSpSRy", - "DyeiGZbRcrjl3fR49EtuSptuLYNM8JaXMF94Z3q+aLtC3C8u2Ta9f8fOb+32yZ7FuH73o0GA39Se12Vz", - "xCgU77qa9I8/r4zu33B8/WoMuPWnlZ+vHl7/PAby+rvIaoC/jUK94bH7F1VSqvOYVa89o74dJWffGHh5", - "JWcA2bWSq14n2beSq10caUp9sLH02H8w1lwy+KHafhgug2S6fCLkxaW6uAzRFFPaQQC/fvdizwJeLwfW", - "+fSmM1zcU6hozllqs5dwJN1jvhAjfZXih/j/EP9B4u8/J/TiCsBUzndNTysVqHegAFpeKNi3Gmh5N6A5", - "KmGaohjm+h0gRgXSdSEJXSCMKItN0Func/z/C0F8v7LYmktUPGtp3vN++YzJwu50j5QX8mgAbM4ufV7+", - "kMtCWpoHI4th/vvgkrH0wJR1PThlaUrkuLzV3WgO/328f4m0xx+n/xucAQ9UGG7etowoawdqvW09bIQR", - "L+TvgD1Q38UrAtBFSgFLEtZ1UHWiW9xgGs/YY8vRVDcO728/XD6fJ14P7tuaEU9jMEn7Bif0juNs+fdL", - "JAxyliQL9esfSXm5QqvVRnVnbk/oBEXd0g74PSQNff9qwxJri3pjiCDbWTeTZNu5K53DskTlmoXjs9or", - "1gWn6X+7ZNAOcJ3g1YJr1v7XkENHmKxAzGS4dAnbDxn7IWOFjGnJQYyjt+8vpq1ytgScyE4Be29afF8r", - "bA2End+BQ47PkSGUrbsHNFq5h/k54Hh1uCbIGeMSRTha6gp+3Jghpd4zw9lFSMg9fO1ag0tyDxSE+K5W", - "oQlhy9UosQCjByKXLJcoWkL0RTnTBQGJTo5R6GsKfu2+kIlj8t0RYG9seObRDGEOvXz3sARdYrLg3rg+", - "QtFbV/fm987pzHkSHAeT4Onz0/8FAAD//w==", + "7D1rb9w4kn+F0C0wwEF2OzPZA9b3ybGdxAfH9radmdsb5Axaqu7mRiI1JGW7E/i/H/iSKLWe7e62Zy+f", + "ErX5qCpWFauKxeL3IGJpxihQKYLD70GGOU5BAtdfR7lcTEGwnEdwheVC/RaDiDjJJGE0OAwu9X9wgo5x", + "ssw44rY1EhHLAOUCYiQZwrlcME6+AYpzju8SQKePWcI4cJTmEqsxxH4QBkSN+UcOfBmEAcUpBIeB6nvr", + "xr3NFBhhIKIFpFjBI5eZaiUkJ3QePD2FgRv6LFZ/10PaXnZEKBuEAYc/csIhDg4lz8EfOSX0HOhcof0m", + "bJjnA1Dg2BCicZ552eA581xx9k+IZMskmf3r+jM8qa4iY1RAsepM/e0djqfwRw5Czx0xKoHq/+IsS0ik", + "MZv8Uxj8y9n+wmEWHAb/NilZa2L+KiannDM+tbOZuasMdbMA9F/XlxcoZlGeApWICJTiZMZ4CnGIFBSY", + "UIFy+pWyB4pmBJJYhIhxxFIiBXJkcExH54jQLJf7wVNY4nacEKDyOGECXgbJSAOAIkwjSCBGGYd7Ag8I", + "HiHKVTt0BzPGAakJElC/1DBgdJaQaIdgn6VKWJX0RjjDdyQhcqnoziECkkkkJJag1ktInIBZkkySlAhJ", + "IhRzPJOTLL9z4Nn20QLTOcRV5N4zfkfiGOhuF0WxDFCpZtBLQmhEMpyoVaJMIg44VlilLCazJZILIkpF", + "ZgWxiscHLOEBL29ICizf4VJdrXATPEYAMcRIyYgAfg98T5AYUAw4TgiFKuBnVAKnONEz7Q7uI4pyCo8Z", + "RNKX4BBlwAUREmhkGMuJywyTJOeAWBTlnNfZ6ILJ9yyn8W65yLFEqPgmJjGWEGoxJolhfCswRkLkApSc", + "2J1iT2QQkRmJPPVVbKoxA4EUI8IjETVOu8LLhOH4hrFzzOewW4zvFJE9JcZB5Im0PCeQXABK8SNJ8xS5", + "rQYJ8q3GczeMfcJ0adWx2C0K3MzqIZElmFZQMFLzkyilai9iQqKMJSRaVnH5TPE9JonSljuUnnLzKzV0", + "6OlnxiFEKZbACU7IN8WiOJLk3upj4PfEytcdjr4CjZU2z0tU6jhmnEUgxG6x9FZLgXePExIbm+Eul1q9", + "PTD+VWQ4MmKXYhqHiOUyy43I+aJINMR604IUa9WfJEtEqB5W4/uB42zx9/MtWENT/KDgbkPSTow+3txc", + "FRgDvYdE2daMI8kxFRnjEpV2O8IcSoPJR2A7Kr0DBwd/uQkpbQ2xs2wwooynOCkQLXRDxPJENVObapxH", + "VTy2w3YD16IwSw2IShkrMDFXng7jhhu19TBjvMRcw//x5tN5DV4Jj3KykGlSBbTBRK+CdKo2ALU33HH2", + "IICjz2cFaHquc5jjaLkFnjUDDxLSRDctLSRPaAsG1eYUEcLsc1Z5lfa6mW3jpu4IJArocaLMv6XZe4Wx", + "ezMmiGR8iWLIErbUjFGxalUzIhChEaPGfvHx2ryVOwKxHlsXR0rEOmxcM9V2lMpANKaAY8U4yh409qH6", + "kv6qWY1TGpXJ0oN+4/bhOoxV4aQ99kAhLv8YMToj89wYh+gBG/tvpoD28NiGtTF4DQoh8PZV38qwP2g0", + "E8ACfJuDGIQcmkZi6haHQ3Iben+dFSvsCwWsJ/rKilhSqdAzZoSxTZRZYqXqDiyVIK4YYrFxapRzLBZ2", + "u7s25tgWdLgdeYw9mWG5CJEOzYUozRNJMsxlqO0uDfvFiTbB7nGS6xiAU+pKAXrGlJ164xp9KEpeDMNM", + "W7WApfHVci4Yd19uYapRC4uAQA9ELrTOsdubj+bmFfxQPLs0fIKjr6JQ8MwHXrfTOt7Iqx/N9RHbjtof", + "zJeVKIFxyxCoXkUc4D9RDBKTxNjDHGJsIgo0RhHjHBJNk7ulQ/yswp4b3xfGLFu5FDGWWIAMURlJDp0S", + "1W4dyaC0MY2KZbmEcuFWNwwLyWdaxOR3j+VRyZhKoiIOsfrEdrXwnVDatFlzbGOrW0MfOurZRVGuNZ5D", + "iAidgzC/eUpDL41Wnl2OdYHhNva5NTiwaz9TbrZizypvenu8pZTaF4W3+1nUoLamIs+UHwvxJ4gJvtHu", + "z44RPzaT7anJnVlSgKU0hTaF8yxhOC7jdfv6iMbOpU9Prs5+VbaoARFongaHvwcJY+l+pM/I9hmfT9wR", + "1F4R5Zvc/xx8WTmcCYOjyJ0w4Tgm5rTtiqv5JQERHM5wIiAMMu8nRbgkT6n+L5GQij4amUmOdS81qYUC", + "c46X6ntGErjQ503fVyGURBo+7Txmcj/0n3eV51i/m7+6KUrysDt95lWQx0K+DpEGAA6Pigk+Ao6BNx85", + "+iDbYRuBzbJkeWwiUsIz6kYBrXubs80euG3bEWxQni3pnmqQlNAz0/fNKl+4PfiE45k8IXOLTxMFy3ae", + "eMwYT7EMDgNC5X+8DfRsJFUic1BMRqiEOXA1jKA4Ewsmb9hXoGNZqaRcfaAW+DwCDlhMq1i2t5oxwXPK", + "hCTR8AU9Kfo0SXVcXbW++Qev3ZumtTNHAsNBt5Sd6m5N0BdOWN9IvxUNu1iiHK6GapVOJSLVFWnkkEKc", + "imD8SP7AGfEo3im65baj9DVgmXMYLvDvXYenMPhKjOHr9i7n9q6io6Bs2rOK3bG63G7Au5wkSpWGgcjn", + "1lxS9LQHL+p/xhH2RVARPmI0IpWNoL67lOxhBzsnKalxnT2JCg7fHBwc9LFtddga/3jLY8lWwb0OhLcu", + "3exite9IXeLOGgfuDQM3P9PwV+XW94ussx8gng8Dg1Ci7P4rDsrkL5JpHKfcHL07Pw3C4P3Z+c3pNAiD", + "449H05tmnjOOH42GTWyOg0Y1rXHREGPHY0fMgcrLcUBaV5ww+onFveS/qrZWvMqYvGDxsMmMx3g5nC5j", + "7T63qMfT06Ob01u3tiefr87Pjiu/nJ6fep/T04ujT/7n5fTkdGq+r4MwuD69MR+308vLmyAMjk5ObqeX", + "n29M60+Xv54Wn+pPx5fnnz9dBGHw+epETVt827b2+8sg27RTkN97aniEJMeQgASndj2D6o6xBLCRMB1f", + "Y/R4gbnRb+1t3pPEZfStNlKmvTGiWxs8Qtw5hFhg3tmkRrg6XCvY1IesAVEFOqzTq2lJ3pkt51piCTva", + "gyMsccLmvUrTNlu1x55hgTVaz237eoU2Tao1ITOIllECBfncEBenv2m5OTr5R2PPio2Gk+RyFhz+Ptha", + "+xIGNE9sqEfyHIbsvzVge4w53+pzC9bEP8duc70GzKPFer4bHaqKTQZsEws8ywOqez0WnkH4ruXerC86", + "dvYR7oJZvALuRn9n4x5Uj0iVVPTs3CY5GcwZz1r/JnFp4YnKGvR7OselrhvDHn4M9TpiGQx2RrfEITbX", + "Flr203g+fr7TeN441bySPd6DrlqV0TNfWCuwPjMHwZJ7iK918479Rp8lXOnktt7sGa/pc9m0kjbfyiOr", + "rFssnqOXW7EaM/todTBzyShb9cOUxeMC/assF8MM54m8eqYjoJPk3Q2OHohm2spqBynBd5AMGCdhcxLh", + "5GZI1HeEBqz6RMOFYpUoq3KRAZbmOKzHgvXX2NOZJZkdmapk8Oao0Nlng1UM27igg3e10hnHtoNd9hln", + "6WCnciizZCzLkzbih4FkA2esexoGqQrQ3mjlKpXzd1D1wsreGFfO5uQds9ycY4202YeLRTcB3YHwQGnk", + "7OEDx4S2bAsPU8bkaULmpEVL1JahEJAKGKsj9a6DkpJ6rGjEajBuT3C6iV4ENjZ3hNWIzVqHVooEvQbA", + "Cp0qEb8MSwmcBofB//5+tPc/eO/b7Rf7n4O9v91++fe/BK37Qt/Uxkevz73mnvHcIJ9htmFhy2vTVi2y", + "2/u6Ot2oRlU0m88BSx1TAb4Arp0zrgvoR/BHZadfIUjdX9GtgzAgOv9iRoC/W14vhYRUH4pJoMpbeLf8", + "zBNtYMWEzr0W7E4AN6lTx45E75bHSk0W21jFwSwhSbGMmmFcDXo6aH89Ov9sosHTax3nOz8PwuDk7Prm", + "7OL4pj9gp7Fvprd/1rTuKXIvt3YGoVd1Db5r+0stoGqCnyasehKE7vvj0cUH/a2Dn7dHJyf6y4Q37eeX", + "Z2gvnca448iAOzoafuRn/AKSAPcm3PlR6yhvz9xJGewL9wQhpub2T/OhiWbA4UjascwhQbMNrRu8TCyj", + "nHzVOaw4lRUCrzJI6J1QOgoNiIEwKjmO5PrbervPNfi0brDz9ix3rW+nq/o8LY5OEw09IRqrhc1WUTNt", + "tFnTYdPYHNH2ycxN+hU4/ykYbd1kUxACzweZ2S73tDnWC/fAbc5AnyjJIRPWw8FufNc/NGQsMWhcIuvR", + "jFX40Wo+QH9GWnOMblxuW1dWW/Vka9Bw78tOa0SEC+q1a+M+AErlyxmT07GOXS5hUDDPRA+bLKgC0AYI", + "3AzlMjXxkM7FfMfiZZ/gNQv54N173J69Gbld0Y29ElXNTB0ZX9i4pQIuo7/3jq5ewJWIi+7eiKaVhE8g", + "cYwlHn0o7iXrrp2FWrduW/NK31ePxdfTdM29Wi16b/rC9+ocp1ju3vST2gT1RStnCwv4W6hS9+7HEafd", + "/hgUm3lqhKlUyFtKANaXmnoo3pEn22Yu2WGbCO3dSltfT3ZZJs/WWk335nqgrF1h1vd1iuvsXJli1GTa", + "1y8OYxrb2y575V1yvdHoqlG1QLZTYLXpiC60gd3gFjdkKICY+pOQPI+kvoWoKDBxbQzWIYohA2punFJz", + "a0rBoEBgFGxuQ53og28fGnX6ZbhCvSzMhRE8T+Jh4d/zVkFdS9GSuDOL35arWS+tIiEpkaMTO0ckAA53", + "amtY+w5pMV8n/rtNsxhtSVc83NeQYWEJ12FOb3iFtXAUpznd5wicPVTJMMrHHJ153Mht5SpbeDwE+mMa", + "V62x2Ovjo/OjqR+MnU6P/uGFY2/dDy5q2xutDYOr8sbcWsqtdud804nAwqW/jdEB3koIm5G2CmYj7U0O", + "/Ho6cQMqqxOmneqp3WuVkg83EjZd5etnhk6V1hNDOXwttu1TMB4EJWMPD5a6ajujLEZdYOBhwQSYIizK", + "EBMLbK5rZsop4fcQo4QJkYAQyRLFuSmgpi9FR7lk98D3gyZ4KoHt7V627N9Gx+xcXSf1a5lqnsIy/b1J", + "usM6U3dBZz2dtdPraBtNpO1IMW6kUxFfG3tIQJKYG3CHHdeUgbwGJht3lq5LJhBGxYJkzWkhYyKRdZ6r", + "Ho1XhmqlYJmZOGYPShL2MLVZWKcuoXP1oES3u4Zkds5Y1tImxY/XEjIxIGpRUa2rIKzM14S0f5G9OyjQ", + "eUSy7iFINRW/Ab726ELfSQcHyZd4WC7RgNBE443/PmJVt5prUyMGqjEKSIm0xQDOGUt/EsjcUzEV+kzj", + "FGeZDVk4NdEfqhhav2BkyPXau0XzjujIxbZCZIO3rPbdpuPi/A2+WyvOsV6m08A0Mbb5q3LKphomBsTf", + "nz0iGtjLkVqoWctdGskUkCRToDHY22llNlEZM/+yPl0zQmlbHmM3gVYw/a1y/2gHnoo93hvhp7gDwafG", + "6grFDcDO05j6ycrqtb6elL2iab9vVJK0aYlXrgL2Hl10qr4G/dV/uKFTp4ZPonTLOuGWYnX8Zbdzr0hd", + "GDzuzdmem8RdEL3/ef837wZa0WaPpBkzeZ7eKwi2i9pGdO5BMCdykd/tRyydmNoyk4SxdEJsca6iyszE", + "7/2ka98QOmNaioxmCtR+Zraxo6szpT8c+wdv9g/2D+ytfoozEhwGv+wf7P9iwdD0neCMTO7fTBT3zThO", + "YVIP14vJdxI/qbZz0HgV9+SVAg0+gJzqHqeuQ1B7leDng4Nd1Hq9znVZtFmeFNVodaWitwdvdjF9rTJW", + "WUcvdLWwTAXjop6Su8yjy83KhVcO7Cfh6od55dveHvzSBl1B7Um9OKnu93Zov6J2mi6MlKcp5ktbq9OW", + "xEKpVVQaaIwKrlkpabZvr84Xb5L83vgGBul+yKNugXx5CiscK0CKyXebd/o0KVPFxOR7+fFUfx+l5SJr", + "2cTdOWk9BfKaeo+JPH0Jg4yJBik55spnODEgf6i/LgJCOrfAY9OiVuNEech77uS9pM7qnbzaayvNPsMM", + "+G1ZObH9bnlFDxdO+h2hWD/y0ntyXc2cbMsW7j/S1LCE3nMp3sCrRlK1t757/Nr00UG/QK5WEC012aCe", + "lSqBQ9XHSu1L3fFvgzsWtUFVvzd/HQFsQxU5NcbPP48Yw6+99xQGfx1B52pRTN35lzGkLuoaVjXnZ1Ny", + "DpdlVwvFXvKwqUO3hlabWCGGnam38HvjO0/FLnBbqP/bkYq9ZeSRL0i1qt8jS6lmBfxDN+xYN7wC0XQc", + "gbAnigjPJHDPqvEKgVbF1AqnL6bOZBetpvI5EfK0aPVMztvI0fRYI3p77NVguQ7ikqZa9tWVVlQvE5LM", + "7wSEtWD9uvhr6tAeo++09DbHTbDyhp+aqd1c3JCi6rOd3rx6/bjygMYL8e8g7Vh7JmOo0dNUV39zImMY", + "F2HzyA7XdkZZRH+4Jpx8Lx9s7IwgeDLyCkMHO+aaNfz1ja289vUxLdb6J4Hs6vOcSpICKi95Pkdn9tud", + "3lugFbd/ML9N/ALNE+/6YR8TviuukG2NFyuFu5oqWuszVek9V4Gpef+PeC856tIGxVnVbji24Z3HoUy7", + "+rbfUCW5+nLmUI5veRVxqBnZ+CicXq8/C99HlQK2fazfVPd2i2LQNF2jena13MuXFctirVo0Pp8hV6H1", + "JeXgWTz5p2Irr0D5bmBude2zLFnWq7eVpYdfqcndWE9+x7HL5jLoTbsRpoySCCfuCQz79gHE/galXWdM", + "EZYsJZF7vhHdYRkthhvvTU9Yv+S+tu7uNMiKb3mP84U3t+drB1cO/MWVg71kUHEvNi/MtTswOxbj+g2U", + "BgF+V3vkl80Qo1C8LmuSUP68Mrp72/PtmzHg1h94fr56ePvzGMjrrzOrAf42CvWGJ/dfVEmpzmNWvfaY", + "+2aUnH3p4OWVnAHk9ccZa/didq0nazdgmnI47KFA7L98a25L/NCOP2yfQWqhfOvkxRVDcatjNbL1Wk8i", + "6vdQdqwj6qXROp8hdeaTexYWzThLbSYXjqR72BhipK+V/NAgPzTIIA3iP6304jrEvCLgmh5XqnFvQQG0", + "vNawazXQ8oZCc2zENEUxzPSbSIwKpGtkEjpHGFEWm+i9Tm35/xcIeb2y2JpXVTzxad42f/ns0cJ0dQ+2", + "F/JoAGzOtH1eLpXLyFqYxzOLYf5775yxdM+UuN07ZmlK5Lgc3u1oDv+twH+JFNAfaQxrHGYPVBhu3rbs", + "MGsHar1t/XyEES/kb489UN9LLMLgRW4ESxLWdeJ2pFtcYxrfsceWM7ZuHD7efDp/Pk+8Hdy39XYAjcFc", + "YDA4oQ8cZ4u/nyNhkLMkmatf/0jKiyZarTaqO3OTRCdr6pZ2wOAVZD+9frVhibVBvTFEkO2s60my7dyV", + "l2JZonLlxPFZ7UXvgtP0v10yaAe4SvByzjVr/2vIoSNMViBmUnW6hO2HjP2QsULGtOQgxtH7j2fTVjlb", + "AE5kp4B9NC1e1wpbA2Hr9wGR43NkCGVrEAKNlogIRJlEHHC83F8R5IxxiSIcLXQ1Q27MkFLvmeHsIiTk", + "Hr51rcE5uQcKQryqVWhC2HI1SizA6IHIBcslihYQfVXOdEFAorN8FPqagt+6L6fimLw6AuyMDU88miHM", + "oZfvHhagy20W3BvXRyh660rn/N45nTlPgsNgEjx9efq/AAAA//8=", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/acceptance/scenario.go b/internal/acceptance/scenario.go index 43acc5de..954b0929 100644 --- a/internal/acceptance/scenario.go +++ b/internal/acceptance/scenario.go @@ -218,6 +218,9 @@ func uploadGeneration(ctx context.Context, cfg ScenarioConfig, client *http.Clie } 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") diff --git a/internal/acceptance/scenario_test.go b/internal/acceptance/scenario_test.go index cb229ddb..abf2339c 100644 --- a/internal/acceptance/scenario_test.go +++ b/internal/acceptance/scenario_test.go @@ -79,6 +79,16 @@ func TestCompareColumnsRejectsMissingOracleColumn(t *testing.T) { } } +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}, diff --git a/internal/api/graphql/graph/aggregate_http_integration_test.go b/internal/api/graphql/graph/aggregate_http_integration_test.go index 27efcf25..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++ { diff --git a/internal/api/graphql/graph/dataframe/aggregates.go b/internal/api/graphql/graph/dataframe/aggregates.go index 80118939..8d4e3483 100644 --- a/internal/api/graphql/graph/dataframe/aggregates.go +++ b/internal/api/graphql/graph/dataframe/aggregates.go @@ -20,7 +20,7 @@ func (s *Service) AggregateInput(ctx context.Context, input model.DataframeAggre if err != nil { return dfmaterialization.AggregateResult{}, err } - call := &aggregateCall{project: input.ProjectID, selector: selector, filters: convertFilters(input.Filters), legacy: input} + 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) @@ -33,7 +33,7 @@ func (s *Service) AggregationsInput(ctx context.Context, input model.DataframeAg if err != nil { return dfmaterialization.AggregationsResult{}, err } - call := &aggregateCall{project: input.ProjectID, selector: selector, filters: convertFilters(input.Filters), rich: input, kind: aggregateCallRich} + 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) @@ -125,7 +125,7 @@ func (s *aggregateOperationState) projectDataset( selector dfmaterialization.DataframeSelector, resolve func() (dfmaterialization.Materialization, projectAccess, error), ) (dfmaterialization.Materialization, projectAccess, error) { - key := strings.TrimSpace(project) + "\x00" + selector.Key() + key := canonicalProjectID(project) + "\x00" + selector.Key() s.mu.Lock() if s.datasets == nil { s.datasets = make(map[string]*projectDatasetCacheEntry) @@ -293,7 +293,7 @@ func (s *Service) dispatchAggregateCalls(ctx context.Context, calls []*aggregate resolutionErrors := make(map[string]error) groupKeys := make([]string, 0) for _, call := range calls { - requestKey := call.project + "\x00" + call.selector.Key() + requestKey := canonicalProjectID(call.project) + "\x00" + call.selector.Key() if group := resolved[requestKey]; group != nil { group.calls = append(group.calls, call) continue diff --git a/internal/api/graphql/graph/dataframe/model.go b/internal/api/graphql/graph/dataframe/model.go index c4aa6a04..b09a062d 100644 --- a/internal/api/graphql/graph/dataframe/model.go +++ b/internal/api/graphql/graph/dataframe/model.go @@ -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,7 +36,7 @@ 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"), diff --git a/internal/api/graphql/graph/dataframe/rows.go b/internal/api/graphql/graph/dataframe/rows.go index fe794d0b..12164ee3 100644 --- a/internal/api/graphql/graph/dataframe/rows.go +++ b/internal/api/graphql/graph/dataframe/rows.go @@ -20,11 +20,11 @@ func (s *Service) ProjectDatasets(ctx context.Context, projectID string) ([]dfma if err != nil { return nil, err } - projectID = strings.TrimSpace(projectID) + projectID = canonicalProjectID(projectID) if _, err := s.authorizeProject(ctx, principal, projectID); err != nil { return nil, err } - values, err := s.reader.CurrentProjectDatasets(ctx, projectID) + values, err := s.reader.CurrentProjectDatasets(ctx, legacyProjectID(projectID)) if err != nil { return nil, mapReaderError(err) } diff --git a/internal/api/graphql/graph/dataframe/service.go b/internal/api/graphql/graph/dataframe/service.go index 4fadd191..4e86d118 100644 --- a/internal/api/graphql/graph/dataframe/service.go +++ b/internal/api/graphql/graph/dataframe/service.go @@ -13,6 +13,7 @@ import ( "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 { @@ -58,7 +59,7 @@ type projectAccess struct { } func (s *Service) authorizeProject(_ context.Context, principal *authscope.Principal, project string) (projectAccess, error) { - project = strings.TrimSpace(project) + project = canonicalProjectID(project) if project == "" { return projectAccess{}, dataframeerrors.NewError(dataframeerrors.CodeInvalidRequest, "") } @@ -69,7 +70,8 @@ func (s *Service) authorizeProject(_ context.Context, principal *authscope.Princ } func (s *Service) authorizeMaterialization(ctx context.Context, principal *authscope.Principal, value dfmaterialization.Materialization) (projectAccess, error) { - if err := authscope.AuthorizeProject(principal, value.Project, false); err != nil { + project := canonicalProjectID(value.Project) + if err := authscope.AuthorizeProject(principal, project, false); err != nil { return projectAccess{}, mapReaderError(err) } if s.scopeResolver == nil { @@ -81,7 +83,7 @@ func (s *Service) authorizeMaterialization(ctx context.Context, principal *auths } return projectAccess{authResourcePaths: append([]string(nil), principal.AuthResourcePaths...)}, nil } - scope, err := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, value.Project, value.DatasetGeneration, nil) + scope, err := s.scopeResolver.ResolveReadScopeForGeneration(ctx, principal, legacyProjectID(project), value.DatasetGeneration, nil) if err != nil { return projectAccess{}, mapReaderError(err) } @@ -113,11 +115,11 @@ func (s *Service) currentProjectDatasetForPrincipal(ctx context.Context, princip } func (s *Service) resolveCurrentProjectDatasetForPrincipal(ctx context.Context, principal *authscope.Principal, project string, selector dfmaterialization.DataframeSelector) (dfmaterialization.Materialization, projectAccess, error) { - project = strings.TrimSpace(project) + project = canonicalProjectID(project) if _, err := s.authorizeProject(ctx, principal, project); err != nil { return dfmaterialization.Materialization{}, projectAccess{}, err } - value, err := s.reader.CurrentProjectDataset(ctx, project, selector) + value, err := s.reader.CurrentProjectDataset(ctx, legacyProjectID(project), selector) if err != nil { return dfmaterialization.Materialization{}, projectAccess{}, mapReaderError(err) } @@ -170,3 +172,11 @@ func mapReaderError(err error) error { 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/explorer/authoringv2/semantic_types.go b/internal/explorer/authoringv2/semantic_types.go index 91db29a2..67880a35 100644 --- a/internal/explorer/authoringv2/semantic_types.go +++ b/internal/explorer/authoringv2/semantic_types.go @@ -27,10 +27,10 @@ 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"` @@ -138,8 +138,8 @@ 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) != "" || strings.TrimSpace(s.Operation) != "" || strings.TrimSpace(s.WherePath) != "" || strings.TrimSpace(s.WhereEquals) != "" || len(s.RequiredValues) != 0 || strings.TrimSpace(s.ProjectionMode) != "" { - 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) != "" { diff --git a/internal/explorer/authoringv2/workspace_test.go b/internal/explorer/authoringv2/workspace_test.go index 8f6b938c..628b7daf 100644 --- a/internal/explorer/authoringv2/workspace_test.go +++ b/internal/explorer/authoringv2/workspace_test.go @@ -157,6 +157,43 @@ func TestDecodeWorkspaceRepairsPreviouslyOmittedEmptyColumns(t *testing.T) { } } +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") diff --git a/internal/explorer/compilation/compile.go b/internal/explorer/compilation/compile.go index 732f3b30..b7d90320 100644 --- a/internal/explorer/compilation/compile.go +++ b/internal/explorer/compilation/compile.go @@ -15,6 +15,7 @@ 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/projectid" ) const TranslationVersion = "authoring-v2-native-5" @@ -96,6 +97,7 @@ type WorkspaceResult struct { } 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) @@ -141,7 +143,7 @@ func Compile(ctx context.Context, project, explorerID string, document authoring if err := contextErr(ctx); err != nil { return Result{}, err } - project = strings.TrimSpace(project) + project = projectid.Canonical(project) explorerID = strings.TrimSpace(explorerID) if project == "" { return Result{}, fail("protocol", "PROJECT_REQUIRED", "$.project", "project is required", nil, nil) @@ -152,7 +154,7 @@ func Compile(ctx context.Context, project, explorerID string, document authoring 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 { + 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 { diff --git a/internal/explorer/compilation/translate_test.go b/internal/explorer/compilation/translate_test.go index d848ca00..01fdb872 100644 --- a/internal/explorer/compilation/translate_test.go +++ b/internal/explorer/compilation/translate_test.go @@ -2,6 +2,7 @@ package compilation import ( "context" + "errors" "testing" "github.com/calypr/loom/internal/dataframe/recipe" @@ -10,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"}} @@ -19,6 +24,40 @@ func fixtureSnapshot() capability.Snapshot { return capability.NewSnapshot(identity, policy, capability.StatusReady, true, false, nodes, edges, candidates, nil) } +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") + + 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) + } + }) + } + + _, 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) + } +} + func TestCompileSemanticWorkspacePreservesAuthoredColumnsAndTypedSources(t *testing.T) { visible, order := true, 0 document := authoringv2.Document{ diff --git a/internal/explorer/compilation/workspace_test.go b/internal/explorer/compilation/workspace_test.go index e9c98492..dc9820da 100644 --- a/internal/explorer/compilation/workspace_test.go +++ b/internal/explorer/compilation/workspace_test.go @@ -33,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/lifecycle/policy.go b/internal/explorer/lifecycle/policy.go index a74395ae..210f2be1 100644 --- a/internal/explorer/lifecycle/policy.go +++ b/internal/explorer/lifecycle/policy.go @@ -13,6 +13,7 @@ import ( "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 { @@ -95,7 +96,7 @@ func (s *Service) lookupReceipt(ctx context.Context, project, explorerID, receip } func (s *Service) validateReceiptRoute(receipt *explorer.CompilationReceipt, project, explorerID string) error { - if receipt == nil || receipt.Project != project || receipt.ExplorerID != explorerID { + 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 { diff --git a/internal/explorer/lifecycle/service_test.go b/internal/explorer/lifecycle/service_test.go index 72edc184..4d2d781a 100644 --- a/internal/explorer/lifecycle/service_test.go +++ b/internal/explorer/lifecycle/service_test.go @@ -232,6 +232,23 @@ func TestPreviewRejectsStaleGenerationAndScope(t *testing.T) { } } +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) diff --git a/internal/server/openapi_explorer_authoring_direct.go b/internal/server/openapi_explorer_authoring_direct.go index 5c7ad4e6..630a13ad 100644 --- a/internal/server/openapi_explorer_authoring_direct.go +++ b/internal/server/openapi_explorer_authoring_direct.go @@ -39,12 +39,19 @@ func (h *explorerHTTPHandlers) authoringReadDirect(ctx context.Context, project return nil } -func (h *explorerHTTPHandlers) authoringWriteDirect(ctx context.Context, project string) error { +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, ""); err != nil { + 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 @@ -97,9 +104,9 @@ func (h *explorerHTTPHandlers) getAuthoringBuilderDirect(ctx context.Context, pr return directAuthoringJSON[loomapi.BuilderState](value) } -func (h *explorerHTTPHandlers) applyAuthoringCommandsDirect(ctx context.Context, project, explorerID string, body *loomapi.ApplyExplorerBuilderCommandsJSONRequestBody) (loomapi.ApplyCommandsResponse, error) { +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); err != nil { + if err := h.authoringWriteDirect(ctx, project, authResourcePath); err != nil { return result, err } if body == nil { @@ -116,9 +123,9 @@ func (h *explorerHTTPHandlers) applyAuthoringCommandsDirect(ctx context.Context, return directAuthoringJSON[loomapi.ApplyCommandsResponse](value) } -func (h *explorerHTTPHandlers) reconcileAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.ReconcileExplorerBuilderJSONRequestBody) (loomapi.CompileResponse, error) { +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); err != nil { + if err := h.authoringWriteDirect(ctx, project, authResourcePath); err != nil { return result, err } if body == nil { @@ -171,9 +178,9 @@ func (h *explorerHTTPHandlers) previewAuthoringDirect(ctx context.Context, proje return result, nil } -func (h *explorerHTTPHandlers) publishAuthoringDirect(ctx context.Context, project, explorerID string, body *loomapi.PublishExplorerJSONRequestBody) (loomapi.PublishResponse, error) { +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); err != nil { + if err := h.authoringWriteDirect(ctx, project, authResourcePath); err != nil { return result, err } if body == nil { diff --git a/internal/server/openapi_explorer_direct.go b/internal/server/openapi_explorer_direct.go index fac3fefa..d2d6f9a9 100644 --- a/internal/server/openapi_explorer_direct.go +++ b/internal/server/openapi_explorer_direct.go @@ -114,14 +114,14 @@ func (r *explorerHTTPHandlers) listExplorers(ctx context.Context, rawProject str return result, nil } -func (r *explorerHTTPHandlers) createExplorer(ctx context.Context, rawProject string, body *loomapi.CreateExplorerJSONRequestBody) (loomapi.CreateExplorer201JSONResponse, error) { +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, ""); err != nil { + if err := r.authorizer.AuthorizeWrite(ctx, principal, project, authResourcePath); err != nil { return result, legacyOperationFailure(http.StatusForbidden, "FORBIDDEN", "forbidden", err) } if body == nil { diff --git a/internal/server/openapi_explorer_routes.go b/internal/server/openapi_explorer_routes.go index c25d41fe..d8a88089 100644 --- a/internal/server/openapi_explorer_routes.go +++ b/internal/server/openapi_explorer_routes.go @@ -25,7 +25,7 @@ func (r *HTTPRoutes) ListExplorers(ctx context.Context, request loomapi.ListExpl } func (r *HTTPRoutes) CreateExplorer(ctx context.Context, request loomapi.CreateExplorerRequestObject) (loomapi.CreateExplorerResponseObject, error) { - value, err := r.explorer.createExplorer(ctx, string(request.Project), request.Body) + value, err := r.explorer.createExplorer(ctx, string(request.Project), authResourcePathFromParam(request.Params.AuthResourcePath), request.Body) if err == nil { return loomapi.CreateExplorer201JSONResponse(value), nil } @@ -174,7 +174,7 @@ func (r *HTTPRoutes) ApplyExplorerBuilderCommands(ctx context.Context, request l } return nil, unexpectedResponseStatus("applyExplorerBuilderCommands", status) } - value, err := r.explorer.applyAuthoringCommandsDirect(ctx, string(request.Project), string(request.ExplorerId), request.Body) + 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 } @@ -204,7 +204,7 @@ func (r *HTTPRoutes) ReconcileExplorerBuilder(ctx context.Context, request looma _, failure := authoringErrorForOpenAPI(ctx, "reconcileExplorerBuilder", explorerUnavailable("reconcile", "AUTHORING_UNAVAILABLE", "Explorer authoring is not configured")) return loomapi.ReconcileExplorerBuilder503JSONResponse{AuthoringUnavailableJSONResponse: loomapi.AuthoringUnavailableJSONResponse(failure)}, nil } - value, err := r.explorer.reconcileAuthoringDirect(ctx, project, explorerID, request.Body) + value, err := r.explorer.reconcileAuthoringDirect(ctx, project, explorerID, authResourcePathFromParam(request.Params.AuthResourcePath), request.Body) if err == nil { return loomapi.ReconcileExplorerBuilder200JSONResponse(value), nil } @@ -276,7 +276,7 @@ func (r *HTTPRoutes) PublishExplorer(ctx context.Context, request loomapi.Publis _, 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, request.Body) + value, err := r.explorer.publishAuthoringDirect(ctx, project, explorerID, authResourcePathFromParam(request.Params.AuthResourcePath), request.Body) if err == nil { return loomapi.PublishExplorer200JSONResponse(value), nil } 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/openapi/openapi.yaml b/openapi/openapi.yaml index f98a39b3..3c79078f 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -71,6 +71,8 @@ paths: - $ref: '#/components/parameters/ExplorerId' post: operationId: applyExplorerBuilderCommands + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -97,6 +99,8 @@ paths: - $ref: '#/components/parameters/ExplorerId' post: operationId: reconcileExplorerBuilder + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -155,6 +159,8 @@ paths: - $ref: '#/components/parameters/ExplorerId' post: operationId: publishExplorer + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -375,6 +381,8 @@ paths: post: operationId: createExplorer summary: Create an interactive Explorer. + parameters: + - $ref: '#/components/parameters/AuthResourcePath' requestBody: required: true content: @@ -454,6 +462,12 @@ 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 diff --git a/scripts/demo-browser-smoke.sh b/scripts/demo-browser-smoke.sh new file mode 100755 index 00000000..f5c17962 --- /dev/null +++ b/scripts/demo-browser-smoke.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ui_url=${LOOM_DEMO_UI_URL:-http://127.0.0.1:3080} + +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 \ + "$ui_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 +} + +viewer_dom="$work_dir/viewer.html" +builder_dom="$work_dir/builder.html" +dump_page viewer "$viewer_dom" "TCGA-" +assert_no_ui_error "$viewer_dom" +assert_contains "$viewer_dom" "TCGA-BRCA patient cohort" +assert_contains "$viewer_dom" "Patient ID" +assert_contains "$viewer_dom" "TCGA-" + +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" + +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..da37ab6a --- /dev/null +++ b/scripts/demo-down.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repo_root" + +docker compose down "$@" diff --git a/scripts/demo-smoke.sh b/scripts/demo-smoke.sh new file mode 100755 index 00000000..51ecf852 --- /dev/null +++ b/scripts/demo-smoke.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +api_url=${LOOM_DEMO_API_URL:-http://127.0.0.1:8080} +ui_url=${LOOM_DEMO_UI_URL:-http://127.0.0.1:3080} + +curl -fsS "$api_url/readyz" >/dev/null +curl -fsS "$api_url/api/v1/projects/NCPI_ACCEPTANCE/explorers/default" | grep -q 'tcga_brca_cohort' +curl -fsS "$ui_url/" | grep -qi '' + +echo "Loom API, seeded Explorer, and standalone UI are reachable" diff --git a/scripts/demo-up.sh b/scripts/demo-up.sh new file mode 100755 index 00000000..d61da27e --- /dev/null +++ b/scripts/demo-up.sh @@ -0,0 +1,31 @@ +#!/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; } + +docker compose up --build -d arangodb clickhouse loom-api + +for _ in $(seq 1 180); do + if curl -fsS http://127.0.0.1:8080/readyz >/dev/null 2>&1; then + break + fi + sleep 1 +done +curl -fsS http://127.0.0.1:8080/readyz >/dev/null + +docker compose run --rm --no-deps demo-seed +docker compose up --build -d loom-ui + +for _ in $(seq 1 120); do + if curl -fsS http://127.0.0.1:3080/ >/dev/null 2>&1; then + break + fi + sleep 1 +done +curl -fsS http://127.0.0.1:3080/ >/dev/null + +echo "Loom demo is ready at http://127.0.0.1:3080" diff --git a/scripts/package_audit.py b/scripts/package_audit.py index bb0fd90a..a6961e2f 100644 --- a/scripts/package_audit.py +++ b/scripts/package_audit.py @@ -35,6 +35,7 @@ "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.", @@ -47,6 +48,7 @@ "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.", 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/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/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..82693c23 --- /dev/null +++ b/ui/apps/demo/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + 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..7fa274c9 --- /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.1", + "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..e94d1369 --- /dev/null +++ b/ui/apps/demo/src/main.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { LoomExplorerBuilder, LoomExplorerViewer, createLoomClient } from '@calypr/loom-ui'; +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..f8549899 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,4152 @@ +{ + "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.1", + "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/@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/@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/@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/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/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/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-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/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-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/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/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/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/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-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/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.1", + "dependencies": { + "@xyflow/react": "^12.10.1", + "elkjs": "^0.9.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@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": { + "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/package.json b/ui/packages/loom-ui/package.json new file mode 100644 index 00000000..c587129c --- /dev/null +++ b/ui/packages/loom-ui/package.json @@ -0,0 +1,44 @@ +{ + "name": "@calypr/loom-ui", + "version": "0.1.1", + "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" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "vite build && tsc -p tsconfig.json", + "test": "npm run check-boundaries && vitest run", + "check-boundaries": "node scripts/check-boundaries.mjs" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "dependencies": { + "@xyflow/react": "^12.10.1", + "elkjs": "^0.9.3", + "zod": "^3.24.1" + }, + "devDependencies": { + "@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..f6292b4d --- /dev/null +++ b/ui/packages/loom-ui/scripts/check-boundaries.mjs @@ -0,0 +1,26 @@ +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 effectImports = /import\s*\{[^}]*\b(?:useEffect|useLayoutEffect|useDeepCompareEffect)\b[^}]*\}\s*from\s*['"]react['"]/s; +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`); + if (/Builder\.tsx$|Viewer\.tsx$/.test(file.pathname) && effectImports.test(source)) violations.push(`${file.pathname}: direct React effect import`); +} +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..f3810a11 --- /dev/null +++ b/ui/packages/loom-ui/src/Viewer.tsx @@ -0,0 +1,105 @@ +import React, { useMemo, useState } from 'react'; +import type { ExplorerRuntimeOutputV1 } from './types'; +import { useLoomRows, useLoomRuntime, LoomProvider } from './react'; +import { createLoomClient, type LoomClient } from './api'; + +export interface LoomExplorerViewerProps { + readonly project: string; + readonly explorerId?: string; + readonly client?: LoomClient; + readonly className?: string; +} + +const textFor = (value: unknown): string => { + if (value === undefined || value === null) return '—'; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value); + if (Array.isArray(value)) return value.map(textFor).filter((value) => value !== '—').join('; ') || '—'; + if (typeof value === 'object') { + const record = value as Record; + for (const key of ['text', 'display', 'value', 'code', 'reference']) { + if (record[key] !== undefined) return textFor(record[key]); + } + try { return JSON.stringify(value); } catch { return String(value); } + } + return String(value); +}; + +const outputColumns = (output: ExplorerRuntimeOutputV1): ReadonlyArray => { + const allowed = new Set(output.columns.filter((column) => column.visible).map((column) => column.column)); + return [...output.table.columns] + .filter((binding) => binding.visible && allowed.has(binding.column)) + .map((binding) => binding.column); +}; + +const ViewerContent = ({ project, explorerId }: Required> & Pick) => { + const runtime = useLoomRuntime({ project, explorerId: explorerId ?? 'default' }); + const [selectedOutputId, setSelectedOutputId] = useState(); + const [filterText, setFilterText] = useState(''); + const output = useMemo(() => { + if (!runtime.data) return undefined; + return runtime.data.outputs.find((candidate) => candidate.outputId === selectedOutputId) ?? runtime.data.outputs[0]; + }, [runtime.data, selectedOutputId]); + const columns = useMemo(() => output ? outputColumns(output) : [], [output]); + const rows = useLoomRows(project, output?.selector, columns, { enabled: Boolean(output), first: 100 }); + const visibleRows = useMemo(() => { + const needle = filterText.trim().toLowerCase(); + if (!needle) return rows.data?.rows ?? []; + return (rows.data?.rows ?? []).filter((row) => columns.some((column) => textFor(row[column]).toLowerCase().includes(needle))); + }, [columns, filterText, rows.data]); + + if (runtime.isLoading) return
Loading Explorer…
; + if (runtime.error || !runtime.data) return
The published Explorer could not be loaded.
; + if (runtime.data.outputs.length === 0) return
This Explorer has no published tables.
; + + return ( +
+
+
+

Loom Explorer

+

{runtime.data.outputs[0]?.title ?? 'Explorer'}

+
+ +
+ + {output ? ( +
+
+
{output.title}{rows.data?.totalCount ?? '—'} rows
+ {rows.isLoading ? Loading rows… : null} + {rows.error ? Rows could not be loaded. : null} +
+
+ + {columns.map((column) => )} + + {visibleRows.map((row, index) => {columns.map((column) => )})} + +
{output.columns.find((candidate) => candidate.column === column)?.label ?? column}
{textFor(row[column])}
+ {!rows.isLoading && visibleRows.length === 0 ?

No rows match the current search.

: null} +
+
+ ) : null} +
+ ); +}; + +export const LoomExplorerViewer = ({ project, explorerId, client, className }: LoomExplorerViewerProps) => { + const ownedClient = useMemo(() => client ?? createLoomClient(), [client]); + return
; +}; + +export default LoomExplorerViewer; diff --git a/ui/packages/loom-ui/src/api.ts b/ui/packages/loom-ui/src/api.ts new file mode 100644 index 00000000..1f36ae0a --- /dev/null +++ b/ui/packages/loom-ui/src/api.ts @@ -0,0 +1,435 @@ +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 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 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; + }); +}; + +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 rows = async (selector: ExplorerRuntimeV1['outputs'][number]['selector'], columns: ReadonlyArray, rowOptions: LoomRowsOptions) => { + const data = await fetchGraphQL<{ dataframeRows: { columns: string[]; rows: unknown; totalCount?: number | null } }>( + `query LoomRows($input: DataframeRowsInput!) { dataframeRows(input: $input) { columns rows totalCount pageInfo { hasNextPage endCursor } } }`, + { input: { projectId: canonicalProject(rowOptions.project), selector, columns: [...columns], first: rowOptions.first ?? 100 } }, + rowOptions.signal, + ); + return { ...data.dataframeRows, rows: shapeRows(data.dataframeRows.rows, data.dataframeRows.columns) }; + }; + return { + listExplorers, + getBuilder, + getCapability, + getExplorer, + applyCommands, + reconcile, + suggestions, + preview, + publish, + createExplorer, + deleteExplorer, + fetchGraphQL, + rows, + 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..e07bedb4 --- /dev/null +++ b/ui/packages/loom-ui/src/api.unit.test.ts @@ -0,0 +1,232 @@ +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); + }); +}); 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..f1432531 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.reconciliation.unit.test.tsx @@ -0,0 +1,348 @@ +// @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, + }: { + readonly onPreview: () => void; + readonly onPublish: () => void; + readonly previewDisabled: boolean; + readonly publishDisabled: boolean; + }) => ( +
+ + +
+ ), +})); + +vi.mock('./components/GuidedGraphWorkspace', () => ({ + GuidedGraphWorkspace: () =>
Graph
, +})); + +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(), +}); + +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('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('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..b615d08c --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx @@ -0,0 +1,1121 @@ +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, publishStatus] = + 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 [pendingCommands, setPendingCommands] = useState(0); + 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 || + publishStatus.isLoading || + 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 = + !state.dirty || + 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(); + 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) return; + 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); + }; + + 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} + 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..379b9772 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts @@ -0,0 +1,328 @@ +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 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, + 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..f91ab8d5 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts @@ -0,0 +1,513 @@ +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 allowsRepeated = + state.catalog.routePolicy.allowRepeatedEdges ?? + state.catalog.routePolicy.repeatedEdges ?? + false; + const allowsSelfLoop = + state.catalog.routePolicy.allowSelfLoops ?? + state.catalog.routePolicy.selfLoops ?? + false; + if (repeated && !allowsRepeated) 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..5c72c7d2 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts @@ -0,0 +1,62 @@ +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 allowsRepeated = + catalog.routePolicy.allowRepeatedEdges ?? + catalog.routePolicy.repeatedEdges ?? + false; + const allowsSelfLoops = + catalog.routePolicy.allowSelfLoops ?? + catalog.routePolicy.selfLoops ?? + false; + const maxSteps = catalog.routePolicy.maxSteps; + if (maxSteps && parent.depth + 1 > maxSteps) return []; + return catalog.edges.filter((edge) => { + if (edge.fromNodeId !== parent.nodeId) return false; + if (edge.fromNodeId === edge.toNodeId && !allowsSelfLoops) return false; + return ( + allowsRepeated || + !occurrences.some( + (occurrence) => occurrence.incomingEdgeId === 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..25706aef --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.unit.test.ts @@ -0,0 +1,103 @@ +import type { ExplorerBuilderCatalog } from '../../../types'; +import type { DraftTable } from './model'; +import { + isLegalRouteExtension, + legalEdgesToNode, + legalOutgoingEdges, +} 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 and repeated-edge 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], + ); + }); +}); 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..3c5f8892 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.tsx @@ -0,0 +1,382 @@ +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; + 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, + 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..baa61e61 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.unit.test.tsx @@ -0,0 +1,136 @@ +// @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); + }); +}); + 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..0b1b4022 --- /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' + ? left.column.label + : left.candidate.label; + const rightLabel = + right.kind === 'configured' + ? right.column.label + : right.candidate.label; + return leftLabel.localeCompare(rightLabel); + }), + [available, configured, 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..831ba8bd --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.unit.test.tsx @@ -0,0 +1,305 @@ +// @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('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..85827ad5 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.tsx @@ -0,0 +1,547 @@ +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 } 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 GuidedGraphWorkspace = ({ + catalog, + table, + selectedOccurrenceId, + disabled, + onSelectOccurrence, + onSetBase, + onChangeBase, + onAppendEdge, + 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 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; + }>({ tableIdentity, occurrenceId: selectedOccurrenceId }); + const activeInspectedNodeId = + inspection.tableIdentity === tableIdentity ? inspection.nodeId : undefined; + const activeSelectedEdgeId = + inspection.tableIdentity === tableIdentity && + inspection.occurrenceId === selectedOccurrenceId + ? inspection.edgeId + : undefined; + const updateInspection = (nodeId?: string, edgeId?: string) => + setInspection({ + tableIdentity, + occurrenceId: selectedOccurrenceId, + nodeId, + edgeId, + }); + const selectEdge = (edgeId?: string) => + updateInspection(activeInspectedNodeId, edgeId); + 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, + 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 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 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: edge.label, + 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; + updateInspection(nodeId); + if (occurrences.length === 0) { + const node = catalog.nodes.find( + (candidate) => candidate.nodeId === nodeId, + ); + if (node?.rowRootEligible) onSetBase(nodeId); + return; + } + const routeOccurrences = occurrences.filter( + (occurrence) => occurrence.nodeId === nodeId, + ); + if (routeOccurrences.length === 1) { + 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]; + onSelectOccurrence(next.occurrenceId); + return; + } + const matchingEdges = legalNextEdges.filter( + (edge) => edge.toNodeId === nodeId, + ); + if (matchingEdges.length === 1) { + onAppendEdge(selectedOccurrenceId, matchingEdges[0].edgeId, 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); + selectEdge(undefined); + }; + 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..412902ba --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.unit.test.tsx @@ -0,0 +1,479 @@ +// @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 { 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', () => { + 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(), + 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', + }, + ], + }; + 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(); + + 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'); + }); + + 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..6852804f --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.tsx @@ -0,0 +1,145 @@ +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, + disabled, + onSelectEdge, + onUseAsRowStart, + onChangeRowStart, + onAddEdge, +}: { + readonly catalog: ExplorerBuilderCatalog; + readonly table?: DraftTable; + readonly selectedOccurrenceId: string; + readonly inspectedNodeId?: string; + readonly selectedEdgeId?: string; + 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) return null; + + if (!hasRoot) { + return ( +
+
+

{inspectedResource}

+

+ Start the query with this resource. +

+
+ +
+ ); + } + + // The graph click applies a single unambiguous relationship directly. + // Controls are only necessary when Loom reports multiple valid edges. + if (inspectedEdges.length === 1) return null; + + if (inspectedEdges.length === 0) { + if (!inspectedNode?.rowRootEligible) return null; + return ( +
+
+

{inspectedResource}

+

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

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

+ Add {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..5c5f2d3e --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/RouteExtensionPanel.unit.test.tsx @@ -0,0 +1,194 @@ +// @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('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/index.ts b/ui/packages/loom-ui/src/index.ts new file mode 100644 index 00000000..68cc3bb3 --- /dev/null +++ b/ui/packages/loom-ui/src/index.ts @@ -0,0 +1,11 @@ +import './styles.css'; + +export { createLoomClient, LoomRequestError } from './api'; +export type { LoomClient, LoomClientOptions, LoomRowsOptions, ExplorerAuthoringApiError, ExplorerSummary } from './api'; +export { LoomProvider, useLoomClient, useLoomRows, useLoomRuntime } from './react'; +export { LoomExplorerBuilder } from './Builder'; +export type { LoomExplorerBuilderProps } from './Builder'; +export { LoomExplorerViewer } from './Viewer'; +export type { LoomExplorerViewerProps } from './Viewer'; +export * from './types'; +export * from './features/ExplorerBuilder'; diff --git a/ui/packages/loom-ui/src/react.tsx b/ui/packages/loom-ui/src/react.tsx new file mode 100644 index 00000000..b916304b --- /dev/null +++ b/ui/packages/loom-ui/src/react.tsx @@ -0,0 +1,219 @@ +import React, { createContext, useCallback, useContext, useMemo, useRef, useState, useSyncExternalStore } from 'react'; +import { createLoomClient, type LoomClient } from './api'; +import type { + ApplyExplorerBuilderCommandsArgs, + CreateExplorerArgs, + DeleteExplorerArgs, + ExplorerAuthoringProjectArgs, + ExplorerAuthoringStateArgs, + ExplorerCandidateSuggestionsArgs, + PreviewExplorerBuilderArgs, + PublishExplorerBuilderArgs, + ReconcileExplorerBuilderArgs, +} from './api'; + +const LoomClientContext = createContext(null); + +export const LoomProvider = ({ + client, + children, +}: { + readonly client: LoomClient; + readonly children: React.ReactNode; +}) => ( + + {children} + +); + +export const useLoomClient = (): LoomClient => { + const client = useContext(LoomClientContext); + if (!client) throw new Error('Loom UI must be rendered inside LoomProvider.'); + return client; +}; + +interface QueryResult { + readonly data?: T; + readonly error?: unknown; + readonly isLoading: boolean; + readonly isFetching: boolean; + readonly refetch: () => Promise<{ readonly data?: T; readonly error?: unknown }>; +} + +interface ResourceSnapshot { + readonly data?: T; + readonly error?: unknown; + readonly loading: boolean; +} + +interface Resource { + readonly getSnapshot: () => ResourceSnapshot; + readonly subscribe: (listener: () => void) => () => void; + readonly refresh: () => Promise<{ readonly data?: T; readonly error?: unknown }>; +} + +export const resourceFor = (loader: (signal: AbortSignal) => Promise): Resource => { + let snapshot: ResourceSnapshot = { loading: true }; + let controller: AbortController | undefined; + const listeners = new Set<() => void>(); + let started = false; + const notify = () => listeners.forEach((listener) => listener()); + const refresh = async () => { + controller?.abort(); + const requestController = new AbortController(); + controller = requestController; + started = true; + snapshot = { ...snapshot, loading: true, error: undefined }; + notify(); + try { + const data = await loader(requestController.signal); + if (controller !== requestController) return { data }; + snapshot = { data, loading: false }; + notify(); + return { data }; + } catch (error) { + if (requestController.signal.aborted || controller !== requestController) { + return { error }; + } + snapshot = { error, loading: false }; + notify(); + return { error }; + } + }; + const resource: Resource = { + getSnapshot: () => snapshot, + subscribe: (listener) => { + listeners.add(listener); + if (!started) void refresh(); + return () => { + listeners.delete(listener); + queueMicrotask(() => { + if (listeners.size > 0) return; + controller?.abort(); + controller = undefined; + started = false; + snapshot = { loading: true }; + }); + }; + }, + refresh, + }; + return resource; +}; + +const useQuery = ( + loader: (signal: AbortSignal) => Promise, + dependencies: ReadonlyArray, +): QueryResult => { + const loaderRef = useRef(loader); + loaderRef.current = loader; + const resource = useMemo( + () => resourceFor((signal) => loaderRef.current(signal)), + dependencies, + ); + const snapshot = useSyncExternalStore(resource.subscribe, resource.getSnapshot, resource.getSnapshot); + return { + ...snapshot, + isLoading: snapshot.loading, + isFetching: snapshot.loading, + refetch: resource.refresh, + }; +}; + +interface MutationState { + readonly isLoading: boolean; +} + +type Trigger = (args: TArgs) => { + readonly unwrap: () => Promise; + readonly abort: () => void; +}; + +const useMutation = ( + operation: (args: TArgs, signal: AbortSignal) => Promise, +): [Trigger, MutationState] => { + const [isLoading, setLoading] = useState(false); + const operationRef = useRef(operation); + operationRef.current = operation; + const trigger = useCallback>((args) => { + const controller = new AbortController(); + const promise = operationRef.current(args, controller.signal); + setLoading(true); + void promise.then(() => setLoading(false), () => setLoading(false)); + return { unwrap: () => promise, abort: () => controller.abort() }; + }, []); + return [trigger, { isLoading }]; +}; + +export const useGetExplorerAuthoringExplorersQuery = (args: ExplorerAuthoringProjectArgs): QueryResult>> => { + const client = useLoomClient(); + return useQuery((signal) => client.listExplorers(args, signal), [client, args.project]); +}; + +export const useGetExplorerBuilderStateV2Query = (args: ExplorerAuthoringStateArgs): QueryResult>> => { + const client = useLoomClient(); + return useQuery((signal) => client.getBuilder(args, { signal }), [client, args.project, args.explorerId]); +}; + +export const useGetExplorerAuthoringCapabilityV2Query = (args: ExplorerAuthoringStateArgs): QueryResult>> => { + const client = useLoomClient(); + return useQuery((signal) => client.getCapability(args, signal), [client, args.project, args.explorerId]); +}; + +export const useApplyExplorerBuilderCommandsV2Mutation = () => { + const client = useLoomClient(); + return useMutation>>((args, signal) => client.applyCommands(args, signal)); +}; + +export const useReconcileExplorerBuilderV2Mutation = () => { + const client = useLoomClient(); + return useMutation>>((args, signal) => client.reconcile(args, signal)); +}; + +export const useGetExplorerCandidateSuggestionsV2Mutation = () => { + const client = useLoomClient(); + return useMutation>>((args, signal) => client.suggestions(args, signal)); +}; + +export const usePreviewExplorerAuthoringV2Mutation = () => { + const client = useLoomClient(); + return useMutation>>((args, signal) => client.preview(args, signal)); +}; + +export const usePublishExplorerAuthoringV2Mutation = () => { + const client = useLoomClient(); + return useMutation>>((args, signal) => client.publish(args, signal)); +}; + +export const useCreateExplorerAuthoringMutation = () => { + const client = useLoomClient(); + return useMutation>>((args, signal) => client.createExplorer(args, signal)); +}; + +export const useDeleteExplorerAuthoringMutation = () => { + const client = useLoomClient(); + return useMutation((args, signal) => client.deleteExplorer(args, signal)); +}; + +export const useLoomRuntime = (args: ExplorerAuthoringStateArgs): QueryResult>> => { + const client = useLoomClient(); + return useQuery((signal) => client.getExplorer(args, signal), [client, 'runtime', args.project, args.explorerId]); +}; + +export const useLoomRows = ( + project: string, + selector: Awaited>['outputs'][number]['selector'] | undefined, + columns: ReadonlyArray, + options: { readonly enabled?: boolean; readonly first?: number } = {}, +): QueryResult>> => { + const client = useLoomClient(); + const enabled = options.enabled ?? Boolean(selector); + const identity = selector ? JSON.stringify(selector) : 'none'; + const query = useQuery( + (signal) => enabled && selector ? client.rows(selector, columns, { project, first: options.first, signal }) : Promise.resolve({ columns: [], rows: [], totalCount: 0 }), + [client, 'rows', enabled, project, identity, columns.join(',')], + ); + return enabled ? query : { data: undefined, error: undefined, isLoading: false, isFetching: false, refetch: async () => ({}) }; +}; + +export type { QueryResult }; diff --git a/ui/packages/loom-ui/src/react.unit.test.tsx b/ui/packages/loom-ui/src/react.unit.test.tsx new file mode 100644 index 00000000..3487096e --- /dev/null +++ b/ui/packages/loom-ui/src/react.unit.test.tsx @@ -0,0 +1,97 @@ +// @vitest-environment jsdom + +import React, { StrictMode } from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { createLoomClient } from './api'; +import { + LoomProvider, + resourceFor, + useGetExplorerAuthoringExplorersQuery, +} from './react'; + +const ExplorerStatus = () => { + const query = useGetExplorerAuthoringExplorersQuery({ project: 'NCPI_ACCEPTANCE' }); + if (query.isLoading) return loading; + if (query.error) return error; + return {query.data?.length} explorers; +}; + +describe('Loom React queries', () => { + it('keeps the initial request alive through the Strict Mode subscription probe', async () => { + const fetch = vi.fn((_input, init) => + new Promise((resolve, reject) => { + const timer = window.setTimeout( + () => + resolve( + new Response('[]', { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + 10, + ); + init?.signal?.addEventListener('abort', () => { + window.clearTimeout(timer); + reject(new DOMException('The operation was aborted.', 'AbortError')); + }); + }), + ); + const client = createLoomClient({ fetch }); + + render( + + + + + , + ); + + await waitFor(() => expect(screen.getByText('0 explorers')).toBeTruthy()); + expect(fetch).toHaveBeenCalledTimes(1); + }); + + it('does not let an obsolete request overwrite a newer refresh', async () => { + const pending: Array<{ + readonly signal: AbortSignal; + readonly resolve: (value: string) => void; + }> = []; + const resource = resourceFor( + (signal) => + new Promise((resolve) => pending.push({ signal, resolve })), + ); + const unsubscribe = resource.subscribe(() => undefined); + + expect(pending).toHaveLength(1); + const newer = resource.refresh(); + expect(pending).toHaveLength(2); + expect(pending[0].signal.aborted).toBe(true); + + pending[1].resolve('newer'); + await newer; + pending[0].resolve('obsolete'); + await waitFor(() => expect(resource.getSnapshot().data).toBe('newer')); + + unsubscribe(); + }); + + it('aborts an in-flight request after a real unmount', async () => { + let requestSignal: AbortSignal | undefined; + const fetch = vi.fn((_input, init) => { + requestSignal = init?.signal ?? undefined; + return new Promise(() => undefined); + }); + const client = createLoomClient({ fetch }); + const view = render( + + + + + , + ); + + await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1)); + expect(requestSignal?.aborted).toBe(false); + view.unmount(); + await waitFor(() => expect(requestSignal?.aborted).toBe(true)); + }); +}); diff --git a/ui/packages/loom-ui/src/styles.css b/ui/packages/loom-ui/src/styles.css new file mode 100644 index 00000000..77c132df --- /dev/null +++ b/ui/packages/loom-ui/src/styles.css @@ -0,0 +1,34 @@ +@tailwind components; +@tailwind utilities; + +.loom-ui-root, .loom-ui-root *, .loom-ui-root *::before, .loom-ui-root *::after { + box-sizing: border-box; + border-width: 0; + border-style: solid; + border-color: #e5e7eb; +} +.loom-ui-root button, .loom-ui-root input, .loom-ui-root select, .loom-ui-root textarea { + color: inherit; + font: inherit; +} + +.loom-viewer { min-height: 100vh; padding: 1.5rem; color: #0f172a; background: #f8fafc; font-family: Inter, ui-sans-serif, system-ui, sans-serif; } +.loom-viewer-header { display: flex; align-items: end; justify-content: space-between; gap: 1rem; margin: 0 auto 1rem; max-width: 1400px; } +.loom-eyebrow { margin: 0 0 .25rem; color: #2563eb; font-size: .72rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; } +.loom-viewer h1 { margin: 0; font-size: 1.8rem; } +.loom-search { display: grid; gap: .25rem; color: #475569; font-size: .75rem; font-weight: 600; } +.loom-search input { min-width: 18rem; border: 1px solid #cbd5e1; border-radius: .4rem; padding: .55rem .7rem; background: white; font: inherit; font-weight: 400; } +.loom-tabs { display: flex; gap: .4rem; max-width: 1400px; margin: 0 auto 1rem; border-bottom: 1px solid #cbd5e1; } +.loom-tabs button { border: 0; border-bottom: 2px solid transparent; padding: .65rem .9rem; color: #475569; background: transparent; cursor: pointer; font-weight: 600; } +.loom-tabs button.active { border-bottom-color: #2563eb; color: #1d4ed8; } +.loom-table-card { max-width: 1400px; margin: auto; border: 1px solid #e2e8f0; border-radius: .7rem; background: white; box-shadow: 0 5px 20px #0f172a0d; } +.loom-table-heading { display: flex; justify-content: space-between; gap: 1rem; padding: .85rem 1rem; border-bottom: 1px solid #e2e8f0; color: #475569; font-size: .8rem; } +.loom-table-heading div { display: flex; gap: .75rem; align-items: baseline; } +.loom-table-heading strong { color: #0f172a; font-size: .95rem; } +.loom-table-scroll { overflow: auto; } +.loom-table-scroll table { width: 100%; border-collapse: collapse; font-size: .82rem; } +.loom-table-scroll th, .loom-table-scroll td { max-width: 22rem; padding: .65rem .8rem; border-bottom: 1px solid #f1f5f9; text-align: left; vertical-align: top; } +.loom-table-scroll th { position: sticky; top: 0; background: #f8fafc; color: #334155; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; } +.loom-table-scroll td { overflow-wrap: anywhere; } +.loom-loading, .loom-error, .loom-empty { padding: 2rem; color: #475569; } +.loom-error-text { color: #b91c1c; } diff --git a/ui/packages/loom-ui/src/test-matchers.d.ts b/ui/packages/loom-ui/src/test-matchers.d.ts new file mode 100644 index 00000000..c2631b46 --- /dev/null +++ b/ui/packages/loom-ui/src/test-matchers.d.ts @@ -0,0 +1,13 @@ +import '@vitest/expect'; + +declare module '@vitest/expect' { + interface Assertion { + toBeInTheDocument(): void; + toBeEnabled(): void; + toBeDisabled(): void; + toHaveAttribute(name: string, value?: string): void; + toHaveClass(...classes: string[]): void; + toHaveTextContent(expected: string | RegExp): void; + toBeEmptyDOMElement(): void; + } +} diff --git a/ui/packages/loom-ui/src/types.ts b/ui/packages/loom-ui/src/types.ts new file mode 100644 index 00000000..304b2760 --- /dev/null +++ b/ui/packages/loom-ui/src/types.ts @@ -0,0 +1,799 @@ +import { z } from 'zod'; +/** Selector identity for published dataframe reads. */ +export interface DataframeSelector { + readonly recipe: string; + readonly translationVersion: string; + readonly output: string; +} + +export const EXPLORER_AUTHORING_API_VERSION = + 'loom.calypr.org/explorer-authoring/v2' as const; + +const opaqueIdSchema = z.string().trim().min(1); +const projectionModeSchema = z.enum(['VALUE', 'FIRST', 'ALL', 'DISTINCT']); +const unknownRecordSchema = z.record(z.string(), z.unknown()); + +export const explorerAuthoringDiagnosticSchema = z + .object({ + severity: z.enum(['error', 'warning', 'info']), + stage: z.string().optional(), + code: opaqueIdSchema, + path: z.string().nullable().optional(), + fieldPath: z.string().nullable().optional(), + message: z.string(), + details: unknownRecordSchema.optional(), + requestId: z.string().optional(), + }) + .strict(); +export type ExplorerAuthoringDiagnostic = z.infer< + typeof explorerAuthoringDiagnosticSchema +>; + +export const explorerTablePresentationSchema = z + .object({ + visible: z.boolean().optional(), + order: z.number().int().nonnegative().optional(), + pinned: z.boolean().optional(), + cellRenderer: z.literal('fileActions').optional(), + }) + .strict(); +export const explorerFilterPresentationSchema = z + .object({ + label: z.string().optional(), + order: z.number().int().nonnegative().optional(), + }) + .strict(); +export const explorerChartPresentationSchema = z + .object({ + type: opaqueIdSchema, + title: z.string().optional(), + order: z.number().int().nonnegative().optional(), + }) + .strict(); +const scalarColumnSourceSchema = z + .object({ + kind: z.enum([ + 'field', + 'identifierBySystem', + 'extensionByUrl', + 'codingBySystem', + 'observationComponentByCode', + 'projectId', + ]), + fieldPath: z.string().optional(), + match: z.string().optional(), + projectionMode: projectionModeSchema.optional(), + }) + .strict(); +const aggregateColumnSourceSchema = z + .object({ + kind: z.literal('aggregate'), + operation: z.enum([ + 'COUNT', + 'COUNT_DISTINCT', + 'DISTINCT_VALUES', + 'MIN', + 'MAX', + 'EXISTS', + 'CONTAINS_ALL', + ]), + fieldPath: z.string().optional(), + wherePath: z.string().optional(), + whereEquals: z.string().optional(), + requiredValues: z.array(z.string()).optional(), + }) + .strict(); +export const explorerColumnSourceSchema = z.union([ + scalarColumnSourceSchema, + aggregateColumnSourceSchema, +]); +export type ExplorerColumnSource = z.infer; +export type ExplorerBuilderRouteNode = { + occurrenceId: string; + resourceType: string; + relationship?: string; + children?: ExplorerBuilderRouteNode[]; +}; +export const explorerBuilderRouteNodeSchema: z.ZodType = + z.lazy(() => + z + .object({ + occurrenceId: opaqueIdSchema, + resourceType: opaqueIdSchema, + relationship: z.string().optional(), + children: z.array(explorerBuilderRouteNodeSchema).optional(), + }) + .strict(), + ); +export const explorerBuilderColumnSchema = z + .object({ + column: opaqueIdSchema.regex(/^[A-Za-z_][A-Za-z0-9_]*$/), + label: z.string().min(1), + logicalType: z.string().optional(), + occurrenceId: opaqueIdSchema, + source: explorerColumnSourceSchema, + table: explorerTablePresentationSchema.optional(), + filter: explorerFilterPresentationSchema.optional(), + chart: explorerChartPresentationSchema.optional(), + }) + .strict(); +export type ExplorerBuilderColumn = z.infer; +export const explorerBuilderDocumentSchema = z + .object({ + kind: z.literal('ExplorerBuilderDocument'), + output: z + .object({ + id: opaqueIdSchema.regex(/^[A-Za-z_][A-Za-z0-9_]*$/), + title: z.string().min(1), + rowLabel: z.string().optional(), + }) + .strict(), + rootResourceType: opaqueIdSchema, + route: explorerBuilderRouteNodeSchema, + columns: z.array(explorerBuilderColumnSchema), + fixedFilters: z + .array( + z + .object({ + column: opaqueIdSchema, + values: z.array(z.string()).min(1), + }) + .strict(), + ) + .optional(), + actions: z + .array( + z + .object({ + type: opaqueIdSchema, + title: z.string().min(1), + fileName: z.string().optional(), + columns: z + .array( + z + .object({ + column: opaqueIdSchema, + exportHeader: z.string().optional(), + }) + .strict(), + ) + .optional(), + }) + .strict(), + ) + .optional(), + }) + .strict(); +export type ExplorerBuilderDocument = z.infer< + typeof explorerBuilderDocumentSchema +>; + +export const explorerBuilderTabSchema = z + .object({ + id: opaqueIdSchema, + title: z.string(), + outputId: opaqueIdSchema, + order: z.number().int().nonnegative(), + visible: z.boolean().optional(), + }) + .strict(); +export const explorerBuilderWorkspaceSchema = z + .object({ + apiVersion: z.literal(EXPLORER_AUTHORING_API_VERSION), + kind: z.literal('ExplorerBuilderWorkspace'), + explorer: z + .object({ title: z.string().min(1), description: z.string().optional() }) + .strict(), + documents: z.array(explorerBuilderDocumentSchema), + tabs: z.array(explorerBuilderTabSchema), + sharedFilters: z + .record( + z.string(), + z.array( + z + .object({ outputId: opaqueIdSchema, column: opaqueIdSchema }) + .strict(), + ), + ) + .optional(), + fileActions: z + .object({ + extensions: z.record(z.string(), z.array(z.string())), + actions: z.record(z.string(), z.string()), + }) + .strict() + .optional(), + }) + .strict() + .superRefine((workspace, context) => { + const outputIds = workspace.documents.map((document) => document.output.id); + const tabIds = workspace.tabs.map((tab) => tab.id); + const tabOutputIds = workspace.tabs.map((tab) => tab.outputId); + const duplicate = (values: ReadonlyArray) => + values.find((value, index) => values.indexOf(value) !== index); + if (duplicate(outputIds)) { + context.addIssue({ + code: 'custom', + path: ['documents'], + message: 'Document output IDs must be unique.', + }); + } + if (duplicate(tabIds)) { + context.addIssue({ + code: 'custom', + path: ['tabs'], + message: 'Tab IDs must be unique.', + }); + } + if ( + duplicate(tabOutputIds) || + outputIds.length !== tabOutputIds.length || + outputIds.some((outputId) => !tabOutputIds.includes(outputId)) + ) { + context.addIssue({ + code: 'custom', + path: ['tabs'], + message: 'Tabs and document outputs must have a one-to-one mapping.', + }); + } + }); +export type ExplorerBuilderWorkspace = z.infer< + typeof explorerBuilderWorkspaceSchema +>; + +export const explorerBuilderRoutePolicySchema = z + .object({ + allowRepeatedEdges: z.boolean().optional(), + allowSelfLoops: z.boolean().optional(), + repeatedEdges: z.boolean().optional(), + selfLoops: z.boolean().optional(), + maxSteps: z.number().int().positive().nullable().optional(), + }) + .strict(); +export const explorerBuilderCatalogNodeSchema = z + .object({ + nodeId: opaqueIdSchema, + resourceType: opaqueIdSchema, + rowRootEligible: z.boolean(), + rowGrain: z.string().optional(), + populated: z.boolean(), + documentCount: z.number().int().nonnegative(), + }) + .strict(); +export const explorerBuilderCatalogEdgeSchema = z + .object({ + edgeId: opaqueIdSchema, + fromNodeId: opaqueIdSchema, + toNodeId: opaqueIdSchema, + label: z.string(), + populated: z.boolean().optional(), + }) + .strict(); +export const explorerBuilderCandidateSchema = z + .object({ + candidateId: opaqueIdSchema, + nodeId: opaqueIdSchema, + fieldPath: opaqueIdSchema, + label: z.string(), + logicalType: opaqueIdSchema, + repeated: z.boolean().optional(), + filterable: z.boolean(), + chartable: z.boolean(), + projectionModes: z.array(projectionModeSchema).min(1), + defaultProjectionMode: projectionModeSchema, + }) + .strict(); +export const explorerBuilderCatalogSchema = z + .object({ + snapshotToken: opaqueIdSchema, + generation: opaqueIdSchema, + resolvedSchemaDigest: z.string().optional(), + authorizationScopeDigest: z.string().optional(), + complete: z.boolean().optional(), + routePolicy: explorerBuilderRoutePolicySchema, + nodes: z.array(explorerBuilderCatalogNodeSchema), + edges: z.array(explorerBuilderCatalogEdgeSchema), + candidates: z.array(explorerBuilderCandidateSchema).optional(), + }) + .strict(); +export type ExplorerBuilderCatalog = z.infer< + typeof explorerBuilderCatalogSchema +>; +export type ExplorerBuilderCandidate = z.infer< + typeof explorerBuilderCandidateSchema +>; + +export const explorerBuilderStateSchema = z + .object({ + apiVersion: z.literal(EXPLORER_AUTHORING_API_VERSION), + kind: z.literal('ExplorerBuilderState'), + lifecycleState: z.enum(['NEW', 'READY']), + draftVersion: z.number().int().nonnegative(), + draftDigest: z.string(), + workspace: explorerBuilderWorkspaceSchema.nullable(), + catalog: explorerBuilderCatalogSchema, + }) + .strict() + .superRefine((state, context) => { + if ((state.lifecycleState === 'NEW') !== (state.workspace === null)) { + context.addIssue({ + code: 'custom', + path: ['workspace'], + message: 'NEW requires null workspace and READY requires a workspace.', + }); + } + }); +export type ExplorerBuilderState = z.infer; + +export const explorerBuilderCommandSchema = z + .object({ + type: z.enum([ + 'CREATE_TABLE', + 'DUPLICATE_TABLE', + 'DELETE_TABLE', + 'RENAME_TABLE', + 'REORDER_TABLES', + 'SET_TABLE_ROOT', + 'ADD_ROUTE', + 'REMOVE_ROUTE', + 'ADD_COLUMN', + 'UPDATE_COLUMN', + 'REMOVE_COLUMN', + ]), + outputId: opaqueIdSchema.optional(), + sourceOutputId: opaqueIdSchema.optional(), + title: z.string().optional(), + rootNodeId: opaqueIdSchema.optional(), + parentOccurrenceId: opaqueIdSchema.optional(), + occurrenceId: opaqueIdSchema.optional(), + edgeId: opaqueIdSchema.optional(), + candidateId: opaqueIdSchema.optional(), + projectionMode: projectionModeSchema.optional(), + initialPresentation: z.enum(['TABLE', 'FILTER', 'CHART']).optional(), + column: opaqueIdSchema.optional(), + columnValue: explorerBuilderColumnSchema.optional(), + outputIds: z.array(opaqueIdSchema).optional(), + }) + .strict(); +export type ExplorerBuilderCommand = z.infer< + typeof explorerBuilderCommandSchema +>; +export const explorerBuilderCommandResultSchema = z + .object({ + type: z.enum([ + 'TABLE_CREATED', + 'TABLE_CHANGED', + 'ROUTE_ADDED', + 'COLUMN_ADDED', + ]), + outputId: opaqueIdSchema.optional(), + tabId: opaqueIdSchema.optional(), + occurrenceId: opaqueIdSchema.optional(), + column: opaqueIdSchema.optional(), + }) + .strict(); +export const explorerBuilderCommandsResultSchema = z + .object({ + commandId: opaqueIdSchema, + workspace: explorerBuilderWorkspaceSchema, + draftVersion: z.number().int().positive(), + draftDigest: opaqueIdSchema, + results: z.array(explorerBuilderCommandResultSchema), + diagnostics: z.array(explorerAuthoringDiagnosticSchema), + }) + .strict(); +export type ExplorerBuilderCommandsResult = z.infer< + typeof explorerBuilderCommandsResultSchema +>; + +export const explorerBuilderContractColumnSchema = z + .object({ + column: opaqueIdSchema, + label: z.string(), + logicalType: opaqueIdSchema, + filterable: z.boolean(), + chartable: z.boolean(), + }) + .strict(); +export type ExplorerBuilderContractColumn = z.infer< + typeof explorerBuilderContractColumnSchema +>; +// Local Builder view-model identities are not part of Loom's wire contract. +export interface ExplorerBuilderSelection { + readonly candidateId: string; + readonly occurrenceId: string; + readonly projectionMode: string; +} +export interface ExplorerPresentationIntent { + readonly label?: string; + readonly visible?: boolean; + readonly order?: number; + readonly table?: { readonly pinned?: boolean }; + readonly filter?: { readonly label?: string }; + readonly chart?: { readonly type: string; readonly title?: string }; +} +export interface ExplorerBuilderEmission extends ExplorerBuilderContractColumn { + readonly outputId: string; + readonly candidateId: string; + readonly occurrenceId: string; + readonly projectionMode: string; + readonly emissionId: string; + readonly publicColumn: string; +} +export const explorerBuilderReceiptOutputSchema = z + .object({ + outputId: opaqueIdSchema, + title: z.string().optional(), + rowGrain: z.string().optional(), + columns: z.array(explorerBuilderContractColumnSchema), + }) + .strict(); +export const explorerBuilderCompileResultSchema = z + .object({ + apiVersion: z.literal(EXPLORER_AUTHORING_API_VERSION), + kind: z.literal('ExplorerBuilderReceipt'), + receiptId: opaqueIdSchema, + snapshotToken: opaqueIdSchema, + generation: z.string().optional(), + intentDigest: z.string().optional(), + compilerVersion: z.string().optional(), + builder: explorerBuilderWorkspaceSchema, + outputs: z.array(explorerBuilderReceiptOutputSchema), + diagnostics: z.array(explorerAuthoringDiagnosticSchema), + }) + .strict(); +export type ExplorerBuilderCompileResult = z.infer< + typeof explorerBuilderCompileResultSchema +>; + +export const explorerBuilderPreviewColumnSchema = + explorerBuilderContractColumnSchema; +export const explorerBuilderPreviewResultSchema = z + .object({ + apiVersion: z.literal(EXPLORER_AUTHORING_API_VERSION), + kind: z.literal('ExplorerBuilderPreview'), + receiptId: opaqueIdSchema, + outputId: opaqueIdSchema, + columns: z.array(explorerBuilderPreviewColumnSchema), + rows: z.array(unknownRecordSchema).nullable(), + rowCount: z.number().int().nonnegative(), + diagnostics: z.array(explorerAuthoringDiagnosticSchema), + }) + .strict(); +export type ExplorerBuilderPreviewResult = z.infer< + typeof explorerBuilderPreviewResultSchema +>; + +export const explorerBuilderPublishResultSchema = z + .object({ + apiVersion: z.literal(EXPLORER_AUTHORING_API_VERSION), + kind: z.literal('ExplorerBuilderPublication'), + receiptId: opaqueIdSchema, + revisionId: opaqueIdSchema, + state: z.string(), + outputs: z.array( + z + .object({ + outputId: opaqueIdSchema, + state: z.string(), + materializationId: z.string().optional(), + }) + .strict(), + ), + diagnostics: z.array(explorerAuthoringDiagnosticSchema), + }) + .strict(); +export type ExplorerBuilderPublishResult = z.infer< + typeof explorerBuilderPublishResultSchema +>; + +export const explorerBuilderSuggestionsResultSchema = z + .object({ + apiVersion: z.literal(EXPLORER_AUTHORING_API_VERSION), + kind: z.literal('ExplorerBuilderCandidateSuggestions'), + snapshotToken: opaqueIdSchema, + nodeId: opaqueIdSchema, + candidates: z.array(explorerBuilderCandidateSchema), + diagnostics: z.array(explorerAuthoringDiagnosticSchema), + }) + .strict(); +export type ExplorerBuilderSuggestionsResult = z.infer< + typeof explorerBuilderSuggestionsResultSchema +>; + +export const explorerAuthoringCapabilitiesSchema = z + .object({ + apiVersion: z.literal(EXPLORER_AUTHORING_API_VERSION), + kind: z.literal('ExplorerAuthoringCapabilities'), + operations: z.array(z.string()), + previewLimits: z.array(z.number().int().positive()).optional(), + features: z + .object({ + emissionFilters: z.boolean(), + emissionCharts: z.boolean(), + sharedFilters: z.boolean(), + fixedFilters: z.boolean(), + fileActions: z.boolean(), + deleteExplorer: z.boolean(), + }) + .strict(), + }) + .strict(); +export type ExplorerAuthoringCapabilities = z.infer< + typeof explorerAuthoringCapabilitiesSchema +>; + +export const explorerAuthoringErrorSchema = z + .object({ + code: opaqueIdSchema, + message: z.string(), + diagnostics: z.array(explorerAuthoringDiagnosticSchema).optional(), + requestId: z.string().optional(), + details: unknownRecordSchema.optional(), + }) + .strict(); +export type ExplorerAuthoringError = z.infer< + typeof explorerAuthoringErrorSchema +>; + +export const assertExplorerBuilderState = (value: unknown) => + explorerBuilderStateSchema.parse(value); +export const assertExplorerBuilderCompileResult = (value: unknown) => + explorerBuilderCompileResultSchema.parse(value); +export const assertExplorerBuilderPreviewResult = (value: unknown) => + explorerBuilderPreviewResultSchema.parse(value); +export const assertExplorerBuilderPublishResult = (value: unknown) => + explorerBuilderPublishResultSchema.parse(value); + +/** Server-owned runtime projection retained for viewer/ETL consumers. */ +export interface PublicationMetadata { + readonly state: string; + readonly generation?: string; + readonly executionId?: string; + readonly revisionId?: string; + readonly updatedAt?: string; +} +export interface ExplorerRuntimeColumnV1 { + readonly column: string; + readonly label: string; + readonly logicalType: string; + readonly visible: boolean; + readonly order: number; + readonly repeated?: boolean; + readonly filterable: boolean; + readonly sortable?: boolean; + readonly chartable: boolean; + readonly aggregatable?: boolean; +} +export type ExplorerRuntimeColumnsV1 = ReadonlyArray; +export interface ExplorerRuntimeBindingV1 { + readonly column: string; + readonly outputId?: string; + readonly label?: string; + readonly type?: string; + readonly title?: string; +} +export interface ExplorerRuntimeOutputV1 { + readonly outputId: string; + readonly name: string; + readonly title: string; + readonly rowLabel: string; + readonly selector: DataframeSelector; + readonly columns: ExplorerRuntimeColumnsV1; + readonly table: { + readonly columns: ReadonlyArray< + ExplorerRuntimeBindingV1 & { readonly visible: boolean } & { + readonly pinned?: boolean; + readonly cellRenderer?: 'fileActions'; + } + >; + }; + readonly filters: ReadonlyArray; + readonly charts: ReadonlyArray; + readonly fixedFilters: Readonly>>; + readonly actions?: ReadonlyArray<{ + readonly type: string; + readonly title: string; + readonly fileName?: string; + readonly output?: string; + readonly columns?: ReadonlyArray; + readonly exportHeaders?: Readonly>; + }>; + readonly query?: Readonly>; + readonly materialization?: Readonly>; +} +export interface ExplorerRuntimeV1 { + readonly generation?: string; + readonly publication?: PublicationMetadata; + readonly schema?: { readonly digest?: string; readonly version?: string }; + readonly outputs: ReadonlyArray; + readonly sharedFilters: Readonly< + Record> + >; + readonly fileActions?: { + readonly extensions?: Readonly>>; + readonly actions?: Readonly>; + }; + readonly diagnostics: ReadonlyArray; +} + +/** Opaque generated metadata retained only for the runtime compatibility adapter. */ +export interface ExplorerStateAuthoringBundleV1 { + readonly apiVersion: 'loom.calypr.org/explorer-authoring/v1'; + readonly kind: 'ExplorerAuthoringBundle'; + readonly project: string; + readonly explorerId: string; + readonly title?: string; + readonly document?: Readonly>; + readonly documents?: ReadonlyArray>>; + readonly tabs?: ReadonlyArray<{ + readonly id: string; + readonly title: string; + readonly outputId: string; + readonly order: number; + readonly visible?: boolean; + }>; +} +export interface ExplorerStateEmittedColumnV1 { + readonly emissionId: string; + readonly outputId: string; + readonly nodeId?: string; + readonly selectionId?: string; + readonly candidateId?: string; + readonly occurrenceId?: string; + readonly publicColumn: string; + readonly logicalType: string; + readonly filterable: boolean; + readonly chartable: boolean; +} +export interface ExplorerStatePhysicalColumnV1 { + readonly name: string; + readonly semanticPath?: string; + readonly clickhouseType?: string; + readonly logicalType?: string; + readonly nullable?: boolean; + readonly repeated?: boolean; + readonly provenance?: string; + readonly loomOwned?: boolean; +} +export interface ExplorerStateDatasetOutputV1 { + readonly name: string; + readonly state: string; + readonly queryable: boolean; + readonly fingerprint?: string; + readonly selector?: DataframeSelector; + readonly columns?: ReadonlyArray; +} +export interface ExplorerStateMaterializationV1 { + readonly outputId: string; + readonly output: string; + readonly materializationId: string; + readonly fingerprint?: string; + readonly selector?: DataframeSelector; + readonly columns: ReadonlyArray; +} + +/** Runtime-only selected Explorer response. Editable state is never read here. */ +export interface ExplorerStateV1 { + readonly apiVersion: 'loom.calypr.org/explorer-state/v1'; + readonly kind: 'ExplorerState'; + readonly project: string; + readonly explorerId: string; + readonly title: string; + readonly management: + | 'repository' + | 'interactive' + | 'REPOSITORY' + | 'INTERACTIVE'; + readonly draft: { + readonly bundle?: ExplorerStateAuthoringBundleV1; + readonly receiptId?: string; + readonly version: number; + readonly digest: string; + readonly intentDigest?: string; + }; + readonly active: { + readonly bundle?: ExplorerStateAuthoringBundleV1; + readonly revisionId?: string; + readonly intentDigest?: string; + readonly status?: string; + }; + readonly generated: { + readonly recipeDigest?: string; + readonly sourceGeneration?: string; + readonly resolvedSchemaDigest?: string; + readonly emittedColumns?: ReadonlyArray; + readonly materializations?: ReadonlyArray; + readonly dataset?: { + readonly outputs: ReadonlyArray; + }; + readonly publication?: PublicationMetadata; + readonly diagnostics?: ReadonlyArray; + }; + readonly activeUrl: string; + readonly updatedBy?: string; + readonly updatedAt?: string; + /** Runtime is null when Loom has no valid published runtime to serve. */ + readonly runtime?: ExplorerRuntimeV1 | null; +} + +const allowedKeys = new Set([ + 'apiVersion', + 'kind', + 'project', + 'explorerId', + 'title', + 'management', + 'active', + 'generated', + 'activeUrl', + 'updatedBy', + 'updatedAt', + 'runtime', + 'draft', +]); +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); +const legacyExplorerStateKeys = new Set(['draftConfig', 'activeConfig']); +export const isExplorerStateV1 = (value: unknown): value is ExplorerStateV1 => { + if ( + !isRecord(value) || + !Object.keys(value).every((key) => allowedKeys.has(key)) + ) + return false; + if ( + value.apiVersion !== 'loom.calypr.org/explorer-state/v1' || + value.kind !== 'ExplorerState' || + typeof value.project !== 'string' || + typeof value.explorerId !== 'string' || + typeof value.title !== 'string' || + typeof value.management !== 'string' + ) + return false; + if (value.runtime === undefined || value.runtime === null) return true; + if (!isRecord(value.runtime)) return false; + const runtime = value.runtime; + return ( + Array.isArray(runtime.outputs) && + isRecord(runtime.sharedFilters) && + Array.isArray(runtime.diagnostics) && + runtime.outputs.every( + (output) => + isRecord(output) && + Array.isArray(output.columns) && + isRecord(output.table) && + Array.isArray(output.table.columns) && + Array.isArray(output.filters) && + Array.isArray(output.charts) && + isRecord(output.fixedFilters), + ) + ); +}; + +const normalizeExplorerStateV1 = (value: unknown): unknown => { + if (!isRecord(value) || !isRecord(value.runtime)) return value; + if (value.runtime.diagnostics !== null) return value; + + // Go encodes a nil diagnostics slice as null. Treat that wire-level empty + // value as the empty collection promised by ExplorerRuntimeV1. + return { + ...value, + runtime: { + ...value.runtime, + diagnostics: [], + }, + }; +}; + +export const assertExplorerStateV1 = (value: unknown): ExplorerStateV1 => { + const normalized = normalizeExplorerStateV1(value); + if (isExplorerStateV1(normalized)) return normalized; + const hasLegacyConfiguration = + isRecord(value) && + Object.keys(value).some((key) => legacyExplorerStateKeys.has(key)); + if (hasLegacyConfiguration) + throw new Error( + 'Loom returned an invalid ExplorerStateV1 response; legacy Explorer configuration fields are not supported.', + ); + throw new Error('Loom returned an invalid ExplorerStateV1 response.'); +}; diff --git a/ui/packages/loom-ui/src/types.unit.test.ts b/ui/packages/loom-ui/src/types.unit.test.ts new file mode 100644 index 00000000..d84663c5 --- /dev/null +++ b/ui/packages/loom-ui/src/types.unit.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { explorerColumnSourceSchema } from './types'; + +describe('explorerColumnSourceSchema', () => { + it('accepts the aggregate sources emitted by Loom authoring workspaces', () => { + expect( + explorerColumnSourceSchema.parse({ + kind: 'aggregate', + operation: 'CONTAINS_ALL', + fieldPath: 'type.coding[].code', + requiredValues: ['Tumor', 'Normal'], + }), + ).toEqual({ + kind: 'aggregate', + operation: 'CONTAINS_ALL', + fieldPath: 'type.coding[].code', + requiredValues: ['Tumor', 'Normal'], + }); + }); + + it('rejects aggregate operations Loom does not support', () => { + expect(() => + explorerColumnSourceSchema.parse({ + kind: 'aggregate', + operation: 'SUM', + fieldPath: 'valueQuantity.value', + }), + ).toThrow(); + }); +}); diff --git a/ui/packages/loom-ui/tailwind.config.cjs b/ui/packages/loom-ui/tailwind.config.cjs new file mode 100644 index 00000000..bea41073 --- /dev/null +++ b/ui/packages/loom-ui/tailwind.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + content: ['./src/**/*.{ts,tsx}'], + theme: { extend: {} }, + plugins: [], +}; diff --git a/ui/packages/loom-ui/tsconfig.json b/ui/packages/loom-ui/tsconfig.json new file mode 100644 index 00000000..02d89949 --- /dev/null +++ b/ui/packages/loom-ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist", + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist"] +} diff --git a/ui/packages/loom-ui/vite.config.ts b/ui/packages/loom-ui/vite.config.ts new file mode 100644 index 00000000..c1450764 --- /dev/null +++ b/ui/packages/loom-ui/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const directory = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + plugins: [react()], + build: { + lib: { + entry: resolve(directory, 'src/index.ts'), + name: 'LoomUI', + fileName: 'loom-ui', + formats: ['es'], + }, + rollupOptions: { + external: ['react', 'react-dom', 'react/jsx-runtime'], + output: { + globals: { react: 'React', 'react-dom': 'ReactDOM' }, + assetFileNames: 'styles.css', + }, + }, + }, +}); diff --git a/ui/packages/loom-ui/vitest.config.ts b/ui/packages/loom-ui/vitest.config.ts new file mode 100644 index 00000000..c7730bfe --- /dev/null +++ b/ui/packages/loom-ui/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + setupFiles: ['./vitest.setup.ts'], + }, +}); diff --git a/ui/packages/loom-ui/vitest.setup.ts b/ui/packages/loom-ui/vitest.setup.ts new file mode 100644 index 00000000..96e6a447 --- /dev/null +++ b/ui/packages/loom-ui/vitest.setup.ts @@ -0,0 +1,70 @@ +import { expect } from 'vitest'; + +const element = (value: unknown): value is HTMLElement => + value instanceof HTMLElement; + +expect.extend({ + toBeInTheDocument(received: unknown) { + const pass = element(received) && document.body.contains(received); + return { + pass, + message: () => + `expected ${String(received)} ${pass ? 'not ' : ''}to be in the document`, + }; + }, + toBeEnabled(received: unknown) { + const pass = element(received) && !received.hasAttribute('disabled'); + return { + pass, + message: () => + `expected ${String(received)} ${pass ? 'not ' : ''}to be enabled`, + }; + }, + toBeDisabled(received: unknown) { + const pass = element(received) && received.hasAttribute('disabled'); + return { + pass, + message: () => + `expected ${String(received)} ${pass ? 'not ' : ''}to be disabled`, + }; + }, + toHaveAttribute(received: unknown, name: string, expected?: unknown) { + const actual = element(received) ? received.getAttribute(name) : null; + const pass = + element(received) && + (expected === undefined + ? received.hasAttribute(name) + : actual === String(expected)); + return { + pass, + message: () => + `expected ${String(received)} ${pass ? 'not ' : ''}to have attribute ${name}`, + }; + }, + toHaveClass(received: unknown, ...classes: string[]) { + const actual = element(received) ? received.classList : undefined; + const pass = Boolean(actual && classes.every((name) => actual.contains(name))); + return { + pass, + message: () => + `expected ${String(received)} ${pass ? 'not ' : ''}to have classes ${classes.join(' ')}`, + }; + }, + toHaveTextContent(received: unknown, expected: string | RegExp) { + const text = element(received) ? received.textContent ?? '' : ''; + const pass = expected instanceof RegExp ? expected.test(text) : text.includes(expected); + return { + pass, + message: () => + `expected ${String(received)} ${pass ? 'not ' : ''}to contain text ${String(expected)}`, + }; + }, + toBeEmptyDOMElement(received: unknown) { + const pass = element(received) && received.childElementCount === 0 && received.textContent === ''; + return { + pass, + message: () => + `expected ${String(received)} ${pass ? 'not ' : ''}to be empty`, + }; + }, +}); From 7b3a75e85145a04bf60b2121fe3743881c5faf64 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Tue, 1 Sep 2026 11:46:46 -0700 Subject: [PATCH 15/17] update lock files --- ui/apps/demo/package.json | 2 +- ui/package-lock.json | 4 ++-- ui/packages/loom-ui/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/apps/demo/package.json b/ui/apps/demo/package.json index 7fa274c9..cb225232 100644 --- a/ui/apps/demo/package.json +++ b/ui/apps/demo/package.json @@ -9,7 +9,7 @@ "preview": "vite preview --host 0.0.0.0 --port 8080" }, "dependencies": { - "@calypr/loom-ui": "0.1.1", + "@calypr/loom-ui": "0.1.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/ui/package-lock.json b/ui/package-lock.json index f8549899..04898129 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -30,7 +30,7 @@ "name": "@calypr/loom-demo", "version": "0.1.0", "dependencies": { - "@calypr/loom-ui": "0.1.1", + "@calypr/loom-ui": "0.1.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -4127,7 +4127,7 @@ }, "packages/loom-ui": { "name": "@calypr/loom-ui", - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "@xyflow/react": "^12.10.1", "elkjs": "^0.9.3", diff --git a/ui/packages/loom-ui/package.json b/ui/packages/loom-ui/package.json index c587129c..09e4ddb5 100644 --- a/ui/packages/loom-ui/package.json +++ b/ui/packages/loom-ui/package.json @@ -1,6 +1,6 @@ { "name": "@calypr/loom-ui", - "version": "0.1.1", + "version": "0.1.2", "description": "Portable Loom Explorer Builder and Viewer components.", "type": "module", "main": "dist/loom-ui.js", From 1d7f1331ac65046b1c75f03441b87d67364f51a0 Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Tue, 1 Sep 2026 12:32:29 -0700 Subject: [PATCH 16/17] release(ui): @calypr/loom-ui 0.1.2 --- Makefile | 23 +- scripts/release-loom-ui.sh | 534 ++++++++++++++++++++++++++ scripts/tests/test_release_loom_ui.py | 478 +++++++++++++++++++++++ 3 files changed, 1034 insertions(+), 1 deletion(-) create mode 100755 scripts/release-loom-ui.sh create mode 100644 scripts/tests/test_release_loom_ui.py diff --git a/Makefile b/Makefile index 39136799..a308c06d 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 acceptance-real acceptance-performance demo-up demo-down demo-smoke demo-browser-smoke +.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 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: diff --git a/scripts/release-loom-ui.sh b/scripts/release-loom-ui.sh new file mode 100755 index 00000000..4fc221e9 --- /dev/null +++ b/scripts/release-loom-ui.sh @@ -0,0 +1,534 @@ +#!/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 + published_version "$version" + printf 'Published and verified %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/test_release_loom_ui.py b/scripts/tests/test_release_loom_ui.py new file mode 100644 index 00000000..5844906e --- /dev/null +++ b/scripts/tests/test_release_loom_ui.py @@ -0,0 +1,478 @@ +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()) + + 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() From b5cb678ae3080e2d7c99c7568328624ef644590f Mon Sep 17 00:00:00 2001 From: matthewpeterkort Date: Thu, 3 Sep 2026 11:02:46 -0700 Subject: [PATCH 17/17] incremental improvements related to stability and perf --- Dockerfile | 1 - Makefile | 5 +- README.md | 4 + cmd/loom-acceptance/main.go | 106 +++++ cmd/loom-acceptance/main_test.go | 46 ++ cmd/loom-repository/main.go | 426 ++++++++++++++++++ cmd/loom-repository/main_test.go | 231 ++++++++++ compose.yaml | 24 +- docs/DEMO_CONFIGURATION.md | 42 ++ docs/DEMO_QUICKSTART.md | 22 +- docs/LOCAL_REPOSITORY_DEMO.md | 88 ++++ .../explorer-v2-capability-authoring.md | 2 +- generated/loomapi/api.gen.go | 395 +++++++++++----- internal/api/bulk/load/operations.go | 19 + internal/api/bulk/load/operations_test.go | 53 +++ internal/api/bulk/load/service.go | 28 ++ internal/api/bulk/load/service_test.go | 39 ++ internal/api/http/errors.go | 4 +- internal/api/http/errors_test.go | 3 + .../compiler/generic_physical_plan_test.go | 70 ++- internal/dataframe/compiler/ir/clone.go | 5 + .../compiler/ir/physical_operations.go | 39 ++ .../compiler/ir/physical_validation.go | 44 +- .../lower/child_set_selector_demand_test.go | 65 +++ .../dataframe/compiler/lower/child_sets.go | 114 ++++- .../compiler/lower/recipe_output_test.go | 12 + .../dataframe/compiler/optimize/optimize.go | 9 + internal/dataframe/compiler/recipe_paging.go | 153 +++++++ .../dataframe/compiler/recipe_paging_test.go | 60 +++ .../compiler/render/aql/collections.go | 4 +- .../compiler/render/aql/navigation_layout.go | 21 +- .../compiler/render/aql/navigation_render.go | 47 +- .../compiler/render/aql/predicates.go | 16 +- .../dataframe/compiler/render/aql/render.go | 57 ++- .../compiler/render/aql/selectors.go | 29 +- .../compiler/render/aql/validation.go | 6 +- .../compiler/selector_first_limit_test.go | 91 ++++ internal/dataframe/errors/errors.go | 9 + internal/dataframe/errors/errors_test.go | 2 +- internal/dataframe/execution/engine.go | 78 +++- internal/dataframe/execution/engine_test.go | 99 +++- .../publication/clickhouse/bundle_store.go | 8 +- .../clickhouse/clickhouse_bundle_test.go | 21 + .../dataframe/published/aggregate_plan.go | 20 +- .../published/aggregate_plan_test.go | 23 + internal/explorer/authoringv2/commands.go | 69 +++ .../explorer/authoringv2/commands_test.go | 54 +++ internal/explorer/compilation_receipt.go | 2 +- internal/explorer/lifecycle/publish.go | 39 ++ .../explorer/lifecycle/repository_publish.go | 3 + internal/explorer/lifecycle/service_test.go | 146 +++++- internal/explorer/lifecycle/types.go | 1 + internal/repositoryseed/preflight.go | 281 ++++++++++++ internal/repositoryseed/preflight_test.go | 105 +++++ internal/server/config_test.go | 35 +- internal/server/dataframe_query_error_test.go | 70 +++ internal/server/explorer_preview_response.go | 22 +- .../server/explorer_preview_response_test.go | 31 +- internal/server/local_workspace_writeback.go | 73 +++ .../server/local_workspace_writeback_test.go | 56 +++ internal/server/openapi_data_routes.go | 29 ++ .../server/openapi_explorer_direct_test.go | 25 + .../server/openapi_route_inventory_test.go | 5 +- internal/server/options.go | 54 ++- internal/server/server.go | 47 +- internal/store/arango/client.go | 24 + .../store/arango/client_queryrows_test.go | 35 ++ openapi/openapi.yaml | 20 +- scripts/demo-browser-smoke.sh | 46 +- scripts/demo-down.sh | 5 +- scripts/demo-smoke.sh | 28 +- scripts/demo-up.sh | 40 +- scripts/loom-repo-up.sh | 27 ++ scripts/release-loom-ui.sh | 3 +- scripts/tests/test_demo_configuration.py | 174 +++++++ scripts/tests/test_release_loom_ui.py | 7 + ui/apps/demo/nginx.conf | 4 + ui/apps/demo/src/main.tsx | 4 +- ui/package-lock.json | 419 +++++++++++++++++ ui/packages/loom-ui/README.md | 10 + ui/packages/loom-ui/package.json | 20 +- .../loom-ui/scripts/check-boundaries.mjs | 5 +- ui/packages/loom-ui/src/Viewer.tsx | 245 ++++++---- ui/packages/loom-ui/src/Viewer.unit.test.tsx | 203 +++++++++ ui/packages/loom-ui/src/api.ts | 186 +++++++- ui/packages/loom-ui/src/api.unit.test.ts | 44 ++ ui/packages/loom-ui/src/builder-entry.ts | 8 + ...lderWorkspace.reconciliation.unit.test.tsx | 118 ++++- .../ExplorerBuilder/BuilderWorkspace.tsx | 68 ++- .../ExplorerBuilder/authoring/model.ts | 2 + .../ExplorerBuilder/authoring/reducer.ts | 6 +- .../ExplorerBuilder/authoring/routeActions.ts | 36 +- .../authoring/routeActions.unit.test.ts | 104 ++++- .../components/BuilderToolbar.tsx | 15 +- .../components/BuilderToolbar.unit.test.tsx | 34 +- .../components/ColumnSelector.tsx | 10 +- .../components/ColumnSelector.unit.test.tsx | 63 ++- .../components/GuidedGraphWorkspace.tsx | 141 +++++- .../GuidedGraphWorkspace.unit.test.tsx | 126 +++++- .../components/RouteExtensionPanel.tsx | 44 +- .../RouteExtensionPanel.unit.test.tsx | 97 +++- .../ExplorerViewer/OutputTable.unit.test.tsx | 72 +++ .../features/ExplorerViewer/ViewerChrome.tsx | 53 +++ .../features/ExplorerViewer/components.tsx | 309 +++++++++++++ .../ExplorerViewer/components.unit.test.ts | 20 + .../src/features/ExplorerViewer/index.ts | 5 + .../src/features/ExplorerViewer/model.ts | 101 +++++ .../src/features/ExplorerViewer/reducer.ts | 101 +++++ .../ExplorerViewer/reducer.unit.test.ts | 58 +++ .../features/ExplorerViewer/serialization.ts | 112 +++++ .../ExplorerViewer/serialization.unit.test.ts | 66 +++ ui/packages/loom-ui/src/index.ts | 6 +- ui/packages/loom-ui/src/react.tsx | 25 +- ui/packages/loom-ui/src/react.unit.test.tsx | 16 + ui/packages/loom-ui/src/styles.css | 148 ++++-- ui/packages/loom-ui/src/types.ts | 1 + ui/packages/loom-ui/src/types.unit.test.ts | 23 +- ui/packages/loom-ui/src/viewer-entry.ts | 8 + ui/packages/loom-ui/tsconfig.json | 3 +- ui/packages/loom-ui/tsconfig.test.json | 10 + ui/packages/loom-ui/vite.config.ts | 14 +- ui/packages/loom-ui/vitest.setup.ts | 28 ++ 122 files changed, 6914 insertions(+), 473 deletions(-) create mode 100644 cmd/loom-acceptance/main_test.go create mode 100644 cmd/loom-repository/main.go create mode 100644 cmd/loom-repository/main_test.go create mode 100644 docs/DEMO_CONFIGURATION.md create mode 100644 docs/LOCAL_REPOSITORY_DEMO.md create mode 100644 internal/dataframe/compiler/lower/child_set_selector_demand_test.go create mode 100644 internal/dataframe/compiler/recipe_paging.go create mode 100644 internal/dataframe/compiler/recipe_paging_test.go create mode 100644 internal/dataframe/compiler/selector_first_limit_test.go create mode 100644 internal/repositoryseed/preflight.go create mode 100644 internal/repositoryseed/preflight_test.go create mode 100644 internal/server/dataframe_query_error_test.go create mode 100644 internal/server/local_workspace_writeback.go create mode 100644 internal/server/local_workspace_writeback_test.go create mode 100755 scripts/loom-repo-up.sh create mode 100644 scripts/tests/test_demo_configuration.py create mode 100644 ui/packages/loom-ui/README.md create mode 100644 ui/packages/loom-ui/src/Viewer.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/builder-entry.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/OutputTable.unit.test.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/ViewerChrome.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/components.tsx create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/components.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/index.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/model.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/reducer.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/reducer.unit.test.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/serialization.ts create mode 100644 ui/packages/loom-ui/src/features/ExplorerViewer/serialization.unit.test.ts create mode 100644 ui/packages/loom-ui/src/viewer-entry.ts create mode 100644 ui/packages/loom-ui/tsconfig.test.json diff --git a/Dockerfile b/Dockerfile index 6021bd50..d0b4fded 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,7 +41,6 @@ RUN --mount=type=cache,target=/go/pkg/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 && \ diff --git a/Makefile b/Makefile index a308c06d..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 acceptance-real acceptance-performance demo-up demo-down demo-smoke demo-browser-smoke release-ui +.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 @@ -131,5 +131,8 @@ demo-smoke: 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 9791ac0d..0a97384f 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ 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 diff --git a/cmd/loom-acceptance/main.go b/cmd/loom-acceptance/main.go index d5ae09be..dbf63301 100644 --- a/cmd/loom-acceptance/main.go +++ b/cmd/loom-acceptance/main.go @@ -2,16 +2,34 @@ 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") @@ -36,8 +54,29 @@ func main() { 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") @@ -112,6 +151,73 @@ func sumCounts(counts map[string]int) int { } 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...) 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 index a9fd468c..35c93b73 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,4 @@ -name: loom-demo +name: ${LOOM_COMPOSE_PROJECT_NAME:-loom-demo} services: arangodb: @@ -29,7 +29,7 @@ services: - clickhouse_data:/var/lib/clickhouse loom-api: - image: loom-demo-api:local + image: ${LOOM_API_IMAGE:-loom-demo-api:local} build: context: . command: @@ -52,6 +52,8 @@ services: - 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 @@ -63,12 +65,14 @@ services: timeout: 5s retries: 30 ports: - - "127.0.0.1:8080:8080" + - target: 8080 + published: ${LOOM_API_PORT:-8080} + host_ip: ${LOOM_API_HOST:-127.0.0.1} volumes: - - ./testdata/acceptance/ncpi-tcga-brca/recipe.json:/app/testdata/recipe.json:ro + - ${LOOM_DEMO_FIXTURE_DIR:-./testdata/acceptance/ncpi-tcga-brca}:/app/testdata:ro demo-seed: - image: loom-demo-api:local + image: ${LOOM_API_IMAGE:-loom-demo-api:local} entrypoint: ["/app/loom-acceptance"] command: - --run-id @@ -94,14 +98,14 @@ services: - --artifacts - /var/lib/loom/artifacts - --project - - NCPI_ACCEPTANCE + - ${LOOM_DEMO_PROJECT:-NCPI_ACCEPTANCE} - --generation - - tcga-brca-locked + - ${LOOM_DEMO_GENERATION:-tcga-brca-locked} depends_on: loom-api: condition: service_healthy volumes: - - ./testdata/acceptance/ncpi-tcga-brca:/app/testdata:ro + - ${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" @@ -114,7 +118,9 @@ services: loom-api: condition: service_healthy ports: - - "127.0.0.1:3080:8080" + - target: 8080 + published: ${LOOM_UI_PORT:-3080} + host_ip: ${LOOM_UI_HOST:-127.0.0.1} volumes: arangodb_data: 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 index e2e4d556..1c8c1c5f 100644 --- a/docs/DEMO_QUICKSTART.md +++ b/docs/DEMO_QUICKSTART.md @@ -24,6 +24,20 @@ Open [the standalone Explorer](http://127.0.0.1:3080). The page starts with the 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: @@ -32,7 +46,10 @@ Run the smoke check: make demo-smoke ``` -This checks Loom readiness, the seeded Explorer state, and the UI document. +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: @@ -51,7 +68,7 @@ The seed job has already verified these deeper contracts: - The GraphQL rows match the locked row digest. - Publishing the same workspace twice reuses the same execution. -To inspect a service, run: +To inspect a service in the default project, run: ```bash docker compose logs loom-api @@ -74,6 +91,7 @@ To remove the demo data and force a clean seed on the next run, remove the Compo ``` 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 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/frontend/explorer-v2-capability-authoring.md b/docs/frontend/explorer-v2-capability-authoring.md index 85ea0c7b..3f93ca38 100644 --- a/docs/frontend/explorer-v2-capability-authoring.md +++ b/docs/frontend/explorer-v2-capability-authoring.md @@ -127,7 +127,7 @@ compile it, persist an immutable receipt, and return: "snapshotToken": "sha256:...", "generation": "generation-...", "intentDigest": "sha256:...", - "compilerVersion": "loom.explorer.compiler/v9+authoring-v2-native-5", + "compilerVersion": "loom.explorer.compiler/v10+authoring-v2-native-5", "outputs": [], "diagnostics": [] } diff --git a/generated/loomapi/api.gen.go b/generated/loomapi/api.gen.go index 1d8f10de..0e4910eb 100644 --- a/generated/loomapi/api.gen.go +++ b/generated/loomapi/api.gen.go @@ -107,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. @@ -145,6 +146,8 @@ func (e AuthoringCommandType) Valid() bool { return true case UPDATECOLUMN: return true + case UPDATEROUTEEDGE: + return true default: return false } @@ -945,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"` @@ -1341,6 +1350,9 @@ 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 @@ -1441,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 { @@ -2199,6 +2264,8 @@ func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, option router.Get(options.BaseURL+"/readyz", wrapper.GetReadiness) + router.Get(options.BaseURL+"/api/v1/datasets/:project/generations/:generation", wrapper.GetDatasetGenerationStatus) + router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation", wrapper.CreateDatasetGeneration) router.Post(options.BaseURL+"/api/v1/datasets/:project/generations/:generation/activate", wrapper.ActivateDatasetGeneration) @@ -2333,6 +2400,83 @@ func (response GetRecipeExecution404JSONResponse) VisitGetRecipeExecutionRespons return ctx.JSON(&response) } +type GetDatasetGenerationStatusRequestObject struct { + Project Project `json:"project"` + Generation Generation `json:"generation"` + Params GetDatasetGenerationStatusParams +} + +type GetDatasetGenerationStatusResponseObject interface { + VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error +} + +type GetDatasetGenerationStatus200JSONResponse RawJSON + +func (response GetDatasetGenerationStatus200JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(200) + + return ctx.JSON(&response) +} + +type GetDatasetGenerationStatus400JSONResponse struct{ ServiceBadRequestJSONResponse } + +func (response GetDatasetGenerationStatus400JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(400) + + return ctx.JSON(&response) +} + +type GetDatasetGenerationStatus401JSONResponse struct { + ServiceUnauthorizedJSONResponse +} + +func (response GetDatasetGenerationStatus401JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(401) + + return ctx.JSON(&response) +} + +type GetDatasetGenerationStatus403JSONResponse struct{ ServiceForbiddenJSONResponse } + +func (response GetDatasetGenerationStatus403JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(403) + + return ctx.JSON(&response) +} + +type GetDatasetGenerationStatus404JSONResponse struct{ ServiceNotFoundJSONResponse } + +func (response GetDatasetGenerationStatus404JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(404) + + return ctx.JSON(&response) +} + +type GetDatasetGenerationStatus500JSONResponse struct { + ServiceInternalErrorJSONResponse +} + +func (response GetDatasetGenerationStatus500JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(500) + + return ctx.JSON(&response) +} + +type GetDatasetGenerationStatus503JSONResponse struct{ ServiceUnavailableJSONResponse } + +func (response GetDatasetGenerationStatus503JSONResponse) VisitGetDatasetGenerationStatusResponse(ctx fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "application/json") + ctx.Status(503) + + return ctx.JSON(&response) +} + type CreateDatasetGenerationRequestObject struct { Project Project `json:"project"` Generation Generation `json:"generation"` @@ -3708,6 +3852,9 @@ 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) @@ -3810,6 +3957,35 @@ func (sh *strictHandler) GetRecipeExecution(ctx fiber.Ctx, id string) error { return nil } +// GetDatasetGenerationStatus operation middleware +func (sh *strictHandler) GetDatasetGenerationStatus(ctx fiber.Ctx, project Project, generation Generation, params GetDatasetGenerationStatusParams) error { + var request GetDatasetGenerationStatusRequestObject + + request.Project = project + request.Generation = generation + request.Params = params + + handler := func(ctx fiber.Ctx, request interface{}) (interface{}, error) { + return sh.ssi.GetDatasetGenerationStatus(ctx.Context(), request.(GetDatasetGenerationStatusRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDatasetGenerationStatus") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetDatasetGenerationStatusResponseObject); ok { + if err := validResponse.VisitGetDatasetGenerationStatusResponse(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 @@ -4414,103 +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{ - "7D1rb9w4kn+F0C0wwEF2OzPZA9b3ybGdxAfH9radmdsb5Axaqu7mRiI1JGW7E/i/H/iSKLWe7e62Zy+f", - "ErX5qCpWFauKxeL3IGJpxihQKYLD70GGOU5BAtdfR7lcTEGwnEdwheVC/RaDiDjJJGE0OAwu9X9wgo5x", - "ssw44rY1EhHLAOUCYiQZwrlcME6+AYpzju8SQKePWcI4cJTmEqsxxH4QBkSN+UcOfBmEAcUpBIeB6nvr", - "xr3NFBhhIKIFpFjBI5eZaiUkJ3QePD2FgRv6LFZ/10PaXnZEKBuEAYc/csIhDg4lz8EfOSX0HOhcof0m", - "bJjnA1Dg2BCicZ552eA581xx9k+IZMskmf3r+jM8qa4iY1RAsepM/e0djqfwRw5Czx0xKoHq/+IsS0ik", - "MZv8Uxj8y9n+wmEWHAb/NilZa2L+KiannDM+tbOZuasMdbMA9F/XlxcoZlGeApWICJTiZMZ4CnGIFBSY", - "UIFy+pWyB4pmBJJYhIhxxFIiBXJkcExH54jQLJf7wVNY4nacEKDyOGECXgbJSAOAIkwjSCBGGYd7Ag8I", - "HiHKVTt0BzPGAakJElC/1DBgdJaQaIdgn6VKWJX0RjjDdyQhcqnoziECkkkkJJag1ktInIBZkkySlAhJ", - "IhRzPJOTLL9z4Nn20QLTOcRV5N4zfkfiGOhuF0WxDFCpZtBLQmhEMpyoVaJMIg44VlilLCazJZILIkpF", - "ZgWxiscHLOEBL29ICizf4VJdrXATPEYAMcRIyYgAfg98T5AYUAw4TgiFKuBnVAKnONEz7Q7uI4pyCo8Z", - "RNKX4BBlwAUREmhkGMuJywyTJOeAWBTlnNfZ6ILJ9yyn8W65yLFEqPgmJjGWEGoxJolhfCswRkLkApSc", - "2J1iT2QQkRmJPPVVbKoxA4EUI8IjETVOu8LLhOH4hrFzzOewW4zvFJE9JcZB5Im0PCeQXABK8SNJ8xS5", - "rQYJ8q3GczeMfcJ0adWx2C0K3MzqIZElmFZQMFLzkyilai9iQqKMJSRaVnH5TPE9JonSljuUnnLzKzV0", - "6OlnxiFEKZbACU7IN8WiOJLk3upj4PfEytcdjr4CjZU2z0tU6jhmnEUgxG6x9FZLgXePExIbm+Eul1q9", - "PTD+VWQ4MmKXYhqHiOUyy43I+aJINMR604IUa9WfJEtEqB5W4/uB42zx9/MtWENT/KDgbkPSTow+3txc", - "FRgDvYdE2daMI8kxFRnjEpV2O8IcSoPJR2A7Kr0DBwd/uQkpbQ2xs2wwooynOCkQLXRDxPJENVObapxH", - "VTy2w3YD16IwSw2IShkrMDFXng7jhhu19TBjvMRcw//x5tN5DV4Jj3KykGlSBbTBRK+CdKo2ALU33HH2", - "IICjz2cFaHquc5jjaLkFnjUDDxLSRDctLSRPaAsG1eYUEcLsc1Z5lfa6mW3jpu4IJArocaLMv6XZe4Wx", - "ezMmiGR8iWLIErbUjFGxalUzIhChEaPGfvHx2ryVOwKxHlsXR0rEOmxcM9V2lMpANKaAY8U4yh409qH6", - "kv6qWY1TGpXJ0oN+4/bhOoxV4aQ99kAhLv8YMToj89wYh+gBG/tvpoD28NiGtTF4DQoh8PZV38qwP2g0", - "E8ACfJuDGIQcmkZi6haHQ3Iben+dFSvsCwWsJ/rKilhSqdAzZoSxTZRZYqXqDiyVIK4YYrFxapRzLBZ2", - "u7s25tgWdLgdeYw9mWG5CJEOzYUozRNJMsxlqO0uDfvFiTbB7nGS6xiAU+pKAXrGlJ164xp9KEpeDMNM", - "W7WApfHVci4Yd19uYapRC4uAQA9ELrTOsdubj+bmFfxQPLs0fIKjr6JQ8MwHXrfTOt7Iqx/N9RHbjtof", - "zJeVKIFxyxCoXkUc4D9RDBKTxNjDHGJsIgo0RhHjHBJNk7ulQ/yswp4b3xfGLFu5FDGWWIAMURlJDp0S", - "1W4dyaC0MY2KZbmEcuFWNwwLyWdaxOR3j+VRyZhKoiIOsfrEdrXwnVDatFlzbGOrW0MfOurZRVGuNZ5D", - "iAidgzC/eUpDL41Wnl2OdYHhNva5NTiwaz9TbrZizypvenu8pZTaF4W3+1nUoLamIs+UHwvxJ4gJvtHu", - "z44RPzaT7anJnVlSgKU0hTaF8yxhOC7jdfv6iMbOpU9Prs5+VbaoARFongaHvwcJY+l+pM/I9hmfT9wR", - "1F4R5Zvc/xx8WTmcCYOjyJ0w4Tgm5rTtiqv5JQERHM5wIiAMMu8nRbgkT6n+L5GQij4amUmOdS81qYUC", - "c46X6ntGErjQ503fVyGURBo+7Txmcj/0n3eV51i/m7+6KUrysDt95lWQx0K+DpEGAA6Pigk+Ao6BNx85", - "+iDbYRuBzbJkeWwiUsIz6kYBrXubs80euG3bEWxQni3pnmqQlNAz0/fNKl+4PfiE45k8IXOLTxMFy3ae", - "eMwYT7EMDgNC5X+8DfRsJFUic1BMRqiEOXA1jKA4Ewsmb9hXoGNZqaRcfaAW+DwCDlhMq1i2t5oxwXPK", - "hCTR8AU9Kfo0SXVcXbW++Qev3ZumtTNHAsNBt5Sd6m5N0BdOWN9IvxUNu1iiHK6GapVOJSLVFWnkkEKc", - "imD8SP7AGfEo3im65baj9DVgmXMYLvDvXYenMPhKjOHr9i7n9q6io6Bs2rOK3bG63G7Au5wkSpWGgcjn", - "1lxS9LQHL+p/xhH2RVARPmI0IpWNoL67lOxhBzsnKalxnT2JCg7fHBwc9LFtddga/3jLY8lWwb0OhLcu", - "3exite9IXeLOGgfuDQM3P9PwV+XW94ussx8gng8Dg1Ci7P4rDsrkL5JpHKfcHL07Pw3C4P3Z+c3pNAiD", - "449H05tmnjOOH42GTWyOg0Y1rXHREGPHY0fMgcrLcUBaV5ww+onFveS/qrZWvMqYvGDxsMmMx3g5nC5j", - "7T63qMfT06Ob01u3tiefr87Pjiu/nJ6fep/T04ujT/7n5fTkdGq+r4MwuD69MR+308vLmyAMjk5ObqeX", - "n29M60+Xv54Wn+pPx5fnnz9dBGHw+epETVt827b2+8sg27RTkN97aniEJMeQgASndj2D6o6xBLCRMB1f", - "Y/R4gbnRb+1t3pPEZfStNlKmvTGiWxs8Qtw5hFhg3tmkRrg6XCvY1IesAVEFOqzTq2lJ3pkt51piCTva", - "gyMsccLmvUrTNlu1x55hgTVaz237eoU2Tao1ITOIllECBfncEBenv2m5OTr5R2PPio2Gk+RyFhz+Ptha", - "+xIGNE9sqEfyHIbsvzVge4w53+pzC9bEP8duc70GzKPFer4bHaqKTQZsEws8ywOqez0WnkH4ruXerC86", - "dvYR7oJZvALuRn9n4x5Uj0iVVPTs3CY5GcwZz1r/JnFp4YnKGvR7OselrhvDHn4M9TpiGQx2RrfEITbX", - "Flr203g+fr7TeN441bySPd6DrlqV0TNfWCuwPjMHwZJ7iK918479Rp8lXOnktt7sGa/pc9m0kjbfyiOr", - "rFssnqOXW7EaM/todTBzyShb9cOUxeMC/assF8MM54m8eqYjoJPk3Q2OHohm2spqBynBd5AMGCdhcxLh", - "5GZI1HeEBqz6RMOFYpUoq3KRAZbmOKzHgvXX2NOZJZkdmapk8Oao0Nlng1UM27igg3e10hnHtoNd9hln", - "6WCnciizZCzLkzbih4FkA2esexoGqQrQ3mjlKpXzd1D1wsreGFfO5uQds9ycY4202YeLRTcB3YHwQGnk", - "7OEDx4S2bAsPU8bkaULmpEVL1JahEJAKGKsj9a6DkpJ6rGjEajBuT3C6iV4ENjZ3hNWIzVqHVooEvQbA", - "Cp0qEb8MSwmcBofB//5+tPc/eO/b7Rf7n4O9v91++fe/BK37Qt/Uxkevz73mnvHcIJ9htmFhy2vTVi2y", - "2/u6Ot2oRlU0m88BSx1TAb4Arp0zrgvoR/BHZadfIUjdX9GtgzAgOv9iRoC/W14vhYRUH4pJoMpbeLf8", - "zBNtYMWEzr0W7E4AN6lTx45E75bHSk0W21jFwSwhSbGMmmFcDXo6aH89Ov9sosHTax3nOz8PwuDk7Prm", - "7OL4pj9gp7Fvprd/1rTuKXIvt3YGoVd1Db5r+0stoGqCnyasehKE7vvj0cUH/a2Dn7dHJyf6y4Q37eeX", - "Z2gvnca448iAOzoafuRn/AKSAPcm3PlR6yhvz9xJGewL9wQhpub2T/OhiWbA4UjascwhQbMNrRu8TCyj", - "nHzVOaw4lRUCrzJI6J1QOgoNiIEwKjmO5PrbervPNfi0brDz9ix3rW+nq/o8LY5OEw09IRqrhc1WUTNt", - "tFnTYdPYHNH2ycxN+hU4/ykYbd1kUxACzweZ2S73tDnWC/fAbc5AnyjJIRPWw8FufNc/NGQsMWhcIuvR", - "jFX40Wo+QH9GWnOMblxuW1dWW/Vka9Bw78tOa0SEC+q1a+M+AErlyxmT07GOXS5hUDDPRA+bLKgC0AYI", - "3AzlMjXxkM7FfMfiZZ/gNQv54N173J69Gbld0Y29ElXNTB0ZX9i4pQIuo7/3jq5ewJWIi+7eiKaVhE8g", - "cYwlHn0o7iXrrp2FWrduW/NK31ePxdfTdM29Wi16b/rC9+ocp1ju3vST2gT1RStnCwv4W6hS9+7HEafd", - "/hgUm3lqhKlUyFtKANaXmnoo3pEn22Yu2WGbCO3dSltfT3ZZJs/WWk335nqgrF1h1vd1iuvsXJli1GTa", - "1y8OYxrb2y575V1yvdHoqlG1QLZTYLXpiC60gd3gFjdkKICY+pOQPI+kvoWoKDBxbQzWIYohA2punFJz", - "a0rBoEBgFGxuQ53og28fGnX6ZbhCvSzMhRE8T+Jh4d/zVkFdS9GSuDOL35arWS+tIiEpkaMTO0ckAA53", - "amtY+w5pMV8n/rtNsxhtSVc83NeQYWEJ12FOb3iFtXAUpznd5wicPVTJMMrHHJ153Mht5SpbeDwE+mMa", - "V62x2Ovjo/OjqR+MnU6P/uGFY2/dDy5q2xutDYOr8sbcWsqtdud804nAwqW/jdEB3koIm5G2CmYj7U0O", - "/Ho6cQMqqxOmneqp3WuVkg83EjZd5etnhk6V1hNDOXwttu1TMB4EJWMPD5a6ajujLEZdYOBhwQSYIizK", - "EBMLbK5rZsop4fcQo4QJkYAQyRLFuSmgpi9FR7lk98D3gyZ4KoHt7V627N9Gx+xcXSf1a5lqnsIy/b1J", - "usM6U3dBZz2dtdPraBtNpO1IMW6kUxFfG3tIQJKYG3CHHdeUgbwGJht3lq5LJhBGxYJkzWkhYyKRdZ6r", - "Ho1XhmqlYJmZOGYPShL2MLVZWKcuoXP1oES3u4Zkds5Y1tImxY/XEjIxIGpRUa2rIKzM14S0f5G9OyjQ", - "eUSy7iFINRW/Ab726ELfSQcHyZd4WC7RgNBE443/PmJVt5prUyMGqjEKSIm0xQDOGUt/EsjcUzEV+kzj", - "FGeZDVk4NdEfqhhav2BkyPXau0XzjujIxbZCZIO3rPbdpuPi/A2+WyvOsV6m08A0Mbb5q3LKphomBsTf", - "nz0iGtjLkVqoWctdGskUkCRToDHY22llNlEZM/+yPl0zQmlbHmM3gVYw/a1y/2gHnoo93hvhp7gDwafG", - "6grFDcDO05j6ycrqtb6elL2iab9vVJK0aYlXrgL2Hl10qr4G/dV/uKFTp4ZPonTLOuGWYnX8Zbdzr0hd", - "GDzuzdmem8RdEL3/ef837wZa0WaPpBkzeZ7eKwi2i9pGdO5BMCdykd/tRyydmNoyk4SxdEJsca6iyszE", - "7/2ka98QOmNaioxmCtR+Zraxo6szpT8c+wdv9g/2D+ytfoozEhwGv+wf7P9iwdD0neCMTO7fTBT3zThO", - "YVIP14vJdxI/qbZz0HgV9+SVAg0+gJzqHqeuQ1B7leDng4Nd1Hq9znVZtFmeFNVodaWitwdvdjF9rTJW", - "WUcvdLWwTAXjop6Su8yjy83KhVcO7Cfh6od55dveHvzSBl1B7Um9OKnu93Zov6J2mi6MlKcp5ktbq9OW", - "xEKpVVQaaIwKrlkpabZvr84Xb5L83vgGBul+yKNugXx5CiscK0CKyXebd/o0KVPFxOR7+fFUfx+l5SJr", - "2cTdOWk9BfKaeo+JPH0Jg4yJBik55spnODEgf6i/LgJCOrfAY9OiVuNEech77uS9pM7qnbzaayvNPsMM", - "+G1ZObH9bnlFDxdO+h2hWD/y0ntyXc2cbMsW7j/S1LCE3nMp3sCrRlK1t757/Nr00UG/QK5WEC012aCe", - "lSqBQ9XHSu1L3fFvgzsWtUFVvzd/HQFsQxU5NcbPP48Yw6+99xQGfx1B52pRTN35lzGkLuoaVjXnZ1Ny", - "DpdlVwvFXvKwqUO3hlabWCGGnam38HvjO0/FLnBbqP/bkYq9ZeSRL0i1qt8jS6lmBfxDN+xYN7wC0XQc", - "gbAnigjPJHDPqvEKgVbF1AqnL6bOZBetpvI5EfK0aPVMztvI0fRYI3p77NVguQ7ikqZa9tWVVlQvE5LM", - "7wSEtWD9uvhr6tAeo++09DbHTbDyhp+aqd1c3JCi6rOd3rx6/bjygMYL8e8g7Vh7JmOo0dNUV39zImMY", - "F2HzyA7XdkZZRH+4Jpx8Lx9s7IwgeDLyCkMHO+aaNfz1ja289vUxLdb6J4Hs6vOcSpICKi95Pkdn9tud", - "3lugFbd/ML9N/ALNE+/6YR8TviuukG2NFyuFu5oqWuszVek9V4Gpef+PeC856tIGxVnVbji24Z3HoUy7", - "+rbfUCW5+nLmUI5veRVxqBnZ+CicXq8/C99HlQK2fazfVPd2i2LQNF2jena13MuXFctirVo0Pp8hV6H1", - "JeXgWTz5p2Irr0D5bmBude2zLFnWq7eVpYdfqcndWE9+x7HL5jLoTbsRpoySCCfuCQz79gHE/galXWdM", - "EZYsJZF7vhHdYRkthhvvTU9Yv+S+tu7uNMiKb3mP84U3t+drB1cO/MWVg71kUHEvNi/MtTswOxbj+g2U", - "BgF+V3vkl80Qo1C8LmuSUP68Mrp72/PtmzHg1h94fr56ePvzGMjrrzOrAf42CvWGJ/dfVEmpzmNWvfaY", - "+2aUnH3p4OWVnAHk9ccZa/didq0nazdgmnI47KFA7L98a25L/NCOP2yfQWqhfOvkxRVDcatjNbL1Wk8i", - "6vdQdqwj6qXROp8hdeaTexYWzThLbSYXjqR72BhipK+V/NAgPzTIIA3iP6304jrEvCLgmh5XqnFvQQG0", - "vNawazXQ8oZCc2zENEUxzPSbSIwKpGtkEjpHGFEWm+i9Tm35/xcIeb2y2JpXVTzxad42f/ns0cJ0dQ+2", - "F/JoAGzOtH1eLpXLyFqYxzOLYf5775yxdM+UuN07ZmlK5Lgc3u1oDv+twH+JFNAfaQxrHGYPVBhu3rbs", - "MGsHar1t/XyEES/kb489UN9LLMLgRW4ESxLWdeJ2pFtcYxrfsceWM7ZuHD7efDp/Pk+8Hdy39XYAjcFc", - "YDA4oQ8cZ4u/nyNhkLMkmatf/0jKiyZarTaqO3OTRCdr6pZ2wOAVZD+9frVhibVBvTFEkO2s60my7dyV", - "l2JZonLlxPFZ7UXvgtP0v10yaAe4SvByzjVr/2vIoSNMViBmUnW6hO2HjP2QsULGtOQgxtH7j2fTVjlb", - "AE5kp4B9NC1e1wpbA2Hr9wGR43NkCGVrEAKNlogIRJlEHHC83F8R5IxxiSIcLXQ1Q27MkFLvmeHsIiTk", - "Hr51rcE5uQcKQryqVWhC2HI1SizA6IHIBcslihYQfVXOdEFAorN8FPqagt+6L6fimLw6AuyMDU88miHM", - "oZfvHhagy20W3BvXRyh660rn/N45nTlPgsNgEjx9efq/AAAA//8=", + "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/internal/api/bulk/load/operations.go b/internal/api/bulk/load/operations.go index 80d43c45..3ec7a840 100644 --- a/internal/api/bulk/load/operations.go +++ b/internal/api/bulk/load/operations.go @@ -10,9 +10,28 @@ import ( "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) { diff --git a/internal/api/bulk/load/operations_test.go b/internal/api/bulk/load/operations_test.go index a0da8425..b5088d9f 100644 --- a/internal/api/bulk/load/operations_test.go +++ b/internal/api/bulk/load/operations_test.go @@ -3,10 +3,13 @@ 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" ) @@ -40,6 +43,56 @@ func TestCreateDatasetGenerationStagesCompleteBundle(t *testing.T) { } } +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 diff --git a/internal/api/bulk/load/service.go b/internal/api/bulk/load/service.go index bbcd26f5..fc3e4124 100644 --- a/internal/api/bulk/load/service.go +++ b/internal/api/bulk/load/service.go @@ -30,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 @@ -96,6 +103,27 @@ func NewService(cfg ServiceConfig) (*Service, error) { }, nil } +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 s.generationActivator == nil { + return nil, errors.New("generation manifest reader is not configured") + } + ref, err := publication.NewRef(project, generation) + if err != nil { + return nil, err + } + manifest, err := s.generationActivator.ReadManifest(ctx, ref) + if err != nil { + return nil, err + } + 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 req.Project == "" || req.Generation == "" || req.StagedDir == "" { return nil, errors.New("project, generation, and staged directory are required") diff --git a/internal/api/bulk/load/service_test.go b/internal/api/bulk/load/service_test.go index a5813a72..4026f835 100644 --- a/internal/api/bulk/load/service_test.go +++ b/internal/api/bulk/load/service_test.go @@ -115,6 +115,45 @@ 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)} diff --git a/internal/api/http/errors.go b/internal/api/http/errors.go index a1ac0ab7..ee220a32 100644 --- a/internal/api/http/errors.go +++ b/internal/api/http/errors.go @@ -110,7 +110,7 @@ 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", "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": + 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" @@ -135,7 +135,7 @@ 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 diff --git a/internal/api/http/errors_test.go b/internal/api/http/errors_test.go index 178899ba..7fccff2f 100644 --- a/internal/api/http/errors_test.go +++ b/internal/api/http/errors_test.go @@ -43,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}, } { diff --git a/internal/dataframe/compiler/generic_physical_plan_test.go b/internal/dataframe/compiler/generic_physical_plan_test.go index 5ca7f275..27678b4f 100644 --- a/internal/dataframe/compiler/generic_physical_plan_test.go +++ b/internal/dataframe/compiler/generic_physical_plan_test.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -106,6 +107,73 @@ 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.OutputPlan{Root: semantic.SemanticNode{ @@ -133,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) diff --git a/internal/dataframe/compiler/ir/clone.go b/internal/dataframe/compiler/ir/clone.go index 33312f5f..cdb59abb 100644 --- a/internal/dataframe/compiler/ir/clone.go +++ b/internal/dataframe/compiler/ir/clone.go @@ -203,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...) diff --git a/internal/dataframe/compiler/ir/physical_operations.go b/internal/dataframe/compiler/ir/physical_operations.go index 3051b9d9..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 diff --git a/internal/dataframe/compiler/ir/physical_validation.go b/internal/dataframe/compiler/ir/physical_validation.go index 0f0161d3..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) { 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 59713040..6f9f3d4c 100644 --- a/internal/dataframe/compiler/lower/child_sets.go +++ b/internal/dataframe/compiler/lower/child_sets.go @@ -315,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 } @@ -330,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 @@ -375,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 @@ -393,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 { @@ -404,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) } @@ -423,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/recipe_output_test.go b/internal/dataframe/compiler/lower/recipe_output_test.go index f78840ca..37f84a6a 100644 --- a/internal/dataframe/compiler/lower/recipe_output_test.go +++ b/internal/dataframe/compiler/lower/recipe_output_test.go @@ -241,6 +241,18 @@ func TestTraversalColumnNamingAliasKeepsNestedPublicColumnsGloballyScoped(t *tes 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) { diff --git a/internal/dataframe/compiler/optimize/optimize.go b/internal/dataframe/compiler/optimize/optimize.go index 96eb10e6..7a93c57e 100644 --- a/internal/dataframe/compiler/optimize/optimize.go +++ b/internal/dataframe/compiler/optimize/optimize.go @@ -182,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. @@ -222,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 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 a7bbbaf7..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 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/validation.go b/internal/dataframe/compiler/render/aql/validation.go index 9053005a..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 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/errors/errors.go b/internal/dataframe/errors/errors.go index 066c38dd..3e1b67c7 100644 --- a/internal/dataframe/errors/errors.go +++ b/internal/dataframe/errors/errors.go @@ -63,6 +63,9 @@ const ( 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 @@ -356,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 458cc85f..a58667a6 100644 --- a/internal/dataframe/errors/errors_test.go +++ b/internal/dataframe/errors/errors_test.go @@ -23,7 +23,7 @@ func TestErrorCodesAreUniqueAndStable(t *testing.T) { CodePublicationLeaseLost, CodePublicationFailed, CodeOutputEncodingFailed, 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/execution/engine.go b/internal/dataframe/execution/engine.go index 7e64e2cc..434f2892 100644 --- a/internal/dataframe/execution/engine.go +++ b/internal/dataframe/execution/engine.go @@ -64,6 +64,9 @@ type Config struct { 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 { @@ -73,6 +76,7 @@ type Engine struct { queryRows QueryRows scopeDigest func(recipe.RuntimeBindings) string batchSize int + rootPageRows int } // Resolved contains the immutable recipe after schema discovery, semantic @@ -116,6 +120,8 @@ type OutputStream struct { bindVars map[string]any stream QueryRows batchSize int + rootPageRows int + page *compiler.CompiledOutputPage } type DynamicColumnCheck struct { @@ -141,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) { @@ -427,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}) } @@ -528,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 } @@ -545,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 @@ -633,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 @@ -648,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/execution/engine_test.go b/internal/dataframe/execution/engine_test.go index 60266b06..e2438496 100644 --- a/internal/dataframe/execution/engine_test.go +++ b/internal/dataframe/execution/engine_test.go @@ -159,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) } @@ -192,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/publication/clickhouse/bundle_store.go b/internal/dataframe/publication/clickhouse/bundle_store.go index faed92ef..c3c7c203 100644 --- a/internal/dataframe/publication/clickhouse/bundle_store.go +++ b/internal/dataframe/publication/clickhouse/bundle_store.go @@ -110,7 +110,13 @@ func (s *ClickHouseBundleStore) beginBundle(ctx context.Context, identity public case publication.BundlePublished, publication.BundleReady: 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)) diff --git a/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go b/internal/dataframe/publication/clickhouse/clickhouse_bundle_test.go index 57a62a7d..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" @@ -257,6 +258,26 @@ func TestClickHouseBundleStoreImplementsPublicationTargetDirectly(t *testing.T) } } +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() diff --git a/internal/dataframe/published/aggregate_plan.go b/internal/dataframe/published/aggregate_plan.go index a1ed4b9b..8241b72a 100644 --- a/internal/dataframe/published/aggregate_plan.go +++ b/internal/dataframe/published/aggregate_plan.go @@ -87,12 +87,22 @@ func buildAggregatePlan(dataset Materialization, req AggregateBatchRequest) aggr 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}) diff --git a/internal/dataframe/published/aggregate_plan_test.go b/internal/dataframe/published/aggregate_plan_test.go index c3ec87b3..5969eb05 100644 --- a/internal/dataframe/published/aggregate_plan_test.go +++ b/internal/dataframe/published/aggregate_plan_test.go @@ -130,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"}} 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/compilation_receipt.go b/internal/explorer/compilation_receipt.go index 8904e19a..1220ddff 100644 --- a/internal/explorer/compilation_receipt.go +++ b/internal/explorer/compilation_receipt.go @@ -23,7 +23,7 @@ const ( CompilationReceiptFormatVersion = 3 // CompilationReceiptCompilerContractVersion changes when compilation // semantics change in a way that can alter a resolved receipt. - CompilationReceiptCompilerContractVersion = "loom.explorer.compiler/v9" + 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. diff --git a/internal/explorer/lifecycle/publish.go b/internal/explorer/lifecycle/publish.go index cadaf903..c2abf4a5 100644 --- a/internal/explorer/lifecycle/publish.go +++ b/internal/explorer/lifecycle/publish.go @@ -4,6 +4,7 @@ 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" @@ -53,6 +54,20 @@ func (s *Service) Publish(ctx context.Context, request PublishRequest) (PublishR 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 { @@ -69,9 +84,33 @@ func (s *Service) Publish(ctx context.Context, request PublishRequest) (PublishR 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) diff --git a/internal/explorer/lifecycle/repository_publish.go b/internal/explorer/lifecycle/repository_publish.go index 3f4cb34d..22633f24 100644 --- a/internal/explorer/lifecycle/repository_publish.go +++ b/internal/explorer/lifecycle/repository_publish.go @@ -50,6 +50,9 @@ func (s *Service) PublishRepository(ctx context.Context, request RepositoryPubli 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 { diff --git a/internal/explorer/lifecycle/service_test.go b/internal/explorer/lifecycle/service_test.go index 4d2d781a..7daad49a 100644 --- a/internal/explorer/lifecycle/service_test.go +++ b/internal/explorer/lifecycle/service_test.go @@ -13,6 +13,7 @@ import ( "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" @@ -276,12 +277,19 @@ func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { 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]"; got != want { + 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 { @@ -296,6 +304,18 @@ func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { 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") } @@ -306,6 +326,130 @@ func TestPublishCommitsReleaseAndRevisionTogether(t *testing.T) { } } +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 }}) diff --git a/internal/explorer/lifecycle/types.go b/internal/explorer/lifecycle/types.go index 85bf6281..49fd1c2b 100644 --- a/internal/explorer/lifecycle/types.go +++ b/internal/explorer/lifecycle/types.go @@ -91,6 +91,7 @@ type Config struct { ValidateReleaseGeneration GenerationValidator ActivateRelease ReleaseActivator PrepareRelease ReleasePreparer + PersistPublishedWorkspace func(context.Context, string, string, []byte) error Now func() time.Time } 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 2f4098b7..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 { 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/explorer_preview_response.go b/internal/server/explorer_preview_response.go index 16aa8bc4..e8bcce6e 100644 --- a/internal/server/explorer_preview_response.go +++ b/internal/server/explorer_preview_response.go @@ -21,11 +21,22 @@ 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 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. @@ -51,6 +62,8 @@ func previewRouteError(err error) error { 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 &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 receiptPreviewConflict(err) default: @@ -64,6 +77,11 @@ func previewRouteError(err error) error { } 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{ @@ -71,7 +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("", cause), + Details: receiptMismatchDetails(receiptID, cause), }, Cause: cause, } diff --git a/internal/server/explorer_preview_response_test.go b/internal/server/explorer_preview_response_test.go index 6e1c3893..000faeb5 100644 --- a/internal/server/explorer_preview_response_test.go +++ b/internal/server/explorer_preview_response_test.go @@ -63,7 +63,10 @@ func TestPreviewErrorPreservesStableClassifications(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: contractMismatch("output_execution", "patients", "private-expected", "private-actual")}, 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 { @@ -75,3 +78,29 @@ func TestPreviewErrorPreservesStableClassifications(t *testing.T) { }) } } + +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/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 99cf579c..bf6aca70 100644 --- a/internal/server/openapi_data_routes.go +++ b/internal/server/openapi_data_routes.go @@ -52,6 +52,35 @@ func (r *HTTPRoutes) GetReadiness(ctx context.Context, _ loomapi.GetReadinessReq return nil, unexpectedResponseStatus("getReadiness", status) } +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.GetDatasetGenerationStatus400JSONResponse{ServiceBadRequestJSONResponse: loomapi.ServiceBadRequestJSONResponse(body)}, nil + case http.StatusUnauthorized: + return loomapi.GetDatasetGenerationStatus401JSONResponse{ServiceUnauthorizedJSONResponse: loomapi.ServiceUnauthorizedJSONResponse(body)}, nil + case http.StatusForbidden: + return loomapi.GetDatasetGenerationStatus403JSONResponse{ServiceForbiddenJSONResponse: loomapi.ServiceForbiddenJSONResponse(body)}, nil + case http.StatusNotFound: + return loomapi.GetDatasetGenerationStatus404JSONResponse{ServiceNotFoundJSONResponse: loomapi.ServiceNotFoundJSONResponse(body)}, nil + case http.StatusInternalServerError: + return loomapi.GetDatasetGenerationStatus500JSONResponse{ServiceInternalErrorJSONResponse: loomapi.ServiceInternalErrorJSONResponse(body)}, nil + case http.StatusServiceUnavailable: + return loomapi.GetDatasetGenerationStatus503JSONResponse{ServiceUnavailableJSONResponse: loomapi.ServiceUnavailableJSONResponse(body)}, nil + default: + return nil, unexpectedResponseStatus("getDatasetGenerationStatus", status) + } +} + 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) diff --git a/internal/server/openapi_explorer_direct_test.go b/internal/server/openapi_explorer_direct_test.go index 747c2b33..0bf9867a 100644 --- a/internal/server/openapi_explorer_direct_test.go +++ b/internal/server/openapi_explorer_direct_test.go @@ -1,9 +1,12 @@ 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) { @@ -15,3 +18,25 @@ func TestLegacyErrorMessagePreservesStructuredLifecycleMessage(t *testing.T) { 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_route_inventory_test.go b/internal/server/openapi_route_inventory_test.go index 8756c381..dce8a1d0 100644 --- a/internal/server/openapi_route_inventory_test.go +++ b/internal/server/openapi_route_inventory_test.go @@ -47,8 +47,8 @@ func TestGeneratedRoutesExactlyMatchOpenAPISpec(t *testing.T) { t.Errorf("registered route is absent from OpenAPI: %s", route) } } - if len(got) != 21 || len(want) != 21 { - t.Errorf("route count got=%d spec=%d, want 21", 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)) } } @@ -118,6 +118,7 @@ func TestOpenAPIDocumentsOperationSpecificFailureStatuses(t *testing.T) { } 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/options.go b/internal/server/options.go index 224e2eb1..4b1a721a 100644 --- a/internal/server/options.go +++ b/internal/server/options.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "errors" "flag" "fmt" "os" @@ -27,8 +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"` + LocalWorkspaceWriteback string `yaml:"local_workspace_writeback"` + LocalWorkspaceProject string `yaml:"local_workspace_project"` } type ClickHouseConfig struct { @@ -65,7 +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, + RecipeBatchRows: 1000, RecipeBatchBytes: 4 << 20, RecipeQueryPageRows: 25, }, Auth: AuthConfig{Mode: "basic", Calypr: CalyprAuthConfig{RequestTimeout: 5 * time.Second, CacheTTL: 30 * time.Second}}, } @@ -73,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") @@ -101,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 } @@ -118,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) } @@ -139,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 @@ -183,6 +200,15 @@ func applyEnvironment(cfg Config) (Config, error) { func (c Config) Validate() error { cfg := c + if cfg.Server.RecipeQueryPageRows < 0 { + return errors.New("server.recipe_query_page_rows cannot be negative") + } + 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 (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.ClickHouse.Enabled && strings.TrimSpace(cfg.Server.Dataframer.Recipe) == "" { return fmt.Errorf("server.dataframer.recipe is required when server.clickhouse.enabled is true") } diff --git a/internal/server/server.go b/internal/server/server.go index 7a2163c1..7317905f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -20,6 +20,7 @@ import ( "github.com/calypr/loom/internal/authscope" "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" @@ -69,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{ @@ -136,7 +168,7 @@ func run(ctx context.Context, serverConfig Config) error { authenticator, authorizer, scopeResolver := auth.authenticator, auth.authorizer, auth.scopeResolver 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 lifecycleClient.QueryRows(ctx, query, batch, binds, visit) + 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. @@ -183,12 +215,13 @@ 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) @@ -302,6 +335,10 @@ func run(ctx context.Context, serverConfig Config) error { compileReceipt := func(ctx context.Context, request lifecycle.CompileReceiptRequest) (*explorer.CompilationReceipt, error) { return compileExplorerReceipt(ctx, request, capabilityResolver, recipeEngine, explorerService, logger) } + 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: lifecycle.CapabilityResolver{ @@ -322,7 +359,8 @@ func run(ctx context.Context, serverConfig Config) error { } resolved, err := compileValidatedReceiptResolution(ctx, recipeEngine, receipt, bindings) if err != nil { - return dataframeexecution.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 { @@ -334,6 +372,7 @@ func run(ctx context.Context, serverConfig Config) error { 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 { diff --git a/internal/store/arango/client.go b/internal/store/arango/client.go index e0ba2eb6..c927594a 100644 --- a/internal/store/arango/client.go +++ b/internal/store/arango/client.go @@ -50,6 +50,30 @@ type RowBatchClient interface { 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 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/openapi.yaml b/openapi/openapi.yaml index 3c79078f..68839943 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -226,6 +226,24 @@ paths: parameters: - $ref: '#/components/parameters/Project' - $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: 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'} + '404': {$ref: '#/components/responses/ServiceNotFound'} + '500': {$ref: '#/components/responses/ServiceInternalError'} + '503': {$ref: '#/components/responses/ServiceUnavailable'} post: operationId: createDatasetGeneration summary: Upload a multipart dataset generation. @@ -908,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} diff --git a/scripts/demo-browser-smoke.sh b/scripts/demo-browser-smoke.sh index f5c17962..5a92b53a 100755 --- a/scripts/demo-browser-smoke.sh +++ b/scripts/demo-browser-smoke.sh @@ -1,7 +1,15 @@ #!/usr/bin/env bash set -Eeuo pipefail -ui_url=${LOOM_DEMO_UI_URL:-http://127.0.0.1:3080} +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 @@ -52,7 +60,7 @@ dump_page() { --user-data-dir="$profile" \ --virtual-time-budget=5000 \ --dump-dom \ - "$ui_url/?mode=$mode" >"$output" 2>"$work_dir/$mode.stderr" & + "$browser_url&mode=$mode" >"$output" 2>"$work_dir/$mode.stderr" & browser_pid=$! for _ in $(seq 1 60); do @@ -91,18 +99,44 @@ assert_no_ui_error() { 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" -dump_page viewer "$viewer_dom" "TCGA-" +assert_served_graph_styles +dump_page viewer "$viewer_dom" "$expected_cell" assert_no_ui_error "$viewer_dom" -assert_contains "$viewer_dom" "TCGA-BRCA patient cohort" -assert_contains "$viewer_dom" "Patient ID" -assert_contains "$viewer_dom" "TCGA-" +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 index da37ab6a..5c855c65 100755 --- a/scripts/demo-down.sh +++ b/scripts/demo-down.sh @@ -4,4 +4,7 @@ set -Eeuo pipefail repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) cd "$repo_root" -docker compose down "$@" +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 index 51ecf852..6b1687dc 100755 --- a/scripts/demo-smoke.sh +++ b/scripts/demo-smoke.sh @@ -1,11 +1,31 @@ #!/usr/bin/env bash set -Eeuo pipefail -api_url=${LOOM_DEMO_API_URL:-http://127.0.0.1:8080} -ui_url=${LOOM_DEMO_UI_URL:-http://127.0.0.1:3080} +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 -curl -fsS "$api_url/api/v1/projects/NCPI_ACCEPTANCE/explorers/default" | grep -q 'tcga_brca_cohort' +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, seeded Explorer, and standalone UI are reachable" +echo "Loom API, expected seeded Explorer, and standalone UI are healthy" diff --git a/scripts/demo-up.sh b/scripts/demo-up.sh index d61da27e..ac08587f 100755 --- a/scripts/demo-up.sh +++ b/scripts/demo-up.sh @@ -7,25 +7,49 @@ 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; } -docker compose up --build -d arangodb clickhouse loom-api +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 http://127.0.0.1:8080/readyz >/dev/null 2>&1; then + if curl -fsS "$api_url/readyz" >/dev/null 2>&1; then break fi sleep 1 done -curl -fsS http://127.0.0.1:8080/readyz >/dev/null +curl -fsS "$api_url/readyz" >/dev/null -docker compose run --rm --no-deps demo-seed -docker compose up --build -d loom-ui +"${compose[@]}" run --rm --no-deps demo-seed +"${compose[@]}" up --build -d loom-ui for _ in $(seq 1 120); do - if curl -fsS http://127.0.0.1:3080/ >/dev/null 2>&1; then + if curl -fsS "$ui_url/" >/dev/null 2>&1; then break fi sleep 1 done -curl -fsS http://127.0.0.1:3080/ >/dev/null +curl -fsS "$ui_url/" >/dev/null -echo "Loom demo is ready at http://127.0.0.1:3080" +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/release-loom-ui.sh b/scripts/release-loom-ui.sh index 4fc221e9..989a0fed 100755 --- a/scripts/release-loom-ui.sh +++ b/scripts/release-loom-ui.sh @@ -458,8 +458,7 @@ publish() { 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 - published_version "$version" - printf 'Published and verified %s\n' "$package_name@$version" + printf 'npm accepted %s\n' "$package_name@$version" } release() { 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_release_loom_ui.py b/scripts/tests/test_release_loom_ui.py index 5844906e..fedffdd3 100644 --- a/scripts/tests/test_release_loom_ui.py +++ b/scripts/tests/test_release_loom_ui.py @@ -351,6 +351,13 @@ def test_release_pushes_before_real_publish(self) -> None: 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") diff --git a/ui/apps/demo/nginx.conf b/ui/apps/demo/nginx.conf index 82693c23..f5a4e37c 100644 --- a/ui/apps/demo/nginx.conf +++ b/ui/apps/demo/nginx.conf @@ -4,6 +4,10 @@ server { 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; diff --git a/ui/apps/demo/src/main.tsx b/ui/apps/demo/src/main.tsx index e94d1369..f5907273 100644 --- a/ui/apps/demo/src/main.tsx +++ b/ui/apps/demo/src/main.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { LoomExplorerBuilder, LoomExplorerViewer, createLoomClient } from '@calypr/loom-ui'; +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'; diff --git a/ui/package-lock.json b/ui/package-lock.json index 04898129..4ff50adb 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1017,6 +1017,64 @@ } } }, + "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", @@ -1067,6 +1125,37 @@ "@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", @@ -1486,6 +1575,39 @@ "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", @@ -2147,6 +2269,16 @@ "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", @@ -2353,6 +2485,13 @@ "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", @@ -2375,6 +2514,30 @@ "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", @@ -2490,6 +2653,12 @@ "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", @@ -2592,6 +2761,16 @@ "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", @@ -3264,6 +3443,17 @@ "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", @@ -3274,6 +3464,96 @@ "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", @@ -3443,6 +3723,12 @@ "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", @@ -3510,6 +3796,13 @@ "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", @@ -3712,6 +4005,25 @@ "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", @@ -3774,6 +4086,99 @@ "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", @@ -4097,6 +4502,15 @@ "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", @@ -4129,11 +4543,15 @@ "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", @@ -4144,6 +4562,7 @@ "vitest": "^4.1.11" }, "peerDependencies": { + "@mantine/core": "8.3.15", "react": "^19.0.0", "react-dom": "^19.0.0" } 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 index 09e4ddb5..83ea3095 100644 --- a/ui/packages/loom-ui/package.json +++ b/ui/packages/loom-ui/package.json @@ -12,26 +12,42 @@ "import": "./dist/loom-ui.js", "default": "./dist/loom-ui.js" }, - "./styles.css": "./dist/styles.css" + "./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 && vitest run", + "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", diff --git a/ui/packages/loom-ui/scripts/check-boundaries.mjs b/ui/packages/loom-ui/scripts/check-boundaries.mjs index f6292b4d..60703774 100644 --- a/ui/packages/loom-ui/scripts/check-boundaries.mjs +++ b/ui/packages/loom-ui/scripts/check-boundaries.mjs @@ -3,7 +3,7 @@ import { join } from 'node:path'; const root = new URL('../src/', import.meta.url); const forbidden = /(?:@gen3\/core|@gen3\/frontend|from ['"]next(?:\/|['"])|ProtectedContent|CalyprAuth)/; -const effectImports = /import\s*\{[^}]*\b(?:useEffect|useLayoutEffect|useDeepCompareEffect)\b[^}]*\}\s*from\s*['"]react['"]/s; +const effectNames = /\b(?:useEffect|useLayoutEffect|useInsertionEffect|useDeepCompareEffect)\b/; const files = []; const walk = async (url) => { for (const entry of await readdir(url, { withFileTypes: true })) { @@ -17,7 +17,8 @@ const violations = []; for (const file of files) { const source = await readFile(file, 'utf8'); if (forbidden.test(source)) violations.push(`${file.pathname}: forbidden Calypr dependency`); - if (/Builder\.tsx$|Viewer\.tsx$/.test(file.pathname) && effectImports.test(source)) violations.push(`${file.pathname}: direct React effect import`); + 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')); diff --git a/ui/packages/loom-ui/src/Viewer.tsx b/ui/packages/loom-ui/src/Viewer.tsx index f3810a11..212b6f3f 100644 --- a/ui/packages/loom-ui/src/Viewer.tsx +++ b/ui/packages/loom-ui/src/Viewer.tsx @@ -1,105 +1,188 @@ -import React, { useMemo, useState } from 'react'; -import type { ExplorerRuntimeOutputV1 } from './types'; -import { useLoomRows, useLoomRuntime, LoomProvider } from './react'; -import { createLoomClient, type LoomClient } from './api'; +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 textFor = (value: unknown): string => { - if (value === undefined || value === null) return '—'; - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value); - if (Array.isArray(value)) return value.map(textFor).filter((value) => value !== '—').join('; ') || '—'; - if (typeof value === 'object') { - const record = value as Record; - for (const key of ['text', 'display', 'value', 'code', 'reference']) { - if (record[key] !== undefined) return textFor(record[key]); - } - try { return JSON.stringify(value); } catch { return String(value); } - } - return String(value); +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 outputColumns = (output: ExplorerRuntimeOutputV1): ReadonlyArray => { - const allowed = new Set(output.columns.filter((column) => column.visible).map((column) => column.column)); - return [...output.table.columns] - .filter((binding) => binding.visible && allowed.has(binding.column)) - .map((binding) => binding.column); +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 ViewerContent = ({ project, explorerId }: Required> & Pick) => { - const runtime = useLoomRuntime({ project, explorerId: explorerId ?? 'default' }); - const [selectedOutputId, setSelectedOutputId] = useState(); - const [filterText, setFilterText] = useState(''); - const output = useMemo(() => { - if (!runtime.data) return undefined; - return runtime.data.outputs.find((candidate) => candidate.outputId === selectedOutputId) ?? runtime.data.outputs[0]; - }, [runtime.data, selectedOutputId]); - const columns = useMemo(() => output ? outputColumns(output) : [], [output]); - const rows = useLoomRows(project, output?.selector, columns, { enabled: Boolean(output), first: 100 }); - const visibleRows = useMemo(() => { - const needle = filterText.trim().toLowerCase(); - if (!needle) return rows.data?.rows ?? []; - return (rows.data?.rows ?? []).filter((row) => columns.some((column) => textFor(row[column]).toLowerCase().includes(needle))); - }, [columns, filterText, rows.data]); +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; - if (runtime.isLoading) return
Loading Explorer…
; - if (runtime.error || !runtime.data) return
The published Explorer could not be loaded.
; - if (runtime.data.outputs.length === 0) return
This Explorer has no published tables.
; + 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

-

{runtime.data.outputs[0]?.title ?? 'Explorer'}

-
- -
- - {output ? ( -
-
-
{output.title}{rows.data?.totalCount ?? '—'} rows
- {rows.isLoading ? Loading rows… : null} - {rows.error ? Rows could not be loaded. : null} -
-
- - {columns.map((column) => )} - - {visibleRows.map((row, index) => {columns.map((column) => )})} - -
{output.columns.find((candidate) => candidate.column === column)?.label ?? column}
{textFor(row[column])}
- {!rows.isLoading && visibleRows.length === 0 ?

No rows match the current search.

: null} +
+
+ +
+ + Loom Explorer + Published + + {output.title}
-
- ) : null} + + + {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 }: LoomExplorerViewerProps) => { +export const LoomExplorerViewer = ({ project, explorerId, client, className, activeOutputId, onActiveOutputChange, renderRowDetails, customActions }: LoomExplorerViewerProps) => { const ownedClient = useMemo(() => client ?? createLoomClient(), [client]); - return
; + 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 index 1f36ae0a..3e5e84dd 100644 --- a/ui/packages/loom-ui/src/api.ts +++ b/ui/packages/loom-ui/src/api.ts @@ -109,6 +109,76 @@ export interface LoomRowsOptions { 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, @@ -164,6 +234,14 @@ export interface LoomClient { 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; } @@ -277,6 +355,53 @@ const shapeRows = (rows: unknown, columns: ReadonlyArray): Array + 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 ?? '/'; @@ -402,13 +527,60 @@ export const createLoomClient = (options: LoomClientOptions = {}): LoomClient => } 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 data = await fetchGraphQL<{ dataframeRows: { columns: string[]; rows: unknown; totalCount?: number | null } }>( - `query LoomRows($input: DataframeRowsInput!) { dataframeRows(input: $input) { columns rows totalCount pageInfo { hasNextPage endCursor } } }`, - { input: { projectId: canonicalProject(rowOptions.project), selector, columns: [...columns], first: rowOptions.first ?? 100 } }, - rowOptions.signal, - ); - return { ...data.dataframeRows, rows: shapeRows(data.dataframeRows.rows, data.dataframeRows.columns) }; + 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, @@ -424,6 +596,8 @@ export const createLoomClient = (options: LoomClientOptions = {}): LoomClient => 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)); diff --git a/ui/packages/loom-ui/src/api.unit.test.ts b/ui/packages/loom-ui/src/api.unit.test.ts index e07bedb4..f2e04363 100644 --- a/ui/packages/loom-ui/src/api.unit.test.ts +++ b/ui/packages/loom-ui/src/api.unit.test.ts @@ -229,4 +229,48 @@ describe('Loom project paths', () => { 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 index f1432531..d6003bc2 100644 --- 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 @@ -35,25 +35,45 @@ vi.mock('./components/BuilderToolbar', () => ({ onPublish, previewDisabled, publishDisabled, + publishing, }: { readonly onPreview: () => void; readonly onPublish: () => void; readonly previewDisabled: boolean; readonly publishDisabled: boolean; + readonly publishing: boolean; }) => (
-
), })); vi.mock('./components/GuidedGraphWorkspace', () => ({ - GuidedGraphWorkspace: () =>
Graph
, + GuidedGraphWorkspace: ({ + onChangeEdge, + }: { + readonly onChangeEdge: (occurrenceId: string, edgeId: string) => void; + }) => ( + + ), })); vi.mock('./components/ColumnSelector', () => ({ @@ -187,6 +207,17 @@ const resolvedRequest = (value: T) => ({ 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; @@ -288,6 +319,54 @@ describe('BuilderWorkspace on-demand reconciliation', () => { 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( { 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({ @@ -345,4 +456,3 @@ describe('BuilderWorkspace on-demand reconciliation', () => { await waitFor(() => expect(preview).toHaveBeenCalledTimes(1)); }); }); - diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx index b615d08c..7fd2485f 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx @@ -153,8 +153,7 @@ const BuilderWorkspaceContent = ({ useGetExplorerCandidateSuggestionsV2Mutation(); const [previewBuilder, previewStatus] = usePreviewExplorerAuthoringV2Mutation(); - const [publishBuilder, publishStatus] = - usePublishExplorerAuthoringV2Mutation(); + const [publishBuilder] = usePublishExplorerAuthoringV2Mutation(); const builderDataKey = builder.data ? builderDataKeyFor(selectedExplorerId, builder.data) : ''; @@ -194,7 +193,12 @@ const BuilderWorkspaceContent = ({ [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'); @@ -394,7 +398,7 @@ const BuilderWorkspaceContent = ({ pendingCommands > 0 || reconcileStatus.isLoading || previewStatus.isLoading || - publishStatus.isLoading || + publishing || createStatus.isLoading || deleteStatus.isLoading; const blockingDiagnostics = state.diagnostics.some( @@ -410,7 +414,8 @@ const BuilderWorkspaceContent = ({ !hasVisibleSelectedColumn || blockingDiagnostics; const publishDisabled = - !state.dirty || + (lastPublished?.explorerId === state.explorerId && + lastPublished.draftDigest === state.draftDigest) || incomplete || blockingDiagnostics || state.tables.some((candidate) => candidate.document.columns.length === 0); @@ -724,6 +729,10 @@ const BuilderWorkspaceContent = ({ authResourcePath, receiptId: activeReceiptId, }).unwrap(); + setLastPublished({ + explorerId: latestState.current.explorerId, + draftDigest: latestState.current.draftDigest, + }); dispatch({ type: 'published' }); setMessage(undefined); return; @@ -763,25 +772,30 @@ const BuilderWorkspaceContent = ({ ); const publish = async () => { - if (publishDisabled) return; - 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.', + 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, + ), ); - return; + if (!publishable) { + setMessage( + 'Every table needs at least one output column before publishing.', + ); + return; + } + await executePublish(receipt.receiptId); + } finally { + setPublishing(false); } - await executePublish(receipt.receiptId); }; if (explorers.isLoading || builder.isLoading) { @@ -858,6 +872,7 @@ const BuilderWorkspaceContent = ({ onPublish={() => void publish()} previewDisabled={previewDisabled} publishDisabled={publishDisabled} + publishing={publishing} busy={busy} columnCreationSupported tableToolbarHost={tableToolbarHost} @@ -995,6 +1010,17 @@ const BuilderWorkspaceContent = ({ }, ]); }} + onChangeEdge={(occurrenceId, edgeId) => { + if (!table) return; + void applyCommands([ + { + type: 'UPDATE_ROUTE_EDGE', + outputId: table.outputId, + occurrenceId, + edgeId, + }, + ]); + }} onTruncate={(occurrenceId) => { if (!table) return; const subtree = routeNode(table.document.route, occurrenceId); diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts index 379b9772..ddc8b67e 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/model.ts @@ -23,6 +23,7 @@ export interface DerivedOccurrence { readonly index: number; readonly nodeId: string; readonly incomingEdgeId?: string; + readonly relationship?: string; readonly parentId?: string; readonly depth: number; readonly resourceType: string; @@ -76,6 +77,7 @@ export const derivedOccurrences = ( index: occurrences.length, nodeId: node.nodeId, incomingEdgeId: edge?.edgeId, + relationship: route.relationship, parentId, depth, resourceType: route.resourceType, diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts index f91ab8d5..1ead27dc 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/reducer.ts @@ -307,15 +307,11 @@ export const builderAuthoringReducer = ( const repeated = occurrences.some( (occurrence) => occurrence.incomingEdgeId === edge.edgeId, ); - const allowsRepeated = - state.catalog.routePolicy.allowRepeatedEdges ?? - state.catalog.routePolicy.repeatedEdges ?? - false; const allowsSelfLoop = state.catalog.routePolicy.allowSelfLoops ?? state.catalog.routePolicy.selfLoops ?? false; - if (repeated && !allowsRepeated) return state; + 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; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts index 5c72c7d2..2eef6d4e 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/authoring/routeActions.ts @@ -16,28 +16,44 @@ export const legalOutgoingEdges = ( (occurrence) => occurrence.id === parentOccurrenceId, ); if (!table || !parent) return []; - const allowsRepeated = - catalog.routePolicy.allowRepeatedEdges ?? - catalog.routePolicy.repeatedEdges ?? - false; 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 ( - allowsRepeated || - !occurrences.some( - (occurrence) => occurrence.incomingEdgeId === edge.edgeId, - ) - ); + 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, 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 index 25706aef..4e4853aa 100644 --- 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 @@ -4,6 +4,7 @@ import { isLegalRouteExtension, legalEdgesToNode, legalOutgoingEdges, + replaceableIncomingEdges, } from './routeActions'; const catalog: ExplorerBuilderCatalog = { @@ -78,7 +79,7 @@ describe('Builder V2 route actions', () => { ).toBe(true); }); - it('honors self-loop and repeated-edge policy without a hidden hop cap', () => { + it('honors self-loop policy without a hidden hop cap', () => { const atSpecimen: DraftTable = { ...table('Patient'), document: { @@ -100,4 +101,105 @@ describe('Builder V2 route actions', () => { 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 index 3c5f8892..68aeb5fe 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.tsx +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/BuilderToolbar.tsx @@ -26,6 +26,7 @@ interface BuilderToolbarProps { onPublish: () => void; previewDisabled: boolean; publishDisabled: boolean; + publishing: boolean; busy?: boolean; columnCreationSupported?: boolean; tableToolbarHost?: HTMLElement | null; @@ -198,6 +199,7 @@ export function BuilderToolbar({ onPublish, previewDisabled, publishDisabled, + publishing, busy = false, columnCreationSupported = true, tableToolbarHost, @@ -365,10 +367,17 @@ export function BuilderToolbar({
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 index baa61e61..4587ddbd 100644 --- 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 @@ -49,6 +49,7 @@ describe('BuilderToolbar', () => { onPublish={vi.fn()} previewDisabled={false} publishDisabled={false} + publishing={false} />, ); @@ -108,6 +109,7 @@ describe('BuilderToolbar', () => { onPublish={vi.fn()} previewDisabled={false} publishDisabled={false} + publishing={false} columnCreationSupported={false} />, ); @@ -132,5 +134,35 @@ describe('BuilderToolbar', () => { 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 index 0b1b4022..60d09fc4 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.tsx +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/ColumnSelector.tsx @@ -389,15 +389,15 @@ export const ColumnSelector = ({ .sort((left, right) => { const leftLabel = left.kind === 'configured' - ? left.column.label + ? configuredCapabilities.get(left.column.column)?.label ?? left.column.source.fieldPath ?? left.column.column : left.candidate.label; const rightLabel = right.kind === 'configured' - ? right.column.label + ? configuredCapabilities.get(right.column.column)?.label ?? right.column.source.fieldPath ?? right.column.column : right.candidate.label; return leftLabel.localeCompare(rightLabel); }), - [available, configured, normalizedQuery], + [available, configured, configuredCapabilities, normalizedQuery], ); const addCandidate = ( candidate: ExplorerBuilderCandidate, @@ -494,7 +494,7 @@ export const ColumnSelector = ({ rows.map((row, order) => row.kind === 'configured' ? ( ) : ( 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 index 831ba8bd..c9cba3ea 100644 --- 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 @@ -96,6 +96,68 @@ describe('configured V2 columns', () => { ); }); + 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', @@ -302,4 +364,3 @@ describe('configured V2 columns', () => { 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 index 85827ad5..418b7338 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.tsx +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/components/GuidedGraphWorkspace.tsx @@ -4,7 +4,10 @@ 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 } from '../authoring/routeActions'; +import { + legalOutgoingEdges, + replaceableIncomingEdges, +} from '../authoring/routeActions'; import { RouteExtensionPanel } from './RouteExtensionPanel'; import { useEscapeToClose } from '../hooks/useEscapeToClose'; import { useGraphLayout } from '../hooks/useGraphLayout'; @@ -26,6 +29,23 @@ const GraphViewportFitter = ({ 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, @@ -35,6 +55,7 @@ export const GuidedGraphWorkspace = ({ onSetBase, onChangeBase, onAppendEdge, + onChangeEdge, onTruncate, onTableToolbarHostChange, }: { @@ -50,6 +71,7 @@ export const GuidedGraphWorkspace = ({ edgeId: string, nodeId: string, ) => void; + readonly onChangeEdge: (occurrenceId: string, edgeId: string) => void; readonly onTruncate: (occurrenceId: string) => void; readonly onTableToolbarHostChange?: (host: HTMLDivElement | null) => void; }) => { @@ -95,6 +117,7 @@ export const GuidedGraphWorkspace = ({ readonly occurrenceId: string; readonly nodeId?: string; readonly edgeId?: string; + readonly allowExistingExtension?: boolean; }>({ tableIdentity, occurrenceId: selectedOccurrenceId }); const activeInspectedNodeId = inspection.tableIdentity === tableIdentity ? inspection.nodeId : undefined; @@ -103,15 +126,28 @@ export const GuidedGraphWorkspace = ({ inspection.occurrenceId === selectedOccurrenceId ? inspection.edgeId : undefined; - const updateInspection = (nodeId?: string, edgeId?: string) => + 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); + updateInspection( + activeInspectedNodeId, + edgeId, + activeAllowExistingExtension, + ); const occurrences = useMemo( () => derivedOccurrences(table, catalog).map((occurrence) => ({ @@ -122,6 +158,7 @@ export const GuidedGraphWorkspace = ({ catalog.nodes.find((node) => node.nodeId === occurrence.nodeId) ?.resourceType ?? occurrence.nodeId, incomingEdgeId: occurrence.incomingEdgeId, + relationship: occurrence.relationship, parentId: occurrence.parentId, depth: occurrence.depth, })), @@ -143,6 +180,20 @@ export const GuidedGraphWorkspace = ({ }); 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], @@ -250,6 +301,10 @@ export const GuidedGraphWorkspace = ({ }, }; }); + const edgeOverviewLabels = useMemo( + () => graphEdgeOverviewLabels(catalog.edges), + [catalog.edges], + ); const edges: Edge[] = catalog.edges.map((edge) => { const isRouteEdge = occurrences.some( (occurrence) => occurrence.incomingEdgeId === edge.edgeId, @@ -269,7 +324,7 @@ export const GuidedGraphWorkspace = ({ // React Flow; do not synthesize a reverse edge for FHIR relationships. source: edge.fromNodeId, target: edge.toNodeId, - label: edge.label, + label: edgeOverviewLabels.get(edge.edgeId), type: 'smoothstep', markerEnd: { type: MarkerType.ArrowClosed, @@ -321,8 +376,8 @@ export const GuidedGraphWorkspace = ({ const graphIdentity = `${layout.identity ?? 'layout-pending'}:${nodesForViewport.map((node) => node.id).join(',')}`; const inspectNode = (nodeId: string) => { if (disabled) return; - updateInspection(nodeId); if (occurrences.length === 0) { + updateInspection(nodeId); const node = catalog.nodes.find( (candidate) => candidate.nodeId === nodeId, ); @@ -332,7 +387,15 @@ export const GuidedGraphWorkspace = ({ 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) { @@ -341,15 +404,16 @@ export const GuidedGraphWorkspace = ({ ); const next = routeOccurrences[(currentIndex + 1) % routeOccurrences.length]; + updateInspection(); onSelectOccurrence(next.occurrenceId); return; } - const matchingEdges = legalNextEdges.filter( - (edge) => edge.toNodeId === nodeId, - ); if (matchingEdges.length === 1) { + updateInspection(nodeId); onAppendEdge(selectedOccurrenceId, matchingEdges[0].edgeId, nodeId); + return; } + updateInspection(nodeId); }; const inspectEdge = (edgeId: string) => { if (disabled) return; @@ -370,7 +434,7 @@ export const GuidedGraphWorkspace = ({ if (disabled || !legalNextEdges.some((edge) => edge.edgeId === edgeId)) return; onAppendEdge(selectedOccurrenceId, edgeId, nodeId); - selectEdge(undefined); + updateInspection(); }; const content = ( <> @@ -462,6 +526,7 @@ export const GuidedGraphWorkspace = ({ selectedOccurrenceId={selectedOccurrenceId} inspectedNodeId={activeInspectedNodeId} selectedEdgeId={activeSelectedEdgeId} + allowExistingExtension={activeAllowExistingExtension} disabled={disabled} onSelectEdge={selectEdge} onUseAsRowStart={useAsRowStart} @@ -500,17 +565,55 @@ export const GuidedGraphWorkspace = ({ {occurrence.depth > 0 && ( ↳ )} - + + {occurrence.occurrenceId === 'base' ? ( + + Row start + + ) : ( + + )} + {occurrence.occurrenceId !== 'base' && ( + + ); + } if (inspectedEdges.length === 0) { + if (alreadyInQuery) return null; if (!inspectedNode?.rowRootEligible) return null; return (
@@ -106,7 +139,8 @@ export const RouteExtensionPanel = ({ return (

- Add {inspectedResource} under {parentResource} + 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 index 5c5f2d3e..e14ffb9f 100644 --- 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 @@ -153,6 +153,102 @@ describe('RouteExtensionPanel', () => { 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, @@ -191,4 +287,3 @@ describe('RouteExtensionPanel', () => { expect(onChangeRowStart).toHaveBeenCalledWith('substance'); }); }); - 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 +