# ADR-006: Domain Layer Purity -- DIP Fix, Singleton Removal, Enum Introduction

## Status

Proposed

## Context

Three violations of domain layer purity were identified in the DES codebase:

1. **DIP violation**: `src/des/domain/invocation_limits_validator.py` (line 56) conditionally imports `RealFileSystem` from the adapter layer when its `filesystem` parameter is `None`. The domain layer must have zero imports from `adapters/`.

2. **Global mutable singleton**: `src/des/domain/tdd_schema.py` uses `_global_loader` module-level variable with convenience functions `get_tdd_schema()`, `get_tdd_schema_loader()`, `reset_global_schema_loader()`. Global mutable state in the domain layer creates hidden coupling, makes test isolation fragile, and violates the principle that domain objects should be created and composed at composition roots.

3. **Primitive obsession**: `src/des/domain/phase_event.py` uses `str` for `status` ("EXECUTED", "SKIPPED", etc.) and `phase_name` ("PREPARE", "RED_ACCEPTANCE", etc.) where domain enums would encode valid values and prevent invalid state at construction time.

Business drivers:
- **Testability**: global singletons and adapter imports in domain make isolated testing harder
- **Maintainability**: domain layer is the most stable layer; violations erode its stability over time
- **Reliability**: stringly-typed fields allow invalid values to propagate silently

## Decision

### Fix 1: Move InvocationLimitsValidator to application layer

Move `invocation_limits_validator.py` from `src/des/domain/` to `src/des/application/`. Make the `filesystem` parameter required (no default, no conditional adapter import). The orchestrator already passes `self._filesystem` explicitly.

### Fix 2: Remove global singleton functions from tdd_schema.py and roadmap_schema.py

Remove `get_tdd_schema()`, `get_tdd_schema_loader()`, `reset_global_schema_loader()`, and `_global_loader` from `tdd_schema.py`. Apply same fix to `roadmap_schema.py` which has the identical pattern.

All callers updated to receive `TDDSchema` via constructor injection:
- `validator.py`: accept `TDDSchema` in `__init__`
- `validation_error_detector.py`: accept `TDDSchema` in `__init__`
- CLI entry points (`log_phase.py`, `verify_deliver_integrity.py`): create `TDDSchemaLoader` at entry, pass schema to consumers
- Hook adapter factory (`create_subagent_stop_service`): create loader, pass schema to service

### Fix 3: Introduce PhaseStatus and PhaseName enums

Add `PhaseStatus` and `PhaseName` enums to `src/des/domain/value_objects.py`. Both extend `StrEnum` for backward-compatible string comparison during migration. `PhaseEventParser` validates and converts at the parsing boundary.

## Alternatives Considered

### For DIP violation

**Alternative A (chosen)**: Move to `application/` layer. The validator performs application-level orchestration (reading files, validating configuration). It belongs in `application/`.

**Alternative B**: Make `filesystem` required, keep in `domain/`. Rejected because the validator's responsibility (reading JSON files, checking configuration structure) is application-level work, not domain logic. Even without the import violation, it does not belong in `domain/`.

### For global singleton

**Alternative A (chosen)**: Constructor injection. Each consumer declares its `TDDSchema` dependency explicitly. Composition roots (factory functions, CLI entry points) create and inject instances.

**Alternative B**: Module-level constant instead of mutable singleton. Load schema once at import time, store as `TDD_SCHEMA = TDDSchemaLoader().load()`. Rejected because import-time I/O causes test failures when schema file is not at expected path, and prevents overriding schema path for testing.

**Alternative C**: Keep global with `@functools.cache` instead of manual `_global_loader`. Rejected because it still hides the dependency. The problem is not the caching mechanism but the global access pattern.

### For primitive obsession

**Alternative A (chosen)**: `StrEnum` for backward compatibility. Existing code comparing `event.status == "EXECUTED"` continues to work because `PhaseStatus.EXECUTED == "EXECUTED"` is `True` with `StrEnum`.

**Alternative B**: Plain `Enum` with immediate migration of all comparison sites. Rejected because of high blast radius -- many files compare against string literals. The `StrEnum` approach allows incremental migration.

**Alternative C**: NewType (type alias). `PhaseStatusStr = NewType("PhaseStatusStr", str)`. Rejected because NewType provides only static type checking with no runtime validation. Enums catch invalid values at construction time.

## Consequences

### Positive

- Domain layer has zero adapter imports (verifiable by import-linter)
- No global mutable state in domain layer
- Dependencies are explicit and visible in constructors
- Invalid phase statuses and names are caught at parse time, not at use time
- Test isolation is reliable without requiring `reset_global_schema_loader()` calls

### Negative

- 6 call sites must be updated for singleton removal (mechanical but widespread)
- `StrEnum` requires Python 3.11+ or a compatibility shim for Python 3.10 support
- Constructor signatures grow by one parameter in affected classes
- Migration period where both enum and string comparisons coexist

### Quality Attribute Impact

| Attribute | Impact | Direction |
|-----------|--------|-----------|
| Testability | Explicit dependencies, no hidden global state | Positive |
| Maintainability | Domain layer is pure, boundaries enforceable | Positive |
| Reliability | Invalid values caught at construction, not propagated | Positive |
| Modifiability | Adding new phase statuses requires updating the enum (explicit) | Positive |
| Compatibility | StrEnum maintains backward compatibility | Neutral |
