fluid-core/internal/intent/store.go
tegwick d52dcc92a9
Some checks failed
ci / build (push) Has been cancelled
Add evidence store, intent store and the fluid CLI
Completes FLUID-WP-0003. The evidence store is append-only at the
database, not by convention in Go: a trigger blocks UPDATE and DELETE on
fluid_events in both SQLite and Postgres, so the guarantee binds a psql
session and the CLI equally, not just callers who go through the Go API.
Records are derived summary state and may be superseded; the event log
stays the authority.

The intent store makes a recorded version immutable and content
addressed, since reassigning what governed a revision after the fact
would break the one audit question Blueprint 27 exists to answer. The
unfilled InterfaceEvolutionIntent template is rejected rather than
defaulted, because a template still listing every FLUID-N mode has not
been completed and defaulting it would pick a permissive authority by
accident.

The CLI reads the evidence store directly rather than through the
control-plane API, so an operator can reconstruct history when the
control plane is down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1116572@bnt-lap001
Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
2026-09-04 02:26:23 +02:00

283 lines
9.5 KiB
Go

// Package intent implements the FLUID intent store.
//
// ArchitectureBlueprint.md section 27: the system must retain the exact
// interface evolution intent governing each revision, so that a later audit can
// answer whether a change was valid under the intent that existed when it was
// made. An intent document is therefore versioned, content-addressed and
// immutable once recorded.
package intent
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/tegwick/fluid-core/internal/contract"
"github.com/tegwick/fluid-core/internal/evidence"
)
// AuthorityMode is the operational authority granted to the Daimon.
//
// FluidAPIStandards.md section 26 is explicit that these describe authority,
// not maturity: a high-assurance system may deliberately remain at FLUID-2.
type AuthorityMode int
const (
ModeInstrumented AuthorityMode = iota // FLUID-0
ModeAnalytical // FLUID-1
ModeAdvisory // FLUID-2
ModeConstructive // FLUID-3
ModeExperimental // FLUID-4
ModeBoundedAutonomous // FLUID-5
ModeEvolutionary // FLUID-6
)
// String renders the canonical FLUID-N form.
func (m AuthorityMode) String() string { return fmt.Sprintf("FLUID-%d", int(m)) }
// Valid reports whether m is a defined mode.
func (m AuthorityMode) Valid() bool { return m >= ModeInstrumented && m <= ModeEvolutionary }
// Allows reports whether this mode permits at least the authority of want.
func (m AuthorityMode) Allows(want AuthorityMode) bool { return m >= want }
var modePattern = regexp.MustCompile(`FLUID-([0-6])\b`)
// Version is one recorded interface evolution intent.
type Version struct {
// Version is the human-facing label, such as "IEI-7".
Version string `json:"version"`
// Digest content-addresses the document. Two intent versions with the same
// digest are the same intent, whatever they were called.
Digest contract.Digest `json:"digest"`
// Document is the full text, retained verbatim. A summary would not settle
// an audit question about what the intent actually said.
Document string `json:"document"`
// Mode is the operational authority the document declares.
Mode AuthorityMode `json:"mode"`
// RecordedAt is when this version entered the store.
RecordedAt time.Time `json:"recorded_at"`
}
// Store holds intent versions and which one is currently active.
type Store struct {
ev evidence.Store
iface contract.InterfaceID
}
// New returns a store backed by ev.
func New(ev evidence.Store, iface contract.InterfaceID) *Store {
return &Store{ev: ev, iface: iface}
}
var (
// ErrNotFound reports an unknown intent version.
ErrNotFound = evidence.ErrNotFound
// ErrImmutable reports an attempt to change a recorded intent version.
ErrImmutable = errors.New("a recorded intent version cannot be changed")
// ErrNoActive reports that no intent has been made active yet.
ErrNoActive = errors.New("no active interface evolution intent")
)
// activeKey is the record id holding the active-version pointer.
const activeKey = "__active__"
// Digest computes the content address of an intent document.
func Digest(document string) contract.Digest {
sum := sha256.Sum256([]byte(document))
return contract.Digest("sha256:" + hex.EncodeToString(sum[:]))
}
// ParseMode extracts the declared authority mode from an intent document.
//
// The template writes the mode as a fenced list of alternatives before it is
// filled in; a document that still contains every mode has not been completed,
// and defaulting it to something permissive would be exactly the wrong failure.
func ParseMode(document string) (AuthorityMode, error) {
matches := modePattern.FindAllStringSubmatch(document, -1)
if len(matches) == 0 {
return 0, errors.New("intent document declares no FLUID-N authority mode")
}
seen := map[string]bool{}
for _, m := range matches {
seen[m[1]] = true
}
if len(seen) > 1 {
return 0, fmt.Errorf(
"intent document declares %d different authority modes; the template placeholder has not been resolved",
len(seen))
}
return AuthorityMode(matches[0][1][0] - '0'), nil
}
// Put records an intent version.
//
// Recording the same version twice with identical content is a no-op, which
// makes startup idempotent. Recording it with different content is refused:
// changing what a revision was governed by, after the fact, would break the
// audit question the store exists to answer.
func (s *Store) Put(ctx context.Context, version, document string) (Version, error) {
if version == "" {
return Version{}, errors.New("intent version label is required")
}
mode, err := ParseMode(document)
if err != nil {
return Version{}, fmt.Errorf("intent %s: %w", version, err)
}
v := Version{
Version: version,
Digest: Digest(document),
Document: document,
Mode: mode,
RecordedAt: time.Now().UTC(),
}
if existing, err := s.Get(ctx, version); err == nil {
if existing.Digest != v.Digest {
return Version{}, fmt.Errorf("%w: %s already recorded with digest %s",
ErrImmutable, version, existing.Digest)
}
return existing, nil
} else if !errors.Is(err, ErrNotFound) {
return Version{}, err
}
body, err := json.Marshal(v)
if err != nil {
return Version{}, err
}
if err := s.ev.PutRecord(ctx, contract.KindIntent, s.key(version), body); err != nil {
return Version{}, err
}
if err := s.ev.AppendEvent(ctx, contract.FluidEvent{
SchemaVersion: "0.1",
ID: contract.EventID("EV-intent-" + string(v.Digest[7:19])),
OccurredAt: v.RecordedAt,
EntityType: contract.FluidEventEntityTypeIntent,
EntityID: version,
EventType: "INTENT_RECORDED",
Actor: contract.Actor{Type: contract.ActorTypeSystem, ID: "fluid-intent-store"},
Reason: fmt.Sprintf("recorded %s at authority mode %s", version, mode),
}); err != nil {
return Version{}, err
}
return v, nil
}
// Get returns a recorded intent version.
func (s *Store) Get(ctx context.Context, version string) (Version, error) {
body, err := s.ev.Record(ctx, contract.KindIntent, s.key(version))
if err != nil {
return Version{}, err
}
var v Version
if err := json.Unmarshal(body, &v); err != nil {
return Version{}, fmt.Errorf("decode intent %s: %w", version, err)
}
return v, nil
}
// SetActive makes a recorded version the governing intent.
func (s *Store) SetActive(ctx context.Context, version string) error {
v, err := s.Get(ctx, version)
if err != nil {
return fmt.Errorf("cannot activate %s: %w", version, err)
}
body, _ := json.Marshal(map[string]string{"version": v.Version, "digest": string(v.Digest)})
if err := s.ev.PutRecord(ctx, contract.KindIntent, s.key(activeKey), body); err != nil {
return err
}
return s.ev.AppendEvent(ctx, contract.FluidEvent{
SchemaVersion: "0.1",
ID: contract.EventID("EV-intent-active-" + string(v.Digest[7:19])),
OccurredAt: time.Now().UTC(),
EntityType: contract.FluidEventEntityTypeIntent,
EntityID: version,
EventType: "INTENT_ACTIVATED",
Actor: contract.Actor{Type: contract.ActorTypeSystem, ID: "fluid-intent-store"},
Reason: fmt.Sprintf("%s is now the governing intent", version),
})
}
// Active returns the currently governing intent version.
func (s *Store) Active(ctx context.Context) (Version, error) {
body, err := s.ev.Record(ctx, contract.KindIntent, s.key(activeKey))
if errors.Is(err, ErrNotFound) {
return Version{}, ErrNoActive
}
if err != nil {
return Version{}, err
}
var ptr struct {
Version string `json:"version"`
}
if err := json.Unmarshal(body, &ptr); err != nil {
return Version{}, err
}
return s.Get(ctx, ptr.Version)
}
// Bind records that a revision is governed by an intent version.
//
// This is the link Blueprint section 27 requires. It is stored as an event
// rather than as a mutable field so that a revision's governing intent cannot
// be quietly reassigned later.
func (s *Store) Bind(ctx context.Context, rev contract.RevisionID, version string) error {
v, err := s.Get(ctx, version)
if err != nil {
return fmt.Errorf("cannot bind %s: %w", rev, err)
}
return s.ev.AppendEvent(ctx, contract.FluidEvent{
SchemaVersion: "0.1",
ID: contract.EventID("EV-bind-" + string(rev) + "-" + version),
OccurredAt: time.Now().UTC(),
EntityType: contract.FluidEventEntityTypeRevision,
EntityID: string(rev),
EventType: "INTENT_BOUND",
Actor: contract.Actor{Type: contract.ActorTypeSystem, ID: "fluid-intent-store"},
Inputs: []string{version},
Reason: fmt.Sprintf("%s is governed by %s (%s)", rev, version, v.Digest),
})
}
// GoverningIntent returns the intent version a revision was bound to.
func (s *Store) GoverningIntent(ctx context.Context, rev contract.RevisionID) (Version, error) {
events, err := s.ev.Events(ctx, evidence.EventFilter{
EntityType: contract.KindRevision,
EntityID: string(rev),
EventType: "INTENT_BOUND",
})
if err != nil {
return Version{}, err
}
if len(events) == 0 {
return Version{}, fmt.Errorf("%w: no intent bound to %s", ErrNotFound, rev)
}
// The first binding governs. A later one would be a reassignment, which the
// audit trail records but must not silently win.
first := events[0]
if len(first.Inputs) == 0 {
return Version{}, fmt.Errorf("binding event %s names no intent version", first.ID)
}
return s.Get(ctx, first.Inputs[0])
}
func (s *Store) key(id string) string {
return string(s.iface) + "/" + strings.TrimSpace(id)
}