All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m42s
CI / Build container images (pull_request) Successful in 1m11s
CI / Auth e2e pack (pull_request) Successful in 7m47s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m50s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m38s
CI / Import/export fidelity gate (push) Successful in 56s
Add docs/vs-nfd/: the analysis brief, the as-is assessment (42 findings, all verified against the code), the prioritized action plan rev. 2 with issue references written back to every checkbox, the two-stage issue/ADR brief, and the full reviewed draft used to create the forge state. Add eight proposed ADRs 0019-0026 covering the VS-NfD architecture decisions: no security base functions (par. 52 VSA anchor), HKDF token key separation, external authentication, page classification, read-access audit trail (variant A), reproducible offline deployment, plugin trust model, and backup target restriction. Forge state created alongside this commit: 11 labels, milestones M24-M31, issues #188-#236 (docs-only change, no code touched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
79 lines
3.8 KiB
Markdown
79 lines
3.8 KiB
Markdown
# 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.
|
|
|
|
## 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).
|