From c843d6418dada8d712753ea38084e0fc343ebbb5 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 09:17:01 -0600 Subject: [PATCH 1/2] Add domain enums and result dataclass contracts for full_build --- src/buildcompiler/domain/__init__.py | 35 ++++++++- src/buildcompiler/domain/approvals.py | 22 ++++++ src/buildcompiler/domain/build_request.py | 21 ++++++ src/buildcompiler/domain/build_result.py | 50 +++++++++++++ src/buildcompiler/domain/build_stage.py | 13 ++++ src/buildcompiler/domain/design.py | 12 ++++ src/buildcompiler/domain/material_state.py | 13 ++++ src/buildcompiler/domain/missing_input.py | 32 +++++++++ src/buildcompiler/domain/plasmid.py | 30 ++++++++ src/buildcompiler/domain/reagent.py | 15 ++++ src/buildcompiler/domain/status.py | 25 +++++++ src/buildcompiler/domain/warnings.py | 17 +++++ tests/unit/domain/test_contracts.py | 84 ++++++++++++++++++++++ 13 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/domain/approvals.py create mode 100644 src/buildcompiler/domain/build_request.py create mode 100644 src/buildcompiler/domain/build_result.py create mode 100644 src/buildcompiler/domain/build_stage.py create mode 100644 src/buildcompiler/domain/design.py create mode 100644 src/buildcompiler/domain/material_state.py create mode 100644 src/buildcompiler/domain/missing_input.py create mode 100644 src/buildcompiler/domain/plasmid.py create mode 100644 src/buildcompiler/domain/reagent.py create mode 100644 src/buildcompiler/domain/status.py create mode 100644 src/buildcompiler/domain/warnings.py create mode 100644 tests/unit/domain/test_contracts.py diff --git a/src/buildcompiler/domain/__init__.py b/src/buildcompiler/domain/__init__.py index 5e19692..72eacbb 100644 --- a/src/buildcompiler/domain/__init__.py +++ b/src/buildcompiler/domain/__init__.py @@ -1 +1,34 @@ -"""Package scaffolding for clean architecture.""" +"""Domain contracts for BuildCompiler clean architecture.""" + +from .approvals import ApprovalStatus, RequiredApproval +from .build_request import BuildRequest +from .build_result import FullBuildResult, StageResult +from .build_stage import BuildStage +from .design import DesignKind +from .material_state import MaterialState +from .missing_input import MissingBuildInput +from .plasmid import IndexedBackbone, IndexedPlasmid +from .reagent import IndexedReagent +from .status import BuildStatus, StageStatus +from .warnings import BuildWarning + +__all__ = [ + "ApprovalStatus", + "BuildRequest", + "BuildResult", + "BuildStage", + "BuildStatus", + "BuildWarning", + "DesignKind", + "FullBuildResult", + "IndexedBackbone", + "IndexedPlasmid", + "IndexedReagent", + "MaterialState", + "MissingBuildInput", + "RequiredApproval", + "StageResult", + "StageStatus", +] + +BuildResult = FullBuildResult diff --git a/src/buildcompiler/domain/approvals.py b/src/buildcompiler/domain/approvals.py new file mode 100644 index 0000000..2ebbd45 --- /dev/null +++ b/src/buildcompiler/domain/approvals.py @@ -0,0 +1,22 @@ +"""Approval contracts for expected gated processes.""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + + +class ApprovalStatus(str, Enum): + """Minimal approval state used by RequiredApproval.""" + + REQUIRED = "required" + APPROVED = "approved" + + +@dataclass +class RequiredApproval: + """Approval record for a process required to proceed.""" + + status: ApprovalStatus + process: str + reason: str + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/build_request.py b/src/buildcompiler/domain/build_request.py new file mode 100644 index 0000000..8bcd9c4 --- /dev/null +++ b/src/buildcompiler/domain/build_request.py @@ -0,0 +1,21 @@ +"""Build request contract dataclass.""" + +from dataclasses import dataclass, field +from typing import Any + +from .build_stage import BuildStage +from .design import DesignKind + + +@dataclass +class BuildRequest: + """Planner-produced request for a single stage/source item.""" + + id: str + stage: BuildStage + source_identity: str + source_display_id: str | None + source_kind: DesignKind + parent_group: str | None = None + variant_index: int | None = None + constraints: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/build_result.py b/src/buildcompiler/domain/build_result.py new file mode 100644 index 0000000..bc252cb --- /dev/null +++ b/src/buildcompiler/domain/build_result.py @@ -0,0 +1,50 @@ +"""Stage/full-build result contracts.""" + +from dataclasses import dataclass, field +from typing import Any + +from .approvals import RequiredApproval +from .build_stage import BuildStage +from .missing_input import MissingBuildInput +from .plasmid import IndexedPlasmid +from .status import BuildStatus, StageStatus +from .warnings import BuildWarning + + +@dataclass +class StageResult: + """Output contract from a single stage invocation.""" + + id: str + stage: BuildStage + status: StageStatus + request_ids: list[str] = field(default_factory=list) + products: list[IndexedPlasmid] = field(default_factory=list) + missing_inputs: list[MissingBuildInput] = field(default_factory=list) + required_approvals: list[RequiredApproval] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + sbol_document: Any | None = None + json_intermediate: dict[str, Any] | list[Any] | None = None + protocol_artifacts: dict[str, Any] = field(default_factory=dict) + logs: list[str] = field(default_factory=list) + + +@dataclass +class FullBuildResult: + """Aggregate full-build output contract. + + Neighboring contracts (plan/graph/summary/report) are intentionally typed + conservatively until their milestone implementations are added. + """ + + status: BuildStatus + plan: Any + build_document: Any + stage_results: list[StageResult] = field(default_factory=list) + graph: Any = None + final_products: list[IndexedPlasmid] = field(default_factory=list) + missing_inputs: list[MissingBuildInput] = field(default_factory=list) + required_approvals: list[RequiredApproval] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + summary: Any = None + report: Any | None = None diff --git a/src/buildcompiler/domain/build_stage.py b/src/buildcompiler/domain/build_stage.py new file mode 100644 index 0000000..a3b52a9 --- /dev/null +++ b/src/buildcompiler/domain/build_stage.py @@ -0,0 +1,13 @@ +"""Build stage enum contracts.""" + +from enum import Enum + + +class BuildStage(str, Enum): + """Planned v1 build stages for full-build execution.""" + + DOMESTICATION = "domestication" + ASSEMBLY_LVL1 = "assembly_lvl1" + ASSEMBLY_LVL2 = "assembly_lvl2" + TRANSFORMATION = "transformation" + PLATING = "plating" diff --git a/src/buildcompiler/domain/design.py b/src/buildcompiler/domain/design.py new file mode 100644 index 0000000..c11bedc --- /dev/null +++ b/src/buildcompiler/domain/design.py @@ -0,0 +1,12 @@ +"""Design identity/type contracts.""" + +from enum import Enum + + +class DesignKind(str, Enum): + """Supported SBOL design source kinds for build requests.""" + + COMPONENT_DEFINITION = "component_definition" + MODULE_DEFINITION = "module_definition" + COMBINATORIAL_DERIVATION = "combinatorial_derivation" + UNSUPPORTED = "unsupported" diff --git a/src/buildcompiler/domain/material_state.py b/src/buildcompiler/domain/material_state.py new file mode 100644 index 0000000..ea5b08e --- /dev/null +++ b/src/buildcompiler/domain/material_state.py @@ -0,0 +1,13 @@ +"""Material lifecycle states used across stages.""" + +from enum import Enum + + +class MaterialState(str, Enum): + """Normalized lifecycle states for build materials.""" + + PLANNED = "planned" + GENERATED = "generated" + ASSEMBLED = "assembled" + TRANSFORMED = "transformed" + PLATED = "plated" diff --git a/src/buildcompiler/domain/missing_input.py b/src/buildcompiler/domain/missing_input.py new file mode 100644 index 0000000..1210eb9 --- /dev/null +++ b/src/buildcompiler/domain/missing_input.py @@ -0,0 +1,32 @@ +"""Missing input/blocker contracts.""" + +from dataclasses import dataclass, field +from typing import Literal + +from .build_stage import BuildStage + +MissingKind = Literal[ + "engineered_region", + "promoter", + "rbs", + "cds", + "terminator", + "backbone", + "restriction_enzyme", + "ligase", + "reagent", +] + + +@dataclass +class MissingBuildInput: + """Expected blocker produced when build inputs are unavailable.""" + + source_stage: BuildStage + source_design_identity: str + missing_identity: str + missing_display_id: str | None + missing_kind: MissingKind + required_stage: BuildStage | Literal["fatal"] + reason: str + candidates_tried: list[str] = field(default_factory=list) diff --git a/src/buildcompiler/domain/plasmid.py b/src/buildcompiler/domain/plasmid.py new file mode 100644 index 0000000..3a2e0f7 --- /dev/null +++ b/src/buildcompiler/domain/plasmid.py @@ -0,0 +1,30 @@ +"""Normalized plasmid/backbone records.""" + +from dataclasses import dataclass, field +from typing import Any + +from .material_state import MaterialState + + +@dataclass +class IndexedPlasmid: + """Inventory/index record for a plasmid-like material.""" + + identity: str + display_id: str | None = None + name: str | None = None + state: MaterialState = MaterialState.PLANNED + roles: list[str] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + sbol_component: Any | None = None + + +@dataclass +class IndexedBackbone: + """Inventory/index record for a backbone material.""" + + identity: str + display_id: str | None = None + name: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) + sbol_component: Any | None = None diff --git a/src/buildcompiler/domain/reagent.py b/src/buildcompiler/domain/reagent.py new file mode 100644 index 0000000..b50e690 --- /dev/null +++ b/src/buildcompiler/domain/reagent.py @@ -0,0 +1,15 @@ +"""Normalized reagent record contracts.""" + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class IndexedReagent: + """Inventory/index record for reagents.""" + + identity: str + display_id: str | None = None + name: str | None = None + reagent_type: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/status.py b/src/buildcompiler/domain/status.py new file mode 100644 index 0000000..aac2ba3 --- /dev/null +++ b/src/buildcompiler/domain/status.py @@ -0,0 +1,25 @@ +"""Status enums and contract-level semantics helpers.""" + +from enum import Enum + + +class StageStatus(str, Enum): + """Status for a single stage result. + + BLOCKED means expected inputs or approvals can unblock this stage later. + FAILED means the request cannot proceed without changing design/options/ + collections/approval state. + """ + + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + BLOCKED = "blocked" + FAILED = "failed" + + +class BuildStatus(str, Enum): + """Status for full-build aggregate results.""" + + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + FAILED = "failed" diff --git a/src/buildcompiler/domain/warnings.py b/src/buildcompiler/domain/warnings.py new file mode 100644 index 0000000..72f130d --- /dev/null +++ b/src/buildcompiler/domain/warnings.py @@ -0,0 +1,17 @@ +"""Domain warning contracts.""" + +from dataclasses import dataclass, field +from typing import Any + +from .build_stage import BuildStage + + +@dataclass +class BuildWarning: + """Structured non-fatal warning for planning/execution/reporting.""" + + code: str + message: str + stage: BuildStage | None = None + source_identity: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/tests/unit/domain/test_contracts.py b/tests/unit/domain/test_contracts.py new file mode 100644 index 0000000..e85183e --- /dev/null +++ b/tests/unit/domain/test_contracts.py @@ -0,0 +1,84 @@ +from buildcompiler.domain import ( + ApprovalStatus, + BuildRequest, + BuildStage, + BuildStatus, + BuildWarning, + DesignKind, + MissingBuildInput, + RequiredApproval, + StageResult, + StageStatus, +) + + +def test_enum_values_and_planned_stages(): + assert StageStatus.BLOCKED.value == "blocked" + assert StageStatus.FAILED.value == "failed" + assert BuildStatus.SUCCESS.value == "success" + assert BuildStage.ASSEMBLY_LVL1.value == "assembly_lvl1" + assert BuildStage.DOMESTICATION.value == "domestication" + assert BuildStage.ASSEMBLY_LVL2.value == "assembly_lvl2" + assert BuildStage.TRANSFORMATION.value == "transformation" + assert BuildStage.PLATING.value == "plating" + + +def test_blocked_vs_failed_semantics_are_documented_as_contract_data(): + blocked = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity="sbol://design/x", + missing_identity="sbol://part/y", + missing_display_id="y", + missing_kind="cds", + required_stage=BuildStage.DOMESTICATION, + reason="Input could be generated by upstream stage", + ) + failed = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity="sbol://design/x", + missing_identity="sbol://part/z", + missing_display_id="z", + missing_kind="cds", + required_stage="fatal", + reason="Unsupported design cannot proceed without changes", + ) + + assert blocked.required_stage != "fatal" + assert failed.required_stage == "fatal" + + +def test_default_mutables_are_isolated_across_instances(): + req1 = BuildRequest("r1", BuildStage.DOMESTICATION, "a", None, DesignKind.COMPONENT_DEFINITION) + req2 = BuildRequest("r2", BuildStage.DOMESTICATION, "b", None, DesignKind.COMPONENT_DEFINITION) + req1.constraints["x"] = 1 + assert req2.constraints == {} + + s1 = StageResult("s1", BuildStage.DOMESTICATION, StageStatus.SUCCESS) + s2 = StageResult("s2", BuildStage.DOMESTICATION, StageStatus.SUCCESS) + s1.request_ids.append("r1") + s1.products.append(object()) + s1.missing_inputs.append( + MissingBuildInput(BuildStage.DOMESTICATION, "a", "b", None, "promoter", "fatal", "missing") + ) + s1.required_approvals.append(RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed")) + s1.warnings.append(BuildWarning("warn", "msg")) + s1.protocol_artifacts["x"] = "y" + s1.logs.append("log") + + assert s2.request_ids == [] + assert s2.products == [] + assert s2.missing_inputs == [] + assert s2.required_approvals == [] + assert s2.warnings == [] + assert s2.protocol_artifacts == {} + assert s2.logs == [] + + a1 = RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed") + a2 = RequiredApproval(ApprovalStatus.APPROVED, "biosafety", "granted") + a1.metadata["id"] = "1" + assert a2.metadata == {} + + w1 = BuildWarning("w1", "warning") + w2 = BuildWarning("w2", "warning") + w1.metadata["code"] = "X" + assert w2.metadata == {} From f16874b146efe9e76d1569ed4a9f23c892118a72 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 10:33:15 -0600 Subject: [PATCH 2/2] Raise declared minimum Python version to 3.10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a0c13bb..f7c72fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "synbio-buildcompiler" version = "0.0.a1" description = "BuildCompiler is an open-source tool that bridges the Design and Build stages of the Synthetic Biology DBTL cycle" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.10" license = {file = "LICENSE.md"} keywords = ["SBOL", "genetic", "automation", "build", "synthetic biology"] authors = [