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$`` (:py:obj:`app.domain.titles.is_adr_filename`). Other files, such as a ``README.md`` next to the ADRs, are ignored. Parsing :py:obj:`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 (:mod:`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 :doc:`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://`` (:py:obj:`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 :doc:`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 :doc:`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 (:doc:`/developer/decisions/0014-derived-display-status`). Relationships are resolved on the detail request only, from the published rows that can affect that record plus its own siblings (:doc:`/developer/decisions/0018-read-time-relationship-issues`). 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_encoding`` : error The bytes are not valid UTF-8. The digest falls back to ``sha256-raw-v1`` and the record cannot be decided. ``bad_yaml`` : error The frontmatter is not valid YAML. No field is read, so the record also has no UUID. ``missing_frontmatter`` : error No ``---`` frontmatter block. ``missing_uuid``, ``bad_uuid`` : error No ``uuid``, or not a valid UUID; the fallback key is used. ``bad_date`` : error ``decision-date`` is not an ISO date. ``bad_field_type`` : error A known field has the wrong shape, for example ``updates`` that is not a list of UUIDs. ``missing_date`` : warning No ``decision-date``. ``unknown_field`` : warning A frontmatter key the catalog does not understand. ``legacy_alias`` : warning ``deprecates`` used; merged into ``obsoletes``. ``legacy_signoff`` : warning ``signed-off-by`` present; shown as unverified, never an approval. ``obsolete_kind_field`` : warning ``kind: decision`` present; ignored. ``legacy_erratum_format`` : error ``kind: erratum`` or a ``corrects`` key: the retired erratum shape. Erratum-only codes are listed in :doc:`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_target`` : warning A link's UUID matches no record. ``ambiguous_target`` : warning A link's UUID matches several identities at the best tier. ``duplicate_uuid`` : error 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_erratum`` : warning 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 :py:obj:`app.services.approval_view.is_approvable`. Other issues, including errors such as ``bad_date``, are shown but do not block a decision.