Goal
Adopt Constructive Domain Modeling incrementally in workflows where the current representation admits unsupported states or uses exceptions for expected domain outcomes.
The style means:
- define the domain's positive space with small required-field values;
- use frozen dataclasses for internal products and closed
type unions for meaningful alternatives;
- use Pydantic at API, MCP, CLI, configuration, and persistence boundaries;
- parse/classify once, then handle variants exhaustively;
- keep cancellation and unpredictable filesystem, network, queue, and database failures exceptional.
This is a tracker for focused, behavior-preserving slices—not one sweeping refactor.
Evidence and first candidates
1. Note-content reconciliation is close, but its contract is still broader than its callers
The planner already returns a closed union of frozen plan values, and the apply path already uses exhaustive matching with assert_never.
However, plan_note_content_reconciliation(current: NoteContentState | None, ...) returns the full union for both input cases. Callers therefore need runtime guards for two combinations the implementation cannot produce:
This is the best first pilot because the constructive model already exists and the remaining change is small and type-checkable.
2. Persisted note-content state is broader than the domain transitions
NoteContentState combines a write status with optional file version/checksum fields. That may admit combinations no workflow should construct.
This must begin with a writer and compatibility audit. The ORM/storage shape may need to remain broad while a parser constructs narrower internal variants.
3. Project lifecycle responses combine unrelated operations
ProjectStatusResponse represents create, update, default-switch, and delete responses through one model with several operation-dependent optional fields.
Project deletion already has a stronger internal accepted result, but expected rejections are carried by an exception with HTTP status and detail. That mixes a normal domain decision with its HTTP presentation.
Proposed sequence
Phase 0 — land the vocabulary
Phase 1 — make note-content planner obligations visible to the type checker
Suggested verification:
uv run pytest -q \
tests/indexing/test_note_content_reconciliation.py \
tests/indexing/test_note_content_reconciler.py \
tests/indexing/test_note_content_batch_reconciliation.py
uv run ty check src tests test-int
Phase 2 — define valid note-content lifecycle states
Phase 3 — separate project operation outcomes
Phase 4 — find the next high-payoff slices
Non-goals
- Removing Pydantic; it remains the runtime boundary model.
- Introducing a universal
Result[T, E] abstraction.
- Converting filesystem, network, queue, database, or cancellation failures into domain variants.
- Adding wrapper-only ID types that do not remove a real invalid operation.
- Reshaping ORM tables before tracing every writer and compatibility constraint.
- Breaking public API/MCP/CLI payloads merely to make internal types look cleaner.
- Mechanically rewriting clear procedural orchestration that does not admit invalid state.
Tracker acceptance criteria
Goal
Adopt Constructive Domain Modeling incrementally in workflows where the current representation admits unsupported states or uses exceptions for expected domain outcomes.
The style means:
typeunions for meaningful alternatives;This is a tracker for focused, behavior-preserving slices—not one sweeping refactor.
Evidence and first candidates
1. Note-content reconciliation is close, but its contract is still broader than its callers
The planner already returns a closed union of frozen plan values, and the apply path already uses exhaustive matching with
assert_never.However,
plan_note_content_reconciliation(current: NoteContentState | None, ...)returns the full union for both input cases. Callers therefore need runtime guards for two combinations the implementation cannot produce:This is the best first pilot because the constructive model already exists and the remaining change is small and type-checkable.
2. Persisted note-content state is broader than the domain transitions
NoteContentStatecombines a write status with optional file version/checksum fields. That may admit combinations no workflow should construct.This must begin with a writer and compatibility audit. The ORM/storage shape may need to remain broad while a parser constructs narrower internal variants.
3. Project lifecycle responses combine unrelated operations
ProjectStatusResponserepresents create, update, default-switch, and delete responses through one model with several operation-dependent optional fields.Project deletion already has a stronger internal accepted result, but expected rejections are carried by an exception with HTTP status and detail. That mixes a normal domain decision with its HTTP presentation.
Proposed sequence
Phase 0 — land the vocabulary
docs/ENGINEERING_STYLE.md..agents/skills/pythonic-code/SKILL.md.Phase 1 — make note-content planner obligations visible to the type checker
NoteContentBootstrap.NonereturnsNoteContentBootstrapandNoteContentStatereturns only existing-state plans.IntegrityErrorremains an external/concurrency failure.Suggested verification:
Phase 2 — define valid note-content lifecycle states
db_version,file_version, checksums, andfile_write_status.Phase 3 — separate project operation outcomes
ProjectStatusResponse.not found,only remaining project) as explicit variants and translate them to HTTP errors in the API adapter.Phase 4 — find the next high-payoff slices
Non-goals
Result[T, E]abstraction.Tracker acceptance criteria