feat(isms): document versioning and history (CS-701)

Merged
TO
4mo ago
+2293-615

Why

Approving an ISMS document made it published, but any subsequent edit reverted the whole document to unpublished and lost the approved version — no history, no way to draft an update without losing what's live. That's an audit exposure (auditors expect the exact version that was live when they sampled) and it discourages customers from keeping documents current.

What this delivers

Brings the six ISMS documents (Context 4.1, Interested Parties Register 4.2a, Requirements 4.2b/c, Scope 4.3, Leadership 5.1, Objectives 6.2) onto the same versioning model the Policies module already uses — published version + working draft + version history.

  • Approving freezes an immutable published version (v1) — content snapshot + rendered PDF/DOCX retained.
  • Editing an approved document creates a draft without touching the published version. v1 stays live and exportable while the draft accumulates.
  • Re-approving publishes the next version (v2); v1 moves into history.
  • Version history on every document detail page: date, approver, and per-version PDF/DOCX download.
  • Historical exports are byte-identical to what was approved (stored file), with a snapshot re-render fallback.

How it works (design)

The schema was already shaped for this but only ever kept one mutable version. Mirroring Policies' draftContent + currentVersionId pattern:

  • The live register tables + IsmsDocument.draftNarrative are the editable draft; IsmsDocumentVersion rows are published-only immutable snapshots, pointed to by IsmsDocument.currentVersionId.
  • On approve, IsmsVersionService freezes a full content snapshot into a new version inside the txn, then renders + uploads PDF/DOCX to S3 after the txn (so an S3 hiccup never orphans a row) — the Policies publish pattern.
  • Editing reverts status to draft but never touches currentVersion, so the published version stays live.

Scope of changes

  • schema + migration — IsmsDocument.{currentVersion,draftNarrative,draftSnapshot}, IsmsDocumentVersion.{contentSnapshot,changelog,publishedBy}; defensive backfill (feature is pre-GA / flag-gated).
  • api — IsmsVersionService, shared export-payload util, reworked approve(), AttachmentsModule wired in, GET /v1/isms/documents/:id/versions, optional versionId on export.
  • app — useIsmsDocumentVersions hook, IsmsVersionHistory section (one choke-point → all six documents inherit it), per-version download, "Published vN / editing creates a new draft" signal.

Testing

  • API: 312 tests pass (npx jest src/isms); new specs for the version lifecycle, snapshot, export routing, and best-effort renders.
  • App: 119 tests pass (npx vitest run …/documents/isms); new component + hook tests, permission-gated coverage retained.
  • Typecheck clean across all ISMS files (api + app).
  • Not yet exercised against a live seeded org (feature is flag-gated behind is-isms-enabled); covered by unit/integration tests.

Closes CS-701.

🤖 Generated with Claude Code

https://claude.ai/code/session_012CweXoSSEP89mX93u3Bdcp


Summary by cubic

Adds versioning and history to ISMS documents (CS-701) so approved versions stay live while edits happen in a draft. Migration preserves existing approvals; exports default to the current published artifact with reliable, org‑scoped per‑version downloads.

  • New Features

    • Approving freezes a published version; editing creates a draft; re-approve publishes the next version.
    • Per-version exports retained (PDF/DOCX) with snapshot re-render fallback; versionId supported on export.
    • Version history via GET /v1/isms/documents/:id/versions and a new UI section with per-version downloads.
    • Schema: IsmsDocument gains currentVersionId, draftNarrative, draftSnapshot; IsmsDocumentVersion gains contentSnapshot, changelog, and publisher linkage. New IsmsVersionService freezes versions and renders/uploads exports after the DB transaction.
  • Bug Fixes

    • Export integrity: versioned exports never read live/draft; default export serves the current published version; snapshot re-render fallback; errors surface via toast; unavailable formats are hidden; org-scoped requests; spinner states remain correct with overlapping downloads.
    • Consistency: approval snapshot reads the org profile through the same transaction; approval success is decoupled from post-approval refresh; history preserves approver via frozen metadata.
    • Migration safety: approvals are preserved; legacy snapshot-less version rows are dropped; currentVersionId is only set by the approve flow; foreign keys are added after legacy cleanup.