Entropyk/_bmad-output/implementation-artifacts/3-2-port-compatibility-validation.md

258 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Story 3.2: Port Compatibility Validation
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## Story
As a system designer,
I want port connection validation at build time,
so that incompatible connections are caught early.
## Acceptance Criteria
1. **Incompatible Fluid Rejection** (AC: #1)
- [x] Given two ports with incompatible fluids (e.g., R134a vs Water)
- [x] When attempting to connect
- [x] Then connection fails with clear error (e.g., `ConnectionError::IncompatibleFluid` or `TopologyError`)
- [x] And error message identifies both fluids
2. **Valid Connections Accepted** (AC: #2)
- [x] Given two ports with same fluid, matching pressure and enthalpy within tolerance
- [x] When attempting to connect
- [x] Then connection is accepted
- [x] And edge is added to system graph
3. **Pressure/Enthalpy Continuity Enforced** (AC: #3)
- [x] Given two ports with same fluid but pressure or enthalpy mismatch beyond tolerance
- [x] When attempting to connect
- [x] Then connection fails with clear error
- [x] And tolerance follows existing port.rs constants (PRESSURE_TOLERANCE_FRACTION, ENTHALPY_TOLERANCE_J_KG)
## Tasks / Subtasks
- [x] Integrate port validation into System graph build (AC: #1, #2, #3)
- [x] Extend `add_edge` API to specify port indices: `add_edge_with_ports(source_node, source_port_idx, target_node, target_port_idx)` -> Result<EdgeIndex, ConnectionError>
- [x] When adding edge: retrieve ports via `component.get_ports()` for source and target nodes
- [x] Compare `fluid_id()` of outlet port (source) vs inlet port (target) — reject if different
- [x] Compare pressure and enthalpy within tolerance — reject if mismatch
- [x] Return `Result<EdgeIndex, ConnectionError>` on failure
- [x] Error handling and propagation (AC: #1)
- [x] Reuse `ConnectionError` from port (re-exported in solver)
- [x] Add `ConnectionError::InvalidPortIndex` for out-of-bounds port indices
- [x] Ensure error messages are clear and actionable
- [x] Tests
- [x] Test: connect R134a outlet to R134a inlet — succeeds
- [x] Test: connect R134a outlet to Water inlet — fails with IncompatibleFluid
- [x] Test: connect with pressure mismatch — fails with PressureMismatch
- [x] Test: connect with enthalpy mismatch — fails with EnthalpyMismatch
- [x] Test: valid connection in 4-node cycle — all edges accepted
## Dev Notes
### Epic Context
**Epic 3: System Topology (Graph)** — Enable component assembly via Ports and manage multi-circuits with thermal coupling. FR10 (component connection via Ports) and FR9FR13 map to `crates/solver` and `crates/components`.
**Story Dependencies:**
- Epic 1 (Component trait, Ports, State machine) — done
- Story 3.1 (System graph structure) — done
- `port.rs` already has `Port::connect()` with fluid, pressure, enthalpy validation — reuse logic or call from solver
### Architecture Context (Step 3.2 — CRITICAL EXTRACTION)
**Technical Stack:**
- Rust, petgraph 0.6.x, thiserror, entropyk-core, entropyk-components
- No new external dependencies required
**Code Structure:**
- `crates/solver/src/system.rs` — primary modification site
- `crates/solver/src/error.rs` — extend TopologyError if needed
- `crates/components/src/port.rs` — existing ConnectionError, FluidId, Port validation logic
**API Patterns:**
- Current: `add_edge(source: NodeIndex, target: NodeIndex) -> EdgeIndex` (no port validation)
- Target: Either extend to `add_edge(source, source_port, target, target_port) -> Result<EdgeIndex, ConnectionError>` or validate in `finalize()` by traversing edges and checking component ports
- Convention: Components typically have `get_ports()` returning `[inlet, outlet]` for 2-port components; multi-port (economizer) need explicit port indices
**Relevant Architecture Sections:**
- **Type-State for Connection Safety** (architecture line 239250): Ports use Disconnected/Connected; connection validation at connect time
- **Component Trait** (architecture line 231237): `get_ports() -> &[Port]` provides port access
- **Error Handling** (architecture line 276308): ThermoError, Result<T, E> throughout; zero-panic policy
- **System Topology** (architecture line 617619): FR9FR13 in solver/system.rs
**Performance Requirements:**
- Validation at build time only (not in solver hot path)
- No dynamic allocation in solver loop — validation happens before finalize()
**Testing Standards:**
- `approx::assert_relative_eq!` for float comparisons
- Tolérance pression: 1e-4 relative ou 1 Pa min (port.rs)
- Tolérance enthalpie: 100 J/kg (port.rs)
**Integration Patterns:**
- Solver depends on components; components expose `get_ports()` and `ConnectionError`
- May need to re-export or map `ConnectionError` from components in solver crate
### Developer Context
**Existing Implementation:**
- `port.rs::Port::connect()` already validates: IncompatibleFluid, PressureMismatch, EnthalpyMismatch
- `port.rs` constants: PRESSURE_TOLERANCE_FRACTION=1e-4, ENTHALPY_TOLERANCE_J_KG=100, MIN_PRESSURE_TOLERANCE_PA=1
- `system.rs::add_edge()` currently accepts any (source, target) without port validation
- `system.rs::validate_topology()` checks isolated nodes only; comment says "Story 3.2" for port validation
- `TopologyError` has `UnconnectedPorts` and `InvalidTopology` (allow dead_code) — ready for use
**Design Decision:**
- Option A: Extend `add_edge(source, source_port_idx, target, target_port_idx)` — explicit, validates at add time
- Option B: Add `validate_port_compatibility()` in `finalize()` — traverses edges, infers port mapping from graph (e.g., outgoing edge from node = outlet port, incoming = inlet)
- Option B is simpler if graph structure implies port mapping (one outlet per node for simple components); Option A is more flexible for multi-port components
**Port Mapping Convention:**
- For 2-port components: `get_ports()[0]` = inlet, `get_ports()[1]` = outlet (verify in compressor, condenser, etc.)
- For economizer (4 ports): explicit port indices required
### Technical Requirements
**Validation Rules:**
1. Fluid compatibility: `source_port.fluid_id() == target_port.fluid_id()`
2. Pressure continuity: `|P_source - P_target| <= max(P * 1e-4, 1 Pa)`
3. Enthalpy continuity: `|h_source - h_target| <= 100 J/kg`
**Error Types:**
- Reuse `ConnectionError::IncompatibleFluid` from entropyk_components
- Reuse `ConnectionError::PressureMismatch`, `EnthalpyMismatch` or add `TopologyError::IncompatiblePorts` with nested cause
- Solver crate depends on components — can use `ConnectionError` via `entropyk_components::ConnectionError`
### Architecture Compliance
- **NewType pattern**: Use `Pressure`, `Enthalpy` from core (ports already use them)
- **No bare f64** in public API
- **tracing** for validation failures (e.g., `tracing::warn!("Port validation failed: {}", err)`)
- **Result<T, E>** — no unwrap/expect in production
- **approx** for float assertions in tests
### Library/Framework Requirements
- **entropyk_components**: ConnectionError, FluidId, Port, ConnectedPort, Component::get_ports
- **petgraph**: Graph, NodeIndex, EdgeIndex — no change
- **thiserror**: TopologyError extension if needed
### File Structure Requirements
**Modified files:**
- `crates/solver/src/system.rs` — add port validation in add_edge or finalize
- `crates/solver/src/error.rs` — possibly add TopologyError variants or use ConnectionError
- `crates/solver/Cargo.toml` — ensure entropyk_components dependency (already present)
**No new files required** unless extracting validation to a separate module (e.g., `validation.rs`).
### Testing Requirements
**Unit tests (system.rs or validation module):**
- `test_valid_connection_same_fluid` — R134a to R134a, matching P/h
- `test_incompatible_fluid_rejected` — R134a to Water
- `test_pressure_mismatch_rejected` — same fluid, P differs > tolerance
- `test_enthalpy_mismatch_rejected` — same fluid, h differs > 100 J/kg
- `test_simple_cycle_port_validation` — 4 components, 4 edges, all valid
**Integration:**
- Use real components (e.g., Compressor, Condenser) with ConnectedPort if available, or mock components with get_ports() returning ports with specific FluidId/P/h
### Project Structure Notes
- Architecture specifies `crates/solver/src/system.rs` for topology — matches
- Story 3.1 created System with add_edge, finalize, validate_topology
- Story 3.2 extends validation to port compatibility
- Story 3.3 (Multi-Circuit) will add circuit tracking — no conflict
### Previous Story Intelligence (3.1)
- System uses `Graph<Box<dyn Component>, FlowEdge, Directed>`
- `add_edge(source, target)` returns EdgeIndex; finalize() assigns state indices
- MockComponent in tests has `get_ports() -> &[]` — need mock with non-empty ports for 3.2 tests
- TopologyError::IsolatedNode already used; UnconnectedPorts/InvalidTopology reserved
- `traverse_for_jacobian` yields (node, component, edge_indices); components get state via edge mapping
### References
- **Epic 3 Story 3.2:** [Source: planning-artifacts/epics.md#Story 3.2]
- **Architecture FR10:** [Source: planning-artifacts/architecture.md — Component connection via Ports]
- **Architecture Type-State:** [Source: planning-artifacts/architecture.md — line 239]
- **port.rs ConnectionError:** [Source: crates/components/src/port.rs]
- **port.rs validation constants:** [Source: crates/components/src/port.rs — PRESSURE_TOLERANCE_FRACTION, etc.]
- **system.rs validate_topology:** [Source: crates/solver/src/system.rs — line 114]
- **Story 3.1:** [Source: implementation-artifacts/3-1-system-graph-structure.md]
## Change Log
- 2026-02-15: Story 3.2 implementation complete. Added add_edge_with_ports with port validation, validate_port_continuity in port.rs, ConnectionError::InvalidPortIndex. All ACs satisfied, 5 new port validation tests + 2 port.rs tests.
- 2026-02-17: Code review complete. Fixed File List documentation, enhanced InvalidPortIndex error messages with context, added pressure tolerance boundary test. Status: review → done. All tests passing (43 solver + 297 components).
## Dev Agent Record
### Agent Model Used
{{agent_model_name_version}}
### Debug Log References
### Completion Notes List
- Added `validate_port_continuity(outlet, inlet)` in port.rs reusing PRESSURE_TOLERANCE_FRACTION, ENTHALPY_TOLERANCE_J_KG
- Added `add_edge_with_ports(source, source_port_idx, target, target_port_idx)` -> Result<EdgeIndex, ConnectionError>
- Convention: port 0 = inlet, port 1 = outlet for 2-port components
- Components with no ports: add_edge (unvalidated) or add_edge_with_ports with empty ports skips validation
- ConnectionError re-exported from solver; InvalidPortIndex for invalid node/port indices
- 5 solver tests + 2 port.rs tests for validate_port_continuity
### File List
- crates/components/src/port.rs (modified: validate_port_continuity, ConnectionError::InvalidPortIndex)
- crates/components/src/lib.rs (modified: re-export validate_port_continuity)
- crates/solver/ (new: initial solver crate implementation)
- src/system.rs: System graph with add_edge_with_ports and port validation
- src/lib.rs: Public API exports including ConnectionError re-export
- src/error.rs: TopologyError and AddEdgeError definitions
- src/coupling.rs: ThermalCoupling for multi-circuit support
- src/graph.rs: Graph traversal utilities
- Cargo.toml: Dependencies on entropyk-components, entropyk-core, petgraph, thiserror, tracing
- crates/solver/tests/multi_circuit.rs (new: integration tests for multi-circuit topology)
- _bmad-output/implementation-artifacts/sprint-status.yaml (modified: 3-2 in-progress → review)
## Senior Developer Review (AI)
**Reviewer:** Code Review Agent
**Date:** 2026-02-17
**Outcome:** ✅ APPROVED
### Issues Found and Fixed
1. **Documentation Inaccuracy (MEDIUM)** - Fixed
- File List incorrectly stated solver files were "modified"
- Corrected to reflect `crates/solver/` is a **new** crate
2. **Error Message Context (LOW)** - Fixed
- `InvalidPortIndex` error lacked context about which index failed
- Enhanced to: `Invalid port index {index}: component has {port_count} ports (valid: 0..{max_index})`
3. **Test Coverage Gap (LOW)** - Fixed
- Added `test_pressure_tolerance_boundary()` to verify exact tolerance boundary behavior
- Tests both at-tolerance (success) and just-outside-tolerance (failure) cases
### Verification Results
- All 43 solver tests passing
- All 297 components tests passing
- All 5 doc-tests passing
- No compiler warnings (solver crate)
### Quality Assessment
- **Architecture Compliance:** ✅ Follows type-state pattern, NewType pattern
- **Error Handling:** ✅ Result<T,E> throughout, zero unwrap/expect in production
- **Test Coverage:** ✅ All ACs covered with unit tests
- **Documentation:** ✅ Clear docstrings, examples in public API
- **Security:** ✅ No injection risks, input validation at boundaries