Skip to content

adr_approve: approval_notes insertion corrupts YAML frontmatter when last field has no trailing newline #26

@le-dawg

Description

@le-dawg

Summary

adr_approve with approval_notes silently produces invalid YAML in the approved ADR file. The bug causes the approval_date/approval_notes keys to be concatenated onto the last character of the previous line with no newline separator, creating a YAML parse error on every subsequent read.

Root Cause

In adr_kit/workflows/approval.py, _update_adr_status (lines 264–280):

if input_data.approval_notes:
    yaml_end = new_content.find("\n---\n")          # finds the \n before the closing ---
    if yaml_end != -1:
        approval_metadata = (
            f'approval_date: {datetime.now().strftime("%Y-%m-%d")}\n'
        )
        if input_data.approval_notes:
            approval_metadata += f'approval_notes: "{input_data.approval_notes}"\n'

        # BUG: new_content[:yaml_end] ends at the last char of the previous line (no \n).
        # approval_metadata has no leading \n.
        # Result: the new keys are pasted directly onto the end of the previous line.
        new_content = (
            new_content[:yaml_end] + approval_metadata + new_content[yaml_end:]
        )

new_content.find("\n---\n") returns the index of the \n character that precedes ---. So new_content[:yaml_end] ends at the last non-newline character of the preceding YAML line. When approval_metadata is appended immediately after, the result is:

rationales: ['...last item']approval_date: 2026-05-19

instead of:

rationales: ['...last item']
approval_date: 2026-05-19

Confirmed Impact

ADR-0011 in the wild:

  rationales: ['...when passed to generateSasToken']approval_date: 2026-05-19
  approval_notes: "Surfaced by Task 6 security review..."

YAML parse result:

yaml.scanner.ScannerError: while parsing a block mapping
  expected <block end>, but found '<scalar>'
  in "<unicode string>", line 8, column 241:
     ... hen passed to generateSasToken']approval_date: 2026-05-19

The corruption is silent. adr_approve returns success: true and the file is written — but the resulting file cannot be parsed by a standards-compliant YAML parser. Future calls to adr_planning_context, adr_preflight, and the constraints contract builder will silently skip or fail on the corrupted ADR.

Secondary Bug: Unsafe String Interpolation in approval_notes

approval_metadata += f'approval_notes: "{input_data.approval_notes}"\n'

If approval_notes contains a double-quote, backslash, or newline, this produces malformed YAML. The value should be YAML-serialized (e.g. via yaml.dump({'approval_notes': value})), not string-interpolated.

Steps to Reproduce

  1. Create any ADR with a policy.rationales list (multiline YAML value at the end of the frontmatter).
  2. Call adr_approve(adr_id="ADR-XXXX", approval_notes="Any notes here").
  3. Open the produced .md file and attempt yaml.safe_load() on the frontmatter.
  4. Observe ScannerError.

Also reproducible when any YAML field other than rationales is last in the frontmatter and lacks a trailing newline (this is the general case; YAML spec does not require one).

Fix

Change line 278 to insert a leading newline in the metadata string:

# Before
new_content = (
    new_content[:yaml_end] + approval_metadata + new_content[yaml_end:]
)

# After — add a leading \n so metadata starts on its own line
new_content = (
    new_content[:yaml_end] + "\n" + approval_metadata + new_content[yaml_end:]
)

Additionally, replace the string-interpolated approval_notes value with a proper YAML serialization:

import yaml as _yaml
approval_metadata += "approval_notes: " + _yaml.dump(
    input_data.approval_notes, default_flow_style=True
).strip() + "\n"

Related

Environment

  • adr-kit version: current PyPI release (tested via uv tool install adr-kit)
  • Python 3.14
  • Triggered by approving an ADR with a policy.rationales array in its frontmatter and passing approval_notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions