fix(isms): centrally serialize all content edits against approve (CS-701)

Merged
TO
4mo ago
+78-0

Why

Follow-up to #3366. On the production-deploy review (#3364), cubic correctly flagged (P1) that #3366's approve-race fix was incomplete: the advisory lock only protected paths that also take it, so a register update/delete (or narrative save / control link / regenerate) could still interleave while approve() loads EXPORT_DOCUMENT_INCLUDE and freezes the version โ€” leaving an approved version that omits the edit, or an edit that never invalidated the approval.

Fix โ€” enforced centrally (as cubic suggested)

Rather than add the lock to ~12 scattered transactions, invalidateApprovalIfNeeded now takes the per-document advisory lock as its first action. Every content-mutation path already calls it โ€” register create/update/delete, narrative save, control add/remove, and regenerate โ€” so all edits now serialize against approve() (which takes the same lock). Either the edit is frozen into the version, or it correctly reverts the document to draft; no interleaving can do neither.

The lock is transaction-scoped and re-entrant, so the register create paths that already acquire it for max(position) allocation are unaffected.

Tests

  • Adds utils/approval.spec.ts (lock-taken-before-status-read ordering + revert / no-op behavior).
  • $executeRaw added to the two tx mocks that exercise the real invalidateApprovalIfNeeded.
  • 324 API ISMS tests passing, typecheck clean, on top of current main (incl. #3366).

Closes the residual flagged on #3364; completes #3366.

๐Ÿค– Generated with Claude Code

https://claude.ai/code/session_012CweXoSSEP89mX93u3Bdcp


Summary by cubic

Centralizes the per-document advisory lock so all ISMS content edits serialize against approval, fixing a race that could freeze an incomplete version or let edits bypass invalidation. Supports CS-701 by ensuring published version snapshots are always accurate.

  • Bug Fixes

    • invalidateApprovalIfNeeded now acquires the per-document advisory lock first, so every edit path (register update/delete, narrative save, control link, regenerate) serializes against approve().
    • Prevents interleaving under READ COMMITTED: either the edit is included in the approved version or the document cleanly reverts to draft.
    • Lock is transaction-scoped and re-entrant, so existing create paths using the lock for position allocation are unaffected.
  • Tests

    • Added utils/approval.spec.ts to assert lock-before-status ordering and revert/no-op behavior.
    • Updated test transaction mocks to include $executeRaw in @db-backed specs.