Skip to content

SCRUM-215 — Config: create an immutable version from the draft

Plan ref: CFG-B5 (docs/11-admin-plane-plan.md). Stacked on SCRUM-214.

What changed

POST /api/admin/config/namespaces/{ns}/versions (live_ops), body {"message": "…", "revision": N} → 201 {namespace, version, schema_version, sha256, size, message, created_by, created_at}.

One transaction (store.CreateVersion): 1. SELECT … FROM config_draft … FOR UPDATE — serialises version creation per namespace. 2. revision ≠ N → 409 stale_revision (you version exactly what you reviewed). 3. Validate the draft against the latest schema (cached validator) → invalid → 400 validation_failed naming up to three pointers. 4. Canonicalise → sha256 → write the canonical bytes to the blob store before the INSERT (an orphan blob is harmless; a version pointing at a missing blob is not). 5. Same sha as the latest version → 409 no_changes "the draft is identical to version M". 6. Insert config_version (version = max+1), set config_draft.base_version, audit version.create {version, schema_version, sha256}.

Canonical form (schema.Canonical): keys sorted, no whitespace, floats canonicalised, integers kept exactly (RFC 8785's Canonicalize would turn 9007199254740993 into …992). Floats are canonicalised because Postgres jsonb can re-print them (1e2 → 100) and Patch re-derives manifest bytes from jsonb; Patch (SCRUM-224) implements the identical rule, and the two must stay identical.

How to verify

cd services/config
export CONFIG_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go vet ./... && go test -race -count=1 ./...
go test -count=1 -v -run 'Version|Canonical' ./internal/...
Test Proves
schema.TestCanonical* key order / whitespace independence, nested sort, 2^53+1 preserved, idempotent, bad JSON rejected, float spellings normalised
store.TestCreateVersion v1: sha256 = sha256(canonical), blob file exists with those exact bytes, base_version = 1, one audit row; repeat → ErrNoChanges
store.TestCreateVersionStaleRevisionWinsOverNoChanges stale revision reported first
store.TestCreateVersionPrepareErrorAborts a prepare failure leaves no version and no audit row
store.TestCreateVersionIsSerialised 5 concurrent creates → exactly one version, others no_changes
server.TestVersionsRoutesThroughTheLiveServer live_ops 201; viewer 403; empty message 400; invalid draft 400 naming the pointer; repeat 409 no_changes; stale 409; then GET namespaces shows latest_version: 1, has_unpublished_changes: false

Results at time of writing

  • go vet, go test -race ./... against Postgres 16: pass (7 packages).

How it was built

DeepSeek run scoped (Landlock) to services/config (165 s, ~31k output tokens). Claude review: transaction order and blob-before-insert checked; changed Canonical to also canonicalise floats (DeepSeek left them raw, which would make Config's and Patch's hashes disagree for a float after a jsonb round-trip) and added the test for it.