fluid-core/internal/contract/ids.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

93 lines
2.7 KiB
Go

package contract
import (
"fmt"
"strings"
)
// Identifier prefixes from FluidHypothesisRevisionSchema.md section 16.
//
// Implementations may use UUIDs internally; these human-readable prefixes exist
// so that operational tooling and audit trails stay legible to people.
const (
PrefixHypothesis = "H-"
PrefixRevision = "R-"
PrefixExperiment = "E-"
PrefixPressure = "P-"
PrefixBackendRequirement = "BR-"
PrefixDecision = "D-"
PrefixEvent = "EV-"
PrefixFeedback = "F-"
PrefixCohort = "C-"
)
// EntityKind names the FLUID artifact an identifier refers to.
type EntityKind string
const (
KindHypothesis EntityKind = "hypothesis"
KindRevision EntityKind = "revision"
KindExperiment EntityKind = "experiment"
KindPressure EntityKind = "pressure"
KindBackendRequirement EntityKind = "backend_requirement"
KindDecision EntityKind = "decision"
KindEvent EntityKind = "event"
KindFeedback EntityKind = "feedback"
KindCohort EntityKind = "cohort"
KindIntent EntityKind = "intent"
KindRoutingPolicy EntityKind = "routing_policy"
)
// prefixOrder matters: "BR-" must be tested before "B"-less single letters
// would otherwise mis-claim it, and "EV-" before "E-".
var prefixOrder = []struct {
prefix string
kind EntityKind
}{
{PrefixBackendRequirement, KindBackendRequirement},
{PrefixEvent, KindEvent},
{PrefixHypothesis, KindHypothesis},
{PrefixRevision, KindRevision},
{PrefixExperiment, KindExperiment},
{PrefixPressure, KindPressure},
{PrefixDecision, KindDecision},
{PrefixFeedback, KindFeedback},
{PrefixCohort, KindCohort},
}
// KindOf reports which FLUID artifact an identifier names.
//
// Audit trails carry bare identifiers across record boundaries, so being able to
// classify one without knowing where it came from keeps trace reconstruction
// from needing a lookup table at every hop.
func KindOf(id string) (EntityKind, bool) {
for _, p := range prefixOrder {
if strings.HasPrefix(id, p.prefix) {
return p.kind, true
}
}
return "", false
}
// ErrWrongKind reports an identifier used in the wrong position.
type ErrWrongKind struct {
ID string
Want EntityKind
Got EntityKind
}
func (e *ErrWrongKind) Error() string {
if e.Got == "" {
return fmt.Sprintf("identifier %q has no recognized FLUID prefix, wanted a %s id", e.ID, e.Want)
}
return fmt.Sprintf("identifier %q is a %s id, wanted a %s id", e.ID, e.Got, e.Want)
}
// RequireKind checks that id names the expected artifact.
func RequireKind(id string, want EntityKind) error {
got, ok := KindOf(id)
if !ok || got != want {
return &ErrWrongKind{ID: id, Want: want, Got: got}
}
return nil
}