# Story 1.3: Port and Connection System Status: ready-for-dev ## Story As a **thermodynamic systems developer**, I want **a type-safe port and connection system with compile-time state validation**, so that **I can prevent runtime connection errors and ensure fluid compatibility between components**. ## Acceptance Criteria ### AC 1: Port Creation **Given** a fluid type and thermodynamic state **When** I create a new port **Then** it initializes in `Disconnected` state with `fluid_id`, `pressure`, and `enthalpy` ### AC 2: Fluid Compatibility Validation **Given** two ports with different `fluid_id` values **When** I attempt to connect them **Then** the connection fails with `ConnectionError::IncompatibleFluid` ### AC 3: Pressure Continuity Validation **Given** two ports with significantly different pressure values **When** I attempt to connect them **Then** the connection fails with `ConnectionError::PressureMismatch` ### AC 4: Enthalpy Continuity Validation **Given** two ports with significantly different enthalpy values **When** I attempt to connect them **Then** the connection fails with `ConnectionError::EnthalpyMismatch` ### AC 5: Successful Connection **Given** two compatible ports (same fluid, matching pressure/enthalpy within tolerance) **When** I connect them **Then** I receive two `Port` instances with averaged thermodynamic values ### AC 6: Connected Port Operations **Given** a `Port` instance **When** I access its properties **Then** I can read `pressure()`, `enthalpy()`, and modify them via `set_pressure()`, `set_enthalpy()` ### AC 7: Compile-Time Type Safety **Given** a `Port` instance **When** code attempts to call `pressure()` or `set_pressure()` on it **Then** the compilation fails (Type-State pattern enforcement) ### AC 8: Component Trait Integration **Given** the `Component` trait **When** I implement it for a component **Then** I can provide `get_ports()` to expose the component's connection points ## Tasks / Subtasks - [x] **Task 1: Define Type-State Pattern Foundation** (AC: 1, 7) - [x] Create `Disconnected` and `Connected` marker structs - [x] Define generic `Port` struct with `PhantomData` - [x] Implement zero-cost state tracking - [x] **Task 2: Implement Port Creation** (AC: 1) - [x] Create `Port::new()` constructor for `Port` - [x] Store `fluid_id: FluidId`, `pressure: Pressure`, `enthalpy: Enthalpy` - [x] **Task 3: Define Connection Errors** (AC: 2, 3, 4) - [x] Create `ConnectionError` enum with `thiserror` - [x] Add `IncompatibleFluid { from, to }` variant - [x] Add `PressureMismatch { from_pressure, to_pressure }` variant - [x] Add `EnthalpyMismatch { from_enthalpy, to_enthalpy }` variant - [x] **Task 4: Implement Connection Logic** (AC: 2, 3, 4, 5) - [x] Add `Port::connect()` method - [x] Validate fluid compatibility - [x] Validate pressure continuity (tolerance: 1e-6 Pa) - [x] Validate enthalpy continuity (tolerance: 1e-6 J/kg) - [x] Return averaged values on success - [x] **Task 5: Implement Connected Port Operations** (AC: 6) - [x] Add `pressure()` getter to `Port` - [x] Add `enthalpy()` getter to `Port` - [x] Add `set_pressure()` setter to `Port` - [x] Add `set_enthalpy()` setter to `Port` - [x] Add `fluid_id()` accessor (available in both states) - [x] **Task 6: Extend Component Trait** (AC: 8) - [x] Add `get_ports()` method to `Component` trait - [x] Return appropriate port references for each component type - [x] **Task 7: Write Tests** - [x] Test port creation with valid parameters - [x] Test connection with compatible ports - [x] Test error handling for incompatible fluids - [x] Test error handling for pressure mismatches - [x] Test error handling for enthalpy mismatches - [x] Test value modification on connected ports - [x] Add compile-time safety verification test ## Dev Notes ### Type-State Pattern Implementation The Type-State pattern uses Rust's type system to encode state machines at compile time: ```rust // Zero-cost marker types pub struct Disconnected; pub struct Connected; // Generic Port with state parameter pub struct Port { fluid_id: FluidId, pressure: Pressure, enthalpy: Enthalpy, _state: PhantomData, // Zero-cost at runtime } // Only Disconnected ports can be connected impl Port { pub fn connect(self, other: Port) -> Result<(Port, Port), ConnectionError> { // Validation logic... } } // Only Connected ports expose mutable operations impl Port { pub fn pressure(&self) -> Pressure { self.pressure } pub fn set_pressure(&mut self, pressure: Pressure) { self.pressure = pressure } } ``` ### Connection Validation Logic ```rust pub fn connect(self, other: Port) -> Result<(Port, Port), ConnectionError> { // 1. Fluid compatibility check if self.fluid_id != other.fluid_id { return Err(ConnectionError::IncompatibleFluid { from: self.fluid_id.to_string(), to: other.fluid_id.to_string() }); } // 2. Pressure continuity (1e-6 Pa tolerance) let pressure_diff = (self.pressure.to_pascals() - other.pressure.to_pascals()).abs(); if pressure_diff > 1e-6 { return Err(ConnectionError::PressureMismatch { from_pressure: self.pressure.to_pascals(), to_pressure: other.pressure.to_pascals() }); } // 3. Enthalpy continuity (1e-6 J/kg tolerance) let enthalpy_diff = (self.enthalpy.to_joules_per_kg() - other.enthalpy.to_joules_per_kg()).abs(); if enthalpy_diff > 1e-6 { return Err(ConnectionError::EnthalpyMismatch { from_enthalpy: self.enthalpy.to_joules_per_kg(), to_enthalpy: other.enthalpy.to_joules_per_kg() }); } // 4. Create connected ports with averaged values let avg_pressure = Pressure::from_pascals( (self.pressure.to_pascals() + other.pressure.to_pascals()) / 2.0 ); let avg_enthalpy = Enthalpy::from_joules_per_kg( (self.enthalpy.to_joules_per_kg() + other.enthalpy.to_joules_per_kg()) / 2.0 ); Ok(( Port { fluid_id: self.fluid_id, pressure: avg_pressure, enthalpy: avg_enthalpy, _state: PhantomData, }, Port { fluid_id: other.fluid_id, pressure: avg_pressure, enthalpy: avg_enthalpy, _state: PhantomData, } )) } ``` ### Testing Approach Use the `approx` crate for floating-point assertions: ```rust #[cfg(test)] mod tests { use super::*; use approx::assert_relative_eq; #[test] fn test_port_creation() { let port = Port::new( FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400_000.0) ); assert_eq!(port.fluid_id().to_string(), "R134a"); } #[test] fn test_incompatible_fluid_error() { let port1 = Port::new(FluidId::new("R134a"), p1, h1); let port2 = Port::new(FluidId::new("Water"), p1, h1); match port1.connect(port2) { Err(ConnectionError::IncompatibleFluid { .. }) => (), // Expected _ => panic!("Expected IncompatibleFluid error"), } } } ``` ### Compile-Time Safety Verification Create a test that intentionally fails to compile: ```rust #[test] fn test_compile_time_safety() { let port: Port = Port::new( FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400_000.0) ); // This line should NOT compile - uncomment to verify: // let _p = port.pressure(); // ERROR: no method `pressure` on Port // This should work after connection: let port2 = Port::new(FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400_000.0)); let (mut connected, _) = port.connect(port2).unwrap(); let _p = connected.pressure(); // OK: Port has pressure() } ``` ## Project Structure Notes ### File Locations ``` crates/ ├── components/ │ ├── Cargo.toml # Add: entropyk-core dependency, approx dev-dependency │ └── src/ │ ├── lib.rs # Add: pub mod port; extend Component trait │ └── port.rs # NEW: Port implementation │ └── core/ └── src/ └── types.rs # Pressure, Enthalpy, FluidId definitions ``` ### Dependencies to Add **In `crates/components/Cargo.toml`:** ```toml [dependencies] entropyk-core = { path = "../core" } thiserror = "1.0" [dev-dependencies] approx = "0.5" ``` ### Alignment with Project Structure - **Path convention**: All new code in `crates/components/src/` - **Naming**: Module name `port.rs` matches functionality - **Trait extension**: `Component` trait extended with `get_ports()` method - **Error handling**: Uses `thiserror` crate (standard in Rust ecosystem) - **Testing**: Uses `approx` for floating-point comparisons (recommended for thermodynamic calculations) ## References ### Technical Documentation - [Source: README_STORY_1_3.md] - Original implementation documentation - [Source: demo/README.md] - Feature status tracking (Story 1.3: Ports & Connexions) - [Source: docs/TUTORIAL.md#5] - Port system usage in tutorial ### External Resources - [Rust Type-State Pattern](https://rust-unofficial.github.io/patterns/patterns/behavioural/phantom-types.html) - Official Rust patterns documentation - [thiserror Documentation](https://docs.rs/thiserror/) - Error handling derive macro - [approx Documentation](https://docs.rs/approx/) - Floating-point assertion macros ### Related Stories - Story 1.1: Component Trait (foundation trait extended with get_ports) - Story 1.2: Physical Types (Pressure, Enthalpy, Temperature) - Story 1.4: Compressor (first component using port system) ## Dev Agent Record ### Agent Model Used N/A - Story documentation created retroactively for existing implementation. ### Debug Log References N/A - Implementation completed prior to story documentation. ### Completion Notes List 1. ✅ Type-State pattern successfully prevents runtime port state errors 2. ✅ All validation rules (fluid, pressure, enthalpy) implemented 3. ✅ Tests cover success and error cases 4. ✅ Component trait extended with `get_ports()` method 5. ✅ Documentation includes compile-time safety verification example ### File List **Created/Modified:** - `crates/components/src/port.rs` - Port implementation with Type-State pattern - `crates/components/src/lib.rs` - Extended Component trait with get_ports() - `crates/components/Cargo.toml` - Added entropyk-core dependency, thiserror, approx **Dependencies:** - `entropyk-core` - Physical types (Pressure, Enthalpy, FluidId) - `thiserror` - Error derive macro - `approx` - Floating-point test assertions (dev dependency) --- **Story Completion Status**: ready-for-dev **Ultimate context engine analysis completed** - comprehensive developer guide created **Date**: 2026-02-22 **Created by**: create-story workflow (BMAD v6.0.1)