dorfteich/docs/architecture/adr/0022-page-classification.md
Claude Fable 5 e505fc74dc
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m34s
CI / Build container images (pull_request) Successful in 14s
CI / Auth e2e pack (pull_request) Successful in 9m36s
CI / Import/export fidelity gate (pull_request) Successful in 57s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
#212: mark attachment downloads by filename prefix and companion file
Downloads whose effective classification is vs_nfd carry the documented
VS-NfD_ filename prefix (single source classificationFilenamePrefix() in
shared; ADR 0022 records the short form for file names). Effective
classification: the linked page's level; an attachment with unset pageId
(paste-then-insert, pond-level) FAILS CLOSED to the highest level of any
live page in its pond. The pond export ZIP adds a sibling
<file>.classification.txt companion with the full marking for classified
media, next to the manifest entry (#210). Documented in operations.md,
incl. the deliberate residual risk: the file's own content carries no
marking (recorded on #231, not hidden). Tests: prefixed classified
download, unchanged open download, fail-closed orphan both ways, ZIP
companion + manifest level.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:29:16 +02:00

90 lines
4.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR 0022: Classification as first-class page metadata
- Status: proposed
- Date: 2026-07-29
## Context
VS-NfD content must be marked, in every output that leaves the system.
Dorfteich has no classification concept today: `model Page` carries title,
slug, tree position and timestamps, and nothing else that could express a
protection level.
The obvious shortcut is to reuse labels. Verification shows why that
fails:
- `Label` is **pond-scoped** (`pondId`), so the same classification would
be a different object in every pond, with no instance-wide meaning.
- Labels are **user-editable** by any editor; a marking must not be
removable as a matter of routine content work.
- Labels do **not inherit** down the page tree, so a subpage of classified
content would silently be unmarked.
- Labels **never leave the application**: `export.service.ts` loads
`labelIds` only to feed `permissions.filterPages`, and no export path
writes them out. A carrier that does not reach the output channels cannot
serve as a marking.
The second question is architectural: should the application separate
classification _levels_? It must not (ADR 0019, and the plan's Phase 0
guardrails). Separation is a platform property.
## Decision
1. **A dedicated enum field on `Page`**, with an instance-wide default from
`instance_settings`. Not labels, for the four reasons above.
2. **Separation of levels happens outside the application: one instance per
classification level.** The application marks; it does not isolate.
This is the central operational decision of the whole undertaking and
belongs here rather than in a manual, because it defines what the
feature is _not_.
3. **The application-side ACL is order, not a protection mechanism.**
Permissions keep working as they do (central, default-closed,
deny-wins), and the classification field does not change them. Anyone
reading the code must not mistake the field for an isolation boundary —
the test in #204 pins that.
4. **Classification inherits down the page tree.** A new or moved page
takes at least its parent's level. Raising is ordinary editorial work;
**lowering requires a dedicated capability** in the central permission
model and is audited with old value, new value, actor and page.
5. **Every output channel carries the marking**, and each is an
independently closable issue: web view, browser print, server-side PDF,
DOCX/ODT, Markdown ZIP, feeds, public API, search results, no-JS shell,
attachment download. A channel that cannot carry it internally
(arbitrary binary attachments) is marked externally — filename prefix
plus companion file — and the remaining gap is a documented residual
risk, not a silent one.
6. **Unclassified content shows no marking.** Marking everything trains
users to ignore markings.
7. **The marking wording is fixed and locale-independent:**
`VS NUR FÜR DEN DIENSTGEBRAUCH` — the official formula of the German
VSA. It is deliberately **not** translated: a marking is a fixed legal
formula, and a localized variant would not be the marking. Only the UI
labels _around_ it (e.g. an accessibility label naming the element) are
i18n'd. The single source is `classificationMarking()` in
`@dorfteich/shared` (`packages/shared/src/pages.ts`); no output channel
hard-codes the string. Where a full wording cannot live — file NAMES of
attachment downloads (#212) — the established short form `VS-NfD` is
used as the prefix `VS-NfD_`, single source
`classificationFilenamePrefix()` in the same module.
## Consequences
- Ten issues, because there are ten output paths; that is the honest cost
of "in every output".
- The no-JS shell and the SPA are separate render paths, so each needs its
own assertion. Likewise the TipTap NodeView path and the server-side
`docToHtml` path differ structurally.
- The field is a precondition for the read-access audit trail (ADR 0023),
which is scoped to classified content only.
- Attachments inherit their page's classification. The case where the
page link is not yet set (paste-then-insert) fails closed.
- Because levels are separated by instance, a page can never "move
between levels" inside one deployment — export/import across instances is
the path, and its marking is covered by the export channels.
## Implementing issues
#204 (field + default), #205 (inheritance + downgrade right), #206 (web),
#207 (print), #208 (PDF), #209 (DOCX/ODT), #210 (Markdown ZIP), #211
(feeds/API/search/no-JS), #212 (attachments), #213 (upload warning).