ADR-0049: Content Model Round 2 — Type Categories, Template Instantiation, Trace Relations, Completeness Rules¶
- Status: accepted
- Refined by: ADR-0051 (fromText-flagged references synced from in-text doc: links), ADR-0085 (one trace vocabulary over document relations and item links)
- Date: 2026-07-06
- Deciders: Nicolas Burri ("use the recommended options for R-1 to R-4")
- Refines: ADR-0021 (relationship CRUD realized), ADR-0004 (type catalog gains category), ADR-0015 (periodic review scoped to PRESCRIPTIVE), ADR-0040 (retention anchored on RECORD closure — unchanged, now category-explicit); relates to the DECIDED checkpoint content-model-round2-design.md (R-1a..R-4a)
Decisions¶
1. Type categories (R-1a)¶
document_type.category text not null default 'PRESCRIPTIVE' check in ('PRESCRIPTIVE','RECORD',
'TEMPLATE') (V032). Behavior keys off the category as a capability flag (REQ-SEP-006 style):
periodic review + validity apply to PRESCRIPTIVE only (sweep + UI); retention/disposal semantics
stay anchored on RECORD closure; TEMPLATE gets the instantiate affordance (§2). WIs/checklists
are additional PRESCRIPTIVE types — categories stay exactly three. UI: category tabs on the
documents view (Documents=PRESCRIPTIVE / Records / Templates), tree-filter applies per tab;
search gains a category facet. DTO: type/category surface on DocumentSummary + VersionDetail.
Demo/type-admin REST: category settable on the existing type-defaults admin endpoints.
2. Template instantiation (R-2a)¶
A TEMPLATE-category document's markdown body may use {{key}} placeholders, declared in a fenced
```template-vars block (one per line: key | label | required(yes/no) | default), which
renders as a definition table in the preview, not as content. Validation at SUBMIT of a template:
every {{key}} in the body must be declared (undeclared → humanized 4xx blocking the freeze).
Instantiate (POST /api/documents/versions/{templateVersionId}/instantiate with
{typeId, scopeId, folderId?, title, variables:{key:value}}): allowed only on RELEASED,
currently-effective template versions; caller needs CREATE_DOCUMENT in the target scope; target
type must be RECORD-category and activated in the scope; required variables enforced (4xx lists
the missing keys); v1 variables are strings. Result: a new RECORD draft whose body is the
template body with placeholders substituted and the template-vars block stripped; provenance =
instantiated-from relationship (version-precise: new document → template VERSION) + audit
(INSTANTIATED, on the new document, changes noting the template version). No re-binding after
creation (form-builder is the staged evolution, checkpoint R-2b).
3. Trace relations (R-3a)¶
Relationship CRUD for the curated vocabulary references, satisfies, verifies — directional
document→document edges (the existing relationship table, ADR-0021; add a document-level
shape if the table is version-keyed: source_document_id/target_document_id columns or a
compatible reuse — implementation decides, migration V032). System-managed derived-from and
instantiated-from stay read-only in the API. Rules: no self-edges; no duplicate
(source,target,type); both ends visible to the creator (RLS enforces); cross-scope allowed when
visible. Permission: add/remove needs EDIT_DRAFT in the SOURCE document's scope; audited
(RELATION_ADDED/RELATION_REMOVED on the source document, changes carrying type+target).
At content-freeze the manifest is NOT changed (edges are document-level metadata; the freeze-time
pinning of target versions is recorded as audit changes on SUBMIT — cheap evidence precision
without touching REQ-DOC-015 manifests).
REST: GET /api/documents/{id}/relations → {outgoing:[{type,targetDocumentId,targetHumanId,
targetTitle,targetState}], incoming:[...symmetric...]};
POST /api/documents/{id}/relations {type,targetDocumentId};
DELETE /api/documents/{id}/relations/{type}/{targetDocumentId}.
UI: detail-page References section — outgoing grouped by type, incoming "referenced by",
add via a search-backed picker (reuses /api/search), remove per edge; both directions always
visible, mutation per permission.
4. Completeness rules (R-4a)¶
coverage_rule (V032): scope-keyed (RLS), {id, scope_id, name, source_type_id, relation_type,
direction ('INCOMING'|'OUTGOING'), counterpart_type_id, min_count} — read "every RELEASED
document of source type must have ≥ min_count relation_type edges in direction to RELEASED
documents of counterpart type". Governed like review policies: CONFIGURE_REVIEW_POLICY-gated
CRUD (/api/scopes/{scopeId}/coverage-rules), audited. Evaluation is live (no persisted state):
GET /api/scopes/{scopeId}/completeness → per rule {rule, satisfied, total, uncovered:[docs]}
— released versions only count as evidence; draft edges surface in the detail view but not in
rule satisfaction. UI: a Completeness page per scope listing each rule with its gap list on
top; plus the R-3 matrix page (source type × relation × counterpart type coverage table).
Release-gating on coverage is explicitly deferred (process decision, future ADR).
Consequences¶
- V032 migration (category column, relationship shape, coverage_rule table + RLS); separation suite gains relationship + coverage-rule cases.
- Sweep (
lqms_sweep_overdue_tasks) must skip periodic-review detection for non-PRESCRIPTIVE types (V032 touches the SECURITY DEFINER function per the established recreate pattern). - Demo seed evolves: REC type → RECORD category, a released checklist TEMPLATE with variables,
a small REQ/TEST type pair with
verifiesedges + one coverage rule (the 62304 story becomes interactive: dashboard shows the uncovered requirement). - Requirements → v1.6 pending: category semantics, template instantiation, REQ-REL-002/004 realized, completeness rules (new REQ-REP/REL rows).
- Deferred: form-builder templates, arbitrary relation vocabularies, coverage release-gating, completeness export.