# ADR-0006: Two-View Repository Data Model via Generics and Composition

## Status

✅ Accepted

## Context

omni-dev builds a structured representation of repository state (commits, metadata,
remotes, branch info) and presents it to two consumers with different needs:

- **Human output** — the CLI user sees commit metadata, file change summaries, and a
  reference path to the diff file. Loading full diff content into this view would bloat
  output and waste memory.

- **AI analysis** — the Claude API receives the same metadata plus the full diff content
  inlined into the YAML payload, and a `pre_validated_checks` list recording deterministic
  checks that the AI should treat as authoritative and skip re-verifying.

This creates a three-level type hierarchy where each level has a human variant and an AI
variant:

| Level      | Human variant    | AI variant             | AI additions                |
|------------|------------------|------------------------|-----------------------------|
| Repository | `RepositoryView` | `RepositoryViewForAI`  | Commits carry diff content  |
| Commit     | `CommitInfo`     | `CommitInfoForAI`      | `pre_validated_checks` list |
| Analysis   | `CommitAnalysis` | `CommitAnalysisForAI`  | `diff_content` field        |

The original implementation used fully duplicated structs at each level: `RepositoryView`
and `RepositoryViewForAI` shared 9 identical fields and differed only in the `commits`
vector type; `CommitInfo` and `CommitInfoForAI` shared 5 fields; `CommitAnalysis` and
`CommitAnalysisForAI` shared 6 fields. Conversion between views was done by manual
field-by-field copying in `from_repository_view_with_options()`.

This duplication was fragile. Adding a field to one struct and forgetting the other caused
silent data loss with no compiler error. The 9-field copy in
`from_repository_view_with_options()` was the worst offender — every new field on
`RepositoryView` required a corresponding addition in both the duplicate struct definition
and the conversion function.

Three alternative approaches were considered:

- **Single view for both consumers** — forces compromise between human output size and AI
  context needs. Either the human view carries unnecessary diff content or the AI view
  lacks it.

- **Adapter pattern** — wrapping `RepositoryView` in an adapter that lazily loads diffs
  introduces unnecessary indirection for what is fundamentally a type difference in one
  field.

- **View decorators** — runtime trait objects with optional fields add dynamic dispatch
  overhead and replace compile-time type guarantees with runtime optionality.

## Decision

We will eliminate struct duplication using Rust generics at the repository and commit levels,
and `#[serde(flatten)]` composition at the commit and analysis levels.

### Analysis level — composition

`CommitAnalysisForAI` composes `CommitAnalysis` via `#[serde(flatten)]` and adds a single
field:

```rust
pub struct CommitAnalysisForAI {
    #[serde(flatten)]
    pub base: CommitAnalysis,
    pub diff_content: String,
}
```

The `from_commit_analysis()` conversion reads the diff file referenced by
`base.diff_file` and stores the content in `diff_content`. Shared fields are accessed
via `base.field_name`.

### Commit level — generic type parameter plus composition

`CommitInfo` takes a type parameter for its analysis type, defaulting to `CommitAnalysis`:

```rust
pub struct CommitInfo<A = CommitAnalysis> {
    pub hash: String,
    pub author: String,
    pub date: DateTime<FixedOffset>,
    pub original_message: String,
    pub in_main_branches: Vec<String>,
    pub analysis: A,
}
```

`CommitInfoForAI` composes `CommitInfo<CommitAnalysisForAI>` via `#[serde(flatten)]` and
adds the `pre_validated_checks` field:

```rust
pub struct CommitInfoForAI {
    #[serde(flatten)]
    pub base: CommitInfo<CommitAnalysisForAI>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub pre_validated_checks: Vec<String>,
}
```

The default type parameter means bare `CommitInfo` (without angle brackets) refers to
`CommitInfo<CommitAnalysis>`, so all existing human-view code compiles unchanged.

### Repository level — generic type parameter

`RepositoryView` takes a type parameter for its commit type, defaulting to `CommitInfo`:

```rust
pub struct RepositoryView<C = CommitInfo> {
    pub versions: Option<VersionInfo>,
    pub explanation: FieldExplanation,
    // ... 7 more shared fields ...
    pub commits: Vec<C>,
}

pub type RepositoryViewForAI = RepositoryView<CommitInfoForAI>;
```

A `map_commits` method on `impl<C> RepositoryView<C>` transforms commits while
transferring all shared fields in a single place, replacing the manual 9-field copy:

```rust
pub fn map_commits<D>(
    self,
    f: impl FnMut(C) -> anyhow::Result<D>,
) -> anyhow::Result<RepositoryView<D>>
```

The `from_repository_view_with_options()` conversion uses `map_commits`, so new fields
added to `RepositoryView` automatically flow through to the AI view with no additional
code.

### Method partitioning

Methods specific to the human view (`update_field_presence`, `single_commit_view`,
`multi_commit_view`) remain on `impl RepositoryView` (which resolves to
`impl RepositoryView<CommitInfo>` via the default). Methods specific to the AI view
(`truncate_diffs`, `replace_diffs_with_stat`, `remove_diffs`, `from_repository_view`)
are on `impl RepositoryViewForAI`.

### `serde(flatten)` applicability

`#[serde(flatten)]` is used at the analysis and commit levels. It produces identical YAML
field layout to the previous explicit structs. The AI view types are serialised to YAML for
prompt construction and are never deserialised from external input, so the known
`serde(flatten)` deserialization quirks (incompatibility with `deny_unknown_fields`, routing
through an intermediate `Value` representation, unexpected behaviour with `Option` fields)
do not apply. If future code deserialises these types from external YAML, this trade-off
should be revisited.

## Consequences

**Positive:**

- **Compiler-enforced field consistency.** Adding a field to `RepositoryView` requires no
  change to `RepositoryViewForAI` — it is a type alias, not a separate struct. Adding a
  field to `CommitAnalysis` automatically includes it in `CommitAnalysisForAI` via
  `#[serde(flatten)]`. The class of silent-data-loss bugs from forgotten field copies is
  eliminated.

- **Single-point conversion.** The `map_commits` method is the sole place where shared
  fields are transferred between view types. Previously the 9-field copy was a maintenance
  hazard; now it exists once and handles any commit type transformation.

- **Zero churn for human-view code.** Default type parameters mean all code that uses
  `RepositoryView`, `CommitInfo`, or `CommitAnalysis` without angle brackets compiles
  without modification. The refactor's diff is confined to AI-view construction sites and
  the type definitions themselves.

**Negative:**

- **Extra `.base.` indirection.** Code that accesses shared fields on AI types must go
  through `.base` (e.g., `commit.base.original_message` instead of
  `commit.original_message`). This affects approximately 12 call sites and adds a small
  amount of syntactic noise.

- **`serde(flatten)` is a commitment.** If the AI view types are later deserialised from
  external input (e.g., round-tripping through YAML in tests or accepting structured AI
  responses), the `serde(flatten)` quirks on deserialisation may surface. The current
  serialisation-only usage avoids this, but the constraint should be documented and
  respected.

**Neutral:**

- **`RepositoryViewForAI` remains a usable name.** The type alias preserves the existing
  name at all import and usage sites. Code reads the same; only the definition changed from
  a struct to a type alias.
