diff --git a/README.md b/README.md
index f87ac76e..5a030139 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Boatstack is a programmable supervisory control runtime for software delivery,
with a first-party standard delivery flow. It compiles one CoreSystem, one
-explicit primary flow, and optional conservative extensions into an immutable
+explicit program runtime, and optional conservative extensions into an immutable
ControlProgram before it observes a repository, resolves one legal transition,
binds exact authority, executes owned effects, verifies the result, and records
a receipt.
@@ -33,6 +33,9 @@ explicit invocation
The immediate value is simple: every host consumes one executable delivery law.
The [technical specification](docs/architecture/boatstack-v2-kernel.md) records
the complete contract and the historical failure synthesis.
+The [Control Program ABI](docs/architecture/control-program-abi.md) defines the
+strict repository source, canonical fingerprint, compatibility gate, and
+program-qualified transition identity used by complete user-facing Flows.
## Install
@@ -90,14 +93,14 @@ The generated [transition catalog](docs/architecture/boatstack-v2-transition-cat
and [Mermaid inventory](docs/architecture/boatstack-v2-transition-catalog.mmd)
come directly from the runtime registry. The generated
[StandardFlow graph](docs/architecture/boatstack-standard-flow.mmd) filters the
-same compiled registry by primary-flow origin; it is not a second graph.
+same compiled registry by control-program origin; it is not a second graph.
The [replacement closure report](docs/architecture/boatstack-v2-closure-report.md)
records the deleted V1 authority and its V2 evidence.
The Go SDK keeps the standard distribution ergonomic with `sdk.New(...)`.
-Custom applications use `sdk.NewKernel(..., sdk.WithFlow(flow),
+Custom applications use `sdk.NewKernel(..., sdk.WithProgramRuntime(runtime),
sdk.WithExtension(extension))`; the lower-level constructor requires an
-explicit trusted in-process primary flow and never inserts StandardFlow.
+explicit trusted in-process program runtime and never inserts StandardFlow.
## Coding-agent skills
diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go
index 9abfc235..80f52fc3 100644
--- a/boatstack/cmd/boatstack-helper/main.go
+++ b/boatstack/cmd/boatstack-helper/main.go
@@ -555,10 +555,10 @@ func renderResponse(response surfaces.Response, format string) error {
return nil
}
if response.Doctor != nil {
- fmt.Printf("healthy=%t kernel=%s core=%s@%s flow=%s@%s core_transitions=%d flow_transitions=%d extension_transitions=%d transitions=%d program=%s drift=%t runtime_healthy=%t update_ready=%t recovery_required=%t snapshot=%s\n%s\n",
+ fmt.Printf("healthy=%t kernel=%s core=%s@%s program=%s@%s core_transitions=%d runtime_transitions=%d extension_transitions=%d transitions=%d fingerprint=%s drift=%t runtime_healthy=%t update_ready=%t recovery_required=%t snapshot=%s\n%s\n",
response.Doctor.Healthy, response.Doctor.KernelVersion, response.Doctor.CoreSystemID, response.Doctor.CoreSystemVersion,
- response.Doctor.PrimaryFlowID, response.Doctor.PrimaryFlowVersion, response.Doctor.CoreTransitionCount,
- response.Doctor.FlowTransitionCount, response.Doctor.ExtensionTransitionCount, response.Doctor.TransitionCount,
+ response.Doctor.ProgramID, response.Doctor.ProgramVersion, response.Doctor.CoreTransitionCount,
+ response.Doctor.RuntimeTransitionCount, response.Doctor.ExtensionTransitionCount, response.Doctor.TransitionCount,
response.Doctor.ProgramFingerprint, response.Doctor.UnresolvedProgramDrift, response.Doctor.RuntimeHealthy, response.Doctor.UpdateReady, response.Doctor.RecoveryRequired, response.Doctor.Snapshot, response.Doctor.Detail)
return nil
}
diff --git a/boatstack/control/control.go b/boatstack/control/control.go
index 65d3e663..58c90c8f 100644
--- a/boatstack/control/control.go
+++ b/boatstack/control/control.go
@@ -30,6 +30,7 @@ type EffectID = catalog.EffectID
type Prescription = catalog.Prescription
type ParameterSpec = catalog.ParameterSpec
type InterruptionContract = catalog.InterruptionContract
+type PolicyContract = catalog.PolicyContract
type Reversibility = catalog.Reversibility
type GoalKind = model.GoalKind
type ProtocolPhase = model.ProtocolPhase
@@ -50,10 +51,10 @@ const (
AuthorityProvider = catalog.AuthorityProvider
SelectionSystemRecovery = catalog.SelectionSystemRecovery
- SelectionFlowRecovery = catalog.SelectionFlowRecovery
+ SelectionProgramRecovery = catalog.SelectionProgramRecovery
SelectionExtensionRecovery = catalog.SelectionExtensionRecovery
SelectionGoalRequired = catalog.SelectionGoalRequired
- SelectionFlowProgress = catalog.SelectionFlowProgress
+ SelectionProgramProgress = catalog.SelectionProgramProgress
SelectionExplicitOnly = catalog.SelectionExplicitOnly
SelectionObservedExternal = catalog.SelectionObservedExternal
@@ -121,9 +122,9 @@ type CoreSystemDefinition interface {
CoreManifest(context.Context) (CoreSystemManifest, error)
}
-// FlowDefinition supplies exactly one trusted in-process primary delivery law.
-type FlowDefinition interface {
- FlowManifest(context.Context) (PrimaryFlowManifest, error)
+// ProgramRuntimeDefinition supplies one trusted in-process execution binding.
+type ProgramRuntimeDefinition interface {
+ RuntimeManifest(context.Context) (ProgramRuntimeManifest, error)
}
// Extension supplies an additive manifest. Runtime behavior is optional for a
@@ -138,23 +139,23 @@ type CoreSystemManifest struct {
Transitions []Transition `json:"transitions"`
}
-type PrimaryFlowManifest struct {
- ID string `json:"id"`
- Version string `json:"version"`
- ProtocolVersion int `json:"protocol_version"`
- RuntimeMode FlowRuntimeMode `json:"runtime_mode"`
- SupportedGoals []GoalKind `json:"supported_goals"`
- GoalContracts []GoalContract `json:"goal_contracts"`
- Transitions []Transition `json:"transitions"`
- Facts []string `json:"facts,omitempty"`
- OwnedResources []string `json:"owned_resources"`
- Effects []string `json:"effects"`
- Verifiers []string `json:"verifiers"`
- RecoveryTransitions []TransitionID `json:"recovery_transitions"`
- Settings json.RawMessage `json:"settings,omitempty"`
- ConfigurationSchema json.RawMessage `json:"configuration_schema,omitempty"`
- PrivacyClassification string `json:"privacy_classification"`
- TelemetryClassification string `json:"telemetry_classification"`
+type ProgramRuntimeManifest struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ ProtocolVersion int `json:"protocol_version"`
+ RuntimeMode ProgramRuntimeMode `json:"runtime_mode"`
+ SupportedGoals []GoalKind `json:"supported_goals"`
+ GoalContracts []GoalContract `json:"goal_contracts"`
+ Transitions []Transition `json:"transitions"`
+ Facts []string `json:"facts,omitempty"`
+ OwnedResources []string `json:"owned_resources"`
+ Effects []string `json:"effects"`
+ Verifiers []string `json:"verifiers"`
+ RecoveryTransitions []TransitionID `json:"recovery_transitions"`
+ Settings json.RawMessage `json:"settings,omitempty"`
+ ConfigurationSchema json.RawMessage `json:"configuration_schema,omitempty"`
+ PrivacyClassification string `json:"privacy_classification"`
+ TelemetryClassification string `json:"telemetry_classification"`
}
type GoalConstraint struct {
@@ -190,11 +191,14 @@ type ComponentIdentity struct {
type ProgramSummary struct {
SchemaVersion int `json:"schema_version"`
KernelVersion string `json:"kernel_version"`
+ ProgramID string `json:"program_id"`
+ ProgramVersion string `json:"program_version"`
+ RequiresRuntime string `json:"requires_runtime,omitempty"`
Core ComponentIdentity `json:"core"`
- Flow ComponentIdentity `json:"flow"`
+ Runtime ComponentIdentity `json:"program_runtime"`
Extensions []ComponentIdentity `json:"extensions,omitempty"`
CoreTransitionCount int `json:"core_transition_count"`
- FlowTransitionCount int `json:"flow_transition_count"`
+ RuntimeTransitionCount int `json:"runtime_transition_count"`
ExtensionTransitionCount int `json:"extension_transition_count"`
TotalTransitionCount int `json:"total_transition_count"`
ProgramFingerprint string `json:"program_fingerprint"`
@@ -209,19 +213,19 @@ type ControlProgram struct {
resourceOwnership map[string]string
settingsFingerprint string
extensions []compiledExtension
- flow compiledFlow
+ programRuntime compiledProgramRuntime
}
-type compiledFlow struct {
- manifest PrimaryFlowManifest
+type compiledProgramRuntime struct {
+ manifest ProgramRuntimeManifest
identity ComponentIdentity
- runtime FlowRuntime
+ runtime ProgramRuntime
}
-type CompiledFlow struct {
- Manifest PrimaryFlowManifest
+type CompiledProgramRuntime struct {
+ Manifest ProgramRuntimeManifest
Identity ComponentIdentity
- Runtime FlowRuntime
+ Runtime ProgramRuntime
}
type compiledExtension struct {
@@ -271,8 +275,8 @@ func (p ControlProgram) ExtensionByID(id string) (CompiledExtension, bool) {
return CompiledExtension{}, false
}
-func (p ControlProgram) Flow() CompiledFlow {
- return CompiledFlow{Manifest: cloneFlowManifest(p.flow.manifest), Identity: p.flow.identity, Runtime: p.flow.runtime}
+func (p ControlProgram) ProgramRuntime() CompiledProgramRuntime {
+ return CompiledProgramRuntime{Manifest: cloneRuntimeManifest(p.programRuntime.manifest), Identity: p.programRuntime.identity, Runtime: p.programRuntime.runtime}
}
// RuntimeRegistry and RuntimeGoalContracts are for the Boatstack mechanism.
@@ -283,7 +287,7 @@ func (p ControlProgram) RuntimeGoalContracts() catalog.GoalContracts { return p.
type CompileRequest struct {
KernelVersion string
Core CoreSystemDefinition
- Flow FlowDefinition
+ Runtime ProgramRuntimeDefinition
Extensions []Extension
Settings any
}
@@ -291,23 +295,23 @@ type CompileRequest struct {
var componentID = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$`)
func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error) {
- if request.KernelVersion == "" || request.Core == nil || request.Flow == nil {
- return ControlProgram{}, fmt.Errorf("control program requires kernel version, CoreSystem, and exactly one PrimaryFlow")
+ if request.KernelVersion == "" || request.Core == nil || request.Runtime == nil {
+ return ControlProgram{}, fmt.Errorf("control program requires kernel version, CoreSystem, and exactly one ProgramRuntime")
}
core, err := request.Core.CoreManifest(ctx)
if err != nil {
return ControlProgram{}, fmt.Errorf("load CoreSystem manifest: %w", err)
}
core = cloneCoreManifest(core)
- flow, err := request.Flow.FlowManifest(ctx)
+ flow, err := request.Runtime.RuntimeManifest(ctx)
if err != nil {
- return ControlProgram{}, fmt.Errorf("load PrimaryFlow manifest: %w", err)
+ return ControlProgram{}, fmt.Errorf("load ProgramRuntime manifest: %w", err)
}
- flow = cloneFlowManifest(flow)
+ flow = cloneRuntimeManifest(flow)
if err := validateCore(core); err != nil {
return ControlProgram{}, err
}
- if err := validateFlow(flow); err != nil {
+ if err := validateProgramRuntime(flow); err != nil {
return ControlProgram{}, err
}
coreFingerprint, err := fingerprint(core)
@@ -322,12 +326,12 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error
if err != nil {
return ControlProgram{}, fmt.Errorf("fingerprint program settings: %w", err)
}
- var flowRuntime FlowRuntime
- if runtimeDefinition, ok := request.Flow.(RuntimeFlowDefinition); ok {
- flowRuntime = runtimeDefinition.FlowRuntime()
+ var flowRuntime ProgramRuntime
+ if runtimeDefinition, ok := request.Runtime.(RuntimeProgramDefinition); ok {
+ flowRuntime = runtimeDefinition.ProgramRuntime()
}
- if flow.RuntimeMode == FlowRuntimeProtocol && flowRuntime == nil {
- return ControlProgram{}, fmt.Errorf("PrimaryFlow %q selects protocol runtime without a FlowRuntime", flow.ID)
+ if flow.RuntimeMode == ProgramRuntimeProtocol && flowRuntime == nil {
+ return ControlProgram{}, fmt.Errorf("ProgramRuntime %q selects protocol runtime without a ProgramRuntime", flow.ID)
}
transitions := make([]Transition, 0, len(core.Transitions)+len(flow.Transitions))
@@ -353,7 +357,7 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error
if err := appendComponent(core.Transitions, catalog.TransitionOrigin{Kind: catalog.OriginCoreSystem, ID: core.ID, Version: core.Version, ManifestFingerprint: coreFingerprint}); err != nil {
return ControlProgram{}, err
}
- if err := appendComponent(flow.Transitions, catalog.TransitionOrigin{Kind: catalog.OriginPrimaryFlow, ID: flow.ID, Version: flow.Version, ManifestFingerprint: flowFingerprint}); err != nil {
+ if err := appendComponent(flow.Transitions, catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: flow.ID, Version: flow.Version, ManifestFingerprint: flowFingerprint}); err != nil {
return ControlProgram{}, err
}
@@ -485,7 +489,7 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error
SchemaVersion int
KernelVersion string
Core ComponentIdentity
- Flow ComponentIdentity
+ Runtime ComponentIdentity
Extensions []ComponentIdentity
SettingsFingerprint string
Transitions []Transition
@@ -503,14 +507,15 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error
}
summary := ProgramSummary{
SchemaVersion: ProgramSchemaVersion, KernelVersion: request.KernelVersion,
- Core: programIdentity.Core, Flow: programIdentity.Flow, Extensions: extensionIdentities,
- CoreTransitionCount: len(core.Transitions), FlowTransitionCount: len(flow.Transitions),
+ ProgramID: flow.ID, ProgramVersion: flow.Version,
+ Core: programIdentity.Core, Runtime: programIdentity.Runtime, Extensions: extensionIdentities,
+ CoreTransitionCount: len(core.Transitions), RuntimeTransitionCount: len(flow.Transitions),
ExtensionTransitionCount: extensionCount, TotalTransitionCount: registry.Len(), ProgramFingerprint: programFingerprint,
}
return ControlProgram{
summary: summary, registry: registry, goalContracts: contracts, resourceOwnership: resources,
settingsFingerprint: settingsFingerprint, extensions: compiledExtensions,
- flow: compiledFlow{manifest: cloneFlowManifest(flow), identity: programIdentity.Flow, runtime: flowRuntime},
+ programRuntime: compiledProgramRuntime{manifest: cloneRuntimeManifest(flow), identity: programIdentity.Runtime, runtime: flowRuntime},
}, nil
}
@@ -521,42 +526,42 @@ func validateCore(manifest CoreSystemManifest) error {
return nil
}
-func validateFlow(manifest PrimaryFlowManifest) error {
- if !componentID.MatchString(manifest.ID) || manifest.Version == "" || manifest.ProtocolVersion != FlowProtocolVersion ||
- (manifest.RuntimeMode != FlowRuntimeNative && manifest.RuntimeMode != FlowRuntimeProtocol) ||
+func validateProgramRuntime(manifest ProgramRuntimeManifest) error {
+ if !componentID.MatchString(manifest.ID) || manifest.Version == "" || manifest.ProtocolVersion != ProgramRuntimeProtocolVersion ||
+ (manifest.RuntimeMode != ProgramRuntimeNative && manifest.RuntimeMode != ProgramRuntimeProtocol) ||
len(manifest.Transitions) == 0 || len(manifest.SupportedGoals) == 0 ||
!validJSONObject(manifest.ConfigurationSchema) ||
manifest.PrivacyClassification == "" || manifest.TelemetryClassification == "" {
- return fmt.Errorf("PrimaryFlow requires semantic id, version, configuration schema, goals, and transitions")
+ return fmt.Errorf("ProgramRuntime requires semantic id, version, configuration schema, goals, and transitions")
}
- if err := validateDeclaredSchema(manifest.ConfigurationSchema, manifest.Settings, "PrimaryFlow "+manifest.ID+" configuration"); err != nil {
+ if err := validateDeclaredSchema(manifest.ConfigurationSchema, manifest.Settings, "ProgramRuntime "+manifest.ID+" configuration"); err != nil {
return err
}
supported := map[GoalKind]bool{}
for _, goal := range manifest.SupportedGoals {
if !goal.Valid() || supported[goal] {
- return fmt.Errorf("PrimaryFlow has invalid or duplicate goal %q", goal)
+ return fmt.Errorf("ProgramRuntime has invalid or duplicate goal %q", goal)
}
supported[goal] = true
}
for _, contract := range manifest.GoalContracts {
if !supported[contract.GoalKind] {
- return fmt.Errorf("PrimaryFlow goal contract %q is not supported", contract.GoalKind)
+ return fmt.Errorf("ProgramRuntime goal contract %q is not supported", contract.GoalKind)
}
delete(supported, contract.GoalKind)
}
if len(supported) != 0 {
- return fmt.Errorf("PrimaryFlow does not define every supported goal contract")
+ return fmt.Errorf("ProgramRuntime does not define every supported goal contract")
}
for _, values := range [][]string{manifest.Facts, manifest.OwnedResources, manifest.Effects, manifest.Verifiers} {
if duplicate := duplicateString(values); duplicate != "" {
- return fmt.Errorf("PrimaryFlow %q duplicates declaration %q", manifest.ID, duplicate)
+ return fmt.Errorf("ProgramRuntime %q duplicates declaration %q", manifest.ID, duplicate)
}
}
- if manifest.RuntimeMode == FlowRuntimeProtocol {
+ if manifest.RuntimeMode == ProgramRuntimeProtocol {
for _, value := range append(append(append([]string(nil), manifest.Facts...), manifest.OwnedResources...), append(manifest.Effects, manifest.Verifiers...)...) {
if !strings.HasPrefix(value, manifest.ID+".") {
- return fmt.Errorf("protocol PrimaryFlow %q declaration %q is not namespaced", manifest.ID, value)
+ return fmt.Errorf("protocol ProgramRuntime %q declaration %q is not namespaced", manifest.ID, value)
}
}
}
@@ -566,33 +571,33 @@ func validateFlow(manifest PrimaryFlowManifest) error {
verifiers := stringSet(manifest.Verifiers)
recoveryDeclarations := transitionSet(manifest.RecoveryTransitions)
if len(recoveryDeclarations) != len(manifest.RecoveryTransitions) {
- return fmt.Errorf("PrimaryFlow %q duplicates a recovery declaration", manifest.ID)
+ return fmt.Errorf("ProgramRuntime %q duplicates a recovery declaration", manifest.ID)
}
transitions := make(map[TransitionID]Transition, len(manifest.Transitions))
for _, transition := range manifest.Transitions {
transitions[transition.ID] = transition
- if manifest.RuntimeMode == FlowRuntimeProtocol && !strings.HasPrefix(string(transition.ID), manifest.ID+".") {
- return fmt.Errorf("protocol PrimaryFlow %q transition %q is not namespaced", manifest.ID, transition.ID)
+ if manifest.RuntimeMode == ProgramRuntimeProtocol && !strings.HasPrefix(string(transition.ID), manifest.ID+".") {
+ return fmt.Errorf("protocol ProgramRuntime %q transition %q is not namespaced", manifest.ID, transition.ID)
}
if transition.Controllable() {
if !effects[string(transition.Effect)] || !verifiers[transition.Verifier] {
- return fmt.Errorf("PrimaryFlow transition %q uses an undeclared effect or verifier", transition.ID)
+ return fmt.Errorf("ProgramRuntime transition %q uses an undeclared effect or verifier", transition.ID)
}
for _, resource := range transition.OwnedResources {
if !resources[resource] {
- return fmt.Errorf("PrimaryFlow transition %q writes undeclared resource %q", transition.ID, resource)
+ return fmt.Errorf("ProgramRuntime transition %q writes undeclared resource %q", transition.ID, resource)
}
- if manifest.RuntimeMode == FlowRuntimeProtocol && !strings.HasPrefix(resource, manifest.ID+".") {
- return fmt.Errorf("protocol PrimaryFlow %q resource %q is not namespaced", manifest.ID, resource)
+ if manifest.RuntimeMode == ProgramRuntimeProtocol && !strings.HasPrefix(resource, manifest.ID+".") {
+ return fmt.Errorf("protocol ProgramRuntime %q resource %q is not namespaced", manifest.ID, resource)
}
}
- if manifest.RuntimeMode == FlowRuntimeProtocol {
+ if manifest.RuntimeMode == ProgramRuntimeProtocol {
for _, condition := range transition.TargetConditions {
if !strings.HasPrefix(string(condition.Facet), manifest.ID+".") {
- return fmt.Errorf("protocol PrimaryFlow transition %q targets non-owned fact %q", transition.ID, condition.Facet)
+ return fmt.Errorf("protocol ProgramRuntime transition %q targets non-owned fact %q", transition.ID, condition.Facet)
}
if !facts[string(condition.Facet)] {
- return fmt.Errorf("protocol PrimaryFlow transition %q targets undeclared fact %q", transition.ID, condition.Facet)
+ return fmt.Errorf("protocol ProgramRuntime transition %q targets undeclared fact %q", transition.ID, condition.Facet)
}
}
}
@@ -601,7 +606,7 @@ func validateFlow(manifest PrimaryFlowManifest) error {
for recovery := range recoveryDeclarations {
transition, ok := transitions[recovery]
if !ok || transition.Class != EventRecovery {
- return fmt.Errorf("PrimaryFlow %q recovery %q is not a declared recovery transition", manifest.ID, recovery)
+ return fmt.Errorf("ProgramRuntime %q recovery %q is not a declared recovery transition", manifest.ID, recovery)
}
}
return nil
@@ -940,7 +945,7 @@ func cloneCoreManifest(value CoreSystemManifest) CoreSystemManifest {
return value
}
-func cloneFlowManifest(value PrimaryFlowManifest) PrimaryFlowManifest {
+func cloneRuntimeManifest(value ProgramRuntimeManifest) ProgramRuntimeManifest {
value.SupportedGoals = append([]GoalKind(nil), value.SupportedGoals...)
value.GoalContracts = append([]GoalContract(nil), value.GoalContracts...)
for index := range value.GoalContracts {
diff --git a/boatstack/control/control_test.go b/boatstack/control/control_test.go
index 414bbb94..b33070e1 100644
--- a/boatstack/control/control_test.go
+++ b/boatstack/control/control_test.go
@@ -14,9 +14,11 @@ import (
"github.com/operatorstack/boatstack/boatstack/flow/standard"
)
-type staticFlowDefinition struct{ manifest control.PrimaryFlowManifest }
+type staticProgramRuntimeDefinition struct {
+ manifest control.ProgramRuntimeManifest
+}
-func (f staticFlowDefinition) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+func (f staticProgramRuntimeDefinition) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) {
return f.manifest, nil
}
@@ -34,7 +36,7 @@ func TestStandardProgramHasExplicitStableComposition(t *testing.T) {
t.Fatalf("identical compilation drifted: %s != %s", one.Fingerprint(), two.Fingerprint())
}
summary := one.Summary()
- if summary.CoreTransitionCount != 33 || summary.FlowTransitionCount != 30 || summary.ExtensionTransitionCount != 0 || summary.TotalTransitionCount != 63 {
+ if summary.CoreTransitionCount != 33 || summary.RuntimeTransitionCount != 30 || summary.ExtensionTransitionCount != 0 || summary.TotalTransitionCount != 63 {
t.Fatalf("compiled counts = %+v", summary)
}
counts := map[string]int{}
@@ -44,7 +46,7 @@ func TestStandardProgramHasExplicitStableComposition(t *testing.T) {
t.Fatalf("transition lost compiled ownership: %+v", transition)
}
}
- if counts["core-system"] != 33 || counts["primary-flow"] != 30 || counts["extension"] != 0 {
+ if counts["core-system"] != 33 || counts["control-program"] != 30 || counts["extension"] != 0 {
t.Fatalf("origin counts = %#v", counts)
}
}
@@ -116,11 +118,11 @@ func TestProgramFingerprintBindsCompositionAndPolicyInputs(t *testing.T) {
})
}
- policyOne, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Flow: standard.Definition(), Settings: map[string]any{"policy": "one"}})
+ policyOne, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: standard.Definition(), Settings: map[string]any{"policy": "one"}})
if err != nil {
t.Fatal(err)
}
- policyTwo, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Flow: standard.Definition(), Settings: map[string]any{"policy": "two"}})
+ policyTwo, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: standard.Definition(), Settings: map[string]any{"policy": "two"}})
if err != nil {
t.Fatal(err)
}
@@ -158,43 +160,43 @@ func TestCompileEnforcesDeclaredComponentSchemas(t *testing.T) {
}
})
}
- flow, err := standard.Definition().FlowManifest(context.Background())
+ flow, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
t.Fatal(err)
}
flow.ConfigurationSchema = json.RawMessage(`{"type":"object","required":["mode"],"additionalProperties":false}`)
flow.Settings = json.RawMessage(`{}`)
- if _, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Flow: staticFlowDefinition{manifest: flow}}); err == nil {
- t.Fatal("PrimaryFlow settings that violate ConfigurationSchema compiled")
+ if _, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Runtime: staticProgramRuntimeDefinition{manifest: flow}}); err == nil {
+ t.Fatal("ProgramRuntime settings that violate ConfigurationSchema compiled")
}
}
func TestComponentsMustDeclareTheirOwnSelectionSemantics(t *testing.T) {
// control-law: generic-compiler-never-infers-flow-order-from-transition-ids
- flow, err := standard.Definition().FlowManifest(context.Background())
+ flow, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
t.Fatal(err)
}
flow.Transitions[0].SelectionClass = ""
if _, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: "kernel", Core: core.System(), Flow: staticFlow{manifest: flow},
+ KernelVersion: "kernel", Core: core.System(), Runtime: staticFlow{manifest: flow},
}); err == nil || !strings.Contains(err.Error(), "selection class") {
t.Fatalf("flow without an explicit selection class was accepted: %v", err)
}
- flow, err = standard.Definition().FlowManifest(context.Background())
+ flow, err = standard.Definition().RuntimeManifest(context.Background())
if err != nil {
t.Fatal(err)
}
flow.Transitions[0].SelectionClass = control.SelectionSystemRecovery
if _, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: "kernel", Core: core.System(), Flow: staticFlow{manifest: flow},
+ KernelVersion: "kernel", Core: core.System(), Runtime: staticFlow{manifest: flow},
}); err == nil || !strings.Contains(err.Error(), "SYSTEM_RECOVERY") {
t.Fatalf("flow claimed CoreSystem recovery precedence: %v", err)
}
}
-func TestPrimaryFlowCannotClaimCoreSystemResources(t *testing.T) {
+func TestProgramRuntimeCannotClaimCoreSystemResources(t *testing.T) {
// control-law: every-resource-has-exactly-one-component-owner
coreManifest, err := core.System().CoreManifest(context.Background())
if err != nil {
@@ -210,16 +212,16 @@ func TestPrimaryFlowCannotClaimCoreSystemResources(t *testing.T) {
if coreResource == "" {
t.Fatal("CoreSystem fixture declares no owned resource")
}
- flow, err := standard.Definition().FlowManifest(context.Background())
+ flow, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
t.Fatal(err)
}
flow.OwnedResources = append(flow.OwnedResources, coreResource)
flow.Transitions[0].OwnedResources = append(flow.Transitions[0].OwnedResources, coreResource)
if _, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: "kernel", Core: core.System(), Flow: staticFlow{manifest: flow},
+ KernelVersion: "kernel", Core: core.System(), Runtime: staticFlow{manifest: flow},
}); err == nil || !strings.Contains(err.Error(), "overlapping owners") {
- t.Fatalf("PrimaryFlow claimed CoreSystem resource %q: %v", coreResource, err)
+ t.Fatalf("ProgramRuntime claimed CoreSystem resource %q: %v", coreResource, err)
}
}
@@ -298,17 +300,17 @@ func TestControlProgramAccessorsCannotMutateCompiledBytes(t *testing.T) {
extensions := program.Extensions()
extensions[0].Manifest.Facts[0] = "mutated.fact.id"
extensions[0].Manifest.Settings = json.RawMessage(`{"mutated":true}`)
- flow := program.Flow()
+ flow := program.ProgramRuntime()
flow.Manifest.GoalContracts[0].Conditions[0].Values = []string{"mutated"}
if program.Fingerprint() != originalFingerprint || program.Transitions()[0].SourcePhases[0] == control.PhaseAbandoned ||
- program.Extensions()[0].Manifest.Facts[0] == "mutated.fact.id" || program.Flow().Manifest.GoalContracts[0].Conditions[0].Values[0] == "mutated" {
+ program.Extensions()[0].Manifest.Facts[0] == "mutated.fact.id" || program.ProgramRuntime().Manifest.GoalContracts[0].Conditions[0].Values[0] == "mutated" {
t.Fatal("public accessor mutated the compiled ControlProgram")
}
}
func TestExtensionGoalConditionsAreConjunctive(t *testing.T) {
- // control-law: extension-terminal-set-is-a-subset-of-primary-flow-terminal-set
+ // control-law: extension-terminal-set-is-a-subset-of-control-program-terminal-set
base, err := distribution.StandardProgram(context.Background())
if err != nil {
t.Fatal(err)
@@ -369,9 +371,11 @@ type declarationOnlyExtension struct {
goalConditions []control.FacetCondition
}
-type staticFlow struct{ manifest control.PrimaryFlowManifest }
+type staticFlow struct {
+ manifest control.ProgramRuntimeManifest
+}
-func (s staticFlow) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+func (s staticFlow) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) {
return s.manifest, nil
}
diff --git a/boatstack/control/flow_runtime.go b/boatstack/control/flow_runtime.go
deleted file mode 100644
index 329526af..00000000
--- a/boatstack/control/flow_runtime.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package control
-
-import (
- "context"
- "encoding/json"
- "fmt"
-)
-
-const FlowProtocolVersion = 1
-
-type FlowRuntimeMode string
-
-const (
- // FlowRuntimeNative selects a trusted in-process first-party adapter.
- FlowRuntimeNative FlowRuntimeMode = "native"
- // FlowRuntimeProtocol selects the bounded public FlowRuntime contract.
- FlowRuntimeProtocol FlowRuntimeMode = "protocol"
-)
-
-type FlowOperation string
-
-const (
- FlowObserveOperation FlowOperation = "observe"
- FlowPlanLocalEffectOperation FlowOperation = "plan-local-effect"
- FlowExecuteExternalOperation FlowOperation = "execute-external"
- FlowVerifyOperation FlowOperation = "verify"
- FlowRecoverOperation FlowOperation = "recover"
-)
-
-type FlowRequest struct {
- ProtocolVersion int `json:"protocol_version"`
- Operation FlowOperation `json:"operation"`
- FlowID string `json:"flow_id"`
- FlowVersion string `json:"flow_version"`
- ProgramFingerprint string `json:"program_fingerprint"`
- CorrelationID string `json:"correlation_id"`
- RepositoryRoot string `json:"repository_root,omitempty"`
- TransitionID TransitionID `json:"transition_id,omitempty"`
- Snapshot json.RawMessage `json:"snapshot,omitempty"`
- Parameters json.RawMessage `json:"parameters,omitempty"`
- Settings json.RawMessage `json:"settings,omitempty"`
-}
-
-type FlowResponse struct {
- ProtocolVersion int `json:"protocol_version"`
- Operation FlowOperation `json:"operation"`
- FlowID string `json:"flow_id"`
- FlowVersion string `json:"flow_version"`
- CorrelationID string `json:"correlation_id"`
- Facts []ExtensionFact `json:"facts,omitempty"`
- Writes []ResourceWrite `json:"writes,omitempty"`
- ExternalResult json.RawMessage `json:"external_result,omitempty"`
- Verified *bool `json:"verified,omitempty"`
- ErrorClass string `json:"error_class,omitempty"`
- Error string `json:"error,omitempty"`
-}
-
-// ValidateFlowOperationResponse enforces the exact payload union for a custom
-// in-process PrimaryFlow runtime. Identity and correlation are checked by the
-// Kernel boundary that owns the request.
-func ValidateFlowOperationResponse(operation FlowOperation, response FlowResponse) error {
- if (response.Error == "") != (response.ErrorClass == "") {
- return fmt.Errorf("primary-flow errors require an explicit classification and message")
- }
- hasPayload := len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
- if response.Error != "" {
- if len(response.ErrorClass) > 128 || len(response.Error) > 4096 {
- return fmt.Errorf("primary-flow error classification or message exceeds its bound")
- }
- if hasPayload {
- return fmt.Errorf("primary-flow error response contains an operation payload")
- }
- return nil
- }
- invalid := false
- switch operation {
- case FlowObserveOperation:
- invalid = len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
- case FlowPlanLocalEffectOperation, FlowRecoverOperation:
- invalid = len(response.Facts) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
- case FlowExecuteExternalOperation:
- invalid = len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) == 0 || response.Verified != nil
- case FlowVerifyOperation:
- invalid = len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified == nil
- default:
- return fmt.Errorf("unsupported primary-flow operation %q", operation)
- }
- if invalid {
- return fmt.Errorf("primary flow returned the wrong response type for %q", operation)
- }
- return nil
-}
-
-// FlowRuntime is the bounded in-process runtime contract for a custom primary
-// flow. It receives projections and returns declarations; it never receives a
-// mutable Kernel object.
-type FlowRuntime interface {
- InvokeFlow(context.Context, FlowRequest) (FlowResponse, error)
-}
-
-// RuntimeFlowDefinition supplies a public runtime together with its trusted
-// primary-flow manifest.
-type RuntimeFlowDefinition interface {
- FlowDefinition
- FlowRuntime() FlowRuntime
-}
diff --git a/boatstack/control/program_manifest.go b/boatstack/control/program_manifest.go
new file mode 100644
index 00000000..f652ec01
--- /dev/null
+++ b/boatstack/control/program_manifest.go
@@ -0,0 +1,580 @@
+package control
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+)
+
+type ProgramErrorCode string
+
+const (
+ ProgramInvalid ProgramErrorCode = "PROGRAM_INVALID"
+ ProgramSchemaUnsupported ProgramErrorCode = "PROGRAM_SCHEMA_UNSUPPORTED"
+ RuntimeTooOld ProgramErrorCode = "RUNTIME_TOO_OLD"
+)
+
+type ProgramError struct {
+ Code ProgramErrorCode
+ Field string
+ Detail string
+}
+
+func (e ProgramError) Error() string {
+ if e.Field == "" {
+ return fmt.Sprintf("%s: %s", e.Code, e.Detail)
+ }
+ return fmt.Sprintf("%s: %s: %s", e.Code, e.Field, e.Detail)
+}
+
+type ProgramCapabilities struct {
+ Effects []string `json:"effects"`
+ Verifiers []string `json:"verifiers"`
+}
+
+// ProgramTransition contains only author-controlled executable semantics.
+// Origin, owner, and the program-qualified ID are derived by validation.
+type ProgramTransition struct {
+ ID TransitionID `json:"id"`
+ Version int `json:"version"`
+ SelectionClass SelectionClass `json:"selection_class"`
+ Class EventClass `json:"class"`
+ SourcePhases []ProtocolPhase `json:"source_phases"`
+ TargetPhases []ProtocolPhase `json:"target_phases"`
+ GoalKinds []GoalKind `json:"goal_kinds,omitempty"`
+ RequiredIdentity []string `json:"required_identity"`
+ Authority []AuthorityClass `json:"authority"`
+ AuthorityAll []AuthorityClass `json:"authority_all,omitempty"`
+ RequiredEvidence []string `json:"required_evidence"`
+ OwnedResources []string `json:"owned_resources,omitempty"`
+ Effect EffectID `json:"effect,omitempty"`
+ LocalEffects []EffectID `json:"local_effects,omitempty"`
+ ExternalEffects []EffectID `json:"external_effects,omitempty"`
+ Idempotent bool `json:"idempotent"`
+ Parameters []ParameterSpec `json:"parameters,omitempty"`
+ Prescription Prescription `json:"prescription"`
+ SourcePredicate string `json:"source_predicate"`
+ SourceConditions []FacetCondition `json:"source_conditions"`
+ AdmissionPredicate string `json:"admission_predicate"`
+ TargetPredicate string `json:"target_predicate"`
+ TargetConditions []FacetCondition `json:"target_conditions"`
+ Verifier string `json:"verifier"`
+ Interruption InterruptionContract `json:"interruption"`
+ Reversibility Reversibility `json:"reversibility"`
+ TerminalEffect string `json:"terminal_effect,omitempty"`
+ PrivacyClassification string `json:"privacy_classification"`
+ TelemetryClassification string `json:"telemetry_classification"`
+ CostClass string `json:"cost_class"`
+ Policy PolicyContract `json:"policy,omitempty"`
+ Priority int `json:"priority"`
+ AllowsIdentityRebind bool `json:"allows_identity_rebind,omitempty"`
+ AllowsWorktreeTransfer bool `json:"allows_worktree_transfer,omitempty"`
+ BindsSourceRevision bool `json:"binds_source_revision,omitempty"`
+ AuthorityFingerprintParameter string `json:"authority_fingerprint_parameter,omitempty"`
+}
+
+// ProgramManifest is the complete source representation of one Control
+// Program. Product surfaces may call the complete program a Flow.
+type ProgramManifest struct {
+ SchemaVersion int `json:"schema_version"`
+ ProgramID string `json:"program_id"`
+ ProgramVersion string `json:"program_version"`
+ RequiresRuntime string `json:"requires_runtime"`
+ Capabilities ProgramCapabilities `json:"capabilities"`
+ OwnedResources []string `json:"owned_resources"`
+ GoalContracts []GoalContract `json:"goal_contracts"`
+ Transitions []ProgramTransition `json:"transitions"`
+}
+
+// RuntimeCompatibility is verified runtime evidence. Declaring a capability
+// does not grant transition authority.
+type RuntimeCompatibility struct {
+ Version string
+ Effects []string
+ Verifiers []string
+}
+
+var (
+ programSemanticID = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$`)
+ programVersionID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`)
+ runtimeConstraint = regexp.MustCompile(`^>=(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
+ runtimeVersion = regexp.MustCompile(`^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z.-]+))?$`)
+)
+
+// LoadProgram strictly parses, validates, normalizes, compatibility-checks,
+// and fingerprints one complete Control Program before registry construction.
+func LoadProgram(source io.Reader, runtime RuntimeCompatibility) (ControlProgram, error) {
+ raw, err := io.ReadAll(io.LimitReader(source, 16<<20))
+ if err != nil {
+ return ControlProgram{}, invalidProgram("", "read program source: "+err.Error())
+ }
+ if err := rejectDuplicateJSONKeys(raw); err != nil {
+ return ControlProgram{}, invalidProgram("", err.Error())
+ }
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.DisallowUnknownFields()
+ var manifest ProgramManifest
+ if err := decoder.Decode(&manifest); err != nil {
+ return ControlProgram{}, invalidProgram("", "parse program source: "+err.Error())
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return ControlProgram{}, invalidProgram("", "program source contains trailing JSON")
+ }
+ return ValidateProgram(manifest, runtime)
+}
+
+// ValidateProgram is the typed form of LoadProgram and enforces the same
+// boundary for programmatic callers.
+func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (ControlProgram, error) {
+ if manifest.SchemaVersion < 1 {
+ return ControlProgram{}, invalidProgram("schema_version", "must be a positive integer")
+ }
+ if manifest.SchemaVersion != ProgramSchemaVersion {
+ return ControlProgram{}, ProgramError{Code: ProgramSchemaUnsupported, Field: "schema_version", Detail: fmt.Sprintf("schema %d is unsupported", manifest.SchemaVersion)}
+ }
+ if !programSemanticID.MatchString(manifest.ProgramID) || strings.Contains(manifest.ProgramID, "/") {
+ return ControlProgram{}, invalidProgram("program_id", "must be a non-empty semantic identifier without '/'")
+ }
+ if !programVersionID.MatchString(manifest.ProgramVersion) {
+ return ControlProgram{}, invalidProgram("program_version", "must be a non-empty deterministic identity")
+ }
+ minimum, err := parseMinimumRuntime(manifest.RequiresRuntime)
+ if err != nil {
+ return ControlProgram{}, invalidProgram("requires_runtime", err.Error())
+ }
+ actual, err := parseRuntimeVersion(runtime.Version)
+ if err != nil {
+ return ControlProgram{}, invalidProgram("runtime.version", err.Error())
+ }
+ if actual.less(minimum) {
+ return ControlProgram{}, ProgramError{Code: RuntimeTooOld, Field: "requires_runtime", Detail: fmt.Sprintf("runtime %s does not satisfy %s", runtime.Version, manifest.RequiresRuntime)}
+ }
+ if len(manifest.Transitions) == 0 {
+ return ControlProgram{}, invalidProgram("transitions", "at least one transition is required")
+ }
+
+ effects, err := normalizedDeclarations("capabilities.effects", manifest.Capabilities.Effects)
+ if err != nil {
+ return ControlProgram{}, err
+ }
+ verifiers, err := normalizedDeclarations("capabilities.verifiers", manifest.Capabilities.Verifiers)
+ if err != nil {
+ return ControlProgram{}, err
+ }
+ resources, err := normalizedDeclarations("owned_resources", manifest.OwnedResources)
+ if err != nil {
+ return ControlProgram{}, err
+ }
+ if missing := missingCapability(effects, runtime.Effects); missing != "" {
+ return ControlProgram{}, invalidProgram("capabilities.effects", fmt.Sprintf("runtime does not provide %q", missing))
+ }
+ if missing := missingCapability(verifiers, runtime.Verifiers); missing != "" {
+ return ControlProgram{}, invalidProgram("capabilities.verifiers", fmt.Sprintf("runtime does not provide %q", missing))
+ }
+
+ normalized := make([]Transition, len(manifest.Transitions))
+ seen := map[TransitionID]bool{}
+ usedEffects := map[string]bool{}
+ usedVerifiers := map[string]bool{}
+ usedResources := map[string]bool{}
+ for index, declared := range manifest.Transitions {
+ source := declared.runtimeTransition()
+ local := source.ID
+ if !programSemanticID.MatchString(string(local)) || strings.Contains(string(local), "/") {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d].id", index), "must be a local semantic identifier without '/'")
+ }
+ if seen[local] {
+ return ControlProgram{}, invalidProgram("transitions", fmt.Sprintf("duplicate transition id %q", local))
+ }
+ seen[local] = true
+ source.ID = TransitionID(manifest.ProgramID + "/" + string(local))
+ source.Owner = manifest.ProgramID
+ source.Origin = catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: manifest.ProgramID, Version: manifest.ProgramVersion, ManifestFingerprint: "pending"}
+ if source.Interruption.Recovery != "" {
+ if !programSemanticID.MatchString(string(source.Interruption.Recovery)) || strings.Contains(string(source.Interruption.Recovery), "/") {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d].interruption.recovery", index), "must be a local semantic identifier without '/'")
+ }
+ source.Interruption.Recovery = TransitionID(manifest.ProgramID + "/" + string(source.Interruption.Recovery))
+ }
+ if source.Policy.BindsRequestedGoal || source.Policy.ReconcilesProgram {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d].policy", index), "runtime-reserved program mutation policy is not repository-declarable")
+ }
+ if source.Controllable() && !containsDeclaration(effects, string(source.Effect)) {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d].effect", index), "effect is not declared by the program")
+ }
+ if source.Controllable() {
+ usedEffects[string(source.Effect)] = true
+ }
+ if !containsDeclaration(verifiers, source.Verifier) {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d].verifier", index), "verifier is not declared by the program")
+ }
+ usedVerifiers[source.Verifier] = true
+ for _, resource := range source.OwnedResources {
+ if !containsDeclaration(resources, resource) {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d].owned_resources", index), fmt.Sprintf("resource %q is not declared by the program", resource))
+ }
+ usedResources[resource] = true
+ }
+ normalized[index], err = normalizeProgramTransition(source)
+ if err != nil {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("transitions[%d]", index), err.Error())
+ }
+ }
+ if extra := firstUnusedDeclaration(effects, usedEffects); extra != "" {
+ return ControlProgram{}, invalidProgram("capabilities.effects", fmt.Sprintf("unused declaration %q", extra))
+ }
+ if extra := firstUnusedDeclaration(verifiers, usedVerifiers); extra != "" {
+ return ControlProgram{}, invalidProgram("capabilities.verifiers", fmt.Sprintf("unused declaration %q", extra))
+ }
+ if extra := firstUnusedDeclaration(resources, usedResources); extra != "" {
+ return ControlProgram{}, invalidProgram("owned_resources", fmt.Sprintf("unused declaration %q", extra))
+ }
+
+ contracts := append([]GoalContract(nil), manifest.GoalContracts...)
+ for index := range contracts {
+ contracts[index].Conditions, err = normalizeConditionSet(contracts[index].Conditions)
+ if err != nil {
+ return ControlProgram{}, invalidProgram(fmt.Sprintf("goal_contracts[%d].conditions", index), err.Error())
+ }
+ }
+ sort.Slice(contracts, func(i, j int) bool { return contracts[i].GoalKind < contracts[j].GoalKind })
+ goalContracts, err := catalog.NewGoalContracts(contracts, nil)
+ if err != nil {
+ return ControlProgram{}, invalidProgram("goal_contracts", err.Error())
+ }
+
+ canonicalTransitions := cloneTransitionsForProgram(normalized)
+ for index := range canonicalTransitions {
+ canonicalTransitions[index].Origin.Version = ""
+ canonicalTransitions[index].Origin.ManifestFingerprint = ""
+ }
+ sort.Slice(canonicalTransitions, func(i, j int) bool { return canonicalTransitions[i].ID < canonicalTransitions[j].ID })
+ canonical := struct {
+ ProgramID string
+ Capabilities ProgramCapabilities
+ OwnedResources []string
+ GoalContracts []GoalContract
+ Transitions []Transition
+ }{manifest.ProgramID, ProgramCapabilities{Effects: effects, Verifiers: verifiers}, resources, goalContracts.All(), canonicalTransitions}
+ fingerprint, err := fingerprint(canonical)
+ if err != nil {
+ return ControlProgram{}, invalidProgram("", "fingerprint canonical program: "+err.Error())
+ }
+ for index := range normalized {
+ normalized[index].Origin.ManifestFingerprint = fingerprint
+ }
+ registry, err := catalog.New(normalized)
+ if err != nil {
+ return ControlProgram{}, invalidProgram("transitions", err.Error())
+ }
+ ownership := make(map[string]string, len(resources))
+ for _, resource := range resources {
+ ownership[resource] = manifest.ProgramID
+ }
+ identity := ComponentIdentity{ID: manifest.ProgramID, Version: manifest.ProgramVersion, Fingerprint: fingerprint}
+ return ControlProgram{
+ summary: ProgramSummary{
+ SchemaVersion: ProgramSchemaVersion, KernelVersion: runtime.Version,
+ ProgramID: manifest.ProgramID, ProgramVersion: manifest.ProgramVersion, RequiresRuntime: manifest.RequiresRuntime,
+ Runtime: identity, RuntimeTransitionCount: registry.Len(), TotalTransitionCount: registry.Len(), ProgramFingerprint: fingerprint,
+ },
+ registry: registry, goalContracts: goalContracts, resourceOwnership: ownership,
+ programRuntime: compiledProgramRuntime{identity: identity},
+ }, nil
+}
+
+func (value ProgramTransition) runtimeTransition() Transition {
+ return Transition{
+ ID: value.ID, Version: value.Version, SelectionClass: value.SelectionClass, Class: value.Class,
+ SourcePhases: value.SourcePhases, TargetPhases: value.TargetPhases, GoalKinds: value.GoalKinds,
+ RequiredIdentity: value.RequiredIdentity, Authority: value.Authority, AuthorityAll: value.AuthorityAll,
+ RequiredEvidence: value.RequiredEvidence, OwnedResources: value.OwnedResources, Effect: value.Effect,
+ LocalEffects: value.LocalEffects, ExternalEffects: value.ExternalEffects, Idempotent: value.Idempotent,
+ Parameters: value.Parameters, Prescription: value.Prescription, SourcePredicate: value.SourcePredicate,
+ SourceConditions: value.SourceConditions, AdmissionPredicate: value.AdmissionPredicate,
+ TargetPredicate: value.TargetPredicate, TargetConditions: value.TargetConditions, Verifier: value.Verifier,
+ Interruption: value.Interruption, Reversibility: value.Reversibility, TerminalEffect: value.TerminalEffect,
+ PrivacyClassification: value.PrivacyClassification, TelemetryClassification: value.TelemetryClassification,
+ CostClass: value.CostClass, Policy: value.Policy, Priority: value.Priority,
+ AllowsIdentityRebind: value.AllowsIdentityRebind, AllowsWorktreeTransfer: value.AllowsWorktreeTransfer,
+ BindsSourceRevision: value.BindsSourceRevision, AuthorityFingerprintParameter: value.AuthorityFingerprintParameter,
+ }
+}
+
+func invalidProgram(field, detail string) error {
+ return ProgramError{Code: ProgramInvalid, Field: field, Detail: detail}
+}
+
+type semanticVersion struct {
+ major, minor, patch int
+ prerelease bool
+}
+
+func parseMinimumRuntime(value string) (semanticVersion, error) {
+ match := runtimeConstraint.FindStringSubmatch(value)
+ if match == nil {
+ return semanticVersion{}, fmt.Errorf("must use exact minimum syntax >=MAJOR.MINOR.PATCH")
+ }
+ return versionParts(match[1:4], false), nil
+}
+
+func parseRuntimeVersion(value string) (semanticVersion, error) {
+ match := runtimeVersion.FindStringSubmatch(value)
+ if match == nil {
+ return semanticVersion{}, fmt.Errorf("must be vMAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH")
+ }
+ return versionParts(match[1:4], match[4] != ""), nil
+}
+
+func versionParts(parts []string, prerelease bool) semanticVersion {
+ values := make([]int, 3)
+ for index, part := range parts {
+ values[index], _ = strconv.Atoi(part)
+ }
+ return semanticVersion{major: values[0], minor: values[1], patch: values[2], prerelease: prerelease}
+}
+
+func (v semanticVersion) less(other semanticVersion) bool {
+ if v.major != other.major {
+ return v.major < other.major
+ }
+ if v.minor != other.minor {
+ return v.minor < other.minor
+ }
+ if v.patch != other.patch {
+ return v.patch < other.patch
+ }
+ return v.prerelease && !other.prerelease
+}
+
+func normalizedDeclarations(field string, values []string) ([]string, error) {
+ result := append([]string(nil), values...)
+ seen := map[string]bool{}
+ for _, value := range result {
+ if value == "" || !programSemanticID.MatchString(value) || seen[value] {
+ return nil, invalidProgram(field, fmt.Sprintf("contains invalid or duplicate declaration %q", value))
+ }
+ seen[value] = true
+ }
+ sort.Strings(result)
+ return result, nil
+}
+
+func missingCapability(required, available []string) string {
+ set := map[string]bool{}
+ for _, value := range available {
+ set[value] = true
+ }
+ for _, value := range required {
+ if !set[value] {
+ return value
+ }
+ }
+ return ""
+}
+
+func firstUnusedDeclaration(declared []string, used map[string]bool) string {
+ for _, value := range declared {
+ if !used[value] {
+ return value
+ }
+ }
+ return ""
+}
+
+func containsDeclaration(values []string, wanted string) bool {
+ index := sort.SearchStrings(values, wanted)
+ return index < len(values) && values[index] == wanted
+}
+
+func normalizeProgramTransition(value Transition) (Transition, error) {
+ var err error
+ value.SourcePhases, err = uniqueSorted(value.SourcePhases, func(v ProtocolPhase) string { return string(v) })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.TargetPhases, err = uniqueSorted(value.TargetPhases, func(v ProtocolPhase) string { return string(v) })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.GoalKinds, err = uniqueSorted(value.GoalKinds, func(v GoalKind) string { return string(v) })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.RequiredIdentity, err = uniqueSorted(value.RequiredIdentity, func(v string) string { return v })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.Authority, err = uniqueSorted(value.Authority, func(v AuthorityClass) string { return string(v) })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.AuthorityAll, err = uniqueSorted(value.AuthorityAll, func(v AuthorityClass) string { return string(v) })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.RequiredEvidence, err = uniqueSorted(value.RequiredEvidence, func(v string) string { return v })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.OwnedResources, err = uniqueSorted(value.OwnedResources, func(v string) string { return v })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.LocalEffects, err = uniqueSorted(value.LocalEffects, func(v EffectID) string { return string(v) })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.ExternalEffects, err = uniqueSorted(value.ExternalEffects, func(v EffectID) string { return string(v) })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.Parameters, err = uniqueSorted(value.Parameters, func(v ParameterSpec) string { return v.Name })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.Interruption.Points, err = uniqueSorted(value.Interruption.Points, func(v string) string { return v })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.Interruption.PartialState, err = uniqueSorted(value.Interruption.PartialState, func(v string) string { return v })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.Policy.ManagedOperations, err = uniqueSorted(value.Policy.ManagedOperations, func(v string) string { return v })
+ if err != nil {
+ return Transition{}, err
+ }
+ value.SourceConditions, err = normalizeConditionSet(value.SourceConditions)
+ if err != nil {
+ return Transition{}, err
+ }
+ value.TargetConditions, err = normalizeConditionSet(value.TargetConditions)
+ if err != nil {
+ return Transition{}, err
+ }
+ return value, nil
+}
+
+func normalizeConditionSet(values []FacetCondition) ([]FacetCondition, error) {
+ result := append([]FacetCondition(nil), values...)
+ for index := range result {
+ var err error
+ result[index].Statuses, err = uniqueSorted(result[index].Statuses, func(value FactStatus) string { return string(value) })
+ if err != nil {
+ return nil, err
+ }
+ result[index].Values, err = uniqueSortedAllowEmpty(result[index].Values)
+ if err != nil {
+ return nil, err
+ }
+ }
+ sort.Slice(result, func(i, j int) bool {
+ left, _ := json.Marshal(result[i])
+ right, _ := json.Marshal(result[j])
+ return string(left) < string(right)
+ })
+ for index := 1; index < len(result); index++ {
+ left, _ := json.Marshal(result[index-1])
+ right, _ := json.Marshal(result[index])
+ if bytes.Equal(left, right) {
+ return nil, fmt.Errorf("contains duplicate condition")
+ }
+ }
+ return result, nil
+}
+
+func uniqueSortedAllowEmpty(values []string) ([]string, error) {
+ result := append([]string(nil), values...)
+ seen := map[string]bool{}
+ for _, value := range result {
+ if seen[value] {
+ return nil, fmt.Errorf("contains duplicate declaration %q", value)
+ }
+ seen[value] = true
+ }
+ sort.Strings(result)
+ return result, nil
+}
+
+func uniqueSorted[T any](values []T, key func(T) string) ([]T, error) {
+ result := append([]T(nil), values...)
+ seen := map[string]bool{}
+ for _, value := range result {
+ name := key(value)
+ if name == "" || seen[name] {
+ return nil, fmt.Errorf("contains empty or duplicate declaration %q", name)
+ }
+ seen[name] = true
+ }
+ sort.Slice(result, func(i, j int) bool { return key(result[i]) < key(result[j]) })
+ return result, nil
+}
+
+func cloneTransitionsForProgram(values []Transition) []Transition {
+ result := make([]Transition, len(values))
+ for index, value := range values {
+ result[index] = cloneTransition(value)
+ }
+ return result
+}
+
+func rejectDuplicateJSONKeys(raw []byte) error {
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ var walk func() error
+ walk = func() error {
+ token, err := decoder.Token()
+ if err != nil {
+ return err
+ }
+ delimiter, ok := token.(json.Delim)
+ if !ok {
+ return nil
+ }
+ switch delimiter {
+ case '{':
+ seen := map[string]bool{}
+ for decoder.More() {
+ keyToken, err := decoder.Token()
+ if err != nil {
+ return err
+ }
+ key := keyToken.(string)
+ if seen[key] {
+ return fmt.Errorf("duplicate JSON field %q", key)
+ }
+ seen[key] = true
+ if err := walk(); err != nil {
+ return err
+ }
+ }
+ _, err = decoder.Token()
+ return err
+ case '[':
+ for decoder.More() {
+ if err := walk(); err != nil {
+ return err
+ }
+ }
+ _, err = decoder.Token()
+ return err
+ default:
+ return nil
+ }
+ }
+ if err := walk(); err != nil {
+ return err
+ }
+ if _, err := decoder.Token(); err != io.EOF {
+ return fmt.Errorf("trailing JSON")
+ }
+ return nil
+}
diff --git a/boatstack/control/program_manifest_test.go b/boatstack/control/program_manifest_test.go
new file mode 100644
index 00000000..dcb6f23b
--- /dev/null
+++ b/boatstack/control/program_manifest_test.go
@@ -0,0 +1,298 @@
+package control_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "sort"
+ "strings"
+ "testing"
+
+ boatstack "github.com/operatorstack/boatstack/boatstack"
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/surfaces"
+)
+
+func TestProgramManifestCanonicalFingerprintContract(t *testing.T) {
+ // control-law: validated-executable-semantics-exactly-determine-program-fingerprint
+ base := programFixture()
+ one := loadManifest(t, base)
+
+ equivalent := programFixture()
+ equivalent.ProgramVersion = "99"
+ equivalent.RequiresRuntime = ">=0.9.0"
+ reverse(equivalent.Capabilities.Effects)
+ reverse(equivalent.Capabilities.Verifiers)
+ reverse(equivalent.OwnedResources)
+ reverse(equivalent.Transitions)
+ reverse(equivalent.Transitions[0].RequiredIdentity)
+ reverse(equivalent.Transitions[0].SourceConditions)
+ two := loadManifest(t, equivalent)
+ if one.Fingerprint() != two.Fingerprint() {
+ t.Fatalf("representation-only or author-version changes changed executable fingerprint: %s != %s", one.Fingerprint(), two.Fingerprint())
+ }
+
+ raw, err := json.MarshalIndent(base, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ three, err := control.LoadProgram(bytes.NewReader(raw), runtimeFixture())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if one.Fingerprint() != three.Fingerprint() {
+ t.Fatal("whitespace changed executable fingerprint")
+ }
+ reordered := reorderTopLevelObject(t, raw)
+ four, err := control.LoadProgram(bytes.NewReader(reordered), runtimeFixture())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if one.Fingerprint() != four.Fingerprint() {
+ t.Fatal("JSON object key order changed executable fingerprint")
+ }
+
+ mutations := map[string]func(*control.ProgramManifest){
+ "program-id": func(value *control.ProgramManifest) { value.ProgramID = "alternate-program" },
+ "source-phase": func(value *control.ProgramManifest) {
+ value.Transitions[0].SourcePhases = []control.ProtocolPhase{control.PhaseObserved}
+ },
+ "target-phase": func(value *control.ProgramManifest) {
+ value.Transitions[0].TargetPhases = []control.ProtocolPhase{control.PhaseFrontier}
+ },
+ "authority": func(value *control.ProgramManifest) {
+ value.Transitions[0].Authority = []control.AuthorityClass{control.AuthorityHuman}
+ },
+ "effect": func(value *control.ProgramManifest) {
+ value.Capabilities.Effects[0] = "alternate.effect"
+ value.Transitions[0].Effect = "alternate.effect"
+ value.Transitions[0].LocalEffects = []control.EffectID{"alternate.effect"}
+ },
+ "verifier": func(value *control.ProgramManifest) {
+ value.Capabilities.Verifiers[1] = "alternate.verifier"
+ value.Transitions[0].Verifier = "alternate.verifier"
+ },
+ "postcondition": func(value *control.ProgramManifest) {
+ value.Transitions[0].TargetConditions[0].Values = []string{"alternate"}
+ },
+ "recovery": func(value *control.ProgramManifest) { value.Transitions[0].Interruption.Recovery = "alternate.recover" },
+ "priority": func(value *control.ProgramManifest) { value.Transitions[0].Priority++ },
+ }
+ for name, mutate := range mutations {
+ t.Run(name, func(t *testing.T) {
+ candidate := programFixture()
+ mutate(&candidate)
+ if name == "recovery" {
+ recovery := candidate.Transitions[1]
+ recovery.ID = "alternate.recover"
+ recovery.Interruption.Recovery = "alternate.recover"
+ candidate.Transitions = append(candidate.Transitions, recovery)
+ }
+ program := loadManifest(t, candidate)
+ if program.Fingerprint() == one.Fingerprint() {
+ t.Fatalf("%s semantic change did not change fingerprint", name)
+ }
+ })
+ }
+}
+
+func TestProgramManifestNamespaceAndCompatibilityBoundary(t *testing.T) {
+ // control-law: only-compatible-validated-programs-reach-the-runtime-registry
+ first := programFixture()
+ first.ProgramID = "first-program"
+ second := programFixture()
+ second.ProgramID = "second-program"
+ one := loadManifest(t, first)
+ two := loadManifest(t, second)
+ if one.Transitions()[0].ID == two.Transitions()[0].ID {
+ t.Fatal("equal local transition IDs collided across programs")
+ }
+ for _, transition := range one.Transitions() {
+ if !strings.HasPrefix(string(transition.ID), "first-program/") {
+ t.Fatalf("transition is not program-qualified: %s", transition.ID)
+ }
+ }
+
+ cases := []struct {
+ name string
+ mutate func(*control.ProgramManifest)
+ runtime control.RuntimeCompatibility
+ code control.ProgramErrorCode
+ }{
+ {"unsupported-schema", func(value *control.ProgramManifest) { value.SchemaVersion = 2 }, runtimeFixture(), control.ProgramSchemaUnsupported},
+ {"invalid-schema", func(value *control.ProgramManifest) { value.SchemaVersion = 0 }, runtimeFixture(), control.ProgramInvalid},
+ {"runtime-too-old", func(value *control.ProgramManifest) { value.RequiresRuntime = ">=2.0.0" }, runtimeFixture(), control.RuntimeTooOld},
+ {"malformed-runtime", func(value *control.ProgramManifest) { value.RequiresRuntime = "^1" }, runtimeFixture(), control.ProgramInvalid},
+ {"malformed-program-id", func(value *control.ProgramManifest) { value.ProgramID = "../program" }, runtimeFixture(), control.ProgramInvalid},
+ {"ambiguous-transition-id", func(value *control.ProgramManifest) { value.Transitions[0].ID = "other/advance" }, runtimeFixture(), control.ProgramInvalid},
+ {"duplicate-transition", func(value *control.ProgramManifest) {
+ value.Transitions = append(value.Transitions, value.Transitions[0])
+ }, runtimeFixture(), control.ProgramInvalid},
+ {"duplicate-capability", func(value *control.ProgramManifest) {
+ value.Capabilities.Effects = append(value.Capabilities.Effects, value.Capabilities.Effects[0])
+ }, runtimeFixture(), control.ProgramInvalid},
+ {"duplicate-condition", func(value *control.ProgramManifest) {
+ value.Transitions[0].SourceConditions = append(value.Transitions[0].SourceConditions, value.Transitions[0].SourceConditions[0])
+ }, runtimeFixture(), control.ProgramInvalid},
+ {"missing-runtime-capability", func(*control.ProgramManifest) {}, control.RuntimeCompatibility{Version: "v1.2.3"}, control.ProgramInvalid},
+ }
+ for _, test := range cases {
+ t.Run(test.name, func(t *testing.T) {
+ manifest := programFixture()
+ test.mutate(&manifest)
+ _, err := control.ValidateProgram(manifest, test.runtime)
+ var programErr control.ProgramError
+ if !errors.As(err, &programErr) || programErr.Code != test.code {
+ t.Fatalf("error = %v, want %s", err, test.code)
+ }
+ })
+ }
+
+ exact := programFixture()
+ exact.RequiresRuntime = ">=1.2.3"
+ if _, err := control.ValidateProgram(exact, runtimeFixture()); err != nil {
+ t.Fatalf("exact minimum was rejected: %v", err)
+ }
+ above := programFixture()
+ above.RequiresRuntime = ">=1.0.0"
+ if _, err := control.ValidateProgram(above, runtimeFixture()); err != nil {
+ t.Fatalf("runtime above minimum was rejected: %v", err)
+ }
+ prerelease := programFixture()
+ prerelease.RequiresRuntime = ">=1.2.3"
+ candidateRuntime := runtimeFixture()
+ candidateRuntime.Version = "v1.2.3-dev"
+ if _, err := control.ValidateProgram(prerelease, candidateRuntime); err == nil {
+ t.Fatal("prerelease runtime incorrectly satisfied the matching stable minimum")
+ }
+}
+
+func TestProgramSourceParserFailsClosed(t *testing.T) {
+ // control-law: uninterpreted-source-cannot-reach-the-executable-program
+ manifest := programFixture()
+ raw, err := json.Marshal(manifest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ unknown := bytes.Replace(raw, []byte(`"program_id"`), []byte(`"requires_human":true,"program_id"`), 1)
+ if _, err := control.LoadProgram(bytes.NewReader(unknown), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "unknown field") {
+ t.Fatalf("unknown control-law field was not rejected: %v", err)
+ }
+ unknownTransition := bytes.Replace(raw, []byte(`"priority":1`), []byte(`"requires_human":true,"priority":1`), 1)
+ if _, err := control.LoadProgram(bytes.NewReader(unknownTransition), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "unknown field") {
+ t.Fatalf("unknown transition field was not rejected: %v", err)
+ }
+ duplicate := bytes.Replace(raw, []byte(`"program_id":"test-program"`), []byte(`"program_id":"test-program","program_id":"weaker-program"`), 1)
+ if _, err := control.LoadProgram(bytes.NewReader(duplicate), runtimeFixture()); err == nil || !strings.Contains(err.Error(), "duplicate JSON field") {
+ t.Fatalf("duplicate JSON field was not rejected: %v", err)
+ }
+ if _, err := control.LoadProgram(bytes.NewReader(append(raw, []byte(` {}`)...)), runtimeFixture()); err == nil {
+ t.Fatal("trailing JSON was accepted")
+ }
+}
+
+func TestValidatedProgramIsTheKernelRegistry(t *testing.T) {
+ // control-law: kernel-consumes-the-exact-validated-fingerprinted-registry
+ program := loadManifest(t, programFixture())
+ kernel, err := boatstack.NewKernel(t.TempDir(), program)
+ if err != nil {
+ t.Fatal(err)
+ }
+ response, err := kernel.Handle(t.Context(), surfaces.Request{SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationCatalog})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(response.Catalog) != len(programFixture().Transitions) {
+ t.Fatalf("kernel catalog count = %d", len(response.Catalog))
+ }
+ for _, transition := range response.Catalog {
+ if !strings.HasPrefix(string(transition.ID), "test-program/") || transition.Origin.ManifestFingerprint != program.Fingerprint() {
+ t.Fatalf("kernel reached around validated identity: %+v", transition)
+ }
+ }
+}
+
+func programFixture() control.ProgramManifest {
+ recovery := control.ProgramTransition{
+ ID: "recover", Version: 1, SelectionClass: control.SelectionProgramRecovery, Class: control.EventRecovery,
+ SourcePhases: []control.ProtocolPhase{control.PhaseRecovery}, TargetPhases: []control.ProtocolPhase{control.PhaseActive},
+ RequiredIdentity: []string{"repository-id"}, Authority: []control.AuthorityClass{control.AuthorityRepository}, RequiredEvidence: []string{"snapshot"},
+ OwnedResources: []string{"program.state"}, Effect: "program.recover", LocalEffects: []control.EffectID{"program.recover"}, Idempotent: true,
+ Prescription: control.Prescription{Operation: "recover", ExpectedPostcondition: "active"}, SourcePredicate: "recovery-required",
+ SourceConditions: []control.FacetCondition{control.KnownCondition(control.FacetRecovery, "required")}, AdmissionPredicate: "exact-admission",
+ TargetPredicate: "active", TargetConditions: []control.FacetCondition{control.KnownCondition(control.FacetProgram, "current")}, Verifier: "program.current",
+ Interruption: interruption("recover"), Reversibility: control.Reversible, TerminalEffect: "none",
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "local", Priority: 1,
+ }
+ advance := recovery
+ advance.ID = "advance"
+ advance.SelectionClass = control.SelectionProgramProgress
+ advance.Class = control.EventOwnedLocal
+ advance.SourcePhases = []control.ProtocolPhase{control.PhaseActive}
+ advance.TargetPhases = []control.ProtocolPhase{control.PhaseTerminal}
+ advance.GoalKinds = []control.GoalKind{control.GoalVerified}
+ advance.Authority = []control.AuthorityClass{control.AuthorityHuman, control.AuthorityRepository}
+ advance.Effect = "program.advance"
+ advance.LocalEffects = []control.EffectID{"program.advance"}
+ advance.Prescription = control.Prescription{Operation: "advance", Arguments: []string{"--exact"}, ExpectedPostcondition: "terminal"}
+ advance.SourcePredicate = "active"
+ advance.SourceConditions = []control.FacetCondition{control.KnownCondition(control.FacetProgram, "current"), control.KnownCondition(control.FacetDelivery, "active")}
+ advance.TargetPredicate = "terminal"
+ advance.TargetConditions = []control.FacetCondition{control.KnownCondition(control.FacetDelivery, "terminal")}
+ advance.Verifier = "program.terminal"
+ return control.ProgramManifest{
+ SchemaVersion: control.ProgramSchemaVersion, ProgramID: "test-program", ProgramVersion: "1", RequiresRuntime: ">=1.0.0",
+ Capabilities: control.ProgramCapabilities{Effects: []string{"program.advance", "program.recover"}, Verifiers: []string{"program.current", "program.terminal"}},
+ OwnedResources: []string{"program.state"}, GoalContracts: []control.GoalContract{{GoalKind: control.GoalVerified, Conditions: []control.FacetCondition{control.KnownCondition(control.FacetDelivery, "terminal")}}},
+ Transitions: []control.ProgramTransition{advance, recovery},
+ }
+}
+
+func runtimeFixture() control.RuntimeCompatibility {
+ return control.RuntimeCompatibility{Version: "v1.2.3", Effects: []string{"program.advance", "program.recover", "alternate.effect"}, Verifiers: []string{"program.current", "program.terminal", "alternate.verifier"}}
+}
+
+func loadManifest(t *testing.T, manifest control.ProgramManifest) control.ControlProgram {
+ t.Helper()
+ program, err := control.ValidateProgram(manifest, runtimeFixture())
+ if err != nil {
+ t.Fatal(err)
+ }
+ return program
+}
+
+func interruption(recovery control.TransitionID) control.InterruptionContract {
+ return control.InterruptionContract{Points: []string{"after-effect"}, PartialState: []string{"effect-may-exist"}, Detection: "fresh-observation", ResumeContract: "resume", RollbackContract: "rollback", CompensationContract: "compensate", Recovery: recovery, RecoveryAuthority: "repository-policy", ResumptionPredicate: "exact-state"}
+}
+
+func reverse[T any](values []T) {
+ for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
+ values[left], values[right] = values[right], values[left]
+ }
+}
+
+func reorderTopLevelObject(t *testing.T, raw []byte) []byte {
+ t.Helper()
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(raw, &fields); err != nil {
+ t.Fatal(err)
+ }
+ keys := make([]string, 0, len(fields))
+ for key := range fields {
+ keys = append(keys, key)
+ }
+ sort.Sort(sort.Reverse(sort.StringSlice(keys)))
+ var result bytes.Buffer
+ result.WriteByte('{')
+ for index, key := range keys {
+ if index != 0 {
+ result.WriteByte(',')
+ }
+ encodedKey, _ := json.Marshal(key)
+ result.Write(encodedKey)
+ result.WriteByte(':')
+ result.Write(fields[key])
+ }
+ result.WriteByte('}')
+ return result.Bytes()
+}
diff --git a/boatstack/control/program_runtime.go b/boatstack/control/program_runtime.go
new file mode 100644
index 00000000..34398b64
--- /dev/null
+++ b/boatstack/control/program_runtime.go
@@ -0,0 +1,106 @@
+package control
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+)
+
+const ProgramRuntimeProtocolVersion = 1
+
+type ProgramRuntimeMode string
+
+const (
+ // ProgramRuntimeNative selects a trusted in-process first-party adapter.
+ ProgramRuntimeNative ProgramRuntimeMode = "native"
+ // ProgramRuntimeProtocol selects the bounded public ProgramRuntime contract.
+ ProgramRuntimeProtocol ProgramRuntimeMode = "protocol"
+)
+
+type ProgramRuntimeOperation string
+
+const (
+ ProgramObserveOperation ProgramRuntimeOperation = "observe"
+ ProgramPlanLocalEffectOperation ProgramRuntimeOperation = "plan-local-effect"
+ ProgramExecuteExternalOperation ProgramRuntimeOperation = "execute-external"
+ ProgramVerifyOperation ProgramRuntimeOperation = "verify"
+ ProgramRecoverOperation ProgramRuntimeOperation = "recover"
+)
+
+type ProgramRuntimeRequest struct {
+ ProtocolVersion int `json:"protocol_version"`
+ Operation ProgramRuntimeOperation `json:"operation"`
+ ProgramID string `json:"program_id"`
+ ProgramVersion string `json:"program_version"`
+ ProgramFingerprint string `json:"program_fingerprint"`
+ CorrelationID string `json:"correlation_id"`
+ RepositoryRoot string `json:"repository_root,omitempty"`
+ TransitionID TransitionID `json:"transition_id,omitempty"`
+ Snapshot json.RawMessage `json:"snapshot,omitempty"`
+ Parameters json.RawMessage `json:"parameters,omitempty"`
+ Settings json.RawMessage `json:"settings,omitempty"`
+}
+
+type ProgramRuntimeResponse struct {
+ ProtocolVersion int `json:"protocol_version"`
+ Operation ProgramRuntimeOperation `json:"operation"`
+ ProgramID string `json:"program_id"`
+ ProgramVersion string `json:"program_version"`
+ CorrelationID string `json:"correlation_id"`
+ Facts []ExtensionFact `json:"facts,omitempty"`
+ Writes []ResourceWrite `json:"writes,omitempty"`
+ ExternalResult json.RawMessage `json:"external_result,omitempty"`
+ Verified *bool `json:"verified,omitempty"`
+ ErrorClass string `json:"error_class,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+// ValidateProgramRuntimeOperationResponse enforces the exact payload union for a custom
+// in-process ProgramRuntime runtime. Identity and correlation are checked by the
+// Kernel boundary that owns the request.
+func ValidateProgramRuntimeOperationResponse(operation ProgramRuntimeOperation, response ProgramRuntimeResponse) error {
+ if (response.Error == "") != (response.ErrorClass == "") {
+ return fmt.Errorf("control-program errors require an explicit classification and message")
+ }
+ hasPayload := len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
+ if response.Error != "" {
+ if len(response.ErrorClass) > 128 || len(response.Error) > 4096 {
+ return fmt.Errorf("control-program error classification or message exceeds its bound")
+ }
+ if hasPayload {
+ return fmt.Errorf("control-program error response contains an operation payload")
+ }
+ return nil
+ }
+ invalid := false
+ switch operation {
+ case ProgramObserveOperation:
+ invalid = len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
+ case ProgramPlanLocalEffectOperation, ProgramRecoverOperation:
+ invalid = len(response.Facts) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
+ case ProgramExecuteExternalOperation:
+ invalid = len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) == 0 || response.Verified != nil
+ case ProgramVerifyOperation:
+ invalid = len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified == nil
+ default:
+ return fmt.Errorf("unsupported control-program operation %q", operation)
+ }
+ if invalid {
+ return fmt.Errorf("program runtime returned the wrong response type for %q", operation)
+ }
+ return nil
+}
+
+// ProgramRuntime is the bounded in-process execution contract for a Control
+// Program. It receives projections and returns declarations; it never receives a
+// mutable Kernel object.
+type ProgramRuntime interface {
+ InvokeProgram(context.Context, ProgramRuntimeRequest) (ProgramRuntimeResponse, error)
+}
+
+// RuntimeProgramDefinition supplies a public runtime together with its trusted
+// control-program manifest.
+type RuntimeProgramDefinition interface {
+ ProgramRuntimeDefinition
+ ProgramRuntime() ProgramRuntime
+}
diff --git a/boatstack/control/runtime_contract_test.go b/boatstack/control/runtime_contract_test.go
index e8fcc584..8c1cfba2 100644
--- a/boatstack/control/runtime_contract_test.go
+++ b/boatstack/control/runtime_contract_test.go
@@ -6,45 +6,45 @@ import (
"testing"
)
-func TestFlowOperationResponsesAreAnExactTaggedUnion(t *testing.T) {
+func TestProgramRuntimeOperationResponsesAreAnExactTaggedUnion(t *testing.T) {
verified := true
- valid := map[FlowOperation]FlowResponse{
- FlowObserveOperation: {Facts: []ExtensionFact{{ID: "flow.ready"}}},
- FlowPlanLocalEffectOperation: {Writes: []ResourceWrite{{Resource: "flow.plan"}}},
- FlowExecuteExternalOperation: {ExternalResult: json.RawMessage(`{"ok":true}`)},
- FlowVerifyOperation: {Verified: &verified},
- FlowRecoverOperation: {Writes: []ResourceWrite{{Resource: "flow.recovery"}}},
+ valid := map[ProgramRuntimeOperation]ProgramRuntimeResponse{
+ ProgramObserveOperation: {Facts: []ExtensionFact{{ID: "flow.ready"}}},
+ ProgramPlanLocalEffectOperation: {Writes: []ResourceWrite{{Resource: "flow.plan"}}},
+ ProgramExecuteExternalOperation: {ExternalResult: json.RawMessage(`{"ok":true}`)},
+ ProgramVerifyOperation: {Verified: &verified},
+ ProgramRecoverOperation: {Writes: []ResourceWrite{{Resource: "flow.recovery"}}},
}
for operation, response := range valid {
- if err := ValidateFlowOperationResponse(operation, response); err != nil {
+ if err := ValidateProgramRuntimeOperationResponse(operation, response); err != nil {
t.Fatalf("valid %q response: %v", operation, err)
}
}
invalid := []struct {
name string
- operation FlowOperation
- response FlowResponse
+ operation ProgramRuntimeOperation
+ response ProgramRuntimeResponse
}{
- {"observe-write", FlowObserveOperation, FlowResponse{Writes: []ResourceWrite{{Resource: "wrong"}}}},
- {"local-fact", FlowPlanLocalEffectOperation, FlowResponse{Facts: []ExtensionFact{{ID: "wrong"}}}},
- {"external-empty", FlowExecuteExternalOperation, FlowResponse{}},
- {"verify-missing", FlowVerifyOperation, FlowResponse{}},
- {"recover-external", FlowRecoverOperation, FlowResponse{ExternalResult: json.RawMessage(`{}`)}},
- {"partial-error", FlowObserveOperation, FlowResponse{ErrorClass: "temporary"}},
- {"error-payload", FlowObserveOperation, FlowResponse{ErrorClass: "temporary", Error: "failed", Facts: []ExtensionFact{{ID: "wrong"}}}},
- {"error-class-too-long", FlowObserveOperation, FlowResponse{ErrorClass: strings.Repeat("x", 129), Error: "failed"}},
- {"error-message-too-long", FlowObserveOperation, FlowResponse{ErrorClass: "temporary", Error: strings.Repeat("x", 4097)}},
- {"unknown-operation", FlowOperation("unknown"), FlowResponse{}},
+ {"observe-write", ProgramObserveOperation, ProgramRuntimeResponse{Writes: []ResourceWrite{{Resource: "wrong"}}}},
+ {"local-fact", ProgramPlanLocalEffectOperation, ProgramRuntimeResponse{Facts: []ExtensionFact{{ID: "wrong"}}}},
+ {"external-empty", ProgramExecuteExternalOperation, ProgramRuntimeResponse{}},
+ {"verify-missing", ProgramVerifyOperation, ProgramRuntimeResponse{}},
+ {"recover-external", ProgramRecoverOperation, ProgramRuntimeResponse{ExternalResult: json.RawMessage(`{}`)}},
+ {"partial-error", ProgramObserveOperation, ProgramRuntimeResponse{ErrorClass: "temporary"}},
+ {"error-payload", ProgramObserveOperation, ProgramRuntimeResponse{ErrorClass: "temporary", Error: "failed", Facts: []ExtensionFact{{ID: "wrong"}}}},
+ {"error-class-too-long", ProgramObserveOperation, ProgramRuntimeResponse{ErrorClass: strings.Repeat("x", 129), Error: "failed"}},
+ {"error-message-too-long", ProgramObserveOperation, ProgramRuntimeResponse{ErrorClass: "temporary", Error: strings.Repeat("x", 4097)}},
+ {"unknown-operation", ProgramRuntimeOperation("unknown"), ProgramRuntimeResponse{}},
}
for _, test := range invalid {
t.Run(test.name, func(t *testing.T) {
- if err := ValidateFlowOperationResponse(test.operation, test.response); err == nil {
+ if err := ValidateProgramRuntimeOperationResponse(test.operation, test.response); err == nil {
t.Fatalf("invalid %q response was accepted", test.operation)
}
})
}
- if err := ValidateFlowOperationResponse(FlowObserveOperation, FlowResponse{ErrorClass: "temporary", Error: "failed"}); err != nil {
+ if err := ValidateProgramRuntimeOperationResponse(ProgramObserveOperation, ProgramRuntimeResponse{ErrorClass: "temporary", Error: "failed"}); err != nil {
t.Fatalf("classified error response: %v", err)
}
}
diff --git a/boatstack/core/system_test.go b/boatstack/core/system_test.go
index db3dfb8d..6bf22986 100644
--- a/boatstack/core/system_test.go
+++ b/boatstack/core/system_test.go
@@ -9,7 +9,7 @@ import (
)
func TestManifestOwnsOnlyOperationalCapabilities(t *testing.T) {
- // control-law: core-system-declarations-exclude-primary-flow-policy
+ // control-law: core-system-declarations-exclude-control-program-policy
manifest, err := core.System().CoreManifest(context.Background())
if err != nil {
t.Fatal(err)
diff --git a/boatstack/distribution/standard.go b/boatstack/distribution/standard.go
index 6b09a5ae..7cb943ab 100644
--- a/boatstack/distribution/standard.go
+++ b/boatstack/distribution/standard.go
@@ -23,7 +23,7 @@ import (
func StandardProgram(ctx context.Context, extensions ...control.Extension) (control.ControlProgram, error) {
return control.Compile(ctx, control.CompileRequest{
KernelVersion: boatstack.Version,
- Core: core.System(), Flow: standard.Definition(), Extensions: extensions,
+ Core: core.System(), Runtime: standard.Definition(), Extensions: extensions,
Settings: programSettings{},
})
}
@@ -60,7 +60,7 @@ func StandardProgramForRepository(ctx context.Context, request RepositoryProgram
extensions := append([]control.Extension(nil), request.Extensions...)
extensions = append(extensions, configured...)
return control.Compile(ctx, control.CompileRequest{
- KernelVersion: boatstack.Version, Core: core.System(), Flow: standard.Definition(),
+ KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(),
Extensions: extensions, Settings: settings,
})
}
diff --git a/boatstack/examples/control_program_test.go b/boatstack/examples/control_program_test.go
index 78fd21c3..e0eeecf6 100644
--- a/boatstack/examples/control_program_test.go
+++ b/boatstack/examples/control_program_test.go
@@ -15,21 +15,21 @@ func Example_standardFlowWithReleaseNoteExtension() {
program, err := control.Compile(context.Background(), control.CompileRequest{
KernelVersion: "example-kernel",
Core: core.System(),
- Flow: standard.Definition(),
+ Runtime: standard.Definition(),
Extensions: []control.Extension{releasenote.Definition()},
})
if err != nil {
panic(err)
}
summary := program.Summary()
- fmt.Printf("%s + %s + %s: %d transitions\n", summary.Core.ID, summary.Flow.ID, summary.Extensions[0].ID, summary.TotalTransitionCount)
+ fmt.Printf("%s + %s + %s: %d transitions\n", summary.Core.ID, summary.Runtime.ID, summary.Extensions[0].ID, summary.TotalTransitionCount)
// Output:
// boatstack.core + boatstack.standard + boatstack.release-note: 64 transitions
}
func Example_sdkCustomKernel() {
_, err := sdk.NewKernel("",
- sdk.WithFlow(standard.Definition()),
+ sdk.WithProgramRuntime(standard.Definition()),
sdk.WithExtension(releasenote.Definition()),
)
fmt.Println(err == nil)
diff --git a/boatstack/extension/subprocess/subprocess_test.go b/boatstack/extension/subprocess/subprocess_test.go
index 830f4912..ecbd84c1 100644
--- a/boatstack/extension/subprocess/subprocess_test.go
+++ b/boatstack/extension/subprocess/subprocess_test.go
@@ -183,7 +183,7 @@ func TestDeclarativeManifestDoesNotStartExecutable(t *testing.T) {
t.Fatal(err)
}
if _, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: "test-kernel", Core: core.System(), Flow: standard.Definition(), Extensions: []control.Extension{extension},
+ KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{extension},
}); err != nil {
t.Fatal(err)
}
diff --git a/boatstack/flow/standard/completeness_test.go b/boatstack/flow/standard/completeness_test.go
index 2507fa0c..d98b44e1 100644
--- a/boatstack/flow/standard/completeness_test.go
+++ b/boatstack/flow/standard/completeness_test.go
@@ -250,7 +250,7 @@ func TestPackageImportsPreserveControlProgramDependencyDirection(t *testing.T) {
}
for _, forbidden := range forbiddenFlow {
if flowOwned && strings.Contains(value, forbidden) {
- t.Errorf("primary flow %s imports forbidden surface %s", relative, value)
+ t.Errorf("program runtime %s imports forbidden surface %s", relative, value)
}
}
}
diff --git a/boatstack/flow/standard/historical_test.go b/boatstack/flow/standard/historical_test.go
index f4c5b8b1..b51256dc 100644
--- a/boatstack/flow/standard/historical_test.go
+++ b/boatstack/flow/standard/historical_test.go
@@ -20,7 +20,7 @@ import (
)
func historicalGoalContracts() catalog.GoalContracts {
- manifest, err := standard.Definition().FlowManifest(context.Background())
+ manifest, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
panic(err)
}
diff --git a/boatstack/flow/standard/standard.go b/boatstack/flow/standard/standard.go
index a2df7ca6..5a0489cd 100644
--- a/boatstack/flow/standard/standard.go
+++ b/boatstack/flow/standard/standard.go
@@ -24,16 +24,16 @@ type definition struct{}
//go:embed transitions.json
var transitionDeclarations []byte
-func Definition() control.FlowDefinition { return definition{} }
+func Definition() control.ProgramRuntimeDefinition { return definition{} }
-func (definition) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+func (definition) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) {
transitions, err := decodeTransitions()
if err != nil {
- return control.PrimaryFlowManifest{}, err
+ return control.ProgramRuntimeManifest{}, err
}
resources, effects, verifiers, recoveries := declarations(transitions)
- return control.PrimaryFlowManifest{
- ID: ID, Version: Version, ProtocolVersion: control.FlowProtocolVersion, RuntimeMode: control.FlowRuntimeNative,
+ return control.ProgramRuntimeManifest{
+ ID: ID, Version: Version, ProtocolVersion: control.ProgramRuntimeProtocolVersion, RuntimeMode: control.ProgramRuntimeNative,
SupportedGoals: []control.GoalKind{
model.GoalApprovedPlan, model.GoalVerified, model.GoalOpenPR,
model.GoalMerged, model.GoalAbandoned,
diff --git a/boatstack/flow/standard/standard_test.go b/boatstack/flow/standard/standard_test.go
index 42fa4d3d..7c0c6d24 100644
--- a/boatstack/flow/standard/standard_test.go
+++ b/boatstack/flow/standard/standard_test.go
@@ -11,7 +11,7 @@ import (
func TestManifestOwnsOnlyStandardDeliverySemantics(t *testing.T) {
// control-law: standard-flow-declarations-live-outside-kernel-mechanism
- manifest, err := standard.Definition().FlowManifest(context.Background())
+ manifest, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
t.Fatal(err)
}
diff --git a/boatstack/flow/standard/supervisor_parity_test.go b/boatstack/flow/standard/supervisor_parity_test.go
index d1b4184d..c02cb45c 100644
--- a/boatstack/flow/standard/supervisor_parity_test.go
+++ b/boatstack/flow/standard/supervisor_parity_test.go
@@ -14,7 +14,7 @@ import (
)
func testGoalContracts() catalog.GoalContracts {
- manifest, err := standard.Definition().FlowManifest(context.Background())
+ manifest, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
panic(err)
}
@@ -225,7 +225,7 @@ func TestSelectionClassOutranksComponentLocalPriority(t *testing.T) {
transitions[index].SelectionClass = catalog.SelectionGoalRequired
transitions[index].Priority = 999
case "gate.test.record":
- transitions[index].SelectionClass = catalog.SelectionFlowProgress
+ transitions[index].SelectionClass = catalog.SelectionProgramProgress
transitions[index].Priority = 1
}
}
diff --git a/boatstack/flow/standard/transitions.json b/boatstack/flow/standard/transitions.json
index 2881cc44..ed622965 100644
--- a/boatstack/flow/standard/transitions.json
+++ b/boatstack/flow/standard/transitions.json
@@ -9,7 +9,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"OBSERVED",
@@ -220,7 +220,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"OBSERVED",
@@ -417,7 +417,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "authority",
"source_phases": [
"ACTIVE",
@@ -635,7 +635,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"OBSERVED",
@@ -1041,7 +1041,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "authority",
"source_phases": [
"ACTIVE",
@@ -1616,7 +1616,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"OBSERVED",
@@ -2034,7 +2034,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"OBSERVED",
@@ -3036,7 +3036,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_RECOVERY",
+ "selection_class": "PROGRAM_RECOVERY",
"class": "recovery",
"source_phases": [
"RECOVERY",
@@ -3201,7 +3201,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"ACTIVE"
@@ -3427,7 +3427,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"ACTIVE"
@@ -3654,7 +3654,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"ACTIVE"
@@ -4342,7 +4342,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"ACTIVE"
@@ -4952,7 +4952,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"ACTIVE"
@@ -5181,7 +5181,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-external",
"source_phases": [
"ACTIVE"
@@ -5410,7 +5410,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_PROGRESS",
+ "selection_class": "PROGRAM_PROGRESS",
"class": "owned-local",
"source_phases": [
"OBSERVED",
@@ -5625,7 +5625,7 @@
"manifest_fingerprint": ""
},
"owner": "",
- "selection_class": "FLOW_RECOVERY",
+ "selection_class": "PROGRAM_RECOVERY",
"class": "recovery",
"source_phases": [
"RECOVERY",
diff --git a/boatstack/internal/effects/extensions.go b/boatstack/internal/effects/extensions.go
index 30076a79..30c6dab9 100644
--- a/boatstack/internal/effects/extensions.go
+++ b/boatstack/internal/effects/extensions.go
@@ -19,11 +19,11 @@ func NewExtensionLocalPrepared(repositoryRoot, extensionID string, writes []cont
return newNamespacedLocalPrepared(repositoryRoot, filepath.Join("extensions", extensionID), extensionID, "extension", writes)
}
-// NewFlowLocalPrepared constrains a protocol-backed primary flow to its own
+// NewFlowLocalPrepared constrains a protocol-backed program runtime to its own
// repository-local namespace while retaining the normal reversible effect
// contract.
func NewFlowLocalPrepared(repositoryRoot, flowID string, writes []control.ResourceWrite) (ports.PreparedEffect, error) {
- return newNamespacedLocalPrepared(repositoryRoot, filepath.Join("flows", flowID), flowID, "primary flow", writes)
+ return newNamespacedLocalPrepared(repositoryRoot, filepath.Join("flows", flowID), flowID, "program runtime", writes)
}
func newNamespacedLocalPrepared(repositoryRoot, namespace, owner, kind string, writes []control.ResourceWrite) (ports.PreparedEffect, error) {
diff --git a/boatstack/internal/effects/integration_test.go b/boatstack/internal/effects/integration_test.go
index 57f903bd..ff079fe7 100644
--- a/boatstack/internal/effects/integration_test.go
+++ b/boatstack/internal/effects/integration_test.go
@@ -36,7 +36,7 @@ type fixedClock struct{ value time.Time }
const testProgramFingerprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
func testGoalContracts() catalog.GoalContracts {
- manifest, err := standard.Definition().FlowManifest(context.Background())
+ manifest, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
panic(err)
}
@@ -49,7 +49,7 @@ func testGoalContracts() catalog.GoalContracts {
func testProgram() control.ControlProgram {
program, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: boatstack.Version, Core: core.System(), Flow: standard.Definition(),
+ KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(),
})
if err != nil {
panic(err)
@@ -249,7 +249,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) {
repository := testRepository(t)
externalRoot := t.TempDir()
oldProgram, err := control.Compile(ctx, control.CompileRequest{
- KernelVersion: boatstack.Version, Core: core.System(), Flow: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()},
+ KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()},
})
if err != nil {
t.Fatal(err)
@@ -415,7 +415,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test
t.Fatal(err)
}
program, err := control.Compile(ctx, control.CompileRequest{
- KernelVersion: boatstack.Version, Core: core.System(), Flow: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()},
+ KernelVersion: boatstack.Version, Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()},
})
if err != nil {
t.Fatal(err)
@@ -516,7 +516,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test
CorrelationID: "extension-after", Goal: goal, Authority: authority(catalog.AuthorityRepository),
})
if err != nil || after.Decision == nil {
- t.Fatalf("verified extension did not return control to PrimaryFlow: %#v error=%v", after.Decision, err)
+ t.Fatalf("verified extension did not return control to ProgramRuntime: %#v error=%v", after.Decision, err)
}
if after.Decision.Transition != nil && after.Decision.Transition.Origin.Kind == catalog.OriginExtension {
t.Fatalf("verified extension remained selectable: %#v", after.Decision)
diff --git a/boatstack/internal/effects/recovery_test.go b/boatstack/internal/effects/recovery_test.go
index 66fe5f44..07a343e4 100644
--- a/boatstack/internal/effects/recovery_test.go
+++ b/boatstack/internal/effects/recovery_test.go
@@ -25,7 +25,7 @@ type recoveryClock struct{ value time.Time }
const testProgramFingerprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
func testGoalContracts() catalog.GoalContracts {
- manifest, err := standard.Definition().FlowManifest(context.Background())
+ manifest, err := standard.Definition().RuntimeManifest(context.Background())
if err != nil {
panic(err)
}
diff --git a/boatstack/internal/kernel/catalog/goal_contract.go b/boatstack/internal/kernel/catalog/goal_contract.go
index 6c413b4a..87d6d2d6 100644
--- a/boatstack/internal/kernel/catalog/goal_contract.go
+++ b/boatstack/internal/kernel/catalog/goal_contract.go
@@ -7,7 +7,7 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
)
-// GoalContract is the compiled terminal law supplied by the primary flow.
+// GoalContract is the compiled terminal law supplied by the program runtime.
// Extension conditions are conjunctive and therefore can only narrow the
// terminal set.
type GoalContract struct {
diff --git a/boatstack/internal/kernel/catalog/transition.go b/boatstack/internal/kernel/catalog/transition.go
index f229b1fa..31559b60 100644
--- a/boatstack/internal/kernel/catalog/transition.go
+++ b/boatstack/internal/kernel/catalog/transition.go
@@ -14,14 +14,14 @@ type EffectID string
type OriginKind string
const (
- OriginCoreSystem OriginKind = "core-system"
- OriginPrimaryFlow OriginKind = "primary-flow"
- OriginExtension OriginKind = "extension"
+ OriginCoreSystem OriginKind = "core-system"
+ OriginControlProgram OriginKind = "control-program"
+ OriginExtension OriginKind = "extension"
)
func (k OriginKind) Valid() bool {
switch k {
- case OriginCoreSystem, OriginPrimaryFlow, OriginExtension:
+ case OriginCoreSystem, OriginControlProgram, OriginExtension:
return true
default:
return false
@@ -39,18 +39,18 @@ type SelectionClass string
const (
SelectionSystemRecovery SelectionClass = "SYSTEM_RECOVERY"
- SelectionFlowRecovery SelectionClass = "FLOW_RECOVERY"
+ SelectionProgramRecovery SelectionClass = "PROGRAM_RECOVERY"
SelectionExtensionRecovery SelectionClass = "EXTENSION_RECOVERY"
SelectionGoalRequired SelectionClass = "GOAL_REQUIRED"
- SelectionFlowProgress SelectionClass = "FLOW_PROGRESS"
+ SelectionProgramProgress SelectionClass = "PROGRAM_PROGRESS"
SelectionExplicitOnly SelectionClass = "EXPLICIT_ONLY"
SelectionObservedExternal SelectionClass = "OBSERVED_EXTERNAL"
)
func (c SelectionClass) Valid() bool {
switch c {
- case SelectionSystemRecovery, SelectionFlowRecovery, SelectionExtensionRecovery, SelectionGoalRequired,
- SelectionFlowProgress, SelectionExplicitOnly, SelectionObservedExternal:
+ case SelectionSystemRecovery, SelectionProgramRecovery, SelectionExtensionRecovery, SelectionGoalRequired,
+ SelectionProgramProgress, SelectionExplicitOnly, SelectionObservedExternal:
return true
default:
return false
@@ -61,13 +61,13 @@ func (c SelectionClass) rank() int {
switch c {
case SelectionSystemRecovery:
return 1
- case SelectionFlowRecovery:
+ case SelectionProgramRecovery:
return 2
case SelectionExtensionRecovery:
return 3
case SelectionGoalRequired:
return 4
- case SelectionFlowProgress:
+ case SelectionProgramProgress:
return 5
case SelectionExplicitOnly:
return 6
@@ -280,10 +280,10 @@ func (t Transition) Controllable() bool { return t.Class.Controllable() }
// admissible from the same snapshot.
func (t Transition) ImplicitlySelectable() bool {
return t.SelectionClass == SelectionSystemRecovery ||
- t.SelectionClass == SelectionFlowRecovery ||
+ t.SelectionClass == SelectionProgramRecovery ||
t.SelectionClass == SelectionExtensionRecovery ||
t.SelectionClass == SelectionGoalRequired ||
- t.SelectionClass == SelectionFlowProgress
+ t.SelectionClass == SelectionProgramProgress
}
func (t Transition) SupportsGoal(goal model.Goal) bool {
@@ -347,7 +347,7 @@ type Registry struct {
managedOperation map[string]TransitionID
}
-var semanticID = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$`)
+var semanticID = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*(?:/[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*)?$`)
func New(transitions []Transition) (Registry, error) {
registry := Registry{
@@ -538,9 +538,9 @@ func validateSelectionOwnership(t Transition) error {
if t.Origin.Kind != OriginCoreSystem || t.Class != EventRecovery {
return fmt.Errorf("%s: SYSTEM_RECOVERY is reserved for CoreSystem recovery events", t.ID)
}
- case SelectionFlowRecovery:
- if t.Origin.Kind != OriginPrimaryFlow || t.Class != EventRecovery {
- return fmt.Errorf("%s: FLOW_RECOVERY is reserved for PrimaryFlow recovery events", t.ID)
+ case SelectionProgramRecovery:
+ if t.Origin.Kind != OriginControlProgram || t.Class != EventRecovery {
+ return fmt.Errorf("%s: PROGRAM_RECOVERY is reserved for ProgramRuntime recovery events", t.ID)
}
case SelectionExtensionRecovery:
if t.Origin.Kind != OriginExtension || t.Class != EventRecovery {
diff --git a/boatstack/internal/kernel/engine/engine_test.go b/boatstack/internal/kernel/engine/engine_test.go
index b0aa8470..4175eb40 100644
--- a/boatstack/internal/kernel/engine/engine_test.go
+++ b/boatstack/internal/kernel/engine/engine_test.go
@@ -170,7 +170,7 @@ func observation(phase model.ProtocolPhase, fingerprint string) model.Observatio
Publication: model.Known(model.PublicationNone, e), Verification: model.Known(model.VerificationUnverified, e), Recovery: model.Known(model.RecoveryNone, e),
Transaction: model.Known(model.TransactionNone, e), RecoveryInfo: model.Absent[model.RecoveryContext]("none", e), TransactionInfo: model.Absent[model.TransactionContext]("none", e),
Terminal: model.Known(model.TerminalNonterminal, e), Goal: model.Known(model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"}, e), ObservedAt: time.Unix(20, 0).UTC(),
- FlowFacts: map[string]model.Fact[string]{"test.synthetic.stage": model.Known(stage, e)},
+ ProgramFacts: map[string]model.Fact[string]{"test.synthetic.stage": model.Known(stage, e)},
}
}
@@ -212,7 +212,7 @@ func testRegistryWithAdvanceClass(t *testing.T, class catalog.EventClass) catalo
}
r, err := catalog.New([]catalog.Transition{{
ID: "test.advance", Version: 1, Class: class,
- Origin: catalog.TransitionOrigin{Kind: catalog.OriginPrimaryFlow, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: syntheticProgramFingerprint}, Owner: "test.synthetic", SelectionClass: catalog.SelectionFlowProgress,
+ Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: syntheticProgramFingerprint}, Owner: "test.synthetic", SelectionClass: catalog.SelectionProgramProgress,
SourcePhases: []model.ProtocolPhase{model.PhaseObserved}, TargetPhases: []model.ProtocolPhase{model.PhaseActive},
RequiredIdentity: identity, Authority: authority, RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"state"}, Effect: "test.advance", LocalEffects: localEffects, ExternalEffects: externalEffects, Idempotent: true,
Prescription: catalog.Prescription{Operation: "test.advance", ExpectedPostcondition: "active"}, SourcePredicate: "observed", AdmissionPredicate: "exact-admission", TargetPredicate: "active", Verifier: "fresh-active",
@@ -222,7 +222,7 @@ func testRegistryWithAdvanceClass(t *testing.T, class catalog.EventClass) catalo
PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "test", Priority: 1,
}, {
ID: "test.recover", Version: 1, Class: catalog.EventRecovery,
- Origin: catalog.TransitionOrigin{Kind: catalog.OriginPrimaryFlow, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: syntheticProgramFingerprint}, Owner: "test.synthetic", SelectionClass: catalog.SelectionFlowRecovery,
+ Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: syntheticProgramFingerprint}, Owner: "test.synthetic", SelectionClass: catalog.SelectionProgramRecovery,
SourcePhases: []model.ProtocolPhase{model.PhaseRecovery}, TargetPhases: []model.ProtocolPhase{model.PhaseFrontier},
RequiredIdentity: identity, Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"state"}, Effect: "test.recover", LocalEffects: []catalog.EffectID{"test.recover"}, Idempotent: true,
Prescription: catalog.Prescription{Operation: "test.recover", ExpectedPostcondition: "frontier"}, SourcePredicate: "recovery", AdmissionPredicate: "exact-recovery-admission", TargetPredicate: "frontier", Verifier: "fresh-frontier",
@@ -366,7 +366,7 @@ func TestApplyCrossesAdmissionEffectVerificationAndReceiptBoundary(t *testing.T)
}
func TestSyntheticStartVerifyTerminalContractNeedsNoStandardFlowFacet(t *testing.T) {
- // control-law: kernel-terminal-is-defined-only-by-the-compiled-primary-flow-contract
+ // control-law: kernel-terminal-is-defined-only-by-the-compiled-control-program-contract
goal := model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"}
source, err := model.CanonicalizeForProgram(observation(model.PhaseObserved, "source"), syntheticProgramFingerprint)
if err != nil {
diff --git a/boatstack/internal/kernel/model/facet.go b/boatstack/internal/kernel/model/facet.go
index cad16b15..6b502b54 100644
--- a/boatstack/internal/kernel/model/facet.go
+++ b/boatstack/internal/kernel/model/facet.go
@@ -105,7 +105,7 @@ func (s Snapshot) Facet(name FacetName) (FactStatus, string, bool) {
value := s.Goal.Value
return s.Goal.Status, strings.Join([]string{value.ID, string(value.Kind), value.DeliveryID, value.EvidenceFingerprint, fmt.Sprint(value.FrontierIsStop)}, "|"), true
default:
- if fact, ok := s.FlowFacts[string(name)]; ok {
+ if fact, ok := s.ProgramFacts[string(name)]; ok {
return fact.Status, fact.Value, true
}
fact, ok := s.ExtensionFacts[string(name)]
diff --git a/boatstack/internal/kernel/model/state.go b/boatstack/internal/kernel/model/state.go
index 75810e0e..4f503056 100644
--- a/boatstack/internal/kernel/model/state.go
+++ b/boatstack/internal/kernel/model/state.go
@@ -409,7 +409,7 @@ type Observation struct {
TransactionInfo Fact[TransactionContext] `json:"transaction_info"`
Terminal Fact[TerminalStatus] `json:"terminal"`
Goal Fact[Goal] `json:"goal"`
- FlowFacts map[string]Fact[string] `json:"flow_facts,omitempty"`
+ ProgramFacts map[string]Fact[string] `json:"flow_facts,omitempty"`
ExtensionFacts map[string]Fact[string] `json:"extension_facts,omitempty"`
ObservedAt time.Time `json:"observed_at"`
}
@@ -553,11 +553,11 @@ func Canonicalize(observation Observation) (Snapshot, error) {
return Snapshot{}, fmt.Errorf("snapshot: invalid goal fact: %w", err)
}
}
- for id, fact := range observation.FlowFacts {
+ for id, fact := range observation.ProgramFacts {
if !FacetName(id).Valid() || controllingFacet(FacetName(id)) {
- return Snapshot{}, fmt.Errorf("snapshot: invalid primary-flow fact id %q", id)
+ return Snapshot{}, fmt.Errorf("snapshot: invalid control-program fact id %q", id)
}
- if err := fact.Validate("primary-flow fact " + id); err != nil {
+ if err := fact.Validate("control-program fact " + id); err != nil {
return Snapshot{}, err
}
}
@@ -605,9 +605,9 @@ func Canonicalize(observation Observation) (Snapshot, error) {
zeroEvidenceTimes(&projection.TransactionInfo)
zeroEvidenceTimes(&projection.Terminal)
zeroEvidenceTimes(&projection.Goal)
- for id, fact := range projection.FlowFacts {
+ for id, fact := range projection.ProgramFacts {
zeroEvidenceTimes(&fact)
- projection.FlowFacts[id] = fact
+ projection.ProgramFacts[id] = fact
}
for id, fact := range projection.ExtensionFacts {
zeroEvidenceTimes(&fact)
diff --git a/boatstack/internal/kernel/protocol/config.go b/boatstack/internal/kernel/protocol/config.go
index 259fee6a..bf38d3ba 100644
--- a/boatstack/internal/kernel/protocol/config.go
+++ b/boatstack/internal/kernel/protocol/config.go
@@ -32,7 +32,7 @@ type PolicySettings struct {
}
// SubprocessExtensionSettings is a repository-selected, checksum-bound
-// additive capability. It cannot select or replace the primary flow.
+// additive capability. It cannot select or replace the program runtime.
type SubprocessExtensionSettings struct {
ID string `json:"id"`
Version string `json:"version"`
diff --git a/boatstack/internal/kernel/supervisor/guard_test.go b/boatstack/internal/kernel/supervisor/guard_test.go
index d17e2568..adcb914d 100644
--- a/boatstack/internal/kernel/supervisor/guard_test.go
+++ b/boatstack/internal/kernel/supervisor/guard_test.go
@@ -10,7 +10,7 @@ import (
func TestManagedCommandRoutingComesOnlyFromCompiledProgram(t *testing.T) {
registry, err := catalog.New([]catalog.Transition{
syntheticManagedTransition("synthetic.publish", catalog.EventOwnedLocal, catalog.SelectionExplicitOnly, "synthetic.recover"),
- syntheticManagedTransition("synthetic.recover", catalog.EventRecovery, catalog.SelectionFlowRecovery, "synthetic.recover"),
+ syntheticManagedTransition("synthetic.recover", catalog.EventRecovery, catalog.SelectionProgramRecovery, "synthetic.recover"),
})
if err != nil {
t.Fatal(err)
@@ -40,7 +40,7 @@ func syntheticManagedTransition(id catalog.TransitionID, class catalog.EventClas
}
return catalog.Transition{
ID: id, Version: 1,
- Origin: catalog.TransitionOrigin{Kind: catalog.OriginPrimaryFlow, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: "manifest"},
+ Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: "manifest"},
Owner: "test.synthetic", SelectionClass: selection, Class: class,
SourcePhases: []model.ProtocolPhase{model.PhaseActive}, TargetPhases: []model.ProtocolPhase{model.PhaseActive},
GoalKinds: []model.GoalKind{model.GoalVerified}, RequiredIdentity: []string{"repository-id"},
diff --git a/boatstack/internal/surfaces/catalog_render.go b/boatstack/internal/surfaces/catalog_render.go
index 3f86b2eb..4acfa132 100644
--- a/boatstack/internal/surfaces/catalog_render.go
+++ b/boatstack/internal/surfaces/catalog_render.go
@@ -67,17 +67,17 @@ func RenderCatalogMermaid(transitions []catalog.Transition) string {
return renderCatalogMermaid(transitions, "%% Generated from the compiled ControlProgram registry by surfaces.RenderCatalogMermaid. Do not edit.\n")
}
-// RenderStandardFlowMermaid projects only the compiled primary-flow
+// RenderStandardFlowMermaid projects only the compiled control-program
// declarations. The owner filter is metadata from the same executable registry,
// not a second transition graph.
func RenderStandardFlowMermaid(transitions []catalog.Transition) string {
flow := make([]catalog.Transition, 0, len(transitions))
for _, transition := range transitions {
- if transition.Origin.Kind == catalog.OriginPrimaryFlow {
+ if transition.Origin.Kind == catalog.OriginControlProgram {
flow = append(flow, transition)
}
}
- return renderCatalogMermaid(flow, "%% Generated from compiled PrimaryFlow declarations by surfaces.RenderStandardFlowMermaid. Do not edit.\n")
+ return renderCatalogMermaid(flow, "%% Generated from compiled ProgramRuntime declarations by surfaces.RenderStandardFlowMermaid. Do not edit.\n")
}
func renderCatalogMermaid(transitions []catalog.Transition, header string) string {
diff --git a/boatstack/internal/surfaces/locus_render.go b/boatstack/internal/surfaces/locus_render.go
index 58293add..c3e72759 100644
--- a/boatstack/internal/surfaces/locus_render.go
+++ b/boatstack/internal/surfaces/locus_render.go
@@ -87,7 +87,7 @@ func renderCatalogLocus(transitions []catalog.Transition, safety bool) (string,
ID: "boatstack-v2-executable-catalog-liveness-v1",
Subject: "Finite stable-phase abstraction generated from the compiled Boatstack ControlProgram registry. It contains one event for every runtime entry and expands each declared source and target phase set. The 18-facet predicates, operating-system behavior, and external-provider truth remain executable evidence obligations rather than theorem assumptions.",
Evidence: []locusEvidence{
- {Path: "boatstack/control/control.go", Note: "Compiler combines exact CoreSystem, PrimaryFlow, extension, contract, and ownership declarations into one immutable runtime registry."},
+ {Path: "boatstack/control/control.go", Note: "Compiler combines exact CoreSystem, ProgramRuntime, extension, contract, and ownership declarations into one immutable runtime registry."},
{Path: "docs/architecture/boatstack-v2-transition-catalog.md", Note: "Generated readable projection from the same runtime registry."},
{Path: "boatstack/internal/kernel/protocol/admission.go", Note: "Exact admission, authority, parameter, source-revision, provider-request, expiry, and stale-snapshot checks."},
{Path: "boatstack/internal/kernel/engine/engine.go", Note: "Single apply path across lock, journal, effect, fresh observation, target predicate, receipt, and recovery."},
diff --git a/boatstack/internal/surfaces/protocol.go b/boatstack/internal/surfaces/protocol.go
index e9cde258..1003d091 100644
--- a/boatstack/internal/surfaces/protocol.go
+++ b/boatstack/internal/surfaces/protocol.go
@@ -91,11 +91,10 @@ type DoctorReport struct {
KernelVersion string `json:"kernel_version"`
CoreSystemID string `json:"core_system_id"`
CoreSystemVersion string `json:"core_system_version"`
- PrimaryFlowID string `json:"primary_flow_id"`
- PrimaryFlowVersion string `json:"primary_flow_version"`
- PrimaryFlowFingerprint string `json:"primary_flow_fingerprint"`
+ ProgramID string `json:"program_id"`
+ ProgramVersion string `json:"program_version"`
CoreTransitionCount int `json:"core_transition_count"`
- FlowTransitionCount int `json:"flow_transition_count"`
+ RuntimeTransitionCount int `json:"runtime_transition_count"`
ExtensionTransitionCount int `json:"extension_transition_count"`
TransitionCount int `json:"transition_count"`
EnabledExtensions []string `json:"enabled_extensions,omitempty"`
diff --git a/boatstack/internal/testprogram/standard.go b/boatstack/internal/testprogram/standard.go
index 4c248a5c..604ea241 100644
--- a/boatstack/internal/testprogram/standard.go
+++ b/boatstack/internal/testprogram/standard.go
@@ -17,7 +17,7 @@ func StandardRegistry() catalog.Registry {
program, err := control.Compile(context.Background(), control.CompileRequest{
KernelVersion: "test-kernel",
Core: core.System(),
- Flow: standard.Definition(),
+ Runtime: standard.Definition(),
})
if err != nil {
panic(err)
diff --git a/boatstack/kernel.go b/boatstack/kernel.go
index 45a0294c..bd3d3922 100644
--- a/boatstack/kernel.go
+++ b/boatstack/kernel.go
@@ -156,8 +156,8 @@ func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces.
}
report := surfaces.DoctorReport{
KernelVersion: summary.KernelVersion, CoreSystemID: summary.Core.ID, CoreSystemVersion: summary.Core.Version,
- PrimaryFlowID: summary.Flow.ID, PrimaryFlowVersion: summary.Flow.Version, PrimaryFlowFingerprint: summary.Flow.Fingerprint,
- CoreTransitionCount: summary.CoreTransitionCount, FlowTransitionCount: summary.FlowTransitionCount,
+ ProgramID: summary.ProgramID, ProgramVersion: summary.ProgramVersion,
+ CoreTransitionCount: summary.CoreTransitionCount, RuntimeTransitionCount: summary.RuntimeTransitionCount,
ExtensionTransitionCount: summary.ExtensionTransitionCount, TransitionCount: summary.TotalTransitionCount,
EnabledExtensions: extensionIDs, ProgramFingerprint: summary.ProgramFingerprint,
}
diff --git a/boatstack/kernel_test.go b/boatstack/kernel_test.go
index 7aaf25fd..5645ba15 100644
--- a/boatstack/kernel_test.go
+++ b/boatstack/kernel_test.go
@@ -11,7 +11,7 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/surfaces"
)
-func TestRecoverSurfaceConsumesCompiledRegistryInsteadOfFixedFlowIDs(t *testing.T) {
+func TestRecoverSurfaceConsumesCompiledRegistryInsteadOfFixedProgramIDs(t *testing.T) {
// control-law: recover-is-classified-by-the-compiled-program-not-a-surface-shadow-list
program, err := distribution.StandardProgram(context.Background())
if err != nil {
diff --git a/boatstack/program_effects.go b/boatstack/program_effects.go
index 1203bbbc..1422760e 100644
--- a/boatstack/program_effects.go
+++ b/boatstack/program_effects.go
@@ -23,46 +23,46 @@ func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Adm
if transition.Origin.Kind == catalog.OriginCoreSystem {
return d.base.Prepare(ctx, admission, transition)
}
- if transition.Origin.Kind == catalog.OriginPrimaryFlow {
- flow := d.program.Flow()
- if flow.Manifest.RuntimeMode == control.FlowRuntimeNative {
+ if transition.Origin.Kind == catalog.OriginControlProgram {
+ flow := d.program.ProgramRuntime()
+ if flow.Manifest.RuntimeMode == control.ProgramRuntimeNative {
return d.base.Prepare(ctx, admission, transition)
}
if flow.Runtime == nil {
- return nil, fmt.Errorf("primary-flow runtime %q is unavailable", flow.Identity.ID)
+ return nil, fmt.Errorf("control-program runtime %q is unavailable", flow.Identity.ID)
}
parameters, err := json.Marshal(admission.Parameters)
if err != nil {
return nil, err
}
- request := control.FlowRequest{
- ProtocolVersion: control.FlowProtocolVersion, FlowID: flow.Identity.ID, FlowVersion: flow.Identity.Version,
+ request := control.ProgramRuntimeRequest{
+ ProtocolVersion: control.ProgramRuntimeProtocolVersion, ProgramID: flow.Identity.ID, ProgramVersion: flow.Identity.Version,
ProgramFingerprint: admission.ProgramFingerprint, CorrelationID: admission.Invocation.Correlation,
RepositoryRoot: admission.Invocation.InvokingPath, TransitionID: transition.ID, Parameters: parameters, Settings: flow.Manifest.Settings,
}
if transition.Class == catalog.EventOwnedExternal {
return effects.NewExtensionExternalPrepared(func(executionContext context.Context) (ports.EffectResult, error) {
- request.Operation = control.FlowExecuteExternalOperation
- response, invokeErr := flow.Runtime.InvokeFlow(executionContext, request)
+ request.Operation = control.ProgramExecuteExternalOperation
+ response, invokeErr := flow.Runtime.InvokeProgram(executionContext, request)
if invokeErr != nil {
return ports.EffectResult{}, invokeErr
}
- if err := validateFlowResponse(flow, request.Operation, request.CorrelationID, response); err != nil {
+ if err := validateProgramRuntimeResponse(flow, request.Operation, request.CorrelationID, response); err != nil {
return ports.EffectResult{}, err
}
return decodeExtensionSettlement(flow.Identity.ID, response.ExternalResult)
})
}
- operation := control.FlowPlanLocalEffectOperation
+ operation := control.ProgramPlanLocalEffectOperation
if transition.Class == catalog.EventRecovery {
- operation = control.FlowRecoverOperation
+ operation = control.ProgramRecoverOperation
}
request.Operation = operation
- response, invokeErr := flow.Runtime.InvokeFlow(ctx, request)
+ response, invokeErr := flow.Runtime.InvokeProgram(ctx, request)
if invokeErr != nil {
return nil, invokeErr
}
- if err := validateFlowResponse(flow, operation, admission.Invocation.Correlation, response); err != nil {
+ if err := validateProgramRuntimeResponse(flow, operation, admission.Invocation.Correlation, response); err != nil {
return nil, err
}
if err := validateProgramWrites(d.program, transition, flow.Identity.ID, response.Writes); err != nil {
diff --git a/boatstack/program_observer.go b/boatstack/program_observer.go
index b7e62fcc..520f3ee5 100644
--- a/boatstack/program_observer.go
+++ b/boatstack/program_observer.go
@@ -34,52 +34,52 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR
if err != nil {
return model.Observation{}, err
}
- flow := o.program.Flow()
- if flow.Manifest.RuntimeMode == control.FlowRuntimeProtocol {
+ flow := o.program.ProgramRuntime()
+ if flow.Manifest.RuntimeMode == control.ProgramRuntimeProtocol {
if flow.Runtime == nil {
- return model.Observation{}, fmt.Errorf("primary flow %q observer is unavailable", flow.Identity.ID)
+ return model.Observation{}, fmt.Errorf("program runtime %q observer is unavailable", flow.Identity.ID)
}
projection := observation
projection.ProgramFingerprint = o.program.Fingerprint()
snapshot, encodeErr := json.Marshal(projection)
if encodeErr != nil {
- return model.Observation{}, fmt.Errorf("encode bounded primary-flow observation: %w", encodeErr)
+ return model.Observation{}, fmt.Errorf("encode bounded control-program observation: %w", encodeErr)
}
- response, invokeErr := flow.Runtime.InvokeFlow(ctx, control.FlowRequest{
- ProtocolVersion: control.FlowProtocolVersion, Operation: control.FlowObserveOperation,
- FlowID: flow.Identity.ID, FlowVersion: flow.Identity.Version,
+ response, invokeErr := flow.Runtime.InvokeProgram(ctx, control.ProgramRuntimeRequest{
+ ProtocolVersion: control.ProgramRuntimeProtocolVersion, Operation: control.ProgramObserveOperation,
+ ProgramID: flow.Identity.ID, ProgramVersion: flow.Identity.Version,
ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation,
RepositoryRoot: request.Invocation.InvokingPath, Snapshot: snapshot, Settings: flow.Manifest.Settings,
})
if invokeErr != nil {
- return model.Observation{}, fmt.Errorf("primary flow %q observation failed: %w", flow.Identity.ID, invokeErr)
+ return model.Observation{}, fmt.Errorf("program runtime %q observation failed: %w", flow.Identity.ID, invokeErr)
}
- if err := validateFlowResponse(flow, control.FlowObserveOperation, request.Invocation.Correlation, response); err != nil {
+ if err := validateProgramRuntimeResponse(flow, control.ProgramObserveOperation, request.Invocation.Correlation, response); err != nil {
return model.Observation{}, err
}
declared := make(map[string]bool, len(flow.Manifest.Facts))
for _, id := range flow.Manifest.Facts {
declared[id] = true
}
- observation.FlowFacts = make(map[string]model.Fact[string], len(declared))
+ observation.ProgramFacts = make(map[string]model.Fact[string], len(declared))
for _, fact := range response.Facts {
if !declared[fact.ID] {
- return model.Observation{}, fmt.Errorf("primary flow %q returned undeclared fact %q", flow.Identity.ID, fact.ID)
+ return model.Observation{}, fmt.Errorf("program runtime %q returned undeclared fact %q", flow.Identity.ID, fact.ID)
}
- if _, exists := observation.FlowFacts[fact.ID]; exists {
- return model.Observation{}, fmt.Errorf("primary-flow fact %q was returned more than once", fact.ID)
+ if _, exists := observation.ProgramFacts[fact.ID]; exists {
+ return model.Observation{}, fmt.Errorf("control-program fact %q was returned more than once", fact.ID)
}
if !fact.Status.Valid() || fact.Fingerprint == "" {
- return model.Observation{}, fmt.Errorf("primary flow %q returned invalid fact %q", flow.Identity.ID, fact.ID)
+ return model.Observation{}, fmt.Errorf("program runtime %q returned invalid fact %q", flow.Identity.ID, fact.ID)
}
- observation.FlowFacts[fact.ID] = model.Fact[string]{
+ observation.ProgramFacts[fact.ID] = model.Fact[string]{
Status: fact.Status, Value: fact.Value, Detail: fact.Detail,
- Evidence: []model.Evidence{{Source: "primary-flow:" + flow.Identity.ID, Fingerprint: fact.Fingerprint, ObservedAt: observation.ObservedAt}},
+ Evidence: []model.Evidence{{Source: "control-program:" + flow.Identity.ID, Fingerprint: fact.Fingerprint, ObservedAt: observation.ObservedAt}},
}
delete(declared, fact.ID)
}
if len(declared) != 0 {
- return model.Observation{}, fmt.Errorf("primary flow %q omitted required observed facts", flow.Identity.ID)
+ return model.Observation{}, fmt.Errorf("program runtime %q omitted required observed facts", flow.Identity.ID)
}
}
// Extension observers receive the same core-plus-flow projection. Facts
@@ -147,25 +147,25 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR
}
if request.VerifyTransitionID != "" {
transition, ok := o.program.RuntimeRegistry().Lookup(request.VerifyTransitionID)
- if ok && transition.Origin.Kind == catalog.OriginPrimaryFlow && flow.Manifest.RuntimeMode == control.FlowRuntimeProtocol {
+ if ok && transition.Origin.Kind == catalog.OriginControlProgram && flow.Manifest.RuntimeMode == control.ProgramRuntimeProtocol {
snapshot, encodeErr := json.Marshal(observation)
if encodeErr != nil {
return model.Observation{}, encodeErr
}
- response, invokeErr := flow.Runtime.InvokeFlow(ctx, control.FlowRequest{
- ProtocolVersion: control.FlowProtocolVersion, Operation: control.FlowVerifyOperation,
- FlowID: flow.Identity.ID, FlowVersion: flow.Identity.Version,
+ response, invokeErr := flow.Runtime.InvokeProgram(ctx, control.ProgramRuntimeRequest{
+ ProtocolVersion: control.ProgramRuntimeProtocolVersion, Operation: control.ProgramVerifyOperation,
+ ProgramID: flow.Identity.ID, ProgramVersion: flow.Identity.Version,
ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation,
RepositoryRoot: request.Invocation.InvokingPath, TransitionID: transition.ID, Snapshot: snapshot, Settings: flow.Manifest.Settings,
})
if invokeErr != nil {
return model.Observation{}, invokeErr
}
- if err := validateFlowResponse(flow, control.FlowVerifyOperation, request.Invocation.Correlation, response); err != nil {
+ if err := validateProgramRuntimeResponse(flow, control.ProgramVerifyOperation, request.Invocation.Correlation, response); err != nil {
return model.Observation{}, err
}
if response.Verified == nil || !*response.Verified {
- return model.Observation{}, fmt.Errorf("primary-flow verifier %q rejected the postcondition", transition.Verifier)
+ return model.Observation{}, fmt.Errorf("control-program verifier %q rejected the postcondition", transition.Verifier)
}
}
if ok && transition.Origin.Kind == catalog.OriginExtension {
@@ -197,16 +197,16 @@ func (o programObserver) Observe(ctx context.Context, request ports.ObservationR
return observation, nil
}
-func validateFlowResponse(flow control.CompiledFlow, operation control.FlowOperation, correlation string, response control.FlowResponse) error {
- if response.ProtocolVersion != control.FlowProtocolVersion || response.Operation != operation ||
- response.FlowID != flow.Identity.ID || response.FlowVersion != flow.Identity.Version || response.CorrelationID != correlation {
- return fmt.Errorf("primary flow %q returned a mismatched protocol response", flow.Identity.ID)
+func validateProgramRuntimeResponse(flow control.CompiledProgramRuntime, operation control.ProgramRuntimeOperation, correlation string, response control.ProgramRuntimeResponse) error {
+ if response.ProtocolVersion != control.ProgramRuntimeProtocolVersion || response.Operation != operation ||
+ response.ProgramID != flow.Identity.ID || response.ProgramVersion != flow.Identity.Version || response.CorrelationID != correlation {
+ return fmt.Errorf("program runtime %q returned a mismatched protocol response", flow.Identity.ID)
}
- if err := control.ValidateFlowOperationResponse(operation, response); err != nil {
- return fmt.Errorf("primary flow %q returned an invalid operation response: %w", flow.Identity.ID, err)
+ if err := control.ValidateProgramRuntimeOperationResponse(operation, response); err != nil {
+ return fmt.Errorf("program runtime %q returned an invalid operation response: %w", flow.Identity.ID, err)
}
if response.ErrorClass != "" || response.Error != "" {
- return ComponentRuntimeError{Component: fmt.Sprintf("primary flow %q", flow.Identity.ID), Operation: string(operation), Class: response.ErrorClass, Message: response.Error}
+ return ComponentRuntimeError{Component: fmt.Sprintf("program runtime %q", flow.Identity.ID), Operation: string(operation), Class: response.ErrorClass, Message: response.Error}
}
return nil
}
diff --git a/boatstack/program_observer_test.go b/boatstack/program_observer_test.go
index 75e14615..2fdefb63 100644
--- a/boatstack/program_observer_test.go
+++ b/boatstack/program_observer_test.go
@@ -82,7 +82,7 @@ func TestExecutableExtensionObservationWaitsForVerifiedProgramBinding(t *testing
var sawFact bool
extension := isolatedObservationExtension{id: "example.external", sawFact: &sawFact, executable: true, calls: &calls}
program, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: "test-kernel", Core: core.System(), Flow: standard.Definition(), Extensions: []control.Extension{extension},
+ KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(), Extensions: []control.Extension{extension},
})
if err != nil {
t.Fatal(err)
@@ -117,7 +117,7 @@ func TestExtensionObserversConsumeOneOrderIndependentProjection(t *testing.T) {
alpha := isolatedObservationExtension{id: "example.alpha", forbid: "example.beta.fact", sawFact: &alphaSawBeta}
beta := isolatedObservationExtension{id: "example.beta", forbid: "example.alpha.fact", sawFact: &betaSawAlpha}
program, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: "test-kernel", Core: core.System(), Flow: standard.Definition(),
+ KernelVersion: "test-kernel", Core: core.System(), Runtime: standard.Definition(),
Extensions: []control.Extension{beta, alpha},
})
if err != nil {
diff --git a/boatstack/sdk/sdk.go b/boatstack/sdk/sdk.go
index 8c3150bf..42abaa19 100644
--- a/boatstack/sdk/sdk.go
+++ b/boatstack/sdk/sdk.go
@@ -82,21 +82,21 @@ const (
const HostIdentity = "sdk"
type options struct {
- flow control.FlowDefinition
+ runtime control.ProgramRuntimeDefinition
extensions []control.Extension
}
type Option func(*options) error
-func WithFlow(flow control.FlowDefinition) Option {
+func WithProgramRuntime(runtime control.ProgramRuntimeDefinition) Option {
return func(configuration *options) error {
- if flow == nil {
- return fmt.Errorf("SDK flow cannot be nil")
+ if runtime == nil {
+ return fmt.Errorf("SDK program runtime cannot be nil")
}
- if configuration.flow != nil {
- return fmt.Errorf("SDK accepts exactly one PrimaryFlow")
+ if configuration.runtime != nil {
+ return fmt.Errorf("SDK accepts exactly one ProgramRuntime")
}
- configuration.flow = flow
+ configuration.runtime = runtime
return nil
}
}
@@ -117,7 +117,7 @@ func WithExtension(extension control.Extension) Option {
type Client struct {
externalStateRoot string
standard bool
- flow control.FlowDefinition
+ runtime control.ProgramRuntimeDefinition
extensions []control.Extension
}
@@ -128,7 +128,7 @@ func New(externalStateRoot string, supplied ...Option) (Client, error) {
if err != nil {
return Client{}, err
}
- if configuration.flow != nil {
+ if configuration.runtime != nil {
return Client{}, fmt.Errorf("sdk.New always uses StandardFlow; use sdk.NewKernel for an explicit flow")
}
if _, err := distribution.StandardProgram(context.Background(), configuration.extensions...); err != nil {
@@ -138,22 +138,22 @@ func New(externalStateRoot string, supplied ...Option) (Client, error) {
}
// NewKernel is the low-level composition API. It never inserts StandardFlow;
-// callers must supply exactly one WithFlow option.
+// callers must supply exactly one WithProgramRuntime option.
func NewKernel(externalStateRoot string, supplied ...Option) (Client, error) {
configuration, err := applyOptions(supplied)
if err != nil {
return Client{}, err
}
- if configuration.flow == nil {
- return Client{}, fmt.Errorf("sdk.NewKernel requires an explicit PrimaryFlow")
+ if configuration.runtime == nil {
+ return Client{}, fmt.Errorf("sdk.NewKernel requires an explicit ProgramRuntime")
}
if _, err := control.Compile(context.Background(), control.CompileRequest{
- KernelVersion: boatstack.Version, Core: core.System(), Flow: configuration.flow,
+ KernelVersion: boatstack.Version, Core: core.System(), Runtime: configuration.runtime,
Extensions: configuration.extensions,
}); err != nil {
return Client{}, err
}
- return Client{externalStateRoot: externalStateRoot, flow: configuration.flow, extensions: append([]control.Extension(nil), configuration.extensions...)}, nil
+ return Client{externalStateRoot: externalStateRoot, runtime: configuration.runtime, extensions: append([]control.Extension(nil), configuration.extensions...)}, nil
}
func applyOptions(supplied []Option) (options, error) {
@@ -193,7 +193,7 @@ func (c Client) Do(ctx context.Context, request Request) (Response, error) {
extensions := append([]control.Extension(nil), c.extensions...)
extensions = append(extensions, configured...)
program, err = control.Compile(ctx, control.CompileRequest{
- KernelVersion: boatstack.Version, Core: core.System(), Flow: c.flow,
+ KernelVersion: boatstack.Version, Core: core.System(), Runtime: c.runtime,
Extensions: extensions, Settings: settings,
})
}
diff --git a/boatstack/sdk/sdk_test.go b/boatstack/sdk/sdk_test.go
index 15b41ca8..c64b29cc 100644
--- a/boatstack/sdk/sdk_test.go
+++ b/boatstack/sdk/sdk_test.go
@@ -23,28 +23,28 @@ func TestPublicProtocolCanBeConstructedWithoutInternalPackages(t *testing.T) {
}
}
-func TestLowLevelSDKRequiresAndAcceptsExactlyOneNonStandardPrimaryFlow(t *testing.T) {
+func TestLowLevelSDKRequiresAndAcceptsExactlyOneNonStandardProgramRuntime(t *testing.T) {
// control-law: low-level-sdk-never-inserts-or-multiplies-standard-flow
if _, err := sdk.NewKernel(""); err == nil {
- t.Fatal("low-level SDK accepted a missing PrimaryFlow")
+ t.Fatal("low-level SDK accepted a missing ProgramRuntime")
}
flow := syntheticFlow{}
- if _, err := sdk.NewKernel("", sdk.WithFlow(flow)); err != nil {
- t.Fatalf("synthetic PrimaryFlow was rejected: %v", err)
+ if _, err := sdk.NewKernel("", sdk.WithProgramRuntime(flow)); err != nil {
+ t.Fatalf("synthetic ProgramRuntime was rejected: %v", err)
}
- if _, err := sdk.NewKernel("", sdk.WithFlow(flow), sdk.WithFlow(flow)); err == nil {
- t.Fatal("low-level SDK accepted two PrimaryFlows")
+ if _, err := sdk.NewKernel("", sdk.WithProgramRuntime(flow), sdk.WithProgramRuntime(flow)); err == nil {
+ t.Fatal("low-level SDK accepted two ProgramRuntimes")
}
- if _, err := sdk.New("", sdk.WithFlow(flow)); err == nil {
+ if _, err := sdk.New("", sdk.WithProgramRuntime(flow)); err == nil {
t.Fatal("standard SDK allowed StandardFlow replacement")
}
}
type syntheticFlow struct{}
-func (syntheticFlow) FlowRuntime() control.FlowRuntime { return syntheticRuntime{} }
+func (syntheticFlow) ProgramRuntime() control.ProgramRuntime { return syntheticRuntime{} }
-func (syntheticFlow) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+func (syntheticFlow) RuntimeManifest(context.Context) (control.ProgramRuntimeManifest, error) {
const (
id = "synthetic.lifecycle"
fact = "synthetic.lifecycle.stage"
@@ -54,7 +54,7 @@ func (syntheticFlow) FlowManifest(context.Context) (control.PrimaryFlowManifest,
effect := control.EffectID(string(id) + "-effect")
verifier := string(id) + "-verifier"
return control.Transition{
- ID: id, Version: 1, SelectionClass: control.SelectionFlowProgress, Class: control.EventOwnedLocal,
+ ID: id, Version: 1, SelectionClass: control.SelectionProgramProgress, Class: control.EventOwnedLocal,
SourcePhases: []control.ProtocolPhase{control.PhaseObserved, control.PhaseActive}, TargetPhases: []control.ProtocolPhase{control.PhaseObserved, control.PhaseActive},
GoalKinds: []control.GoalKind{control.GoalVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"},
Authority: []control.AuthorityClass{control.AuthorityRepository}, RequiredEvidence: []string{"snapshot", "goal", "facet:" + fact},
@@ -74,8 +74,8 @@ func (syntheticFlow) FlowManifest(context.Context) (control.PrimaryFlowManifest,
}
verify := transition("synthetic.lifecycle.verify", "start", "verify", 1)
finish := transition("synthetic.lifecycle.finish", "verify", "terminal", 2)
- return control.PrimaryFlowManifest{
- ID: id, Version: "1.0.0", ProtocolVersion: control.FlowProtocolVersion, RuntimeMode: control.FlowRuntimeProtocol,
+ return control.ProgramRuntimeManifest{
+ ID: id, Version: "1.0.0", ProtocolVersion: control.ProgramRuntimeProtocolVersion, RuntimeMode: control.ProgramRuntimeProtocol,
SupportedGoals: []control.GoalKind{control.GoalVerified},
GoalContracts: []control.GoalContract{{GoalKind: control.GoalVerified, Conditions: []control.FacetCondition{control.KnownCondition(control.FacetName(fact), "terminal")}}},
Transitions: []control.Transition{verify, finish}, Facts: []string{fact}, OwnedResources: []string{resource},
@@ -87,9 +87,9 @@ func (syntheticFlow) FlowManifest(context.Context) (control.PrimaryFlowManifest,
type syntheticRuntime struct{}
-func (syntheticRuntime) InvokeFlow(_ context.Context, request control.FlowRequest) (control.FlowResponse, error) {
- return control.FlowResponse{
- ProtocolVersion: control.FlowProtocolVersion, Operation: request.Operation,
- FlowID: request.FlowID, FlowVersion: request.FlowVersion, CorrelationID: request.CorrelationID,
+func (syntheticRuntime) InvokeProgram(_ context.Context, request control.ProgramRuntimeRequest) (control.ProgramRuntimeResponse, error) {
+ return control.ProgramRuntimeResponse{
+ ProtocolVersion: control.ProgramRuntimeProtocolVersion, Operation: request.Operation,
+ ProgramID: request.ProgramID, ProgramVersion: request.ProgramVersion, CorrelationID: request.CorrelationID,
}, nil
}
diff --git a/docs/architecture/boatstack-standard-flow.mmd b/docs/architecture/boatstack-standard-flow.mmd
index bb0f5ee0..8efbcb37 100644
--- a/docs/architecture/boatstack-standard-flow.mmd
+++ b/docs/architecture/boatstack-standard-flow.mmd
@@ -1,4 +1,4 @@
-%% Generated from compiled PrimaryFlow declarations by surfaces.RenderStandardFlowMermaid. Do not edit.
+%% Generated from compiled ProgramRuntime declarations by surfaces.RenderStandardFlowMermaid. Do not edit.
flowchart TB
subgraph phases["protocol phases"]
p_ABANDONED["ABANDONED"]
diff --git a/docs/architecture/boatstack-v2-kernel.md b/docs/architecture/boatstack-v2-kernel.md
index b689709f..4f2e30d2 100644
--- a/docs/architecture/boatstack-v2-kernel.md
+++ b/docs/architecture/boatstack-v2-kernel.md
@@ -23,7 +23,7 @@ slices. They are logical ownership boundaries, not rollout phases.
| Slice | Domain | Structure | Goal | Operator | Immediate value |
| --- | --- | --- | --- | --- | --- |
-| 1. Compiled control law | Repository-local delivery control | CoreSystem plus one PrimaryFlow and zero or more conservative Extensions compiled into one immutable ControlProgram | Every managed state has a safe path to progress, recovery, authority frontier, or terminal | Compile, observe, resolve, admit, execute, verify, record, recover | Delivery policy can evolve without changing the mechanism that protects authority and effects |
+| 1. Compiled control law | Repository-local delivery control | CoreSystem plus one ProgramRuntime and zero or more conservative Extensions compiled into one immutable ControlProgram | Every managed state has a safe path to progress, recovery, authority frontier, or terminal | Compile, observe, resolve, admit, execute, verify, record, recover | Delivery policy can evolve without changing the mechanism that protects authority and effects |
| 2. Product surfaces | Shipped CLI, hooks, SDK/MCP, hosts, and renderers | One adapter protocol projected from Kernel decisions and prescriptions | Every consumer observes and requests the same compiled semantics | Assemble, decode, invoke, render | Hosts stop acting as independent controllers while useful workflows remain available |
Canonical form for slice 1: one domain, the `ControlProgram` and `Snapshot`
@@ -61,7 +61,7 @@ with a first-party standard delivery flow. Its dependency direction is:
kernel contracts
^
|-- CoreSystem
- |-- one PrimaryFlow
+ |-- one ProgramRuntime
`-- zero or more Extensions
^
|
@@ -74,7 +74,7 @@ kernel contracts
The application assembles one immutable program before resolution:
```text
-CoreSystem + PrimaryFlow + Extensions + RepositoryPolicy
+CoreSystem + ProgramRuntime + Extensions + RepositoryPolicy
-> Compile
-> ControlProgram
-> Kernel
@@ -87,16 +87,16 @@ CoreSystem + PrimaryFlow + Extensions + RepositoryPolicy
compiled program and owns observation orchestration, canonicalization,
resolution, admission, effect routing, postcondition verification,
journaling, receipts, replay, recovery, and drift refusal. It imports no
- primary flow, extension implementation, CLI, SDK wrapper, or host renderer.
+ program runtime, extension implementation, CLI, SDK wrapper, or host renderer.
- **CoreSystem** declares Boatstack operational capabilities: invocation and
repository identity, engagement, runtime, configuration, installation,
generic goal identity, transactions, recovery, process events, and external
observations.
-- **PrimaryFlow** is exactly one trusted in-process delivery law. It declares
+- **ProgramRuntime** is one trusted in-process execution binding. It declares
goal contracts, facts, transitions, resources, effects, verifiers, recovery,
policy projection, and telemetry. The application selects it; repository
configuration cannot select an arbitrary executable flow.
-- **StandardFlow** is the first-party primary flow preserving the familiar
+- **StandardFlow** is the first-party complete Control Program preserving the familiar
plan, approval, workspace, gate, evidence, publication, correction, and
abandonment behavior.
- **Extensions** are additive. In-process extensions are trusted compiled Go
@@ -110,7 +110,7 @@ CoreSystem + PrimaryFlow + Extensions + RepositoryPolicy
### ControlProgram
-`Compile` consumes an explicit CoreSystem definition, one PrimaryFlow manifest,
+`Compile` consumes an explicit CoreSystem definition, one ProgramRuntime manifest,
zero or more extension manifests, and canonical program-affecting settings. It
rejects missing or multiple flows, ID collisions, unnamespaced extension IDs,
overlapping mutable-resource ownership, undeclared effects or verifiers,
@@ -125,8 +125,8 @@ flow, extension, terminal, or verification shadow graph.
The stable Go authoring and construction boundaries are:
```go
-type FlowDefinition interface {
- FlowManifest(context.Context) (PrimaryFlowManifest, error)
+type ProgramRuntimeDefinition interface {
+ RuntimeManifest(context.Context) (ProgramRuntimeManifest, error)
}
type Extension interface {
@@ -137,19 +137,19 @@ func Compile(context.Context, CompileRequest) (ControlProgram, error)
func NewKernel(externalStateRoot string, program control.ControlProgram) (Kernel, error)
```
-Primary-flow runtime adapters are trusted in-process implementations of
-`FlowRuntime`; this release has no subprocess primary-flow loader. The bounded
+Program runtime adapters are trusted in-process implementations of
+`ProgramRuntime`; repository Control Programs use the strict public loader. The bounded
request/response contract gives custom flows immutable projections rather than
a mutable Kernel object. Every operation has an exact tagged response payload,
and identity, version, correlation, error classification, and operation type
are checked at the Kernel boundary.
`sdk.New(...)` assembles CoreSystem plus StandardFlow and repository-scoped
-extensions. `sdk.NewKernel(..., sdk.WithFlow(flow), sdk.WithExtension(...))`
-requires exactly one explicit primary flow and never inserts StandardFlow.
+extensions. `sdk.NewKernel(..., sdk.WithProgramRuntime(runtime), sdk.WithExtension(...))`
+requires exactly one explicit program runtime and never inserts StandardFlow.
The fingerprint covers the Kernel version; CoreSystem ID, version, manifest,
-and transitions; PrimaryFlow ID, version, manifest, goal contracts, and
+and transitions; ProgramRuntime ID, version, manifest, goal contracts, and
transitions; extension manifests, versions, executable SHA-256 values,
settings, goal constraints, and transitions; the compiled transition registry;
resource ownership; verifier and recovery declarations; and canonical
@@ -180,17 +180,17 @@ verification facts without taking ownership of that boundary.
### Selection and terminal contracts
Every transition records its origin, owner, manifest fingerprint, and bounded
-selection class: `SYSTEM_RECOVERY`, `FLOW_RECOVERY`, `EXTENSION_RECOVERY`,
-`GOAL_REQUIRED`, `FLOW_PROGRESS`, `EXPLICIT_ONLY`, or `OBSERVED_EXTERNAL`. Third-party
+selection class: `SYSTEM_RECOVERY`, `PROGRAM_RECOVERY`, `EXTENSION_RECOVERY`,
+`GOAL_REQUIRED`, `PROGRAM_PROGRESS`, `EXPLICIT_ONLY`, or `OBSERVED_EXTERNAL`. Third-party
extensions cannot supply raw numeric priority. An extension becomes implicitly
selectable only to discharge an active unmet extension obligation or its own
recovery contract.
-CoreSystem and PrimaryFlow declarations own their selection semantics; the
+CoreSystem and ProgramRuntime declarations own their selection semantics; the
compiler never infers ordering from a transition ID or family name. An omitted
extension selection is bounded to `EXPLICIT_ONLY`, or to
`EXTENSION_RECOVERY` for an explicitly declared extension recovery. A
-PrimaryFlow recovery manifest lists only recovery transitions owned by that
+ProgramRuntime recovery manifest lists only recovery transitions owned by that
flow; cross-component interruption references are resolved only after the one
compiled registry exists.
@@ -199,7 +199,7 @@ semantic managed operation, and the compiled registry maps that operation to a
transition through `PolicyContract.ManagedOperations`. A custom program that
does not claim an operation does not inherit StandardFlow transition IDs.
-The five software-delivery goal kinds remain closed. The PrimaryFlow supplies
+The five software-delivery goal kinds remain closed. The ProgramRuntime supplies
the base terminal contract. Extension obligations are conjoined with that
contract, so for the same base state:
@@ -213,13 +213,13 @@ cannot report terminal state directly.
### Observation and effects
Observation is layered in deterministic owner and ID order: core observation,
-PrimaryFlow observation, then extension observations. Owners receive bounded
+ProgramRuntime observation, then extension observations. Owners receive bounded
immutable projections. Required observer failure remains explicit unresolved,
blocked, or recovery evidence; it never disappears or becomes false. Snapshot
identity includes all controlling core, flow, and extension facts plus the
program fingerprint.
-PrimaryFlow and extension responses are validated as exact operation-specific
+ProgramRuntime and extension responses are validated as exact operation-specific
unions before their facts, writes, external settlement, or verifier result can
be interpreted. Classified errors cannot carry success payloads. Subprocess
extensions additionally use strict JSON with no unknown fields or trailing
@@ -235,7 +235,7 @@ admission, journal, verification, recovery, and Kernel-written receipt path.
The default SDK and CLI explicitly assemble `CoreSystem + StandardFlow +
configured extensions`; users acquire no new configuration burden. A low-level
-SDK constructor requires an explicit PrimaryFlow. A custom application can
+SDK constructor requires an explicit ProgramRuntime. A custom application can
assemble `CoreSystem + another trusted flow + selected extensions` without
forking Kernel and without parsing CLI output.
@@ -243,7 +243,7 @@ forking Kernel and without parsing CLI output.
standardClient, err := sdk.New(stateRoot, sdk.WithExtension(extension))
customClient, err := sdk.NewKernel(
stateRoot,
- sdk.WithFlow(primaryFlow),
+ sdk.WithProgramRuntime(programRuntime),
sdk.WithExtension(extension),
)
```
@@ -523,7 +523,7 @@ The checked [catalog table](boatstack-v2-transition-catalog.md) and
[Mermaid graph](boatstack-v2-transition-catalog.mmd) are deterministic
projections of this registry. Golden tests reject either artifact when it drifts.
The checked [StandardFlow graph](boatstack-standard-flow.mmd) filters that same
-compiled registry by primary-flow origin and contains exactly 30 transitions;
+compiled registry by control-program origin and contains exactly 30 transitions;
it is not an independently maintained graph.
## 8. Supervisory control law
@@ -693,7 +693,7 @@ Dependencies point downward in this table and are acyclic.
| Package | Owns | Public boundary and verifier | Allowed dependencies | Forbidden dependencies |
| --- | --- | --- | --- | --- |
| `internal/kernel/model` | typed facts, identity, snapshot, goal, fingerprints | constructors/canonical encoding; schema and invariant tests | standard library | plant, effects, surfaces, facade |
-| `control` | stable CoreSystem, PrimaryFlow, Extension, and immutable ControlProgram compiler contracts | strict manifests, conservative extension compilation, fingerprints, ownership map | kernel contracts | concrete distribution or surfaces |
+| `control` | stable CoreSystem, ProgramRuntime, Extension, and immutable ControlProgram compiler contracts | strict manifests, conservative extension compilation, fingerprints, ownership map | kernel contracts | concrete distribution or surfaces |
| `core` | 32 operational-capability transition declarations | embedded strict declaration bytes through `CoreManifest` | control contracts | StandardFlow, extensions, surfaces |
| `flow/standard` | 30 first-party delivery transitions and five base goal contracts | `standard.Definition()` plus default-flow parity, historical, ownership, and completeness tests | control contracts and model vocabulary | Kernel mechanism, CLI, host rendering, SDK |
| `extension/*` | additive in-process and checksum-bound subprocess capabilities | strict extension manifests and bounded runtime protocol | control contracts | Kernel state, admissions, receipts, foreign resources |
diff --git a/docs/architecture/boatstack-v2-locus-liveness.json b/docs/architecture/boatstack-v2-locus-liveness.json
index 6db60553..989b7acb 100644
--- a/docs/architecture/boatstack-v2-locus-liveness.json
+++ b/docs/architecture/boatstack-v2-locus-liveness.json
@@ -5,7 +5,7 @@
"evidence": [
{
"path": "boatstack/control/control.go",
- "note": "Compiler combines exact CoreSystem, PrimaryFlow, extension, contract, and ownership declarations into one immutable runtime registry."
+ "note": "Compiler combines exact CoreSystem, ProgramRuntime, extension, contract, and ownership declarations into one immutable runtime registry."
},
{
"path": "docs/architecture/boatstack-v2-transition-catalog.md",
diff --git a/docs/architecture/boatstack-v2-locus-safety.json b/docs/architecture/boatstack-v2-locus-safety.json
index 950254f5..04b18d1e 100644
--- a/docs/architecture/boatstack-v2-locus-safety.json
+++ b/docs/architecture/boatstack-v2-locus-safety.json
@@ -5,7 +5,7 @@
"evidence": [
{
"path": "boatstack/control/control.go",
- "note": "Compiler combines exact CoreSystem, PrimaryFlow, extension, contract, and ownership declarations into one immutable runtime registry."
+ "note": "Compiler combines exact CoreSystem, ProgramRuntime, extension, contract, and ownership declarations into one immutable runtime registry."
},
{
"path": "docs/architecture/boatstack-v2-transition-catalog.md",
diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md
index 356a2884..444d106f 100644
--- a/docs/architecture/boatstack-v2-transition-catalog.md
+++ b/docs/architecture/boatstack-v2-transition-catalog.md
@@ -11,12 +11,12 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w
| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` |
| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` |
| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `delivery.slice.advance` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` |
+| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` |
| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | GOAL_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` |
| `engagement.release` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` |
| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` |
-| `evidence.approval.revoke` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` |
-| `evidence.visual.attach` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` |
+| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` |
+| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` |
| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.branch-changed` | `-` | `declared-neutral` |
| `external.ci-completed` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.ci-completed` | `-` | `declared-neutral` |
| `external.configuration-drifted` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` |
@@ -30,30 +30,30 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w
| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-updated` | `-` | `declared-neutral` |
| `external.provider-unavailable` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.provider-unavailable` | `-` | `declared-neutral` |
| `external.runtime-disappeared` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` |
-| `gate.build.record` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` |
-| `gate.change.record` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` |
-| `gate.journey.record` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` |
-| `gate.review.record` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` |
-| `gate.test.record` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` |
+| `gate.build.record` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` |
+| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` |
+| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` |
+| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` |
+| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` |
| `goal.configure` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `goal_kind*`, `delivery_id*` | `goal` | `verifier:fresh-observation:goal.configure` | `recovery.resume` | `declared-neutral` |
| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | GOAL_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` |
| `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` |
| `installation.update` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` |
| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` |
-| `plan.abandon` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` |
-| `plan.activate` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` |
-| `plan.amend` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` |
-| `plan.approve` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` |
-| `plan.approve-amendment` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` |
-| `plan.create` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` |
-| `plan.invalidate` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` |
-| `plan.validate` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` |
-| `publication.abandon` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` |
-| `publication.correct` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` |
-| `publication.execute` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` |
-| `publication.observe` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` |
-| `publication.preview` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` |
-| `publication.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` |
+| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` |
+| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` |
+| `plan.approve` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` |
+| `plan.approve-amendment` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` |
+| `plan.create` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` |
+| `plan.invalidate` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` |
+| `plan.validate` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` |
+| `publication.abandon` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` |
+| `publication.correct` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` |
+| `publication.execute` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` |
+| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` |
+| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` |
+| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` |
| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` |
| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` |
| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` |
@@ -62,13 +62,13 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w
| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` |
| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` |
| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`cd8c2e6872499ca7276f580c88206dd0f0882bb26051bee667e90419c723f834` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` |
-| `workspace.abandon` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` |
-| `workspace.activate` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` |
-| `workspace.cleanup` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` |
-| `workspace.cut` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` |
-| `workspace.publish` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` |
-| `workspace.reap` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` |
-| `workspace.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `workspace.sync` | primary-flow:`boatstack.standard@1.0.0`
`59e4d6078d2745d6885b5ccc4b155ebbf069332ce7474d6ba4fbd2b5bbbd0310` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` |
+| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` |
+| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` |
+| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` |
+| `workspace.cut` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` |
+| `workspace.publish` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` |
+| `workspace.reap` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` |
+| `workspace.reconcile` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `workspace.sync` | control-program:`boatstack.standard@1.0.0`
`537f5bd4ad3dbef327f6525d7d3c54bae3d0bb42ac0afe1619f580891a5e9207` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` |
`*` marks a required parameter. OR authority is shown with `/`; mandatory authority clauses are shown with `AND`. Source and target facet predicates remain in the canonical JSON returned by `boatstack catalog --format json`.
diff --git a/docs/architecture/control-program-abi.md b/docs/architecture/control-program-abi.md
new file mode 100644
index 00000000..8b4c5dc4
--- /dev/null
+++ b/docs/architecture/control-program-abi.md
@@ -0,0 +1,71 @@
+# Control Program ABI
+
+A Flow is the product-facing name for one complete Control Program. The
+runtime validates that complete program before it constructs a registry or
+resolves a transition.
+
+```text
+repository source
+ -> strict parse
+ -> structural and semantic validation
+ -> typed normalization
+ -> canonical executable representation
+ -> SHA-256 program fingerprint
+ -> runtime compatibility
+ -> Kernel
+```
+
+## Manifest
+
+| Field | Class | Canonical rule |
+| --- | --- | --- |
+| `schema_version` | compatibility | Must equal the supported positive ABI version; excluded from the executable fingerprint. |
+| `program_id` | identity | Lowercase semantic ID without `/`; included because it qualifies every transition ID. |
+| `program_version` | descriptive author identity | Non-empty deterministic token; excluded because changing it alone does not change executable semantics. |
+| `requires_runtime` | compatibility | Exact `>=MAJOR.MINOR.PATCH` minimum; checked before registry construction and excluded from the executable fingerprint. |
+| `capabilities` | executable semantics | Exact, duplicate-free sets of used effect and verifier IDs; sorted canonically. Declaration does not grant authority. |
+| `owned_resources` | executable semantics | Exact, duplicate-free set of resources written by transitions; sorted canonically. |
+| `goal_contracts` | executable semantics | Sorted by goal; conjunctive conditions and their set-valued members are sorted. |
+| `transitions` | executable semantics | Local declarations are normalized, program-qualified, validated, and sorted by complete ID for hashing. |
+
+`goal_contracts` and `owned_resources` are required beyond the tentative six
+fields because terminal resolution and effect ownership consume them directly.
+No repository state, runtime path, agent session, or granted authority belongs
+to this ABI.
+
+## Ordering
+
+Explicit `selection_class` and `priority` carry selection semantics. Source
+declaration order does not. Phase lists, goals, identities, authorities,
+evidence, resources, parameters, conditions, interruption points, managed
+operations, capabilities, and goal contracts are sets or name-keyed
+declarations and are normalized into canonical order. Prescription arguments
+retain source order because argument order is executable.
+
+Unknown fields, duplicate JSON keys, duplicate declarations, ambiguous IDs,
+and implicit aliases fail closed. The parser does not hash raw JSON, preserve
+whitespace, or depend on JSON object key order.
+
+## Identity and compatibility
+
+The canonical internal transition identity is:
+
+```text
+/
+```
+
+Both components reject `/`, so the mapping is injective. Recovery references
+are qualified through the same rule. Renaming `program_id` intentionally
+creates a different program fingerprint and different transition identities.
+
+The runtime returns `PROGRAM_SCHEMA_UNSUPPORTED` for a newer schema,
+`RUNTIME_TOO_OLD` when the verified runtime is below the minimum, and
+`PROGRAM_INVALID` for malformed or semantically incomplete input. None of
+those failures constructs a registry or reaches effects.
+
+The executable fingerprint excludes `program_version` and runtime
+compatibility because those are separate identities. It includes the complete
+normalized transition graph, exact goal contracts, capability bindings,
+resource ownership, and program-qualified identity. Thus representation-only
+changes remain stable while every kernel-observable control-law change changes
+the fingerprint.
diff --git a/docs/configuration.md b/docs/configuration.md
index bd372f4b..eb4856ff 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -52,7 +52,7 @@ high-risk derivation fails closed whenever that policy is active.
## Optional additive extensions
Repository configuration may enable checksum-bound subprocess extensions, but
-it cannot select or replace the trusted primary flow:
+it cannot select or replace the trusted program runtime:
```json
{
diff --git a/docs/generated-files.md b/docs/generated-files.md
index f95c810f..781fb7dd 100644
--- a/docs/generated-files.md
+++ b/docs/generated-files.md
@@ -46,7 +46,7 @@ architecture artifacts:
Repository and Go tests compare every checked byte with a fresh render and
require both Locus alphabets to equal all 63 executable catalog transitions.
The StandardFlow graph contains exactly the 30 transitions whose compiled
-origin is the primary flow.
+origin is the program runtime.
The Locus phase graph is intentionally conservative: it expands each declared
source phase against each declared target phase. Facet predicates and reducer
branches remain executable-test obligations.
diff --git a/release-notes/2026-08-11-control-program-abi.md b/release-notes/2026-08-11-control-program-abi.md
new file mode 100644
index 00000000..e3477a1f
--- /dev/null
+++ b/release-notes/2026-08-11-control-program-abi.md
@@ -0,0 +1,3 @@
+### Add a strict Control Program ABI
+
+Repositories can load complete user-facing Flows through a strict, compatibility-checked Control Program boundary with stable semantic fingerprints and program-qualified transition identities.