Data Model: Docs lint

All shapes are frozen dataclasses in the two scripts. They are pure values with no I/O.

UnreleasedSection (scripts/release/validate_release.py)

FieldTypeNotes
start_lineint1-based line number of the ## [Unreleased]… heading in the source file
linestuple[str, ...]lines after the heading, up to but excluding the next level-2 release heading outside a fenced block (or EOF)

Invariant: unreleased_section(text) returns None when no Unreleased heading exists. A line inside a ``` fence never ends the section.

Entry (scripts/docs/check_changelog_style.py)

FieldTypeNotes
sectionstrthe enclosing ### heading text
subsection`str \None`
lineint1-based file line of the - bullet
linestuple[str, ...]the entry's own raw lines (bullet plus continuation lines at indent < 2)
nestedtuple[tuple[str, ...], ...]each nested sub-item's raw lines; deeper nesting belongs to its level-1 item
headline`str \None`

Derived: prose = the joined text of lines; length = len(prose) in code points; body = the text after the headline and refs.

Finding (shared shape in both scripts)

FieldTypeNotes
severityLiteral["error", "warning"]only error affects the exit code
rulestra stable rule id, e.g. heading-order, refs-in-bold, contrast, length, banned-token, typo, us-spelling
pathstrrepo-relative canonical path
lineint1-based real file line (the section offset is already applied)
wherestrfor the guard, "[<section>] <headline excerpt ≤ 60 chars>"; for spelling, the word
fixstran imperative, exact fix sentence

Rendering: path:line: [rule] where — fix. Sort order is (path, line, rule), which keeps output deterministic (NFR-003).