---
phase: 01-core-hardening
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/ztlctl/domain/registry.py
  - tests/domain/test_registry.py
autonomous: true
requirements: [HARD-02, HARD-09]

must_haves:
  truths:
    - "All 4 content types (note, reference, task, log) have registered NoteTypeDefinitions"
    - "All subtypes (decision, knowledge, article, tool, spec) have registered NoteTypeDefinitions with is_subtype=True"
    - "Subtypes inherit parent transitions when no custom transitions provided"
    - "DecisionModel has its own custom DECISION_TRANSITIONS in its NoteTypeDefinition"
    - "NoteTypeRegistry validates transition map integrity on registration"
    - "Plugin-contributed NoteTypeDefinitions can be registered via NoteTypeRegistry.register()"
  artifacts:
    - path: "src/ztlctl/domain/registry.py"
      provides: "NoteTypeDefinition frozen dataclass + NoteTypeRegistry + built-in registrations"
      contains: "class NoteTypeDefinition"
    - path: "tests/domain/test_registry.py"
      provides: "Unit tests for NoteTypeDefinition, NoteTypeRegistry, built-in registrations"
      contains: "def test_"
  key_links:
    - from: "src/ztlctl/domain/registry.py"
      to: "src/ztlctl/domain/content.py"
      via: "imports ContentModel, NoteModel, KnowledgeModel, DecisionModel, ReferenceModel, TaskModel"
      pattern: "from ztlctl\\.domain\\.content import"
    - from: "src/ztlctl/domain/registry.py"
      to: "src/ztlctl/domain/lifecycle.py"
      via: "imports NOTE_TRANSITIONS, REFERENCE_TRANSITIONS, TASK_TRANSITIONS, LOG_TRANSITIONS, DECISION_TRANSITIONS"
      pattern: "from ztlctl\\.domain\\.lifecycle import"
---

<objective>
Create NoteTypeDefinition as a frozen dataclass and NoteTypeRegistry as the canonical registry for all note types (built-in + plugin-contributed). Register all 4 content types and 5 subtypes as NoteTypeDefinitions with embedded transition maps.

Purpose: HARD-02 (data model consistency) and HARD-09 (NoteTypeDefinition as extensible primitive) — this is the foundation that Phase 2 Action Registry builds on.
Output: `src/ztlctl/domain/registry.py` with NoteTypeDefinition, NoteTypeRegistry, built-in registrations, and `get_note_type_registry()` accessor. Full test coverage in `tests/domain/test_registry.py`.
</objective>

<execution_context>
@/Users/shparki/.claude/get-shit-done/workflows/execute-plan.md
@/Users/shparki/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-core-hardening/01-CONTEXT.md
@.planning/phases/01-core-hardening/01-RESEARCH.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From src/ztlctl/domain/content.py:
```python
CONTENT_REGISTRY: dict[str, type[ContentModel]] = {}

class ContentModel(BaseModel):
    template_name: str = ""
    content_type: str = ""
    # ... fields ...
    @classmethod
    def status_transitions(cls) -> dict[str, list[str]]: ...
    @classmethod
    def required_sections(cls) -> list[str]: ...
    @classmethod
    def validate_create(cls, data: dict[str, Any]) -> ValidationResult: ...

class NoteModel(ContentModel): ...         # template_name = "note.md.j2"
class KnowledgeModel(NoteModel): ...       # template_name = "knowledge.md.j2"
class DecisionModel(NoteModel): ...        # template_name = "decision.md.j2", required_sections = ["Context","Choice","Rationale","Alternatives","Consequences"]
class ReferenceModel(ContentModel): ...    # template_name = "reference.md.j2"
class TaskModel(ContentModel): ...         # template_name = "task.md.j2"

def _builtin_model_map() -> dict[str, type[ContentModel]]:
    return {"note": NoteModel, "knowledge": KnowledgeModel, "decision": DecisionModel,
            "reference": ReferenceModel, "task": TaskModel}
```

From src/ztlctl/domain/lifecycle.py:
```python
NOTE_TRANSITIONS: dict[str, list[str]] = {
    "draft": ["active", "archived"], "active": ["archived"], "archived": ["active"],
}
REFERENCE_TRANSITIONS: dict[str, list[str]] = {
    "draft": ["active", "archived"], "active": ["archived"], "archived": ["active"],
}
LOG_TRANSITIONS: dict[str, list[str]] = {
    "open": ["closed"], "closed": ["open"],
}
TASK_TRANSITIONS: dict[str, list[str]] = {
    "open": ["in_progress", "done", "blocked", "archived"],
    "in_progress": ["done", "blocked", "open", "archived"],
    "blocked": ["open", "in_progress", "archived"],
    "done": ["archived", "open"], "archived": ["open"],
}
DECISION_TRANSITIONS: dict[str, list[str]] = {
    "proposed": ["accepted", "rejected", "superseded"],
    "accepted": ["superseded", "deprecated"], "rejected": [],
    "superseded": [], "deprecated": [],
}
```

From src/ztlctl/domain/types.py:
```python
class ContentType(StrEnum):
    NOTE = "note"; REFERENCE = "reference"; LOG = "log"; TASK = "task"
class NoteSubtype(StrEnum):
    DECISION = "decision"; KNOWLEDGE = "knowledge"
class RefSubtype(StrEnum):
    ARTICLE = "article"; TOOL = "tool"; SPEC = "spec"
```

Templates on disk: note.md.j2, knowledge.md.j2, decision.md.j2, reference.md.j2, task.md.j2
No template for log (sessions are DB-only), article, tool, spec (these use parent template).
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create NoteTypeDefinition and NoteTypeRegistry with tests</name>
  <files>src/ztlctl/domain/registry.py, tests/domain/test_registry.py</files>
  <read_first>
    - src/ztlctl/domain/content.py (ContentModel hierarchy, CONTENT_REGISTRY, _builtin_model_map, register_content_model)
    - src/ztlctl/domain/lifecycle.py (all transition maps, status enums)
    - src/ztlctl/domain/types.py (ContentType, NoteSubtype, RefSubtype enums)
    - tests/domain/test_content.py (existing test patterns for domain layer)
  </read_first>
  <behavior>
    - Test: NoteTypeDefinition is a frozen dataclass with fields: name, content_type, model_cls, transitions, template_name, required_sections, initial_status, is_subtype, parent_type
    - Test: NoteTypeRegistry.register() stores a NoteTypeDefinition and NoteTypeRegistry.get() retrieves it by name
    - Test: NoteTypeRegistry.register() raises ValueError if transition map has unreachable states (e.g., a target state not present as a key)
    - Test: NoteTypeRegistry.list_types() returns all registered types; list_types(content_type="note") filters to note types only
    - Test: get_note_type_registry() returns the module-level singleton with all 9 built-in types registered
    - Test: Built-in "note" type has content_type="note", model_cls=NoteModel, transitions=NOTE_TRANSITIONS, template_name="note.md.j2", is_subtype=False
    - Test: Built-in "decision" type has content_type="note", model_cls=DecisionModel, transitions=DECISION_TRANSITIONS, template_name="decision.md.j2", is_subtype=True, parent_type="note", required_sections=["Context","Choice","Rationale","Alternatives","Consequences"], initial_status="proposed"
    - Test: Built-in "knowledge" type has is_subtype=True, parent_type="note", transitions=NOTE_TRANSITIONS (inherits parent)
    - Test: Built-in "article" type has content_type="reference", is_subtype=True, parent_type="reference", transitions=REFERENCE_TRANSITIONS (inherits parent), template_name="reference.md.j2"
    - Test: Built-in "log" type has content_type="log", transitions=LOG_TRANSITIONS, template_name="" (no template, DB-only)
    - Test: Registering a custom NoteTypeDefinition with name="custom_note", content_type="note", custom transitions succeeds
    - Test: Registering a duplicate name raises ValueError
    - Test: NoteTypeDefinition with is_subtype=True and parent_type=None raises ValueError on register
  </behavior>
  <action>
    Create `src/ztlctl/domain/registry.py` with:

    1. `from __future__ import annotations` at top (mandatory convention).

    2. `NoteTypeDefinition` as a `@dataclass(frozen=True)` with fields:
       - `name: str` (e.g., "note", "decision", "knowledge")
       - `content_type: str` (parent type: "note", "reference", "task", "log")
       - `model_cls: type[ContentModel]` (the Pydantic model class)
       - `transitions: dict[str, list[str]]` (status transition map, embedded inline)
       - `template_name: str` (Jinja2 template filename, e.g., "note.md.j2"; empty string for DB-only types like log)
       - `required_sections: list[str] = field(default_factory=list)` (body sections required for validation)
       - `initial_status: str = ""` (initial status on creation; empty means use first key of transitions)
       - `is_subtype: bool = False` (True for decision, knowledge, article, tool, spec)
       - `parent_type: str | None = None` (for subtypes: which content_type is the parent, e.g., "note" for decision)

    3. `NoteTypeRegistry` class with:
       - `__init__()`: initializes `self._types: dict[str, NoteTypeDefinition] = {}`
       - `register(note_type: NoteTypeDefinition) -> None`: validates, stores. Raises `ValueError` if name already registered. Raises `ValueError` if `is_subtype=True` and `parent_type is None`. Calls `_validate_transitions()`.
       - `get(name: str) -> NoteTypeDefinition`: returns by name, raises `KeyError` if not found.
       - `list_types(content_type: str | None = None) -> list[NoteTypeDefinition]`: filters by content_type if provided, otherwise returns all.
       - `_validate_transitions(note_type: NoteTypeDefinition) -> None`: checks that all target states in transition values also exist as keys in the transition map (no orphaned targets). Raises `ValueError` with descriptive message if invalid.

    4. Module-level registration function `_register_builtins()` that registers all 9 types:
       - "note": content_type="note", model_cls=NoteModel, transitions=NOTE_TRANSITIONS, template_name="note.md.j2"
       - "knowledge": content_type="note", model_cls=KnowledgeModel, transitions=NOTE_TRANSITIONS, template_name="knowledge.md.j2", is_subtype=True, parent_type="note"
       - "decision": content_type="note", model_cls=DecisionModel, transitions=DECISION_TRANSITIONS, template_name="decision.md.j2", required_sections=["Context","Choice","Rationale","Alternatives","Consequences"], initial_status="proposed", is_subtype=True, parent_type="note"
       - "reference": content_type="reference", model_cls=ReferenceModel, transitions=REFERENCE_TRANSITIONS, template_name="reference.md.j2"
       - "article": content_type="reference", model_cls=ReferenceModel, transitions=REFERENCE_TRANSITIONS, template_name="reference.md.j2", is_subtype=True, parent_type="reference"
       - "tool": content_type="reference", model_cls=ReferenceModel, transitions=REFERENCE_TRANSITIONS, template_name="reference.md.j2", is_subtype=True, parent_type="reference"
       - "spec": content_type="reference", model_cls=ReferenceModel, transitions=REFERENCE_TRANSITIONS, template_name="reference.md.j2", is_subtype=True, parent_type="reference"
       - "task": content_type="task", model_cls=TaskModel, transitions=TASK_TRANSITIONS, template_name="task.md.j2"
       - "log": content_type="log", model_cls=ContentModel, transitions=LOG_TRANSITIONS, template_name=""

       NOTE: "log" uses base ContentModel since no LogModel class exists (sessions are DB-only). template_name="" because there is no log template file.

    5. Module-level singleton: `_REGISTRY = NoteTypeRegistry()` then `_register_builtins()` called at import time.

    6. Public accessor: `def get_note_type_registry() -> NoteTypeRegistry: return _REGISTRY`

    Decision: NoteTypeDefinition lives in domain/ (frozen dataclass, no infrastructure imports). Template paths are strings resolved by services at runtime. This follows the 6-layer architecture dependency rules per CONTEXT.md discretion item.

    Then create `tests/domain/test_registry.py` with tests per the behavior block above. Follow existing test patterns from `tests/domain/test_content.py`.
  </action>
  <verify>
    <automated>uv run pytest tests/domain/test_registry.py -x -v</automated>
  </verify>
  <acceptance_criteria>
    - src/ztlctl/domain/registry.py exists and contains `class NoteTypeDefinition` and `class NoteTypeRegistry`
    - src/ztlctl/domain/registry.py contains `from __future__ import annotations` as first non-comment line
    - src/ztlctl/domain/registry.py contains `@dataclass(frozen=True)` before NoteTypeDefinition
    - src/ztlctl/domain/registry.py contains `def get_note_type_registry() -> NoteTypeRegistry`
    - src/ztlctl/domain/registry.py contains `_register_builtins()` call at module level
    - tests/domain/test_registry.py contains at least 10 test functions starting with `def test_`
    - `uv run pytest tests/domain/test_registry.py -x -v` exits 0 with all tests passing
    - `uv run mypy src/ztlctl/domain/registry.py --strict` exits 0
    - `uv run ruff check src/ztlctl/domain/registry.py` exits 0
  </acceptance_criteria>
  <done>NoteTypeDefinition frozen dataclass exists in domain/registry.py. NoteTypeRegistry holds all 9 built-in types (note, knowledge, decision, reference, article, tool, spec, task, log). All tests pass. mypy strict and ruff clean.</done>
</task>

</tasks>

<verification>
- `uv run pytest tests/domain/test_registry.py -x -v` — all NoteTypeDefinition/NoteTypeRegistry tests pass
- `uv run mypy src/ztlctl/domain/registry.py --strict` — no type errors
- `uv run ruff check src/ztlctl/domain/registry.py` — no lint issues
- `uv run pytest tests/ -x -q` — full suite still passes (no regressions)
</verification>

<success_criteria>
- NoteTypeDefinition frozen dataclass in domain/registry.py with all 9 fields
- NoteTypeRegistry with register/get/list_types/validate methods
- 9 built-in types registered: note, knowledge, decision, reference, article, tool, spec, task, log
- Subtypes have is_subtype=True and parent_type set
- DecisionModel has custom DECISION_TRANSITIONS; knowledge/article/tool/spec inherit parent transitions
- Plugin registration path works (custom NoteTypeDefinition can be registered)
- Full test coverage in tests/domain/test_registry.py
</success_criteria>

<output>
After completion, create `.planning/phases/01-core-hardening/01-01-SUMMARY.md`
</output>
