diff --git a/integration/ddd_layer_rules_test.ts b/integration/ddd_layer_rules_test.ts index 56d28abc..249889ea 100644 --- a/integration/ddd_layer_rules_test.ts +++ b/integration/ddd_layer_rules_test.ts @@ -69,9 +69,7 @@ function isTracingImport(filePath: string, importPath: string): boolean { // If someone fixes a violation, the count decreases and the test still passes. // If someone adds a new violation, the count increases and the test fails. // -// Tracked refactor (data services → domain-side ports): swamp-club#229. -// -// Issue #223 (W1b extension catalog rearchitecture) added 4 new +// Issue #223 (W1b extension catalog rearchitecture) added 4 transitional // domain→infrastructure imports: // - src/domain/extensions/bundle_location.ts → canonicalizePath // - src/domain/extensions/source_location.ts → canonicalizePath @@ -83,9 +81,8 @@ function isTracingImport(filePath: string, importPath: string): boolean { // references for I-Repo-1 cross-aggregate uniqueness. Both are accepted // as transitional ports — the canonicalizer should move to a shared // path-utility module (W3 territory) and ExtensionKind should hoist to -// the domain layer when the catalog gets fully replaced (W4). Until -// then the violations are bounded and the ratchet rises by 4 (27 + 4). -const KNOWN_DOMAIN_INFRA_VIOLATIONS = 31; +// the domain layer when the catalog gets fully replaced (W4). +const KNOWN_DOMAIN_INFRA_VIOLATIONS = 22; Deno.test( "domain layer must not add new infrastructure imports (ratchet)", diff --git a/src/domain/data/data_access_service.ts b/src/domain/data/data_access_service.ts index d35544bc..8862ae61 100644 --- a/src/domain/data/data_access_service.ts +++ b/src/domain/data/data_access_service.ts @@ -19,7 +19,7 @@ import type { DefinitionRepository } from "../definitions/repositories.ts"; import type { ModelType } from "../models/model_type.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; import type { VaultService } from "../vaults/vault_service.ts"; import type { SecretRedactor } from "../secrets/mod.ts"; import type { Data } from "./data.ts"; diff --git a/src/domain/data/data_access_service_test.ts b/src/domain/data/data_access_service_test.ts index 24b61b93..0fbdd147 100644 --- a/src/domain/data/data_access_service_test.ts +++ b/src/domain/data/data_access_service_test.ts @@ -24,7 +24,7 @@ import { ModelType } from "../models/model_type.ts"; import { Definition } from "../definitions/definition.ts"; import { computeDefinitionHash } from "../models/model_output.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; import type { VaultService } from "../vaults/vault_service.ts"; // Import models barrel to trigger self-registration diff --git a/src/domain/data/data_delete_service.ts b/src/domain/data/data_delete_service.ts index 491c5173..cb8eb0f7 100644 --- a/src/domain/data/data_delete_service.ts +++ b/src/domain/data/data_delete_service.ts @@ -17,8 +17,8 @@ // You should have received a copy of the GNU Affero General Public License // along with Swamp. If not, see . -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; -import type { YamlDefinitionRepository } from "../../infrastructure/persistence/yaml_definition_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; +import type { DefinitionRepository } from "../definitions/repositories.ts"; import { findDefinitionByIdOrName } from "../models/model_lookup.ts"; /** @@ -60,7 +60,7 @@ export interface DeletePreview { export class DataDeleteService { constructor( private readonly dataRepo: UnifiedDataRepository, - private readonly definitionRepo: YamlDefinitionRepository, + private readonly definitionRepo: DefinitionRepository, ) {} async delete( diff --git a/src/domain/data/data_lifecycle_service.ts b/src/domain/data/data_lifecycle_service.ts index 7c0b5f74..b27d2ce1 100644 --- a/src/domain/data/data_lifecycle_service.ts +++ b/src/domain/data/data_lifecycle_service.ts @@ -21,7 +21,7 @@ import { getLogger } from "@logtape/logtape"; import type { Data } from "./data.ts"; import type { Lifetime } from "./data_metadata.ts"; import { parseDataDuration } from "./duration.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; import type { WorkflowRunRepository } from "../workflows/repositories.ts"; import type { ModelType } from "../models/model_type.ts"; import { diff --git a/src/domain/data/data_query_service.ts b/src/domain/data/data_query_service.ts index ac0589a6..53ef27a9 100644 --- a/src/domain/data/data_query_service.ts +++ b/src/domain/data/data_query_service.ts @@ -23,7 +23,7 @@ import type { CatalogRow, CatalogStore, } from "../../infrastructure/persistence/catalog_store.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; import type { DataRecord } from "./data_record.ts"; import { type ASTNode, diff --git a/src/domain/data/data_record_mapper.ts b/src/domain/data/data_record_mapper.ts index 9a883282..1c49b8ff 100644 --- a/src/domain/data/data_record_mapper.ts +++ b/src/domain/data/data_record_mapper.ts @@ -21,7 +21,7 @@ import type { CatalogRow } from "../../infrastructure/persistence/catalog_store. import type { DataRecord, FileDataRecord } from "./data_record.ts"; import type { Data } from "./data.ts"; import { ModelType } from "../models/model_type.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; import type { VaultService } from "../vaults/vault_service.ts"; import type { SecretRedactor } from "../secrets/mod.ts"; import type { DataHandle } from "../models/model.ts"; diff --git a/src/domain/data/data_record_mapper_test.ts b/src/domain/data/data_record_mapper_test.ts index 960f31c9..750b72c2 100644 --- a/src/domain/data/data_record_mapper_test.ts +++ b/src/domain/data/data_record_mapper_test.ts @@ -20,7 +20,7 @@ import { assertEquals } from "@std/assert"; import { fromData, fromRow } from "./data_record_mapper.ts"; import type { CatalogRow } from "../../infrastructure/persistence/catalog_store.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; import { Data } from "./data.ts"; import { ModelType } from "../models/model_type.ts"; diff --git a/src/domain/data/data_rename_service.ts b/src/domain/data/data_rename_service.ts index a3e40b14..34cb1e1a 100644 --- a/src/domain/data/data_rename_service.ts +++ b/src/domain/data/data_rename_service.ts @@ -18,8 +18,8 @@ // along with Swamp. If not, see . import { DataMetadataSchema } from "./data_metadata.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; -import type { YamlDefinitionRepository } from "../../infrastructure/persistence/yaml_definition_repository.ts"; +import type { UnifiedDataRepository } from "./repositories.ts"; +import type { DefinitionRepository } from "../definitions/repositories.ts"; import { findDefinitionByIdOrName } from "../models/model_lookup.ts"; /** @@ -44,7 +44,7 @@ export interface RenameResult { export class DataRenameService { constructor( private readonly dataRepo: UnifiedDataRepository, - private readonly definitionRepo: YamlDefinitionRepository, + private readonly definitionRepo: DefinitionRepository, ) {} /** diff --git a/src/domain/data/repositories.ts b/src/domain/data/repositories.ts new file mode 100644 index 00000000..8d437d79 --- /dev/null +++ b/src/domain/data/repositories.ts @@ -0,0 +1,403 @@ +// Swamp, an Automation Framework +// Copyright (C) 2026 System Initiative, Inc. +// +// This file is part of Swamp. +// +// Swamp is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License version 3 +// as published by the Free Software Foundation, with the Swamp +// Extension and Definition Exception (found in the "COPYING-EXCEPTION" +// file). +// +// Swamp is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Swamp. If not, see . + +import type { Data } from "./data.ts"; +import type { DataId } from "./data_id.ts"; +import type { ModelType } from "../models/model_type.ts"; + +/** + * Error thrown when ownership validation fails. + */ +export class OwnershipValidationError extends Error { + constructor( + readonly dataName: string, + readonly existingOwner: { ownerType: string; ownerRef: string }, + readonly newOwner: { ownerType: string; ownerRef: string }, + ) { + super( + `Ownership validation failed for "${dataName}": ` + + `existing owner "${existingOwner.ownerType}:${existingOwner.ownerRef}" ` + + `does not match new owner "${newOwner.ownerType}:${newOwner.ownerRef}"`, + ); + this.name = "OwnershipValidationError"; + } +} + +/** + * Result of garbage collection operation. + */ +export interface GarbageCollectionResult { + versionsRemoved: number; + bytesReclaimed: number; +} + +/** + * Repository interface for unified Data storage with versioning. + */ +export interface UnifiedDataRepository { + /** + * Finds all data across all model types and models. + * + * @returns Array of data with their model type and model ID + */ + findAllGlobal(): Promise< + Array<{ data: Data; modelType: ModelType; modelId: string }> + >; + + /** + * Finds data by name, optionally for a specific version. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - Optional version (defaults to latest) + * @returns The data if found, or null + */ + findByName( + type: ModelType, + modelId: string, + dataName: string, + version?: number, + ): Promise; + + /** + * Finds data by ID, optionally for a specific version. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataId - The data ID + * @param version - Optional version (defaults to latest) + * @returns The data if found, or null + */ + findById( + type: ModelType, + modelId: string, + dataId: DataId, + version?: number, + ): Promise; + + /** + * Lists all versions for a data name. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @returns Array of version numbers in ascending order + */ + listVersions( + type: ModelType, + modelId: string, + dataName: string, + ): Promise; + + /** + * Finds all data for a model. + * + * @param type - The model type + * @param modelId - The model input ID + * @returns Array of data (latest version of each) + */ + findAllForModel(type: ModelType, modelId: string): Promise; + + /** + * Saves data with its content, creating a new version. + * Validates ownership if data with the same name already exists. + * + * @param type - The model type + * @param modelId - The model input ID + * @param data - The data entity + * @param content - The content to save + * @returns The saved version number + * @throws OwnershipValidationError if ownership validation fails + */ + save( + type: ModelType, + modelId: string, + data: Data, + content: Uint8Array, + ): Promise<{ version: number }>; + + /** + * Appends content to streaming data. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param content - The content to append + */ + append( + type: ModelType, + modelId: string, + dataName: string, + content: Uint8Array, + ): Promise; + + /** + * Streams content from data. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - Optional version (defaults to latest) + * @returns Async iterable of content chunks + */ + stream( + type: ModelType, + modelId: string, + dataName: string, + version?: number, + ): AsyncIterable; + + /** + * Gets the full content of data. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - Optional version (defaults to latest) + * @returns The content or null if not found + */ + getContent( + type: ModelType, + modelId: string, + dataName: string, + version?: number, + ): Promise; + + /** + * Deletes data, optionally for a specific version. + * If no version is specified, deletes all versions. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - Optional version to delete (all versions if not specified) + */ + delete( + type: ModelType, + modelId: string, + dataName: string, + version?: number, + ): Promise; + + /** + * Removes the latest symlink for expired data (soft delete). + * Version directories remain on disk but data becomes inaccessible. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + */ + removeLatestMarker( + type: ModelType, + modelId: string, + dataName: string, + ): Promise; + + /** + * Allocates a new version directory without writing content. + * Used by DataWriter for direct file I/O. + * + * @param type - The model type + * @param modelId - The model input ID + * @param data - The data entity (for ownership validation) + * @returns The allocated version number and content file path + */ + allocateVersion( + type: ModelType, + modelId: string, + data: Data, + ): Promise<{ version: number; contentPath: string }>; + + /** + * Finalizes a previously allocated version by writing metadata and updating symlinks. + * Content must already exist on disk at the content path. + * + * @param type - The model type + * @param modelId - The model input ID + * @param data - The data entity + * @param version - The version number to finalize + * @returns Size and checksum of the content + */ + finalizeVersion( + type: ModelType, + modelId: string, + data: Data, + version: number, + ): Promise<{ size: number; checksum: string }>; + + /** + * Generates a new unique ID. + */ + nextId(): DataId; + + /** + * Returns the directory path for a data version. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - The version number + * @returns The directory path + */ + getPath( + type: ModelType, + modelId: string, + dataName: string, + version: number, + ): string; + + /** + * Returns the content file path for a data version. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - The version number + * @returns The content file path + */ + getContentPath( + type: ModelType, + modelId: string, + dataName: string, + version: number, + ): string; + + /** + * Collects garbage according to each data's garbage collection policy. + * + * When `options.dryRun` is true, no versions are deleted — the returned + * `versionsRemoved` and `bytesReclaimed` reflect what would be removed. + * + * @param type - The model type + * @param modelId - The model input ID + * @param options - Options for the operation + * @returns The result of garbage collection + */ + collectGarbage( + type: ModelType, + modelId: string, + options?: { dryRun?: boolean }, + ): Promise; + + /** + * Renames a data instance by copying the latest version to a new name + * and writing a tombstone with a forward reference on the old name. + * + * @param type - The model type + * @param modelId - The model input ID + * @param oldName - The current data name + * @param newName - The new data name + * @returns The rename result with version info + */ + rename( + type: ModelType, + modelId: string, + oldName: string, + newName: string, + ): Promise< + { + oldName: string; + newName: string; + copiedVersion: number; + newVersion: number; + } + >; + + // --- Sync read methods (for CEL expression evaluation) --- + + /** + * Gets the latest version number synchronously. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @returns The latest version number, or null if not found + */ + getLatestVersionSync( + type: ModelType, + modelId: string, + dataName: string, + ): number | null; + + /** + * Finds data by name synchronously, optionally for a specific version. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - Optional version (defaults to latest) + * @returns The data if found, or null + */ + findByNameSync( + type: ModelType, + modelId: string, + dataName: string, + version?: number, + ): Data | null; + + /** + * Lists all versions for a data name synchronously. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @returns Array of version numbers in ascending order + */ + listVersionsSync( + type: ModelType, + modelId: string, + dataName: string, + ): number[]; + + /** + * Gets the full content of data synchronously. + * + * @param type - The model type + * @param modelId - The model input ID + * @param dataName - The data name + * @param version - Optional version (defaults to latest) + * @returns The content or null if not found + */ + getContentSync( + type: ModelType, + modelId: string, + dataName: string, + version?: number, + ): Uint8Array | null; + + /** + * Finds all data for a model synchronously. + * + * @param type - The model type + * @param modelId - The model input ID + * @returns Array of data (latest version of each) + */ + findAllForModelSync(type: ModelType, modelId: string): Data[]; + + /** + * Finds all data across all model types and models synchronously. + * Used by DataQueryService for catalog backfill in sync contexts. + * + * @returns Array of data with their model type and model ID + */ + findAllGlobalSync(): Array< + { data: Data; modelType: ModelType; modelId: string } + >; +} diff --git a/src/domain/drivers/raw_execution_driver_test.ts b/src/domain/drivers/raw_execution_driver_test.ts index 96b4d5d3..3e88f460 100644 --- a/src/domain/drivers/raw_execution_driver_test.ts +++ b/src/domain/drivers/raw_execution_driver_test.ts @@ -30,7 +30,7 @@ import type { ModelDefinition, } from "../models/model.ts"; import { z } from "zod"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; import { type DataId, generateDataId } from "../data/data_id.ts"; import { getLogger } from "@logtape/logtape"; diff --git a/src/domain/expressions/model_resolver.ts b/src/domain/expressions/model_resolver.ts index 4b6502d4..6d7888a1 100644 --- a/src/domain/expressions/model_resolver.ts +++ b/src/domain/expressions/model_resolver.ts @@ -22,7 +22,7 @@ import type { ModelType } from "../models/model_type.ts"; import type { Definition, InputsSchema } from "../definitions/definition.ts"; import type { YamlOutputRepository } from "../../infrastructure/persistence/yaml_output_repository.ts"; import type { YamlDefinitionRepository } from "../../infrastructure/persistence/yaml_definition_repository.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { Data } from "../data/data.ts"; import type { DataRecord } from "../data/data_record.ts"; import type { DataQueryService } from "../data/data_query_service.ts"; diff --git a/src/domain/models/command/shell/shell_model_test.ts b/src/domain/models/command/shell/shell_model_test.ts index 25eb777d..fa15fd27 100644 --- a/src/domain/models/command/shell/shell_model_test.ts +++ b/src/domain/models/command/shell/shell_model_test.ts @@ -28,7 +28,7 @@ import { shellModel, } from "./shell_model.ts"; import type { DataHandle, DataWriter, MethodContext } from "../../model.ts"; -import type { UnifiedDataRepository } from "../../../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../../../data/repositories.ts"; import type { DefinitionRepository } from "../../../definitions/repositories.ts"; import { type DataId, generateDataId } from "../../../data/data_id.ts"; import { getLogger } from "@logtape/logtape"; diff --git a/src/domain/models/data_writer.ts b/src/domain/models/data_writer.ts index 3acddfd5..9334f87d 100644 --- a/src/domain/models/data_writer.ts +++ b/src/domain/models/data_writer.ts @@ -21,7 +21,7 @@ import { z } from "zod"; import { getLogger } from "@logtape/logtape"; import { Data, isReservedDataName } from "../data/mod.ts"; import type { DataId, OwnerDefinition } from "../data/mod.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { ModelType } from "./model_type.ts"; import type { MethodExecutionEvent } from "./method_events.ts"; import type { diff --git a/src/domain/models/data_writer_test.ts b/src/domain/models/data_writer_test.ts index 8a75704c..9a6fd4ac 100644 --- a/src/domain/models/data_writer_test.ts +++ b/src/domain/models/data_writer_test.ts @@ -29,7 +29,7 @@ import { } from "./data_writer.ts"; import { ModelType } from "./model_type.ts"; import type { ResourceOutputSpec } from "./model.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import { generateDataId } from "../data/data_id.ts"; import { VaultService } from "../vaults/vault_service.ts"; diff --git a/src/domain/models/method_context_test.ts b/src/domain/models/method_context_test.ts index d5793c69..53a56058 100644 --- a/src/domain/models/method_context_test.ts +++ b/src/domain/models/method_context_test.ts @@ -31,7 +31,7 @@ import type { DefinitionRepository } from "../definitions/repositories.ts"; import type { OutputRepository } from "./repositories.ts"; import type { VaultService } from "../vaults/vault_service.ts"; import type { SecretRedactor } from "../secrets/mod.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; function makeCommon( overrides: Partial = {}, diff --git a/src/domain/models/method_execution_service_test.ts b/src/domain/models/method_execution_service_test.ts index 5a4d55f2..a06d46bb 100644 --- a/src/domain/models/method_execution_service_test.ts +++ b/src/domain/models/method_execution_service_test.ts @@ -29,7 +29,7 @@ import type { ModelDefinition, } from "./model.ts"; import { z } from "zod"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; import { type DataId, generateDataId } from "../data/data_id.ts"; import { Data } from "../data/data.ts"; diff --git a/src/domain/models/model.ts b/src/domain/models/model.ts index 01b00174..1c0f5655 100644 --- a/src/domain/models/model.ts +++ b/src/domain/models/model.ts @@ -38,7 +38,7 @@ import { LifetimeSchema, type OwnerDefinition, } from "../data/mod.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { OutputRepository } from "./repositories.ts"; import type { DataRecord } from "../data/data_record.ts"; diff --git a/src/domain/models/model_test.ts b/src/domain/models/model_test.ts index 1b946132..c5da77c5 100644 --- a/src/domain/models/model_test.ts +++ b/src/domain/models/model_test.ts @@ -32,7 +32,7 @@ import { } from "./model.ts"; import { ModelType } from "./model_type.ts"; import { createDefinitionId } from "../definitions/definition.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; import { type DataId, generateDataId } from "../data/data_id.ts"; import { getLogger } from "@logtape/logtape"; diff --git a/src/domain/models/user_model_loader_test.ts b/src/domain/models/user_model_loader_test.ts index cbf3edb6..30e69088 100644 --- a/src/domain/models/user_model_loader_test.ts +++ b/src/domain/models/user_model_loader_test.ts @@ -42,7 +42,7 @@ function makeRepoForCatalog( } import type { DataHandle, DataWriter, MethodContext } from "./model.ts"; import type { ModelType } from "./model_type.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; import { type DataId, generateDataId } from "../data/data_id.ts"; import { createDefinitionId } from "../definitions/definition.ts"; diff --git a/src/domain/models/validation_service_test.ts b/src/domain/models/validation_service_test.ts index 02f2b85c..5eaf3fd6 100644 --- a/src/domain/models/validation_service_test.ts +++ b/src/domain/models/validation_service_test.ts @@ -1078,7 +1078,7 @@ Deno.test("validateModel loads lazy types before resolving cross-model reference // ---------- Check Validation Tests ---------- import type { CheckValidationContext } from "./validation_service.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import { generateDataId } from "../data/data_id.ts"; import { createDefinitionId } from "../definitions/definition.ts"; diff --git a/src/domain/reports/report_context.ts b/src/domain/reports/report_context.ts index d9bb221d..40d2dc29 100644 --- a/src/domain/reports/report_context.ts +++ b/src/domain/reports/report_context.ts @@ -20,7 +20,7 @@ import type { Logger } from "@logtape/logtape"; import type { ModelType } from "../models/model_type.ts"; import type { DataHandle } from "../models/model.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; import { resolveExtensionFile } from "../extensions/extension_file_resolver.ts"; diff --git a/src/domain/reports/report_execution_service.ts b/src/domain/reports/report_execution_service.ts index e86a322c..5851bc61 100644 --- a/src/domain/reports/report_execution_service.ts +++ b/src/domain/reports/report_execution_service.ts @@ -23,7 +23,7 @@ import type { ReportRef, ReportSelection } from "./report_selection.ts"; import type { ReportRegistry } from "./report_registry.ts"; import type { DataHandle } from "../models/model.ts"; import type { ModelType } from "../models/model_type.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import { DefaultDataWriter } from "../models/data_writer.ts"; import { modelRegistry } from "../models/model.ts"; import { diff --git a/src/domain/workflows/execution_service.ts b/src/domain/workflows/execution_service.ts index 5dd9ba91..5c345702 100644 --- a/src/domain/workflows/execution_service.ts +++ b/src/domain/workflows/execution_service.ts @@ -39,7 +39,7 @@ import type { import { YamlDefinitionRepository } from "../../infrastructure/persistence/yaml_definition_repository.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; import type { OutputRepository } from "../models/repositories.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import type { MethodExecutionService } from "../models/method_execution_service.ts"; import { YamlEvaluatedDefinitionRepository } from "../../infrastructure/persistence/yaml_evaluated_definition_repository.ts"; import { YamlEvaluatedWorkflowRepository } from "../../infrastructure/persistence/yaml_evaluated_workflow_repository.ts"; diff --git a/src/domain/workflows/method_report_runner.ts b/src/domain/workflows/method_report_runner.ts index b91b49c9..eee0e28d 100644 --- a/src/domain/workflows/method_report_runner.ts +++ b/src/domain/workflows/method_report_runner.ts @@ -24,7 +24,7 @@ import { modelRegistry } from "../models/model.ts"; import type { Definition } from "../definitions/definition.ts"; import type { DataArtifactRef } from "../models/model_output.ts"; import type { DefinitionRepository } from "../definitions/repositories.ts"; -import type { UnifiedDataRepository } from "../../infrastructure/persistence/unified_data_repository.ts"; +import type { UnifiedDataRepository } from "../data/repositories.ts"; import { buildOutputSpecs } from "../models/output_spec_builder.ts"; import { executeReports, diff --git a/src/infrastructure/persistence/unified_data_repository.ts b/src/infrastructure/persistence/unified_data_repository.ts index 1801c10f..702814d8 100644 --- a/src/infrastructure/persistence/unified_data_repository.ts +++ b/src/infrastructure/persistence/unified_data_repository.ts @@ -35,390 +35,22 @@ import { import { ModelType } from "../../domain/models/model_type.ts"; import type { MarkDirtyHook } from "../../domain/datastore/datastore_sync_service.ts"; import type { CatalogStore } from "./catalog_store.ts"; +import { + type GarbageCollectionResult, + OwnershipValidationError, + type UnifiedDataRepository, +} from "../../domain/data/repositories.ts"; + +// Re-export domain repository types so existing infra-path importers keep working. +// New domain code should import directly from src/domain/data/repositories.ts. +export { + type GarbageCollectionResult, + OwnershipValidationError, + type UnifiedDataRepository, +}; const logger = getSwampLogger(["data", "repository"]); -/** - * Error thrown when ownership validation fails. - */ -export class OwnershipValidationError extends Error { - constructor( - readonly dataName: string, - readonly existingOwner: { ownerType: string; ownerRef: string }, - readonly newOwner: { ownerType: string; ownerRef: string }, - ) { - super( - `Ownership validation failed for "${dataName}": ` + - `existing owner "${existingOwner.ownerType}:${existingOwner.ownerRef}" ` + - `does not match new owner "${newOwner.ownerType}:${newOwner.ownerRef}"`, - ); - this.name = "OwnershipValidationError"; - } -} - -/** - * Result of garbage collection operation. - */ -export interface GarbageCollectionResult { - versionsRemoved: number; - bytesReclaimed: number; -} - -/** - * Repository interface for unified Data storage with versioning. - */ -export interface UnifiedDataRepository { - /** - * Finds all data across all model types and models. - * - * @returns Array of data with their model type and model ID - */ - findAllGlobal(): Promise< - Array<{ data: Data; modelType: ModelType; modelId: string }> - >; - - /** - * Finds data by name, optionally for a specific version. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - Optional version (defaults to latest) - * @returns The data if found, or null - */ - findByName( - type: ModelType, - modelId: string, - dataName: string, - version?: number, - ): Promise; - - /** - * Finds data by ID, optionally for a specific version. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataId - The data ID - * @param version - Optional version (defaults to latest) - * @returns The data if found, or null - */ - findById( - type: ModelType, - modelId: string, - dataId: DataId, - version?: number, - ): Promise; - - /** - * Lists all versions for a data name. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @returns Array of version numbers in ascending order - */ - listVersions( - type: ModelType, - modelId: string, - dataName: string, - ): Promise; - - /** - * Finds all data for a model. - * - * @param type - The model type - * @param modelId - The model input ID - * @returns Array of data (latest version of each) - */ - findAllForModel(type: ModelType, modelId: string): Promise; - - /** - * Saves data with its content, creating a new version. - * Validates ownership if data with the same name already exists. - * - * @param type - The model type - * @param modelId - The model input ID - * @param data - The data entity - * @param content - The content to save - * @returns The saved version number - * @throws OwnershipValidationError if ownership validation fails - */ - save( - type: ModelType, - modelId: string, - data: Data, - content: Uint8Array, - ): Promise<{ version: number }>; - - /** - * Appends content to streaming data. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param content - The content to append - */ - append( - type: ModelType, - modelId: string, - dataName: string, - content: Uint8Array, - ): Promise; - - /** - * Streams content from data. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - Optional version (defaults to latest) - * @returns Async iterable of content chunks - */ - stream( - type: ModelType, - modelId: string, - dataName: string, - version?: number, - ): AsyncIterable; - - /** - * Gets the full content of data. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - Optional version (defaults to latest) - * @returns The content or null if not found - */ - getContent( - type: ModelType, - modelId: string, - dataName: string, - version?: number, - ): Promise; - - /** - * Deletes data, optionally for a specific version. - * If no version is specified, deletes all versions. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - Optional version to delete (all versions if not specified) - */ - delete( - type: ModelType, - modelId: string, - dataName: string, - version?: number, - ): Promise; - - /** - * Removes the latest symlink for expired data (soft delete). - * Version directories remain on disk but data becomes inaccessible. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - */ - removeLatestMarker( - type: ModelType, - modelId: string, - dataName: string, - ): Promise; - - /** - * Allocates a new version directory without writing content. - * Used by DataWriter for direct file I/O. - * - * @param type - The model type - * @param modelId - The model input ID - * @param data - The data entity (for ownership validation) - * @returns The allocated version number and content file path - */ - allocateVersion( - type: ModelType, - modelId: string, - data: Data, - ): Promise<{ version: number; contentPath: string }>; - - /** - * Finalizes a previously allocated version by writing metadata and updating symlinks. - * Content must already exist on disk at the content path. - * - * @param type - The model type - * @param modelId - The model input ID - * @param data - The data entity - * @param version - The version number to finalize - * @returns Size and checksum of the content - */ - finalizeVersion( - type: ModelType, - modelId: string, - data: Data, - version: number, - ): Promise<{ size: number; checksum: string }>; - - /** - * Generates a new unique ID. - */ - nextId(): DataId; - - /** - * Returns the directory path for a data version. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - The version number - * @returns The directory path - */ - getPath( - type: ModelType, - modelId: string, - dataName: string, - version: number, - ): string; - - /** - * Returns the content file path for a data version. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - The version number - * @returns The content file path - */ - getContentPath( - type: ModelType, - modelId: string, - dataName: string, - version: number, - ): string; - - /** - * Collects garbage according to each data's garbage collection policy. - * - * When `options.dryRun` is true, no versions are deleted — the returned - * `versionsRemoved` and `bytesReclaimed` reflect what would be removed. - * - * @param type - The model type - * @param modelId - The model input ID - * @param options - Options for the operation - * @returns The result of garbage collection - */ - collectGarbage( - type: ModelType, - modelId: string, - options?: { dryRun?: boolean }, - ): Promise; - - /** - * Renames a data instance by copying the latest version to a new name - * and writing a tombstone with a forward reference on the old name. - * - * @param type - The model type - * @param modelId - The model input ID - * @param oldName - The current data name - * @param newName - The new data name - * @returns The rename result with version info - */ - rename( - type: ModelType, - modelId: string, - oldName: string, - newName: string, - ): Promise< - { - oldName: string; - newName: string; - copiedVersion: number; - newVersion: number; - } - >; - - // --- Sync read methods (for CEL expression evaluation) --- - - /** - * Gets the latest version number synchronously. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @returns The latest version number, or null if not found - */ - getLatestVersionSync( - type: ModelType, - modelId: string, - dataName: string, - ): number | null; - - /** - * Finds data by name synchronously, optionally for a specific version. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - Optional version (defaults to latest) - * @returns The data if found, or null - */ - findByNameSync( - type: ModelType, - modelId: string, - dataName: string, - version?: number, - ): Data | null; - - /** - * Lists all versions for a data name synchronously. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @returns Array of version numbers in ascending order - */ - listVersionsSync( - type: ModelType, - modelId: string, - dataName: string, - ): number[]; - - /** - * Gets the full content of data synchronously. - * - * @param type - The model type - * @param modelId - The model input ID - * @param dataName - The data name - * @param version - Optional version (defaults to latest) - * @returns The content or null if not found - */ - getContentSync( - type: ModelType, - modelId: string, - dataName: string, - version?: number, - ): Uint8Array | null; - - /** - * Finds all data for a model synchronously. - * - * @param type - The model type - * @param modelId - The model input ID - * @returns Array of data (latest version of each) - */ - findAllForModelSync(type: ModelType, modelId: string): Data[]; - - /** - * Finds all data across all model types and models synchronously. - * Used by DataQueryService for catalog backfill in sync contexts. - * - * @returns Array of data with their model type and model ID - */ - findAllGlobalSync(): Array< - { data: Data; modelType: ModelType; modelId: string } - >; -} - /** * File system implementation of UnifiedDataRepository. * diff --git a/src/infrastructure/persistence/unified_data_repository_re_export_test.ts b/src/infrastructure/persistence/unified_data_repository_re_export_test.ts new file mode 100644 index 00000000..3689a7dd --- /dev/null +++ b/src/infrastructure/persistence/unified_data_repository_re_export_test.ts @@ -0,0 +1,42 @@ +// Swamp, an Automation Framework +// Copyright (C) 2026 System Initiative, Inc. +// +// This file is part of Swamp. +// +// Swamp is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License version 3 +// as published by the Free Software Foundation, with the Swamp +// Extension and Definition Exception (found in the "COPYING-EXCEPTION" +// file). +// +// Swamp is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Swamp. If not, see . + +import { assert, assertStrictEquals } from "@std/assert"; +import { OwnershipValidationError as DomainOwnershipValidationError } from "../../domain/data/repositories.ts"; +import { OwnershipValidationError as InfraOwnershipValidationError } from "./unified_data_repository.ts"; + +// Locks the value re-export contract. UnifiedDataRepository's canonical home is +// the domain layer; the infra module re-exports the symbols so existing +// importers keep working. A future regression that converts this to a type-only +// re-export, or duplicates the class definition, would silently break +// `instanceof` checks across the two import paths — these tests catch that. + +Deno.test("OwnershipValidationError class identity is preserved across the re-export", () => { + assertStrictEquals( + DomainOwnershipValidationError, + InfraOwnershipValidationError, + ); +}); + +Deno.test("OwnershipValidationError instances are caught by instanceof against either import path", () => { + const owner = { ownerType: "model-method", ownerRef: "x:y" }; + const thrown = new InfraOwnershipValidationError("name", owner, owner); + assert(thrown instanceof DomainOwnershipValidationError); + assert(thrown instanceof InfraOwnershipValidationError); +});