# YAML Merge *Structural merge that keeps your comments, anchors, and block style intact.* ## Overview YAML is JSON's cousin with opinions. It has comments, anchors, aliases, multi-document streams, block vs flow styles, multi-line scalars, and meaningful whitespace. A text merger destroys all of it at the first sign of indentation drift. A naive structural merger round-trips through a plain object and loses comments, anchor references, and the precise quoting choices your team relies on. Aura's YAML merger sits in the middle: it is fully structural, node-aware, and **preserves formatting, comments, and anchors** through the merge. It uses the same three-way model as the other mergers — ancestor, ours, theirs — and it ships with presets for the YAML files you actually have in your repo: GitHub Actions workflows, Kubernetes manifests, Helm charts, Docker Compose files, CI configs. > YAML is not a data format. It is a configuration language with a personality. Merge it accordingly. ## How It Works ### Parse with events, not just values Aura parses YAML using a grammar that emits both the structural tree **and** the attached trivia: comments (leading, trailing, footer), blank-line separators, flow vs block style choices, scalar style (plain, single-quoted, double-quoted, literal `|`, folded `>`), and anchor/alias relationships. Each node carries: ```text Node { kind: Scalar | Mapping | Sequence | Anchor | Alias, value: normalized form, style: Plain | Quoted("\"") | Literal | Folded | Flow | Block, leading: comment[], trailing: comment[], anchor: Option<"name">, alias_of: Option<"name">, } ``` ### Three-way diff on the structural tree Just like [JSON deep merge](/json-deep-merge), the YAML merger walks the three trees in lockstep, classifying changes at each path. The interaction table is the same. The difference is what counts as "modified": - Changing a scalar value → modified. - Changing only its quote style → **not** modified. (Aura notes the style change and picks ours's style on conflict.) - Adding a leading comment → not a structural modification, but tracked. - Reordering mapping keys → tracked as a reorder; never a conflict unless paired with structural edits. ### Anchors and aliases Anchors (`&name`) and aliases (`*name`) form a reference graph. A merge that breaks this graph would silently corrupt the document. Aura's algorithm: 1. Collect the anchor table from all three versions. 2. Merge anchor **targets** (the node labeled with the `&`) as normal mapping/sequence nodes. 3. Aliases (`*name`) follow the merged target automatically. 4. If both sides add different anchors with the same name, Aura renames the theirs-side anchor (`name-2`) and rewrites its aliases — never silently losing one. ### Comments travel with their anchor point Comments are attached to a semantic anchor: the key they precede, the sequence item they sit above, or the scalar they annotate. During merge, comments follow their anchor: - If the anchor node survives, its comments survive. - If both sides added leading comments to the same anchor, both are preserved in order (ours-first). - If both sides **modified** the exact same comment text, Aura raises a comment-level conflict — rare but real for doc-heavy YAML. ### Serialize with the inherited style profile After merge, Aura writes the tree back using a style profile learned from ancestor + ours: indent width (2 or 4), sequence dash indentation, flow vs block preference per path, scalar quoting habits, document-end `...` markers. The output diffs cleanly against the ours side on all unchanged regions. ## Examples ### A: Two steps added to a GitHub Actions workflow Ancestor `.github/workflows/ci.yml`: ```yaml name: CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install - run: npm test ``` Ours adds a cache step: ```yaml steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: node_modules key: ${{ hashFiles('package-lock.json') }} - run: npm install - run: npm test ``` Theirs adds a lint step: ```yaml steps: - uses: actions/checkout@v4 - run: npm install - run: npm run lint - run: npm test ``` With `keyed:sequence-index` this would conflict. With Aura's default for workflow steps — `append-distinct` keyed by the `uses`/`run` content — both additions interleave in the order the user would expect: ```yaml steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: node_modules key: ${{ hashFiles('package-lock.json') }} - run: npm install - run: npm run lint - run: npm test ``` Comments above each step, if present, travel with the step. ### B: Comments preserved Ancestor: ```yaml # Retry policy for the payment worker retries: 3 # Exponential base in seconds backoff: 2 ``` Ours: ```yaml # Retry policy for the payment worker retries: 5 # Exponential base in seconds backoff: 2 ``` Theirs: ```yaml # Retry policy for the payment worker retries: 3 # Exponential base in seconds backoff: 4 ``` Merged: ```yaml # Retry policy for the payment worker retries: 5 # Exponential base in seconds backoff: 4 ``` Both comments intact. Line merge often drops the second comment when hunks straddle it. ### C: Anchors and aliases Ancestor: ```yaml defaults: &defaults timeout: 30 retries: 3 dev: <<: *defaults host: dev.local prod: <<: *defaults host: prod.example.com ``` Ours tightens defaults: ```yaml defaults: &defaults timeout: 15 retries: 3 ``` Theirs adds a field to defaults: ```yaml defaults: &defaults timeout: 30 retries: 3 user_agent: aura/1.0 ``` Merged: ```yaml defaults: &defaults timeout: 15 retries: 3 user_agent: aura/1.0 dev: <<: *defaults host: dev.local prod: <<: *defaults host: prod.example.com ``` Both aliases continue to resolve against the merged anchor. ### D: Flow vs block style Ancestor: ```yaml ports: [80, 443] ``` Ours switches to block style and adds a port: ```yaml ports: - 80 - 443 - 8080 ``` Theirs adds a port in place: ```yaml ports: [80, 443, 8443] ``` Aura detects both as inserts into the same sequence. It applies both, chooses the ours-side style (block), and emits: ```yaml ports: - 80 - 443 - 8080 - 8443 ``` The style change is surfaced in the merge summary, not as a conflict. ### E: Multi-document streams Kubernetes manifests routinely hold multiple documents separated by `---`. Aura merges documents by identity — `apiVersion + kind + metadata.name + metadata.namespace` — rather than by position. Adding a new `Service` in ours and a new `ConfigMap` in theirs merges both; editing the same `Deployment` on both sides raises a conflict scoped to that document. ## Edge Cases **Mixed tabs and spaces.** YAML disallows tabs for indentation. If any side introduces a tab, the parser errors and Aura falls back to text merge with a diagnostic. **Merge keys (`<<:`).** Treated as structural inheritance, not as a scalar. Aura merges the referenced anchor and keeps the `<<:` in place. **Tags and custom types.** `!!str`, `!!int`, `!Secret` — tags are preserved on nodes. A tag change on one side combined with a value edit on the other is a conflict; isolated tag changes follow the same take-theirs / keep-ours rules as values. **Duplicate keys.** Invalid per spec but occasionally seen. Aura emits a warning and treats the last occurrence as authoritative for merge; the duplicated key in the output is preserved if both ancestor and merged copies still carry duplicates. **Very long scalars.** Block scalars (`|`, `>`) preserve their chomping indicator (`|-`, `|+`, `>-`). Content edits within a block scalar fall back to **line-level** merge *within that scalar* — YAML does not give you finer structure than a literal string. **Round-trip fidelity.** Aura passes a round-trip test on every supported YAML file in its corpus: parse, serialize, re-parse, check structural equality. The test is run as part of CI on every grammar or serializer change. Files failing round-trip are either excluded from AST merge or the adapter is patched — Aura never silently drops structure. **Custom schemas.** For in-house YAML formats, `.aura/merge.json` accepts per-path strategy rules exactly like JSON. A workflow file might declare `steps` as `append-distinct`, `env` as `keyed:name`, and `artifacts` as `union`. Strategies cascade — an unspecified child path inherits its parent's rule. **Block scalar styles.** The `|` (literal) and `>` (folded) indicators, along with chomping (`-`, `+`) and indentation indicators, are preserved verbatim on unchanged scalars. When one side edits a block scalar's content, Aura keeps the style from that side. When both sides edit content with differing styles, the conflict record surfaces both styles so the resolver can pick. **Stream separators and directives.** Multi-document streams use `---` separators and can declare `%YAML 1.2` directives. Aura preserves the separator placement and any directive comments attached to it. A document added on one side appears after the existing documents, with a fresh separator, unless a positional hint (top-of-file header comment) indicates otherwise. **Empty values and missing keys.** `timeout:` (no value, implicit null) and `timeout: null` parse to the same value but serialize differently. Aura preserves the observed form of the winning side; explicit `null` and implicit null do not cross-contaminate. **`.j2` / Go templates inside YAML.** Helm and Ansible embed Jinja or Go template syntax that is not valid YAML until rendered. Aura's grammar is tolerant of templated braces in scalars but errors on structural interpolation (template constructs spanning mapping keys). On failure, Aura falls back to text merge. See [limitations and edge cases](/limitations-and-edge-cases). ## See Also - [JSON deep merge](/json-deep-merge) - [.env merge](/env-merge) - [How AST merge works](/how-ast-merge-works) - [Merge strategies](/merge-strategies) - [Limitations and edge cases](/limitations-and-edge-cases)