Some checks failed
ci / build (push) Has been cancelled
Completes FLUID-WP-0006. The loop now runs end to end from the command line: two competing presentation hypotheses, an experiment that issues a routing policy rather than touching traffic, an amendment, a stop that returns traffic to the default, a confirmed outcome, a resolved competition, and a promotion the gate can refuse. Starting or stopping an experiment returns the routing policy for the operator to install rather than installing it. Blueprint 17 keeps the controller out of the traffic path, and installing from the handler would put it straight back in; emitting the document keeps the separation visible instead of implied. `fluid audit trace` now answers the section 25 questions from events rather than summary records, and names the rivals a hypothesis beat: an audit asking which hypotheses were considered is not answered by naming only the winner. Two fixes found by driving the CLI rather than only the tests. Go's flag package stops at the first positional, so ids given after flags silently swallowed them; ids are now taken before parsing. And there was no way to attach a revision to the hypothesis that produced it, which left `audit trace` unable to say why a revision existed -- `hypothesis attach` closes that. 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
164 lines
4.1 KiB
Go
164 lines
4.1 KiB
Go
// Command fluid is the FLUID operator CLI.
|
|
//
|
|
// It reads and writes the evidence store directly rather than going through the
|
|
// control-plane API, so that an operator can still inspect an interface's
|
|
// history when the control plane is down. Auditability must not depend on the
|
|
// availability of the thing being audited.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const usage = `fluid — FLUID operator CLI
|
|
|
|
Usage:
|
|
fluid [--store PATH] [--interface ID] <command> [arguments]
|
|
|
|
Commands:
|
|
intent put Record an interface evolution intent version
|
|
intent activate Make a recorded version the governing intent
|
|
intent show Show the active or a named intent version
|
|
revision publish Publish a signed revision descriptor
|
|
revision list List published revisions
|
|
revision show Show one revision descriptor
|
|
policy put Install a routing policy
|
|
policy show Show the current routing policy
|
|
pressure list List recorded interface pressure
|
|
pressure analyze Classify recent telemetry into pressure records
|
|
pressure dismiss Record that a pressure will not be acted on
|
|
cohort Summarize cohort populations
|
|
fitness compare Compare a candidate revision against its control
|
|
hypothesis Create, advance and compete hypotheses
|
|
experiment Design, start, stop and finalize experiments
|
|
promote Record a promotion decision
|
|
events Show audit events
|
|
telemetry Show recorded telemetry
|
|
audit trace Reconstruct the history behind a revision
|
|
|
|
Global flags:
|
|
--store PATH Evidence store path (default $FLUID_STORE or ./fluid.db)
|
|
--interface ID Interface identifier (default $FLUID_INTERFACE)
|
|
`
|
|
|
|
func main() {
|
|
if err := run(os.Args[1:]); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
os.Exit(0)
|
|
}
|
|
fmt.Fprintln(os.Stderr, "fluid:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type globals struct {
|
|
store string
|
|
iface string
|
|
remaining []string
|
|
}
|
|
|
|
func parseGlobals(args []string) (globals, error) {
|
|
g := globals{
|
|
store: envOr("FLUID_STORE", "fluid.db"),
|
|
iface: os.Getenv("FLUID_INTERFACE"),
|
|
}
|
|
|
|
i := 0
|
|
for i < len(args) {
|
|
switch args[i] {
|
|
case "--store":
|
|
if i+1 >= len(args) {
|
|
return g, errors.New("--store needs a value")
|
|
}
|
|
g.store, i = args[i+1], i+2
|
|
case "--interface":
|
|
if i+1 >= len(args) {
|
|
return g, errors.New("--interface needs a value")
|
|
}
|
|
g.iface, i = args[i+1], i+2
|
|
case "-h", "--help", "help":
|
|
fmt.Print(usage)
|
|
return g, flag.ErrHelp
|
|
default:
|
|
if strings.HasPrefix(args[i], "--") {
|
|
return g, fmt.Errorf("unknown global flag %q", args[i])
|
|
}
|
|
g.remaining = args[i:]
|
|
return g, nil
|
|
}
|
|
}
|
|
g.remaining = nil
|
|
return g, nil
|
|
}
|
|
|
|
func run(args []string) error {
|
|
g, err := parseGlobals(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(g.remaining) == 0 {
|
|
fmt.Print(usage)
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmd, rest := g.remaining[0], g.remaining[1:]
|
|
|
|
switch cmd {
|
|
case "intent":
|
|
return runIntent(ctx, g, rest)
|
|
case "revision":
|
|
return runRevision(ctx, g, rest)
|
|
case "policy":
|
|
return runPolicy(ctx, g, rest)
|
|
case "events":
|
|
return runEvents(ctx, g, rest)
|
|
case "telemetry":
|
|
return runTelemetry(ctx, g, rest)
|
|
case "pressure":
|
|
return runPressure(ctx, g, rest)
|
|
case "cohort":
|
|
return runCohort(ctx, g, rest)
|
|
case "fitness":
|
|
return runFitness(ctx, g, rest)
|
|
case "hypothesis":
|
|
return runHypothesis(ctx, g, rest)
|
|
case "experiment":
|
|
return runExperiment(ctx, g, rest)
|
|
case "promote":
|
|
return runPromote(ctx, g, rest)
|
|
case "audit":
|
|
return runAudit(ctx, g, rest)
|
|
default:
|
|
return fmt.Errorf("unknown command %q (try `fluid help`)", cmd)
|
|
}
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// requireInterface fails clearly rather than silently operating on the wrong
|
|
// interface's history.
|
|
func (g globals) requireInterface() (string, error) {
|
|
if g.iface == "" {
|
|
return "", errors.New("no interface selected: pass --interface or set FLUID_INTERFACE")
|
|
}
|
|
return g.iface, nil
|
|
}
|
|
|
|
func (g globals) storePath() string {
|
|
if abs, err := filepath.Abs(g.store); err == nil {
|
|
return abs
|
|
}
|
|
return g.store
|
|
}
|