# ADR-0012: Project SSOT lives in the `Project` class

**Status:** Accepted
**Date:** 2026-05-07
**Deciders:** JT, Claude (in dialogue, dual-consult round Console v2 + Engine Overhaul)
**Supersedes:** none directly; locks rejection of `project_policy.yaml` and any nested `policy` sub-object inside `project_config.json`.
**Superseded by:** none

## Decision

The `Project` class at `recoil/core/project.py` is the **runtime SSOT** for project policy: `aspect_ratio`, `project_type`, `default_models`, `aspect_synthesized`. All readers go through `Project(slug).aspect_ratio`, `Project(slug).project_type`, etc.

On-disk persistence is `projects/{slug}/project_config.json` with **flat top-level fields**. Specifically:

- NO `projects/{slug}/project_policy.yaml` (rejected — see `recoil/.out-of-scope/project-policy-yaml.md`).
- NO nested `policy: {...}` sub-object inside `project_config.json`.
- The on-disk schema model is the Pydantic `ProjectConfig` defined alongside `Project` in `recoil/core/project.py`, with field set: `schema_version`, `project_type`, `aspect_ratio`, `default_models`, `mode` (legacy alias, deprecated for one cycle).

## Context

The dual-consult round on Console v2 + Engine Overhaul surfaced four parallel sources for project policy: a `project_config.json` file, an `aspect` key inside `global_bible.json`, hard-coded defaults inside `_coerce_aspect`, and a referenced-but-never-created `project_policy.yaml` from a prior Console v2 SYNTHESIS. The result was that a Console v2 caller asking "what aspect ratio is this project?" could get four different answers depending on which adapter it routed through. SYNTHESIS.md §1 lock U-5 collapses these into one runtime SSOT (`Project`) and one on-disk SSOT (`project_config.json` with flat fields).

`global_bible.json` is a separate concern — it's the narrative bible, not project policy. The `_coerce_aspect` helper that read from `bible.get("aspect")` becomes `_resolve_aspect`, and during CP-B the bible-aspect read site is folded into the `Project` migration ladder, then deleted.

## Consequences

- All Console v2 reads of project policy go through `Project(slug)`. No Console v2 surface reaches into `project_config.json` directly with `json.load`.
- `_coerce_aspect` is renamed to `_resolve_aspect` in CP-B Phase 10 and gains the `AspectUnresolvable` typed exception (Law 4).
- `aspect_synthesized` boolean flag rides on `ProjectResponse` to mark configs that were resolved via the legacy migration path; consumers get a UI hint that a write-back to `project_config.json` is needed (CP-B Phase 13).
- The `project_policy.yaml` proposal is durably rejected — see `recoil/.out-of-scope/project-policy-yaml.md`.
- Cited Laws: Law 1 (SSOT), Law 2 (typed contracts — `ProjectConfig` is Pydantic), Law 4 (errors visible — `AspectUnresolvable`), Law 7 (versioned — `schema_version` field), Law 14 (anti-pattern memory).
- Related ADRs: ADR-0011 (Hybrid A canonical — establishes filesystem-as-DB premise), ADR-0013 (import idiom — `from recoil.core.project import Project`).
