app.domain.records

ADR record parsing, validation, title/date derivation and identity.

parse_record() turns the raw bytes of one Markdown ADR file into a ParsedRecord. It never raises for content problems: every problem becomes an Issue and the record stays storable and browseable.

Field handling (see docs/developer/domain/schema.rst):

  • uuid – required; canonical hyphenated UUID. Without a valid UUID the record is keyed by its source location (record_key()), its outbound relationships are disabled and it cannot be approved.

  • title – optional; fallback to the first Markdown H1, then to the humanised filename slug (ParsedRecord.title_source).

  • decision-date – optional YYYY-MM-DD. The filename date is never substituted silently: it is exposed as ParsedRecord.inferred_date and flagged by ParsedRecord.decision_date_inferred.

  • erratum-for – makes the document an erratum of the decision with that UUID (stored as the internal corrects link). Errata also use type (required), section, reported-by and date (the report date, exposed like decision-date), and their body is parsed into ParsedRecord.erratum (app.domain.errata). Without an explicit title an erratum is titled “Erratum: <section>”, else “Erratum: <humanised filename slug>”.

  • kind / corrects – the retired erratum shape; rejected with legacy_erratum_format and the file is treated as a decision.

  • updates / obsoletes – lists of UUIDs.

  • deprecates – legacy alias merged into obsoletes (warning).

  • signed-off-by – legacy, exposed as ParsedRecord.legacy_signoff for display as “Legacy sign-off (unverified)”; it never implies approval.

The value types and helpers live in app.domain.values, app.domain.titles and app.domain.validation; this module re-exports them so callers need a single import.

Public API:

class app.domain.records.ErratumContent(type, section, reported_by, reported_date, original, corrected, notes)

Bases: object

The full content of one erratum, safe to persist.

Parameters:
  • type (ErratumType | None) – Erratum type, None if missing or invalid (reported as bad_erratum_type).

  • section (str | None) – Where in the decision the error is, if given.

  • reported_by (str | None) – Free-text reporter; informational only.

  • reported_date (date | None) – Authored report date, if valid.

  • original (str | None) – Markdown of the “Original text” section, if any.

  • corrected (str) – Markdown of the “Corrected text” section; empty when missing (reported as erratum_missing_corrected_text).

  • notes (str | None) – Markdown of the notes, including unrecognised content.

type: ErratumType | None
section: str | None
reported_by: str | None
reported_date: date | None
original: str | None
corrected: str
notes: str | None
to_dict()

Convert to a JSON-serialisable mapping.

Return type:

dict[str, str | None]

Returns:

All fields by name; the type as its value and the date as YYYY-MM-DD.

classmethod from_dict(data)

Rebuild content from to_dict() output.

Parameters:

data (Mapping[str, object]) – Stored mapping; missing optional keys mean None.

Return type:

ErratumContent

Returns:

The content.

Raises:

ValueError – If the type or date is malformed, or a text value is not a string.

class app.domain.records.ErratumType(*values)

Bases: StrEnum

Kind of correction an erratum reports.

TECHNICAL = 'technical'
EDITORIAL = 'editorial'
class app.domain.records.Identity(source_id, project, record_key)

Bases: object

Logical identity of an ADR: record key within registration + project.

Parameters:
  • source_id (str) – Source registration id.

  • project (str) – Owning project path.

  • record_key (str) – UUID string or fallback key (record_key()).

source_id: str
project: str
record_key: str
class app.domain.records.LinkType(*values)

Bases: StrEnum

Outbound relationship types, named as written in frontmatter.

UPDATES = 'updates'
OBSOLETES = 'obsoletes'
CORRECTS = 'corrects'

Bases: object

Outbound UUID relationships of one record.

Parameters:
  • updates (tuple[UUID, ...]) – Earlier decisions this record updates.

  • obsoletes (tuple[UUID, ...]) – Decisions this record replaces (includes the legacy deprecates value).

  • corrects (UUID | None) – The decision this erratum corrects, if any.

updates: tuple[UUID, ...]
obsoletes: tuple[UUID, ...]
corrects: UUID | None
pairs()

List every outbound link with its type, in a stable order.

Return type:

tuple[tuple[LinkType, UUID], ...]

Returns:

(link type, target UUID) pairs.

to_dict()

Convert to the JSON-serialisable shape stored in record.links.

Return type:

dict[str, list[str] | str | None]

Returns:

{"updates": [...], "obsoletes": [...], "corrects": uuid-or-None} with UUIDs as strings.

classmethod from_dict(data)

Rebuild links from to_dict() output.

Parameters:

data (Mapping[str, object]) – Stored mapping; missing keys mean no links.

Return type:

Links

Returns:

The links.

Raises:

ValueError – If a stored UUID is malformed.

class app.domain.records.ParsedRecord(body, metadata, title, title_source, decision_date, inferred_date, links, adr_uuid, legacy_signoff, issues, encoding_ok=True, erratum=None)

Bases: object

Result of parsing one ADR file. All fields are safe to persist.

Parameters:
  • body (str) – Markdown body without the frontmatter block.

  • metadata (dict[str, JsonValue]) – Raw frontmatter as JSON-serialisable values.

  • title (str) – Display title.

  • title_source (TitleSource) – Where title came from.

  • decision_date (date | None) – Authored decision-date (date for an erratum), if valid.

  • inferred_date (date | None) – Date taken from the filename, if any.

  • links (Links) – Validated outbound links (empty without a valid UUID).

  • adr_uuid (UUID | None) – Authored record UUID, or None if missing/invalid.

  • legacy_signoff (tuple[str, ...]) – Unverified legacy signed-off-by values.

  • issues (tuple[Issue, ...]) – Validation issues in a stable order.

  • encoding_ok (bool) – False if the bytes were not valid UTF-8.

  • erratum (ErratumContent | None) – Erratum content, None for a decision.

body: str
metadata: dict[str, JsonValue]
title: str
title_source: TitleSource
decision_date: date | None
inferred_date: date | None
adr_uuid: UUID | None
legacy_signoff: tuple[str, ...]
issues: tuple[Issue, ...]
encoding_ok: bool
erratum: ErratumContent | None
property record_kind: RecordKind

Tell whether this is a decision or an erratum.

Returns:

RecordKind.ERRATUM iff erratum is set.

property decision_date_inferred: bool

Tell whether the shown date is only inferred from the filename.

Returns:

True when no decision-date was authored but the filename carries a date.

property display_date: date | None

Date to display.

Label it “inferred from filename” when decision_date_inferred is true.

Returns:

The authored date, else the filename date, else None.

property has_errors: bool

Tell whether any issue has error severity.

Returns:

True if at least one error was found.

property approval_blocker: str | None

Explain why this content cannot be approved, if it cannot.

Returns:

A reader-facing reason, or None if approvable.

property approvable: bool

Tell whether a reviewer may accept or reject this content.

Returns:

True if there is no identity or encoding error.

issue_dicts()

Convert issues to the stored JSON shape.

Return type:

list[dict[str, str]]

Returns:

One mapping per issue.

class app.domain.records.RecordKind(*values)

Bases: StrEnum

Kind of ADR record.

DECISION = 'decision'
ERRATUM = 'erratum'
class app.domain.records.TitleSource(*values)

Bases: StrEnum

Where the display title came from.

FRONTMATTER = 'frontmatter'
H1 = 'h1'
FILENAME = 'filename'
app.domain.records.derive_title(explicit, body, filename)

Choose the display title: frontmatter, first H1, then filename.

Parameters:
  • explicit (str | None) – Validated frontmatter title, if any.

  • body (str) – Markdown body.

  • filename (str) – File name or repository path.

Return type:

tuple[str, TitleSource]

Returns:

The title and where it came from.

app.domain.records.filename_date(name)

Extract the date prefix of an ADR file name.

Parameters:

name (str) – File name or repository path.

Return type:

date | None

Returns:

The date if the basename starts with a valid YYYY-MM-DD- prefix, else None.

app.domain.records.is_adr_filename(name)

Tell whether a file name matches the legacy ADR discovery pattern.

Parameters:

name (str) – File name or repository path; only the basename counts.

Return type:

bool

Returns:

True for names like 2024-12-24-use-postgresql.md.

app.domain.records.parse_record(raw, *, filename)

Parse and validate one ADR file. Never raises for content problems.

Parameters:
  • raw (bytes) – The exact bytes fetched or pushed.

  • filename (str) – File name or repository path (used for the title fallback and the inferred date only).

Return type:

ParsedRecord

Returns:

The parsed record with all issues found.

app.domain.records.record_key(adr_uuid, source_id, project, path)

Compute the stable record key used in the logical identity.

Parameters:
  • adr_uuid (UUID | None) – Authored UUID, if valid.

  • source_id (str) – Source registration id.

  • project (str) – Owning project path.

  • path (str) – File path inside the project.

Return type:

str

Returns:

The lowercase UUID string, or src:<source_id>/<project>/<path> when there is no valid UUID. A UUID is never invented.