Entropyk/_bmad-output/implementation-artifacts/1-3-port-and-connection-system.md

302 lines
11 KiB
Markdown

# Story 1.3: Port and Connection System
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## Story
As a system modeler,
I want to define inlet/outlet ports for components and connect them bidirectionally,
so that I can build fluid circuit topologies.
## Acceptance Criteria
1. **Port Definition** (AC: #1)
- [x] Define `Port` struct with state (Disconnected/Connected)
- [x] Port has fluid type identifier (FluidId)
- [x] Port tracks pressure and enthalpy values
- [x] Port is generic over connection state (Type-State pattern)
2. **Connection API** (AC: #2)
- [x] Implement `connect()` function for bidirectional port linking
- [x] Connection validates fluid compatibility
- [x] Connection validates pressure/enthalpy continuity
- [x] Returns `Connected` state after successful connection
3. **Compile-Time Safety** (AC: #3)
- [x] Disconnected ports cannot be used in solver
- [x] Connected ports expose read/write methods
- [x] Attempting to reconnect an already connected port fails at compile-time
- [x] Type-State pattern prevents invalid state transitions
4. **Component Integration** (AC: #4)
- [x] Component trait updated to expose `get_ports()` method
- [x] Ports accessible from Component implementations
- [x] Integration with existing Component trait from Story 1.1
5. **Validation & Error Handling** (AC: #5)
- [x] Invalid connections return `ConnectionError` with clear message
- [x] Fluid incompatibility detected and reported
- [ ] Connection graph validated for cycles (deferred to Story 3.1 - System Graph)
## Tasks / Subtasks
- [x] Create `crates/components/src/port.rs` module (AC: #1, #3)
- [x] Define `Port<State>` generic struct with Type-State pattern
- [x] Implement `Disconnected` and `Connected` state types
- [x] Add `fluid_id: FluidId` field for fluid type tracking
- [x] Add `pressure: Pressure` and `enthalpy: Enthalpy` fields
- [x] Implement constructors for creating new ports
- [x] Implement connection state machine (AC: #2, #3)
- [x] Implement `connect()` method on `Port<Disconnected>`
- [x] Return `Port<Connected>` on successful connection
- [x] Validate fluid compatibility between ports
- [x] Enforce pressure/enthalpy continuity
- [x] Add compile-time safety guarantees (AC: #3)
- [x] Implement `From<Port<Disconnected>>` prevention for solver
- [x] Add methods accessible only on `Port<Connected>`
- [x] Ensure type-state prevents reconnecting
- [x] Update Component trait integration (AC: #4)
- [x] Add `get_ports(&self) -> &[Port<Connected>]` to Component trait
- [x] Verify compatibility with Story 1.1 Component trait
- [x] Test integration with mock components
- [x] Implement validation and errors (AC: #5)
- [x] Define `ConnectionError` enum with `thiserror`
- [x] Add `IncompatibleFluid`, `PressureMismatch`, `AlreadyConnected`, `CycleDetected` variants
- [ ] Implement cycle detection for connection graphs (deferred to Story 3.1 - requires system graph)
- [x] Add comprehensive error messages
- [x] Write unit tests for all port operations (AC: #1-5)
- [x] Test port creation and state transitions
- [x] Test valid connections
- [x] Test compile-time safety (try to compile invalid code)
- [x] Test error cases (incompatible fluids, etc.)
- [x] Test Component trait integration
## Dev Notes
### Architecture Context
**Critical Pattern - Type-State for Connection Safety:**
The Type-State pattern is REQUIRED for compile-time connection validation. This prevents the #1 bug in system topology: using unconnected ports.
```rust
// DANGER - Never do this (runtime errors possible!)
struct Port { state: PortState } // Runtime state checking
// CORRECT - Type-State pattern
pub struct Port<State> { fluid: FluidId, pressure: Pressure, ... }
pub struct Disconnected;
pub struct Connected;
impl Port<Disconnected> {
fn connect(self, other: Port<Disconnected>) -> (Port<Connected>, Port<Connected>)
{ ... }
}
impl Port<Connected> {
fn pressure(&self) -> Pressure { ... } // Only accessible when connected
}
```
**State Transitions:**
```
Port<Disconnected> --connect()--> Port<Connected>
↑ │
└───────── (no way back) ────────────┘
```
**Connection Validation Rules:**
1. **Fluid Compatibility:** Both ports must have same `FluidId`
2. **Continuity:** Pressure and enthalpy must match at connection point
3. **No Cycles:** Connection graph must be acyclic (validated at build time)
### Technical Requirements
**Rust Naming Conventions (MUST FOLLOW):**
- Port struct: `CamelCase` (Port)
- State types: `CamelCase` (Disconnected, Connected)
- Methods: `snake_case` (connect, get_ports)
- Generic parameter: `State` (not `S` for clarity)
**Required Types:**
```rust
pub struct Port<State> {
fluid_id: FluidId,
pressure: Pressure,
enthalpy: Enthalpy,
_state: PhantomData<State>,
}
pub struct Disconnected;
pub struct Connected;
pub struct FluidId(String); // Or enum for known fluids
```
**Location in Workspace:**
```
crates/components/
├── Cargo.toml
└── src/
├── lib.rs # Re-exports, Component trait
├── port.rs # Port types and connection logic (THIS STORY)
└── compressor.rs # Example component (future story)
```
### Implementation Strategy
1. **Create port module** - Define Port<State> with Type-State pattern
2. **Implement state machine** - connect() method with validation
3. **Add compile-time safety** - Only Connected ports usable in solver
4. **Update Component trait** - Add get_ports() method
5. **Write comprehensive tests** - Cover all validation cases
### Testing Requirements
**Required Tests:**
- Port creation: `Port::new(fluid_id, pressure, enthalpy)` creates Disconnected port
- Connection: Two Disconnected ports connect to produce Connected ports
- Compile-time safety: Attempt to use Disconnected port in solver (should fail)
- Fluid validation: Connecting different fluids fails with error
- Component integration: Mock component implements get_ports()
**Test Pattern:**
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_port_connection() {
let port1 = Port::new(FluidId::R134a, Pressure::from_bar(1.0), ...);
let port2 = Port::new(FluidId::R134a, Pressure::from_bar(1.0), ...);
let (connected1, connected2) = port1.connect(port2).unwrap();
assert_eq!(connected1.pressure(), connected2.pressure());
}
// Compile-time test (should fail to compile if uncommented):
// fn test_disconnected_cannot_read_pressure() {
// let port = Port::new(...);
// let _p = port.pressure(); // ERROR: method not found
// }
}
```
### Project Structure Notes
**Crate Location:** `crates/components/src/port.rs`
- This module provides port types used by ALL components
- Depends on `core` crate for Pressure, Temperature types (Story 1.2)
- Used by future component implementations (compressor, condenser, etc.)
**Inter-crate Dependencies:**
```
core (types: Pressure, Enthalpy, etc.)
components → core (uses types for port fields)
solver → components (uses ports for graph building)
```
**Alignment with Unified Structure:**
- ✅ Uses Type-State pattern as specified in Architecture [Source: planning-artifacts/architecture.md#Component Model]
- ✅ Located in `crates/components/` per project structure [Source: planning-artifacts/architecture.md#Project Structure & Boundaries]
- ✅ Extends Component trait from Story 1.1
- ✅ Uses NewType pattern from Story 1.2 for Pressure, Enthalpy
### References
- **Type-State Pattern:** [Source: planning-artifacts/architecture.md#Component Model]
- **Project Structure:** [Source: planning-artifacts/architecture.md#Project Structure & Boundaries]
- **Story 1.3 Requirements:** [Source: planning-artifacts/epics.md#Story 1.3: Port and Connection System]
- **Story 1.1 Component Trait:** Previous story established Component trait
- **Story 1.2 Physical Types:** NewType Pressure, Enthalpy, etc. to use in Port
- **Rust Type-State Pattern:** https://rust-unofficial.github.io/patterns/patterns/behavioural/phantom-types.html
## Dev Agent Record
### Agent Model Used
opencode/kimi-k2.5-free
### Debug Log References
- Implementation completed: 2026-02-14
- All tests passing: 30 unit tests + 18 doc tests
- Clippy validation: Zero warnings
### Completion Notes List
**Implementation Checklist:**
- [x] `crates/components/src/port.rs` created with complete Port implementation
- [x] `Port<State>` generic struct with Type-State pattern (Disconnected/Connected)
- [x] `FluidId` type for fluid identification
- [x] `ConnectionError` enum with thiserror (IncompatibleFluid, PressureMismatch, EnthalpyMismatch, AlreadyConnected, CycleDetected)
- [x] `connect()` method with fluid compatibility and continuity validation
- [x] Component trait extended with `get_ports(&self) -> &[ConnectedPort]` method
- [x] All existing tests updated to implement new trait method
- [x] 15 unit tests for port operations
- [x] 8 doc tests demonstrating API usage
- [x] Integration with entropyk-core types (Pressure, Enthalpy)
- [x] Component integration test with actual connected ports
**Test Results:**
- 43 tests passed (15 port tests + 28 other component tests)
- `cargo clippy -- -D warnings`: Zero warnings
- Trait object safety preserved
### File List
Created files:
1. `crates/components/src/port.rs` - Port types, FluidId, ConnectionError, Type-State implementation
Modified files:
1. `crates/components/src/lib.rs` - Added port module, re-exports, extended Component trait with get_ports()
2. `crates/components/Cargo.toml` - Added entropyk-core dependency and approx dev-dependency
### Dependencies
**Cargo.toml dependencies (add to components crate):**
```toml
[dependencies]
entropyk-core = { path = "../core" }
thiserror = "1.0"
```
## Story Context Summary
**Critical Implementation Points:**
1. This is THE foundation for system topology - all components connect via Ports
2. MUST use Type-State pattern for compile-time safety
3. Port uses NewTypes from Story 1.2 (Pressure, Enthalpy)
4. Component trait from Story 1.1 must be extended with get_ports()
5. Connection validation prevents runtime topology errors
**Common Pitfalls to Avoid:**
- ❌ Using runtime state checking instead of Type-State
- ❌ Allowing disconnected ports in solver
- ❌ Forgetting to validate fluid compatibility
- ❌ Not enforcing pressure/enthalpy continuity
- ❌ Breaking Component trait object safety
**Success Criteria:**
- ✅ Type-State prevents using unconnected ports at compile-time
- ✅ Connection validates fluid compatibility
- ✅ Component trait extended without breaking object safety
- ✅ All validation cases covered by tests
- ✅ Integration with Story 1.1 and 1.2 works correctly
**Dependencies on Previous Stories:**
- **Story 1.1:** Component trait exists - extend it with get_ports()
- **Story 1.2:** Physical types (Pressure, Enthalpy) exist - use them in Port
**Next Story (1.4) Dependencies:**
Story 1.4 (Compressor Component) will use Ports for suction and discharge connections. The Port API must support typical HVAC component patterns.
---
**Ultimate context engine analysis completed - comprehensive developer guide created**