Deferred refactor from WS2 Phase 2. Not user-visible — internal type hygiene.
Today, `FetchResult` and `ContentSource` are dataclasses where success-only fields (`text`, `extracted_at`, …) and failure-only fields (`http_status`, `failure_reason`, `attempts`, …) coexist as Optionals. Nothing in the type system prevents a malformed state where both are populated or both are absent. Callers must check sentinel fields to know which case they have.
Goal
Make success and failure mutually exclusive variants by construction: tagged unions / discriminated types (e.g. `FetchSuccess` + `FetchFailure` under a `FetchResult = FetchSuccess | FetchFailure` alias), with pydantic / pattern matching enforcement.
Acceptance
- `mypy --strict` rules out illegal states.
- Callers switch on the variant (pattern match or `isinstance`); no more `if result.text is not None` smell.
- JSON serialization / deserialization preserves the variant (`type: "success" | "failure"` discriminator on the wire).
- All existing tests pass without behavior change.
Risk
Touches the persistence layer (`items.json` schema). Needs a one-shot migration of existing `ContentSource` payloads — handled by reading the legacy shape and writing the new shape on next save. Snapshot (#C) becomes very useful here.
Deferred refactor from WS2 Phase 2. Not user-visible — internal type hygiene.
Today, `FetchResult` and `ContentSource` are dataclasses where success-only fields (`text`, `extracted_at`, …) and failure-only fields (`http_status`, `failure_reason`, `attempts`, …) coexist as Optionals. Nothing in the type system prevents a malformed state where both are populated or both are absent. Callers must check sentinel fields to know which case they have.
Goal
Make success and failure mutually exclusive variants by construction: tagged unions / discriminated types (e.g. `FetchSuccess` + `FetchFailure` under a `FetchResult = FetchSuccess | FetchFailure` alias), with pydantic / pattern matching enforcement.
Acceptance
Risk
Touches the persistence layer (`items.json` schema). Needs a one-shot migration of existing `ContentSource` payloads — handled by reading the legacy shape and writing the new shape on next save. Snapshot (#C) becomes very useful here.