---
phase: 01-foundation
plan: 02
type: tdd
wave: 1
depends_on: []
files_modified:
  - src/corpus_analyzer/config/__init__.py
  - src/corpus_analyzer/config/schema.py
  - src/corpus_analyzer/config/io.py
  - tests/config/__init__.py
  - tests/config/test_schema.py
  - tests/config/test_io.py
autonomous: true
requirements:
  - CONF-01
  - CONF-02
  - CONF-03
  - CONF-04

must_haves:
  truths:
    - "CorpusConfig, SourceConfig, and EmbeddingConfig Pydantic models parse a valid corpus.toml dict"
    - "Missing required fields raise ValidationError with a clear message"
    - "load_config() reads a corpus.toml file and returns a validated CorpusConfig"
    - "load_config() returns a default config (no sources, default embedding) when the file does not exist"
    - "save_config() writes a valid TOML file that round-trips back to the same CorpusConfig"
    - "Per-source include/exclude glob lists are preserved correctly through save/load"
  artifacts:
    - path: "src/corpus_analyzer/config/schema.py"
      provides: "CorpusConfig, SourceConfig, EmbeddingConfig Pydantic models"
      exports: ["CorpusConfig", "SourceConfig", "EmbeddingConfig"]
    - path: "src/corpus_analyzer/config/io.py"
      provides: "load_config(), save_config()"
      exports: ["load_config", "save_config"]
    - path: "tests/config/test_schema.py"
      provides: "Config model validation tests"
    - path: "tests/config/test_io.py"
      provides: "TOML read/write round-trip tests"
  key_links:
    - from: "src/corpus_analyzer/config/io.py"
      to: "tomllib"
      via: "tomllib.load()"
      pattern: "tomllib\\.load"
    - from: "src/corpus_analyzer/config/io.py"
      to: "tomli_w"
      via: "tomli_w.dump()"
      pattern: "tomli_w\\.dump"
    - from: "src/corpus_analyzer/config/io.py"
      to: "src/corpus_analyzer/config/schema.py"
      via: "CorpusConfig.model_validate()"
      pattern: "CorpusConfig\\.model_validate"
---

<objective>
Define the CorpusConfig Pydantic models and the TOML load/save I/O functions using TDD.

Purpose: Config is the entry point for every corpus command. The schema defines what a valid corpus.toml looks like; the I/O layer is tested independently of CLI logic. These are pure functions — perfect TDD candidates.

Output: src/corpus_analyzer/config/schema.py and config/io.py, verified by passing tests.
</objective>

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

<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
</context>

<feature>
  <name>Config models and TOML I/O</name>
  <files>
    src/corpus_analyzer/config/__init__.py
    src/corpus_analyzer/config/schema.py
    src/corpus_analyzer/config/io.py
    tests/config/__init__.py
    tests/config/test_schema.py
    tests/config/test_io.py
  </files>
  <behavior>
    EmbeddingConfig fields:
      model: str = "nomic-embed-text"   — embedding model name
      provider: str = "ollama"          — Phase 1: always "ollama"
      host: str = "http://localhost:11434"

    SourceConfig fields:
      name: str                         — required; unique source identifier
      path: str                         — required; directory path (may use ~)
      include: list[str] = ["**/*"]     — glob patterns to include
      exclude: list[str] = []           — glob patterns to exclude

    CorpusConfig fields:
      embedding: EmbeddingConfig = EmbeddingConfig()   — default embedding config
      sources: list[SourceConfig] = []                 — list of configured sources

    load_config(path: Path) -> CorpusConfig:
      If path does not exist: return CorpusConfig() with defaults (no sources)
      If path exists: read with tomllib, validate with CorpusConfig.model_validate(), return

    save_config(path: Path, config: CorpusConfig) -> None:
      Create parent directories if needed
      Convert to dict via model.model_dump()
      Write with tomli_w.dump() to path (binary mode)

    Cases (schema):
      CorpusConfig() instantiates with empty sources and default embedding
      SourceConfig(name="x", path="/foo") instantiates with defaults for include/exclude
      SourceConfig(name="x") raises ValidationError (path is required)
      CorpusConfig with two sources both appear in .sources list

    Cases (io):
      load_config(non_existent_path) -> CorpusConfig with 0 sources
      load_config(valid_toml_path) -> CorpusConfig matching file content
      save_config + load_config round-trip preserves source name, path, include, exclude
      save_config creates parent dirs when they don't exist (use tmp_path fixture)
  </behavior>
  <implementation>
    1. Create src/corpus_analyzer/config/__init__.py — re-export CorpusConfig, SourceConfig, EmbeddingConfig, load_config, save_config
    2. Create src/corpus_analyzer/config/schema.py:
       - Use pydantic BaseModel (NOT pydantic-settings; this is a TOML file model, not env-var model)
       - EmbeddingConfig, SourceConfig, CorpusConfig as described above
       - Full type annotations and PEP 257 docstrings
    3. Create src/corpus_analyzer/config/io.py:
       - load_config(path: Path) -> CorpusConfig
       - save_config(path: Path, config: CorpusConfig) -> None
       - Import tomllib (stdlib), tomli_w, platformdirs is NOT used here (that goes in CLI)
       - Use tomllib.load() with rb mode; tomli_w.dump() with wb mode
       - Handle non-existent file gracefully in load_config
    4. Create tests/config/__init__.py and tests/config/test_schema.py + tests/config/test_io.py
       - test_schema.py: instantiation, validation, defaults
       - test_io.py: uses pytest tmp_path fixture for all file operations

    Follow RED-GREEN-REFACTOR cycle.
    Note: Do NOT use pydantic-settings or CORPUS_ env-var prefixes — this config comes from a TOML file, not environment variables.
    Note: `tomli-w` and `platformdirs` should already be added from Plan 01's `uv add` step. If not, run `uv add tomli-w platformdirs`.
  </implementation>
</feature>

<verification>
uv run pytest tests/config/ -v
uv run mypy src/corpus_analyzer/config/
uv run ruff check src/corpus_analyzer/config/
</verification>

<success_criteria>
All tests in tests/config/ pass. mypy reports no errors. CorpusConfig, load_config, save_config exported from config package.
</success_criteria>

<output>
After completion, create .planning/phases/01-foundation/01-02-SUMMARY.md
</output>
