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);
+});