Records

A record is one indexed ADR or erratum file, stored as a row of the record table. This page covers how a file becomes a row, how rows are identified, and how they relate to each other.

From file to row

Discovery

Source adapters list files under the registration’s path (default doc/decisions), recursively, and keep only file names matching ^\d{4}-\d{2}-\d{2}-(.*)\.md$ (app.domain.titles.is_adr_filename). Other files, such as a README.md next to the ADRs, are ignored.

Parsing

app.domain.records.parse_record turns the exact bytes into a ParsedRecord. It never raises for content problems: every problem becomes an issue on the record, so one broken file never affects another. It splits the --- frontmatter, loads it with yaml.safe_load, validates known fields (app.domain.validation) and derives the title and date.

Title

The title field, else the first ATX # heading outside code fences, else the humanised filename slug. title_source (frontmatter, h1 or filename) records which one was used.

Date

decision-date only. A date taken from the filename prefix is stored with decision_date_inferred = true and always shown as inferred.

Kind

record_kind is erratum when the frontmatter has erratum-for, otherwise decision. It is derived, never authored; see Errata.

Rendering

The Markdown body is rendered with markdown-it-py (CommonMark plus tables and strikethrough) and sanitized with nh3 at ingest. The stored rendered_html is safe to embed as-is.

Identity

Three identifiers are easy to confuse:

record.id

A surrogate UUID generated by the database for one row. It is what URLs and the API use (/records/{id}). It carries no meaning outside Adriano.

adr_uuid

The uuid authored in the frontmatter, if valid.

record_key

The authored UUID in lower case, or, when there is no valid UUID, the fallback src:<source_id>/<project>/<path> (app.domain.values.record_key). A UUID is never invented.

The logical identity of a record is (source_id, project, record_key): the same decision as seen through one registration and one owning project. Approval evidence is scoped to the logical identity, so the same UUID in another project or registration is a different decision as far as approval is concerned.

A record without a valid UUID is still stored and browseable, but it takes no part in UUID relationships and cannot be decided (see Approval). Moving such a file changes its fallback key and therefore its identity.

Published and candidate rows

The kind column separates two states:

published

Written only by polling, from the registration’s published_ref. Unique per (source_id, project, path). A complete poll deletes published rows whose files are gone.

candidate

Written only by CI push intake, from any other ref. Unique per (source_id, project, record_key, ref), so several candidates of one identity can exist on different refs (“competing candidates”). Polling never deletes candidates.

All rows that share a logical identity are siblings: revisions of one decision, never duplicates of each other. The detail page lists them as other revisions.

Revisions and change tracking

Every content version is stored once in the revision table, keyed by its digest (see Content hashing). A revision holds the canonical text (BOM removed, LF line endings), not the raw bytes. It is immutable and is what diffs and approval evidence refer to, so a diff never needs the source’s Git history.

On each row:

digest

The current content revision.

previous_digest

The digest the row had before its last content change. It is the fallback base of a diff when the identity has no decisions.

updated_at

Moves only when the digest changes, so “recently changed” sorting reflects content changes.

last_seen_at

Moves on every upsert: the poll or push heartbeat.

first_seen_at

Set once when the row is created.

change_token

An opaque adapter value (for example mtime_ns:size or a GitLab blob id) that lets the next poll skip unchanged files without fetching them.

Relationships

Links are authored by UUID and stored as record.links JSON (app.domain.values.Links):

updates

From the newer decision to the earlier decision it updates.

obsoletes

From the replacing decision to the decision it replaces. The legacy deprecates field is merged into it (legacy_alias warning).

corrects

From an erratum to the decision it corrects, authored as erratum-for.

app.domain.relationships.RelationshipIndex resolves a link to UUID U by preferring, in order, the same project of the same registration, then the same registration, then any registration. An identity is represented by its published row, else by its newest candidate. When several identities tie at the best tier the link is ambiguous and stays unresolved; nothing is picked arbitrarily.

From resolved links the index derives the inbound links (updated by, obsoleted by), the errata of a decision, and a display status: obsoleted if anything obsoletes the record, else updated if anything updates it, else active. There is no authored status field (0014: Derive display status from links).

Relationships are resolved on the detail request only, from the published rows that can affect that record plus its own siblings (0018: Compute relationship issues at read time). Candidates of other identities are never link targets.

Validation issues

Each problem is an app.domain.issues.Issue with a severity (error or warning), a stable code and a short message meant for readers. Per-document issues are found by parse_record and stored in record.issues. Relationship issues depend on other records and are computed when the record is read; they are never stored.

Stored at ingest

bad_encodingerror

The bytes are not valid UTF-8. The digest falls back to sha256-raw-v1 and the record cannot be decided.

bad_yamlerror

The frontmatter is not valid YAML. No field is read, so the record also has no UUID.

missing_frontmattererror

No --- frontmatter block.

missing_uuid, bad_uuiderror

No uuid, or not a valid UUID; the fallback key is used.

bad_dateerror

decision-date is not an ISO date.

bad_field_typeerror

A known field has the wrong shape, for example updates that is not a list of UUIDs.

missing_datewarning

No decision-date.

unknown_fieldwarning

A frontmatter key the catalog does not understand.

legacy_aliaswarning

deprecates used; merged into obsoletes.

legacy_signoffwarning

signed-off-by present; shown as unverified, never an approval.

obsolete_kind_fieldwarning

kind: decision present; ignored.

legacy_erratum_formaterror

kind: erratum or a corrects key: the retired erratum shape.

Erratum-only codes are listed in Errata. corrects_on_non_erratum is retired; it is no longer emitted but still decodes from rows stored by earlier versions.

Computed when read

missing_targetwarning

A link’s UUID matches no record.

ambiguous_targetwarning

A link’s UUID matches several identities at the best tier.

duplicate_uuiderror or warning

An error when another published file of the same identity uses the UUID; a warning when other identities (other projects or registrations) use it. A duplicate does not block decisions; only a missing UUID and bad encoding do.

erratum_target_is_erratumwarning

An erratum’s erratum-for resolves to another erratum.

When a record can be decided

ParsedRecord.approvable is true unless the file is not valid UTF-8 or the record has no valid UUID. For stored rows the same rule is app.services.approval_view.is_approvable. Other issues, including errors such as bad_date, are shown but do not block a decision.