8. Crosscutting Concepts¶
8.1 Domain Model¶
erDiagram
MANDATOR ||--o{ SCOPE : "owns (null for global)"
SCOPE ||--o{ DOCUMENT : contains
SCOPE ||--o{ ROLE_ACTIVATION : activates
SCOPE ||--o{ TYPE_ACTIVATION : activates
ROLE ||--o{ ROLE_ACTIVATION : "from catalog"
DOCUMENT_TYPE ||--o{ TYPE_ACTIVATION : "from catalog"
ROLE_ACTIVATION ||--o{ ROLE_ASSIGNMENT : staffs
USER ||--o{ ROLE_ASSIGNMENT : holds
DOCUMENT ||--|{ DOCUMENT_VERSION : "has versions"
DOCUMENT_VERSION ||--|{ CONTENT_PART : comprises
DOCUMENT }o--|| DOCUMENT_TYPE : "is a"
DOCUMENT_VERSION }o--o| DOCUMENT_VERSION : "derived-from (provenance)"
DOCUMENT ||--o{ RELATIONSHIP : "source of"
DOCUMENT ||--o{ RELATIONSHIP : "target of"
DOCUMENT ||--o{ ACKNOWLEDGEMENT : "acknowledged via"
USER ||--o{ ACKNOWLEDGEMENT : makes
SCOPE ||--o{ REVIEW_POLICY : configures
DOCUMENT_TYPE ||--o{ REVIEW_POLICY : "per type"
REVIEW_POLICY ||--|{ APPROVAL_REQUIREMENT : requires
DOCUMENT_VERSION ||--o{ APPROVAL : "approved by"
DOCUMENT_VERSION ||--o{ SIGNATURE : "signed by"
DOCUMENT_VERSION ||--o{ REVIEW_COMMENT : "commented in review"
DOCUMENT ||--o{ PERIODIC_REVIEW : "periodically reviewed"
SCOPE ||--o{ AUDIT_EVENT : "logged in"
SCOPE ||--o{ TASK : "scoped in"
SCOPE ||--o{ NOTIFICATION : "scoped in"
USER ||--o{ TASK : "assigned / acts on"
Entities¶
| Entity | Description | Decided by |
|---|---|---|
MANDATOR |
Customer, or the operating organization itself; the hard data separation boundary. Carries an internal classification (operating org vs. customer) — a reporting attribute, not a behaviour switch |
ADR-0002, ADR-0010 |
SCOPE |
Workspace: type GLOBAL / MANDATOR / PROJECT, capability flags, nullable mandator FK. Every content-bearing table hangs off a scope for RLS enforcement |
ADR-0002 |
USER |
Individual identity anchored to a stable OIDC subject (table app_user, V003); lifecycle state (invited/active/deactivated); global (no RLS); acts within scopes via role assignments. Deactivation preserves attribution; never deleted |
ADR-0011, ADR-0013 |
ROLE |
Global catalog entry (name, description, translations); configurably mapped to a set of permissions | ADR-0004, ADR-0012 |
PERMISSION |
System-defined, closed-set named capability (content/functional or administrative); referenced by role mappings | ADR-0012 |
ROLE_ACTIVATION |
A scope opting in to a catalog role | ADR-0004 |
ROLE_ASSIGNMENT |
A user staffing an activated role in a scope; users may hold several roles | ADR-0004 |
DOCUMENT_TYPE |
Global catalog entry (SOP, WI, template, record, …); activated per scope like roles; carries the ID prefix (see below) | ADR-0004 |
DOCUMENT |
Identity + metadata of a managed document; lives in exactly one scope; carries the human-readable document ID and a content language; may link to a source via translation-of |
ADR-0001, ADR-0020 |
DOCUMENT_VERSION |
The unit all processes operate on: life-cycle state, version number, author, content (one or more parts; frozen as blobs on content-freezing, with a manifest hash), a content_format marker (PM_DOC — the tree record — is the only format written since ADR-0058; MARKDOWN is retained as unused schema headroom), version-level provenance link, effective_from date |
ADR-0001/0002/0003/0015/0017/0057 |
CONTENT_PART |
A named file within a version, stored as an immutable SHA-256 blob, carrying a kind (TEXT / IMAGE / TREE); the version's manifest hash binds the ordered set. A PM_DOC version (ADR-0057/0058) freezes TWO body parts — the authoritative ProseMirror tree (kind=TREE, media type application/prosemirror+json) and its derived canonical Markdown projection (kind=TEXT, the human-readable and searchable body) — plus any IMAGE attachment parts (PNG/JPEG, edited in a per-draft working area, frozen on release). The tree is the record; the projection is computed from it |
ADR-0017, ADR-0045, ADR-0057/0058 |
RELATIONSHIP |
A typed, directional link between documents (derived-from, translation-of, supersedes, references, related-to); provenance and translation are instances |
ADR-0021 |
ACKNOWLEDGEMENT |
A controlled record that a user read/acknowledged a document version, with an optional simple-questionnaire result; required roles per document drive who must acknowledge | ADR-0022 |
PERIODIC_REVIEW |
A recorded periodic review of a document: reviewer(s), date, outcome (confirmed unchanged / revision initiated), next-review-due; may carry a signature | ADR-0015 |
REVIEW_POLICY |
Per scope × document type: approval requirements, four-eyes flag (default on), transition permissions, release mode | ADR-0003 |
APPROVAL_REQUIREMENT |
Part of a policy: role + minimum count | ADR-0003 |
APPROVAL |
Recorded per version: user, role-in-which-approved, timestamp | ADR-0003 |
SIGNATURE |
Electronic signature on a version (when the policy requires it): signer, meaning (authored/reviewed/approved/released), timestamp, bound content hash, authentication proof; manifested and immutable | ADR-0014 |
REVIEW_COMMENT |
Review/rejection feedback, attached to the reviewed version | ADR-0003 |
AUDIT_EVENT |
Append-only: every state transition and security-relevant action (who, what, when, from → to state) | — |
TASK |
Actionable item generated from a domain event, assigned by role/user, tracked open → done/cancelled, auto-resolving; operational (not the record of truth) |
ADR-0019 |
NOTIFICATION |
Informational message (read/unread), scope-keyed; may reference a task/event | ADR-0019 |
Human-readable document IDs¶
- Every scope has a short, unique, immutable scope code (e.g.
P42,ACME); the global scope is the unprefixed root namespace. - Format:
[<SCOPE-CODE>-]<TYPE-PREFIX>-<DESIGNATOR>— the type prefix comes from the document type's catalog entry; the designator is either an auto-sequence number (per scope and type) or a short human-readable slug (charset[A-Z0-9_], length-limited, validated against reserved words such as type prefixes and scope codes). Examples:SOP-007,SOP-SALES(global),ACME-WI-003(mandator scope),P42-SOP-001,P42-SOP-CUSTOMER_CARE(created in project P42). - The numbering mode (
sequence/slug/ author's choice) is configured per document type in the catalog: slugs suit low-volume governing documents (SOPs, WIs), auto-sequences suit high-volume records (test reports, minutes). - IDs are immutable once assigned; display form appends the version:
SOP-007 v3. This applies to slugs too: a slug whose meaning has drifted is not renamed — the title carries current meaning, and a truly misleading slug is resolved by superseding (new document, revoke the old), as in any paper QMS. - Derived documents inherit the source's full document ID unchanged — the project's
tailored
SOP-007is recognizably theSOP-007everywhere, with provenance recording the exact source version. - Collision-free by construction: locally created documents always carry their scope's prefix, inherited IDs always carry the (different) source scope's namespace, and derivation only flows downward in the hierarchy — the namespaces are disjoint.
- Bonus: the ID alone reveals a document's origin (base-derived vs. project-specific) — provenance at a glance for auditors and daily work.
Provenance¶
The derived-from link points version → version ("derived from base SOP-007 v3",
never just "from SOP-007") so the audit question "which exact base version governed this
project on date X" is answerable by a single lookup (ADR-0002).
Content record (tree record + projection)¶
The authoritative content of a document version is a ProseMirror document tree
(the TipTap schema, format PM_DOC), not markdown text — the Confluence/ADF
pattern (ADR-0057). Markdown is a derived,
deterministic, canonical projection computed from the tree by ONE shared projection
function at save time and stored alongside it (the human-readable record copy, the text-diff
surface, the AI/MCP content surface, and schema-lock-in exit insurance). The projection's
correctness is pinned by a standing fixed-point test —
project(import(project(t))) == project(t) — over the repurposed fidelity corpus
(npm run projection). Editing modes over the tree: visual (TipTap — the engine chosen by
ADR-0059, which superseded the Milkdown/Crepe
choice of ADR-0055/0056) loads/saves the tree natively with NO round-trip guard (the
guard/fidelity machinery of ADR-0055/0056 is retired);
source mode edits the projection and re-imports text→tree on leave (normalization is by
design and immediately visible). Plain-text search extraction is computed server-side by
structurally walking the tree JSON (PmDocText, the Kotlin port of the projection module's
extractText) into a per-version search_text column, so the extraction can never drift
from the record and never depends on the client-computed projection (§8.7).
PM_DOC is the only content format (ADR-0058,
pre-production cut): every content write REQUIRES {treeJson, projection} (markdown-only writes
are rejected, 400), the dual-format coexistence and v1 markdown-it record path of ADR-0057 §5
are retired, and dev/demo databases are reseeded rather than migrated. ONE narrow as-built
exception remains: server-side template instantiation builds a MARKDOWN draft where no
JS runtime can construct a tree; such a draft converts to PM_DOC on its first editor save, so
the V040 record-integrity CHECK is state-qualified (a live PM_DOC draft must carry its tree)
rather than universal. Adding node types (math, color, block-in-cell tables) is
backward-compatible schema evolution under ADR-0057's known-limitations mechanism; renaming or
removing a node type requires an explicit, tested tree-blob migration with its own ADR.
8.2 Multi-Mandator Data Separation¶
See ADR-0002: row-level tenancy keys on all content-bearing tables enforced by PostgreSQL Row-Level Security, blob storage partitioned per scope (ADR-0001), automated separation test suite in CI (RISK-001). Database-per-mandator is the documented fallback for contractual demands.
RLS boundary (ADR-0027): the RLS
backstop guards content-bearing tables (documents, versions, content parts, audit
events, search index) keyed to lqms.authorized_scope_ids. Control-plane tables
(scope, mandator, catalogs, identity, authorization tables) are not under
per-request content RLS — their access is governed by the permission model (ADR-0012).
(Realized: 39 tables carry ENABLE/FORCE ROW LEVEL SECURITY with their policies, from the
V002 scope/mandator pattern through the content tables added by later migrations.)
RLS mechanism (ADR-0009): policies
key off the session variable lqms.authorized_scope_ids; every content-bearing table has
ENABLE/FORCE RLS and a policy granting the session its authorized scopes plus the
shared global base (REQ-SEP-005). The application connects as the RLS-subject role
lqms_app. The pattern and its proof are established on scope/mandator (V002,
RlsSeparationTest); every future content table follows it.
One deliberate exception to "policies key off lqms.authorized_scope_ids", and only one
(ADR-0107, V080): the
audit_event append policy keys off the acting user's assignment reach
(lqms_assignment_reach_ids(), resolved live from lqms.current_user_id), not their
VIEW-derived scope set. Its read policy is unchanged. Write where you are staffed,
read what you may view — so an administration-only account (admin ≠ content, REQ-ADM-004)
can record the acts its authority produces without being handed customer content.
Runtime mechanism (RlsScopeContext, ADR-0026): a unit of work runs inside a transaction
that sets SET LOCAL ROLE lqms_app and SET LOCAL lqms.authorized_scope_ids = …. Both are
transaction-local, so a pooled connection cannot carry a role or scope set into the next
request (RlsConnectionReuseTest), and even a superuser datasource (Quarkus Dev Services
in dev) is RLS-enforced inside the work (RlsScopeContextTest).
Runtime status (RISK-002): the mechanism is implemented, proven, and adopted — the
authenticated request path routes its work through AuthorizedRequestContext.runAs with the
caller's authorized scopes (241 runAs call sites across 55 main-source files as of
2026-08-18), so content transactions run transaction-locally as lqms_app. The residual
exposure is defence-in-depth, not a live separation gap: a new code path that forgot the
runAs wrapper would run with the datasource's privileges rather than failing closed, because
the two-role datasource split is not packaged yet — tracked as DEBT-001 in §11. The technical
mitigation is therefore complete; formal risk acceptance is still open in
process/risk_management.md (severity/probability scales and
acceptance criteria are undecided).
8.3 Document Life Cycles¶
See ADR-0003: fixed
states DRAFT → IN_REVIEW → RELEASED → REVOKED, plus the terminal CANCELLED
(from DRAFT or IN_REVIEW, ADR-0024); configurable review policies per scope and document
type; four-eyes rule default-on; editing a released document starts a new draft version.
Creating a new version requires a change description (REQ-DOC-017, §4.2.4©).
Release flow — STANDARD vs DIRECT (ADR-0091):
the per-(scope, type) review policy carries a release_flow (V062). STANDARD is the
submit→approve→release ceremony above; DIRECT lets an authorized role holder release straight
from DRAFT (no IN_REVIEW) — the friction-free path for records ("proof the process ran"). This
is a policy-permitted transition, not a new state: effectivity, retention, disposal,
acknowledgement, training fan-out and audit are all unchanged, and the comments-resolved +
training-questionnaire gates still evaluate (only the approvals gate is N/A). A structural
floor bars DIRECT for PRESCRIPTIVE and TEMPLATE types (§4.2.4 requires their independent
review) — enforced server-side and in the schema, never by convention. release_flow is
orthogonal to the pre-existing release_mode (AUTOMATIC/EXPLICIT, V013), which only qualifies a
STANDARD flow's release trigger. The runtime walkthrough is §6.1.
Effectivity & periodic review (ADR-0015):
a released version has an effective_from date (default = release, may be future); the
currently effective version is derived by date, not a stored state. Documents have a
configurable periodic-review interval (per type, per-document override); a periodic review
records reviewer/date/outcome and either resets the review clock (confirmed unchanged) or
starts a new draft. Overdue review / passed valid_until are flagged and surfaced, never
auto-revoked.
8.4 Security & Access Control¶
Authentication (ADR-0011, ADR-0028): delegated to an external OIDC provider (SSO; corporate IdP or bundled Keycloak) — LQMS stores no credentials. The web client uses a server-side (BFF) session held in an encrypted, httpOnly, Secure, SameSite token-state cookie — there is no server-side session store (stateless, so any instance serves any request), with idle/absolute timeouts and CSRF protection. The cookie is opaque to the client (no extractable claims) but is not server-revocable mid-lifetime; instead authorization is re-evaluated per request against live state, so administrative revocation (deactivation / role removal) takes effect on the next request (ADR-0028). Machine / MCP clients use OAuth2 bearer tokens (hybrid mode). Authentication grants no authorization; users are JIT-provisioned and gain access only via role assignments.
Authorization (ADR-0012): the
system defines a fixed, closed set of permissions; catalog roles are configurably
mapped to permission sets; holding a role in a scope grants its permissions across that
scope and its descendants (global-scope role = installation-wide admin; mandator-scope =
delegated admin over its projects). Administrative and content permissions are
independent — system administration does not implicitly grant content read, preserving
mandator separation. Document life-cycle transitions remain governed by review policies
(§8.3); the permission model governs all other functional and administrative actions.
Audit trail via AUDIT_EVENT. All enforcement is server-side — the frontend only
reflects permissions (ADR-0007); the API
exposes effective permissions for that purpose. On each authenticated request the user's
authorized scope set is resolved from role assignments and applied as the RLS context
(lqms.authorized_scope_ids, ADR-0009) — closing RISK-002 at runtime.
Bootstrap: a deployment-configured identity is granted system administration idempotently on startup (break-glass, no lock-out); first initialization auto-creates the global base scope and an internal mandator with a QMS-operations scope (ADR-0010, ADR-0012).
Authorization resolution (ADR-0009 hardening path): role activations and assignments
(role_activation, role_assignment, V005) are scope-bearing but not under
per-request RLS — they are read by SECURITY DEFINER functions that resolve, across scopes,
(a) lqms_authorized_scopes(user) — the scopes whose content the user may see (those where
the user has the VIEW permission effective, expanded over the scope subtree), which feeds
lqms.authorized_scope_ids; and (b) lqms_effective_permissions(user, scope). Because the
authorized set is gated on a content-read permission, an admin-only role grants no content
visibility — realizing "admin ≠ content" (REQ-ADM-004) at the RLS level. The functions'
owner must bypass RLS (superuser in dev; a BYPASSRLS admin role in prod).
Operation-scoped RLS exceptions (ADR-0031/0034/0035): beyond the resolution functions, several narrow SECURITY DEFINER paths deliberately cross the per-request RLS boundary, each self-gating on the caller's authorization (the caller-less system sweeps gate on being run only by the owner-connection scheduler):
- Derive-source read (V017): a
DERIVEholder reads exactly the one released base version being derived — no standing base visibility. - Overdue sweep (V022/V023/V024,
lqms_sweep_overdue_tasks) — the SYSTEM-context detector (ADR-0037/0039): a scheduler tick (advisory-lock elected, one instance) runs reminders, escalations, periodic-review-due and validity-expired detection across all scopes as the function owner. It has no caller gate (there is no acting user); its EXECUTE grant is deliberately not given tolqms_app— only the owner-connection scheduler path may run it (ADR-0039). Idempotent via stamps and role-qualified open-task guards; recipients resolve by the same V005 role cascade as enforcement. - Recurring-obligation sweep (V063/V066/V069,
lqms_sweep_recurring_obligations) and certification-renewal sweep (V050,lqms_sweep_certification_renewals) — additional SYSTEM-context detectors on the same pattern, each with its own advisory-lock-elected scheduler (RecurringObligationScheduler/CertificationRenewalScheduler) and its EXECUTE grant revoked fromPUBLIC. The obligation sweep raisesOBLIGATION_DUEtasks to a duty's responsible-role holders when its derived next-due window opens, and reconciles (marks DONE) an open task the moment a completing release moves next-due out of window (ADR-0089/0094, §6.2); like the overdue sweep it writes no per-raise audit row and stays scope-local per row. - Source-event fan-out write (V021,
lqms_fanout_source_event) — the only mechanism that carries one tenant's event into another tenant's scope (the sweep above writes system-side but strictly scope-locally): on release/revoke of a derive-source version, the SYSTEM inserts one SOURCE_UPDATED/SOURCE_REVOKED task + notification per affected derived document into the derived scopes (REQ-DOC-012/REQ-LC-013). Gates: event/state match, derive-source scope, caller authorized for the source scope; idempotent via a partial unique index; payload limited to derived-scope-local identifiers; every evaluated fan-out is audited in the source scope; the recipient-count return is an accepted, documented disclosure (ADR-0035). A startup check refuses to boot if the function owner cannot bypass RLS (the silent-failure deployment case).
Notification delivery (ADR-0041): the notification row is the outbox — a scheduler tick
(its own advisory lock, distinct from the sweep's) pushes undelivered rows through the pluggable
NotificationChannel interface (email via quarkus-mailer, disabled by default); per-row stamping
gives at-least-once delivery, poison rows are skipped and retried; content renders from structured
params in the recipient's locale.
Disposal execution (ADR-0042/0043): the one destructive path — a DISPOSE_RECORDS holder
disposes a fully-closed document in one hard-gated, locked transaction (legal hold, closure,
retention re-verified; tombstoning, retained disposal record, audit, index-purge hook); blob
deletion follows post-commit, manifest-aware and GC-backstopped (ADR-0043).
Orphan-blob garbage collection (ADR-0092, REQ-DOC-013):
the sweep that discharges the disposal path's GC-backstop promise. A nightly maintenance sweep
(system-scheduled, no acting user: BlobGcScheduler with its own advisory session lock; the
BlobGcSweepService/BlobGcRepository in document) iterates the content store's scopes and runs
each pass inside that scope's own RLS bounds (rls.withAuthorizedScopes), so blob_quarantine
(itself FORCE-RLS) is never read or written across scopes. Per scope it runs two-phase quarantine,
then purge: it computes stored blobs (ContentStore.list, off the file system) minus the live
reference set, records new orphans in a blob_quarantine
registry (V065), and only permanently deletes a quarantined blob once it has aged past the
configured window (default 30 days) and re-verifies unreferenced at purge time. The bin is a
registry row, not a file move, so the content-addressed store's invariants hold — an idempotent
re-put (dedup re-reference) simply works and rescue is a registry-row delete. The safety
invariant (referenced blobs are never deleted) is structural: reference check and registry
mutation share one transaction, file deletion is post-commit and idempotent, and a failed deletion
is simply re-quarantined next pass. The set of hash-carrying blob-reference columns is enumerated in
one place and an architecture test asserts the schema's column set equals that enumeration — so a
new blob-referencing column fails the test until the GC inventory is extended (the
schema-extension lifecycle trap, made structural).
Domain events (ADR-0034): life-cycle transitions publish typed events
(DomainEventPublisher, lifecycle module) synchronously inside the transition's
transaction; the notification module's handler persists the review workload atomically with
the transition. Emitting modules never reference consumers; the api module wires
subscriptions at startup. The registry is shaped for a later outbox replacement.
8.5 Persistence¶
See ADR-0001: PostgreSQL (via jOOQ) is the single source of truth; version content as immutable SHA-256-keyed blobs behind a storage interface (file system now, S3 later); blob-first write protocol; orphan-blob garbage collection.
Schema evolution: Flyway plain-SQL migrations in backend/persistence; jOOQ classes
are generated from the real migrated schema at build time
(ADR-0008).
Transactional boundaries and jOOQ usage patterns are settled by ADR-0036 (2026-07-02) — services carry rules, repositories carry jOOQ:
- The only transaction/RLS boundary is the service's
AuthorizedRequestContext.runAsblock (§8.2). Repositories never open transactions and never own connections — each repository method takes the transaction'sDSLContextas its first parameter, so the RLS discipline cannot be sidestepped from below. - Repositories own all persistence access: the generated
ch.lqms.persistence.jooq.*imports, query construction, row↔domain mapping, and the raw-SQL calls into thelqms_*SECURITY DEFINER functions. One repository per domain concept, in the module that owns it. - Services hold authorization checks, state-machine guards, policy evaluation, event
publication and orchestration, and carry no persistence-model imports. The
apimodule contains no persistence access at all. - Enforced by a Konsist architecture test: outside
persistence, test code, and the two RLS plumbing classes inscope, only files named*Repositorymay importch.lqms.persistence.jooq. - Repositories are deliberately not interfaces (ADR-0001 commits to PostgreSQL as the single source of truth, so swappability buys nothing).
8.6 Internationalization¶
See ADR-0020: UI text per-user
translatable (REQ-UI-002) and catalog entries carry translations (ADR-0004); timestamps
stored in UTC, displayed in the user's time zone, and manifested unambiguously on
signatures/audit; dates/numbers formatted per the user's locale. Document content
language is a per-document attribute; a translation is a separate, independently
controlled document linked via translation-of, with one optionally designated the
authoritative source. Search indexing is language-aware.
8.7 Search¶
See ADR-0006: three layers — (1) PostgreSQL full-text search as the baseline, (2) local RAG with locally computed embeddings stored in pgvector under RLS, (3) pluggable LLM provider (cloud/on-prem) receiving only retrieved, permission-filtered chunks. LLM policy per mandator (cloud / on-prem / none); every LLM call audit-logged with the chunks sent. The index is derived data — rebuildable, skippable in backups.
Layer 1 is realized (ADR-0044): a PostgreSQL
FTS search_index (V029, under FORCE RLS so results are scope-filtered like all content) is
populated event-driven at content-freeze; drafts are searchable by metadata, frozen bodies by
full text; disposal/cancellation scrub the index. Exposed as GET /api/search with an admin
reindex path. Layers 2–3 (RAG + pluggable LLM) remain future work.
Tree record and the extraction column (ADR-0057/0058). The frozen body that the FTS
indexes is now the derived Markdown projection of the PM_DOC tree (§8.1), so full-text
search already indexes the record's canonical text — the projection tokenizes to the same
lexemes the tree carries. Separately, each PM_DOC version carries a server-computed
search_text (V039), produced by a structural walk of the tree JSON (PmDocText, ADR-0057
§3) that can never drift from the record and never trusts a client extraction. This column is
not on the FTS query path: the M5′ "search cutover" was evaluated and shipped as a
deliberate no-op for user-visible search (the projection FTS already covers it); it becomes
load-bearing at the RAG/AI layer 2 (ADR-0058).
8.8 Error Handling, Logging, Audit¶
AUDIT_EVENT (§8.1) is the domain-level audit trail. It records not only life-cycle
transitions but field-level changes (old → new values) to mutable records and
security-relevant configuration (REQ-AUD-001/005, Part 11 §11.10(e) / Annex 11 §9), and
supports a complete human-readable inspection copy of a record (REQ-AUD-006).
Technical logging is separate from the domain audit trail.
The trail is evidence, not a read model (ADR-0110): every act writes it, but at runtime it is read only to be shown (activity surfaces, evidence exports) or counted — never to answer a business question. A lifecycle fact a surface needs (who released, who invited) lives on the domain row, written in the same transaction as its trail event; if it exists only in the trail, a migration promotes it to state (the V088 shape).
8.9 Data Protection & Retention¶
See ADR-0016: retention-primary (for regulated records, retention obligations override the right to erasure during the retention period); configurable retention per type/scope with a retention-start event, legal hold, and deliberate, audited disposal at end of retention. Personal data is minimized and attribution is pseudonymous (stable internal id; PII only in the user record), so PII can be severed without breaking record integrity. Erasure is narrow (no-retention-basis data, non-regulated scopes, end of retention) and done by physical deletion or user-record anonymization. Access and rectification are supported. Encryption-at-rest / crypto-shredding is deferred to the security NFRs (GAP-10). Swiss revised FADP + EU GDPR; ISO 13485 §4.2.5.
8.10 Derived state & parity contracts¶
Derived-never-stored is a house doctrine. Anything that can be computed from the record set is
derived live at read time, not persisted as a status that could silently drift. Instances:
effective version (by date, not a stored flag — §8.3, REQ-EFF-002); periodic-review due/overdue
(ADR-0076); requirement/risk coverage and the requirement/risk trace chains (TraceCoverage
edge-type-set engine, RequirementChainService, RiskTraceabilityService — ADR-0086); the
verification evidence class (planned vs. verified, derived from a per-type verification_evidence
flag + source-release state, no link-type change — ADR-0088); recurring-obligation due/overdue
(ADR-0089/0094); the validation matrix and GSPR-conformity views (ValidationMatrixService,
GsprConformityService — Stage C, ADR-0084: mirror-of-chains derivations that reuse the same
ADR-0088 evidence classing, nothing stored); and the scope traceability report — a point-in-time
compilation over the existing read models (its digest now spans all five axes, incl. the two Stage-C
views' sections), deliberately never a controlled document (ADR-0087). The pathology this
avoids is matrix-as-content: a stored overview is stale the moment a link changes and either rots
or forces perpetual re-release. Two exports on two days may legitimately differ, and say so.
Parity contracts are the load-bearing consequence: when the same fact is computed in two places, a test pins the two implementations against each other so they can never disagree. The standing ones:
- Markdown projection ↔ tree — the fixed-point
project(import(project(t))) == project(t)over the fidelity corpus (npm run projection, §8.1). PmDocText(Kotlin) ↔extractText(JS) — the server search extraction is a structural walk that must yield what the shared projection module would (§8.1/§8.7).- Obligation sweep (SQL) ↔ oversight derivation (Kotlin) — RESOLVED into a single encoding by
V069:
lqms_sweep_recurring_obligations(both its RAISE and RESOLVE passes) andRecurringObligationServiceall call the shared functionlqms_obligation_next_due, so the raised task and the reported verdict cannot diverge by construction (ADR-0089 §3, ADR-0094 §7 — the V049/V068 "one predicate, one encoding" precedent). Since V070 that rule is a FOLD over the obligation's completion history (each completion discharges the occurrence outstanding at its moment, ADR-0094's 2026-07-25 clarification); the function stays PURE and receives the history as an argument, so the data access — and with it the RLS boundary — remains on each caller's side while the encoding stays single. The test that used to pin two implementations against each other now pins the RULE itself on the hostile calendar histories (Jan-31→Feb, leap years, a completion exactly on an occurrence, LATE completions, cross-period late completions, and multi-completion periods). - Reflection ↔ enforcement — one
WorkflowActionEvaluatorseam (extracted fromDocumentService, 2026-07) computes both whatgetVersion.actionsadvertises to the UI and what the transition actually enforces, so a "hidden or greyed tile" can never diverge from the server's decision (the "server is the authority" principle, ADR-0080; the DIRECT-release action is one consumer, ADR-0091 §4).
Where a duplicated computation can instead be collapsed to a single source, that is preferred over
pinning two encodings: the reflection↔enforcement seam above is one instance, and since V068 the
effective training baseline anchor is a single SQL function (lqms_effective_training_baseline)
that the acknowledgement-coverage card and the training compliance roll-up both read — the two former
hand-coded encodings are gone (ADR-0070 §5's rule made structural; TrainingBaselineParityTest stays
as a guard), mirroring the lqms_effective_training_mode single-source (V049).
8.11 Trace items & the traceability web¶
Controlled documents carry keyed trace items — typed blocks inside the content tree, each with a
scope-unique key — and trace links between those items (or between whole documents). The
machinery is type-agnostic (ADR-0078): one set
of engines (extraction, the per-type registry, key-ownership/suspect resolution, coverage) reads a
type registry instead of hard-coding any single type, so adding a type is register a type — the
engines show a zero diff (the ADR-0078 acceptance property, guarded by TraceItemGeneralizationTest).
Registered types (TraceItemRegistry.DEFAULT, in registration order): requirement
(ADR-0077); risk + risk-control
(ADR-0079); and — Stage C
(ADR-0084) — user-need, validation-case,
design-element, and regulatory-ref. Each type's ordered attribute list is the content-hash
input and is hash-frozen once released content exists (a changed attribute flips the link
suspect flag; the key is identity and is not hashed). Edge vocabulary (trace_link.type):
verifies, satisfies, derives-from, references, mitigates, implemented-by, and — Stage C
(V067) — validates (validation-case/document → user-need) and complies-with
(requirement/design-element/document → regulatory-ref). These complete the five trace axes of the
traceability model: requirements, risk, design, validation, regulatory. Keys are one keyspace per
scope across all types, so key extraction, ownership and suspect resolution walk every registered
type over a document's tree (ADR-0078 §4).
May-define governance is configuration, not code. Whether a (scope, document-type) may define
items of a type is a policy row: requirement uses the may_define_requirements column, and the six
non-requirement types are gated per-(scope, type) via type_trace_item_definition (V058). One
attribute value set is itself per-scope configuration rather than a fixed tool enum — the
design-element designKind is validated on save against the scope's design-kind vocabulary
(design_kind_vocabulary, V067; shipped default architecture | interface | component | unit,
editable per scope, 422 on an unknown value) — a proven variation point per the
opinionated-by-default principle, in contrast to the normative fixed enums (risk-control
controlKind, regulatory-ref applicability) that the authoring UI constrains but the server does
not value-check. One deliberate exception to that last clause
(ADR-0102): a template variable declared date
or number is value-checked server-side at instantiation and stored normalized (ISO-8601 /
plain decimal) — the second server-side value rule after designKind, placed exactly where a wrong
value becomes a frozen record.
Table surfaces are views over the one tree
(ADR-0082): a grid holds no second store — it
drives the live content tree through generic, node-name-parameterized editor seams (listTraceNodes /
updateTraceAttrsAt / insertTraceItemAtEnd / deleteTraceAt). The risk grid and the GSPR authoring
grid are structural twins over that same four-method handle (the four bespoke risk seams were
folded into the generic ones), so the projection round-trips byte-identically no matter which grid did
the editing.
The derived read models over this web — coverage, the requirement/risk trace chains, the validation matrix and the GSPR-conformity view — are all derived-never-stored and RLS-bounded (§8.10); item authoring/dialogs and PDF rendering are covered by the content-record concept (§8.1) and the registry-driven PDF fence expansion (ADR-0074).