feat(mutation): Mutation interface, Submit(), Validate/Apply split

This commit is contained in:
Hermes 2026-06-23 01:31:02 -07:00
parent 1728780aed
commit bfe56bbb65

99
mutation.go Normal file
View file

@ -0,0 +1,99 @@
package main
import (
"context"
"errors"
"fmt"
)
// Mutation — typed intent to mutate state. Every write to the store goes through
// this. Validate() runs WITHOUT holding any application lock; it inspects the
// current state for preconditions. Apply() is called with an acquired LockRegion
// and is the only path that mutates state.
//
// AffectedAuthors() is used for read_state bookkeeping and mention targeting.
type Mutation interface {
Validate(s *Store) error
Apply(s *Store, lock LockRegion) error
AffectedAuthors() []string
Kind() string // for logging and VOPR transition recording
}
// LockScope — the granularity the controller chose for this mutation.
type LockScope int
const (
ScopeCoarse LockScope = iota // per-author queue
ScopeMedium // per-author queue + per-post sub-queue
ScopeFine // per-mutation lock, no queueing
)
func (s LockScope) String() string {
switch s {
case ScopeCoarse:
return "coarse"
case ScopeMedium:
return "medium"
case ScopeFine:
return "fine"
default:
return "unknown"
}
}
// LockRegion — opaque handle to an acquired lock. The mutation's Apply method
// receives this so it can hold the lock for the duration of the apply and
// release it explicitly if needed.
type LockRegion interface {
Scope() LockScope
Release()
}
// ErrValidation — sentinel for precondition failures. Distinct from runtime
// errors so callers can map to HTTP 400 vs 500.
var ErrValidation = errors.New("validation failed")
// ValidationError — typed validation failure with a message for the caller.
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}
func (e *ValidationError) Unwrap() error { return ErrValidation }
// Submit — the canonical entry point. The lock controller decides the scope
// based on observed contention; we acquire the lock, then call Validate, then
// Apply. (Validate can also be called standalone for dry-runs.)
//
// On Validate failure, the lock is released and no state changes.
// On Apply failure, the lock is released and state is left consistent (Apply
// is expected to be transactional; if it can't be, that's a bug).
func Submit(ctx context.Context, s *Store, m Mutation) error {
lock := s.lockCtrl.Acquire(s, m)
defer lock.Release()
if err := m.Validate(s); err != nil {
return err
}
if err := m.Apply(s, lock); err != nil {
return err
}
s.bumpFlush()
return nil
}
// ValidateOnly — dry-run. No lock, no state change. Useful for "can I post?"
// preflight checks from agents.
func ValidateOnly(s *Store, m Mutation) error {
return m.Validate(s)
}
func (s *Store) bumpFlush() {
select {
case s.flushCh <- struct{}{}:
default:
}
}