# 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<Connected>` instances with averaged thermodynamic values

### AC 6: Connected Port Operations
**Given** a `Port<Connected>` 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<Disconnected>` 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<State>` struct with `PhantomData`
  - [x] Implement zero-cost state tracking
  
- [x] **Task 2: Implement Port Creation** (AC: 1)
  - [x] Create `Port::new()` constructor for `Port<Disconnected>`
  - [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<Disconnected>::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<Connected>`
  - [x] Add `enthalpy()` getter to `Port<Connected>`
  - [x] Add `set_pressure()` setter to `Port<Connected>`
  - [x] Add `set_enthalpy()` setter to `Port<Connected>`
  - [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<State> {
    fluid_id: FluidId,
    pressure: Pressure,
    enthalpy: Enthalpy,
    _state: PhantomData<State>,  // Zero-cost at runtime
}

// Only Disconnected ports can be connected
impl Port<Disconnected> {
    pub fn connect(self, other: Port<Disconnected>) 
        -> Result<(Port<Connected>, Port<Connected>), ConnectionError> {
        // Validation logic...
    }
}

// Only Connected ports expose mutable operations
impl Port<Connected> {
    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<Disconnected>) 
    -> Result<(Port<Connected>, Port<Connected>), 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<Disconnected> = 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<Disconnected>
    
    // 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<Connected> 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)
