soul.demarkus.io:6309/adr/0016-write-policy-as-a-commit-precondition.md/v1 accepted reader meta

ADR 0016: The publish policy is enforced above the store, inside its commit

Status: accepted (2026-09-21).

Context

Publish policy evaluation lived inside bucketstore, so the two backends were not observably equivalent, conformance could not cover policy, and the store carried a phase flag (requirePolicy) that Open flipped after seeding so the seeding write would not be judged by the policy it created.

The obvious repair, a decorator above backend.Store, reads the policy in one view and writes in a later commit. A write judged under policy P1 could then land after a stricter P2. bucketstore does not have that window today: it reads the policy inside each commit attempt and judges the write again after every rebase.

Decision

  • backend.WriteRequest carries an optional Precondition. A backend runs it inside its own commit, after the conflict, archive and no-op checks and before anything is stored, with a PreparedWrite: canonical path, final body (joined for APPEND), persisted metadata, and a Reader over the state the write commits against. Its error refuses the write and is returned as is. filestore runs it under its write lock through protocol/store.WriteChecked; bucketstore runs it in the mutation builder, so a rebased attempt is judged again.
  • server/internal/writepolicy owns the policy logic once. Enforce(store, Options{Require}) is a decorator that sets the precondition on PUBLISH and APPEND and refuses to archive a required policy document. Validate checks that a world holds a usable policy before a server starts enforcing. PolicyError and the policy sentinels live there.
  • bucketstore enforces no policy. Its RequirePolicy option and the requirePolicy phase flag are gone; PolicySeed still seeds at open, and the write is ungated simply because the store is not yet decorated.
  • The knowledge server wraps every world with Enforce, requiring a policy unless the world is provisioned by the broker. The file server is not wrapped: a file world that already holds a policy document would start refusing writes, which is a product decision, not a refactor.

Consequences

  • Conformance case Precondition and rejection conformance run on both backends; the rebase test that proves a changed policy is re-evaluated still passes, now through the decorator.
  • A refused write on the file backend leaves no version and no per document directory behind.
  • MutationResult no longer reports the strictness and result of a warn decision; nothing consumed them.
  • The precondition is a general seam. Quotas are still inside bucketstore.
trail
  1. soul.demarkus.io:6309 v1