S1: Saving a draft can change a published contract document type, with no reason and no step-up. Decision D615. Migration 416. Found by #1165's audit (area 7, registry publish).
What is true on main
- Publishing is guarded.
POST /api/admin/contracts/registry/:doc_type/publish (routes/admin_contract_doc_registry.ts) is behind the Super Admin's write bar: a TOTP session, a step-up, and a reason of at least 10 characters. It is audited as registry.type.publish.
- Saving a draft is not.
services/contractDocTypeRegistryStore.ts saves a draft with ON CONFLICT(doc_type) DO UPDATE. That overwrites the row's title, layer, party roles and required fields, and keeps status = 'published' when it was published. So a draft save on a published type changes the live type at once:
- with no reason, no step-up and no audit;
- and with no pending copy kept.
The work
- A pending draft beside the live values. Migration 416 adds what the store needs to hold a draft for a published type apart from its published values, for example
draft_* columns or a draft row keyed apart. Additive only.
- Saving a draft never changes a published type's live values. On a type that is only a draft, saving works as it does today.
- Publishing applies the pending draft to the live values, under the existing bar and audit, then clears the draft.
- The registry reads (
doc-types and the hub) return the live values plus the pending draft, so a page can show both.
- D615 records the rule. Update the copy on
/admin/contracts (frontend/src/pages/hq/ContractsPage.jsx) only if it describes the old behaviour.
Files this task owns
cloudflare-worker/sql/migrations/416_*.sql
cloudflare-worker/src/services/contractDocTypeRegistryStore.ts
cloudflare-worker/src/routes/admin_contract_doc_registry.ts
- new Worker tests
documentation/architecture/decisions/D615.md
Done when
- Tests cover:
- a draft save on a published type leaves its live values and status unchanged, and keeps the draft;
- publishing applies the draft, clears it and is audited;
- a draft-only type saves as today;
- the reads return both the live values and the draft.
- Every new assertion is mutation-checked.
Generated by Claude Code
S1: Saving a draft can change a published contract document type, with no reason and no step-up. Decision D615. Migration 416. Found by #1165's audit (area 7, registry publish).
What is true on
mainPOST /api/admin/contracts/registry/:doc_type/publish(routes/admin_contract_doc_registry.ts) is behind the Super Admin's write bar: a TOTP session, a step-up, and a reason of at least 10 characters. It is audited asregistry.type.publish.services/contractDocTypeRegistryStore.tssaves a draft withON CONFLICT(doc_type) DO UPDATE. That overwrites the row's title, layer, party roles and required fields, and keepsstatus = 'published'when it was published. So a draft save on a published type changes the live type at once:The work
draft_*columns or a draft row keyed apart. Additive only.doc-typesand the hub) return the live values plus the pending draft, so a page can show both./admin/contracts(frontend/src/pages/hq/ContractsPage.jsx) only if it describes the old behaviour.Files this task owns
cloudflare-worker/sql/migrations/416_*.sqlcloudflare-worker/src/services/contractDocTypeRegistryStore.tscloudflare-worker/src/routes/admin_contract_doc_registry.tsdocumentation/architecture/decisions/D615.mdDone when
Generated by Claude Code