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(defaultdoc/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 aREADME.mdnext to the ADRs, are ignored.- Parsing
app.domain.records.parse_recordturns the exact bytes into aParsedRecord. 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 withyaml.safe_load, validates known fields (app.domain.validation) and derives the title and date.- Title
The
titlefield, else the first ATX#heading outside code fences, else the humanised filename slug.title_source(frontmatter,h1orfilename) records which one was used.- Date
decision-dateonly. A date taken from the filename prefix is stored withdecision_date_inferred = trueand always shown as inferred.- Kind
record_kindiserratumwhen the frontmatter haserratum-for, otherwisedecision. 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_htmlis safe to embed as-is.
Identity¶
Three identifiers are easy to confuse:
record.idA 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_uuidThe
uuidauthored in the frontmatter, if valid.record_keyThe 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:
digestThe current content revision.
previous_digestThe digest the row had before its last content change. It is the fallback base of a diff when the identity has no decisions.
updated_atMoves only when the digest changes, so “recently changed” sorting reflects content changes.
last_seen_atMoves on every upsert: the poll or push heartbeat.
first_seen_atSet once when the row is created.
change_tokenAn opaque adapter value (for example
mtime_ns:sizeor 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):
updatesFrom the newer decision to the earlier decision it updates.
obsoletesFrom the replacing decision to the decision it replaces. The legacy
deprecatesfield is merged into it (legacy_aliaswarning).correctsFrom 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_encodingerrorThe bytes are not valid UTF-8. The digest falls back to
sha256-raw-v1and the record cannot be decided.bad_yamlerrorThe frontmatter is not valid YAML. No field is read, so the record also has no UUID.
missing_frontmattererrorNo
---frontmatter block.missing_uuid,bad_uuiderrorNo
uuid, or not a valid UUID; the fallback key is used.bad_dateerrordecision-dateis not an ISO date.bad_field_typeerrorA known field has the wrong shape, for example
updatesthat is not a list of UUIDs.missing_datewarningNo
decision-date.unknown_fieldwarningA frontmatter key the catalog does not understand.
legacy_aliaswarningdeprecatesused; merged intoobsoletes.legacy_signoffwarningsigned-off-bypresent; shown as unverified, never an approval.obsolete_kind_fieldwarningkind: decisionpresent; ignored.legacy_erratum_formaterrorkind: erratumor acorrectskey: 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_targetwarningA link’s UUID matches no record.
ambiguous_targetwarningA link’s UUID matches several identities at the best tier.
duplicate_uuiderror or warningAn 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_erratumwarningAn erratum’s
erratum-forresolves 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.