# ADR-0013: Self-Describing YAML Output with Field Presence Tracking

## Status

✅ Accepted

## Context

omni-dev serializes a `RepositoryView` to YAML and passes it verbatim to AI
models as prompt input. The YAML contains many fields, but which fields are
populated depends on the command (`view` vs `info` vs `check`) and the
current repository state (whether commits exist, whether a PR template exists,
whether the branch has open pull requests, and so on).

An AI model receiving this YAML needs to know, without external documentation
or parsing the entire document, which fields are actually available in the
current output. Without that information a model may hallucinate field
references (e.g., referencing `branch_info.branch` when the `view` command
was used and that field is absent) or conservatively avoid fields that are
present but whose existence it cannot confirm.

Four approaches were evaluated:

- **No tracking.** The model inspects the YAML to determine what is present.
  This forces the model to parse and navigate the full document, and it still
  cannot distinguish a field that is absent from one that is present but
  empty. It also provides no human-readable explanation of what each field
  means.

- **Static documentation in the system prompt.** All fields are documented
  once in the system prompt, always listed as potentially present. The model
  cannot distinguish runtime availability from theoretical availability, so
  it must hedge every reference. Documentation and code diverge independently.

- **External schema (JSON Schema or separate doc).** A schema file describes
  the output structure. The schema is accurate about field shapes but not
  about runtime presence; it must be consulted out-of-band. AI models do not
  have reliable access to external files at inference time. Keeping the schema
  in sync with the code is a separate maintenance burden. An embedded JSON
  Schema (the `$schema` convention) avoids the out-of-band problem but still
  cannot represent runtime presence — the schema describes the shape of the
  type, not which optional fields the current invocation actually populated.

- **Self-describing output with inline field presence tracking.** Embed
  documentation metadata directly in the serialized output. Each field is
  listed with a description, an optional source git command, and a `present`
  flag computed from the actual runtime data before serialization. The model
  reads one document and knows both what each field means and whether it
  exists in the current output.

## Decision

We will embed a `FieldExplanation` value in every `RepositoryView` that
documents all output fields and tracks which are present at runtime.

### Data model

`FieldExplanation` contains a prose `text` introduction (including explicit
guidance for AI consumers) and a `Vec<FieldDocumentation>`. Each
`FieldDocumentation` entry carries:

- `name` — the YAML field path (e.g., `commits[].hash`, `branch_info.branch`)
- `text` — a description of what the field contains
- `command` — the git command that produces the data, when applicable
- `present` — a boolean set at output time to reflect actual data availability

### Presence computation

`RepositoryView::update_field_presence()` iterates over all documented fields
and sets each `present` flag based on the live data:

- **Always-present fields** (`working_directory.*`, `remotes`, `ai.scratch`)
  are set to `true` unconditionally.
- **Commit-dependent fields** (`commits[].*`) are set to `true` only when
  the commits list is non-empty.
- **Optional fields** (`versions.omni_dev`, `branch_info.branch`,
  `pr_template`, `pr_template_location`, `branch_prs`) are set to `true`
  only when the corresponding `Option` is `Some`.
- **Array-element fields** (`branch_prs[].*`) are set to `true` only when
  the array exists and is non-empty.
- **Unknown field names** — any name not matched by an explicit arm — are
  set to `false` by a catch-all.

For pure output commands (`view`, `info`) presence tracking is triggered
through `RepositoryView::to_yaml_output(&mut self)`, which calls
`update_field_presence()` then serializes. For commands that also pass the
view to AI (`twiddle`, `check`, `create_pr`), `update_field_presence()` is
called in the view-builder method. This is necessary because
`RepositoryViewForAI::from_repository_view` converts the view via
`map_commits`, which preserves `self.explanation` verbatim. The resulting
`RepositoryViewForAI` is serialized directly into the AI prompt, so the
explanation section — including all `present` flags — is part of what the
model reads as its input context, not just a side-effect of terminal output.

## Consequences

**Positive:**

- **AI consumers are self-orienting.** The explanation section is part of the
  YAML the model reads, so the model knows the field inventory and runtime
  availability without consulting external documentation or parsing the full
  document. The `present = true` guarantee is stated explicitly in the
  explanation text.

- **Always accurate.** Presence flags are computed from the same data object
  that is serialized, so they cannot lag behind the actual output. Adding a
  new command that populates a previously-optional field automatically causes
  that field's flag to flip to `true` in that command's output.

- **Human-readable.** Running any output command and reading the `explanation`
  block is sufficient to understand the full field vocabulary, the source of
  each value, and which fields are available in that invocation. No separate
  schema or documentation file is required.

**Negative:**

- **Two locations must be maintained.** Field names appear in both
  `FieldExplanation::default()` (as `name` values) and in the `match` arms
  of `update_field_presence()`. A name misspelled or omitted in one location
  causes a field to show `present = false` with data present, or to be
  undocumented. A test mitigates this: it constructs a fully-populated
  `RepositoryView`, calls `update_field_presence()`, and asserts every
  documented field has `present = true`. Any entry whose name is unmatched
  falls through to the catch-all (`false`) and fails the test. This catches
  divergence at test time, but not at compile time.

- **No compile-time link to struct fields.** If a struct field is renamed, the
  corresponding `FieldDocumentation` name and match arm must be updated
  manually. The compiler does not connect string literals to struct field
  identifiers.

**Neutral:**

- **Output size increases.** The `explanation` section adds one entry per
  documented field (currently 29) to every YAML output, adding roughly two to
  three kilobytes per invocation. This is acceptable for interactive use.
  The stripped-down `single_commit_view()` and `multi_commit_view()` used
  during batch AI dispatch omit the explanation to keep per-commit prompt
  sizes small.

- **`present = false` fields remain in the explanation.** Fields that are not
  present in the current output are still listed (with `present = false`) in
  the explanation section. This is intentional: a consumer can see the full
  field vocabulary and understand which fields might appear in other contexts,
  while knowing exactly what is available now.
