diff --git a/.gitattributes b/.gitattributes
index 16244fc..bb66a06 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,4 @@
docs/architecture/boatstack-v2-*.md text eol=lf
docs/architecture/boatstack-v2-*.mmd text eol=lf
docs/architecture/boatstack-v2-*.json text eol=lf
+docs/architecture/boatstack-standard-flow.mmd text eol=lf
diff --git a/.github/tests/test_repository_contract.py b/.github/tests/test_repository_contract.py
index 4815923..6adfc2d 100644
--- a/.github/tests/test_repository_contract.py
+++ b/.github/tests/test_repository_contract.py
@@ -309,19 +309,23 @@ def test_catalog_and_generated_artifacts_match_the_executable_registry(self) ->
"docs/architecture/boatstack-v2-*.md text eol=lf",
"docs/architecture/boatstack-v2-*.mmd text eol=lf",
"docs/architecture/boatstack-v2-*.json text eol=lf",
+ "docs/architecture/boatstack-standard-flow.mmd text eol=lf",
):
self.assertIn(pattern, attributes)
response = json.loads(self.run_helper("catalog").stdout)
transitions = response["catalog"]
- self.assertEqual(len(transitions), 61)
- self.assertEqual(len({item["id"] for item in transitions}), 61)
+ self.assertEqual(len(transitions), 62)
+ self.assertEqual(len({item["id"] for item in transitions}), 62)
self.assertEqual(
{item["class"] for item in transitions},
{"authority", "owned-local", "owned-external", "recovery", "observed-external"},
)
markdown = self.run_helper("catalog", "--format", "markdown").stdout
mermaid = self.run_helper("catalog", "--format", "mermaid").stdout
+ standard_flow = self.run_helper(
+ "catalog", "--format", "standard-flow-mermaid"
+ ).stdout
locus_safety = self.run_helper(
"catalog", "--format", "locus-safety"
).stdout
@@ -336,6 +340,11 @@ def test_catalog_and_generated_artifacts_match_the_executable_registry(self) ->
mermaid,
(REPO / "docs" / "architecture" / "boatstack-v2-transition-catalog.mmd").read_text(),
)
+ self.assertEqual(
+ standard_flow,
+ (REPO / "docs" / "architecture" / "boatstack-standard-flow.mmd").read_text(),
+ )
+ self.assertEqual(standard_flow.count("
"), 30)
for name, rendered in (
("boatstack-v2-locus-safety.json", locus_safety),
("boatstack-v2-locus-liveness.json", locus_liveness),
@@ -343,7 +352,7 @@ def test_catalog_and_generated_artifacts_match_the_executable_registry(self) ->
checked = (REPO / "docs" / "architecture" / name).read_text()
self.assertEqual(rendered, checked)
model = json.loads(checked)
- self.assertEqual(len(model["events"]), 61)
+ self.assertEqual(len(model["events"]), 62)
self.assertEqual(
{event["id"] for event in model["events"]},
{item["id"] for item in transitions},
@@ -413,7 +422,7 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
self.run_command(launcher, "doctor", "--repo", repository, env=env).stdout
)
self.assertTrue(doctor["doctor"]["healthy"])
- self.assertEqual(doctor["doctor"]["transition_count"], 61)
+ self.assertEqual(doctor["doctor"]["transition_count"], 62)
self.assertEqual(doctor["snapshot"]["runtime"]["value"], "verified")
goal = (
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 32b439d..16c9d2a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,6 +14,35 @@ concurrency:
cancel-in-progress: true
jobs:
+ component:
+ name: component-${{ matrix.name }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: kernel-mechanism
+ packages: ./internal/kernel/model ./internal/kernel/catalog ./internal/kernel/supervisor ./internal/kernel/engine
+ - name: control-program-compiler
+ packages: ./control ./core
+ - name: standard-flow
+ packages: ./flow/standard ./internal/kernel/protocol
+ - name: extension-conformance
+ packages: ./extension/... ./distribution
+ - name: surface-parity
+ packages: ./internal/surfaces ./sdk ./cmd/boatstack-helper
+ - name: plant-integration
+ packages: ./internal/plant ./internal/effects
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-go@v7
+ with:
+ go-version-file: boatstack/go.mod
+ cache-dependency-path: boatstack/go.sum
+ - name: Test ${{ matrix.name }}
+ working-directory: boatstack
+ run: go test ${{ matrix.packages }}
+
# Unix runs the full suite serially (~1-2 min) and is the unsharded correctness
# reference. Windows is sharded in `test-windows` (see below).
test:
diff --git a/README.md b/README.md
index 9d4b37f..f1b8988 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,11 @@
# Boatstack
-Boatstack V2 is one repository-delivery controller for humans and coding agents.
-It observes a repository, resolves one legal transition, binds exact authority,
-executes owned effects transactionally, verifies the result, and records a
-receipt.
+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
+ControlProgram before it observes a repository, resolves one legal transition,
+binds exact authority, executes owned effects, verifies the result, and records
+a receipt.
Cursor, Codex, Claude Code, Gemini CLI, MCP, the CLI, and the Go SDK use the same
versioned protocol. They do not keep separate workflow state machines.
@@ -70,7 +72,7 @@ boatstack apply --repo . --transition --format json
```
- `status`, `next`, `doctor`, `catalog`, and `events` are read-only.
-- `apply` and `recover` request stable transition IDs from the 61-event
+- `apply` and `recover` request stable transition IDs from the 62-event
executable catalog.
- Friendly aliases such as `plan-create`, `plan-approve`,
`workspace-cut`, `record-test`, and `publish-pr` map to those IDs.
@@ -85,10 +87,17 @@ boatstack apply --repo . --transition --format json
The generated [transition catalog](docs/architecture/boatstack-v2-transition-catalog.md)
and [Mermaid inventory](docs/architecture/boatstack-v2-transition-catalog.mmd)
-come directly from the runtime registry.
+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.
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),
+sdk.WithExtension(extension))`; the lower-level constructor requires an
+explicit trusted in-process primary flow and never inserts StandardFlow.
+
## Coding-agent skills
Boatstack exposes exactly three operation skills on every supported interactive
diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go
index c0bccd7..9bb05e2 100644
--- a/boatstack/cmd/boatstack-helper/main.go
+++ b/boatstack/cmd/boatstack-helper/main.go
@@ -18,6 +18,7 @@ import (
boatstack "github.com/operatorstack/boatstack/boatstack"
"github.com/operatorstack/boatstack/boatstack/analysis"
+ "github.com/operatorstack/boatstack/boatstack/distribution"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
@@ -84,7 +85,7 @@ func run(arguments []string) error {
if err != nil {
return err
}
- kernel, err := boatstack.NewV2Kernel("")
+ kernel, err := standardKernel(context.Background(), request)
if err != nil {
return err
}
@@ -116,7 +117,7 @@ func runRPC() error {
if err := decoder.Decode(&trailing); err != io.EOF {
return fmt.Errorf("V2 RPC request contains trailing JSON")
}
- kernel, err := boatstack.NewV2Kernel("")
+ kernel, err := standardKernel(context.Background(), request)
if err != nil {
return err
}
@@ -220,7 +221,7 @@ func parseOptions(command string, arguments []string, transition catalog.Transit
flags.Var(&options.parameters, "param", "transition parameter name=value (repeatable)")
flags.Var(&options.authorityReceipts, "authority-receipt", "authority receipt JSON path (repeatable)")
flags.BoolVar(&options.follow, "follow", false, "follow passive process events (events with jsonl only)")
- flags.StringVar(&options.host, "host", options.host, "cli, cursor, codex, claude, gemini, or mcp")
+ flags.StringVar(&options.host, "host", options.host, "cli, sdk, cursor, codex, claude, gemini, or mcp")
flags.StringVar(&options.command, "command", "", "raw command to classify at the guard boundary")
if err := flags.Parse(arguments); err != nil {
return commandOptions{}, err
@@ -245,7 +246,22 @@ func parseOptions(command string, arguments []string, transition catalog.Transit
return options, nil
}
-func followEvents(kernel boatstack.V2Kernel, request surfaces.Request) error {
+func standardKernel(ctx context.Context, request surfaces.Request) (boatstack.Kernel, error) {
+ programRequest := distribution.RepositoryProgramRequest{
+ Repository: request.Repository, Host: request.Host, CorrelationID: request.CorrelationID,
+ }
+ if request.TransitionID == "installation.initialize" || request.TransitionID == "configuration.initialize" {
+ programRequest.ConfigurationPath, _ = request.Parameters.Get("config_path")
+ programRequest.ConfigurationFingerprint, _ = request.Parameters.Get("config_sha256")
+ }
+ program, err := distribution.StandardProgramForRepository(ctx, programRequest)
+ if err != nil {
+ return boatstack.Kernel{}, err
+ }
+ return boatstack.NewKernel("", program)
+}
+
+func followEvents(kernel boatstack.Kernel, request surfaces.Request) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
encoder := json.NewEncoder(os.Stdout)
@@ -480,6 +496,9 @@ func renderResponse(response surfaces.Response, format string) error {
case "mermaid":
fmt.Print(surfaces.RenderCatalogMermaid(response.Catalog))
return nil
+ case "standard-flow-mermaid":
+ fmt.Print(surfaces.RenderStandardFlowMermaid(response.Catalog))
+ return nil
case "locus-safety":
value, err := surfaces.RenderCatalogLocusSafety(response.Catalog)
if err != nil {
@@ -518,7 +537,11 @@ func renderResponse(response surfaces.Response, format string) error {
return nil
}
if response.Doctor != nil {
- fmt.Printf("healthy=%t transitions=%d snapshot=%s\n%s\n", response.Doctor.Healthy, response.Doctor.TransitionCount, response.Doctor.Snapshot, response.Doctor.Detail)
+ 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 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.ProgramFingerprint, response.Doctor.UnresolvedProgramDrift, response.Doctor.Snapshot, response.Doctor.Detail)
return nil
}
if response.Decision != nil {
diff --git a/boatstack/cmd/boatstack-helper/main_test.go b/boatstack/cmd/boatstack-helper/main_test.go
index 6f24aae..f5107da 100644
--- a/boatstack/cmd/boatstack-helper/main_test.go
+++ b/boatstack/cmd/boatstack-helper/main_test.go
@@ -6,13 +6,13 @@ import (
"testing"
boatstack "github.com/operatorstack/boatstack/boatstack"
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/surfaces"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
func TestEveryFriendlyMutationAliasMapsToOneRegistryTransition(t *testing.T) {
// control-law: cli-verbs-are-adapters-not-transition-authority
- registry := catalog.Default()
+ registry := testprogram.StandardRegistry()
commands := []string{"init", "update", "attach", "detach", "hydrate-runtime", "configure", "goal-configure", "plan-create", "plan-validate", "plan-approve", "plan-activate", "plan-amend", "workspace-cut", "workspace-sync", "workspace-cleanup", "workspace-reap", "record-build", "record-test", "record-review", "record-change", "record-journey", "publication-preview", "publish-pr", "observe-pr", "correct-pr", "abandon"}
for _, command := range commands {
operation, transitionID, _, err := classifyCommand(command)
diff --git a/boatstack/control/control.go b/boatstack/control/control.go
new file mode 100644
index 0000000..0411137
--- /dev/null
+++ b/boatstack/control/control.go
@@ -0,0 +1,1003 @@
+// Package control defines the stable authoring and compilation contracts for
+// Boatstack control programs. It contains no default flow or product surface.
+package control
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+ "github.com/santhosh-tekuri/jsonschema/v6"
+)
+
+type Transition = catalog.Transition
+type TransitionID = catalog.TransitionID
+type EventClass = catalog.EventClass
+type AuthorityClass = catalog.AuthorityClass
+type FacetCondition = catalog.FacetCondition
+type SelectionClass = catalog.SelectionClass
+type GoalContract = catalog.GoalContract
+type EffectID = catalog.EffectID
+type Prescription = catalog.Prescription
+type ParameterSpec = catalog.ParameterSpec
+type InterruptionContract = catalog.InterruptionContract
+type Reversibility = catalog.Reversibility
+type GoalKind = model.GoalKind
+type ProtocolPhase = model.ProtocolPhase
+type FactStatus = model.FactStatus
+type FacetName = model.FacetName
+
+const (
+ EventOwnedLocal = catalog.EventOwnedLocal
+ EventOwnedExternal = catalog.EventOwnedExternal
+ EventAuthority = catalog.EventAuthority
+ EventObservedExternal = catalog.EventObservedExternal
+ EventRecovery = catalog.EventRecovery
+
+ AuthorityNone = catalog.AuthorityNone
+ AuthorityRepository = catalog.AuthorityRepository
+ AuthorityHuman = catalog.AuthorityHuman
+ AuthorityAutonomy = catalog.AuthorityAutonomy
+ AuthorityProvider = catalog.AuthorityProvider
+
+ SelectionSystemRecovery = catalog.SelectionSystemRecovery
+ SelectionFlowRecovery = catalog.SelectionFlowRecovery
+ SelectionExtensionRecovery = catalog.SelectionExtensionRecovery
+ SelectionGoalRequired = catalog.SelectionGoalRequired
+ SelectionFlowProgress = catalog.SelectionFlowProgress
+ SelectionExplicitOnly = catalog.SelectionExplicitOnly
+ SelectionObservedExternal = catalog.SelectionObservedExternal
+
+ GoalApprovedPlan = model.GoalApprovedPlan
+ GoalVerified = model.GoalVerified
+ GoalOpenPR = model.GoalOpenPR
+ GoalMerged = model.GoalMerged
+ GoalAbandoned = model.GoalAbandoned
+
+ PhaseDormant = model.PhaseDormant
+ PhaseObserved = model.PhaseObserved
+ PhaseActive = model.PhaseActive
+ PhaseRecovery = model.PhaseRecovery
+ PhaseFrontier = model.PhaseFrontier
+ PhaseTerminal = model.PhaseTerminal
+ PhaseUnresolved = model.PhaseUnresolved
+ PhaseAbandoned = model.PhaseAbandoned
+
+ FactKnown = model.FactKnown
+ FactAbsent = model.FactAbsent
+ FactUnknown = model.FactUnknown
+ FactStale = model.FactStale
+ FactConflicting = model.FactConflicting
+
+ Reversible = catalog.Reversible
+ Compensatable = catalog.Compensatable
+ Irreversible = catalog.Irreversible
+ ObservationOnly = catalog.ObservationOnly
+
+ FacetPhase = model.FacetPhase
+ FacetProgram = model.FacetProgram
+ FacetTopology = model.FacetTopology
+ FacetEngagement = model.FacetEngagement
+ FacetDelivery = model.FacetDelivery
+ FacetWorkspace = model.FacetWorkspace
+ FacetPlan = model.FacetPlan
+ FacetConfiguration = model.FacetConfiguration
+ FacetConfigurationPolicy = model.FacetConfigurationPolicy
+ FacetRuntime = model.FacetRuntime
+ FacetPublication = model.FacetPublication
+ FacetVerification = model.FacetVerification
+ FacetRecovery = model.FacetRecovery
+ FacetTransaction = model.FacetTransaction
+ FacetRecoveryInfo = model.FacetRecoveryInfo
+ FacetTransactionInfo = model.FacetTransactionInfo
+ FacetTerminal = model.FacetTerminal
+ FacetGoal = model.FacetGoal
+)
+
+const ProgramSchemaVersion = 1
+
+func KnownCondition(facet FacetName, values ...string) FacetCondition {
+ return FacetCondition{Facet: facet, Statuses: []FactStatus{FactKnown}, Values: append([]string(nil), values...)}
+}
+
+func StatusCondition(facet FacetName, statuses ...FactStatus) FacetCondition {
+ return FacetCondition{Facet: facet, Statuses: append([]FactStatus(nil), statuses...)}
+}
+
+// CoreSystemDefinition supplies Boatstack's operational capabilities. Trust is
+// assigned by the application calling Compile, not by the implementation.
+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)
+}
+
+// Extension supplies an additive manifest. Runtime behavior is optional for a
+// declaration-only extension and is assigned by the assembling application.
+type Extension interface {
+ ExtensionManifest(context.Context) (ExtensionManifest, error)
+}
+
+type CoreSystemManifest struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ 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 GoalConstraint struct {
+ GoalKind GoalKind `json:"goal_kind"`
+ Conditions []FacetCondition `json:"conditions"`
+}
+
+type ExtensionManifest struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ ProtocolVersion int `json:"protocol_version"`
+ ExecutableSHA256 string `json:"executable_sha256,omitempty"`
+ Settings json.RawMessage `json:"settings,omitempty"`
+ SettingsSchema json.RawMessage `json:"settings_schema"`
+ Facts []string `json:"facts,omitempty"`
+ Transitions []Transition `json:"transitions,omitempty"`
+ GoalConstraints []GoalConstraint `json:"goal_constraints,omitempty"`
+ OwnedResources []string `json:"owned_resources,omitempty"`
+ Effects []string `json:"effects,omitempty"`
+ Verifiers []string `json:"verifiers,omitempty"`
+ RecoveryTransitions []TransitionID `json:"recovery_transitions,omitempty"`
+ PrivacyClassification string `json:"privacy_classification"`
+ TelemetryClassification string `json:"telemetry_classification"`
+ Dependencies []string `json:"dependencies,omitempty"`
+}
+
+type ComponentIdentity struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Fingerprint string `json:"fingerprint"`
+}
+
+type ProgramSummary struct {
+ SchemaVersion int `json:"schema_version"`
+ KernelVersion string `json:"kernel_version"`
+ Core ComponentIdentity `json:"core"`
+ Flow ComponentIdentity `json:"flow"`
+ Extensions []ComponentIdentity `json:"extensions,omitempty"`
+ CoreTransitionCount int `json:"core_transition_count"`
+ FlowTransitionCount int `json:"flow_transition_count"`
+ ExtensionTransitionCount int `json:"extension_transition_count"`
+ TotalTransitionCount int `json:"total_transition_count"`
+ ProgramFingerprint string `json:"program_fingerprint"`
+}
+
+// ControlProgram is immutable after Compile. Its accessors always return
+// copies; the runtime registry remains the one executable graph.
+type ControlProgram struct {
+ summary ProgramSummary
+ registry catalog.Registry
+ goalContracts catalog.GoalContracts
+ resourceOwnership map[string]string
+ settingsFingerprint string
+ extensions []compiledExtension
+ flow compiledFlow
+}
+
+type compiledFlow struct {
+ manifest PrimaryFlowManifest
+ identity ComponentIdentity
+ runtime FlowRuntime
+}
+
+type CompiledFlow struct {
+ Manifest PrimaryFlowManifest
+ Identity ComponentIdentity
+ Runtime FlowRuntime
+}
+
+type compiledExtension struct {
+ manifest ExtensionManifest
+ identity ComponentIdentity
+ runtime ExtensionRuntime
+}
+
+type CompiledExtension struct {
+ Manifest ExtensionManifest
+ Identity ComponentIdentity
+ Runtime ExtensionRuntime
+}
+
+func (p ControlProgram) Summary() ProgramSummary {
+ result := p.summary
+ result.Extensions = append([]ComponentIdentity(nil), result.Extensions...)
+ return result
+}
+
+func (p ControlProgram) Fingerprint() string { return p.summary.ProgramFingerprint }
+func (p ControlProgram) TransitionCount() int { return p.registry.Len() }
+func (p ControlProgram) Transitions() []Transition { return p.registry.All() }
+func (p ControlProgram) ResourceOwnership() map[string]string {
+ result := make(map[string]string, len(p.resourceOwnership))
+ for resource, owner := range p.resourceOwnership {
+ result[resource] = owner
+ }
+ return result
+}
+
+func (p ControlProgram) Extensions() []CompiledExtension {
+ result := make([]CompiledExtension, 0, len(p.extensions))
+ for _, extension := range p.extensions {
+ result = append(result, CompiledExtension{Manifest: cloneExtensionManifest(extension.manifest), Identity: extension.identity, Runtime: extension.runtime})
+ }
+ sort.Slice(result, func(i, j int) bool { return result[i].Identity.ID < result[j].Identity.ID })
+ return result
+}
+
+func (p ControlProgram) ExtensionByID(id string) (CompiledExtension, bool) {
+ for _, extension := range p.extensions {
+ if extension.identity.ID == id {
+ return CompiledExtension{Manifest: cloneExtensionManifest(extension.manifest), Identity: extension.identity, Runtime: extension.runtime}, true
+ }
+ }
+ return CompiledExtension{}, false
+}
+
+func (p ControlProgram) Flow() CompiledFlow {
+ return CompiledFlow{Manifest: cloneFlowManifest(p.flow.manifest), Identity: p.flow.identity, Runtime: p.flow.runtime}
+}
+
+// RuntimeRegistry and RuntimeGoalContracts are for the Boatstack mechanism.
+// External applications should use Transitions and Summary.
+func (p ControlProgram) RuntimeRegistry() catalog.Registry { return p.registry }
+func (p ControlProgram) RuntimeGoalContracts() catalog.GoalContracts { return p.goalContracts.Clone() }
+
+type CompileRequest struct {
+ KernelVersion string
+ Core CoreSystemDefinition
+ Flow FlowDefinition
+ Extensions []Extension
+ Settings any
+}
+
+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")
+ }
+ 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)
+ if err != nil {
+ return ControlProgram{}, fmt.Errorf("load PrimaryFlow manifest: %w", err)
+ }
+ flow = cloneFlowManifest(flow)
+ if err := validateCore(core); err != nil {
+ return ControlProgram{}, err
+ }
+ if err := validateFlow(flow); err != nil {
+ return ControlProgram{}, err
+ }
+ coreFingerprint, err := fingerprint(core)
+ if err != nil {
+ return ControlProgram{}, err
+ }
+ flowFingerprint, err := fingerprint(flow)
+ if err != nil {
+ return ControlProgram{}, err
+ }
+ settingsFingerprint, err := fingerprint(request.Settings)
+ 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()
+ }
+ if flow.RuntimeMode == FlowRuntimeProtocol && flowRuntime == nil {
+ return ControlProgram{}, fmt.Errorf("PrimaryFlow %q selects protocol runtime without a FlowRuntime", flow.ID)
+ }
+
+ transitions := make([]Transition, 0, len(core.Transitions)+len(flow.Transitions))
+ resources := map[string]string{}
+ appendComponent := func(items []Transition, origin catalog.TransitionOrigin) error {
+ for _, item := range items {
+ item = cloneTransition(item)
+ item.Origin = origin
+ item.Owner = origin.ID
+ if item.Controllable() && !item.Policy.ReconcilesProgram && !hasFacet(item.SourceConditions, model.FacetProgram) {
+ item.SourceConditions = append(item.SourceConditions, KnownCondition(model.FacetProgram, string(model.ProgramUnbound), string(model.ProgramCurrent)))
+ }
+ for _, resource := range item.OwnedResources {
+ if prior, exists := resources[resource]; exists && prior != origin.ID {
+ return fmt.Errorf("resource %q has overlapping owners %q and %q", resource, prior, origin.ID)
+ }
+ resources[resource] = origin.ID
+ }
+ transitions = append(transitions, item)
+ }
+ return nil
+ }
+ 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 {
+ return ControlProgram{}, err
+ }
+
+ extensionConditions := map[model.GoalKind][]catalog.FacetCondition{}
+ compiledExtensions := make([]compiledExtension, 0, len(request.Extensions))
+ extensionIdentities := make([]ComponentIdentity, 0, len(request.Extensions))
+ extensionCount := 0
+ seenExtensions := map[string]bool{}
+ reservedComponents := map[string]bool{core.ID: true, flow.ID: true}
+ claimedFacts := map[string]string{}
+ for _, fact := range flow.Facts {
+ claimedFacts[fact] = flow.ID
+ }
+ for _, definition := range request.Extensions {
+ if definition == nil {
+ return ControlProgram{}, fmt.Errorf("nil extension definition")
+ }
+ manifest, manifestErr := definition.ExtensionManifest(ctx)
+ if manifestErr != nil {
+ return ControlProgram{}, fmt.Errorf("load extension manifest: %w", manifestErr)
+ }
+ manifest = cloneExtensionManifest(manifest)
+ if err := validateExtension(manifest, seenExtensions, reservedComponents); err != nil {
+ return ControlProgram{}, err
+ }
+ seenExtensions[manifest.ID] = true
+ for _, fact := range manifest.Facts {
+ if prior := claimedFacts[fact]; prior != "" {
+ return ControlProgram{}, fmt.Errorf("fact %q is declared by both %q and %q", fact, prior, manifest.ID)
+ }
+ claimedFacts[fact] = manifest.ID
+ }
+ manifestFingerprint, manifestErr := fingerprint(manifest)
+ if manifestErr != nil {
+ return ControlProgram{}, manifestErr
+ }
+ identity := ComponentIdentity{ID: manifest.ID, Version: manifest.Version, Fingerprint: manifestFingerprint}
+ extensionIdentities = append(extensionIdentities, identity)
+ var runtime ExtensionRuntime
+ if runtimeDefinition, ok := definition.(RuntimeExtension); ok {
+ runtime = runtimeDefinition.Runtime()
+ }
+ if (len(manifest.Facts) != 0 || len(manifest.Transitions) != 0) && runtime == nil {
+ return ControlProgram{}, fmt.Errorf("extension %q declares runtime behavior without an ExtensionRuntime", manifest.ID)
+ }
+ compiledExtensions = append(compiledExtensions, compiledExtension{manifest: manifest, identity: identity, runtime: runtime})
+ for _, constraint := range manifest.GoalConstraints {
+ extensionConditions[constraint.GoalKind] = append(extensionConditions[constraint.GoalKind], constraint.Conditions...)
+ }
+ for _, resource := range manifest.OwnedResources {
+ if !strings.HasPrefix(resource, manifest.ID+".") {
+ return ControlProgram{}, fmt.Errorf("extension %q resource %q is not namespaced", manifest.ID, resource)
+ }
+ if prior, exists := resources[resource]; exists && prior != manifest.ID {
+ return ControlProgram{}, fmt.Errorf("resource %q has overlapping owners %q and %q", resource, prior, manifest.ID)
+ }
+ resources[resource] = manifest.ID
+ }
+ for index := range manifest.Transitions {
+ item := cloneTransition(manifest.Transitions[index])
+ if item.Controllable() && !item.Policy.ReconcilesProgram && !hasFacet(item.SourceConditions, model.FacetProgram) {
+ item.SourceConditions = append(item.SourceConditions, KnownCondition(model.FacetProgram, string(model.ProgramUnbound), string(model.ProgramCurrent)))
+ }
+ if !strings.HasPrefix(string(item.ID), manifest.ID+".") {
+ return ControlProgram{}, fmt.Errorf("extension %q transition %q is not namespaced", manifest.ID, item.ID)
+ }
+ if item.Priority != 0 {
+ return ControlProgram{}, fmt.Errorf("extension %q transition %q cannot declare raw priority", manifest.ID, item.ID)
+ }
+ if item.SelectionClass == "" {
+ if item.Class == catalog.EventRecovery {
+ item.SelectionClass = catalog.SelectionExtensionRecovery
+ } else {
+ item.SelectionClass = catalog.SelectionExplicitOnly
+ }
+ }
+ if item.SelectionClass != catalog.SelectionGoalRequired && item.SelectionClass != catalog.SelectionExtensionRecovery &&
+ item.SelectionClass != catalog.SelectionExplicitOnly && item.SelectionClass != catalog.SelectionObservedExternal {
+ return ControlProgram{}, fmt.Errorf("extension %q transition %q uses forbidden selection class %q", manifest.ID, item.ID, item.SelectionClass)
+ }
+ if item.Class == catalog.EventRecovery && (item.SelectionClass != catalog.SelectionExtensionRecovery || !containsTransition(manifest.RecoveryTransitions, item.ID)) {
+ return ControlProgram{}, fmt.Errorf("extension recovery %q requires EXTENSION_RECOVERY selection and an explicit recovery declaration", item.ID)
+ }
+ if item.SelectionClass == catalog.SelectionExtensionRecovery && item.Class != catalog.EventRecovery {
+ return ControlProgram{}, fmt.Errorf("extension transition %q cannot use EXTENSION_RECOVERY selection outside a recovery event", item.ID)
+ }
+ item.Priority = 1
+ item.Origin = catalog.TransitionOrigin{Kind: catalog.OriginExtension, ID: manifest.ID, Version: manifest.Version, ManifestFingerprint: manifestFingerprint}
+ item.Owner = manifest.ID
+ if !containsString(manifest.Effects, string(item.Effect)) {
+ return ControlProgram{}, fmt.Errorf("extension transition %q uses undeclared effect %q", item.ID, item.Effect)
+ }
+ if !containsString(manifest.Verifiers, item.Verifier) {
+ return ControlProgram{}, fmt.Errorf("extension transition %q uses undeclared verifier %q", item.ID, item.Verifier)
+ }
+ if item.Interruption.Recovery != "" && item.Interruption.Recovery != "recovery.escalate" && !containsTransition(manifest.RecoveryTransitions, item.Interruption.Recovery) {
+ return ControlProgram{}, fmt.Errorf("extension transition %q uses undeclared recovery %q", item.ID, item.Interruption.Recovery)
+ }
+ for _, resource := range item.OwnedResources {
+ if owner := resources[resource]; owner != manifest.ID {
+ return ControlProgram{}, fmt.Errorf("extension transition %q writes undeclared resource %q", item.ID, resource)
+ }
+ }
+ transitions = append(transitions, item)
+ extensionCount++
+ }
+ }
+ if err := validateDependencies(compiledExtensions); err != nil {
+ return ControlProgram{}, err
+ }
+ for goal, conditions := range extensionConditions {
+ sort.SliceStable(conditions, func(i, j int) bool {
+ left, _ := json.Marshal(conditions[i])
+ right, _ := json.Marshal(conditions[j])
+ return string(left) < string(right)
+ })
+ extensionConditions[goal] = conditions
+ }
+ registry, err := catalog.New(transitions)
+ if err != nil {
+ return ControlProgram{}, fmt.Errorf("compile transition registry: %w", err)
+ }
+ contracts, err := catalog.NewGoalContracts(flow.GoalContracts, extensionConditions)
+ if err != nil {
+ return ControlProgram{}, fmt.Errorf("compile goal contracts: %w", err)
+ }
+ sort.Slice(extensionIdentities, func(i, j int) bool { return extensionIdentities[i].ID < extensionIdentities[j].ID })
+ programIdentity := struct {
+ SchemaVersion int
+ KernelVersion string
+ Core ComponentIdentity
+ Flow ComponentIdentity
+ Extensions []ComponentIdentity
+ SettingsFingerprint string
+ Transitions []Transition
+ GoalContracts []catalog.GoalContract
+ Resources map[string]string
+ }{
+ ProgramSchemaVersion, request.KernelVersion,
+ ComponentIdentity{ID: core.ID, Version: core.Version, Fingerprint: coreFingerprint},
+ ComponentIdentity{ID: flow.ID, Version: flow.Version, Fingerprint: flowFingerprint},
+ extensionIdentities, settingsFingerprint, registry.All(), contracts.All(), resources,
+ }
+ programFingerprint, err := fingerprint(programIdentity)
+ if err != nil {
+ return ControlProgram{}, err
+ }
+ summary := ProgramSummary{
+ SchemaVersion: ProgramSchemaVersion, KernelVersion: request.KernelVersion,
+ Core: programIdentity.Core, Flow: programIdentity.Flow, Extensions: extensionIdentities,
+ CoreTransitionCount: len(core.Transitions), FlowTransitionCount: 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},
+ }, nil
+}
+
+func validateCore(manifest CoreSystemManifest) error {
+ if !componentID.MatchString(manifest.ID) || manifest.Version == "" || len(manifest.Transitions) == 0 {
+ return fmt.Errorf("CoreSystem requires semantic id, version, and transitions")
+ }
+ return nil
+}
+
+func validateFlow(manifest PrimaryFlowManifest) error {
+ if !componentID.MatchString(manifest.ID) || manifest.Version == "" || manifest.ProtocolVersion != FlowProtocolVersion ||
+ (manifest.RuntimeMode != FlowRuntimeNative && manifest.RuntimeMode != FlowRuntimeProtocol) ||
+ 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")
+ }
+ if err := validateDeclaredSchema(manifest.ConfigurationSchema, manifest.Settings, "PrimaryFlow "+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)
+ }
+ supported[goal] = true
+ }
+ for _, contract := range manifest.GoalContracts {
+ if !supported[contract.GoalKind] {
+ return fmt.Errorf("PrimaryFlow 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")
+ }
+ 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)
+ }
+ }
+ if manifest.RuntimeMode == FlowRuntimeProtocol {
+ 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)
+ }
+ }
+ }
+ resources := stringSet(manifest.OwnedResources)
+ facts := stringSet(manifest.Facts)
+ effects := stringSet(manifest.Effects)
+ 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)
+ }
+ 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 transition.Controllable() {
+ if !effects[string(transition.Effect)] || !verifiers[transition.Verifier] {
+ return fmt.Errorf("PrimaryFlow 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)
+ }
+ 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 == FlowRuntimeProtocol {
+ 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)
+ }
+ if !facts[string(condition.Facet)] {
+ return fmt.Errorf("protocol PrimaryFlow transition %q targets undeclared fact %q", transition.ID, condition.Facet)
+ }
+ }
+ }
+ }
+ }
+ 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 nil
+}
+
+func stringSet(values []string) map[string]bool {
+ result := make(map[string]bool, len(values))
+ for _, value := range values {
+ if value != "" {
+ result[value] = true
+ }
+ }
+ return result
+}
+
+func transitionSet(values []TransitionID) map[TransitionID]bool {
+ result := make(map[TransitionID]bool, len(values))
+ for _, value := range values {
+ if value != "" {
+ result[value] = true
+ }
+ }
+ return result
+}
+
+func validateExtension(manifest ExtensionManifest, seen, reserved map[string]bool) error {
+ if !componentID.MatchString(manifest.ID) || manifest.Version == "" || manifest.ProtocolVersion != ExtensionProtocolVersion || seen[manifest.ID] || reserved[manifest.ID] {
+ return fmt.Errorf("extension requires unique semantic id, version, and protocol version")
+ }
+ if manifest.PrivacyClassification == "" || manifest.TelemetryClassification == "" {
+ return fmt.Errorf("extension %q requires privacy and telemetry classifications", manifest.ID)
+ }
+ if !validJSONObject(manifest.SettingsSchema) {
+ return fmt.Errorf("extension %q requires a JSON-object settings schema", manifest.ID)
+ }
+ if err := validateDeclaredSchema(manifest.SettingsSchema, manifest.Settings, "extension "+manifest.ID+" settings"); err != nil {
+ return err
+ }
+ if manifest.ExecutableSHA256 != "" {
+ if len(manifest.ExecutableSHA256) != 64 {
+ return fmt.Errorf("extension %q executable SHA-256 is invalid", manifest.ID)
+ }
+ if _, err := hex.DecodeString(manifest.ExecutableSHA256); err != nil {
+ return fmt.Errorf("extension %q executable SHA-256 is invalid", manifest.ID)
+ }
+ }
+ for _, fact := range manifest.Facts {
+ if !strings.HasPrefix(fact, manifest.ID+".") {
+ return fmt.Errorf("extension %q fact %q is not namespaced", manifest.ID, fact)
+ }
+ }
+ declaredFacts := stringSet(manifest.Facts)
+ if duplicate := duplicateString(manifest.Facts); duplicate != "" {
+ return fmt.Errorf("extension %q duplicates fact %q", manifest.ID, duplicate)
+ }
+ for _, effect := range manifest.Effects {
+ if !strings.HasPrefix(effect, manifest.ID+".") {
+ return fmt.Errorf("extension %q effect %q is not namespaced", manifest.ID, effect)
+ }
+ }
+ if duplicate := duplicateString(manifest.Effects); duplicate != "" {
+ return fmt.Errorf("extension %q duplicates effect %q", manifest.ID, duplicate)
+ }
+ for _, verifier := range manifest.Verifiers {
+ if !strings.HasPrefix(verifier, manifest.ID+".") {
+ return fmt.Errorf("extension %q verifier %q is not namespaced", manifest.ID, verifier)
+ }
+ }
+ if duplicate := duplicateString(manifest.Verifiers); duplicate != "" {
+ return fmt.Errorf("extension %q duplicates verifier %q", manifest.ID, duplicate)
+ }
+ if duplicate := duplicateString(manifest.OwnedResources); duplicate != "" {
+ return fmt.Errorf("extension %q duplicates resource %q", manifest.ID, duplicate)
+ }
+ if duplicate := duplicateString(manifest.Dependencies); duplicate != "" {
+ return fmt.Errorf("extension %q duplicates dependency %q", manifest.ID, duplicate)
+ }
+ for _, dependency := range manifest.Dependencies {
+ if dependency == manifest.ID {
+ return fmt.Errorf("extension %q cannot depend on itself", manifest.ID)
+ }
+ }
+ constrainedFacets := map[GoalKind]map[FacetName][]FacetCondition{}
+ for _, constraint := range manifest.GoalConstraints {
+ if !constraint.GoalKind.Valid() || len(constraint.Conditions) == 0 {
+ return fmt.Errorf("extension %q has invalid goal constraint", manifest.ID)
+ }
+ for _, condition := range constraint.Conditions {
+ if !condition.Facet.Valid() || len(condition.Statuses) == 0 || condition.Facet == model.FacetTerminal {
+ return fmt.Errorf("extension %q has invalid or terminal-reporting goal condition", manifest.ID)
+ }
+ for _, status := range condition.Statuses {
+ if !status.Valid() {
+ return fmt.Errorf("extension %q has invalid goal-condition status %q", manifest.ID, status)
+ }
+ }
+ if constrainedFacets[constraint.GoalKind] == nil {
+ constrainedFacets[constraint.GoalKind] = map[FacetName][]FacetCondition{}
+ }
+ constrainedFacets[constraint.GoalKind][condition.Facet] = append(constrainedFacets[constraint.GoalKind][condition.Facet], condition)
+ }
+ }
+ declaredRecovery := map[TransitionID]bool{}
+ for _, recovery := range manifest.RecoveryTransitions {
+ if declaredRecovery[recovery] {
+ return fmt.Errorf("extension %q duplicates recovery %q", manifest.ID, recovery)
+ }
+ declaredRecovery[recovery] = true
+ }
+ seenTransitions := make(map[TransitionID]bool, len(manifest.Transitions))
+ for _, transition := range manifest.Transitions {
+ seenTransitions[transition.ID] = true
+ for _, condition := range transition.TargetConditions {
+ if !strings.HasPrefix(string(condition.Facet), manifest.ID+".") {
+ return fmt.Errorf("extension transition %q targets non-owned fact %q", transition.ID, condition.Facet)
+ }
+ if !declaredFacts[string(condition.Facet)] {
+ return fmt.Errorf("extension transition %q targets undeclared fact %q", transition.ID, condition.Facet)
+ }
+ }
+ if transition.SelectionClass == SelectionGoalRequired {
+ if len(transition.GoalKinds) == 0 {
+ return fmt.Errorf("extension transition %q is implicitly selectable without an explicit constrained goal", transition.ID)
+ }
+ for _, goal := range transition.GoalKinds {
+ discharges := false
+ for _, target := range transition.TargetConditions {
+ for _, obligation := range constrainedFacets[goal][target.Facet] {
+ discharges = discharges || conditionImplies(target, obligation)
+ }
+ }
+ if !discharges {
+ return fmt.Errorf("extension transition %q is implicitly selectable for goal %q without discharging an active obligation", transition.ID, goal)
+ }
+ }
+ }
+ if declaredRecovery[transition.ID] && transition.Class != EventRecovery {
+ return fmt.Errorf("extension recovery %q is not a recovery transition", transition.ID)
+ }
+ }
+ for recovery := range declaredRecovery {
+ if !seenTransitions[recovery] {
+ return fmt.Errorf("extension %q recovery %q has no declared transition", manifest.ID, recovery)
+ }
+ }
+ return nil
+}
+
+type rejectingSchemaLoader struct{}
+
+func (rejectingSchemaLoader) Load(url string) (any, error) {
+ return nil, fmt.Errorf("external JSON Schema reference %q is not permitted", url)
+}
+
+func validateDeclaredSchema(schemaRaw, instanceRaw json.RawMessage, label string) error {
+ decode := func(raw json.RawMessage, fallback string) (any, error) {
+ if len(raw) == 0 {
+ raw = json.RawMessage(fallback)
+ }
+ decoder := json.NewDecoder(strings.NewReader(string(raw)))
+ decoder.UseNumber()
+ var value any
+ if err := decoder.Decode(&value); err != nil {
+ return nil, err
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return nil, fmt.Errorf("contains trailing JSON")
+ }
+ return value, nil
+ }
+ schemaValue, err := decode(schemaRaw, `{}`)
+ if err != nil {
+ return fmt.Errorf("%s schema is invalid JSON: %w", label, err)
+ }
+ instance, err := decode(instanceRaw, `{}`)
+ if err != nil {
+ return fmt.Errorf("%s value is invalid JSON: %w", label, err)
+ }
+ compiler := jsonschema.NewCompiler()
+ compiler.DefaultDraft(jsonschema.Draft2020)
+ compiler.UseLoader(rejectingSchemaLoader{})
+ const location = "urn:boatstack:component-schema"
+ if err := compiler.AddResource(location, schemaValue); err != nil {
+ return fmt.Errorf("%s schema could not be loaded: %w", label, err)
+ }
+ schema, err := compiler.Compile(location)
+ if err != nil {
+ return fmt.Errorf("%s schema is invalid: %w", label, err)
+ }
+ if err := schema.Validate(instance); err != nil {
+ return fmt.Errorf("%s does not satisfy its declared schema: %w", label, err)
+ }
+ return nil
+}
+
+func conditionImplies(target, obligation FacetCondition) bool {
+ if target.Facet != obligation.Facet {
+ return false
+ }
+ for _, status := range target.Statuses {
+ found := false
+ for _, allowed := range obligation.Statuses {
+ found = found || status == allowed
+ }
+ if !found {
+ return false
+ }
+ }
+ if len(obligation.Values) == 0 {
+ return true
+ }
+ if len(target.Values) == 0 {
+ return false
+ }
+ for _, value := range target.Values {
+ found := false
+ for _, allowed := range obligation.Values {
+ found = found || value == allowed
+ }
+ if !found {
+ return false
+ }
+ }
+ return true
+}
+
+func duplicateString(values []string) string {
+ seen := map[string]bool{}
+ for _, value := range values {
+ if seen[value] {
+ return value
+ }
+ seen[value] = true
+ }
+ return ""
+}
+
+func hasFacet(conditions []FacetCondition, facet FacetName) bool {
+ for _, condition := range conditions {
+ if condition.Facet == facet {
+ return true
+ }
+ }
+ return false
+}
+
+func containsString(values []string, wanted string) bool {
+ for _, value := range values {
+ if value == wanted {
+ return true
+ }
+ }
+ return false
+}
+
+func containsTransition(values []TransitionID, wanted TransitionID) bool {
+ for _, value := range values {
+ if value == wanted {
+ return true
+ }
+ }
+ return false
+}
+
+func validateDependencies(extensions []compiledExtension) error {
+ known := map[string]bool{}
+ for _, extension := range extensions {
+ known[extension.manifest.ID] = true
+ }
+ visiting, visited := map[string]bool{}, map[string]bool{}
+ byID := map[string]ExtensionManifest{}
+ for _, extension := range extensions {
+ byID[extension.manifest.ID] = extension.manifest
+ }
+ var visit func(string) error
+ visit = func(id string) error {
+ if visiting[id] {
+ return fmt.Errorf("extension dependency cycle includes %q", id)
+ }
+ if visited[id] {
+ return nil
+ }
+ visiting[id] = true
+ for _, dependency := range byID[id].Dependencies {
+ if !known[dependency] {
+ return fmt.Errorf("extension %q depends on unavailable extension %q", id, dependency)
+ }
+ if err := visit(dependency); err != nil {
+ return err
+ }
+ }
+ visiting[id], visited[id] = false, true
+ return nil
+ }
+ for id := range known {
+ if err := visit(id); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func cloneTransition(value Transition) Transition {
+ value.SourcePhases = append([]model.ProtocolPhase(nil), value.SourcePhases...)
+ value.TargetPhases = append([]model.ProtocolPhase(nil), value.TargetPhases...)
+ value.GoalKinds = append([]model.GoalKind(nil), value.GoalKinds...)
+ value.RequiredIdentity = append([]string(nil), value.RequiredIdentity...)
+ value.Authority = append([]catalog.AuthorityClass(nil), value.Authority...)
+ value.AuthorityAll = append([]catalog.AuthorityClass(nil), value.AuthorityAll...)
+ value.RequiredEvidence = append([]string(nil), value.RequiredEvidence...)
+ value.OwnedResources = append([]string(nil), value.OwnedResources...)
+ value.LocalEffects = append([]catalog.EffectID(nil), value.LocalEffects...)
+ value.ExternalEffects = append([]catalog.EffectID(nil), value.ExternalEffects...)
+ value.Parameters = append([]catalog.ParameterSpec(nil), value.Parameters...)
+ value.SourceConditions = cloneConditions(value.SourceConditions)
+ value.TargetConditions = cloneConditions(value.TargetConditions)
+ value.Interruption.Points = append([]string(nil), value.Interruption.Points...)
+ value.Interruption.PartialState = append([]string(nil), value.Interruption.PartialState...)
+ value.Policy.ManagedOperations = append([]string(nil), value.Policy.ManagedOperations...)
+ return value
+}
+
+func cloneConditions(values []catalog.FacetCondition) []catalog.FacetCondition {
+ result := make([]catalog.FacetCondition, len(values))
+ for index, value := range values {
+ value.Statuses = append([]model.FactStatus(nil), value.Statuses...)
+ value.Values = append([]string(nil), value.Values...)
+ result[index] = value
+ }
+ return result
+}
+
+func cloneCoreManifest(value CoreSystemManifest) CoreSystemManifest {
+ value.Transitions = cloneTransitions(value.Transitions)
+ return value
+}
+
+func cloneFlowManifest(value PrimaryFlowManifest) PrimaryFlowManifest {
+ value.SupportedGoals = append([]GoalKind(nil), value.SupportedGoals...)
+ value.GoalContracts = append([]GoalContract(nil), value.GoalContracts...)
+ for index := range value.GoalContracts {
+ value.GoalContracts[index].Conditions = cloneConditions(value.GoalContracts[index].Conditions)
+ }
+ value.Transitions = cloneTransitions(value.Transitions)
+ value.Facts = append([]string(nil), value.Facts...)
+ value.OwnedResources = append([]string(nil), value.OwnedResources...)
+ value.Effects = append([]string(nil), value.Effects...)
+ value.Verifiers = append([]string(nil), value.Verifiers...)
+ value.RecoveryTransitions = append([]TransitionID(nil), value.RecoveryTransitions...)
+ value.Settings = append(json.RawMessage(nil), value.Settings...)
+ value.ConfigurationSchema = append(json.RawMessage(nil), value.ConfigurationSchema...)
+ return value
+}
+
+func cloneExtensionManifest(value ExtensionManifest) ExtensionManifest {
+ value.Settings = append(json.RawMessage(nil), value.Settings...)
+ value.SettingsSchema = append(json.RawMessage(nil), value.SettingsSchema...)
+ value.Facts = append([]string(nil), value.Facts...)
+ value.Transitions = cloneTransitions(value.Transitions)
+ value.GoalConstraints = append([]GoalConstraint(nil), value.GoalConstraints...)
+ for index := range value.GoalConstraints {
+ value.GoalConstraints[index].Conditions = cloneConditions(value.GoalConstraints[index].Conditions)
+ }
+ value.OwnedResources = append([]string(nil), value.OwnedResources...)
+ value.Effects = append([]string(nil), value.Effects...)
+ value.Verifiers = append([]string(nil), value.Verifiers...)
+ value.RecoveryTransitions = append([]TransitionID(nil), value.RecoveryTransitions...)
+ value.Dependencies = append([]string(nil), value.Dependencies...)
+ return value
+}
+
+func validJSONObject(value json.RawMessage) bool {
+ if len(value) == 0 {
+ return false
+ }
+ var decoded map[string]any
+ decoder := json.NewDecoder(strings.NewReader(string(value)))
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&decoded); err != nil || decoded == nil {
+ return false
+ }
+ var trailing any
+ return decoder.Decode(&trailing) == io.EOF
+}
+
+func cloneTransitions(values []Transition) []Transition {
+ result := make([]Transition, len(values))
+ for index, value := range values {
+ result[index] = cloneTransition(value)
+ }
+ return result
+}
+
+func fingerprint(value any) (string, error) {
+ raw, err := json.Marshal(value)
+ if err != nil {
+ return "", fmt.Errorf("encode control identity: %w", err)
+ }
+ digest := sha256.Sum256(raw)
+ return hex.EncodeToString(digest[:]), nil
+}
diff --git a/boatstack/control/control_test.go b/boatstack/control/control_test.go
new file mode 100644
index 0000000..3ce2af1
--- /dev/null
+++ b/boatstack/control/control_test.go
@@ -0,0 +1,402 @@
+package control_test
+
+import (
+ "context"
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/distribution"
+ "github.com/operatorstack/boatstack/boatstack/extension"
+ "github.com/operatorstack/boatstack/boatstack/extension/releasenote"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
+)
+
+type staticFlowDefinition struct{ manifest control.PrimaryFlowManifest }
+
+func (f staticFlowDefinition) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+ return f.manifest, nil
+}
+
+func TestStandardProgramHasExplicitStableComposition(t *testing.T) {
+ // control-law: compile-produces-one-immutable-registry-with-explicit-origins
+ one, err := distribution.StandardProgram(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ two, err := distribution.StandardProgram(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if one.Fingerprint() != two.Fingerprint() {
+ t.Fatalf("identical compilation drifted: %s != %s", one.Fingerprint(), two.Fingerprint())
+ }
+ summary := one.Summary()
+ if summary.CoreTransitionCount != 32 || summary.FlowTransitionCount != 30 || summary.ExtensionTransitionCount != 0 || summary.TotalTransitionCount != 62 {
+ t.Fatalf("compiled counts = %+v", summary)
+ }
+ counts := map[string]int{}
+ for _, transition := range one.Transitions() {
+ counts[string(transition.Origin.Kind)]++
+ if transition.Owner != transition.Origin.ID || len(transition.Origin.ManifestFingerprint) != 64 {
+ t.Fatalf("transition lost compiled ownership: %+v", transition)
+ }
+ }
+ if counts["core-system"] != 32 || counts["primary-flow"] != 30 || counts["extension"] != 0 {
+ t.Fatalf("origin counts = %#v", counts)
+ }
+}
+
+func TestProgramFingerprintBindsCompositionAndPolicyInputs(t *testing.T) {
+ // control-law: any-control-law-input-change-changes-program-identity
+ base, err := distribution.StandardProgram(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ reference := releasenote.Definition()
+ manifest, err := reference.ExtensionManifest(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ variants := map[string]control.ExtensionManifest{}
+ version := cloneManifest(t, manifest)
+ version.Version = "1.0.1"
+ variants["extension-version"] = version
+ executable := cloneManifest(t, manifest)
+ executable.ExecutableSHA256 = strings.Repeat("a", 64)
+ variants["executable"] = executable
+ settings := cloneManifest(t, manifest)
+ settings.Settings = json.RawMessage(`{"required":true}`)
+ settings.SettingsSchema = json.RawMessage(`{"type":"object","properties":{"required":{"type":"boolean"}},"additionalProperties":false}`)
+ variants["extension-settings"] = settings
+ settingsSchema := cloneManifest(t, manifest)
+ settingsSchema.SettingsSchema = json.RawMessage(`{"type":"object","description":"alternate valid schema","additionalProperties":false}`)
+ variants["extension-settings-schema"] = settingsSchema
+ resource := cloneManifest(t, manifest)
+ resource.OwnedResources = []string{"boatstack.release-note.alternate-evidence"}
+ resource.Transitions[0].OwnedResources = append([]string(nil), resource.OwnedResources...)
+ variants["resource-ownership"] = resource
+ verifier := cloneManifest(t, manifest)
+ verifier.Verifiers = []string{"boatstack.release-note.alternate-verifier"}
+ verifier.Transitions[0].Verifier = verifier.Verifiers[0]
+ variants["verifier"] = verifier
+ recovery := cloneManifest(t, manifest)
+ recoveryTransition := manifest.Transitions[0]
+ recoveryTransition.ID = "boatstack.release-note.recover"
+ recoveryTransition.Class = control.EventRecovery
+ recoveryTransition.SelectionClass = control.SelectionExtensionRecovery
+ recoveryTransition.SourcePhases = []control.ProtocolPhase{control.PhaseRecovery}
+ recoveryTransition.TargetPhases = []control.ProtocolPhase{control.PhaseActive}
+ recoveryTransition.Effect = "boatstack.release-note.recover-effect"
+ recoveryTransition.LocalEffects = []control.EffectID{recoveryTransition.Effect}
+ recoveryTransition.Verifier = "boatstack.release-note.recover-verifier"
+ recoveryTransition.Interruption.Recovery = "recovery.escalate"
+ recoveryTransition.Priority = 0
+ recovery.Transitions = append(recovery.Transitions, recoveryTransition)
+ recovery.Effects = append(recovery.Effects, string(recoveryTransition.Effect))
+ recovery.Verifiers = append(recovery.Verifiers, recoveryTransition.Verifier)
+ recovery.RecoveryTransitions = []control.TransitionID{recoveryTransition.ID}
+ variants["recovery-declaration"] = recovery
+
+ for name, variant := range variants {
+ t.Run(name, func(t *testing.T) {
+ definition, err := extension.NewInProcess(variant, reference.Runtime())
+ if err != nil {
+ t.Fatal(err)
+ }
+ program, err := distribution.StandardProgram(context.Background(), definition)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if program.Fingerprint() == base.Fingerprint() {
+ t.Fatalf("%s did not change program fingerprint", name)
+ }
+ })
+ }
+
+ policyOne, err := control.Compile(context.Background(), control.CompileRequest{KernelVersion: "kernel", Core: core.System(), Flow: 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"}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if policyOne.Fingerprint() == policyTwo.Fingerprint() {
+ t.Fatal("repository policy did not change program fingerprint")
+ }
+}
+
+func TestCompileEnforcesDeclaredComponentSchemas(t *testing.T) {
+ // control-law: component-settings-cannot-cross-runtime-boundaries-unvalidated
+ reference := releasenote.Definition()
+ manifest, err := reference.ExtensionManifest(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ cases := []struct {
+ name, schema, settings string
+ }{
+ {"missing-required", `{"type":"object","required":["mode"]}`, `{}`},
+ {"wrong-type", `{"type":"object","properties":{"mode":{"type":"string"}}}`, `{"mode":1}`},
+ {"additional-property", `{"type":"object","additionalProperties":false}`, `{"mode":"strict"}`},
+ {"invalid-schema", `{"type":"object","required":"mode"}`, `{}`},
+ }
+ for _, test := range cases {
+ t.Run("extension-"+test.name, func(t *testing.T) {
+ candidate := cloneManifest(t, manifest)
+ candidate.SettingsSchema = json.RawMessage(test.schema)
+ candidate.Settings = json.RawMessage(test.settings)
+ definition, err := extension.NewInProcess(candidate, reference.Runtime())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := distribution.StandardProgram(context.Background(), definition); err == nil {
+ t.Fatal("invalid extension settings or schema compiled")
+ }
+ })
+ }
+ flow, err := standard.Definition().FlowManifest(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")
+ }
+}
+
+func TestComponentsMustDeclareTheirOwnSelectionSemantics(t *testing.T) {
+ // control-law: generic-compiler-never-infers-flow-order-from-transition-ids
+ flow, err := standard.Definition().FlowManifest(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},
+ }); 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())
+ 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},
+ }); err == nil || !strings.Contains(err.Error(), "SYSTEM_RECOVERY") {
+ t.Fatalf("flow claimed CoreSystem recovery precedence: %v", err)
+ }
+}
+
+func TestPrimaryFlowCannotClaimCoreSystemResources(t *testing.T) {
+ // control-law: every-resource-has-exactly-one-component-owner
+ coreManifest, err := core.System().CoreManifest(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ var coreResource string
+ for _, transition := range coreManifest.Transitions {
+ if len(transition.OwnedResources) != 0 {
+ coreResource = transition.OwnedResources[0]
+ break
+ }
+ }
+ if coreResource == "" {
+ t.Fatal("CoreSystem fixture declares no owned resource")
+ }
+ flow, err := standard.Definition().FlowManifest(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},
+ }); err == nil || !strings.Contains(err.Error(), "overlapping owners") {
+ t.Fatalf("PrimaryFlow claimed CoreSystem resource %q: %v", coreResource, err)
+ }
+}
+
+func TestExtensionCompilationRejectsBoundaryViolations(t *testing.T) {
+ // control-law: extensions-are-namespaced-additive-and-declarative
+ reference := releasenote.Definition()
+ manifest, err := reference.ExtensionManifest(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ cases := map[string]func(*control.ExtensionManifest){
+ "reserved-component-id": func(value *control.ExtensionManifest) { value.ID = standard.ID },
+ "unnamespaced-fact": func(value *control.ExtensionManifest) { value.Facts[0] = "present" },
+ "raw-priority": func(value *control.ExtensionManifest) { value.Transitions[0].Priority = 7 },
+ "undeclared-effect": func(value *control.ExtensionManifest) { value.Effects = nil },
+ "undeclared-verifier": func(value *control.ExtensionManifest) { value.Verifiers = nil },
+ "phantom-recovery": func(value *control.ExtensionManifest) {
+ value.RecoveryTransitions = []control.TransitionID{"boatstack.release-note.missing-recovery"}
+ },
+ "invalid-goal-status": func(value *control.ExtensionManifest) {
+ value.GoalConstraints[0].Conditions[0].Statuses = []control.FactStatus{"invented"}
+ },
+ "goal-selection-without-matching-obligation": func(value *control.ExtensionManifest) {
+ value.Transitions[0].GoalKinds = []control.GoalKind{control.GoalVerified}
+ },
+ "goal-selection-does-not-discharge-obligation": func(value *control.ExtensionManifest) {
+ value.Transitions[0].TargetConditions[0].Values = []string{"missing"}
+ },
+ "recovery-selection-on-progress": func(value *control.ExtensionManifest) {
+ value.Transitions[0].SelectionClass = control.SelectionExtensionRecovery
+ },
+ "program-reconciliation-claim": func(value *control.ExtensionManifest) {
+ value.Transitions[0].Policy.ReconcilesProgram = true
+ },
+ "requested-goal-binding-claim": func(value *control.ExtensionManifest) {
+ value.Transitions[0].Policy.BindsRequestedGoal = true
+ },
+ "foreign-target": func(value *control.ExtensionManifest) {
+ value.Transitions[0].TargetConditions = []control.FacetCondition{control.KnownCondition(control.FacetPlan, "approved")}
+ },
+ "undeclared-owned-target": func(value *control.ExtensionManifest) {
+ value.Transitions[0].TargetConditions = []control.FacetCondition{control.KnownCondition(control.FacetName(value.ID+".phantom"), "verified")}
+ },
+ }
+ for name, mutate := range cases {
+ t.Run(name, func(t *testing.T) {
+ value := cloneManifest(t, manifest)
+ mutate(&value)
+ definition, err := extension.NewInProcess(value, reference.Runtime())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := distribution.StandardProgram(context.Background(), definition); err == nil {
+ t.Fatalf("%s extension violation was accepted", name)
+ }
+ })
+ }
+
+ left := declarationOnlyExtension{id: "example.left", dependencies: []string{"example.right"}}
+ right := declarationOnlyExtension{id: "example.right", dependencies: []string{"example.left"}}
+ if _, err := distribution.StandardProgram(context.Background(), left, right); err == nil || !strings.Contains(err.Error(), "cycle") {
+ t.Fatalf("dependency cycle error = %v", err)
+ }
+}
+
+func TestControlProgramAccessorsCannotMutateCompiledBytes(t *testing.T) {
+ // control-law: compiled-control-program-is-immutable
+ program, err := distribution.StandardProgram(context.Background(), releasenote.Definition())
+ if err != nil {
+ t.Fatal(err)
+ }
+ originalFingerprint := program.Fingerprint()
+ transitions := program.Transitions()
+ transitions[0].SourcePhases[0] = control.PhaseAbandoned
+ transitions[0].SourceConditions[0].Values = []string{"mutated"}
+ extensions := program.Extensions()
+ extensions[0].Manifest.Facts[0] = "mutated.fact.id"
+ extensions[0].Manifest.Settings = json.RawMessage(`{"mutated":true}`)
+ flow := program.Flow()
+ 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" {
+ 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
+ base, err := distribution.StandardProgram(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ extended, err := distribution.StandardProgram(context.Background(), releasenote.Definition())
+ if err != nil {
+ t.Fatal(err)
+ }
+ conditionCount := func(program control.ControlProgram) int {
+ for _, contract := range program.RuntimeGoalContracts().All() {
+ if contract.GoalKind == control.GoalVerified {
+ return len(contract.Conditions)
+ }
+ }
+ return 0
+ }
+ if conditionCount(extended) != conditionCount(base) {
+ t.Fatalf("release-note extension unexpectedly changed the verified goal")
+ }
+ for _, goal := range []control.GoalKind{control.GoalOpenPR, control.GoalMerged} {
+ baseCount, extendedCount := 0, 0
+ for _, contract := range base.RuntimeGoalContracts().All() {
+ if contract.GoalKind == goal {
+ baseCount = len(contract.Conditions)
+ }
+ }
+ for _, contract := range extended.RuntimeGoalContracts().All() {
+ if contract.GoalKind == goal {
+ extendedCount = len(contract.Conditions)
+ }
+ }
+ if extendedCount != baseCount+1 {
+ t.Fatalf("goal %s conditions: base=%d extended=%d", goal, baseCount, extendedCount)
+ }
+ }
+}
+
+func TestExtensionOrderDoesNotChangeProgramIdentity(t *testing.T) {
+ // control-law: extension-observation-and-compilation-order-is-deterministic
+ left := declarationOnlyExtension{id: "example.left", goalConditions: []control.FacetCondition{control.KnownCondition(control.FacetPlan, "locked")}}
+ right := declarationOnlyExtension{id: "example.right", goalConditions: []control.FacetCondition{control.KnownCondition(control.FacetConfiguration, "verified")}}
+ one, err := distribution.StandardProgram(context.Background(), left, right)
+ if err != nil {
+ t.Fatal(err)
+ }
+ two, err := distribution.StandardProgram(context.Background(), right, left)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if one.Fingerprint() != two.Fingerprint() {
+ t.Fatalf("extension order changed program identity: %s != %s", one.Fingerprint(), two.Fingerprint())
+ }
+}
+
+type declarationOnlyExtension struct {
+ id string
+ dependencies []string
+ goalConditions []control.FacetCondition
+}
+
+type staticFlow struct{ manifest control.PrimaryFlowManifest }
+
+func (s staticFlow) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+ return s.manifest, nil
+}
+
+func cloneManifest(t *testing.T, value control.ExtensionManifest) control.ExtensionManifest {
+ t.Helper()
+ raw, err := json.Marshal(value)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var result control.ExtensionManifest
+ if err := json.Unmarshal(raw, &result); err != nil {
+ t.Fatal(err)
+ }
+ return result
+}
+
+func (e declarationOnlyExtension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) {
+ var constraints []control.GoalConstraint
+ if len(e.goalConditions) != 0 {
+ constraints = []control.GoalConstraint{{GoalKind: control.GoalOpenPR, Conditions: append([]control.FacetCondition(nil), e.goalConditions...)}}
+ }
+ return control.ExtensionManifest{
+ ID: e.id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion,
+ SettingsSchema: json.RawMessage(`{"type":"object"}`),
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
+ Dependencies: append([]string(nil), e.dependencies...), GoalConstraints: constraints,
+ }, nil
+}
diff --git a/boatstack/control/extension.go b/boatstack/control/extension.go
new file mode 100644
index 0000000..de1cb4a
--- /dev/null
+++ b/boatstack/control/extension.go
@@ -0,0 +1,123 @@
+package control
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+)
+
+const ExtensionProtocolVersion = 1
+
+type ExtensionOperation string
+
+const (
+ ExtensionManifestOperation ExtensionOperation = "manifest"
+ ExtensionObserveOperation ExtensionOperation = "observe"
+ ExtensionPlanLocalEffectOperation ExtensionOperation = "plan-local-effect"
+ ExtensionExecuteExternalOperation ExtensionOperation = "execute-external"
+ ExtensionVerifyOperation ExtensionOperation = "verify"
+ ExtensionRecoverOperation ExtensionOperation = "recover"
+)
+
+type ExtensionRequest struct {
+ ProtocolVersion int `json:"protocol_version"`
+ Operation ExtensionOperation `json:"operation"`
+ ExtensionID string `json:"extension_id"`
+ ExtensionVersion string `json:"extension_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 ExtensionFact struct {
+ ID string `json:"id"`
+ Status FactStatus `json:"status"`
+ Value string `json:"value,omitempty"`
+ Fingerprint string `json:"fingerprint,omitempty"`
+ Detail string `json:"detail,omitempty"`
+}
+
+type ResourceWrite struct {
+ Resource string `json:"resource"`
+ Path string `json:"path"`
+ Content []byte `json:"content,omitempty"`
+ SHA256 string `json:"sha256"`
+ Mode uint32 `json:"mode,omitempty"`
+ Delete bool `json:"delete,omitempty"`
+}
+
+type ExtensionResponse struct {
+ ProtocolVersion int `json:"protocol_version"`
+ Operation ExtensionOperation `json:"operation"`
+ ExtensionID string `json:"extension_id"`
+ ExtensionVersion string `json:"extension_version"`
+ CorrelationID string `json:"correlation_id"`
+ Manifest *ExtensionManifest `json:"manifest,omitempty"`
+ 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"`
+}
+
+// ValidateExtensionOperationResponse enforces the exact payload union for the
+// language-neutral extension protocol. Identity and correlation are checked by
+// the caller that owns the request boundary.
+func ValidateExtensionOperationResponse(operation ExtensionOperation, response ExtensionResponse) error {
+ if (response.Error == "") != (response.ErrorClass == "") {
+ return fmt.Errorf("extension errors require an explicit classification and message")
+ }
+ hasPayload := response.Manifest != nil || 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("extension error classification or message exceeds its bound")
+ }
+ if hasPayload {
+ return fmt.Errorf("extension error response contains an operation payload")
+ }
+ return nil
+ }
+ invalid := false
+ switch operation {
+ case ExtensionManifestOperation:
+ invalid = response.Manifest == nil || len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
+ case ExtensionObserveOperation:
+ invalid = response.Manifest != nil || len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
+ case ExtensionPlanLocalEffectOperation, ExtensionRecoverOperation:
+ invalid = response.Manifest != nil || len(response.Facts) != 0 || len(response.ExternalResult) != 0 || response.Verified != nil
+ case ExtensionExecuteExternalOperation:
+ invalid = response.Manifest != nil || len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) == 0 || response.Verified != nil
+ case ExtensionVerifyOperation:
+ invalid = response.Manifest != nil || len(response.Facts) != 0 || len(response.Writes) != 0 || len(response.ExternalResult) != 0 || response.Verified == nil
+ default:
+ return fmt.Errorf("unsupported extension operation %q", operation)
+ }
+ if invalid {
+ return fmt.Errorf("extension returned the wrong response type for %q", operation)
+ }
+ return nil
+}
+
+// ExtensionRuntime is the language-neutral execution contract shared by
+// trusted in-process and subprocess extensions. Kernel admission and resource
+// ownership remain outside the implementation.
+type ExtensionRuntime interface {
+ Invoke(context.Context, ExtensionRequest) (ExtensionResponse, error)
+}
+
+type RuntimeExtension interface {
+ Extension
+ Runtime() ExtensionRuntime
+}
+
+type SubprocessLimits struct {
+ Deadline time.Duration
+ StdoutBytes int64
+ StderrBytes int64
+}
diff --git a/boatstack/control/flow_runtime.go b/boatstack/control/flow_runtime.go
new file mode 100644
index 0000000..329526a
--- /dev/null
+++ b/boatstack/control/flow_runtime.go
@@ -0,0 +1,106 @@
+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/runtime_contract_test.go b/boatstack/control/runtime_contract_test.go
new file mode 100644
index 0000000..e8fcc58
--- /dev/null
+++ b/boatstack/control/runtime_contract_test.go
@@ -0,0 +1,95 @@
+package control
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+)
+
+func TestFlowOperationResponsesAreAnExactTaggedUnion(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"}}},
+ }
+ for operation, response := range valid {
+ if err := ValidateFlowOperationResponse(operation, response); err != nil {
+ t.Fatalf("valid %q response: %v", operation, err)
+ }
+ }
+
+ invalid := []struct {
+ name string
+ operation FlowOperation
+ response FlowResponse
+ }{
+ {"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{}},
+ }
+ for _, test := range invalid {
+ t.Run(test.name, func(t *testing.T) {
+ if err := ValidateFlowOperationResponse(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 {
+ t.Fatalf("classified error response: %v", err)
+ }
+}
+
+func TestExtensionOperationResponsesAreAnExactTaggedUnion(t *testing.T) {
+ verified := true
+ valid := map[ExtensionOperation]ExtensionResponse{
+ ExtensionManifestOperation: {Manifest: &ExtensionManifest{ID: "example.extension"}},
+ ExtensionObserveOperation: {Facts: []ExtensionFact{{ID: "example.ready"}}},
+ ExtensionPlanLocalEffectOperation: {Writes: []ResourceWrite{{Resource: "example.plan"}}},
+ ExtensionExecuteExternalOperation: {ExternalResult: json.RawMessage(`{"ok":true}`)},
+ ExtensionVerifyOperation: {Verified: &verified},
+ ExtensionRecoverOperation: {Writes: []ResourceWrite{{Resource: "example.recovery"}}},
+ }
+ for operation, response := range valid {
+ if err := ValidateExtensionOperationResponse(operation, response); err != nil {
+ t.Fatalf("valid %q response: %v", operation, err)
+ }
+ }
+
+ invalid := []struct {
+ name string
+ operation ExtensionOperation
+ response ExtensionResponse
+ }{
+ {"manifest-missing", ExtensionManifestOperation, ExtensionResponse{}},
+ {"observe-write", ExtensionObserveOperation, ExtensionResponse{Writes: []ResourceWrite{{Resource: "wrong"}}}},
+ {"local-fact", ExtensionPlanLocalEffectOperation, ExtensionResponse{Facts: []ExtensionFact{{ID: "wrong"}}}},
+ {"external-empty", ExtensionExecuteExternalOperation, ExtensionResponse{}},
+ {"verify-missing", ExtensionVerifyOperation, ExtensionResponse{}},
+ {"recover-external", ExtensionRecoverOperation, ExtensionResponse{ExternalResult: json.RawMessage(`{}`)}},
+ {"partial-error", ExtensionObserveOperation, ExtensionResponse{Error: "failed"}},
+ {"error-payload", ExtensionObserveOperation, ExtensionResponse{ErrorClass: "temporary", Error: "failed", Facts: []ExtensionFact{{ID: "wrong"}}}},
+ {"error-class-too-long", ExtensionObserveOperation, ExtensionResponse{ErrorClass: strings.Repeat("x", 129), Error: "failed"}},
+ {"error-message-too-long", ExtensionObserveOperation, ExtensionResponse{ErrorClass: "temporary", Error: strings.Repeat("x", 4097)}},
+ {"unknown-operation", ExtensionOperation("unknown"), ExtensionResponse{}},
+ }
+ for _, test := range invalid {
+ t.Run(test.name, func(t *testing.T) {
+ if err := ValidateExtensionOperationResponse(test.operation, test.response); err == nil {
+ t.Fatalf("invalid %q response was accepted", test.operation)
+ }
+ })
+ }
+ if err := ValidateExtensionOperationResponse(ExtensionObserveOperation, ExtensionResponse{ErrorClass: "temporary", Error: "failed"}); err != nil {
+ t.Fatalf("classified error response: %v", err)
+ }
+}
diff --git a/boatstack/core/system.go b/boatstack/core/system.go
new file mode 100644
index 0000000..36a72c2
--- /dev/null
+++ b/boatstack/core/system.go
@@ -0,0 +1,48 @@
+// Package core owns the first-party CoreSystem definition. It declares
+// Boatstack operational capabilities but no software-delivery flow.
+package core
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+)
+
+const (
+ ID = "boatstack.core"
+ Version = "1.0.0"
+)
+
+type system struct{}
+
+//go:embed transitions.json
+var transitionDeclarations []byte
+
+func System() control.CoreSystemDefinition { return system{} }
+
+func (system) CoreManifest(context.Context) (control.CoreSystemManifest, error) {
+ transitions, err := decodeTransitions()
+ if err != nil {
+ return control.CoreSystemManifest{}, err
+ }
+ return control.CoreSystemManifest{ID: ID, Version: Version, Transitions: transitions}, nil
+}
+
+func decodeTransitions() ([]control.Transition, error) {
+ decoder := json.NewDecoder(bytes.NewReader(transitionDeclarations))
+ decoder.DisallowUnknownFields()
+ var transitions []control.Transition
+ if err := decoder.Decode(&transitions); err != nil {
+ return nil, fmt.Errorf("decode CoreSystem transitions: %w", err)
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return nil, fmt.Errorf("CoreSystem transition declarations contain trailing JSON")
+ }
+ return transitions, nil
+}
diff --git a/boatstack/core/system_test.go b/boatstack/core/system_test.go
new file mode 100644
index 0000000..8ffd57c
--- /dev/null
+++ b/boatstack/core/system_test.go
@@ -0,0 +1,35 @@
+package core_test
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/core"
+)
+
+func TestManifestOwnsOnlyOperationalCapabilities(t *testing.T) {
+ // control-law: core-system-declarations-exclude-primary-flow-policy
+ manifest, err := core.System().CoreManifest(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if manifest.ID != core.ID || manifest.Version != core.Version || len(manifest.Transitions) != 32 {
+ t.Fatalf("CoreSystem identity/count = %s@%s/%d", manifest.ID, manifest.Version, len(manifest.Transitions))
+ }
+ for _, transition := range manifest.Transitions {
+ id := string(transition.ID)
+ if !hasPrefix(id, "engagement.", "invocation.", "repository.", "runtime.", "configuration.", "installation.", "catalog.", "goal.", "recovery.", "external.") {
+ t.Errorf("CoreSystem owns delivery-flow transition %s", id)
+ }
+ }
+}
+
+func hasPrefix(value string, prefixes ...string) bool {
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(value, prefix) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/boatstack/core/transitions.json b/boatstack/core/transitions.json
new file mode 100644
index 0000000..7eeb7f2
--- /dev/null
+++ b/boatstack/core/transitions.json
@@ -0,0 +1,4584 @@
+[
+ {
+ "id": "engagement.begin",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "GOAL_REQUIRED",
+ "class": "authority",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "engagement"
+ ],
+ "effect": "engagement.begin",
+ "local_effects": [
+ "engagement.begin"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "engagement.begin",
+ "expected_postcondition": "predicate:target-phase:engagement.begin"
+ },
+ "source_predicate": "predicate:source-phase:engagement.begin",
+ "source_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "dormant",
+ "command"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:engagement.begin",
+ "target_predicate": "predicate:target-phase:engagement.begin",
+ "target_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:engagement.begin",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:engagement.begin"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 10
+ },
+ {
+ "id": "engagement.renew",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "authority",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "engagement"
+ ],
+ "effect": "engagement.renew",
+ "local_effects": [
+ "engagement.renew"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "engagement.renew",
+ "expected_postcondition": "predicate:target-phase:engagement.renew"
+ },
+ "source_predicate": "predicate:source-phase:engagement.renew",
+ "source_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:engagement.renew",
+ "target_predicate": "predicate:target-phase:engagement.renew",
+ "target_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:engagement.renew",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:engagement.renew"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 70
+ },
+ {
+ "id": "engagement.release",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "authority",
+ "source_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "DORMANT"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "engagement"
+ ],
+ "effect": "engagement.release",
+ "local_effects": [
+ "engagement.release"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "engagement.release",
+ "expected_postcondition": "predicate:target-phase:engagement.release"
+ },
+ "source_predicate": "predicate:source-phase:engagement.release",
+ "source_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "stale",
+ "command"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:engagement.release",
+ "target_predicate": "predicate:target-phase:engagement.release",
+ "target_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "dormant"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:engagement.release",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:engagement.release"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 95
+ },
+ {
+ "id": "invocation.rebind",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:topology",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "identity-binding"
+ ],
+ "effect": "invocation.rebind",
+ "local_effects": [
+ "invocation.rebind"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "invocation.rebind",
+ "expected_postcondition": "predicate:target-phase:invocation.rebind"
+ },
+ "source_predicate": "predicate:source-phase:invocation.rebind",
+ "source_conditions": [
+ {
+ "facet": "topology",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:invocation.rebind",
+ "target_predicate": "predicate:target-phase:invocation.rebind",
+ "target_conditions": [
+ {
+ "facet": "topology",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:invocation.rebind",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:invocation.rebind"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 15,
+ "allows_identity_rebind": true
+ },
+ {
+ "id": "repository.attach",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED"
+ ],
+ "target_phases": [
+ "OBSERVED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:topology",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "repository-binding"
+ ],
+ "effect": "repository.attach",
+ "local_effects": [
+ "repository.attach"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "topology",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "config_authority",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "repository.attach",
+ "expected_postcondition": "predicate:target-phase:repository.attach"
+ },
+ "source_predicate": "predicate:source-phase:repository.attach",
+ "source_conditions": [
+ {
+ "facet": "topology",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "embedded"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:repository.attach",
+ "target_predicate": "predicate:target-phase:repository.attach",
+ "target_conditions": [
+ {
+ "facet": "topology",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "detached",
+ "hybrid"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:repository.attach",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:repository.attach"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 12,
+ "allows_identity_rebind": true
+ },
+ {
+ "id": "repository.detach",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "DORMANT"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:topology",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "repository-binding"
+ ],
+ "effect": "repository.detach",
+ "local_effects": [
+ "repository.detach"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "repository.detach",
+ "expected_postcondition": "predicate:target-phase:repository.detach"
+ },
+ "source_predicate": "predicate:source-phase:repository.detach",
+ "source_conditions": [
+ {
+ "facet": "topology",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "detached",
+ "hybrid"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:repository.detach",
+ "target_predicate": "predicate:target-phase:repository.detach",
+ "target_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "dormant"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:repository.detach",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:repository.detach"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 96,
+ "allows_identity_rebind": true
+ },
+ {
+ "id": "runtime.hydrate",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "GOAL_REQUIRED",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "RECOVERY",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:runtime",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "runtime"
+ ],
+ "effect": "runtime.hydrate",
+ "local_effects": [
+ "runtime.hydrate"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_sha256",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "runtime.hydrate",
+ "expected_postcondition": "predicate:target-phase:runtime.hydrate"
+ },
+ "source_predicate": "predicate:source-phase:runtime.hydrate",
+ "source_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "absent",
+ "stale",
+ "invalid",
+ "conflicting",
+ "wrong-source",
+ "partially-published"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:runtime.hydrate",
+ "target_predicate": "predicate:target-phase:runtime.hydrate",
+ "target_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:runtime.hydrate",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "runtime.reconcile",
+ "recovery_authority": "declared-by:runtime.reconcile",
+ "resumption_predicate": "recovery-contract-for:runtime.hydrate"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 20
+ },
+ {
+ "id": "runtime.replace",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "RECOVERY"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:runtime",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "runtime"
+ ],
+ "effect": "runtime.replace",
+ "local_effects": [
+ "runtime.replace"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_sha256",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "runtime.replace",
+ "expected_postcondition": "predicate:target-phase:runtime.replace"
+ },
+ "source_predicate": "predicate:source-phase:runtime.replace",
+ "source_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified",
+ "stale",
+ "invalid",
+ "conflicting",
+ "wrong-source",
+ "partially-published"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:runtime.replace",
+ "target_predicate": "predicate:target-phase:runtime.replace",
+ "target_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:runtime.replace",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "runtime.reconcile",
+ "recovery_authority": "declared-by:runtime.reconcile",
+ "resumption_predicate": "recovery-contract-for:runtime.replace"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 25
+ },
+ {
+ "id": "runtime.reconcile",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "SYSTEM_RECOVERY",
+ "class": "recovery",
+ "source_phases": [
+ "RECOVERY",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "FRONTIER",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:recovery-info",
+ "facet:program"
+ ],
+ "owned_resources": [
+ "runtime"
+ ],
+ "effect": "runtime.reconcile",
+ "local_effects": [
+ "runtime.reconcile"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_sha256",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "transaction_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "runtime.reconcile",
+ "expected_postcondition": "predicate:target-phase:runtime.reconcile"
+ },
+ "source_predicate": "predicate:source-phase:runtime.reconcile",
+ "source_conditions": [
+ {
+ "facet": "recovery-info",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:runtime.reconcile",
+ "target_predicate": "predicate:target-phase:runtime.reconcile",
+ "target_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:runtime.reconcile",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "never-blindly-retry-interrupted-recovery",
+ "rollback_contract": "preserve-original-transaction-group",
+ "compensation_contract": "escalation-only-after-nested-interruption",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:runtime.reconcile"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 4
+ },
+ {
+ "id": "configuration.initialize",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "GOAL_REQUIRED",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:configuration",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "configuration"
+ ],
+ "effect": "configuration.initialize",
+ "local_effects": [
+ "configuration.initialize"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "config_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "config_sha256",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "configuration.initialize",
+ "expected_postcondition": "predicate:target-phase:configuration.initialize"
+ },
+ "source_predicate": "predicate:source-phase:configuration.initialize",
+ "source_conditions": [
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unsupported",
+ "stale",
+ "conflicting"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:configuration.initialize",
+ "target_predicate": "predicate:target-phase:configuration.initialize",
+ "target_conditions": [
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:configuration.initialize",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "configuration.reconcile",
+ "recovery_authority": "declared-by:configuration.reconcile",
+ "resumption_predicate": "recovery-contract-for:configuration.initialize"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 22
+ },
+ {
+ "id": "configuration.mutate",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:configuration",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "configuration"
+ ],
+ "effect": "configuration.mutate",
+ "local_effects": [
+ "configuration.mutate"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "config_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "config_sha256",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "configuration.mutate",
+ "expected_postcondition": "predicate:target-phase:configuration.mutate"
+ },
+ "source_predicate": "predicate:source-phase:configuration.mutate",
+ "source_conditions": [
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified",
+ "stale",
+ "divergent",
+ "conflicting"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:configuration.mutate",
+ "target_predicate": "predicate:target-phase:configuration.mutate",
+ "target_conditions": [
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:configuration.mutate",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "configuration.reconcile",
+ "recovery_authority": "declared-by:configuration.reconcile",
+ "resumption_predicate": "recovery-contract-for:configuration.mutate"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 60
+ },
+ {
+ "id": "configuration.reconcile",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "SYSTEM_RECOVERY",
+ "class": "recovery",
+ "source_phases": [
+ "RECOVERY",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "FRONTIER",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:recovery-info",
+ "facet:program"
+ ],
+ "owned_resources": [
+ "configuration"
+ ],
+ "effect": "configuration.reconcile",
+ "local_effects": [
+ "configuration.reconcile"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "transaction_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "configuration.reconcile",
+ "expected_postcondition": "predicate:target-phase:configuration.reconcile"
+ },
+ "source_predicate": "predicate:source-phase:configuration.reconcile",
+ "source_conditions": [
+ {
+ "facet": "recovery-info",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:configuration.reconcile",
+ "target_predicate": "predicate:target-phase:configuration.reconcile",
+ "target_conditions": [
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:configuration.reconcile",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "never-blindly-retry-interrupted-recovery",
+ "rollback_contract": "preserve-original-transaction-group",
+ "compensation_contract": "escalation-only-after-nested-interruption",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:configuration.reconcile"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 3
+ },
+ {
+ "id": "installation.initialize",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "GOAL_REQUIRED",
+ "class": "owned-local",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED"
+ ],
+ "target_phases": [
+ "OBSERVED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:runtime",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "installation"
+ ],
+ "effect": "installation.initialize",
+ "local_effects": [
+ "installation.initialize"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_sha256",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "config_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "config_sha256",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "installation.initialize",
+ "expected_postcondition": "predicate:target-phase:installation.initialize"
+ },
+ "source_predicate": "predicate:source-phase:installation.initialize",
+ "source_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "absent",
+ "invalid",
+ "stale"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:installation.initialize",
+ "target_predicate": "predicate:target-phase:installation.initialize",
+ "target_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:installation.initialize",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "runtime.reconcile",
+ "recovery_authority": "declared-by:runtime.reconcile",
+ "resumption_predicate": "recovery-contract-for:installation.initialize"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 11
+ },
+ {
+ "id": "installation.update",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:runtime",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration"
+ ],
+ "owned_resources": [
+ "installation"
+ ],
+ "effect": "installation.update",
+ "local_effects": [
+ "installation.update"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "runtime_sha256",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "installation.update",
+ "expected_postcondition": "predicate:target-phase:installation.update"
+ },
+ "source_predicate": "predicate:source-phase:installation.update",
+ "source_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified",
+ "stale",
+ "invalid",
+ "conflicting",
+ "wrong-source",
+ "partially-published"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:installation.update",
+ "target_predicate": "predicate:target-phase:installation.update",
+ "target_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:installation.update",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "runtime.reconcile",
+ "recovery_authority": "declared-by:runtime.reconcile",
+ "resumption_predicate": "recovery-contract-for:installation.update"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 65
+ },
+ {
+ "id": "catalog.reconcile",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "target_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:program",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal"
+ ],
+ "owned_resources": [
+ "catalog-identity"
+ ],
+ "effect": "catalog.reconcile",
+ "local_effects": [
+ "catalog.reconcile"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "prior_program_fingerprint",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "accept_obligation_change",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "catalog.reconcile",
+ "expected_postcondition": "predicate:target-phase:catalog.reconcile"
+ },
+ "source_predicate": "predicate:source-phase:catalog.reconcile",
+ "source_conditions": [
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "drift"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "drift"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale",
+ "established"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:catalog.reconcile",
+ "target_predicate": "predicate:target-phase:catalog.reconcile",
+ "target_conditions": [
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:catalog.reconcile",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:catalog.reconcile"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "reconciles_program": true
+ },
+ "priority": 1
+ },
+ {
+ "id": "goal.configure",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "GOAL_REQUIRED",
+ "class": "authority",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:goal",
+ "facet:program"
+ ],
+ "owned_resources": [
+ "goal"
+ ],
+ "effect": "goal.configure",
+ "local_effects": [
+ "goal.configure"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "goal_kind",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "delivery_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "goal.configure",
+ "expected_postcondition": "predicate:target-phase:goal.configure"
+ },
+ "source_predicate": "predicate:source-phase:goal.configure",
+ "source_conditions": [
+ {
+ "facet": "goal",
+ "statuses": [
+ "known",
+ "absent"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:goal.configure",
+ "target_predicate": "predicate:target-phase:goal.configure",
+ "target_conditions": [
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:goal.configure",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:goal.configure"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "binds_requested_goal": true
+ },
+ "priority": 30
+ },
+ {
+ "id": "recovery.resume",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "SYSTEM_RECOVERY",
+ "class": "recovery",
+ "source_phases": [
+ "RECOVERY"
+ ],
+ "target_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:recovery-info",
+ "facet:program"
+ ],
+ "owned_resources": [
+ "recovery-journal"
+ ],
+ "effect": "recovery.resume",
+ "local_effects": [
+ "recovery.resume"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "transaction_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "recovery.resume",
+ "expected_postcondition": "predicate:target-phase:recovery.resume"
+ },
+ "source_predicate": "predicate:source-phase:recovery.resume",
+ "source_conditions": [
+ {
+ "facet": "recovery-info",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:recovery.resume",
+ "target_predicate": "predicate:target-phase:recovery.resume",
+ "target_conditions": [
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:recovery.resume",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "never-blindly-retry-interrupted-recovery",
+ "rollback_contract": "preserve-original-transaction-group",
+ "compensation_contract": "escalation-only-after-nested-interruption",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:recovery.resume"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 2
+ },
+ {
+ "id": "recovery.rollback",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "SYSTEM_RECOVERY",
+ "class": "recovery",
+ "source_phases": [
+ "RECOVERY"
+ ],
+ "target_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:recovery-info",
+ "facet:program"
+ ],
+ "owned_resources": [
+ "recovery-journal"
+ ],
+ "effect": "recovery.rollback",
+ "local_effects": [
+ "recovery.rollback"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "transaction_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "recovery.rollback",
+ "expected_postcondition": "predicate:target-phase:recovery.rollback"
+ },
+ "source_predicate": "predicate:source-phase:recovery.rollback",
+ "source_conditions": [
+ {
+ "facet": "recovery-info",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:recovery.rollback",
+ "target_predicate": "predicate:target-phase:recovery.rollback",
+ "target_conditions": [
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:recovery.rollback",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "never-blindly-retry-interrupted-recovery",
+ "rollback_contract": "preserve-original-transaction-group",
+ "compensation_contract": "escalation-only-after-nested-interruption",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:recovery.rollback"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 3
+ },
+ {
+ "id": "recovery.escalate",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "SYSTEM_RECOVERY",
+ "class": "recovery",
+ "source_phases": [
+ "RECOVERY",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:recovery-info",
+ "facet:program"
+ ],
+ "owned_resources": [
+ "recovery-journal"
+ ],
+ "effect": "recovery.escalate",
+ "local_effects": [
+ "recovery.escalate"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "transaction_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "recovery.escalate",
+ "expected_postcondition": "predicate:target-phase:recovery.escalate"
+ },
+ "source_predicate": "predicate:source-phase:recovery.escalate",
+ "source_conditions": [
+ {
+ "facet": "recovery-info",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:recovery.escalate",
+ "target_predicate": "predicate:target-phase:recovery.escalate",
+ "target_conditions": [
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "escalated"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:recovery.escalate",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "never-blindly-retry-interrupted-recovery",
+ "rollback_contract": "preserve-original-transaction-group",
+ "compensation_contract": "escalation-only-after-nested-interruption",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:recovery.escalate"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 5
+ },
+ {
+ "id": "external.files-changed",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:verification"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.files-changed",
+ "expected_postcondition": "predicate:target-phase:external.files-changed"
+ },
+ "source_predicate": "predicate:source-phase:external.files-changed",
+ "source_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.files-changed",
+ "target_predicate": "predicate:target-phase:external.files-changed",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.files-changed",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.files-changed"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.head-changed",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:verification"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.head-changed",
+ "expected_postcondition": "predicate:target-phase:external.head-changed"
+ },
+ "source_predicate": "predicate:source-phase:external.head-changed",
+ "source_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.head-changed",
+ "target_predicate": "predicate:target-phase:external.head-changed",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.head-changed",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.head-changed"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.branch-changed",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.branch-changed",
+ "expected_postcondition": "predicate:target-phase:external.branch-changed"
+ },
+ "source_predicate": "predicate:source-phase:external.branch-changed",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.branch-changed",
+ "target_predicate": "predicate:target-phase:external.branch-changed",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.branch-changed",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.branch-changed"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.runtime-disappeared",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "RECOVERY"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:runtime"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.runtime-disappeared",
+ "expected_postcondition": "predicate:target-phase:external.runtime-disappeared"
+ },
+ "source_predicate": "predicate:source-phase:external.runtime-disappeared",
+ "source_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified",
+ "stale"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.runtime-disappeared",
+ "target_predicate": "predicate:target-phase:external.runtime-disappeared",
+ "target_conditions": [
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "absent",
+ "stale"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.runtime-disappeared",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.runtime-disappeared"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.configuration-drifted",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "UNRESOLVED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:configuration"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.configuration-drifted",
+ "expected_postcondition": "predicate:target-phase:external.configuration-drifted"
+ },
+ "source_predicate": "predicate:source-phase:external.configuration-drifted",
+ "source_conditions": [
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.configuration-drifted",
+ "target_predicate": "predicate:target-phase:external.configuration-drifted",
+ "target_conditions": [
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "stale",
+ "divergent",
+ "conflicting"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.configuration-drifted",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.configuration-drifted"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.lease-expired",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "DORMANT",
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:engagement"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.lease-expired",
+ "expected_postcondition": "predicate:target-phase:external.lease-expired"
+ },
+ "source_predicate": "predicate:source-phase:external.lease-expired",
+ "source_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "command"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.lease-expired",
+ "target_predicate": "predicate:target-phase:external.lease-expired",
+ "target_conditions": [
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "dormant",
+ "stale"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.lease-expired",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.lease-expired"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.host-interrupted",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "RECOVERY"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:transaction",
+ "facet:transaction-info"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.host-interrupted",
+ "expected_postcondition": "predicate:target-phase:external.host-interrupted"
+ },
+ "source_predicate": "predicate:source-phase:external.host-interrupted",
+ "source_conditions": [
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "staged",
+ "local-applied",
+ "external-uncertain",
+ "verifying"
+ ]
+ },
+ {
+ "facet": "transaction-info",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.host-interrupted",
+ "target_predicate": "predicate:target-phase:external.host-interrupted",
+ "target_conditions": [
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.host-interrupted",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.host-interrupted"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.ci-completed",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:verification"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.ci-completed",
+ "expected_postcondition": "predicate:target-phase:external.ci-completed"
+ },
+ "source_predicate": "predicate:source-phase:external.ci-completed",
+ "source_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.ci-completed",
+ "target_predicate": "predicate:target-phase:external.ci-completed",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.ci-completed",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.ci-completed"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.pr-opened",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.pr-opened",
+ "expected_postcondition": "predicate:target-phase:external.pr-opened"
+ },
+ "source_predicate": "predicate:source-phase:external.pr-opened",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.pr-opened",
+ "target_predicate": "predicate:target-phase:external.pr-opened",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "open"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.pr-opened",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.pr-opened"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.pr-updated",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.pr-updated",
+ "expected_postcondition": "predicate:target-phase:external.pr-updated"
+ },
+ "source_predicate": "predicate:source-phase:external.pr-updated",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.pr-updated",
+ "target_predicate": "predicate:target-phase:external.pr-updated",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "open"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.pr-updated",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.pr-updated"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.pr-closed",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.pr-closed",
+ "expected_postcondition": "predicate:target-phase:external.pr-closed"
+ },
+ "source_predicate": "predicate:source-phase:external.pr-closed",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.pr-closed",
+ "target_predicate": "predicate:target-phase:external.pr-closed",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "closed-unmerged",
+ "merged"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.pr-closed",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.pr-closed"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.pr-merged",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.pr-merged",
+ "expected_postcondition": "predicate:target-phase:external.pr-merged"
+ },
+ "source_predicate": "predicate:source-phase:external.pr-merged",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.pr-merged",
+ "target_predicate": "predicate:target-phase:external.pr-merged",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "merged"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.pr-merged",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.pr-merged"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ },
+ {
+ "id": "external.provider-unavailable",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "OBSERVED_EXTERNAL",
+ "class": "observed-external",
+ "source_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "UNRESOLVED",
+ "RECOVERY"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "none"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication"
+ ],
+ "idempotent": false,
+ "prescription": {
+ "operation": "external.provider-unavailable",
+ "expected_postcondition": "predicate:target-phase:external.provider-unavailable"
+ },
+ "source_predicate": "predicate:source-phase:external.provider-unavailable",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:external.provider-unavailable",
+ "target_predicate": "predicate:target-phase:external.provider-unavailable",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unavailable"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:external.provider-unavailable",
+ "interruption": {
+ "points": [
+ "after-observation"
+ ],
+ "partial_state": [
+ "no-owned-partial-state"
+ ],
+ "detection": "fresh-canonical-observation",
+ "resume_contract": "not-applicable",
+ "rollback_contract": "not-applicable",
+ "compensation_contract": "not-applicable",
+ "recovery_authority": "not-applicable",
+ "resumption_predicate": "predicate:target-phase:external.provider-unavailable"
+ },
+ "reversibility": "observation-only",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 100
+ }
+]
diff --git a/boatstack/distribution/standard.go b/boatstack/distribution/standard.go
new file mode 100644
index 0000000..6b09a5a
--- /dev/null
+++ b/boatstack/distribution/standard.go
@@ -0,0 +1,160 @@
+// Package distribution is the composition root for shipped Boatstack
+// distributions. Kernel mechanism packages do not import it.
+package distribution
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "time"
+
+ boatstack "github.com/operatorstack/boatstack/boatstack"
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/extension/subprocess"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
+ "github.com/operatorstack/boatstack/boatstack/internal/plant"
+)
+
+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,
+ Settings: programSettings{},
+ })
+}
+
+// programSettings contains only repository policy that changes the compiled
+// graph. Approval, host, visual, and risk policy remain controlling snapshot
+// facts; the configured extension composition changes the ControlProgram.
+type programSettings struct {
+ Extensions []protocol.SubprocessExtensionSettings `json:"extensions"`
+}
+
+// RepositoryProgramRequest identifies one immutable repository-scoped
+// composition. It contains no authority receipt and cannot replace
+// StandardFlow.
+type RepositoryProgramRequest struct {
+ Repository string
+ ExternalStateRoot string
+ Host string
+ CorrelationID string
+ Extensions []control.Extension
+ ConfigurationPath string
+ ConfigurationFingerprint string
+}
+
+// StandardProgramForRepository compiles StandardFlow plus the exact
+// checksum-verified subprocess extensions selected by the repository's strict
+// project configuration. A new value is returned per call, so concurrent
+// repositories never share mutable program state.
+func StandardProgramForRepository(ctx context.Context, request RepositoryProgramRequest) (control.ControlProgram, error) {
+ configured, settings, err := ConfiguredExtensions(ctx, request)
+ if err != nil {
+ return control.ControlProgram{}, err
+ }
+ 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(),
+ Extensions: extensions, Settings: settings,
+ })
+}
+
+// ConfiguredExtensions resolves only additive subprocess extensions. The
+// returned settings identity binds every repository policy byte that can
+// affect the compiled program.
+func ConfiguredExtensions(ctx context.Context, request RepositoryProgramRequest) ([]control.Extension, any, error) {
+ if request.Repository == "" {
+ return nil, programSettings{}, nil
+ }
+ host := request.Host
+ if host == "" {
+ host = "cli"
+ }
+ correlation := request.CorrelationID
+ if correlation == "" {
+ correlation = "program-assembly"
+ }
+ resolver, err := plant.NewResolver(request.ExternalStateRoot)
+ if err != nil {
+ return nil, nil, err
+ }
+ invocation, err := resolver.ResolveInvocation(ctx, request.Repository, host, correlation)
+ if err != nil {
+ return nil, nil, fmt.Errorf("resolve repository control program: %w", err)
+ }
+ layout, _, err := resolver.ResolveLayout(ctx, invocation)
+ if err != nil {
+ return nil, nil, fmt.Errorf("resolve repository program layout: %w", err)
+ }
+ configurationPath := layout.ConfigPath
+ if request.ConfigurationPath != "" {
+ if !filepath.IsAbs(request.ConfigurationPath) || filepath.Clean(request.ConfigurationPath) != request.ConfigurationPath {
+ return nil, nil, fmt.Errorf("candidate repository program configuration path must be exact and absolute")
+ }
+ configurationPath = request.ConfigurationPath
+ }
+ raw, err := os.ReadFile(configurationPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, programSettings{}, nil
+ }
+ return nil, nil, fmt.Errorf("read repository program configuration: %w", err)
+ }
+ configuration, fingerprint, err := protocol.ProjectConfigFingerprint(raw)
+ if err != nil {
+ return nil, nil, fmt.Errorf("verify repository program configuration: %w", err)
+ }
+ if request.ConfigurationFingerprint != "" && request.ConfigurationFingerprint != fingerprint {
+ return nil, nil, fmt.Errorf("candidate repository program configuration fingerprint mismatch")
+ }
+ configured := make([]control.Extension, 0, len(configuration.Extensions))
+ for _, declaration := range configuration.Extensions {
+ extension, extensionErr := subprocess.New(subprocess.Config{
+ ID: declaration.ID, Version: declaration.Version, Executable: declaration.Executable, SHA256: declaration.SHA256,
+ Manifest: declaration.Manifest, Settings: declaration.Settings,
+ Limits: control.SubprocessLimits{Deadline: time.Duration(declaration.DeadlineMillis) * time.Millisecond, StdoutBytes: declaration.StdoutBytes, StderrBytes: declaration.StderrBytes},
+ })
+ if extensionErr != nil {
+ return nil, nil, fmt.Errorf("verify configured subprocess extension %q: %w", declaration.ID, extensionErr)
+ }
+ configured = append(configured, extension)
+ }
+ settings, err := canonicalProgramSettings(configuration.Extensions)
+ if err != nil {
+ return nil, nil, err
+ }
+ return configured, settings, nil
+}
+
+func canonicalProgramSettings(values []protocol.SubprocessExtensionSettings) (programSettings, error) {
+ extensions := append([]protocol.SubprocessExtensionSettings(nil), values...)
+ for index := range extensions {
+ items := []struct {
+ name string
+ value *json.RawMessage
+ }{{"manifest", &extensions[index].Manifest}, {"settings", &extensions[index].Settings}}
+ for _, item := range items {
+ name, value := item.name, item.value
+ if len(*value) == 0 {
+ continue
+ }
+ var decoded any
+ if err := json.Unmarshal(*value, &decoded); err != nil {
+ return programSettings{}, fmt.Errorf("canonicalize extension %q %s: %w", extensions[index].ID, name, err)
+ }
+ canonical, err := json.Marshal(decoded)
+ if err != nil {
+ return programSettings{}, fmt.Errorf("canonicalize extension %q %s: %w", extensions[index].ID, name, err)
+ }
+ *value = canonical
+ }
+ }
+ sort.Slice(extensions, func(i, j int) bool { return extensions[i].ID < extensions[j].ID })
+ return programSettings{Extensions: extensions}, nil
+}
diff --git a/boatstack/distribution/standard_test.go b/boatstack/distribution/standard_test.go
new file mode 100644
index 0000000..bb11398
--- /dev/null
+++ b/boatstack/distribution/standard_test.go
@@ -0,0 +1,207 @@
+package distribution
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
+)
+
+func TestRepositoryScopedProgramsAreIndependentUnderConcurrency(t *testing.T) {
+ // control-law: repository-program-selection-has-no-mutable-global-state
+ if runtime.GOOS == "windows" {
+ t.Skip("the language-neutral Python fixture is exercised on POSIX")
+ }
+ if _, err := os.Stat("/usr/bin/python3"); err != nil {
+ t.Skip("/usr/bin/python3 is unavailable")
+ }
+ fixturePath, err := filepath.Abs(filepath.Join("..", "extension", "subprocess", "testdata", "reference_extension.py"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ source, err := os.ReadFile(fixturePath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ extensions := func(id string) protocol.SubprocessExtensionSettings {
+ path := filepath.Join(t.TempDir(), strings.ReplaceAll(id, ".", "-")+".py")
+ content := []byte(strings.ReplaceAll(string(source), "fixture.echo", id))
+ if err := os.WriteFile(path, content, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ resolved, err := filepath.EvalSymlinks(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(content)
+ manifest, err := json.Marshal(control.ExtensionManifest{
+ ID: id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion,
+ SettingsSchema: json.RawMessage(`{"type":"object"}`), Facts: []string{id + ".present"},
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return protocol.SubprocessExtensionSettings{
+ ID: id, Version: "1.0.0", Executable: resolved, SHA256: hex.EncodeToString(digest[:]), Manifest: manifest, DeadlineMillis: 30_000,
+ }
+ }
+ x, y := extensions("fixture.echo"), extensions("fixture.second")
+ candidateConfig := protocol.ProjectConfig{
+ SchemaVersion: protocol.ConfigSchemaVersion,
+ Project: protocol.ProjectSettings{Name: "fixture", DefaultBranch: "main", Commands: map[string]string{}},
+ Policy: protocol.PolicySettings{PlanApproval: "human", VisualEvidence: "optional"},
+ Hosts: []string{"cli"}, Extensions: []protocol.SubprocessExtensionSettings{x},
+ }
+ candidateBytes, err := json.Marshal(candidateConfig)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, candidateFingerprint, err := protocol.ProjectConfigFingerprint(candidateBytes)
+ if err != nil {
+ t.Fatal(err)
+ }
+ candidatePath := filepath.Join(t.TempDir(), "candidate.json")
+ if err := os.WriteFile(candidatePath, candidateBytes, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ candidateProgram, err := StandardProgramForRepository(context.Background(), RepositoryProgramRequest{
+ Repository: repositoryFixture(t, nil), ExternalStateRoot: t.TempDir(), Host: "cli", CorrelationID: "candidate-program",
+ ConfigurationPath: candidatePath, ConfigurationFingerprint: candidateFingerprint,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(candidateProgram.Summary().Extensions) != 1 {
+ t.Fatalf("candidate program extensions = %d, want 1", len(candidateProgram.Summary().Extensions))
+ }
+ if _, err := StandardProgramForRepository(context.Background(), RepositoryProgramRequest{
+ Repository: repositoryFixture(t, nil), ExternalStateRoot: t.TempDir(), Host: "cli", CorrelationID: "candidate-mismatch",
+ ConfigurationPath: candidatePath, ConfigurationFingerprint: strings.Repeat("0", 64),
+ }); err == nil {
+ t.Fatal("candidate program accepted a mismatched configuration fingerprint")
+ }
+ repositories := []string{repositoryFixture(t, nil), repositoryFixture(t, []protocol.SubprocessExtensionSettings{x}), repositoryFixture(t, []protocol.SubprocessExtensionSettings{x, y})}
+ expected := []int{0, 1, 2}
+ fingerprints := make([]string, len(repositories))
+ externalRoots := []string{t.TempDir(), t.TempDir(), t.TempDir()}
+ var wait sync.WaitGroup
+ errors := make(chan error, len(repositories))
+ for index, repository := range repositories {
+ wait.Add(1)
+ go func(index int, repository string) {
+ defer wait.Done()
+ program, err := StandardProgramForRepository(context.Background(), RepositoryProgramRequest{
+ Repository: repository, ExternalStateRoot: externalRoots[index], Host: "cli", CorrelationID: "repository-program",
+ })
+ if err != nil {
+ errors <- err
+ return
+ }
+ if len(program.Summary().Extensions) != expected[index] {
+ errors <- &countError{got: len(program.Summary().Extensions), want: expected[index]}
+ return
+ }
+ fingerprints[index] = program.Fingerprint()
+ }(index, repository)
+ }
+ wait.Wait()
+ close(errors)
+ for err := range errors {
+ t.Fatal(err)
+ }
+ if fingerprints[0] == fingerprints[1] || fingerprints[1] == fingerprints[2] || fingerprints[0] == fingerprints[2] {
+ t.Fatalf("repository program identities collided: %#v", fingerprints)
+ }
+}
+
+func TestSnapshotPolicyDoesNotCreateCatalogDriftWithoutCompositionChange(t *testing.T) {
+ // control-law: only-repository-policy-that-changes-composition-enters-program-identity
+ left := repositoryFixture(t, []protocol.SubprocessExtensionSettings{})
+ right := repositoryFixture(t, []protocol.SubprocessExtensionSettings{})
+ path := filepath.Join(right, ".boatstack", "project.json")
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var configuration protocol.ProjectConfig
+ if err := json.Unmarshal(raw, &configuration); err != nil {
+ t.Fatal(err)
+ }
+ configuration.Policy.PlanApproval = "human-or-autonomy"
+ configuration.Policy.VisualEvidence = "required"
+ configuration.Hosts = append(configuration.Hosts, "sdk")
+ raw, err = json.Marshal(configuration)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, append(raw, '\n'), 0o600); err != nil {
+ t.Fatal(err)
+ }
+
+ compile := func(repository string) string {
+ program, err := StandardProgramForRepository(context.Background(), RepositoryProgramRequest{
+ Repository: repository, ExternalStateRoot: t.TempDir(), Host: "cli", CorrelationID: "policy-projection",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return program.Fingerprint()
+ }
+ if one, two := compile(left), compile(right); one != two {
+ t.Fatalf("snapshot-only policy changed compiled program identity: %s != %s", one, two)
+ }
+}
+
+type countError struct{ got, want int }
+
+func (e *countError) Error() string { return "extension count mismatch" }
+
+func repositoryFixture(t *testing.T, extensions []protocol.SubprocessExtensionSettings) string {
+ t.Helper()
+ repository := t.TempDir()
+ command := func(arguments ...string) {
+ cmd := exec.Command("git", arguments...)
+ cmd.Dir = repository
+ if output, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("git %v: %v\n%s", arguments, err, output)
+ }
+ }
+ command("init", "-q")
+ command("config", "user.email", "boatstack@example.invalid")
+ command("config", "user.name", "Boatstack Test")
+ if err := os.WriteFile(filepath.Join(repository, "README.md"), []byte("fixture\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ command("add", "README.md")
+ command("commit", "-q", "-m", "fixture")
+ if extensions != nil {
+ configuration := protocol.ProjectConfig{
+ SchemaVersion: protocol.ConfigSchemaVersion,
+ Project: protocol.ProjectSettings{Name: "fixture", DefaultBranch: "main", Commands: map[string]string{}},
+ Policy: protocol.PolicySettings{PlanApproval: "human", VisualEvidence: "optional"},
+ Hosts: []string{"cli"}, Extensions: extensions,
+ }
+ raw, err := json.Marshal(configuration)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(repository, ".boatstack"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(repository, ".boatstack", "project.json"), append(raw, '\n'), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return repository
+}
diff --git a/boatstack/examples/control_program_test.go b/boatstack/examples/control_program_test.go
new file mode 100644
index 0000000..7ad9d59
--- /dev/null
+++ b/boatstack/examples/control_program_test.go
@@ -0,0 +1,38 @@
+package examples_test
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/extension/releasenote"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
+ "github.com/operatorstack/boatstack/boatstack/sdk"
+)
+
+func Example_standardFlowWithReleaseNoteExtension() {
+ program, err := control.Compile(context.Background(), control.CompileRequest{
+ KernelVersion: "example-kernel",
+ Core: core.System(),
+ Flow: 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)
+ // Output:
+ // boatstack.core + boatstack.standard + boatstack.release-note: 63 transitions
+}
+
+func Example_sdkCustomKernel() {
+ _, err := sdk.NewKernel("",
+ sdk.WithFlow(standard.Definition()),
+ sdk.WithExtension(releasenote.Definition()),
+ )
+ fmt.Println(err == nil)
+ // Output:
+ // true
+}
diff --git a/boatstack/extension/inprocess.go b/boatstack/extension/inprocess.go
new file mode 100644
index 0000000..40eb13e
--- /dev/null
+++ b/boatstack/extension/inprocess.go
@@ -0,0 +1,28 @@
+// Package extension contains public helpers for trusted in-process Boatstack
+// extensions. Trust is assigned by the application compiling the program.
+package extension
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+)
+
+type InProcess struct {
+ manifest control.ExtensionManifest
+ runtime control.ExtensionRuntime
+}
+
+func NewInProcess(manifest control.ExtensionManifest, runtime control.ExtensionRuntime) (*InProcess, error) {
+ if runtime == nil {
+ return nil, fmt.Errorf("in-process extension requires a runtime")
+ }
+ return &InProcess{manifest: manifest, runtime: runtime}, nil
+}
+
+func (e *InProcess) ExtensionManifest(context.Context) (control.ExtensionManifest, error) {
+ return e.manifest, nil
+}
+
+func (e *InProcess) Runtime() control.ExtensionRuntime { return e.runtime }
diff --git a/boatstack/extension/releasenote/releasenote.go b/boatstack/extension/releasenote/releasenote.go
new file mode 100644
index 0000000..61f751e
--- /dev/null
+++ b/boatstack/extension/releasenote/releasenote.go
@@ -0,0 +1,198 @@
+// Package releasenote is a deterministic reference extension that adds a
+// conservative release-note evidence obligation to PR and merged goals.
+package releasenote
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+)
+
+const (
+ ID = "boatstack.release-note"
+ Version = "1.0.0"
+ FactID = "boatstack.release-note.present"
+ Transition = "boatstack.release-note.verify"
+ Resource = "boatstack.release-note.evidence"
+ Effect = "boatstack.release-note.write-evidence"
+ Verifier = "boatstack.release-note.verify-evidence"
+)
+
+type Extension struct{}
+
+func Definition() control.RuntimeExtension { return Extension{} }
+
+func (Extension) Runtime() control.ExtensionRuntime { return Extension{} }
+
+func (Extension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) {
+ transition := catalog.Transition{
+ ID: Transition, Version: 1, Class: catalog.EventOwnedLocal, SelectionClass: catalog.SelectionGoalRequired,
+ SourcePhases: []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, TargetPhases: []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal},
+ GoalKinds: []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id", "controller-id", "topology", "host", "correlation-id"},
+ Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot-fingerprint", "goal", "facet:" + FactID},
+ OwnedResources: []string{Resource}, Effect: Effect, LocalEffects: []catalog.EffectID{Effect}, Idempotent: true,
+ Prescription: catalog.Prescription{Operation: Transition, ExpectedPostcondition: "release-note evidence is verified"},
+ SourcePredicate: "reference-release-note-missing", AdmissionPredicate: "exact-extension-admission", TargetPredicate: "reference-release-note-verified", Verifier: Verifier,
+ SourceConditions: []catalog.FacetCondition{
+ known(model.FacetProgram, string(model.ProgramUnbound), string(model.ProgramCurrent)),
+ known(model.FacetPlan, string(model.PlanLocked)),
+ known(model.FacetVerification, string(model.VerificationCurrent)),
+ known(model.FacetName(FactID), "missing"),
+ },
+ TargetConditions: []catalog.FacetCondition{known(model.FacetName(FactID), "verified")},
+ Interruption: catalog.InterruptionContract{
+ Points: []string{"after-stage", "after-effect", "before-receipt"}, PartialState: []string{"namespaced evidence may be installed"},
+ Detection: "fresh extension observation", ResumeContract: "re-observe exact namespaced evidence", RollbackContract: "restore prior namespaced bytes",
+ CompensationContract: "not-required", Recovery: "recovery.escalate", RecoveryAuthority: "repository-policy", ResumptionPredicate: "program and extension evidence are current",
+ },
+ Reversibility: catalog.Reversible, TerminalEffect: "conjunctive-goal-obligation",
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "local-verification",
+ }
+ constraint := func(goal model.GoalKind) control.GoalConstraint {
+ return control.GoalConstraint{GoalKind: goal, Conditions: []catalog.FacetCondition{known(model.FacetName(FactID), "verified", "not-required")}}
+ }
+ return control.ExtensionManifest{
+ ID: ID, Version: Version, ProtocolVersion: control.ExtensionProtocolVersion,
+ SettingsSchema: json.RawMessage(`{"type":"object","additionalProperties":false}`),
+ Facts: []string{FactID}, Transitions: []control.Transition{transition},
+ GoalConstraints: []control.GoalConstraint{constraint(model.GoalOpenPR), constraint(model.GoalMerged)},
+ OwnedResources: []string{Resource}, Effects: []string{Effect}, Verifiers: []string{Verifier},
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
+ }, nil
+}
+
+func (Extension) Invoke(_ context.Context, request control.ExtensionRequest) (control.ExtensionResponse, error) {
+ response := control.ExtensionResponse{
+ ProtocolVersion: control.ExtensionProtocolVersion, Operation: request.Operation,
+ ExtensionID: ID, ExtensionVersion: Version, CorrelationID: request.CorrelationID,
+ }
+ switch request.Operation {
+ case control.ExtensionObserveOperation:
+ fact, err := observe(request.RepositoryRoot, request.ProgramFingerprint)
+ if err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ response.Facts = []control.ExtensionFact{fact}
+ case control.ExtensionPlanLocalEffectOperation:
+ if request.TransitionID != Transition {
+ return control.ExtensionResponse{}, fmt.Errorf("release-note extension received an unknown transition")
+ }
+ content, err := evidenceBytes(request.RepositoryRoot, request.ProgramFingerprint)
+ if err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ response.Writes = []control.ResourceWrite{{
+ Resource: Resource, Path: evidencePath(request.RepositoryRoot), Content: content, SHA256: digest(content), Mode: 0o600,
+ }}
+ case control.ExtensionVerifyOperation:
+ fact, err := observe(request.RepositoryRoot, request.ProgramFingerprint)
+ if err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ verified := fact.Status == model.FactKnown && fact.Value == "verified"
+ response.Verified = &verified
+ default:
+ return control.ExtensionResponse{}, fmt.Errorf("release-note extension does not support %q", request.Operation)
+ }
+ return response, nil
+}
+
+type evidence struct {
+ SchemaVersion int `json:"schema_version"`
+ ReleaseNotesSHA256 string `json:"release_notes_sha256"`
+ ProgramFingerprint string `json:"program_fingerprint"`
+}
+
+func observe(repository, programFingerprint string) (control.ExtensionFact, error) {
+ releaseDigest, relevant, err := releaseNotesDigest(repository)
+ if err != nil {
+ return control.ExtensionFact{}, err
+ }
+ if !relevant {
+ return control.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "not-required", Fingerprint: digest([]byte("not-required"))}, nil
+ }
+ raw, err := os.ReadFile(evidencePath(repository))
+ if err != nil {
+ if os.IsNotExist(err) {
+ return control.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "missing", Fingerprint: releaseDigest}, nil
+ }
+ return control.ExtensionFact{}, err
+ }
+ var record evidence
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&record); err != nil || record.SchemaVersion != 1 || record.ReleaseNotesSHA256 != releaseDigest || record.ProgramFingerprint != programFingerprint {
+ return control.ExtensionFact{ID: FactID, Status: model.FactStale, Detail: "release-note evidence is stale", Fingerprint: digest(raw)}, nil
+ }
+ return control.ExtensionFact{ID: FactID, Status: model.FactKnown, Value: "verified", Fingerprint: digest(raw)}, nil
+}
+
+func evidenceBytes(repository, programFingerprint string) ([]byte, error) {
+ releaseDigest, relevant, err := releaseNotesDigest(repository)
+ if err != nil {
+ return nil, err
+ }
+ if !relevant || len(programFingerprint) != 64 {
+ return nil, fmt.Errorf("release-note evidence requires relevant notes and exact program identity")
+ }
+ raw, err := json.MarshalIndent(evidence{SchemaVersion: 1, ReleaseNotesSHA256: releaseDigest, ProgramFingerprint: programFingerprint}, "", " ")
+ if err != nil {
+ return nil, err
+ }
+ return append(raw, '\n'), nil
+}
+
+func releaseNotesDigest(repository string) (string, bool, error) {
+ entries, err := os.ReadDir(filepath.Join(repository, "release-notes"))
+ if err != nil {
+ if os.IsNotExist(err) {
+ return "", false, nil
+ }
+ return "", false, err
+ }
+ var names []string
+ for _, entry := range entries {
+ if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".md") {
+ names = append(names, entry.Name())
+ }
+ }
+ if len(names) == 0 {
+ return "", false, nil
+ }
+ sort.Strings(names)
+ hash := sha256.New()
+ for _, name := range names {
+ raw, err := os.ReadFile(filepath.Join(repository, "release-notes", name))
+ if err != nil {
+ return "", false, err
+ }
+ _, _ = hash.Write([]byte(name))
+ _, _ = hash.Write([]byte{0})
+ _, _ = hash.Write(raw)
+ }
+ return hex.EncodeToString(hash.Sum(nil)), true, nil
+}
+
+func evidencePath(repository string) string {
+ return filepath.Join(repository, ".boatstack", "extensions", ID, "evidence.json")
+}
+
+func digest(raw []byte) string {
+ value := sha256.Sum256(raw)
+ return hex.EncodeToString(value[:])
+}
+
+func known(facet model.FacetName, values ...string) catalog.FacetCondition {
+ return catalog.FacetCondition{Facet: facet, Statuses: []model.FactStatus{model.FactKnown}, Values: values}
+}
diff --git a/boatstack/extension/releasenote/releasenote_test.go b/boatstack/extension/releasenote/releasenote_test.go
new file mode 100644
index 0000000..001c642
--- /dev/null
+++ b/boatstack/extension/releasenote/releasenote_test.go
@@ -0,0 +1,66 @@
+package releasenote
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/effects"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+)
+
+const testProgram = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+func TestReferenceExtensionPlansVerifiesAndInvalidatesNamespacedEvidence(t *testing.T) {
+ // control-law: release-note-obligation-is-evidenced-by-reversible-namespaced-bytes
+ repository := t.TempDir()
+ if err := os.MkdirAll(filepath.Join(repository, "release-notes"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(repository, "release-notes", "change.md"), []byte("### Change\n\nUser-facing behavior changed.\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ runtime := Extension{}
+ request := control.ExtensionRequest{
+ ProtocolVersion: control.ExtensionProtocolVersion, ExtensionID: ID, ExtensionVersion: Version,
+ ProgramFingerprint: testProgram, CorrelationID: "reference", RepositoryRoot: repository,
+ }
+ request.Operation = control.ExtensionObserveOperation
+ observed, err := runtime.Invoke(context.Background(), request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(observed.Facts) != 1 || observed.Facts[0].Status != model.FactKnown || observed.Facts[0].Value != "missing" {
+ t.Fatalf("initial fact = %#v", observed.Facts)
+ }
+ request.Operation, request.TransitionID = control.ExtensionPlanLocalEffectOperation, Transition
+ planned, err := runtime.Invoke(context.Background(), request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ prepared, err := effects.NewExtensionLocalPrepared(repository, ID, planned.Writes)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := prepared.Execute(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ request.Operation = control.ExtensionVerifyOperation
+ verified, err := runtime.Invoke(context.Background(), request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if verified.Verified == nil || !*verified.Verified {
+ t.Fatal("fresh reference evidence did not verify")
+ }
+ request.ProgramFingerprint = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ stale, err := runtime.Invoke(context.Background(), request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if stale.Verified == nil || *stale.Verified {
+ t.Fatal("evidence from another ControlProgram was accepted")
+ }
+}
diff --git a/boatstack/extension/subprocess/subprocess.go b/boatstack/extension/subprocess/subprocess.go
new file mode 100644
index 0000000..46fa0ca
--- /dev/null
+++ b/boatstack/extension/subprocess/subprocess.go
@@ -0,0 +1,237 @@
+// Package subprocess implements Boatstack's strict language-neutral extension
+// protocol. A subprocess extension is a trusted executable boundary, not an OS
+// sandbox.
+package subprocess
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/effects"
+)
+
+const maxRequestBytes = 1 << 20
+
+type Config struct {
+ ID string
+ Version string
+ Executable string
+ SHA256 string
+ Manifest json.RawMessage
+ Settings json.RawMessage
+ Limits control.SubprocessLimits
+}
+
+type Extension struct {
+ config Config
+ manifestRaw json.RawMessage
+ beforeStart func()
+}
+
+func New(config Config) (*Extension, error) {
+ if config.ID == "" || config.Version == "" || !filepath.IsAbs(config.Executable) || len(config.SHA256) != 64 || len(config.Manifest) == 0 {
+ return nil, fmt.Errorf("subprocess extension requires id, version, absolute executable path, SHA-256, and declarative manifest")
+ }
+ clean := filepath.Clean(config.Executable)
+ resolved, err := filepath.EvalSymlinks(clean)
+ if err != nil {
+ return nil, fmt.Errorf("resolve subprocess extension executable: %w", err)
+ }
+ if resolved != clean {
+ return nil, fmt.Errorf("subprocess extension executable path must be exact and symlink-free")
+ }
+ config.Executable = clean
+ if config.Limits.Deadline == 0 {
+ config.Limits.Deadline = 5 * time.Second
+ }
+ if config.Limits.StdoutBytes == 0 {
+ config.Limits.StdoutBytes = 1 << 20
+ }
+ if config.Limits.StderrBytes == 0 {
+ config.Limits.StderrBytes = 64 << 10
+ }
+ if config.Limits.Deadline < time.Millisecond || config.Limits.Deadline > 30*time.Second ||
+ config.Limits.StdoutBytes < 1 || config.Limits.StdoutBytes > 4<<20 ||
+ config.Limits.StderrBytes < 1 || config.Limits.StderrBytes > 1<<20 {
+ return nil, fmt.Errorf("subprocess extension limits are outside the supported bounds")
+ }
+ manifest, err := decodeManifest(config.Manifest)
+ if err != nil {
+ return nil, fmt.Errorf("decode declarative subprocess extension manifest: %w", err)
+ }
+ if manifest.ID != config.ID || manifest.Version != config.Version || manifest.ProtocolVersion != control.ExtensionProtocolVersion {
+ return nil, fmt.Errorf("subprocess extension manifest identity mismatch")
+ }
+ if manifest.ExecutableSHA256 != "" && manifest.ExecutableSHA256 != config.SHA256 {
+ return nil, fmt.Errorf("subprocess extension manifest executable fingerprint mismatch")
+ }
+ manifest.ExecutableSHA256 = config.SHA256
+ manifest.Settings = append(json.RawMessage(nil), config.Settings...)
+ manifestRaw, err := json.Marshal(manifest)
+ if err != nil {
+ return nil, fmt.Errorf("encode declarative subprocess extension manifest: %w", err)
+ }
+ extension := &Extension{config: config, manifestRaw: manifestRaw}
+ if _, err := extension.verifiedExecutable(); err != nil {
+ return nil, err
+ }
+ return extension, nil
+}
+
+func (e *Extension) Runtime() control.ExtensionRuntime { return e }
+
+func (e *Extension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) {
+ return decodeManifest(e.manifestRaw)
+}
+
+func (e *Extension) Invoke(ctx context.Context, request control.ExtensionRequest) (control.ExtensionResponse, error) {
+ if request.ProtocolVersion != control.ExtensionProtocolVersion || request.ExtensionID != e.config.ID || request.ExtensionVersion != e.config.Version || request.CorrelationID == "" {
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension request identity mismatch")
+ }
+ switch request.Operation {
+ case control.ExtensionManifestOperation, control.ExtensionObserveOperation, control.ExtensionPlanLocalEffectOperation,
+ control.ExtensionExecuteExternalOperation, control.ExtensionVerifyOperation, control.ExtensionRecoverOperation:
+ default:
+ return control.ExtensionResponse{}, fmt.Errorf("unsupported subprocess extension operation %q", request.Operation)
+ }
+ executable, err := e.verifiedExecutable()
+ if err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ stagedPath, cleanup, err := effects.StageVerifiedExecutable(e.config.Executable, executable)
+ if err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ defer cleanup()
+ if e.beforeStart != nil {
+ e.beforeStart()
+ }
+ raw, err := json.Marshal(request)
+ if err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ if len(raw) > maxRequestBytes {
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension request exceeds 1 MiB")
+ }
+ deadlineContext, cancel := context.WithTimeout(ctx, e.config.Limits.Deadline)
+ defer cancel()
+ command := exec.CommandContext(deadlineContext, stagedPath)
+ command.Env = []string{"LANG=C", "LC_ALL=C"}
+ command.Stdin = bytes.NewReader(raw)
+ stdout := &boundedBuffer{limit: e.config.Limits.StdoutBytes, cancel: cancel}
+ stderr := &boundedBuffer{limit: e.config.Limits.StderrBytes, cancel: cancel}
+ command.Stdout, command.Stderr = stdout, stderr
+ if err := command.Run(); err != nil {
+ if stdout.exceeded || stderr.exceeded {
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension output exceeded its bound")
+ }
+ if errors.Is(deadlineContext.Err(), context.DeadlineExceeded) {
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension deadline exceeded")
+ }
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension process failed")
+ }
+ if stdout.exceeded || stderr.exceeded {
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension output exceeded its bound")
+ }
+ var response control.ExtensionResponse
+ decoder := json.NewDecoder(bytes.NewReader(stdout.Bytes()))
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&response); err != nil {
+ return control.ExtensionResponse{}, fmt.Errorf("decode subprocess extension response: %w", err)
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension response contains trailing JSON")
+ }
+ if response.ProtocolVersion != control.ExtensionProtocolVersion || response.Operation != request.Operation ||
+ response.ExtensionID != e.config.ID || response.ExtensionVersion != e.config.Version || response.CorrelationID != request.CorrelationID {
+ return control.ExtensionResponse{}, fmt.Errorf("subprocess extension response identity mismatch")
+ }
+ if err := control.ValidateExtensionOperationResponse(request.Operation, response); err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ return response, nil
+}
+
+func (e *Extension) verifiedExecutable() ([]byte, error) {
+ info, err := os.Lstat(e.config.Executable)
+ if err != nil {
+ return nil, fmt.Errorf("stat subprocess extension executable: %w", err)
+ }
+ if info.Mode()&os.ModeSymlink != 0 {
+ return nil, fmt.Errorf("subprocess extension executable path drifted to a symlink")
+ }
+ if !info.Mode().IsRegular() {
+ return nil, fmt.Errorf("subprocess extension executable is not a regular file")
+ }
+ resolved, err := filepath.EvalSymlinks(e.config.Executable)
+ if err != nil {
+ return nil, fmt.Errorf("resolve subprocess extension executable: %w", err)
+ }
+ if resolved != e.config.Executable {
+ return nil, fmt.Errorf("subprocess extension executable path must remain exact and symlink-free")
+ }
+ raw, err := os.ReadFile(e.config.Executable)
+ if err != nil {
+ return nil, fmt.Errorf("read subprocess extension executable: %w", err)
+ }
+ digest := sha256.Sum256(raw)
+ if hex.EncodeToString(digest[:]) != e.config.SHA256 {
+ return nil, fmt.Errorf("subprocess extension executable fingerprint drifted")
+ }
+ return raw, nil
+}
+
+func decodeManifest(raw json.RawMessage) (control.ExtensionManifest, error) {
+ var manifest control.ExtensionManifest
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&manifest); err != nil {
+ return control.ExtensionManifest{}, err
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return control.ExtensionManifest{}, fmt.Errorf("manifest contains trailing JSON")
+ }
+ return manifest, nil
+}
+
+type boundedBuffer struct {
+ buffer bytes.Buffer
+ limit int64
+ exceeded bool
+ cancel context.CancelFunc
+}
+
+func (b *boundedBuffer) Write(value []byte) (int, error) {
+ if int64(b.buffer.Len()+len(value)) > b.limit {
+ if !b.exceeded {
+ b.exceeded = true
+ if b.cancel != nil {
+ b.cancel()
+ }
+ }
+ remaining := int(b.limit) - b.buffer.Len()
+ if remaining > 0 {
+ _, _ = b.buffer.Write(value[:remaining])
+ }
+ // Report the write as consumed while cancellation terminates the child.
+ // This keeps the os/exec copy goroutine deterministic and prevents an
+ // output-flooding process from running until the independent deadline.
+ return len(value), nil
+ }
+ return b.buffer.Write(value)
+}
+
+func (b *boundedBuffer) Bytes() []byte { return b.buffer.Bytes() }
diff --git a/boatstack/extension/subprocess/subprocess_test.go b/boatstack/extension/subprocess/subprocess_test.go
new file mode 100644
index 0000000..830f491
--- /dev/null
+++ b/boatstack/extension/subprocess/subprocess_test.go
@@ -0,0 +1,303 @@
+package subprocess
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
+)
+
+func fixtureManifest(t *testing.T, id string) json.RawMessage {
+ t.Helper()
+ raw, err := json.Marshal(control.ExtensionManifest{
+ ID: id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion,
+ SettingsSchema: json.RawMessage(`{"type":"object"}`), Facts: []string{id + ".present"},
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return raw
+}
+
+func fixtureExtension(t *testing.T) *Extension {
+ t.Helper()
+ if runtime.GOOS == "windows" {
+ t.Skip("the language-neutral Python fixture is exercised on POSIX; the Go protocol implementation remains platform-tested")
+ }
+ if _, err := os.Stat("/usr/bin/python3"); err != nil {
+ t.Skip("/usr/bin/python3 is unavailable")
+ }
+ path, err := filepath.Abs(filepath.Join("testdata", "reference_extension.py"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(raw)
+ extension, err := New(Config{
+ ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]),
+ Manifest: fixtureManifest(t, "fixture.echo"),
+ Limits: control.SubprocessLimits{Deadline: 30 * time.Second},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return extension
+}
+
+func pythonFixture(t *testing.T, mutate func(string) string) *Extension {
+ t.Helper()
+ if runtime.GOOS == "windows" {
+ t.Skip("the language-neutral Python fixture is exercised on POSIX")
+ }
+ raw, err := os.ReadFile(filepath.Join("testdata", "reference_extension.py"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ content := []byte(mutate(string(raw)))
+ path := filepath.Join(exactPath(t, t.TempDir()), "extension.py")
+ if err := os.WriteFile(path, content, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(content)
+ extension, err := New(Config{
+ ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]),
+ Manifest: fixtureManifest(t, "fixture.echo"),
+ Limits: control.SubprocessLimits{Deadline: 30 * time.Second},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return extension
+}
+
+func exactPath(t *testing.T, path string) string {
+ t.Helper()
+ resolved, err := filepath.EvalSymlinks(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return resolved
+}
+
+func TestPythonFixtureImplementsStrictLanguageNeutralProtocol(t *testing.T) {
+ // control-law: subprocess-extension-is-exact-bounded-and-environment-clean
+ t.Setenv("BOATSTACK_TEST_SECRET", "must-not-cross")
+ extension := fixtureExtension(t)
+ manifest, err := extension.ExtensionManifest(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if manifest.ID != "fixture.echo" || manifest.ExecutableSHA256 == "" || len(manifest.Facts) != 1 {
+ t.Fatalf("manifest = %#v", manifest)
+ }
+ response, err := extension.Invoke(context.Background(), control.ExtensionRequest{
+ ProtocolVersion: 1, Operation: control.ExtensionObserveOperation, ExtensionID: "fixture.echo", ExtensionVersion: "1.0.0", CorrelationID: "observe-fixture",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(response.Facts) != 1 || response.Facts[0].Value != "clean" {
+ t.Fatalf("subprocess inherited arbitrary environment or lost fact: %#v", response)
+ }
+}
+
+func TestExecutableDriftFailsBeforeInvocation(t *testing.T) {
+ source, err := os.ReadFile(filepath.Join("testdata", "reference_extension.py"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(exactPath(t, t.TempDir()), "extension.py")
+ if err := os.WriteFile(path, source, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(source)
+ extension, err := New(Config{ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo")})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, append(source, []byte("\n# drift\n")...), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ _, err = extension.Invoke(context.Background(), control.ExtensionRequest{ProtocolVersion: 1, Operation: control.ExtensionObserveOperation, ExtensionID: "fixture.echo", ExtensionVersion: "1.0.0", CorrelationID: "drift"})
+ if err == nil || !strings.Contains(err.Error(), "fingerprint drifted") {
+ t.Fatalf("drift error = %v", err)
+ }
+}
+
+func TestInvocationExecutesTheExactVerifiedBytesAfterPathReplacement(t *testing.T) {
+ // control-law: executable-identity-is-bound-to-the-bytes-actually-executed
+ if runtime.GOOS == "windows" {
+ t.Skip("POSIX atomic replacement semantics are exercised here")
+ }
+ extension := pythonFixture(t, func(source string) string { return source })
+ replacement := []byte("#!/bin/sh\nexit 42\n")
+ extension.beforeStart = func() {
+ temporary := extension.config.Executable + ".replacement"
+ if err := os.WriteFile(temporary, replacement, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(temporary, extension.config.Executable); err != nil {
+ t.Fatal(err)
+ }
+ }
+ response, err := extension.Invoke(context.Background(), control.ExtensionRequest{
+ ProtocolVersion: 1, Operation: control.ExtensionObserveOperation, ExtensionID: "fixture.echo", ExtensionVersion: "1.0.0", CorrelationID: "atomic-replacement",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(response.Facts) != 1 || response.Facts[0].Value != "clean" {
+ t.Fatalf("response = %#v", response)
+ }
+}
+
+func TestDeclarativeManifestDoesNotStartExecutable(t *testing.T) {
+ // control-law: program-compilation-never-executes-repository-selected-code
+ if runtime.GOOS == "windows" {
+ t.Skip("POSIX script fixture")
+ }
+ directory := exactPath(t, t.TempDir())
+ marker := filepath.Join(directory, "started")
+ content := []byte("#!/bin/sh\ntouch \"" + marker + "\"\n")
+ path := filepath.Join(directory, "extension.sh")
+ if err := os.WriteFile(path, content, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(content)
+ extension, err := New(Config{ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo")})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := control.Compile(context.Background(), control.CompileRequest{
+ KernelVersion: "test-kernel", Core: core.System(), Flow: standard.Definition(), Extensions: []control.Extension{extension},
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(marker); !os.IsNotExist(err) {
+ t.Fatalf("declarative manifest started executable: %v", err)
+ }
+}
+
+func TestExecutableCannotBecomeASymlinkAfterConstruction(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("symlink replacement semantics are exercised on POSIX")
+ }
+ source, err := os.ReadFile(filepath.Join("testdata", "reference_extension.py"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ directory := exactPath(t, t.TempDir())
+ path := filepath.Join(directory, "extension.py")
+ replacement := filepath.Join(directory, "replacement.py")
+ if err := os.WriteFile(path, source, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(replacement, source, 0o700); err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(source)
+ extension, err := New(Config{ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo")})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(replacement, path); err != nil {
+ t.Fatal(err)
+ }
+ _, err = extension.Invoke(context.Background(), control.ExtensionRequest{ProtocolVersion: 1, Operation: control.ExtensionObserveOperation, ExtensionID: "fixture.echo", ExtensionVersion: "1.0.0", CorrelationID: "symlink-drift"})
+ if err == nil || !strings.Contains(err.Error(), "drifted to a symlink") {
+ t.Fatalf("symlink drift error = %v", err)
+ }
+}
+
+func TestDeadlineAndOutputBoundsFailClosed(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("POSIX script fixtures are not executable on Windows")
+ }
+ for _, fixture := range []struct {
+ name, body, want string
+ deadline time.Duration
+ stdout int64
+ }{
+ {"deadline", "#!/bin/sh\nsleep 1\n", "deadline exceeded", 10 * time.Millisecond, 1024},
+ {"stdout", "#!/bin/sh\nprintf '%02048d' 1\n", "output exceeded", 30 * time.Second, 64},
+ } {
+ t.Run(fixture.name, func(t *testing.T) {
+ path := filepath.Join(exactPath(t, t.TempDir()), "fixture.sh")
+ if err := os.WriteFile(path, []byte(fixture.body), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256([]byte(fixture.body))
+ extension, err := New(Config{ID: "fixture.echo", Version: "1.0.0", Executable: path, SHA256: hex.EncodeToString(digest[:]), Manifest: fixtureManifest(t, "fixture.echo"), Limits: control.SubprocessLimits{Deadline: fixture.deadline, StdoutBytes: fixture.stdout, StderrBytes: 64}})
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = extension.Invoke(context.Background(), control.ExtensionRequest{ProtocolVersion: 1, Operation: control.ExtensionObserveOperation, ExtensionID: "fixture.echo", ExtensionVersion: "1.0.0", CorrelationID: fixture.name})
+ if err == nil || !strings.Contains(err.Error(), fixture.want) {
+ t.Fatalf("error = %v, want %q", err, fixture.want)
+ }
+ })
+ }
+}
+
+func TestBoundedBufferCancelsAtTheFirstOverflow(t *testing.T) {
+ // control-law: output-bound-observation-terminates-the-subprocess
+ cancelled := 0
+ buffer := &boundedBuffer{limit: 4, cancel: func() { cancelled++ }}
+ value := []byte("overflow")
+ written, err := buffer.Write(value)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if written != len(value) || !buffer.exceeded || cancelled != 1 || string(buffer.Bytes()) != "over" {
+ t.Fatalf("write = %d, exceeded = %v, cancelled = %d, bytes = %q", written, buffer.exceeded, cancelled, buffer.Bytes())
+ }
+ if _, err := buffer.Write(value); err != nil {
+ t.Fatal(err)
+ }
+ if cancelled != 1 {
+ t.Fatalf("cancel called %d times, want once", cancelled)
+ }
+}
+
+func TestStrictJSONRejectsUnknownTrailingAndWrongOperationPayloads(t *testing.T) {
+ // control-law: subprocess-response-is-strict-correlated-and-operation-typed
+ cases := map[string]func(string) string{
+ "unknown-field": func(source string) string {
+ return strings.Replace(source, "json.dump(response, sys.stdout, separators=(\",\", \":\"))", "response[\"unknown_field\"] = True\njson.dump(response, sys.stdout, separators=(\",\", \":\"))", 1)
+ },
+ "trailing-json": func(source string) string {
+ return strings.Replace(source, "json.dump(response, sys.stdout, separators=(\",\", \":\"))", "json.dump(response, sys.stdout, separators=(\",\", \":\"))\nprint(\"{}\", end=\"\")", 1)
+ },
+ "wrong-payload": func(source string) string {
+ return strings.Replace(source, "elif operation == \"observe\":", "elif operation == \"observe\":\n response[\"verified\"] = True", 1)
+ },
+ }
+ for name, mutate := range cases {
+ t.Run(name, func(t *testing.T) {
+ extension := pythonFixture(t, mutate)
+ _, err := extension.Invoke(context.Background(), control.ExtensionRequest{
+ ProtocolVersion: 1, Operation: control.ExtensionObserveOperation, ExtensionID: "fixture.echo", ExtensionVersion: "1.0.0", CorrelationID: name,
+ })
+ if err == nil {
+ t.Fatalf("%s response was accepted", name)
+ }
+ })
+ }
+}
diff --git a/boatstack/extension/subprocess/testdata/reference_extension.py b/boatstack/extension/subprocess/testdata/reference_extension.py
new file mode 100755
index 0000000..41943bd
--- /dev/null
+++ b/boatstack/extension/subprocess/testdata/reference_extension.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python3
+import json
+import os
+import sys
+
+request = json.load(sys.stdin)
+operation = request["operation"]
+response = {
+ "protocol_version": 1,
+ "operation": operation,
+ "extension_id": request["extension_id"],
+ "extension_version": request["extension_version"],
+ "correlation_id": request["correlation_id"],
+}
+
+if operation == "manifest":
+ response["manifest"] = {
+ "id": "fixture.echo",
+ "version": "1.0.0",
+ "protocol_version": 1,
+ "settings_schema": {"type": "object"},
+ "facts": ["fixture.echo.present"],
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ }
+elif operation == "observe":
+ value = "leaked" if "BOATSTACK_TEST_SECRET" in os.environ else "clean"
+ response["facts"] = [{
+ "id": "fixture.echo.present",
+ "status": "known",
+ "value": value,
+ "fingerprint": "fixture-observation",
+ }]
+elif operation == "verify":
+ response["verified"] = True
+elif operation in ("plan-local-effect", "recover"):
+ response["writes"] = []
+elif operation == "execute-external":
+ response["external_result"] = {"settlement": "settled"}
+else:
+ response["error_class"] = "unsupported-operation"
+ response["error"] = "unsupported"
+
+json.dump(response, sys.stdout, separators=(",", ":"))
diff --git a/boatstack/internal/kernel/catalog/completeness_test.go b/boatstack/flow/standard/completeness_test.go
similarity index 66%
rename from boatstack/internal/kernel/catalog/completeness_test.go
rename to boatstack/flow/standard/completeness_test.go
index 6673487..85a1383 100644
--- a/boatstack/internal/kernel/catalog/completeness_test.go
+++ b/boatstack/flow/standard/completeness_test.go
@@ -1,4 +1,4 @@
-package catalog
+package standard_test
import (
"go/ast"
@@ -12,7 +12,9 @@ import (
"strings"
"testing"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
func sourceRoot(t *testing.T) string {
@@ -21,24 +23,24 @@ func sourceRoot(t *testing.T) string {
if !ok {
t.Fatal("cannot locate source inventory")
}
- return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
+ return filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
}
func TestEveryControllingFacetAndEventIsClassifiedByTheRuntimeCatalog(t *testing.T) {
// control-law: runtime-catalog-is-the-complete-executable-model
- registry := Default()
+ registry := testprogram.StandardRegistry()
classified := map[model.FacetName]bool{model.FacetPhase: true}
families := map[string]int{}
for _, transition := range registry.All() {
- for _, condition := range append(append([]FacetCondition(nil), transition.SourceConditions...), transition.TargetConditions...) {
+ for _, condition := range append(append([]catalog.FacetCondition(nil), transition.SourceConditions...), transition.TargetConditions...) {
classified[condition.Facet] = true
}
family := familyFor(transition.ID)
families[family]++
- if transition.Controllable() && transition.Effect != EffectID(transition.ID) {
+ if transition.Controllable() && transition.Effect != catalog.EffectID(transition.ID) {
t.Errorf("transition %s effect=%s; effect identity must be exact", transition.ID, transition.Effect)
}
- if transition.Class == EventOwnedExternal && !containsAuthority(transition.AuthorityAll, AuthorityProvider) {
+ if transition.Class == catalog.EventOwnedExternal && !containsAuthority(transition.AuthorityAll, catalog.AuthorityProvider) {
t.Errorf("external transition %s does not require provider authority", transition.ID)
}
}
@@ -47,7 +49,7 @@ func TestEveryControllingFacetAndEventIsClassifiedByTheRuntimeCatalog(t *testing
t.Errorf("controlling facet %s is absent from executable predicates", facet)
}
}
- want := map[string]int{"invocation-engagement": 6, "installation-runtime-configuration": 8, "goal-plan": 9, "workspace": 8, "gate-evidence-delivery": 8, "publication": 6, "recovery": 3, "external": 13}
+ want := map[string]int{"invocation-engagement": 6, "installation-runtime-configuration": 8, "catalog": 1, "goal-plan": 9, "workspace": 8, "gate-evidence-delivery": 8, "publication": 6, "recovery": 3, "external": 13}
for family, count := range want {
if families[family] != count {
t.Errorf("family %s=%d, want %d", family, families[family], count)
@@ -55,7 +57,7 @@ func TestEveryControllingFacetAndEventIsClassifiedByTheRuntimeCatalog(t *testing
}
}
-func containsAuthority(values []AuthorityClass, wanted AuthorityClass) bool {
+func containsAuthority(values []catalog.AuthorityClass, wanted catalog.AuthorityClass) bool {
for _, value := range values {
if value == wanted {
return true
@@ -64,13 +66,15 @@ func containsAuthority(values []AuthorityClass, wanted AuthorityClass) bool {
return false
}
-func familyFor(id TransitionID) string {
+func familyFor(id catalog.TransitionID) string {
value := string(id)
switch {
case strings.HasPrefix(value, "engagement."), strings.HasPrefix(value, "invocation."), strings.HasPrefix(value, "repository."):
return "invocation-engagement"
case strings.HasPrefix(value, "installation."), strings.HasPrefix(value, "runtime."), strings.HasPrefix(value, "configuration."):
return "installation-runtime-configuration"
+ case strings.HasPrefix(value, "catalog."):
+ return "catalog"
case strings.HasPrefix(value, "goal."), strings.HasPrefix(value, "plan."):
return "goal-plan"
case strings.HasPrefix(value, "workspace."):
@@ -142,12 +146,13 @@ func TestSourceInventoryHasNoWriterOrLifecycleAuthorityOutsideOwnedPackages(t *t
t.Errorf("managed writer os.%s escaped effects package in %s", selector.Sel.Name, relative)
}
if importPath == "os/exec" && (selector.Sel.Name == "Command" || selector.Sel.Name == "CommandContext") {
- if relative != "internal/effects/command_boundary.go" && relative != "internal/plant/resolver.go" {
+ if relative != "internal/effects/command_boundary.go" && relative != "internal/plant/resolver.go" && relative != "extension/subprocess/subprocess.go" {
t.Errorf("unclassified command boundary in %s", relative)
}
}
if strings.HasSuffix(importPath, "/internal/kernel/model") && lifecycleSelector(selector.Sel.Name) &&
- !strings.HasPrefix(relative, "internal/kernel/") && !strings.HasPrefix(relative, "internal/effects/") && !strings.HasPrefix(relative, "internal/plant/") {
+ !strings.HasPrefix(relative, "internal/kernel/") && !strings.HasPrefix(relative, "internal/effects/") && !strings.HasPrefix(relative, "internal/plant/") &&
+ !strings.HasPrefix(relative, "control/") && !strings.HasPrefix(relative, "flow/") && !strings.HasPrefix(relative, "extension/") {
t.Errorf("lifecycle selector model.%s escaped kernel/plant/effects in %s", selector.Sel.Name, relative)
}
return true
@@ -169,16 +174,16 @@ func TestSourceInventoryHasNoWriterOrLifecycleAuthorityOutsideOwnedPackages(t *t
func TestEveryControllableRuntimeEventHasAnExecutableStateReducer(t *testing.T) {
// control-law: registry-entry-cannot-exist-without-runtime-effect-reduction
- path := filepath.Join(sourceRoot(t), "internal", "kernel", "reducer", "reducer.go")
+ path := filepath.Join(sourceRoot(t), "internal", "effects", "state_reducer.go")
parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
if err != nil {
t.Fatal(err)
}
- covered := map[TransitionID]bool{}
- registry := Default()
+ covered := map[catalog.TransitionID]bool{}
+ registry := testprogram.StandardRegistry()
for _, declaration := range parsed.Decls {
function, ok := declaration.(*ast.FuncDecl)
- if !ok || function.Name.Name != "Apply" {
+ if !ok || function.Name.Name != "applyStateTransition" {
continue
}
ast.Inspect(function.Body, func(node ast.Node) bool {
@@ -195,7 +200,7 @@ func TestEveryControllableRuntimeEventHasAnExecutableStateReducer(t *testing.T)
if unquoteErr != nil {
t.Fatal(unquoteErr)
}
- id := TransitionID(value)
+ id := catalog.TransitionID(value)
if transition, exists := registry.Lookup(id); exists && transition.Controllable() {
covered[id] = true
}
@@ -210,11 +215,59 @@ func TestEveryControllableRuntimeEventHasAnExecutableStateReducer(t *testing.T)
}
}
+func TestPackageImportsPreserveControlProgramDependencyDirection(t *testing.T) {
+ // control-law: kernel-mechanism-cannot-depend-on-standard-flow-or-product-surfaces
+ root := sourceRoot(t)
+ forbiddenKernel := []string{"/flow/standard", "/distribution", "/sdk", "/cmd/boatstack-helper"}
+ forbiddenFlow := []string{"/distribution", "/sdk", "/cmd/boatstack-helper", "/internal/surfaces"}
+ err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ return walkErr
+ }
+ if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
+ return nil
+ }
+ relative, err := filepath.Rel(root, path)
+ if err != nil {
+ return err
+ }
+ relative = filepath.ToSlash(relative)
+ kernelOwned := relative == "kernel.go" || relative == "program_effects.go" || relative == "program_observer.go" || strings.HasPrefix(relative, "internal/kernel/")
+ flowOwned := strings.HasPrefix(relative, "flow/")
+ if !kernelOwned && !flowOwned {
+ return nil
+ }
+ parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
+ if err != nil {
+ return err
+ }
+ for _, item := range parsed.Imports {
+ value, _ := strconv.Unquote(item.Path.Value)
+ for _, forbidden := range forbiddenKernel {
+ if kernelOwned && strings.Contains(value, forbidden) {
+ t.Errorf("kernel mechanism %s imports forbidden layer %s", relative, value)
+ }
+ }
+ for _, forbidden := range forbiddenFlow {
+ if flowOwned && strings.Contains(value, forbidden) {
+ t.Errorf("primary flow %s imports forbidden surface %s", relative, value)
+ }
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
func classifiedProductionFile(relative string) bool {
- return relative == "v2_kernel.go" || strings.HasPrefix(relative, "cmd/boatstack-helper/") ||
+ return relative == "kernel.go" || relative == "program_effects.go" || relative == "program_observer.go" || strings.HasPrefix(relative, "cmd/boatstack-helper/") ||
+ strings.HasPrefix(relative, "control/") || strings.HasPrefix(relative, "core/") ||
+ strings.HasPrefix(relative, "flow/") || strings.HasPrefix(relative, "distribution/") || strings.HasPrefix(relative, "extension/") ||
strings.HasPrefix(relative, "internal/kernel/") || strings.HasPrefix(relative, "internal/plant/") ||
strings.HasPrefix(relative, "internal/effects/") || strings.HasPrefix(relative, "internal/surfaces/") ||
- strings.HasPrefix(relative, "internal/retromine/") || strings.HasPrefix(relative, "sdk/") ||
+ strings.HasPrefix(relative, "internal/retromine/") || strings.HasPrefix(relative, "internal/testprogram/") || strings.HasPrefix(relative, "sdk/") ||
strings.HasPrefix(relative, "analysis/")
}
diff --git a/boatstack/internal/kernel/catalog/historical_test.go b/boatstack/flow/standard/historical_test.go
similarity index 93%
rename from boatstack/internal/kernel/catalog/historical_test.go
rename to boatstack/flow/standard/historical_test.go
index ef5c557..f4c5b8b 100644
--- a/boatstack/internal/kernel/catalog/historical_test.go
+++ b/boatstack/flow/standard/historical_test.go
@@ -1,6 +1,7 @@
-package catalog_test
+package standard_test
import (
+ "context"
"encoding/json"
"os"
"path/filepath"
@@ -11,11 +12,25 @@ import (
"testing"
"time"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
+func historicalGoalContracts() catalog.GoalContracts {
+ manifest, err := standard.Definition().FlowManifest(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil)
+ if err != nil {
+ panic(err)
+ }
+ return contracts
+}
+
type historicalCorpus struct {
SchemaVersion int `json:"schema_version"`
FixtureCount int `json:"fixture_count"`
@@ -39,7 +54,7 @@ type historicalFixture struct {
func loadHistoricalCorpus(t *testing.T) historicalCorpus {
t.Helper()
- raw, err := os.ReadFile("../../../testdata/v2-scenarios/historical.json")
+ raw, err := os.ReadFile("../../testdata/v2-scenarios/historical.json")
if err != nil {
t.Fatal(err)
}
@@ -100,8 +115,8 @@ func authoritySet(values []catalog.AuthorityClass) catalog.AuthoritySet {
func TestHistoricalFailureCorpusUsesTheRuntimeControlLaw(t *testing.T) {
// control-law: historical failures bind to executable catalog predicates
corpus := loadHistoricalCorpus(t)
- registry := catalog.Default()
- control := supervisor.New(registry)
+ registry := testprogram.StandardRegistry()
+ control := supervisor.New(registry, historicalGoalContracts())
seenNames := map[string]bool{}
for _, fixture := range corpus.Fixtures {
fixture := fixture
diff --git a/boatstack/flow/standard/standard.go b/boatstack/flow/standard/standard.go
new file mode 100644
index 0000000..a2df7ca
--- /dev/null
+++ b/boatstack/flow/standard/standard.go
@@ -0,0 +1,112 @@
+// Package standard owns Boatstack's first-party opinionated software-delivery
+// flow. It has no CLI, SDK, host-rendering, journal, or receipt authority.
+package standard
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+)
+
+const (
+ ID = "boatstack.standard"
+ Version = "1.0.0"
+)
+
+type definition struct{}
+
+//go:embed transitions.json
+var transitionDeclarations []byte
+
+func Definition() control.FlowDefinition { return definition{} }
+
+func (definition) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+ transitions, err := decodeTransitions()
+ if err != nil {
+ return control.PrimaryFlowManifest{}, err
+ }
+ resources, effects, verifiers, recoveries := declarations(transitions)
+ return control.PrimaryFlowManifest{
+ ID: ID, Version: Version, ProtocolVersion: control.FlowProtocolVersion, RuntimeMode: control.FlowRuntimeNative,
+ SupportedGoals: []control.GoalKind{
+ model.GoalApprovedPlan, model.GoalVerified, model.GoalOpenPR,
+ model.GoalMerged, model.GoalAbandoned,
+ },
+ GoalContracts: []control.GoalContract{
+ contract(model.GoalApprovedPlan,
+ known(model.FacetPlan, string(model.PlanApproved))),
+ contract(model.GoalVerified,
+ known(model.FacetVerification, string(model.VerificationCurrent)),
+ known(model.FacetConfiguration, string(model.ConfigurationVerified)),
+ known(model.FacetRuntime, string(model.RuntimeVerified)),
+ known(model.FacetDelivery, string(model.DeliveryTerminal))),
+ contract(model.GoalOpenPR,
+ known(model.FacetVerification, string(model.VerificationCurrent)),
+ known(model.FacetConfiguration, string(model.ConfigurationVerified)),
+ known(model.FacetRuntime, string(model.RuntimeVerified)),
+ known(model.FacetPublication, string(model.PublicationOpen))),
+ contract(model.GoalMerged,
+ known(model.FacetPublication, string(model.PublicationMerged)),
+ known(model.FacetDelivery, string(model.DeliveryTerminal)),
+ known(model.FacetWorkspace, string(model.WorkspaceLanded), string(model.WorkspaceAbsent))),
+ contract(model.GoalAbandoned,
+ known(model.FacetDelivery, string(model.DeliveryDiscarded)),
+ known(model.FacetWorkspace, string(model.WorkspaceAbandoned), string(model.WorkspaceAbsent))),
+ },
+ Facts: []string{"plan", "workspace", "delivery", "verification", "publication"},
+ Transitions: transitions, OwnedResources: resources, Effects: effects, Verifiers: verifiers, RecoveryTransitions: recoveries,
+ Settings: json.RawMessage(`{}`), ConfigurationSchema: json.RawMessage(`{"type":"object"}`),
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
+ }, nil
+}
+
+func decodeTransitions() ([]control.Transition, error) {
+ decoder := json.NewDecoder(bytes.NewReader(transitionDeclarations))
+ decoder.DisallowUnknownFields()
+ var transitions []control.Transition
+ if err := decoder.Decode(&transitions); err != nil {
+ return nil, fmt.Errorf("decode StandardFlow transitions: %w", err)
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return nil, fmt.Errorf("StandardFlow transition declarations contain trailing JSON")
+ }
+ return transitions, nil
+}
+
+func declarations(transitions []control.Transition) ([]string, []string, []string, []control.TransitionID) {
+ var resources, effects, verifiers []string
+ var recoveries []control.TransitionID
+ seenResources, seenEffects, seenVerifiers, seenRecoveries := map[string]bool{}, map[string]bool{}, map[string]bool{}, map[control.TransitionID]bool{}
+ for _, transition := range transitions {
+ for _, resource := range transition.OwnedResources {
+ if !seenResources[resource] {
+ seenResources[resource], resources = true, append(resources, resource)
+ }
+ }
+ if transition.Effect != "" && !seenEffects[string(transition.Effect)] {
+ seenEffects[string(transition.Effect)], effects = true, append(effects, string(transition.Effect))
+ }
+ if transition.Verifier != "" && !seenVerifiers[transition.Verifier] {
+ seenVerifiers[transition.Verifier], verifiers = true, append(verifiers, transition.Verifier)
+ }
+ if transition.Class == control.EventRecovery && !seenRecoveries[transition.ID] {
+ seenRecoveries[transition.ID], recoveries = true, append(recoveries, transition.ID)
+ }
+ }
+ return resources, effects, verifiers, recoveries
+}
+
+func contract(goal model.GoalKind, conditions ...control.FacetCondition) control.GoalContract {
+ return control.GoalContract{GoalKind: goal, Conditions: conditions}
+}
+
+func known(facet model.FacetName, values ...string) control.FacetCondition {
+ return control.FacetCondition{Facet: facet, Statuses: []model.FactStatus{model.FactKnown}, Values: values}
+}
diff --git a/boatstack/flow/standard/standard_test.go b/boatstack/flow/standard/standard_test.go
new file mode 100644
index 0000000..42fa4d3
--- /dev/null
+++ b/boatstack/flow/standard/standard_test.go
@@ -0,0 +1,48 @@
+package standard_test
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
+)
+
+func TestManifestOwnsOnlyStandardDeliverySemantics(t *testing.T) {
+ // control-law: standard-flow-declarations-live-outside-kernel-mechanism
+ manifest, err := standard.Definition().FlowManifest(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if manifest.ID != standard.ID || manifest.Version != standard.Version || len(manifest.Transitions) != 30 {
+ t.Fatalf("StandardFlow identity/count = %s@%s/%d", manifest.ID, manifest.Version, len(manifest.Transitions))
+ }
+ for _, transition := range manifest.Transitions {
+ id := string(transition.ID)
+ if !hasPrefix(id, "plan.", "workspace.", "gate.", "evidence.", "delivery.", "publication.") {
+ t.Errorf("StandardFlow owns non-delivery transition %s", id)
+ }
+ }
+ if len(manifest.GoalContracts) != 5 {
+ t.Fatalf("goal contracts = %d, want 5", len(manifest.GoalContracts))
+ }
+ for _, goal := range []control.GoalKind{control.GoalApprovedPlan, control.GoalVerified, control.GoalOpenPR, control.GoalMerged, control.GoalAbandoned} {
+ found := false
+ for _, contract := range manifest.GoalContracts {
+ found = found || contract.GoalKind == goal
+ }
+ if !found {
+ t.Errorf("missing goal contract %s", goal)
+ }
+ }
+}
+
+func hasPrefix(value string, prefixes ...string) bool {
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(value, prefix) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/boatstack/internal/kernel/supervisor/supervisor_test.go b/boatstack/flow/standard/supervisor_parity_test.go
similarity index 78%
rename from boatstack/internal/kernel/supervisor/supervisor_test.go
rename to boatstack/flow/standard/supervisor_parity_test.go
index 71dd1f6..4b0b3f0 100644
--- a/boatstack/internal/kernel/supervisor/supervisor_test.go
+++ b/boatstack/flow/standard/supervisor_parity_test.go
@@ -1,14 +1,30 @@
-package supervisor
+package standard_test
import (
+ "context"
"path/filepath"
"testing"
"time"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+ . "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
+func testGoalContracts() catalog.GoalContracts {
+ manifest, err := standard.Definition().FlowManifest(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil)
+ if err != nil {
+ panic(err)
+ }
+ return contracts
+}
+
func snapshotFor(t *testing.T, phase model.ProtocolPhase, terminal model.TerminalStatus) model.Snapshot {
t.Helper()
e := model.Evidence{Source: "fixture", Fingerprint: "fixture", ObservedAt: time.Unix(10, 0).UTC()}
@@ -73,7 +89,7 @@ func openPRSnapshot(t *testing.T, recordedGates ...string) (model.Snapshot, mode
func TestTerminalGoalOutranksLocalTransitions(t *testing.T) {
// control-law: configured-terminal-outranks-local-lifecycle
- s := New(catalog.Default())
+ s := New(testprogram.StandardRegistry(), testGoalContracts())
decision := s.Resolve(snapshotFor(t, model.PhaseTerminal, model.TerminalEstablished), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "")
if decision.Kind != DecisionTerminal || decision.Transition != nil {
t.Fatalf("decision = %#v, want terminal without transition", decision)
@@ -89,7 +105,7 @@ func TestExplicitPostTerminalCleanupRemainsAdmissible(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- decision := New(catalog.Default()).Resolve(canonical, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "workspace.cleanup")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(canonical, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "workspace.cleanup")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "workspace.cleanup" {
t.Fatalf("decision = %#v, want explicit post-terminal cleanup", decision)
}
@@ -97,7 +113,7 @@ func TestExplicitPostTerminalCleanupRemainsAdmissible(t *testing.T) {
func TestTerminalEvidenceForOldGoalDoesNotTerminateNewGoal(t *testing.T) {
// control-law: terminal-evidence-is-bound-to-exact-goal-not-local-phase
- s := New(catalog.Default())
+ s := New(testprogram.StandardRegistry(), testGoalContracts())
newGoal := model.Goal{ID: "next-goal", Kind: model.GoalOpenPR, DeliveryID: "delivery"}
decision := s.Resolve(snapshotFor(t, model.PhaseTerminal, model.TerminalEstablished), newGoal, catalog.AuthoritySet{catalog.AuthorityHuman: true}, "goal.configure")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "goal.configure" {
@@ -111,14 +127,14 @@ func TestUntargetedResolutionReconfiguresDifferentGoalAndSkipsSatisfiedGoal(t *t
authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true}
newGoal := model.Goal{ID: "new-goal", Kind: model.GoalOpenPR, DeliveryID: "delivery"}
- decision := New(catalog.Default()).Resolve(snapshot, newGoal, authority, "")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, newGoal, authority, "")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "goal.configure" {
t.Fatalf("different-goal decision = %#v, want goal.configure", decision)
}
snapshot.Plan = model.Known(model.PlanValid, snapshot.Plan.Evidence[0])
snapshot = recanonicalize(t, snapshot)
- decision = New(catalog.Default()).Resolve(snapshot, goalFor(), authority, "")
+ decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "plan.approve" {
t.Fatalf("exact-goal decision = %#v, want plan.approve without goal.configure stutter", decision)
}
@@ -132,20 +148,46 @@ func TestUntargetedResolutionExcludesExplicitControlTransitions(t *testing.T) {
snapshot = recanonicalize(t, snapshot)
authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true}
- decision := New(catalog.Default()).Resolve(snapshot, goalFor(), authority, "")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "gate.build.record" {
t.Fatalf("untargeted decision = %#v, want gate.build.record", decision)
}
- decision = New(catalog.Default()).Resolve(snapshot, goalFor(), authority, "plan.invalidate")
+ decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "plan.invalidate")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "plan.invalidate" {
t.Fatalf("explicit invalidation decision = %#v, want requested plan.invalidate", decision)
}
- decision = New(catalog.Default()).Resolve(snapshot, goalFor(), authority, "delivery.slice.advance")
+ decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), authority, "delivery.slice.advance")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "delivery.slice.advance" {
t.Fatalf("explicit slice-marker decision = %#v, want requested delivery.slice.advance", decision)
}
}
+func TestSelectionClassOutranksComponentLocalPriority(t *testing.T) {
+ // control-law: bounded-selection-class-prevents-lower-layer-priority-inversion
+ transitions := testprogram.StandardRegistry().All()
+ for index := range transitions {
+ switch transitions[index].ID {
+ case "gate.build.record":
+ transitions[index].SelectionClass = catalog.SelectionGoalRequired
+ transitions[index].Priority = 999
+ case "gate.test.record":
+ transitions[index].SelectionClass = catalog.SelectionFlowProgress
+ transitions[index].Priority = 1
+ }
+ }
+ registry, err := catalog.New(transitions)
+ if err != nil {
+ t.Fatal(err)
+ }
+ snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal)
+ snapshot.Plan = model.Known(model.PlanLocked, snapshot.Plan.Evidence[0])
+ snapshot = recanonicalize(t, snapshot)
+ decision := New(registry, testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "")
+ if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "gate.build.record" {
+ t.Fatalf("decision = %#v, want higher selection class despite lower-layer numeric priority", decision)
+ }
+}
+
func TestUntargetedResolutionUsesCurrentGateEvidenceForProgress(t *testing.T) {
// control-law: verified-gate-progress-is-derived-from-canonical-evidence
authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true}
@@ -153,7 +195,7 @@ func TestUntargetedResolutionUsesCurrentGateEvidenceForProgress(t *testing.T) {
snapshot.Publication = model.Known(model.PublicationNone, snapshot.Publication.Evidence[0])
snapshot = recanonicalize(t, snapshot)
- decision := New(catalog.Default()).Resolve(snapshot, goal, authority, "")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goal, authority, "")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "gate.test.record" {
t.Fatalf("one-gate decision = %#v, want gate.test.record", decision)
}
@@ -161,7 +203,7 @@ func TestUntargetedResolutionUsesCurrentGateEvidenceForProgress(t *testing.T) {
snapshot, goal = openPRSnapshot(t, "build", "test", "review")
snapshot.Publication = model.Known(model.PublicationNone, snapshot.Publication.Evidence[0])
snapshot = recanonicalize(t, snapshot)
- decision = New(catalog.Default()).Resolve(snapshot, goal, authority, "")
+ decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goal, authority, "")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "publication.preview" {
t.Fatalf("complete-gates decision = %#v, want publication.preview", decision)
}
@@ -170,7 +212,7 @@ func TestUntargetedResolutionUsesCurrentGateEvidenceForProgress(t *testing.T) {
func TestUntargetedResolutionStopsAtSelectedProviderBoundary(t *testing.T) {
// control-law: unavailable-authority-cannot-be-skipped-for-a-lower-priority-effect
snapshot, goal := openPRSnapshot(t, "build", "test", "review")
- supervisor := New(catalog.Default())
+ supervisor := New(testprogram.StandardRegistry(), testGoalContracts())
authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true}
decision := supervisor.Resolve(snapshot, goal, authority, "")
@@ -186,7 +228,7 @@ func TestUntargetedResolutionStopsAtSelectedProviderBoundary(t *testing.T) {
func TestRequestedTransitionRequiresExactAuthority(t *testing.T) {
// control-law: useful-action-is-not-effect-authority
- s := New(catalog.Default())
+ s := New(testprogram.StandardRegistry(), testGoalContracts())
decision := s.Resolve(snapshotFor(t, model.PhaseActive, model.TerminalNonterminal), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "plan.approve")
if decision.Kind != DecisionFrontier {
t.Fatalf("decision = %s, want FRONTIER", decision.Kind)
@@ -196,7 +238,7 @@ func TestRequestedTransitionRequiresExactAuthority(t *testing.T) {
func TestPlanApprovalPolicyDistinguishesHumanFromAutonomyAuthority(t *testing.T) {
snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal)
autonomy := catalog.AuthoritySet{catalog.AuthorityAutonomy: true}
- decision := New(catalog.Default()).Resolve(snapshot, goalFor(), autonomy, "plan.approve")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), autonomy, "plan.approve")
if decision.Kind != DecisionFrontier {
t.Fatalf("human-only policy decision = %#v, want FRONTIER", decision)
}
@@ -205,7 +247,7 @@ func TestPlanApprovalPolicyDistinguishesHumanFromAutonomyAuthority(t *testing.T)
if err != nil {
t.Fatal(err)
}
- decision = New(catalog.Default()).Resolve(snapshot, goalFor(), autonomy, "plan.approve")
+ decision = New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), autonomy, "plan.approve")
if decision.Kind != DecisionPrescribed {
t.Fatalf("autonomy-enabled policy decision = %#v, want PRESCRIBED", decision)
}
@@ -218,7 +260,7 @@ func TestDisabledHostCannotRequestManagedTransition(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- decision := New(catalog.Default()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "plan.approve")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "plan.approve")
if decision.Kind != DecisionRefused {
t.Fatalf("disabled host decision = %#v, want REFUSED", decision)
}
@@ -233,7 +275,7 @@ func TestHighRiskReviewPolicyRequiresHumanAuthority(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- supervisor := New(catalog.Default())
+ supervisor := New(testprogram.StandardRegistry(), testGoalContracts())
decision := supervisor.Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "gate.review.record")
if decision.Kind != DecisionFrontier {
t.Fatalf("repository-only high-risk review = %#v, want FRONTIER", decision)
@@ -251,7 +293,7 @@ func TestVisualEvidenceOffRefusesAttachment(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- decision := New(catalog.Default()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "evidence.visual.attach")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "evidence.visual.attach")
if decision.Kind != DecisionRefused {
t.Fatalf("visual-off decision = %#v, want REFUSED", decision)
}
@@ -259,7 +301,7 @@ func TestVisualEvidenceOffRefusesAttachment(t *testing.T) {
func TestRecoveryModeOnlyPrescribesRecoveryTransition(t *testing.T) {
// control-law: recovery-outranks-slice-position
- s := New(catalog.Default())
+ s := New(testprogram.StandardRegistry(), testGoalContracts())
decision := s.Resolve(snapshotFor(t, model.PhaseRecovery, model.TerminalNonterminal), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "recovery.resume")
if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.Class != catalog.EventRecovery {
t.Fatalf("decision = %#v, want a recovery prescription", decision)
@@ -268,7 +310,7 @@ func TestRecoveryModeOnlyPrescribesRecoveryTransition(t *testing.T) {
func TestRecoveryModeRejectsARecoveryEventOutsideExactJournalContract(t *testing.T) {
snapshot := snapshotFor(t, model.PhaseRecovery, model.TerminalNonterminal)
- decision := New(catalog.Default()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "recovery.rollback")
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityHuman: true}, "recovery.rollback")
if decision.Kind != DecisionRefused {
t.Fatalf("unpermitted recovery decision = %#v, want REFUSED", decision)
}
@@ -276,7 +318,7 @@ func TestRecoveryModeRejectsARecoveryEventOutsideExactJournalContract(t *testing
func TestUncontrollableEventCannotBeRequested(t *testing.T) {
// control-law: surfaces-cannot-assert-external-facts
- s := New(catalog.Default())
+ s := New(testprogram.StandardRegistry(), testGoalContracts())
decision := s.Resolve(snapshotFor(t, model.PhaseActive, model.TerminalNonterminal), goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "external.pr-merged")
if decision.Kind != DecisionRefused {
t.Fatalf("decision = %s, want REFUSED", decision.Kind)
@@ -284,13 +326,13 @@ func TestUncontrollableEventCannotBeRequested(t *testing.T) {
}
func TestGuardDeniesDestructionAndRoutesManagedBypassThroughAdmission(t *testing.T) {
- s := New(catalog.Default())
+ s := New(testprogram.StandardRegistry(), testGoalContracts())
snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal)
destructive := s.Guard(snapshot, CommandIntent{Class: IntentDestructive, Operation: "git.reset-hard", Fingerprint: "fingerprint"})
if destructive.Allowed {
t.Fatal("destructive command was allowed")
}
- managed := s.Guard(snapshot, CommandIntent{Class: IntentManagedBypass, Operation: "publication.push", Fingerprint: "fingerprint", Transition: "publication.execute"})
+ managed := s.Guard(snapshot, CommandIntent{Class: IntentManagedBypass, Operation: "publication.push", Fingerprint: "fingerprint"})
if managed.Allowed || managed.RequiredTransition != "publication.execute" {
t.Fatalf("managed bypass decision = %#v", managed)
}
diff --git a/boatstack/flow/standard/transitions.json b/boatstack/flow/standard/transitions.json
new file mode 100644
index 0000000..49be596
--- /dev/null
+++ b/boatstack/flow/standard/transitions.json
@@ -0,0 +1,6192 @@
+[
+ {
+ "id": "plan.create",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "plan"
+ ],
+ "effect": "plan.create",
+ "local_effects": [
+ "plan.create"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "delivery_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "plan.create",
+ "expected_postcondition": "predicate:target-phase:plan.create"
+ },
+ "source_predicate": "predicate:source-phase:plan.create",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "absent",
+ "invalid",
+ "stale"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.create",
+ "target_predicate": "predicate:target-phase:plan.create",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "draft"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.create",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.create"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 35
+ },
+ {
+ "id": "plan.validate",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "plan-evidence"
+ ],
+ "effect": "plan.validate",
+ "local_effects": [
+ "plan.validate"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "plan.validate",
+ "expected_postcondition": "predicate:target-phase:plan.validate"
+ },
+ "source_predicate": "predicate:source-phase:plan.validate",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "draft"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.validate",
+ "target_predicate": "predicate:target-phase:plan.validate",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "valid"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.validate",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.validate"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 40
+ },
+ {
+ "id": "plan.approve",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "authority",
+ "source_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime",
+ "facet:configuration-policy"
+ ],
+ "owned_resources": [
+ "approval"
+ ],
+ "effect": "plan.approve",
+ "local_effects": [
+ "plan.approve"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "plan_fingerprint",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "actor",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "plan.approve",
+ "expected_postcondition": "predicate:target-phase:plan.approve"
+ },
+ "source_predicate": "predicate:source-phase:plan.approve",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "valid"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "configuration-policy",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.approve",
+ "target_predicate": "predicate:target-phase:plan.approve",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "approved"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.approve",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.approve"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "authority_rule": "plan-approval"
+ },
+ "priority": 45
+ },
+ {
+ "id": "plan.activate",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "delivery-state"
+ ],
+ "effect": "plan.activate",
+ "local_effects": [
+ "plan.activate"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "plan.activate",
+ "expected_postcondition": "predicate:target-phase:plan.activate"
+ },
+ "source_predicate": "predicate:source-phase:plan.activate",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "approved"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.activate",
+ "target_predicate": "predicate:target-phase:plan.activate",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "locked"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.activate",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.activate"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 50
+ },
+ {
+ "id": "plan.amend",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "plan"
+ ],
+ "effect": "plan.amend",
+ "local_effects": [
+ "plan.amend"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "delivery_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "plan.amend",
+ "expected_postcondition": "predicate:target-phase:plan.amend"
+ },
+ "source_predicate": "predicate:source-phase:plan.amend",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "approved",
+ "locked",
+ "stale",
+ "amendment-required"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.amend",
+ "target_predicate": "predicate:target-phase:plan.amend",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "amendment-required"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.amend",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.amend"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 42
+ },
+ {
+ "id": "plan.approve-amendment",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "authority",
+ "source_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime",
+ "facet:configuration-policy"
+ ],
+ "owned_resources": [
+ "approval"
+ ],
+ "effect": "plan.approve-amendment",
+ "local_effects": [
+ "plan.approve-amendment"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "plan_fingerprint",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "actor",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "plan.approve-amendment",
+ "expected_postcondition": "predicate:target-phase:plan.approve-amendment"
+ },
+ "source_predicate": "predicate:source-phase:plan.approve-amendment",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "amendment-required"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "configuration-policy",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.approve-amendment",
+ "target_predicate": "predicate:target-phase:plan.approve-amendment",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "approved"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.approve-amendment",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.approve-amendment"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "authority_rule": "plan-approval"
+ },
+ "priority": 46
+ },
+ {
+ "id": "plan.invalidate",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE",
+ "OBSERVED"
+ ],
+ "target_phases": [
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal"
+ ],
+ "owned_resources": [
+ "plan-evidence"
+ ],
+ "effect": "plan.invalidate",
+ "local_effects": [
+ "plan.invalidate"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "plan.invalidate",
+ "expected_postcondition": "predicate:target-phase:plan.invalidate"
+ },
+ "source_predicate": "predicate:source-phase:plan.invalidate",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "draft",
+ "valid",
+ "approved",
+ "locked",
+ "stale"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.invalidate",
+ "target_predicate": "predicate:target-phase:plan.invalidate",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "invalid"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.invalidate",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.invalidate"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 41
+ },
+ {
+ "id": "plan.abandon",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "authority",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:delivery",
+ "facet:program",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal"
+ ],
+ "owned_resources": [
+ "plan"
+ ],
+ "effect": "plan.abandon",
+ "local_effects": [
+ "plan.abandon"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "plan.abandon",
+ "expected_postcondition": "predicate:target-phase:plan.abandon"
+ },
+ "source_predicate": "predicate:source-phase:plan.abandon",
+ "source_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "uninitialized",
+ "planning",
+ "approved",
+ "active",
+ "gates-passed",
+ "published",
+ "amendment",
+ "invalid",
+ "recovery"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:plan.abandon",
+ "target_predicate": "predicate:target-phase:plan.abandon",
+ "target_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "discarded"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:plan.abandon",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:plan.abandon"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-safe-abandonment",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 90
+ },
+ {
+ "id": "workspace.cut",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "workspace"
+ ],
+ "effect": "workspace.cut",
+ "local_effects": [
+ "workspace.cut"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "branch",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "base_ref",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "destination",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.cut",
+ "expected_postcondition": "predicate:target-phase:workspace.cut"
+ },
+ "source_predicate": "predicate:source-phase:workspace.cut",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "absent"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.cut",
+ "target_predicate": "predicate:target-phase:workspace.cut",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "cut"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.cut",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "workspace.reconcile",
+ "recovery_authority": "declared-by:workspace.reconcile",
+ "resumption_predicate": "recovery-contract-for:workspace.cut"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 52,
+ "allows_worktree_transfer": true
+ },
+ {
+ "id": "workspace.sync",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "workspace"
+ ],
+ "effect": "workspace.sync",
+ "local_effects": [
+ "workspace.sync"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "branch",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.sync",
+ "expected_postcondition": "predicate:target-phase:workspace.sync"
+ },
+ "source_predicate": "predicate:source-phase:workspace.sync",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "cut",
+ "active"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.sync",
+ "target_predicate": "predicate:target-phase:workspace.sync",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.sync",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:workspace.sync"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 58
+ },
+ {
+ "id": "workspace.activate",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "workspace"
+ ],
+ "effect": "workspace.activate",
+ "local_effects": [
+ "workspace.activate"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "branch",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.activate",
+ "expected_postcondition": "predicate:target-phase:workspace.activate"
+ },
+ "source_predicate": "predicate:source-phase:workspace.activate",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "cut",
+ "active"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.activate",
+ "target_predicate": "predicate:target-phase:workspace.activate",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.activate",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:workspace.activate"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 53
+ },
+ {
+ "id": "workspace.publish",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "open-or-updated-pr",
+ "merged-delivery"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "workspace-state"
+ ],
+ "effect": "workspace.publish",
+ "local_effects": [
+ "workspace.publish"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "branch",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.publish",
+ "expected_postcondition": "predicate:target-phase:workspace.publish"
+ },
+ "source_predicate": "predicate:source-phase:workspace.publish",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.publish",
+ "target_predicate": "predicate:target-phase:workspace.publish",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "published"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.publish",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:workspace.publish"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 75
+ },
+ {
+ "id": "workspace.cleanup",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "workspace"
+ ],
+ "effect": "workspace.cleanup",
+ "local_effects": [
+ "workspace.cleanup"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "branch",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.cleanup",
+ "expected_postcondition": "predicate:target-phase:workspace.cleanup"
+ },
+ "source_predicate": "predicate:source-phase:workspace.cleanup",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "landed",
+ "abandoned"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "established"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.cleanup",
+ "target_predicate": "predicate:target-phase:workspace.cleanup",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "absent"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.cleanup",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "forbidden-after-destructive-git-effect",
+ "rollback_contract": "not-guaranteed-after-worktree-removal",
+ "compensation_contract": "no-generic-compensation",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:workspace.cleanup"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "managed_operations": [
+ "workspace.remove"
+ ]
+ },
+ "priority": 92,
+ "allows_worktree_transfer": true
+ },
+ {
+ "id": "workspace.reap",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "target_phases": [
+ "OBSERVED",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "workspace"
+ ],
+ "effect": "workspace.reap",
+ "local_effects": [
+ "workspace.reap"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "branch",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.reap",
+ "expected_postcondition": "predicate:target-phase:workspace.reap"
+ },
+ "source_predicate": "predicate:source-phase:workspace.reap",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "landed",
+ "abandoned"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "established"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.reap",
+ "target_predicate": "predicate:target-phase:workspace.reap",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "absent"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.reap",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "forbidden-after-destructive-git-effect",
+ "rollback_contract": "not-guaranteed-after-worktree-removal",
+ "compensation_contract": "no-generic-compensation",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:workspace.reap"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 98,
+ "allows_worktree_transfer": true
+ },
+ {
+ "id": "workspace.abandon",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:workspace",
+ "facet:program",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal"
+ ],
+ "owned_resources": [
+ "workspace"
+ ],
+ "effect": "workspace.abandon",
+ "local_effects": [
+ "workspace.abandon"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "branch",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.abandon",
+ "expected_postcondition": "predicate:target-phase:workspace.abandon"
+ },
+ "source_predicate": "predicate:source-phase:workspace.abandon",
+ "source_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "cut",
+ "active",
+ "published",
+ "attention-required"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.abandon",
+ "target_predicate": "predicate:target-phase:workspace.abandon",
+ "target_conditions": [
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "abandoned"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.abandon",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:workspace.abandon"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-safe-abandonment",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 91
+ },
+ {
+ "id": "workspace.reconcile",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_RECOVERY",
+ "class": "recovery",
+ "source_phases": [
+ "RECOVERY",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "DORMANT",
+ "OBSERVED",
+ "ACTIVE",
+ "FRONTIER",
+ "TERMINAL",
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:recovery-info",
+ "facet:program",
+ "facet:engagement",
+ "facet:goal"
+ ],
+ "owned_resources": [
+ "workspace"
+ ],
+ "effect": "workspace.reconcile",
+ "local_effects": [
+ "workspace.reconcile"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "transaction_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "workspace.reconcile",
+ "expected_postcondition": "predicate:target-phase:workspace.reconcile"
+ },
+ "source_predicate": "predicate:source-phase:workspace.reconcile",
+ "source_conditions": [
+ {
+ "facet": "recovery-info",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:workspace.reconcile",
+ "target_predicate": "predicate:target-phase:workspace.reconcile",
+ "target_conditions": [
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:workspace.reconcile",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "never-blindly-retry-interrupted-recovery",
+ "rollback_contract": "preserve-original-transaction-group",
+ "compensation_contract": "escalation-only-after-nested-interruption",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:workspace.reconcile"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 2,
+ "allows_worktree_transfer": true
+ },
+ {
+ "id": "gate.build.record",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:delivery",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "gate-evidence"
+ ],
+ "effect": "gate.build.record",
+ "local_effects": [
+ "gate.build.record"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_fingerprint",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "gate.build.record",
+ "expected_postcondition": "predicate:target-phase:gate.build.record"
+ },
+ "source_predicate": "predicate:source-phase:gate.build.record",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "locked"
+ ]
+ },
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "gates-passed"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:gate.build.record",
+ "target_predicate": "predicate:target-phase:gate.build.record",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:gate.build.record",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:gate.build.record"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "current_evidence_prefix": "gate-evidence:build:"
+ },
+ "priority": 61,
+ "binds_source_revision": true
+ },
+ {
+ "id": "gate.test.record",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:delivery",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "gate-evidence"
+ ],
+ "effect": "gate.test.record",
+ "local_effects": [
+ "gate.test.record"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_fingerprint",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "gate.test.record",
+ "expected_postcondition": "predicate:target-phase:gate.test.record"
+ },
+ "source_predicate": "predicate:source-phase:gate.test.record",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "locked"
+ ]
+ },
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "gates-passed"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:gate.test.record",
+ "target_predicate": "predicate:target-phase:gate.test.record",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:gate.test.record",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:gate.test.record"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "current_evidence_prefix": "gate-evidence:test:"
+ },
+ "priority": 62,
+ "binds_source_revision": true
+ },
+ {
+ "id": "gate.review.record",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:delivery",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime",
+ "facet:configuration-policy"
+ ],
+ "owned_resources": [
+ "gate-evidence"
+ ],
+ "effect": "gate.review.record",
+ "local_effects": [
+ "gate.review.record"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_fingerprint",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "gate.review.record",
+ "expected_postcondition": "predicate:target-phase:gate.review.record"
+ },
+ "source_predicate": "predicate:source-phase:gate.review.record",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "locked"
+ ]
+ },
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "gates-passed"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "configuration-policy",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:gate.review.record",
+ "target_predicate": "predicate:target-phase:gate.review.record",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:gate.review.record",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:gate.review.record"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "authority_rule": "independent-high-risk-review",
+ "current_evidence_prefix": "gate-evidence:review:"
+ },
+ "priority": 63,
+ "binds_source_revision": true
+ },
+ {
+ "id": "gate.change.record",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:delivery",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "gate-evidence"
+ ],
+ "effect": "gate.change.record",
+ "local_effects": [
+ "gate.change.record"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_fingerprint",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "gate.change.record",
+ "expected_postcondition": "predicate:target-phase:gate.change.record"
+ },
+ "source_predicate": "predicate:source-phase:gate.change.record",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "locked"
+ ]
+ },
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "gates-passed"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:gate.change.record",
+ "target_predicate": "predicate:target-phase:gate.change.record",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:gate.change.record",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:gate.change.record"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "current_evidence_prefix": "gate-evidence:change:"
+ },
+ "priority": 64,
+ "binds_source_revision": true
+ },
+ {
+ "id": "gate.journey.record",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:delivery",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "gate-evidence"
+ ],
+ "effect": "gate.journey.record",
+ "local_effects": [
+ "gate.journey.record"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "evidence_fingerprint",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "gate.journey.record",
+ "expected_postcondition": "predicate:target-phase:gate.journey.record"
+ },
+ "source_predicate": "predicate:source-phase:gate.journey.record",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "locked"
+ ]
+ },
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "gates-passed"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:gate.journey.record",
+ "target_predicate": "predicate:target-phase:gate.journey.record",
+ "target_conditions": [
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:gate.journey.record",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:gate.journey.record"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "current_evidence_prefix": "gate-evidence:journey:"
+ },
+ "priority": 64,
+ "binds_source_revision": true
+ },
+ {
+ "id": "evidence.visual.attach",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:delivery",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime",
+ "facet:configuration-policy"
+ ],
+ "owned_resources": [
+ "evidence"
+ ],
+ "effect": "evidence.visual.attach",
+ "local_effects": [
+ "evidence.visual.attach"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "manifest_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "privacy_receipt",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "evidence.visual.attach",
+ "expected_postcondition": "predicate:target-phase:evidence.visual.attach"
+ },
+ "source_predicate": "predicate:source-phase:evidence.visual.attach",
+ "source_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "gates-passed",
+ "published"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "configuration-policy",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:evidence.visual.attach",
+ "target_predicate": "predicate:target-phase:evidence.visual.attach",
+ "target_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:evidence.visual.attach",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:evidence.visual.attach"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "required_when": "visual-evidence-required",
+ "availability_rule": "visual-evidence-enabled",
+ "current_evidence_prefix": "visual-evidence:"
+ },
+ "priority": 66,
+ "binds_source_revision": true
+ },
+ {
+ "id": "evidence.approval.revoke",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "authority",
+ "source_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "FRONTIER"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal"
+ ],
+ "owned_resources": [
+ "approval"
+ ],
+ "effect": "evidence.approval.revoke",
+ "local_effects": [
+ "evidence.approval.revoke"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "evidence.approval.revoke",
+ "expected_postcondition": "predicate:target-phase:evidence.approval.revoke"
+ },
+ "source_predicate": "predicate:source-phase:evidence.approval.revoke",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "approved",
+ "locked"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:evidence.approval.revoke",
+ "target_predicate": "predicate:target-phase:evidence.approval.revoke",
+ "target_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "valid"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:evidence.approval.revoke",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:evidence.approval.revoke"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 44
+ },
+ {
+ "id": "delivery.slice.advance",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "goal_kinds": [
+ "approved-plan",
+ "verified-implementation",
+ "open-or-updated-pr",
+ "merged-delivery",
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:delivery",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "delivery-state"
+ ],
+ "effect": "delivery.slice.advance",
+ "local_effects": [
+ "delivery.slice.advance"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "slice_id",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "source_revision",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "delivery.slice.advance",
+ "expected_postcondition": "predicate:target-phase:delivery.slice.advance"
+ },
+ "source_predicate": "predicate:source-phase:delivery.slice.advance",
+ "source_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:delivery.slice.advance",
+ "target_predicate": "predicate:target-phase:delivery.slice.advance",
+ "target_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:delivery.slice.advance",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:delivery.slice.advance"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 68,
+ "binds_source_revision": true
+ },
+ {
+ "id": "publication.preview",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE"
+ ],
+ "goal_kinds": [
+ "open-or-updated-pr",
+ "merged-delivery"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:plan",
+ "facet:verification",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "publication-preview"
+ ],
+ "effect": "publication.preview",
+ "local_effects": [
+ "publication.preview"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "base_ref",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "head_ref",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "body_path",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "publication.preview",
+ "expected_postcondition": "predicate:target-phase:publication.preview"
+ },
+ "source_predicate": "predicate:source-phase:publication.preview",
+ "source_conditions": [
+ {
+ "facet": "plan",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "locked"
+ ]
+ },
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ },
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "published"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:publication.preview",
+ "target_predicate": "predicate:target-phase:publication.preview",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "candidate"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:publication.preview",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:publication.preview"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 72
+ },
+ {
+ "id": "publication.execute",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-external",
+ "source_phases": [
+ "ACTIVE"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "RECOVERY"
+ ],
+ "goal_kinds": [
+ "open-or-updated-pr",
+ "merged-delivery"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "authority_all": [
+ "external-provider"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication",
+ "facet:verification",
+ "facet:workspace",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "publication"
+ ],
+ "effect": "publication.execute",
+ "external_effects": [
+ "publication.execute"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "preview_fingerprint",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "publication.execute",
+ "expected_postcondition": "predicate:target-phase:publication.execute"
+ },
+ "source_predicate": "predicate:source-phase:publication.execute",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "candidate"
+ ]
+ },
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ },
+ {
+ "facet": "workspace",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "active",
+ "published"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:publication.execute",
+ "target_predicate": "predicate:target-phase:publication.execute",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "published-not-landed"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:publication.execute",
+ "interruption": {
+ "points": [
+ "before-request",
+ "after-request",
+ "before-settlement-observation",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "request-not-sent",
+ "request-possibly-accepted",
+ "provider-settlement-unobserved"
+ ],
+ "detection": "pending-journal-plus-fresh-provider-observation",
+ "resume_contract": "forbidden-without-provider-observation",
+ "rollback_contract": "not-provable-after-external-request",
+ "compensation_contract": "provider-reconciliation-only",
+ "recovery": "publication.reconcile",
+ "recovery_authority": "declared-by:publication.reconcile",
+ "resumption_predicate": "recovery-contract-for:publication.execute"
+ },
+ "reversibility": "compensatable",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "managed_operations": [
+ "publication.create",
+ "publication.push"
+ ]
+ },
+ "priority": 76,
+ "authority_fingerprint_parameter": "preview_fingerprint"
+ },
+ {
+ "id": "publication.observe",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_PROGRESS",
+ "class": "owned-local",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "RECOVERY",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "TERMINAL",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "goal_kinds": [
+ "open-or-updated-pr",
+ "merged-delivery"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "repository-policy"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "publication-evidence"
+ ],
+ "effect": "publication.observe",
+ "local_effects": [
+ "publication.observe"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "publication_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "publication.observe",
+ "expected_postcondition": "predicate:target-phase:publication.observe"
+ },
+ "source_predicate": "predicate:source-phase:publication.observe",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "candidate",
+ "published-not-landed",
+ "open",
+ "closed-unmerged",
+ "unavailable",
+ "conflicting"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:publication.observe",
+ "target_predicate": "predicate:target-phase:publication.observe",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "open",
+ "merged",
+ "closed-unmerged",
+ "unavailable",
+ "conflicting"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:publication.observe",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:publication.observe"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 77
+ },
+ {
+ "id": "publication.reconcile",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "FLOW_RECOVERY",
+ "class": "recovery",
+ "source_phases": [
+ "RECOVERY",
+ "UNRESOLVED"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "TERMINAL",
+ "FRONTIER",
+ "UNRESOLVED"
+ ],
+ "goal_kinds": [
+ "open-or-updated-pr",
+ "merged-delivery"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "external-provider"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:recovery-info",
+ "facet:publication",
+ "facet:program",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "publication"
+ ],
+ "effect": "publication.reconcile",
+ "local_effects": [
+ "publication.reconcile"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "publication_id",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "transaction_id",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "publication.reconcile",
+ "expected_postcondition": "predicate:target-phase:publication.reconcile"
+ },
+ "source_predicate": "predicate:source-phase:publication.reconcile",
+ "source_conditions": [
+ {
+ "facet": "recovery-info",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:publication.reconcile",
+ "target_predicate": "predicate:target-phase:publication.reconcile",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "open",
+ "merged",
+ "closed-unmerged",
+ "unavailable",
+ "conflicting"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:publication.reconcile",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "never-blindly-retry-interrupted-recovery",
+ "rollback_contract": "preserve-original-transaction-group",
+ "compensation_contract": "escalation-only-after-nested-interruption",
+ "recovery": "recovery.escalate",
+ "recovery_authority": "declared-by:recovery.escalate",
+ "resumption_predicate": "recovery-contract-for:publication.reconcile"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-configured-goal-after-fresh-observation",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 1,
+ "authority_fingerprint_parameter": "publication_id"
+ },
+ {
+ "id": "publication.correct",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "owned-external",
+ "source_phases": [
+ "OBSERVED",
+ "ACTIVE",
+ "TERMINAL"
+ ],
+ "target_phases": [
+ "ACTIVE",
+ "RECOVERY"
+ ],
+ "goal_kinds": [
+ "open-or-updated-pr",
+ "merged-delivery"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human",
+ "autonomy"
+ ],
+ "authority_all": [
+ "external-provider"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:publication",
+ "facet:verification",
+ "facet:program",
+ "facet:recovery",
+ "facet:transaction",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal",
+ "facet:configuration",
+ "facet:runtime"
+ ],
+ "owned_resources": [
+ "publication"
+ ],
+ "effect": "publication.correct",
+ "external_effects": [
+ "publication.correct"
+ ],
+ "idempotent": true,
+ "parameters": [
+ {
+ "name": "publication_id",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "body_path",
+ "required": true,
+ "secret": false
+ },
+ {
+ "name": "body_sha256",
+ "required": true,
+ "secret": false
+ }
+ ],
+ "prescription": {
+ "operation": "publication.correct",
+ "expected_postcondition": "predicate:target-phase:publication.correct"
+ },
+ "source_predicate": "predicate:source-phase:publication.correct",
+ "source_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "open"
+ ]
+ },
+ {
+ "facet": "verification",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "current"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal",
+ "stale",
+ "established"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ },
+ {
+ "facet": "configuration",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ },
+ {
+ "facet": "runtime",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "verified"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:publication.correct",
+ "target_predicate": "predicate:target-phase:publication.correct",
+ "target_conditions": [
+ {
+ "facet": "publication",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "published-not-landed"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:publication.correct",
+ "interruption": {
+ "points": [
+ "before-request",
+ "after-request",
+ "before-settlement-observation",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "request-not-sent",
+ "request-possibly-accepted",
+ "provider-settlement-unobserved"
+ ],
+ "detection": "pending-journal-plus-fresh-provider-observation",
+ "resume_contract": "forbidden-without-provider-observation",
+ "rollback_contract": "not-provable-after-external-request",
+ "compensation_contract": "provider-reconciliation-only",
+ "recovery": "publication.reconcile",
+ "recovery_authority": "declared-by:publication.reconcile",
+ "resumption_predicate": "recovery-contract-for:publication.correct"
+ },
+ "reversibility": "compensatable",
+ "terminal_effect": "none",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {
+ "managed_operations": [
+ "publication.edit",
+ "publication.ready",
+ "publication.api-write"
+ ]
+ },
+ "priority": 80,
+ "authority_fingerprint_parameter": "body_sha256"
+ },
+ {
+ "id": "publication.abandon",
+ "version": 1,
+ "origin": {
+ "kind": "",
+ "id": "",
+ "version": "",
+ "manifest_fingerprint": ""
+ },
+ "owner": "",
+ "selection_class": "EXPLICIT_ONLY",
+ "class": "authority",
+ "source_phases": [
+ "ACTIVE",
+ "FRONTIER"
+ ],
+ "target_phases": [
+ "ABANDONED"
+ ],
+ "goal_kinds": [
+ "safely-abandoned"
+ ],
+ "required_identity": [
+ "repository-id",
+ "git-common-id",
+ "worktree-id",
+ "ref",
+ "controller-id",
+ "invoking-path",
+ "runtime-path",
+ "runtime-fingerprint",
+ "topology",
+ "host",
+ "correlation-id"
+ ],
+ "authority": [
+ "human"
+ ],
+ "required_evidence": [
+ "invocation-context",
+ "snapshot-fingerprint",
+ "goal",
+ "facet:delivery",
+ "facet:program",
+ "facet:terminal",
+ "facet:engagement",
+ "facet:goal"
+ ],
+ "owned_resources": [
+ "publication"
+ ],
+ "effect": "publication.abandon",
+ "local_effects": [
+ "publication.abandon"
+ ],
+ "idempotent": true,
+ "prescription": {
+ "operation": "publication.abandon",
+ "expected_postcondition": "predicate:target-phase:publication.abandon"
+ },
+ "source_predicate": "predicate:source-phase:publication.abandon",
+ "source_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "uninitialized",
+ "planning",
+ "approved",
+ "active",
+ "gates-passed",
+ "published",
+ "amendment",
+ "invalid",
+ "recovery"
+ ]
+ },
+ {
+ "facet": "program",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "unbound",
+ "current"
+ ]
+ },
+ {
+ "facet": "terminal",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "nonterminal"
+ ]
+ },
+ {
+ "facet": "engagement",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "command",
+ "active"
+ ]
+ },
+ {
+ "facet": "goal",
+ "statuses": [
+ "known"
+ ]
+ }
+ ],
+ "admission_predicate": "predicate:exact-admission:publication.abandon",
+ "target_predicate": "predicate:target-phase:publication.abandon",
+ "target_conditions": [
+ {
+ "facet": "delivery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "discarded"
+ ]
+ },
+ {
+ "facet": "recovery",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ },
+ {
+ "facet": "transaction",
+ "statuses": [
+ "known"
+ ],
+ "values": [
+ "none"
+ ]
+ }
+ ],
+ "verifier": "verifier:fresh-observation:publication.abandon",
+ "interruption": {
+ "points": [
+ "after-lock",
+ "after-stage",
+ "after-effect",
+ "before-receipt"
+ ],
+ "partial_state": [
+ "journal-begun",
+ "effect-staged",
+ "effect-possibly-installed",
+ "postcondition-unreceipted"
+ ],
+ "detection": "pending-journal-plus-fresh-canonical-observation",
+ "resume_contract": "journal-target-replay-when-permitted",
+ "rollback_contract": "exact-prior-byte-replay-when-permitted",
+ "compensation_contract": "not-required-for-owned-local-effects",
+ "recovery": "recovery.resume",
+ "recovery_authority": "declared-by:recovery.resume",
+ "resumption_predicate": "recovery-contract-for:publication.abandon"
+ },
+ "reversibility": "reversible",
+ "terminal_effect": "may-establish-safe-abandonment",
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt",
+ "cost_class": "declared-neutral",
+ "policy": {},
+ "priority": 93
+ }
+]
diff --git a/boatstack/go.mod b/boatstack/go.mod
index 8e4c22b..dc63655 100644
--- a/boatstack/go.mod
+++ b/boatstack/go.mod
@@ -2,4 +2,6 @@ module github.com/operatorstack/boatstack/boatstack
go 1.26
-require go.yaml.in/yaml/v3 v3.0.4
+require github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
+
+require golang.org/x/text v0.14.0 // indirect
diff --git a/boatstack/go.sum b/boatstack/go.sum
index 56a75c7..5b447c8 100644
--- a/boatstack/go.sum
+++ b/boatstack/go.sum
@@ -1,4 +1,6 @@
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
-go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
diff --git a/boatstack/internal/effects/artifacts.go b/boatstack/internal/effects/artifacts.go
index ae3c535..42fb056 100644
--- a/boatstack/internal/effects/artifacts.go
+++ b/boatstack/internal/effects/artifacts.go
@@ -183,7 +183,7 @@ func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admissio
if actual := sha256Bytes(evidenceRaw); actual != fingerprint {
return nil, fmt.Errorf("gate evidence fingerprint mismatch: got %s", actual)
}
- gate, _ := catalog.GateName(transition.ID)
+ gate, _ := standardGateName(transition.ID)
var input gateEvidenceInput
if decodeErr := decodeStrictArtifact(evidenceRaw, &input); decodeErr != nil || input.SchemaVersion != 1 ||
input.Gate != gate || input.SourceRevision != revision || input.Outcome != "passed" || input.Producer == "" || input.CompletedAt.IsZero() {
diff --git a/boatstack/internal/effects/command_boundary.go b/boatstack/internal/effects/command_boundary.go
index 37bf86b..41db8d0 100644
--- a/boatstack/internal/effects/command_boundary.go
+++ b/boatstack/internal/effects/command_boundary.go
@@ -133,7 +133,7 @@ func (b NativeBoundary) Execute(ctx context.Context, admission protocol.Admissio
if err != nil {
return settled, err
}
- gate, _ := catalog.GateName(transition.ID)
+ gate, _ := standardGateName(transition.ID)
command := strings.TrimSpace(config.Project.Commands[gate])
if command == "" {
return settled, fmt.Errorf("repository configuration has no %s command", gate)
diff --git a/boatstack/internal/effects/command_boundary_test.go b/boatstack/internal/effects/command_boundary_test.go
index db4864a..505146c 100644
--- a/boatstack/internal/effects/command_boundary_test.go
+++ b/boatstack/internal/effects/command_boundary_test.go
@@ -10,11 +10,11 @@ import (
"testing"
"time"
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/durable"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
type boundaryRunner struct {
@@ -57,7 +57,7 @@ func TestConfiguredBuildCommandMustPassBeforeGateInstallation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- transition, _ := catalog.Default().Lookup("gate.build.record")
+ transition, _ := testprogram.StandardRegistry().Lookup("gate.build.record")
_, err = boundary.Execute(context.Background(), protocol.Admission{}, transition, writeBoundaryConfig(t, "go test ./..."), durable.State{})
if err == nil || runner.calls != 1 {
t.Fatalf("failed configured command result: err=%v calls=%d", err, runner.calls)
@@ -73,7 +73,7 @@ func TestConfiguredBuildCommandCannotCrossConstitutionalGuard(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- transition, _ := catalog.Default().Lookup("gate.build.record")
+ transition, _ := testprogram.StandardRegistry().Lookup("gate.build.record")
_, err = boundary.Execute(context.Background(), protocol.Admission{}, transition, writeBoundaryConfig(t, "git reset --hard HEAD~1"), durable.State{})
if err == nil || runner.calls != 0 {
t.Fatalf("protected configured command result: err=%v calls=%d", err, runner.calls)
@@ -86,7 +86,7 @@ func TestPublicationObservationTerminatesOptionsBeforeIdentifier(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- transition, _ := catalog.Default().Lookup("publication.observe")
+ transition, _ := testprogram.StandardRegistry().Lookup("publication.observe")
admission := protocol.Admission{
Invocation: model.InvocationContext{Ref: "refs/heads/feature"}, SourceRevision: "revision",
Parameters: protocol.Parameters{{Name: "publication_id", Value: "-dangerous"}},
@@ -108,7 +108,7 @@ func TestPublicationObservationTerminatesOptionsBeforeIdentifier(t *testing.T) {
func TestPublicationObservationRejectsUnrelatedProviderIdentity(t *testing.T) {
runner := &boundaryRunner{output: []byte(`{"state":"OPEN","url":"https://example.invalid/pull/8","number":8,"baseRefName":"main","headRefName":"other","headRefOid":"revision","isCrossRepository":false}`)}
boundary, _ := NewNativeBoundaryWithRunner(runner)
- transition, _ := catalog.Default().Lookup("publication.observe")
+ transition, _ := testprogram.StandardRegistry().Lookup("publication.observe")
admission := protocol.Admission{
Invocation: model.InvocationContext{Ref: "refs/heads/feature"}, SourceRevision: "revision",
Parameters: protocol.Parameters{{Name: "publication_id", Value: "8"}},
@@ -156,7 +156,7 @@ func TestPublicationPreviewRejectsFieldTamperingUnderAnOldFingerprint(t *testing
func TestPublicationCorrectionRejectsBodyDriftBeforeProviderCall(t *testing.T) {
runner := &boundaryRunner{}
boundary, _ := NewNativeBoundaryWithRunner(runner)
- transition, _ := catalog.Default().Lookup("publication.correct")
+ transition, _ := testprogram.StandardRegistry().Lookup("publication.correct")
layout := writeBoundaryConfig(t, "go test ./...")
bodyPath := filepath.Join(layout.RepositoryRoot, "body.md")
if err := os.WriteFile(bodyPath, []byte("changed"), 0o600); err != nil {
diff --git a/boatstack/internal/effects/driver.go b/boatstack/internal/effects/driver.go
index a77f813..82999d7 100644
--- a/boatstack/internal/effects/driver.go
+++ b/boatstack/internal/effects/driver.go
@@ -14,7 +14,6 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/reducer"
)
type CommandBoundary interface {
@@ -23,9 +22,10 @@ type CommandBoundary interface {
}
type Driver struct {
- resolver ports.InvocationResolver
- clock ports.Clock
- boundary CommandBoundary
+ resolver ports.InvocationResolver
+ clock ports.Clock
+ boundary CommandBoundary
+ resourceOwnership map[string]string
}
func NewDriver(resolver ports.InvocationResolver, clock ports.Clock, boundary CommandBoundary) (Driver, error) {
@@ -35,7 +35,29 @@ func NewDriver(resolver ports.InvocationResolver, clock ports.Clock, boundary Co
return Driver{resolver: resolver, clock: clock, boundary: boundary}, nil
}
+func NewProgramDriver(resolver ports.InvocationResolver, clock ports.Clock, boundary CommandBoundary, ownership map[string]string) (Driver, error) {
+ driver, err := NewDriver(resolver, clock, boundary)
+ if err != nil {
+ return Driver{}, err
+ }
+ if len(ownership) == 0 {
+ return Driver{}, fmt.Errorf("effect driver requires compiled resource ownership")
+ }
+ driver.resourceOwnership = make(map[string]string, len(ownership))
+ for resource, owner := range ownership {
+ driver.resourceOwnership[resource] = owner
+ }
+ return driver, nil
+}
+
func (d Driver) Prepare(ctx context.Context, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) {
+ if len(d.resourceOwnership) != 0 {
+ for _, resource := range transition.OwnedResources {
+ if owner := d.resourceOwnership[resource]; owner == "" || owner != transition.Owner {
+ return nil, fmt.Errorf("transition %q cannot write resource %q owned by %q", transition.ID, resource, owner)
+ }
+ }
+ }
layout, currentInvocation, err := d.resolver.ResolveLayout(ctx, admission.Invocation)
if err != nil {
return nil, err
@@ -53,6 +75,9 @@ func (d Driver) Prepare(ctx context.Context, admission protocol.Admission, trans
if state.RepositoryID != admission.Invocation.RepositoryID || state.GitCommonID != admission.Invocation.GitCommonID || state.WorktreeID != admission.Invocation.WorktreeID {
return nil, fmt.Errorf("durable state belongs to a different invocation")
}
+ if state.ProgramFingerprint != "" && state.ProgramFingerprint != admission.ProgramFingerprint && !transition.Policy.ReconcilesProgram {
+ return nil, fmt.Errorf("compiled control program drifted; explicit program reconciliation is required")
+ }
if err := verifyWorkspaceBranchParameter(state, admission, transition.ID); err != nil {
return nil, err
}
@@ -60,10 +85,13 @@ func (d Driver) Prepare(ctx context.Context, admission protocol.Admission, trans
return nil, err
}
next := state
+ if next.ProgramFingerprint == "" {
+ next.ProgramFingerprint = admission.ProgramFingerprint
+ }
if err := d.boundary.PrepareObservation(ctx, admission, transition, layout, &next); err != nil {
return nil, err
}
- if err := reducer.Apply(&next, admission, transition); err != nil {
+ if err := applyStateTransition(&next, admission, transition); err != nil {
return nil, err
}
next.Revision++
@@ -198,6 +226,13 @@ func (d Driver) Prepare(ctx context.Context, admission protocol.Admission, trans
}
mutations = append(mutations, recoveryMutations...)
}
+ for index := range mutations {
+ if len(transition.OwnedResources) == 0 {
+ return nil, fmt.Errorf("transition %q produced an undeclared resource write", transition.ID)
+ }
+ mutations[index].Resource = transition.OwnedResources[0]
+ mutations[index].Owner = transition.Owner
+ }
prepared := &preparedEffect{mutations: mutations, verifyInvocation: verificationInvocation}
if requiresCommandBoundary(transition.ID) {
prepared.boundary = func(boundaryContext context.Context) (ports.EffectResult, error) {
@@ -298,7 +333,7 @@ func parkedSourceState(state durable.State, transition catalog.TransitionID, now
state.PublicationURL = ""
state.PreviewFingerprint = ""
state.Gates = nil
- reducer.ClearRecoveryContext(&state)
+ clearRecoveryContext(&state)
state.LastTransition = transition
state.UpdatedAt = now.UTC()
return state
diff --git a/boatstack/internal/effects/executable.go b/boatstack/internal/effects/executable.go
new file mode 100644
index 0000000..37fdbf8
--- /dev/null
+++ b/boatstack/internal/effects/executable.go
@@ -0,0 +1,24 @@
+package effects
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+// StageVerifiedExecutable materializes already verified bytes in a private,
+// single-use location. Callers execute the returned path instead of reopening
+// a mutable repository path after verification.
+func StageVerifiedExecutable(source string, raw []byte) (string, func(), error) {
+ directory, err := os.MkdirTemp("", "boatstack-extension-")
+ if err != nil {
+ return "", func() {}, fmt.Errorf("create private subprocess extension staging directory: %w", err)
+ }
+ cleanup := func() { _ = os.RemoveAll(directory) }
+ path := filepath.Join(directory, "extension"+filepath.Ext(source))
+ if err := os.WriteFile(path, raw, 0o700); err != nil {
+ cleanup()
+ return "", func() {}, fmt.Errorf("stage verified subprocess extension bytes: %w", err)
+ }
+ return path, cleanup, nil
+}
diff --git a/boatstack/internal/effects/extensions.go b/boatstack/internal/effects/extensions.go
new file mode 100644
index 0000000..30076a7
--- /dev/null
+++ b/boatstack/internal/effects/extensions.go
@@ -0,0 +1,116 @@
+package effects
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
+)
+
+// NewExtensionLocalPrepared turns a declarative extension write plan into the
+// same reversible prepared-effect contract used by first-party effects.
+func NewExtensionLocalPrepared(repositoryRoot, extensionID string, writes []control.ResourceWrite) (ports.PreparedEffect, error) {
+ return newNamespacedLocalPrepared(repositoryRoot, filepath.Join("extensions", extensionID), extensionID, "extension", writes)
+}
+
+// NewFlowLocalPrepared constrains a protocol-backed primary flow 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)
+}
+
+func newNamespacedLocalPrepared(repositoryRoot, namespace, owner, kind string, writes []control.ResourceWrite) (ports.PreparedEffect, error) {
+ if len(writes) == 0 {
+ return nil, fmt.Errorf("%s %q planned no local writes", kind, owner)
+ }
+ canonicalRepository, err := filepath.EvalSymlinks(repositoryRoot)
+ if err != nil {
+ return nil, fmt.Errorf("resolve %s repository root: %w", kind, err)
+ }
+ lexicalRoot := filepath.Join(filepath.Clean(repositoryRoot), ".boatstack", namespace)
+ root := filepath.Join(canonicalRepository, ".boatstack", namespace)
+ mutations := make([]ports.ResourceMutation, 0, len(writes))
+ seen := map[string]bool{}
+ totalBytes := 0
+ for _, write := range writes {
+ if !strings.HasPrefix(write.Resource, owner+".") {
+ return nil, fmt.Errorf("%s %q planned undeclared resource %q", kind, owner, write.Resource)
+ }
+ if !filepath.IsAbs(write.Path) {
+ return nil, fmt.Errorf("%s %q planned a non-absolute path", kind, owner)
+ }
+ clean := filepath.Clean(write.Path)
+ relative, err := filepath.Rel(lexicalRoot, clean)
+ if err != nil || relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) {
+ return nil, fmt.Errorf("%s %q write escapes its namespaced resource root", kind, owner)
+ }
+ clean = filepath.Join(root, relative)
+ if seen[clean] {
+ return nil, fmt.Errorf("%s %q planned duplicate path %q", kind, owner, clean)
+ }
+ seen[clean] = true
+ if write.Delete && len(write.Content) != 0 {
+ return nil, fmt.Errorf("%s %q delete contains target bytes", kind, owner)
+ }
+ if !write.Delete {
+ totalBytes += len(write.Content)
+ if totalBytes > 4<<20 {
+ return nil, fmt.Errorf("%s %q write plan exceeds 4 MiB", kind, owner)
+ }
+ digest := sha256.Sum256(write.Content)
+ if write.SHA256 == "" || hex.EncodeToString(digest[:]) != write.SHA256 {
+ return nil, fmt.Errorf("%s %q write digest mismatch", kind, owner)
+ }
+ }
+ mode := os.FileMode(write.Mode)
+ if mode == 0 {
+ mode = 0o600
+ }
+ if err := rejectSymlinkComponents(canonicalRepository, clean); err != nil {
+ return nil, fmt.Errorf("%s %q write path is unsafe: %w", kind, owner, err)
+ }
+ mutation, err := mutationFor(clean, write.Content, mode, false, write.Delete)
+ if err != nil {
+ return nil, err
+ }
+ mutation.Resource, mutation.Owner = write.Resource, owner
+ mutations = append(mutations, mutation)
+ }
+ return &preparedEffect{mutations: mutations}, nil
+}
+
+func rejectSymlinkComponents(repositoryRoot, target string) error {
+ relative, err := filepath.Rel(repositoryRoot, target)
+ if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) {
+ return fmt.Errorf("target escapes the canonical repository")
+ }
+ current := repositoryRoot
+ for _, segment := range strings.Split(relative, string(filepath.Separator)) {
+ current = filepath.Join(current, segment)
+ info, statErr := os.Lstat(current)
+ if os.IsNotExist(statErr) {
+ continue
+ }
+ if statErr != nil {
+ return statErr
+ }
+ if info.Mode()&os.ModeSymlink != 0 {
+ return fmt.Errorf("symlink component %q", current)
+ }
+ }
+ return nil
+}
+
+func NewExtensionExternalPrepared(execute func(context.Context) (ports.EffectResult, error)) (ports.PreparedEffect, error) {
+ if execute == nil {
+ return nil, fmt.Errorf("external extension effect requires an executor")
+ }
+ return &preparedEffect{boundary: execute}, nil
+}
diff --git a/boatstack/internal/effects/extensions_test.go b/boatstack/internal/effects/extensions_test.go
new file mode 100644
index 0000000..2132b84
--- /dev/null
+++ b/boatstack/internal/effects/extensions_test.go
@@ -0,0 +1,39 @@
+package effects
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+)
+
+func TestNamespacedExtensionWriteRejectsSymlinkEscapeWithoutMutation(t *testing.T) {
+ // control-law: declarative-extension-write-cannot-escape-its-owned-resource-root
+ if runtime.GOOS == "windows" {
+ t.Skip("symlink creation may require elevated Windows privileges")
+ }
+ repository := t.TempDir()
+ outside := t.TempDir()
+ link := filepath.Join(repository, ".boatstack", "extensions", "example.guard", "link")
+ if err := os.MkdirAll(filepath.Dir(link), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(outside, link); err != nil {
+ t.Fatal(err)
+ }
+ content := []byte("must-not-escape")
+ digest := sha256.Sum256(content)
+ _, err := NewExtensionLocalPrepared(repository, "example.guard", []control.ResourceWrite{{
+ Resource: "example.guard.evidence", Path: filepath.Join(link, "evidence.json"), Content: content, SHA256: hex.EncodeToString(digest[:]),
+ }})
+ if err == nil {
+ t.Fatal("symlink escape was accepted")
+ }
+ if _, statErr := os.Stat(filepath.Join(outside, "evidence.json")); !os.IsNotExist(statErr) {
+ t.Fatalf("rejected write mutated outside path: %v", statErr)
+ }
+}
diff --git a/boatstack/internal/effects/integration_test.go b/boatstack/internal/effects/integration_test.go
index 35c5e4a..005cf6d 100644
--- a/boatstack/internal/effects/integration_test.go
+++ b/boatstack/internal/effects/integration_test.go
@@ -14,18 +14,48 @@ import (
"time"
boatstack "github.com/operatorstack/boatstack/boatstack"
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/extension/releasenote"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
"github.com/operatorstack/boatstack/boatstack/internal/effects"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/engine"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor"
"github.com/operatorstack/boatstack/boatstack/internal/plant"
"github.com/operatorstack/boatstack/boatstack/internal/surfaces"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
type fixedClock struct{ value time.Time }
+const testProgramFingerprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+func testGoalContracts() catalog.GoalContracts {
+ manifest, err := standard.Definition().FlowManifest(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil)
+ if err != nil {
+ panic(err)
+ }
+ return contracts
+}
+
+func testProgram() control.ControlProgram {
+ program, err := control.Compile(context.Background(), control.CompileRequest{
+ KernelVersion: boatstack.Version, Core: core.System(), Flow: standard.Definition(),
+ })
+ if err != nil {
+ panic(err)
+ }
+ return program
+}
+
func (c fixedClock) Now() time.Time { return c.value }
func run(t *testing.T, directory, name string, arguments ...string) {
@@ -84,7 +114,7 @@ func TestConcreteBoundaryAppliesAndReceiptsOneTransition(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- kernel, err := engine.New(catalog.Default(), observer, clock, locker, journal, driver, receipts)
+ kernel, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramFingerprint, observer, clock, locker, journal, driver, receipts)
if err != nil {
t.Fatal(err)
}
@@ -119,7 +149,7 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing
ctx := context.Background()
repository := testRepository(t)
externalRoot := t.TempDir()
- kernel, err := boatstack.NewV2Kernel(externalRoot)
+ kernel, err := boatstack.NewKernel(externalRoot, testProgram())
if err != nil {
t.Fatal(err)
}
@@ -199,6 +229,236 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing
}
}
+func TestProgramDriftRequiresExplicitCatalogReconciliation(t *testing.T) {
+ // control-law: frozen-program-cannot-change-without-exact-human-reconciliation
+ ctx := context.Background()
+ 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()},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ oldKernel, err := boatstack.NewKernel(externalRoot, oldProgram)
+ if err != nil {
+ t.Fatal(err)
+ }
+ goal := model.Goal{ID: "program-drift", Kind: model.GoalApprovedPlan, DeliveryID: "program-drift"}
+ now := time.Now().UTC()
+ human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{
+ ID: "program-drift-human", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "explicit-program-reconciliation",
+ IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour),
+ }}}
+ executable, _ := os.Executable()
+ executable, _ = filepath.Abs(executable)
+ executable, _ = filepath.EvalSymlinks(executable)
+ runtimeRaw, _ := os.ReadFile(executable)
+ configPath := filepath.Join(t.TempDir(), "project.json")
+ configRaw := []byte("{\"schema_version\":2,\"project\":{\"name\":\"drift\",\"default_branch\":\"main\",\"commands\":{}},\"policy\":{\"plan_approval\":\"human\",\"visual_evidence\":\"optional\"},\"hosts\":[\"cli\"]}\n")
+ if err := os.WriteFile(configPath, configRaw, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ initialized, err := oldKernel.Handle(ctx, surfaces.Request{
+ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-old",
+ FlowID: "flow-program-drift", Goal: goal, TransitionID: "installation.initialize", Authority: human,
+ Parameters: protocol.Parameters{
+ {Name: "source_revision", Value: "program-old"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)},
+ {Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)},
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if initialized.Receipt == nil || initialized.Receipt.ProgramFingerprint != oldProgram.Fingerprint() {
+ t.Fatalf("initial receipt did not freeze old program: %#v", initialized.Receipt)
+ }
+
+ newProgram := testProgram()
+ newKernel, err := boatstack.NewKernel(externalRoot, newProgram)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resolved, err := newKernel.Handle(ctx, surfaces.Request{
+ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, Repository: repository, Host: "cli", CorrelationID: "program-drift-resolve", Goal: goal,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resolved.Decision == nil || resolved.Decision.Kind != supervisor.DecisionUnresolved {
+ t.Fatalf("program drift decision = %#v", resolved.Decision)
+ }
+
+ resolver, err := plant.NewResolver(externalRoot)
+ if err != nil {
+ t.Fatal(err)
+ }
+ invocation, err := resolver.ResolveInvocation(ctx, repository, "cli", "program-drift-inspect")
+ if err != nil {
+ t.Fatal(err)
+ }
+ layout, _, err := resolver.ResolveLayout(ctx, invocation)
+ if err != nil {
+ t.Fatal(err)
+ }
+ before, err := os.ReadFile(layout.StatePath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ request := surfaces.Request{
+ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-drift-reconcile",
+ FlowID: "flow-program-drift", Goal: goal, TransitionID: "catalog.reconcile",
+ Parameters: protocol.Parameters{{Name: "prior_program_fingerprint", Value: oldProgram.Fingerprint()}, {Name: "accept_obligation_change", Value: "true"}},
+ }
+ frontier, err := newKernel.Handle(ctx, request)
+ if err == nil || frontier.Decision == nil || frontier.Decision.Kind != supervisor.DecisionFrontier {
+ t.Fatalf("authority-free reconciliation = response %#v error %v", frontier, err)
+ }
+ afterRejected, _ := os.ReadFile(layout.StatePath)
+ if !bytes.Equal(before, afterRejected) {
+ t.Fatal("authority-free reconciliation mutated durable state")
+ }
+ request.Authority = human
+ request.Parameters[1].Value = "false"
+ if _, err := newKernel.Handle(ctx, request); err == nil {
+ t.Fatal("reconciliation without explicit obligation acceptance succeeded")
+ }
+ afterInvalid, _ := os.ReadFile(layout.StatePath)
+ if !bytes.Equal(before, afterInvalid) {
+ t.Fatal("invalid reconciliation mutated durable state")
+ }
+ request.Parameters[1].Value = "true"
+ reconciled, err := newKernel.Handle(ctx, request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if reconciled.Receipt == nil || reconciled.Receipt.ProgramFingerprint != newProgram.Fingerprint() || reconciled.Snapshot == nil || reconciled.Snapshot.Program.Value != model.ProgramCurrent {
+ t.Fatalf("reconciliation did not establish exact program identity: %#v", reconciled)
+ }
+}
+
+func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *testing.T) {
+ // control-law: extension-obligation-cannot-be-terminal-before-verified-kernel-receipt
+ ctx := context.Background()
+ repository := testRepository(t)
+ if err := os.MkdirAll(filepath.Join(repository, "release-notes"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(repository, "release-notes", "extension.md"), []byte("### Extension\n\nVerifiable user impact.\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ program, err := control.Compile(ctx, control.CompileRequest{
+ KernelVersion: boatstack.Version, Core: core.System(), Flow: standard.Definition(), Extensions: []control.Extension{releasenote.Definition()},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ kernel, err := boatstack.NewKernel(t.TempDir(), program)
+ if err != nil {
+ t.Fatal(err)
+ }
+ goal := model.Goal{ID: "extension-receipt", Kind: model.GoalOpenPR, DeliveryID: "extension-receipt"}
+ now := time.Now().UTC()
+ authority := func(class catalog.AuthorityClass) protocol.AuthorityBundle {
+ fingerprint, subject := "explicit-human", "integration"
+ if class == catalog.AuthorityRepository {
+ subject = filepath.Join(repository, ".boatstack", "project.json")
+ raw, readErr := os.ReadFile(subject)
+ if readErr != nil {
+ t.Fatal(readErr)
+ }
+ fingerprint = configFingerprint(t, raw)
+ }
+ return protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{
+ ID: "extension-" + string(class), Class: class, Subject: subject, Fingerprint: fingerprint,
+ IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour),
+ }}}
+ }
+ apply := func(id catalog.TransitionID, authorization protocol.AuthorityBundle, parameters protocol.Parameters) surfaces.Response {
+ t.Helper()
+ response, applyErr := kernel.Handle(ctx, surfaces.Request{
+ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli",
+ CorrelationID: "extension-" + string(id), FlowID: "flow-extension-receipt", Goal: goal, TransitionID: id,
+ Authority: authorization, Parameters: parameters,
+ })
+ if applyErr != nil {
+ t.Fatalf("apply %s: %v", id, applyErr)
+ }
+ return response
+ }
+ executable, _ := os.Executable()
+ executable, _ = filepath.Abs(executable)
+ executable, _ = filepath.EvalSymlinks(executable)
+ runtimeRaw, _ := os.ReadFile(executable)
+ configPath := filepath.Join(t.TempDir(), "project.json")
+ configRaw := []byte("{\"schema_version\":2,\"project\":{\"name\":\"extension\",\"default_branch\":\"main\",\"commands\":{\"build\":\"go version\",\"test\":\"go version\"}},\"policy\":{\"plan_approval\":\"human\",\"visual_evidence\":\"optional\"},\"hosts\":[\"cli\"]}\n")
+ if err := os.WriteFile(configPath, configRaw, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ apply("installation.initialize", authority(catalog.AuthorityHuman), protocol.Parameters{
+ {Name: "source_revision", Value: "extension-fixture"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)},
+ {Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)},
+ })
+ apply("engagement.begin", authority(catalog.AuthorityRepository), nil)
+ planPath := filepath.Join(t.TempDir(), "plan.md")
+ planRaw := []byte("# Extension plan\n")
+ if err := os.WriteFile(planPath, planRaw, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ apply("plan.create", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "source_path", Value: planPath}, {Name: "delivery_id", Value: goal.DeliveryID}})
+ apply("plan.validate", authority(catalog.AuthorityRepository), nil)
+ apply("plan.approve", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "plan_fingerprint", Value: digestBytes(planRaw)}, {Name: "actor", Value: "integration"}})
+ apply("plan.activate", authority(catalog.AuthorityHuman), nil)
+ head := strings.TrimSpace(commandOutput(t, repository, "git", "rev-parse", "HEAD"))
+ gate := func(name string) protocol.Parameters {
+ raw, marshalErr := json.Marshal(map[string]any{"schema_version": 1, "gate": name, "source_revision": head, "outcome": "passed", "producer": "integration", "completed_at": now})
+ if marshalErr != nil {
+ t.Fatal(marshalErr)
+ }
+ path := filepath.Join(t.TempDir(), name+".json")
+ if err := os.WriteFile(path, raw, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ return protocol.Parameters{{Name: "source_revision", Value: head}, {Name: "evidence_path", Value: path}, {Name: "evidence_fingerprint", Value: digestBytes(raw)}}
+ }
+ apply("gate.build.record", authority(catalog.AuthorityRepository), gate("build"))
+ apply("gate.test.record", authority(catalog.AuthorityRepository), gate("test"))
+ beforeExtension := apply("gate.review.record", authority(catalog.AuthorityRepository), gate("review"))
+ if beforeExtension.Snapshot == nil || beforeExtension.Snapshot.ExtensionFacts[releasenote.FactID].Value != "missing" {
+ t.Fatalf("extension obligation disappeared before receipt: %#v", beforeExtension.Snapshot)
+ }
+ next, err := kernel.Handle(ctx, surfaces.Request{
+ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, Repository: repository, Host: "cli",
+ CorrelationID: "extension-next", Goal: goal, Authority: authority(catalog.AuthorityRepository),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if next.Decision == nil || next.Decision.Kind != supervisor.DecisionPrescribed || next.Decision.Transition == nil || next.Decision.Transition.ID != releasenote.Transition {
+ t.Fatalf("unmet extension obligation decision = %#v", next.Decision)
+ }
+ completed := apply(releasenote.Transition, authority(catalog.AuthorityRepository), nil)
+ if completed.Receipt == nil || completed.Receipt.TransitionID != releasenote.Transition || completed.Receipt.ProgramFingerprint != program.Fingerprint() ||
+ completed.Snapshot == nil || completed.Snapshot.ExtensionFacts[releasenote.FactID].Value != "verified" {
+ t.Fatalf("extension did not traverse verified receipt path: %#v", completed)
+ }
+ after, err := kernel.Handle(ctx, surfaces.Request{
+ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, Repository: repository, Host: "cli",
+ 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)
+ }
+ if after.Decision.Transition != nil && after.Decision.Transition.Origin.Kind == catalog.OriginExtension {
+ t.Fatalf("verified extension remained selectable: %#v", after.Decision)
+ }
+ for _, candidate := range after.Decision.Candidates {
+ if candidate == releasenote.Transition {
+ t.Fatalf("verified extension remained a frontier candidate: %#v", after.Decision)
+ }
+ }
+}
+
func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing.T) {
// control-law: successful-writes-remain-independently-verifiable-and-goal-specific
ctx := context.Background()
@@ -217,7 +477,7 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing.
journal, _ := effects.NewJournal(resolver, clock)
receipts, _ := effects.NewReceiptStore(resolver, clock)
driver, _ := effects.NewDriver(resolver, clock, effects.NewNativeBoundary())
- kernel, err := engine.New(catalog.Default(), observer, clock, locker, journal, driver, receipts)
+ kernel, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramFingerprint, observer, clock, locker, journal, driver, receipts)
if err != nil {
t.Fatal(err)
}
@@ -360,7 +620,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T)
journal, _ := effects.NewJournal(resolver, clock)
receipts, _ := effects.NewReceiptStore(resolver, clock)
driver, _ := effects.NewDriver(resolver, clock, effects.NewNativeBoundary())
- kernel, err := engine.New(catalog.Default(), observer, clock, locker, journal, driver, receipts)
+ kernel, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramFingerprint, observer, clock, locker, journal, driver, receipts)
if err != nil {
t.Fatal(err)
}
diff --git a/boatstack/internal/effects/receipts.go b/boatstack/internal/effects/receipts.go
index d7a9e6e..b07016c 100644
--- a/boatstack/internal/effects/receipts.go
+++ b/boatstack/internal/effects/receipts.go
@@ -137,6 +137,7 @@ type processEvent struct {
Timestamp time.Time `json:"timestamp"`
GoalID string `json:"goal_id"`
TransitionID string `json:"transition_id"`
+ ProgramFingerprint string `json:"program_fingerprint"`
SourceFingerprint string `json:"source_fingerprint"`
TargetFingerprint string `json:"target_fingerprint"`
Outcome string `json:"outcome"`
@@ -180,7 +181,7 @@ func (s *ReceiptStore) Append(ctx context.Context, receipt protocol.TransitionRe
}
event := processEvent{
SchemaVersion: 1, FlowID: receipt.FlowID, Sequence: receipt.Sequence, Timestamp: s.clock.Now().UTC(), GoalID: receipt.GoalID,
- TransitionID: string(receipt.TransitionID), SourceFingerprint: receipt.SourceFingerprint, TargetFingerprint: receipt.TargetFingerprint,
+ TransitionID: string(receipt.TransitionID), ProgramFingerprint: receipt.ProgramFingerprint, SourceFingerprint: receipt.SourceFingerprint, TargetFingerprint: receipt.TargetFingerprint,
Outcome: string(receipt.Outcome), DurationNanoseconds: receipt.DurationNanoseconds,
AuthorityClasses: append([]string(nil), receipt.AuthorityClasses...), Recovery: string(receipt.Recovery), Terminal: string(receipt.Terminal), FailureClass: receipt.FailureClass,
}
diff --git a/boatstack/internal/effects/recovery_test.go b/boatstack/internal/effects/recovery_test.go
index 4e3b62b..070bc4e 100644
--- a/boatstack/internal/effects/recovery_test.go
+++ b/boatstack/internal/effects/recovery_test.go
@@ -8,16 +8,32 @@ import (
"testing"
"time"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/engine"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
"github.com/operatorstack/boatstack/boatstack/internal/plant"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
type recoveryClock struct{ value time.Time }
+const testProgramFingerprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+func testGoalContracts() catalog.GoalContracts {
+ manifest, err := standard.Definition().FlowManifest(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ contracts, err := catalog.NewGoalContracts(manifest.GoalContracts, nil)
+ if err != nil {
+ panic(err)
+ }
+ return contracts
+}
+
func (c recoveryClock) Now() time.Time { return c.value }
func recoveryRepository(t *testing.T) string {
@@ -65,7 +81,7 @@ func TestRestartRecoveryRollsBackExactPriorBytesAndArchivesJournal(t *testing.T)
if err != nil {
t.Fatal(err)
}
- initial, err := model.Canonicalize(initialObservation)
+ initial, err := model.CanonicalizeForProgram(initialObservation, testProgramFingerprint)
if err != nil {
t.Fatal(err)
}
@@ -74,7 +90,7 @@ func TestRestartRecoveryRollsBackExactPriorBytesAndArchivesJournal(t *testing.T)
ID: "recovery-human", Class: catalog.AuthorityHuman, Subject: "fixture", Fingerprint: "recovery-human-fingerprint",
IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour),
}}}
- transition, _ := catalog.Default().Lookup("installation.initialize")
+ transition, _ := testprogram.StandardRegistry().Lookup("installation.initialize")
executable, _ := os.Executable()
executable, _ = filepath.Abs(executable)
executable, _ = filepath.EvalSymlinks(executable)
@@ -126,7 +142,7 @@ func TestRestartRecoveryRollsBackExactPriorBytesAndArchivesJournal(t *testing.T)
if err != nil {
t.Fatal(err)
}
- recoverySnapshot, err := model.Canonicalize(recoveryObservation)
+ recoverySnapshot, err := model.CanonicalizeForProgram(recoveryObservation, testProgramFingerprint)
if err != nil {
t.Fatal(err)
}
@@ -137,7 +153,7 @@ func TestRestartRecoveryRollsBackExactPriorBytesAndArchivesJournal(t *testing.T)
locker, _ := NewLocker(resolver)
journalAfterRestart, _ := NewJournal(resolver, clock)
receipts, _ := NewReceiptStore(resolver, clock)
- restartedEngine, err := engine.New(catalog.Default(), observer, clock, locker, journalAfterRestart, driver, receipts)
+ restartedEngine, err := engine.New(testprogram.StandardRegistry(), testGoalContracts(), testProgramFingerprint, observer, clock, locker, journalAfterRestart, driver, receipts)
if err != nil {
t.Fatal(err)
}
diff --git a/boatstack/internal/effects/standard_adapter.go b/boatstack/internal/effects/standard_adapter.go
new file mode 100644
index 0000000..ee132cb
--- /dev/null
+++ b/boatstack/internal/effects/standard_adapter.go
@@ -0,0 +1,18 @@
+package effects
+
+import (
+ "strings"
+
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+)
+
+// standardGateName belongs to the trusted StandardFlow native adapter. The
+// generic Kernel registry does not infer gate semantics from transition IDs.
+func standardGateName(id catalog.TransitionID) (string, bool) {
+ value := string(id)
+ if !strings.HasPrefix(value, "gate.") || !strings.HasSuffix(value, ".record") {
+ return "", false
+ }
+ name := strings.TrimSuffix(strings.TrimPrefix(value, "gate."), ".record")
+ return name, name != ""
+}
diff --git a/boatstack/internal/kernel/reducer/reducer.go b/boatstack/internal/effects/state_reducer.go
similarity index 93%
rename from boatstack/internal/kernel/reducer/reducer.go
rename to boatstack/internal/effects/state_reducer.go
index 7e8a0cc..5e0d784 100644
--- a/boatstack/internal/kernel/reducer/reducer.go
+++ b/boatstack/internal/effects/state_reducer.go
@@ -1,4 +1,4 @@
-package reducer
+package effects
import (
"fmt"
@@ -9,7 +9,7 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
)
-func Apply(state *durable.State, admission protocol.Admission, transition catalog.Transition) error {
+func applyStateTransition(state *durable.State, admission protocol.Admission, transition catalog.Transition) error {
configured := state.Goal.Validate() == nil
switch transition.ID {
case "installation.initialize", "goal.configure":
@@ -46,7 +46,7 @@ func Apply(state *durable.State, admission protocol.Admission, transition catalo
state.RuntimeFingerprint, _ = admission.Parameters.Get("runtime_sha256")
state.RuntimePath, _ = admission.Parameters.Get("runtime_path")
state.RuntimeSource, _ = admission.Parameters.Get("source_revision")
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
state.Phase = settledPhase(*state)
case "configuration.initialize", "configuration.mutate":
state.Configuration = model.ConfigurationVerified
@@ -54,8 +54,15 @@ func Apply(state *durable.State, admission protocol.Admission, transition catalo
state.Phase = settledPhase(*state)
case "configuration.reconcile":
state.Configuration, state.Recovery, state.Transaction = model.ConfigurationVerified, model.RecoveryNone, model.TransactionNone
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
state.Phase = settledPhase(*state)
+ case "catalog.reconcile":
+ prior, _ := admission.Parameters.Get("prior_program_fingerprint")
+ accepted, _ := admission.Parameters.Get("accept_obligation_change")
+ if prior == "" || prior != state.ProgramFingerprint || accepted != "true" {
+ return fmt.Errorf("catalog reconciliation must bind the prior program and explicitly accept obligation changes")
+ }
+ state.ProgramFingerprint = admission.ProgramFingerprint
case "installation.initialize":
state.Runtime, state.Configuration = model.RuntimeVerified, model.ConfigurationVerified
state.RuntimeFingerprint, _ = admission.Parameters.Get("runtime_sha256")
@@ -99,12 +106,12 @@ func Apply(state *durable.State, admission protocol.Admission, transition catalo
state.Workspace = model.WorkspaceAbandoned
}
state.Recovery, state.Transaction = model.RecoveryNone, model.TransactionNone
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
establishTerminal(state, model.PhaseAbandoned)
case "workspace.abandon":
state.Delivery, state.Workspace = model.DeliveryDiscarded, model.WorkspaceAbandoned
state.Recovery, state.Transaction = model.RecoveryNone, model.TransactionNone
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
establishTerminal(state, model.PhaseAbandoned)
case "workspace.cut":
state.Workspace, state.Phase = model.WorkspaceCut, model.PhaseActive
@@ -129,9 +136,9 @@ func Apply(state *durable.State, admission protocol.Admission, transition catalo
state.Phase = terminalPhase(*state)
case "workspace.reconcile":
state.Recovery, state.Transaction, state.Phase = model.RecoveryNone, model.TransactionNone, engagedPhase(*state)
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
case "gate.build.record", "gate.test.record", "gate.review.record", "gate.change.record", "gate.journey.record":
- gate, _ := catalog.GateName(transition.ID)
+ gate, _ := standardGateName(transition.ID)
revision, _ := admission.Parameters.Get("source_revision")
fingerprint, _ := admission.Parameters.Get("evidence_fingerprint")
upsertGate(state, durable.GateEvidence{Gate: gate, Revision: revision, Fingerprint: fingerprint})
@@ -167,7 +174,7 @@ func Apply(state *durable.State, admission protocol.Admission, transition catalo
state.Publication, state.Workspace, state.Delivery, state.Phase = model.PublicationPublishedNotLanded, model.WorkspacePublished, model.DeliveryPublished, model.PhaseActive
case "publication.observe", "publication.reconcile":
state.Recovery, state.Transaction = model.RecoveryNone, model.TransactionNone
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
if state.Publication == model.PublicationUnavailable || state.Publication == model.PublicationConflicting {
state.Phase = model.PhaseUnresolved
} else if state.Publication == model.PublicationClosedUnmerged {
@@ -185,10 +192,10 @@ func Apply(state *durable.State, admission protocol.Admission, transition catalo
state.Terminal, state.Phase = model.TerminalNonterminal, model.PhaseActive
case "recovery.resume":
state.Recovery, state.Transaction, state.Delivery, state.Phase = model.RecoveryNone, model.TransactionNone, model.DeliveryActive, model.PhaseActive
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
case "recovery.rollback":
state.Recovery, state.Transaction, state.Phase = model.RecoveryNone, model.TransactionNone, model.PhaseObserved
- ClearRecoveryContext(state)
+ clearRecoveryContext(state)
case "recovery.escalate":
state.Recovery, state.Phase = model.RecoveryEscalated, model.PhaseFrontier
state.Transaction = model.TransactionNone
@@ -207,7 +214,7 @@ func Apply(state *durable.State, admission protocol.Admission, transition catalo
return nil
}
-func ClearRecoveryContext(state *durable.State) {
+func clearRecoveryContext(state *durable.State) {
state.TransactionID = ""
state.TransactionTransition = ""
state.RecoveryCause = ""
diff --git a/boatstack/internal/kernel/reducer/reducer_test.go b/boatstack/internal/effects/state_reducer_test.go
similarity index 85%
rename from boatstack/internal/kernel/reducer/reducer_test.go
rename to boatstack/internal/effects/state_reducer_test.go
index 4141105..c9f1ed4 100644
--- a/boatstack/internal/kernel/reducer/reducer_test.go
+++ b/boatstack/internal/effects/state_reducer_test.go
@@ -1,4 +1,4 @@
-package reducer
+package effects
import (
"testing"
@@ -7,6 +7,7 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/kernel/durable"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
func TestRequiredVisualEvidenceParticipatesInVerifiedTerminal(t *testing.T) {
@@ -22,11 +23,11 @@ func TestRequiredVisualEvidenceParticipatesInVerifiedTerminal(t *testing.T) {
}
apply := func(id catalog.TransitionID, parameters protocol.Parameters) {
t.Helper()
- transition, ok := catalog.Default().Lookup(id)
+ transition, ok := testprogram.StandardRegistry().Lookup(id)
if !ok {
t.Fatalf("missing transition %s", id)
}
- if err := Apply(&state, protocol.Admission{Goal: goal, Parameters: parameters, SourceRevision: "revision", WorktreeFingerprint: "worktree"}, transition); err != nil {
+ if err := applyStateTransition(&state, protocol.Admission{Goal: goal, Parameters: parameters, SourceRevision: "revision", WorktreeFingerprint: "worktree"}, transition); err != nil {
t.Fatalf("apply %s: %v", id, err)
}
}
@@ -56,17 +57,17 @@ func TestPublicationCorrectionRequiresIndependentObservationForTerminal(t *testi
Verification: model.VerificationCurrent, Recovery: model.RecoveryNone, Transaction: model.TransactionNone, Terminal: model.TerminalEstablished,
Goal: goal,
}
- correct, _ := catalog.Default().Lookup("publication.correct")
+ correct, _ := testprogram.StandardRegistry().Lookup("publication.correct")
admission := protocol.Admission{Goal: goal, Parameters: protocol.Parameters{{Name: "publication_id", Value: "7"}, {Name: "body_path", Value: "/body"}}}
- if err := Apply(&state, admission, correct); err != nil {
+ if err := applyStateTransition(&state, admission, correct); err != nil {
t.Fatal(err)
}
if state.Terminal != model.TerminalNonterminal || state.Publication != model.PublicationPublishedNotLanded || state.Phase != model.PhaseActive {
t.Fatalf("external correction self-certified terminal: %#v", state)
}
state.Publication = model.PublicationOpen // supplied only by PrepareObservation through gh pr view
- observe, _ := catalog.Default().Lookup("publication.observe")
- if err := Apply(&state, admission, observe); err != nil {
+ observe, _ := testprogram.StandardRegistry().Lookup("publication.observe")
+ if err := applyStateTransition(&state, admission, observe); err != nil {
t.Fatal(err)
}
if state.Terminal != model.TerminalEstablished || state.Phase != model.PhaseTerminal {
@@ -90,8 +91,8 @@ func TestWorkspaceReapPreservesEstablishedTerminalPhase(t *testing.T) {
Plan: model.PlanLocked, Configuration: model.ConfigurationVerified, Runtime: model.RuntimeVerified, Publication: model.PublicationMerged,
Verification: model.VerificationCurrent, Recovery: model.RecoveryNone, Transaction: model.TransactionNone, Terminal: model.TerminalEstablished, Goal: goal,
}
- transition, _ := catalog.Default().Lookup("workspace.reap")
- if err := Apply(&state, protocol.Admission{Goal: goal}, transition); err != nil {
+ transition, _ := testprogram.StandardRegistry().Lookup("workspace.reap")
+ if err := applyStateTransition(&state, protocol.Admission{Goal: goal}, transition); err != nil {
t.Fatal(err)
}
if state.Workspace != model.WorkspaceAbsent || state.Phase != fixture.phase || state.Terminal != model.TerminalEstablished {
@@ -111,18 +112,18 @@ func TestEscalatedRecoveryCanOnlyBeReconfiguredTowardExplicitAbandonment(t *test
Goal: original, TransactionID: "adm-interrupted", RecoveryCause: "provider unknown", RecoverySourcePhase: model.PhaseActive,
RecoveryResumption: model.PhaseFrontier, RecoveryBudget: 0,
}
- configure, _ := catalog.Default().Lookup("goal.configure")
+ configure, _ := testprogram.StandardRegistry().Lookup("goal.configure")
configureAdmission := protocol.Admission{Goal: abandoned, Parameters: protocol.Parameters{
{Name: "goal_kind", Value: string(abandoned.Kind)}, {Name: "delivery_id", Value: abandoned.DeliveryID},
}}
- if err := Apply(&state, configureAdmission, configure); err != nil {
+ if err := applyStateTransition(&state, configureAdmission, configure); err != nil {
t.Fatal(err)
}
if state.Phase != model.PhaseFrontier || state.Recovery != model.RecoveryEscalated {
t.Fatalf("goal reconfiguration bypassed escalated recovery: %#v", state)
}
- abandon, _ := catalog.Default().Lookup("plan.abandon")
- if err := Apply(&state, protocol.Admission{Goal: abandoned}, abandon); err != nil {
+ abandon, _ := testprogram.StandardRegistry().Lookup("plan.abandon")
+ if err := applyStateTransition(&state, protocol.Admission{Goal: abandoned}, abandon); err != nil {
t.Fatal(err)
}
if state.Phase != model.PhaseAbandoned || state.Recovery != model.RecoveryNone || state.TransactionID != "" || state.Terminal != model.TerminalEstablished {
diff --git a/boatstack/internal/kernel/catalog/default.go b/boatstack/internal/kernel/catalog/default.go
deleted file mode 100644
index 424e609..0000000
--- a/boatstack/internal/kernel/catalog/default.go
+++ /dev/null
@@ -1,345 +0,0 @@
-package catalog
-
-import (
- "strings"
-
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
-)
-
-const DefaultTransitionCount = 61
-
-type seed struct {
- id TransitionID
- class EventClass
- source []model.ProtocolPhase
- target []model.ProtocolPhase
- authority []AuthorityClass
- resource string
- goals []model.GoalKind
- priority int
-}
-
-var activeSources = []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseFrontier, model.PhaseUnresolved}
-var managedSources = []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved, model.PhaseActive, model.PhaseRecovery, model.PhaseFrontier, model.PhaseUnresolved}
-
-func Default() Registry {
- registry, err := New(defaultTransitions())
- if err != nil {
- panic(err)
- }
- if registry.Len() != DefaultTransitionCount {
- panic("boatstack V2 default transition count drifted")
- }
- return registry
-}
-
-func defaultTransitions() []Transition {
- allGoals := []model.GoalKind{model.GoalApprovedPlan, model.GoalVerified, model.GoalOpenPR, model.GoalMerged, model.GoalAbandoned}
- seeds := []seed{
- {"engagement.begin", EventAuthority, []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive}, []AuthorityClass{AuthorityRepository}, "engagement", allGoals, 10},
- {"engagement.renew", EventAuthority, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityRepository, AuthorityAutonomy}, "engagement", allGoals, 70},
- {"engagement.release", EventAuthority, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseDormant}, []AuthorityClass{AuthorityRepository}, "engagement", allGoals, 95},
- {"invocation.rebind", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseObserved}, []AuthorityClass{AuthorityRepository}, "identity-binding", allGoals, 15},
- {"repository.attach", EventOwnedLocal, []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved}, []model.ProtocolPhase{model.PhaseObserved}, []AuthorityClass{AuthorityHuman}, "repository-binding", allGoals, 12},
- {"repository.detach", EventOwnedLocal, []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseDormant}, []AuthorityClass{AuthorityHuman}, "repository-binding", allGoals, 96},
-
- {"runtime.hydrate", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseRecovery, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityRepository}, "runtime", allGoals, 20},
- {"runtime.replace", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseRecovery}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityRepository}, "runtime", allGoals, 25},
- {"runtime.reconcile", EventRecovery, []model.ProtocolPhase{model.PhaseRecovery, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseFrontier, model.PhaseTerminal}, []AuthorityClass{AuthorityRepository}, "runtime", allGoals, 4},
- {"configuration.initialize", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityRepository}, "configuration", allGoals, 22},
- {"configuration.mutate", EventOwnedLocal, activeSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "configuration", allGoals, 60},
- {"configuration.reconcile", EventRecovery, []model.ProtocolPhase{model.PhaseRecovery, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseFrontier, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityRepository}, "configuration", allGoals, 3},
- {"installation.initialize", EventOwnedLocal, []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved}, []model.ProtocolPhase{model.PhaseObserved}, []AuthorityClass{AuthorityHuman}, "installation", allGoals, 11},
- {"installation.update", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "installation", allGoals, 65},
-
- {"goal.configure", EventAuthority, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseFrontier, model.PhaseTerminal, model.PhaseAbandoned}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseFrontier}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "goal", allGoals, 30},
- {"plan.create", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "plan", allGoals, 35},
- {"plan.validate", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []AuthorityClass{AuthorityRepository}, "plan-evidence", allGoals, 40},
- {"plan.approve", EventAuthority, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "approval", allGoals, 45},
- {"plan.activate", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "delivery-state", []model.GoalKind{model.GoalVerified, model.GoalOpenPR, model.GoalMerged}, 50},
- {"plan.amend", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "plan", allGoals, 42},
- {"plan.approve-amendment", EventAuthority, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "approval", allGoals, 46},
- {"plan.invalidate", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive, model.PhaseObserved}, []model.ProtocolPhase{model.PhaseFrontier}, []AuthorityClass{AuthorityRepository}, "plan-evidence", allGoals, 41},
- {"plan.abandon", EventAuthority, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman}, "plan", []model.GoalKind{model.GoalAbandoned}, 90},
-
- {"workspace.cut", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "workspace", allGoals, 52},
- {"workspace.sync", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "workspace", allGoals, 58},
- {"workspace.activate", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityRepository}, "workspace", allGoals, 53},
- {"workspace.publish", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityRepository}, "workspace-state", []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, 75},
- {"workspace.cleanup", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal, model.PhaseAbandoned}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseTerminal, model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "workspace", allGoals, 92},
- {"workspace.reap", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseTerminal, model.PhaseAbandoned}, []model.ProtocolPhase{model.PhaseObserved, model.PhaseTerminal, model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman}, "workspace", allGoals, 98},
- {"workspace.abandon", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman}, "workspace", []model.GoalKind{model.GoalAbandoned}, 91},
- {"workspace.reconcile", EventRecovery, []model.ProtocolPhase{model.PhaseRecovery, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved, model.PhaseActive, model.PhaseFrontier, model.PhaseTerminal, model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman, AuthorityRepository}, "workspace", allGoals, 2},
-
- {"gate.build.record", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityRepository}, "gate-evidence", allGoals, 61},
- {"gate.test.record", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityRepository}, "gate-evidence", allGoals, 62},
- {"gate.review.record", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityRepository}, "gate-evidence", allGoals, 63},
- {"gate.change.record", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityRepository}, "gate-evidence", allGoals, 64},
- {"gate.journey.record", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityRepository}, "gate-evidence", allGoals, 64},
- {"evidence.visual.attach", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityRepository}, "evidence", allGoals, 66},
- {"evidence.approval.revoke", EventAuthority, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseFrontier}, []AuthorityClass{AuthorityHuman}, "approval", allGoals, 44},
- {"delivery.slice.advance", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "delivery-state", allGoals, 68},
-
- {"publication.preview", EventOwnedLocal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive}, []AuthorityClass{AuthorityRepository}, "publication-preview", []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, 72},
- {"publication.execute", EventOwnedExternal, []model.ProtocolPhase{model.PhaseActive}, []model.ProtocolPhase{model.PhaseActive, model.PhaseRecovery}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "publication", []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, 76},
- {"publication.observe", EventOwnedLocal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseRecovery, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal, model.PhaseFrontier, model.PhaseUnresolved}, []AuthorityClass{AuthorityRepository}, "publication-evidence", []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, 77},
- {"publication.reconcile", EventRecovery, []model.ProtocolPhase{model.PhaseRecovery, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal, model.PhaseFrontier, model.PhaseUnresolved}, []AuthorityClass{AuthorityHuman, AuthorityProvider}, "publication", []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, 1},
- {"publication.correct", EventOwnedExternal, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []model.ProtocolPhase{model.PhaseActive, model.PhaseRecovery}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy}, "publication", []model.GoalKind{model.GoalOpenPR, model.GoalMerged}, 80},
- {"publication.abandon", EventAuthority, []model.ProtocolPhase{model.PhaseActive, model.PhaseFrontier}, []model.ProtocolPhase{model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman}, "publication", []model.GoalKind{model.GoalAbandoned}, 93},
-
- {"recovery.resume", EventRecovery, []model.ProtocolPhase{model.PhaseRecovery}, []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved, model.PhaseActive, model.PhaseFrontier, model.PhaseTerminal, model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman, AuthorityAutonomy, AuthorityRepository}, "recovery-journal", allGoals, 2},
- {"recovery.rollback", EventRecovery, []model.ProtocolPhase{model.PhaseRecovery}, []model.ProtocolPhase{model.PhaseDormant, model.PhaseObserved, model.PhaseActive, model.PhaseFrontier, model.PhaseTerminal, model.PhaseAbandoned}, []AuthorityClass{AuthorityHuman, AuthorityRepository}, "recovery-journal", allGoals, 3},
- {"recovery.escalate", EventRecovery, []model.ProtocolPhase{model.PhaseRecovery, model.PhaseUnresolved}, []model.ProtocolPhase{model.PhaseFrontier}, []AuthorityClass{AuthorityRepository}, "recovery-journal", allGoals, 5},
-
- {"external.files-changed", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.head-changed", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.branch-changed", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.runtime-disappeared", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseRecovery}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.configuration-drifted", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseUnresolved}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.lease-expired", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseDormant, model.PhaseFrontier}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.host-interrupted", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseRecovery}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.ci-completed", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.pr-opened", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.pr-updated", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.pr-closed", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseFrontier}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.pr-merged", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseObserved, model.PhaseActive, model.PhaseTerminal}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- {"external.provider-unavailable", EventObservedExternal, managedSources, []model.ProtocolPhase{model.PhaseUnresolved, model.PhaseRecovery}, []AuthorityClass{AuthorityNone}, "", allGoals, 100},
- }
-
- transitions := make([]Transition, 0, len(seeds))
- for _, item := range seeds {
- transitions = append(transitions, materialize(item))
- }
- return transitions
-}
-
-func materialize(item seed) Transition {
- controllable := item.class.Controllable()
- effect := EffectID("")
- var localEffects, externalEffects []EffectID
- resources := []string(nil)
- reversibility := ObservationOnly
- recovery := TransitionID("")
- points := []string{"after-observation"}
- if controllable {
- effect = EffectID(item.id)
- resources = []string{item.resource}
- reversibility = Reversible
- points = []string{"after-lock", "after-stage", "after-effect", "before-receipt"}
- if item.class == EventOwnedExternal {
- externalEffects = []EffectID{effect}
- reversibility = Compensatable
- points = []string{"before-request", "after-request", "before-settlement-observation", "before-receipt"}
- } else {
- localEffects = []EffectID{effect}
- }
- if item.class == EventRecovery {
- recovery = "recovery.escalate"
- } else {
- recovery = interruptionRecovery(item)
- }
- }
- transition := Transition{
- ID: item.id, Version: 1, Class: item.class, SourcePhases: item.source, TargetPhases: item.target,
- GoalKinds: item.goals, RequiredIdentity: invocationIdentityRequirements(), Authority: item.authority,
- RequiredEvidence: []string{"invocation-context", "snapshot-fingerprint", "goal"},
- OwnedResources: resources, Effect: effect, Idempotent: controllable,
- LocalEffects: localEffects, ExternalEffects: externalEffects,
- Prescription: Prescription{Operation: string(item.id), ExpectedPostcondition: "predicate:target-phase:" + string(item.id)},
- SourcePredicate: "predicate:source-phase:" + string(item.id), AdmissionPredicate: "predicate:exact-admission:" + string(item.id), TargetPredicate: "predicate:target-phase:" + string(item.id),
- Verifier: "verifier:fresh-observation:" + string(item.id),
- Interruption: interruptionContract(item, points, recovery),
- Reversibility: reversibility, TerminalEffect: terminalEffect(item.id), PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
- CostClass: "declared-neutral", Priority: item.priority,
- }
- transition.SourceConditions = sourceConditions(item.id)
- if item.class.Controllable() && item.class != EventRecovery && !isSafeAbandonment(item.id) {
- transition.SourceConditions = append(transition.SourceConditions,
- known(model.FacetRecovery, string(model.RecoveryNone)),
- known(model.FacetTransaction, string(model.TransactionNone)),
- )
- }
- if item.class.Controllable() && item.class != EventRecovery && item.id != "goal.configure" {
- terminalValues := []string{string(model.TerminalNonterminal)}
- if allowsStaleTerminalRepair(item.id) {
- terminalValues = append(terminalValues, string(model.TerminalStale))
- }
- if allowsPostTerminalMaintenance(item.id) {
- terminalValues = append(terminalValues, string(model.TerminalEstablished))
- }
- transition.SourceConditions = append(transition.SourceConditions, known(model.FacetTerminal, terminalValues...))
- }
- if requiresEngagement(item.id) {
- transition.SourceConditions = append(transition.SourceConditions, known(model.FacetEngagement, string(model.EngagementCommand), string(model.EngagementActive)))
- transition.SourceConditions = append(transition.SourceConditions, known(model.FacetGoal))
- }
- if requiresHealthyConfiguration(item.id) {
- transition.SourceConditions = append(transition.SourceConditions, known(model.FacetConfiguration, string(model.ConfigurationVerified)))
- }
- if requiresHealthyRuntime(item.id) {
- transition.SourceConditions = append(transition.SourceConditions, known(model.FacetRuntime, string(model.RuntimeVerified)))
- }
- if usesConfigurationPolicy(item.id) {
- transition.SourceConditions = append(transition.SourceConditions, known(model.FacetConfigurationPolicy))
- }
- transition.TargetConditions = targetConditions(item.id)
- transition.AuthorityAll = requiredAuthorities(item.id)
- for _, condition := range transition.SourceConditions {
- transition.RequiredEvidence = append(transition.RequiredEvidence, "facet:"+string(condition.Facet))
- }
- transition.Parameters = parameterSpecs(item.id)
- if item.id == "repository.attach" || item.id == "repository.detach" || item.id == "invocation.rebind" {
- transition.AllowsIdentityRebind = true
- }
- if item.id == "workspace.cut" || item.id == "workspace.cleanup" || item.id == "workspace.reap" || item.id == "workspace.reconcile" {
- transition.AllowsWorktreeTransfer = true
- }
- return transition
-}
-
-func isSafeAbandonment(id TransitionID) bool {
- return id == "goal.configure" || id == "plan.abandon" || id == "publication.abandon" || id == "workspace.abandon"
-}
-
-func invocationIdentityRequirements() []string {
- return []string{"repository-id", "git-common-id", "worktree-id", "ref", "controller-id", "invoking-path", "runtime-path", "runtime-fingerprint", "topology", "host", "correlation-id"}
-}
-
-func interruptionContract(item seed, points []string, recovery TransitionID) InterruptionContract {
- contract := InterruptionContract{
- Points: points, PartialState: []string{"no-owned-partial-state"}, Detection: "fresh-canonical-observation",
- ResumeContract: "not-applicable", RollbackContract: "not-applicable", CompensationContract: "not-applicable",
- Recovery: recovery, RecoveryAuthority: "not-applicable", ResumptionPredicate: "predicate:target-phase:" + string(item.id),
- }
- if !item.class.Controllable() {
- return contract
- }
- contract.PartialState = []string{"journal-begun", "effect-staged", "effect-possibly-installed", "postcondition-unreceipted"}
- contract.Detection = "pending-journal-plus-fresh-canonical-observation"
- contract.ResumeContract = "journal-target-replay-when-permitted"
- contract.RollbackContract = "exact-prior-byte-replay-when-permitted"
- contract.CompensationContract = "not-required-for-owned-local-effects"
- contract.RecoveryAuthority = "declared-by:" + string(recovery)
- contract.ResumptionPredicate = "recovery-contract-for:" + string(item.id)
- if item.class == EventOwnedExternal {
- contract.PartialState = []string{"request-not-sent", "request-possibly-accepted", "provider-settlement-unobserved"}
- contract.Detection = "pending-journal-plus-fresh-provider-observation"
- contract.ResumeContract = "forbidden-without-provider-observation"
- contract.RollbackContract = "not-provable-after-external-request"
- contract.CompensationContract = "provider-reconciliation-only"
- }
- if item.id == "workspace.cleanup" || item.id == "workspace.reap" {
- contract.ResumeContract = "forbidden-after-destructive-git-effect"
- contract.RollbackContract = "not-guaranteed-after-worktree-removal"
- contract.CompensationContract = "no-generic-compensation"
- }
- if item.class == EventRecovery {
- contract.ResumeContract = "never-blindly-retry-interrupted-recovery"
- contract.RollbackContract = "preserve-original-transaction-group"
- contract.CompensationContract = "escalation-only-after-nested-interruption"
- }
- return contract
-}
-
-func terminalEffect(id TransitionID) string {
- value := string(id)
- if strings.HasPrefix(value, "gate.") || id == "evidence.visual.attach" || id == "plan.approve" || id == "plan.approve-amendment" ||
- id == "runtime.hydrate" || id == "runtime.replace" || id == "runtime.reconcile" || id == "configuration.initialize" ||
- id == "configuration.mutate" || id == "configuration.reconcile" || id == "installation.update" ||
- id == "publication.observe" || id == "publication.reconcile" || id == "workspace.cleanup" || id == "workspace.reap" {
- return "may-establish-configured-goal-after-fresh-observation"
- }
- if id == "plan.abandon" || id == "publication.abandon" || id == "workspace.abandon" {
- return "may-establish-safe-abandonment"
- }
- return "none"
-}
-
-func interruptionRecovery(item seed) TransitionID {
- if item.class == EventOwnedExternal {
- return "publication.reconcile"
- }
- switch item.id {
- case "runtime.hydrate", "runtime.replace", "installation.initialize", "installation.update":
- return "runtime.reconcile"
- case "configuration.initialize", "configuration.mutate":
- return "configuration.reconcile"
- case "workspace.cut":
- return "workspace.reconcile"
- case "workspace.cleanup", "workspace.reap":
- return "recovery.escalate"
- default:
- return "recovery.resume"
- }
-}
-
-func allowsStaleTerminalRepair(id TransitionID) bool {
- value := string(id)
- return strings.HasPrefix(value, "gate.") || id == "evidence.visual.attach" ||
- id == "plan.create" || id == "plan.validate" || id == "plan.amend" || id == "plan.invalidate" ||
- id == "runtime.hydrate" || id == "runtime.replace" || id == "installation.update" ||
- id == "configuration.initialize" || id == "configuration.mutate" || id == "publication.correct"
-}
-
-func allowsPostTerminalMaintenance(id TransitionID) bool {
- switch id {
- case "workspace.cleanup", "workspace.reap", "publication.correct":
- return true
- default:
- return false
- }
-}
-
-func parameterSpecs(id TransitionID) []ParameterSpec {
- required := func(names ...string) []ParameterSpec {
- result := make([]ParameterSpec, 0, len(names))
- for _, name := range names {
- result = append(result, ParameterSpec{Name: name, Required: true})
- }
- return result
- }
- switch id {
- case "repository.attach":
- return required("topology", "config_authority")
- case "runtime.hydrate", "runtime.replace", "installation.update":
- return required("source_revision", "runtime_path", "runtime_sha256")
- case "runtime.reconcile":
- return required("source_revision", "runtime_path", "runtime_sha256", "transaction_id")
- case "installation.initialize":
- return required("source_revision", "runtime_path", "runtime_sha256", "config_path", "config_sha256")
- case "configuration.initialize", "configuration.mutate":
- return required("config_path", "config_sha256")
- case "configuration.reconcile", "workspace.reconcile":
- return required("transaction_id")
- case "goal.configure":
- return required("goal_kind", "delivery_id")
- case "plan.create", "plan.amend":
- return required("source_path", "delivery_id")
- case "plan.approve", "plan.approve-amendment":
- return required("plan_fingerprint", "actor")
- case "workspace.cut":
- return required("branch", "base_ref", "destination")
- case "workspace.sync", "workspace.activate", "workspace.publish", "workspace.cleanup", "workspace.reap", "workspace.abandon":
- return required("branch")
- case "gate.build.record", "gate.test.record", "gate.review.record", "gate.change.record", "gate.journey.record":
- return required("source_revision", "evidence_path", "evidence_fingerprint")
- case "evidence.visual.attach":
- return required("manifest_path", "privacy_receipt", "source_revision")
- case "delivery.slice.advance":
- return required("slice_id", "source_revision")
- case "publication.preview":
- return required("base_ref", "head_ref", "body_path")
- case "publication.execute":
- return required("preview_fingerprint")
- case "publication.observe":
- return required("publication_id")
- case "publication.reconcile":
- return required("publication_id", "transaction_id")
- case "publication.correct":
- return required("publication_id", "body_path", "body_sha256")
- case "recovery.resume", "recovery.rollback", "recovery.escalate":
- return required("transaction_id")
- default:
- return nil
- }
-}
diff --git a/boatstack/internal/kernel/catalog/default_test.go b/boatstack/internal/kernel/catalog/default_test.go
deleted file mode 100644
index 0725138..0000000
--- a/boatstack/internal/kernel/catalog/default_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package catalog
-
-import (
- "testing"
-
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
-)
-
-func TestDefaultRegistryIsTheCompleteRuntimeCatalog(t *testing.T) {
- // control-law: one-runtime-registry-owns-every-managed-event
- registry := Default()
- if got := registry.Len(); got != DefaultTransitionCount {
- t.Fatalf("registry contains %d transitions, want %d", got, DefaultTransitionCount)
- }
- counts := map[EventClass]int{}
- for _, transition := range registry.All() {
- counts[transition.Class]++
- if transition.Controllable() && transition.Effect == "" {
- t.Fatalf("controllable transition %q has no effect", transition.ID)
- }
- if !transition.Controllable() && (transition.Effect != "" || len(transition.OwnedResources) != 0) {
- t.Fatalf("observed event %q owns an effect", transition.ID)
- }
- }
- if counts[EventObservedExternal] != 13 {
- t.Fatalf("observed external events = %d, want 13", counts[EventObservedExternal])
- }
-}
-
-func TestExternalEffectsRequireProviderAndHumanOrAutonomyAuthority(t *testing.T) {
- transition, _ := Default().Lookup("publication.execute")
- if (AuthoritySet{AuthorityHuman: true}).Satisfies(transition.Authority, transition.AuthorityAll) {
- t.Fatal("human authority substituted for provider authority")
- }
- if (AuthoritySet{AuthorityProvider: true}).Satisfies(transition.Authority, transition.AuthorityAll) {
- t.Fatal("provider authority substituted for human/autonomy authority")
- }
- if !(AuthoritySet{AuthorityHuman: true, AuthorityProvider: true}).Satisfies(transition.Authority, transition.AuthorityAll) {
- t.Fatal("complete external authority set was rejected")
- }
-}
-
-func TestEveryControllableTransitionNamesAnExecutableRecoveryOperator(t *testing.T) {
- registry := Default()
- for _, transition := range registry.All() {
- if !transition.Controllable() {
- continue
- }
- recovery, ok := registry.Lookup(transition.Interruption.Recovery)
- if !ok || recovery.Class != EventRecovery {
- t.Fatalf("transition %s recovery=%s is not executable", transition.ID, transition.Interruption.Recovery)
- }
- if transition.Interruption.Recovery == "recovery.enter" {
- t.Fatalf("transition %s references deleted synthetic recovery entry", transition.ID)
- }
- }
-}
-
-func TestRegistryGraphEveryDeclaredPhaseCanReachMarkedOutcome(t *testing.T) {
- // control-law: every-reachable-managed-state-is-coreachable
- registry := Default()
- edges := map[model.ProtocolPhase][]model.ProtocolPhase{}
- states := map[model.ProtocolPhase]bool{}
- for _, transition := range registry.All() {
- for _, source := range transition.SourcePhases {
- states[source] = true
- for _, target := range transition.TargetPhases {
- edges[source] = append(edges[source], target)
- states[target] = true
- }
- }
- }
- marked := map[model.ProtocolPhase]bool{model.PhaseFrontier: true, model.PhaseTerminal: true, model.PhaseAbandoned: true}
- for state := range states {
- if !canReachMarked(state, edges, marked, map[model.ProtocolPhase]bool{}) {
- t.Errorf("phase %s has no catalog path to a marked outcome", state)
- }
- }
-}
-
-func canReachMarked(state model.ProtocolPhase, edges map[model.ProtocolPhase][]model.ProtocolPhase, marked, visiting map[model.ProtocolPhase]bool) bool {
- if marked[state] {
- return true
- }
- if visiting[state] {
- return false
- }
- visiting[state] = true
- defer delete(visiting, state)
- for _, next := range edges[state] {
- if canReachMarked(next, edges, marked, visiting) {
- return true
- }
- }
- return false
-}
diff --git a/boatstack/internal/kernel/catalog/goal_contract.go b/boatstack/internal/kernel/catalog/goal_contract.go
new file mode 100644
index 0000000..6c413b4
--- /dev/null
+++ b/boatstack/internal/kernel/catalog/goal_contract.go
@@ -0,0 +1,85 @@
+package catalog
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+)
+
+// GoalContract is the compiled terminal law supplied by the primary flow.
+// Extension conditions are conjunctive and therefore can only narrow the
+// terminal set.
+type GoalContract struct {
+ GoalKind model.GoalKind `json:"goal_kind"`
+ Conditions []FacetCondition `json:"conditions"`
+}
+
+type GoalContracts map[model.GoalKind]GoalContract
+
+func (c GoalContracts) Clone() GoalContracts {
+ result := make(GoalContracts, len(c))
+ for goal, contract := range c {
+ contract.Conditions = cloneConditions(contract.Conditions)
+ result[goal] = contract
+ }
+ return result
+}
+
+func NewGoalContracts(base []GoalContract, extension map[model.GoalKind][]FacetCondition) (GoalContracts, error) {
+ contracts := make(GoalContracts, len(base))
+ for _, contract := range base {
+ if !contract.GoalKind.Valid() || len(contract.Conditions) == 0 {
+ return nil, fmt.Errorf("goal contract requires a valid goal and conditions")
+ }
+ if _, exists := contracts[contract.GoalKind]; exists {
+ return nil, fmt.Errorf("duplicate goal contract %q", contract.GoalKind)
+ }
+ conditions := append([]FacetCondition(nil), contract.Conditions...)
+ conditions = append(conditions, extension[contract.GoalKind]...)
+ for _, condition := range conditions {
+ if !condition.Facet.Valid() || len(condition.Statuses) == 0 {
+ return nil, fmt.Errorf("goal %q has invalid terminal condition", contract.GoalKind)
+ }
+ for _, status := range condition.Statuses {
+ if !status.Valid() {
+ return nil, fmt.Errorf("goal %q has invalid terminal status %q", contract.GoalKind, status)
+ }
+ }
+ }
+ contract.Conditions = conditions
+ contracts[contract.GoalKind] = contract
+ }
+ for goal := range extension {
+ if _, exists := contracts[goal]; !exists {
+ return nil, fmt.Errorf("extension constrains unsupported goal %q", goal)
+ }
+ }
+ return contracts, nil
+}
+
+func (c GoalContracts) Matches(snapshot model.Snapshot, goal model.Goal) bool {
+ if snapshot.Goal.Status != model.FactKnown || snapshot.Goal.Value != goal {
+ return false
+ }
+ contract, ok := c[goal.Kind]
+ if !ok {
+ return false
+ }
+ for _, condition := range contract.Conditions {
+ if !condition.Matches(snapshot) {
+ return false
+ }
+ }
+ return true
+}
+
+func (c GoalContracts) All() []GoalContract {
+ result := make([]GoalContract, 0, len(c))
+ for _, contract := range c {
+ contract.Conditions = append([]FacetCondition(nil), contract.Conditions...)
+ result = append(result, contract)
+ }
+ sort.Slice(result, func(i, j int) bool { return result[i].GoalKind < result[j].GoalKind })
+ return result
+}
diff --git a/boatstack/internal/kernel/catalog/predicates.go b/boatstack/internal/kernel/catalog/predicates.go
deleted file mode 100644
index e7af512..0000000
--- a/boatstack/internal/kernel/catalog/predicates.go
+++ /dev/null
@@ -1,281 +0,0 @@
-package catalog
-
-import (
- "strings"
-
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
-)
-
-func known(facet model.FacetName, values ...string) FacetCondition {
- return FacetCondition{Facet: facet, Statuses: []model.FactStatus{model.FactKnown}, Values: values}
-}
-
-func statuses(facet model.FacetName, values ...model.FactStatus) FacetCondition {
- return FacetCondition{Facet: facet, Statuses: values}
-}
-
-func requiresEngagement(id TransitionID) bool {
- value := string(id)
- return strings.HasPrefix(value, "plan.") || strings.HasPrefix(value, "workspace.") ||
- strings.HasPrefix(value, "gate.") || strings.HasPrefix(value, "evidence.") ||
- strings.HasPrefix(value, "delivery.") || strings.HasPrefix(value, "publication.") ||
- id == "configuration.mutate" || id == "installation.update"
-}
-
-func requiresHealthyConfiguration(id TransitionID) bool {
- if id == "engagement.begin" || id == "engagement.renew" || id == "engagement.release" || id == "installation.update" {
- return true
- }
- value := string(id)
- if strings.HasPrefix(value, "plan.") {
- return id != "plan.invalidate" && id != "plan.abandon"
- }
- if strings.HasPrefix(value, "workspace.") {
- return id != "workspace.abandon" && id != "workspace.reconcile"
- }
- if strings.HasPrefix(value, "gate.") || id == "evidence.visual.attach" || id == "delivery.slice.advance" {
- return true
- }
- return strings.HasPrefix(value, "publication.") && id != "publication.abandon"
-}
-
-func requiresHealthyRuntime(id TransitionID) bool {
- if id == "engagement.begin" || id == "engagement.renew" || id == "engagement.release" || id == "configuration.mutate" {
- return true
- }
- value := string(id)
- if strings.HasPrefix(value, "plan.") {
- return id != "plan.invalidate" && id != "plan.abandon"
- }
- if strings.HasPrefix(value, "workspace.") {
- return id != "workspace.abandon" && id != "workspace.reconcile"
- }
- if strings.HasPrefix(value, "gate.") || id == "evidence.visual.attach" || id == "delivery.slice.advance" {
- return true
- }
- return strings.HasPrefix(value, "publication.") && id != "publication.abandon"
-}
-
-func usesConfigurationPolicy(id TransitionID) bool {
- switch id {
- case "plan.approve", "plan.approve-amendment", "gate.review.record", "evidence.visual.attach":
- return true
- default:
- return false
- }
-}
-
-func sourceConditions(id TransitionID) []FacetCondition {
- switch id {
- case "engagement.begin":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementDormant), string(model.EngagementCommand)), known(model.FacetGoal)}
- case "engagement.renew":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementActive)), known(model.FacetGoal)}
- case "engagement.release":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementActive), string(model.EngagementStale), string(model.EngagementCommand)), known(model.FacetGoal)}
- case "invocation.rebind":
- return []FacetCondition{known(model.FacetTopology)}
- case "repository.attach":
- return []FacetCondition{known(model.FacetTopology, string(model.TopologyEmbedded))}
- case "repository.detach":
- return []FacetCondition{known(model.FacetTopology, string(model.TopologyDetached), string(model.TopologyHybrid))}
- case "runtime.hydrate":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeAbsent), string(model.RuntimeStale), string(model.RuntimeInvalid), string(model.RuntimeConflicting), string(model.RuntimeWrongSource), string(model.RuntimePartiallyPublished))}
- case "runtime.replace", "installation.update":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeVerified), string(model.RuntimeStale), string(model.RuntimeInvalid), string(model.RuntimeConflicting), string(model.RuntimeWrongSource), string(model.RuntimePartiallyPublished))}
- case "runtime.reconcile", "configuration.reconcile", "workspace.reconcile":
- return []FacetCondition{known(model.FacetRecoveryInfo)}
- case "configuration.initialize":
- return []FacetCondition{known(model.FacetConfiguration, string(model.ConfigurationUnsupported), string(model.ConfigurationStale), string(model.ConfigurationConflicting))}
- case "configuration.mutate":
- return []FacetCondition{known(model.FacetConfiguration, string(model.ConfigurationVerified), string(model.ConfigurationStale), string(model.ConfigurationDivergent), string(model.ConfigurationConflicting))}
- case "installation.initialize":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeAbsent), string(model.RuntimeInvalid), string(model.RuntimeStale))}
- case "goal.configure":
- return []FacetCondition{statuses(model.FacetGoal, model.FactKnown, model.FactAbsent)}
- case "plan.create":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanAbsent), string(model.PlanInvalid), string(model.PlanStale))}
- case "plan.validate":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanDraft))}
- case "plan.approve":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanValid))}
- case "plan.activate":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanApproved))}
- case "plan.amend":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanApproved), string(model.PlanLocked), string(model.PlanStale), string(model.PlanAmendmentRequired))}
- case "plan.approve-amendment":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanAmendmentRequired))}
- case "plan.invalidate":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanDraft), string(model.PlanValid), string(model.PlanApproved), string(model.PlanLocked), string(model.PlanStale))}
- case "plan.abandon", "publication.abandon":
- return []FacetCondition{known(model.FacetDelivery, string(model.DeliveryUninitialized), string(model.DeliveryPlanning), string(model.DeliveryApproved), string(model.DeliveryActive), string(model.DeliveryGatesPassed), string(model.DeliveryPublished), string(model.DeliveryAmendment), string(model.DeliveryInvalid), string(model.DeliveryRecovery))}
- case "workspace.cut":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceAbsent))}
- case "workspace.sync", "workspace.activate":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceCut), string(model.WorkspaceActive))}
- case "workspace.publish":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceActive))}
- case "workspace.cleanup", "workspace.reap":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceLanded), string(model.WorkspaceAbandoned))}
- case "workspace.abandon":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceCut), string(model.WorkspaceActive), string(model.WorkspacePublished), string(model.WorkspaceAttentionRequired))}
- case "gate.build.record", "gate.test.record", "gate.review.record", "gate.change.record", "gate.journey.record":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanLocked)), known(model.FacetDelivery, string(model.DeliveryActive), string(model.DeliveryGatesPassed))}
- case "evidence.visual.attach":
- return []FacetCondition{known(model.FacetDelivery, string(model.DeliveryActive), string(model.DeliveryGatesPassed), string(model.DeliveryPublished))}
- case "evidence.approval.revoke":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanApproved), string(model.PlanLocked))}
- case "delivery.slice.advance":
- return []FacetCondition{known(model.FacetDelivery, string(model.DeliveryActive))}
- case "publication.preview":
- return []FacetCondition{
- known(model.FacetPlan, string(model.PlanLocked)),
- known(model.FacetVerification, string(model.VerificationCurrent)),
- known(model.FacetWorkspace, string(model.WorkspaceActive), string(model.WorkspacePublished)),
- }
- case "publication.execute":
- return []FacetCondition{
- known(model.FacetPublication, string(model.PublicationCandidate)),
- known(model.FacetVerification, string(model.VerificationCurrent)),
- known(model.FacetWorkspace, string(model.WorkspaceActive), string(model.WorkspacePublished)),
- }
- case "publication.observe":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationCandidate), string(model.PublicationPublishedNotLanded), string(model.PublicationOpen), string(model.PublicationClosedUnmerged), string(model.PublicationUnavailable), string(model.PublicationConflicting))}
- case "publication.reconcile":
- return []FacetCondition{known(model.FacetRecoveryInfo), known(model.FacetPublication)}
- case "publication.correct":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationOpen)), known(model.FacetVerification, string(model.VerificationCurrent))}
- case "recovery.resume", "recovery.rollback", "recovery.escalate":
- return []FacetCondition{known(model.FacetRecoveryInfo)}
- case "external.files-changed", "external.head-changed":
- return []FacetCondition{known(model.FacetVerification)}
- case "external.branch-changed":
- return []FacetCondition{known(model.FacetWorkspace)}
- case "external.runtime-disappeared":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeVerified), string(model.RuntimeStale))}
- case "external.configuration-drifted":
- return []FacetCondition{known(model.FacetConfiguration, string(model.ConfigurationVerified))}
- case "external.lease-expired":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementActive), string(model.EngagementCommand))}
- case "external.host-interrupted":
- return []FacetCondition{known(model.FacetTransaction, string(model.TransactionStaged), string(model.TransactionLocalApplied), string(model.TransactionExternalUncertain), string(model.TransactionVerifying)), known(model.FacetTransactionInfo)}
- case "external.ci-completed":
- return []FacetCondition{known(model.FacetVerification)}
- case "external.pr-opened", "external.pr-updated", "external.pr-closed", "external.pr-merged", "external.provider-unavailable":
- return []FacetCondition{known(model.FacetPublication)}
- default:
- return nil
- }
-}
-
-func targetConditions(id TransitionID) []FacetCondition {
- switch id {
- case "engagement.begin":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementCommand))}
- case "engagement.renew":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementActive))}
- case "engagement.release", "repository.detach":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementDormant))}
- case "invocation.rebind":
- return []FacetCondition{known(model.FacetTopology)}
- case "repository.attach":
- return []FacetCondition{known(model.FacetTopology, string(model.TopologyDetached), string(model.TopologyHybrid))}
- case "runtime.hydrate", "runtime.replace", "installation.update":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeVerified))}
- case "runtime.reconcile":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeVerified)), known(model.FacetTransaction, string(model.TransactionNone))}
- case "configuration.initialize", "configuration.mutate":
- return []FacetCondition{known(model.FacetConfiguration, string(model.ConfigurationVerified))}
- case "configuration.reconcile":
- return []FacetCondition{known(model.FacetConfiguration, string(model.ConfigurationVerified)), known(model.FacetTransaction, string(model.TransactionNone))}
- case "installation.initialize":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeVerified)), known(model.FacetConfiguration, string(model.ConfigurationVerified))}
- case "goal.configure":
- return []FacetCondition{known(model.FacetGoal)}
- case "plan.create":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanDraft))}
- case "plan.validate":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanValid))}
- case "plan.approve", "plan.approve-amendment":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanApproved))}
- case "plan.activate":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanLocked))}
- case "plan.amend":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanAmendmentRequired))}
- case "plan.invalidate":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanInvalid))}
- case "plan.abandon", "publication.abandon":
- return []FacetCondition{
- known(model.FacetDelivery, string(model.DeliveryDiscarded)),
- known(model.FacetRecovery, string(model.RecoveryNone)),
- known(model.FacetTransaction, string(model.TransactionNone)),
- }
- case "workspace.cut":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceCut))}
- case "workspace.sync", "workspace.activate":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceActive))}
- case "workspace.publish":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspacePublished))}
- case "workspace.cleanup", "workspace.reap":
- return []FacetCondition{known(model.FacetWorkspace, string(model.WorkspaceAbsent))}
- case "workspace.abandon":
- return []FacetCondition{
- known(model.FacetWorkspace, string(model.WorkspaceAbandoned)),
- known(model.FacetRecovery, string(model.RecoveryNone)),
- known(model.FacetTransaction, string(model.TransactionNone)),
- }
- case "workspace.reconcile", "recovery.resume", "recovery.rollback":
- return []FacetCondition{known(model.FacetRecovery, string(model.RecoveryNone)), known(model.FacetTransaction, string(model.TransactionNone))}
- case "gate.build.record", "gate.test.record", "gate.review.record", "gate.change.record", "gate.journey.record":
- return []FacetCondition{known(model.FacetVerification, string(model.VerificationCurrent))}
- case "evidence.visual.attach":
- return []FacetCondition{known(model.FacetDelivery)}
- case "evidence.approval.revoke":
- return []FacetCondition{known(model.FacetPlan, string(model.PlanValid))}
- case "delivery.slice.advance":
- return []FacetCondition{known(model.FacetDelivery, string(model.DeliveryActive))}
- case "publication.preview":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationCandidate))}
- case "publication.execute":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationPublishedNotLanded))}
- case "publication.observe":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationOpen), string(model.PublicationMerged), string(model.PublicationClosedUnmerged), string(model.PublicationUnavailable), string(model.PublicationConflicting))}
- case "publication.reconcile":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationOpen), string(model.PublicationMerged), string(model.PublicationClosedUnmerged), string(model.PublicationUnavailable), string(model.PublicationConflicting)), known(model.FacetTransaction, string(model.TransactionNone))}
- case "publication.correct":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationPublishedNotLanded))}
- case "recovery.escalate":
- return []FacetCondition{known(model.FacetRecovery, string(model.RecoveryEscalated)), known(model.FacetTransaction, string(model.TransactionNone))}
- case "external.files-changed", "external.head-changed", "external.ci-completed":
- return []FacetCondition{known(model.FacetVerification)}
- case "external.branch-changed":
- return []FacetCondition{known(model.FacetWorkspace)}
- case "external.runtime-disappeared":
- return []FacetCondition{known(model.FacetRuntime, string(model.RuntimeAbsent), string(model.RuntimeStale))}
- case "external.configuration-drifted":
- return []FacetCondition{known(model.FacetConfiguration, string(model.ConfigurationStale), string(model.ConfigurationDivergent), string(model.ConfigurationConflicting))}
- case "external.lease-expired":
- return []FacetCondition{known(model.FacetEngagement, string(model.EngagementDormant), string(model.EngagementStale))}
- case "external.host-interrupted":
- return []FacetCondition{known(model.FacetRecovery)}
- case "external.pr-opened", "external.pr-updated":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationOpen))}
- case "external.pr-closed":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationClosedUnmerged), string(model.PublicationMerged))}
- case "external.pr-merged":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationMerged))}
- case "external.provider-unavailable":
- return []FacetCondition{known(model.FacetPublication, string(model.PublicationUnavailable))}
- default:
- return nil
- }
-}
-
-func requiredAuthorities(id TransitionID) []AuthorityClass {
- switch id {
- case "publication.execute", "publication.correct":
- return []AuthorityClass{AuthorityProvider}
- default:
- return nil
- }
-}
diff --git a/boatstack/internal/kernel/catalog/transition.go b/boatstack/internal/kernel/catalog/transition.go
index 990eccf..7485211 100644
--- a/boatstack/internal/kernel/catalog/transition.go
+++ b/boatstack/internal/kernel/catalog/transition.go
@@ -4,7 +4,6 @@ import (
"fmt"
"regexp"
"sort"
- "strings"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
)
@@ -12,6 +11,73 @@ import (
type TransitionID string
type EffectID string
+type OriginKind string
+
+const (
+ OriginCoreSystem OriginKind = "core-system"
+ OriginPrimaryFlow OriginKind = "primary-flow"
+ OriginExtension OriginKind = "extension"
+)
+
+func (k OriginKind) Valid() bool {
+ switch k {
+ case OriginCoreSystem, OriginPrimaryFlow, OriginExtension:
+ return true
+ default:
+ return false
+ }
+}
+
+type TransitionOrigin struct {
+ Kind OriginKind `json:"kind"`
+ ID string `json:"id"`
+ Version string `json:"version"`
+ ManifestFingerprint string `json:"manifest_fingerprint"`
+}
+
+type SelectionClass string
+
+const (
+ SelectionSystemRecovery SelectionClass = "SYSTEM_RECOVERY"
+ SelectionFlowRecovery SelectionClass = "FLOW_RECOVERY"
+ SelectionExtensionRecovery SelectionClass = "EXTENSION_RECOVERY"
+ SelectionGoalRequired SelectionClass = "GOAL_REQUIRED"
+ SelectionFlowProgress SelectionClass = "FLOW_PROGRESS"
+ SelectionExplicitOnly SelectionClass = "EXPLICIT_ONLY"
+ SelectionObservedExternal SelectionClass = "OBSERVED_EXTERNAL"
+)
+
+func (c SelectionClass) Valid() bool {
+ switch c {
+ case SelectionSystemRecovery, SelectionFlowRecovery, SelectionExtensionRecovery, SelectionGoalRequired,
+ SelectionFlowProgress, SelectionExplicitOnly, SelectionObservedExternal:
+ return true
+ default:
+ return false
+ }
+}
+
+func (c SelectionClass) rank() int {
+ switch c {
+ case SelectionSystemRecovery:
+ return 1
+ case SelectionFlowRecovery:
+ return 2
+ case SelectionExtensionRecovery:
+ return 3
+ case SelectionGoalRequired:
+ return 4
+ case SelectionFlowProgress:
+ return 5
+ case SelectionExplicitOnly:
+ return 6
+ case SelectionObservedExternal:
+ return 7
+ default:
+ return 99
+ }
+}
+
type EventClass string
const (
@@ -24,15 +90,6 @@ const (
func (c EventClass) Controllable() bool { return c != EventObservedExternal }
-func GateName(id TransitionID) (string, bool) {
- value := string(id)
- if !strings.HasPrefix(value, "gate.") || !strings.HasSuffix(value, ".record") {
- return "", false
- }
- name := strings.TrimSuffix(strings.TrimPrefix(value, "gate."), ".record")
- return name, name != ""
-}
-
func (c EventClass) Valid() bool {
switch c {
case EventOwnedLocal, EventOwnedExternal, EventAuthority, EventObservedExternal, EventRecovery:
@@ -119,6 +176,16 @@ type InterruptionContract struct {
ResumptionPredicate string `json:"resumption_predicate"`
}
+type PolicyContract struct {
+ RequiredWhen string `json:"required_when,omitempty"`
+ AuthorityRule string `json:"authority_rule,omitempty"`
+ AvailabilityRule string `json:"availability_rule,omitempty"`
+ CurrentEvidencePrefix string `json:"current_evidence_prefix,omitempty"`
+ ManagedOperations []string `json:"managed_operations,omitempty"`
+ BindsRequestedGoal bool `json:"binds_requested_goal,omitempty"`
+ ReconcilesProgram bool `json:"reconciles_program,omitempty"`
+}
+
// FacetCondition is an executable, serializable predicate over one canonical
// control facet. Values are ORed; conditions on a transition are ANDed.
type FacetCondition struct {
@@ -156,38 +223,44 @@ func (c FacetCondition) Matches(snapshot model.Snapshot) bool {
// Transition is both the executable runtime declaration and the source for the
// generated finite-state model. No second graph is maintained.
type Transition struct {
- ID TransitionID `json:"id"`
- Version int `json:"version"`
- Class EventClass `json:"class"`
- SourcePhases []model.ProtocolPhase `json:"source_phases"`
- TargetPhases []model.ProtocolPhase `json:"target_phases"`
- GoalKinds []model.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"`
- Priority int `json:"priority"`
- AllowsIdentityRebind bool `json:"allows_identity_rebind,omitempty"`
- AllowsWorktreeTransfer bool `json:"allows_worktree_transfer,omitempty"`
+ ID TransitionID `json:"id"`
+ Version int `json:"version"`
+ Origin TransitionOrigin `json:"origin"`
+ Owner string `json:"owner"`
+ SelectionClass SelectionClass `json:"selection_class"`
+ Class EventClass `json:"class"`
+ SourcePhases []model.ProtocolPhase `json:"source_phases"`
+ TargetPhases []model.ProtocolPhase `json:"target_phases"`
+ GoalKinds []model.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"`
}
func (t Transition) Controllable() bool { return t.Class.Controllable() }
@@ -198,18 +271,11 @@ func (t Transition) Controllable() bool { return t.Class.Controllable() }
// transition, but cannot outrank the configured goal by merely being
// admissible from the same snapshot.
func (t Transition) ImplicitlySelectable() bool {
- switch t.ID {
- case "engagement.renew", "engagement.release",
- "invocation.rebind", "repository.attach", "repository.detach",
- "runtime.replace", "configuration.mutate", "installation.update",
- "plan.amend", "plan.invalidate", "plan.abandon",
- "workspace.sync", "workspace.publish", "workspace.cleanup", "workspace.reap", "workspace.abandon",
- "gate.change.record", "gate.journey.record", "evidence.approval.revoke", "delivery.slice.advance",
- "publication.correct", "publication.abandon":
- return false
- default:
- return true
- }
+ return t.SelectionClass == SelectionSystemRecovery ||
+ t.SelectionClass == SelectionFlowRecovery ||
+ t.SelectionClass == SelectionExtensionRecovery ||
+ t.SelectionClass == SelectionGoalRequired ||
+ t.SelectionClass == SelectionFlowProgress
}
func (t Transition) SupportsGoal(goal model.Goal) bool {
@@ -262,14 +328,18 @@ func (t Transition) DeclaresTargetPhase(phase model.ProtocolPhase) bool {
}
type Registry struct {
- ordered []Transition
- byID map[TransitionID]Transition
+ ordered []Transition
+ byID map[TransitionID]Transition
+ managedOperation map[string]TransitionID
}
var semanticID = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$`)
func New(transitions []Transition) (Registry, error) {
- registry := Registry{ordered: append([]Transition(nil), transitions...), byID: make(map[TransitionID]Transition, len(transitions))}
+ registry := Registry{
+ ordered: cloneTransitions(transitions), byID: make(map[TransitionID]Transition, len(transitions)),
+ managedOperation: make(map[string]TransitionID),
+ }
for index, transition := range registry.ordered {
if err := validateTransition(transition); err != nil {
return Registry{}, fmt.Errorf("transition %d: %w", index, err)
@@ -277,7 +347,13 @@ func New(transitions []Transition) (Registry, error) {
if _, exists := registry.byID[transition.ID]; exists {
return Registry{}, fmt.Errorf("duplicate transition id %q", transition.ID)
}
- registry.byID[transition.ID] = transition
+ registry.byID[transition.ID] = cloneTransition(transition)
+ for _, operation := range transition.Policy.ManagedOperations {
+ if prior, exists := registry.managedOperation[operation]; exists {
+ return Registry{}, fmt.Errorf("managed operation %q is owned by both %q and %q", operation, prior, transition.ID)
+ }
+ registry.managedOperation[operation] = transition.ID
+ }
}
for _, transition := range registry.ordered {
if recovery := transition.Interruption.Recovery; recovery != "" {
@@ -288,6 +364,9 @@ func New(transitions []Transition) (Registry, error) {
}
}
sort.SliceStable(registry.ordered, func(i, j int) bool {
+ if registry.ordered[i].SelectionClass.rank() != registry.ordered[j].SelectionClass.rank() {
+ return registry.ordered[i].SelectionClass.rank() < registry.ordered[j].SelectionClass.rank()
+ }
if registry.ordered[i].Priority != registry.ordered[j].Priority {
return registry.ordered[i].Priority < registry.ordered[j].Priority
}
@@ -303,6 +382,21 @@ func validateTransition(t Transition) error {
if !t.Class.Valid() {
return fmt.Errorf("%s: invalid event class %q", t.ID, t.Class)
}
+ if !t.Origin.Kind.Valid() || t.Origin.ID == "" || t.Origin.Version == "" || t.Origin.ManifestFingerprint == "" || t.Owner == "" {
+ return fmt.Errorf("%s: transition origin, owner, version, and manifest fingerprint are required", t.ID)
+ }
+ if !t.SelectionClass.Valid() {
+ return fmt.Errorf("%s: invalid selection class %q", t.ID, t.SelectionClass)
+ }
+ if t.Class == EventObservedExternal && t.SelectionClass != SelectionObservedExternal {
+ return fmt.Errorf("%s: observed external transition must use OBSERVED_EXTERNAL selection", t.ID)
+ }
+ if t.Class != EventObservedExternal && t.SelectionClass == SelectionObservedExternal {
+ return fmt.Errorf("%s: controllable transition cannot use OBSERVED_EXTERNAL selection", t.ID)
+ }
+ if err := validateSelectionOwnership(t); err != nil {
+ return err
+ }
if t.Version < 1 || len(t.SourcePhases) == 0 || len(t.TargetPhases) == 0 {
return fmt.Errorf("%s: version, source phases, and target phases are required", t.ID)
}
@@ -311,6 +405,13 @@ func validateTransition(t Transition) error {
return fmt.Errorf("%s: invalid phase %q", t.ID, phase)
}
}
+ goalKinds := map[model.GoalKind]bool{}
+ for _, goal := range t.GoalKinds {
+ if !goal.Valid() || goalKinds[goal] {
+ return fmt.Errorf("%s: goal kinds must be valid and unique", t.ID)
+ }
+ goalKinds[goal] = true
+ }
if len(t.Authority) == 0 || len(t.RequiredEvidence) == 0 {
return fmt.Errorf("%s: authority and evidence declarations are required", t.ID)
}
@@ -376,6 +477,23 @@ func validateTransition(t Transition) error {
}
parameterNames[parameter.Name] = true
}
+ managedOperations := map[string]bool{}
+ for _, operation := range t.Policy.ManagedOperations {
+ if !semanticID.MatchString(operation) || managedOperations[operation] {
+ return fmt.Errorf("%s: managed operations must be semantic and unique", t.ID)
+ }
+ managedOperations[operation] = true
+ }
+ if t.Policy.BindsRequestedGoal && (t.Origin.Kind != OriginCoreSystem || !conditionNamesFacet(t.TargetConditions, model.FacetGoal)) {
+ return fmt.Errorf("%s: requested-goal binding requires a CoreSystem goal target", t.ID)
+ }
+ if t.Policy.ReconcilesProgram && (t.Origin.Kind != OriginCoreSystem || !conditionNamesFacet(t.TargetConditions, model.FacetProgram)) {
+ return fmt.Errorf("%s: program reconciliation requires a CoreSystem program target", t.ID)
+ }
+ providerDeclared := containsAuthorityClass(t.Authority, AuthorityProvider) || containsAuthorityClass(t.AuthorityAll, AuthorityProvider)
+ if t.AuthorityFingerprintParameter != "" && (!parameterNames[t.AuthorityFingerprintParameter] || !providerDeclared) {
+ return fmt.Errorf("%s: provider authority binding requires a declared parameter and mandatory provider authority", t.ID)
+ }
if t.Reversibility == "" || t.PrivacyClassification == "" || t.TelemetryClassification == "" || t.CostClass == "" {
return fmt.Errorf("%s: recovery/telemetry metadata are incomplete", t.ID)
}
@@ -385,21 +503,110 @@ func validateTransition(t Transition) error {
return nil
}
+func conditionNamesFacet(conditions []FacetCondition, facet model.FacetName) bool {
+ for _, condition := range conditions {
+ if condition.Facet == facet {
+ return true
+ }
+ }
+ return false
+}
+
+func validateSelectionOwnership(t Transition) error {
+ switch t.SelectionClass {
+ case SelectionSystemRecovery:
+ 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 SelectionExtensionRecovery:
+ if t.Origin.Kind != OriginExtension || t.Class != EventRecovery {
+ return fmt.Errorf("%s: EXTENSION_RECOVERY is reserved for extension recovery events", t.ID)
+ }
+ default:
+ if t.Class == EventRecovery {
+ return fmt.Errorf("%s: recovery event requires its origin-specific recovery selection class", t.ID)
+ }
+ }
+ return nil
+}
+
+func containsAuthorityClass(values []AuthorityClass, wanted AuthorityClass) bool {
+ for _, value := range values {
+ if value == wanted {
+ return true
+ }
+ }
+ return false
+}
+
func (r Registry) Len() int { return len(r.ordered) }
-func (r Registry) All() []Transition { return append([]Transition(nil), r.ordered...) }
+func (r Registry) All() []Transition { return cloneTransitions(r.ordered) }
func (r Registry) Lookup(id TransitionID) (Transition, bool) {
transition, ok := r.byID[id]
- return transition, ok
+ return cloneTransition(transition), ok
+}
+
+// ManagedTransition resolves a host-classified operation through the compiled
+// program. Command classifiers never select flow transition IDs themselves.
+func (r Registry) ManagedTransition(operation string) (Transition, bool) {
+ id, ok := r.managedOperation[operation]
+ if !ok {
+ return Transition{}, false
+ }
+ return r.Lookup(id)
}
func (r Registry) Admissible(snapshot model.Snapshot, goal model.Goal) []Transition {
var result []Transition
for _, transition := range r.ordered {
if transition.Controllable() && transition.SourceMatches(snapshot) && transition.SupportsGoal(goal) {
- result = append(result, transition)
+ result = append(result, cloneTransition(transition))
}
}
return result
}
+
+func cloneTransitions(values []Transition) []Transition {
+ result := make([]Transition, len(values))
+ for index, value := range values {
+ result[index] = cloneTransition(value)
+ }
+ return result
+}
+
+func cloneTransition(value Transition) Transition {
+ value.SourcePhases = append([]model.ProtocolPhase(nil), value.SourcePhases...)
+ value.TargetPhases = append([]model.ProtocolPhase(nil), value.TargetPhases...)
+ value.GoalKinds = append([]model.GoalKind(nil), value.GoalKinds...)
+ value.RequiredIdentity = append([]string(nil), value.RequiredIdentity...)
+ value.Authority = append([]AuthorityClass(nil), value.Authority...)
+ value.AuthorityAll = append([]AuthorityClass(nil), value.AuthorityAll...)
+ value.RequiredEvidence = append([]string(nil), value.RequiredEvidence...)
+ value.OwnedResources = append([]string(nil), value.OwnedResources...)
+ value.LocalEffects = append([]EffectID(nil), value.LocalEffects...)
+ value.ExternalEffects = append([]EffectID(nil), value.ExternalEffects...)
+ value.Parameters = append([]ParameterSpec(nil), value.Parameters...)
+ value.Prescription.Arguments = append([]string(nil), value.Prescription.Arguments...)
+ value.SourceConditions = cloneConditions(value.SourceConditions)
+ value.TargetConditions = cloneConditions(value.TargetConditions)
+ value.Interruption.Points = append([]string(nil), value.Interruption.Points...)
+ value.Interruption.PartialState = append([]string(nil), value.Interruption.PartialState...)
+ value.Policy.ManagedOperations = append([]string(nil), value.Policy.ManagedOperations...)
+ return value
+}
+
+func cloneConditions(values []FacetCondition) []FacetCondition {
+ result := make([]FacetCondition, len(values))
+ for index, value := range values {
+ value.Statuses = append([]model.FactStatus(nil), value.Statuses...)
+ value.Values = append([]string(nil), value.Values...)
+ result[index] = value
+ }
+ return result
+}
diff --git a/boatstack/internal/kernel/durable/state.go b/boatstack/internal/kernel/durable/state.go
index dcd8c4d..64e2624 100644
--- a/boatstack/internal/kernel/durable/state.go
+++ b/boatstack/internal/kernel/durable/state.go
@@ -25,6 +25,7 @@ type State struct {
RepositoryID string `json:"repository_id"`
GitCommonID string `json:"git_common_id"`
WorktreeID string `json:"worktree_id"`
+ ProgramFingerprint string `json:"program_fingerprint,omitempty"`
Revision uint64 `json:"revision"`
Phase model.ProtocolPhase `json:"phase"`
Engagement model.EngagementState `json:"engagement"`
@@ -88,6 +89,9 @@ func (s State) Validate() error {
if s.RepositoryID == "" || s.GitCommonID == "" || s.WorktreeID == "" || s.Revision == 0 || s.UpdatedAt.IsZero() {
return fmt.Errorf("durable state identity, revision, and update time are required")
}
+ if s.ProgramFingerprint != "" && len(s.ProgramFingerprint) != 64 {
+ return fmt.Errorf("durable state has invalid program fingerprint")
+ }
if !s.Phase.Valid() || !s.Engagement.Valid() || !s.Delivery.Valid() || !s.Workspace.Valid() || !s.Plan.Valid() ||
!s.Configuration.Valid() || !s.Runtime.Valid() || !s.Publication.Valid() || !s.Verification.Valid() ||
!s.Recovery.Valid() || !s.Transaction.Valid() || !s.Terminal.Valid() {
diff --git a/boatstack/internal/kernel/engine/engine.go b/boatstack/internal/kernel/engine/engine.go
index 19626d1..e0d24f0 100644
--- a/boatstack/internal/kernel/engine/engine.go
+++ b/boatstack/internal/kernel/engine/engine.go
@@ -14,21 +14,26 @@ import (
)
type Engine struct {
- registry catalog.Registry
- control supervisor.Supervisor
- observer ports.Observer
- clock ports.Clock
- locker ports.Locker
- journal ports.Journal
- effects ports.EffectDriver
- receipts ports.ReceiptStore
+ registry catalog.Registry
+ control supervisor.Supervisor
+ programFingerprint string
+ observer ports.Observer
+ clock ports.Clock
+ locker ports.Locker
+ journal ports.Journal
+ effects ports.EffectDriver
+ receipts ports.ReceiptStore
}
-func New(registry catalog.Registry, observer ports.Observer, clock ports.Clock, locker ports.Locker, journal ports.Journal, effects ports.EffectDriver, receipts ports.ReceiptStore) (Engine, error) {
- if registry.Len() == 0 || observer == nil || clock == nil || locker == nil || journal == nil || effects == nil || receipts == nil {
- return Engine{}, fmt.Errorf("kernel engine requires registry, observer, clock, locker, journal, effects, and receipt store")
+func New(registry catalog.Registry, contracts catalog.GoalContracts, programFingerprint string, observer ports.Observer, clock ports.Clock, locker ports.Locker, journal ports.Journal, effects ports.EffectDriver, receipts ports.ReceiptStore) (Engine, error) {
+ if registry.Len() == 0 || len(contracts) == 0 || len(programFingerprint) != 64 || observer == nil || clock == nil || locker == nil || journal == nil || effects == nil || receipts == nil {
+ return Engine{}, fmt.Errorf("kernel engine requires registry, goal contracts, observer, clock, locker, journal, effects, and receipt store")
}
- return Engine{registry: registry, control: supervisor.New(registry), observer: observer, clock: clock, locker: locker, journal: journal, effects: effects, receipts: receipts}, nil
+ return Engine{registry: registry, control: supervisor.New(registry, contracts), programFingerprint: programFingerprint, observer: observer, clock: clock, locker: locker, journal: journal, effects: effects, receipts: receipts}, nil
+}
+
+func (e Engine) canonicalize(observation model.Observation) (model.Snapshot, error) {
+ return model.CanonicalizeForProgram(observation, e.programFingerprint)
}
type ResolveRequest struct {
@@ -46,15 +51,15 @@ type Resolution struct {
func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution, error) {
if err := request.Invocation.Validate(false); err != nil {
- return Resolution{}, err
+ return unresolvedResolution(request.Goal, "invocation identity is invalid"), err
}
observation, err := e.observer.Observe(ctx, ports.ObservationRequest{Invocation: request.Invocation})
if err != nil {
- return Resolution{}, fmt.Errorf("observe plant: %w", err)
+ return unresolvedResolution(request.Goal, "required observation failed"), fmt.Errorf("observe plant: %w", err)
}
- snapshot, err := model.Canonicalize(observation)
+ snapshot, err := e.canonicalize(observation)
if err != nil {
- return Resolution{}, fmt.Errorf("canonicalize observation: %w", err)
+ return unresolvedResolution(request.Goal, "canonical observation is invalid"), fmt.Errorf("canonicalize observation: %w", err)
}
if snapshot.Invocation != request.Invocation {
return Resolution{}, fmt.Errorf("observer returned a different invocation identity")
@@ -74,6 +79,10 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution
return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil
}
+func unresolvedResolution(goal model.Goal, reason string) Resolution {
+ return Resolution{Goal: goal, Decision: supervisor.Decision{Kind: supervisor.DecisionUnresolved, Reason: reason}}
+}
+
type ApplyRequest struct {
ResolveRequest
FlowID string
@@ -137,14 +146,14 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
return result, fmt.Errorf("check supplied idempotency key: %w", err)
}
if ok {
- if err := validateReplayRequest(prior, request); err != nil {
+ if err := validateReplayRequest(prior, request, e.programFingerprint); err != nil {
return result, err
}
observation, observeErr := e.observer.Observe(ctx, ports.ObservationRequest{Invocation: request.Invocation})
if observeErr != nil {
return result, observeErr
}
- snapshot, canonicalErr := model.Canonicalize(observation)
+ snapshot, canonicalErr := e.canonicalize(observation)
if canonicalErr != nil {
return result, canonicalErr
}
@@ -162,10 +171,10 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
request.AdmissionLifetime = 2 * time.Minute
}
resolution, err := e.Resolve(ctx, request.ResolveRequest)
+ result.Source, result.Goal, result.Decision = resolution.Snapshot, resolution.Goal, resolution.Decision
if err != nil {
return result, err
}
- result.Source, result.Goal, result.Decision = resolution.Snapshot, resolution.Goal, resolution.Decision
request.Goal = resolution.Goal
if resolution.Decision.Kind != supervisor.DecisionPrescribed || resolution.Decision.Transition == nil {
return result, DecisionError{Decision: resolution.Decision}
@@ -187,14 +196,14 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
if prior, ok, err := e.receipts.FindByIdempotency(ctx, request.Invocation, admission.IdempotencyKey); err != nil {
return result, fmt.Errorf("check idempotency: %w", err)
} else if ok {
- if err := validateReplayRequest(prior, request); err != nil {
+ if err := validateReplayRequest(prior, request, e.programFingerprint); err != nil {
return result, err
}
observation, observeErr := e.observer.Observe(ctx, ports.ObservationRequest{Invocation: request.Invocation})
if observeErr != nil {
return result, observeErr
}
- current, canonicalErr := model.Canonicalize(observation)
+ current, canonicalErr := e.canonicalize(observation)
if canonicalErr != nil {
return result, canonicalErr
}
@@ -219,14 +228,14 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
if err != nil {
return result, err
}
- lockedSnapshot, err := model.Canonicalize(lockedObservation)
+ lockedSnapshot, err := e.canonicalize(lockedObservation)
if err != nil {
return result, err
}
if prior, ok, findErr := e.receipts.FindByIdempotency(ctx, request.Invocation, admission.IdempotencyKey); findErr != nil {
return result, fmt.Errorf("check locked idempotency: %w", findErr)
} else if ok {
- if err := validateReplayRequest(prior, request); err != nil {
+ if err := validateReplayRequest(prior, request, e.programFingerprint); err != nil {
return result, err
}
if !replayStateSettled(lockedSnapshot) {
@@ -268,6 +277,10 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
startedAt := e.clock.Now()
effectResult, effectErr := prepared.Execute(ctx)
if effectErr != nil {
+ if transition.Class == catalog.EventOwnedExternal {
+ unknown := ExternalOutcomeUnknownError{Transition: transition.ID, Recovery: transition.Interruption.Recovery}
+ return result, requireRecovery("external effect returned without a provable outcome", errors.Join(unknown, effectErr))
+ }
rollbackErr := prepared.Rollback(ctx)
if rollbackErr != nil {
return result, requireRecovery("effect and rollback failed", errors.Join(effectErr, rollbackErr))
@@ -281,11 +294,11 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
if transferred, ok := prepared.VerificationInvocation(); ok {
verificationInvocation = transferred
}
- targetObservation, err := e.observer.Observe(ctx, ports.ObservationRequest{Invocation: verificationInvocation, IgnoreAdmissionID: admission.ID})
+ targetObservation, err := e.observer.Observe(ctx, ports.ObservationRequest{Invocation: verificationInvocation, IgnoreAdmissionID: admission.ID, VerifyTransitionID: transition.ID})
if err != nil {
return result, requireRecovery("target observation failed after effect", err)
}
- target, err := model.Canonicalize(targetObservation)
+ target, err := e.canonicalize(targetObservation)
if err != nil {
return result, requireRecovery("target canonicalization failed after effect", err)
}
@@ -324,7 +337,10 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
return result, nil
}
-func validateReplayRequest(prior protocol.TransitionReceipt, request ApplyRequest) error {
+func validateReplayRequest(prior protocol.TransitionReceipt, request ApplyRequest, programFingerprint string) error {
+ if prior.ProgramFingerprint != programFingerprint {
+ return fmt.Errorf("idempotency receipt belongs to a different control program")
+ }
if prior.FlowID != request.FlowID {
return fmt.Errorf("idempotency receipt belongs to flow %q, not %q", prior.FlowID, request.FlowID)
}
diff --git a/boatstack/internal/kernel/engine/engine_test.go b/boatstack/internal/kernel/engine/engine_test.go
index 568b25c..94f22f2 100644
--- a/boatstack/internal/kernel/engine/engine_test.go
+++ b/boatstack/internal/kernel/engine/engine_test.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"path/filepath"
+ "reflect"
"strings"
"sync"
"testing"
@@ -13,10 +14,25 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor"
)
type fixedClock struct{ now time.Time }
+const syntheticProgramFingerprint = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+
+func syntheticGoalContracts(t *testing.T) catalog.GoalContracts {
+ t.Helper()
+ contracts, err := catalog.NewGoalContracts([]catalog.GoalContract{{
+ GoalKind: model.GoalVerified,
+ Conditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}},
+ }}, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return contracts
+}
+
func (c fixedClock) Now() time.Time { return c.now }
func fixtureAbsolutePath(parts ...string) string {
@@ -43,6 +59,12 @@ func (o *sequenceObserver) Observe(context.Context, ports.ObservationRequest) (m
return item, nil
}
+type failingObserver struct{ err error }
+
+func (o failingObserver) Observe(context.Context, ports.ObservationRequest) (model.Observation, error) {
+ return model.Observation{}, o.err
+}
+
type fakeLock struct{ released bool }
func (l *fakeLock) Release() error { l.released = true; return nil }
@@ -82,6 +104,7 @@ func (j *fakeJournal) RequireRecovery(context.Context, string, string) error {
type fakeEffects struct {
executions, rollbacks int
result ports.EffectResult
+ err error
}
func (e *fakeEffects) Prepare(context.Context, protocol.Admission, catalog.Transition) (ports.PreparedEffect, error) {
@@ -93,7 +116,7 @@ func (e *fakeEffects) VerificationInvocation() (model.InvocationContext, bool) {
}
func (e *fakeEffects) Execute(context.Context) (ports.EffectResult, error) {
e.executions++
- return e.result, nil
+ return e.result, e.err
}
func (e *fakeEffects) Rollback(context.Context) error {
e.rollbacks++
@@ -128,6 +151,12 @@ func (s *memoryReceipts) Append(_ context.Context, receipt protocol.TransitionRe
func observation(phase model.ProtocolPhase, fingerprint string) model.Observation {
e := model.Evidence{Source: "fixture", Fingerprint: fingerprint, ObservedAt: time.Unix(20, 0).UTC()}
configurationEvidence := model.Evidence{Source: "configuration:/repo/.boatstack/project.json", Fingerprint: "config-fingerprint", ObservedAt: time.Unix(20, 0).UTC()}
+ stage := "start"
+ if phase == model.PhaseActive {
+ stage = "terminal"
+ } else if phase == model.PhaseRecovery {
+ stage = "verify"
+ }
return model.Observation{
SchemaVersion: model.SnapshotSchemaVersion,
Invocation: model.InvocationContext{RepositoryID: "repo", GitCommonID: "git", WorktreeID: "wt", Ref: "refs/heads/f", ControllerID: "ctl", InvokingPath: fixtureAbsolutePath("test-fixture", "repo"), RuntimePath: fixtureAbsolutePath("test-fixture", "runtime"), RuntimeFingerprint: "runtime", Topology: model.TopologyEmbedded, Host: "cli", Correlation: "corr"},
@@ -137,6 +166,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)},
}
}
@@ -155,6 +185,10 @@ func recoveryObservation(fingerprint string) model.Observation {
}
func testRegistry(t *testing.T) catalog.Registry {
+ return testRegistryWithAdvanceClass(t, catalog.EventOwnedLocal)
+}
+
+func testRegistryWithAdvanceClass(t *testing.T, class catalog.EventClass) catalog.Registry {
t.Helper()
identity := []string{"repository-id", "git-common-id", "worktree-id"}
interruption := func(recovery catalog.TransitionID) catalog.InterruptionContract {
@@ -164,17 +198,27 @@ func testRegistry(t *testing.T) catalog.Registry {
Recovery: recovery, RecoveryAuthority: "test-authority", ResumptionPredicate: "test-resumption",
}
}
+ authority := []catalog.AuthorityClass{catalog.AuthorityRepository}
+ localEffects := []catalog.EffectID{"test.advance"}
+ var externalEffects []catalog.EffectID
+ if class == catalog.EventOwnedExternal {
+ authority = []catalog.AuthorityClass{catalog.AuthorityHuman}
+ localEffects = nil
+ externalEffects = []catalog.EffectID{"test.advance"}
+ }
r, err := catalog.New([]catalog.Transition{{
- ID: "test.advance", Version: 1, Class: catalog.EventOwnedLocal,
+ 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,
SourcePhases: []model.ProtocolPhase{model.PhaseObserved}, TargetPhases: []model.ProtocolPhase{model.PhaseActive},
- RequiredIdentity: identity, Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"state"}, Effect: "test.advance", LocalEffects: []catalog.EffectID{"test.advance"}, Idempotent: true,
+ 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",
- SourceConditions: []catalog.FacetCondition{{Facet: model.FacetEngagement, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.EngagementActive)}}},
- TargetConditions: []catalog.FacetCondition{{Facet: model.FacetEngagement, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.EngagementActive)}}},
+ SourceConditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"start"}}},
+ TargetConditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}},
Interruption: interruption("test.recover"), Reversibility: catalog.Reversible, TerminalEffect: "none",
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,
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",
@@ -197,12 +241,45 @@ func request(now time.Time) ApplyRequest {
}, FlowID: "flow", AdmissionLifetime: time.Minute}
}
+func TestRequiredObserverFailureReturnsTypedUnresolvedDecision(t *testing.T) {
+ // control-law: required-observation-failure-is-a-typed-fail-closed-decision
+ now := time.Unix(30, 0).UTC()
+ journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, &fakeLock{}
+ kernel, err := New(
+ testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint,
+ failingObserver{err: errors.New("observer unavailable")}, fixedClock{now},
+ fakeLocker{lock}, journal, effects, receipts,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resolved, resolveErr := kernel.Resolve(context.Background(), request(now).ResolveRequest)
+ if resolveErr == nil || !strings.Contains(resolveErr.Error(), "observer unavailable") {
+ t.Fatalf("resolve error = %v, want observer failure", resolveErr)
+ }
+ if resolved.Decision.Kind != supervisor.DecisionUnresolved || resolved.Decision.Reason != "required observation failed" {
+ t.Fatalf("resolve decision = %+v, want typed UNRESOLVED", resolved.Decision)
+ }
+
+ applied, applyErr := kernel.Apply(context.Background(), request(now))
+ if applyErr == nil || !strings.Contains(applyErr.Error(), "observer unavailable") {
+ t.Fatalf("apply error = %v, want observer failure", applyErr)
+ }
+ if applied.Decision.Kind != supervisor.DecisionUnresolved || applied.Decision.Reason != "required observation failed" {
+ t.Fatalf("apply decision = %+v, want typed UNRESOLVED", applied.Decision)
+ }
+ if effects.executions != 0 || journal.begun != 0 || len(receipts.values) != 0 {
+ t.Fatalf("observer failure crossed mutation boundary: effects=%d journal=%d receipts=%d", effects.executions, journal.begun, len(receipts.values))
+ }
+}
+
func TestApplyCrossesAdmissionEffectVerificationAndReceiptBoundary(t *testing.T) {
- // control-law: managed-effect-requires-exact-admission-and-postcondition
+ // control-law: synthetic-flow-crosses-exact-admission-and-postcondition-without-standard-flow
now := time.Unix(30, 0).UTC()
observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source"), observation(model.PhaseActive, "target"), observation(model.PhaseActive, "target")}}
journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{}
- kernel, err := New(testRegistry(t), observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
+ kernel, err := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
if err != nil {
t.Fatal(err)
}
@@ -224,6 +301,46 @@ func TestApplyCrossesAdmissionEffectVerificationAndReceiptBoundary(t *testing.T)
}
}
+func TestSyntheticStartVerifyTerminalContractNeedsNoStandardFlowFacet(t *testing.T) {
+ // control-law: kernel-terminal-is-defined-only-by-the-compiled-primary-flow-contract
+ goal := model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"}
+ source, err := model.CanonicalizeForProgram(observation(model.PhaseObserved, "source"), syntheticProgramFingerprint)
+ if err != nil {
+ t.Fatal(err)
+ }
+ syntheticSupervisor := supervisor.New(testRegistry(t), syntheticGoalContracts(t))
+ one := syntheticSupervisor.Resolve(source, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "")
+ two := syntheticSupervisor.Resolve(source, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "")
+ if !reflect.DeepEqual(one, two) || one.Kind != supervisor.DecisionPrescribed || one.Transition == nil || one.Transition.ID != "test.advance" {
+ t.Fatalf("synthetic resolution is not deterministic: one=%+v two=%+v", one, two)
+ }
+ outside := syntheticSupervisor.Resolve(source, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "not.compiled")
+ if outside.Kind != supervisor.DecisionRefused || outside.Transition != nil {
+ t.Fatalf("transition outside compiled program was not refused: %+v", outside)
+ }
+
+ observed := observation(model.PhaseActive, "target")
+ evidence := observed.Phase.Evidence[0]
+ observed.Plan = model.Known(model.PlanAbsent, evidence)
+ observed.Workspace = model.Known(model.WorkspaceAbsent, evidence)
+ observed.Delivery = model.Known(model.DeliveryPlanning, evidence)
+ target, err := model.CanonicalizeForProgram(observed, syntheticProgramFingerprint)
+ if err != nil {
+ t.Fatal(err)
+ }
+ decision := syntheticSupervisor.Resolve(target, goal, nil, "")
+ if decision.Kind != supervisor.DecisionTerminal {
+ t.Fatalf("synthetic terminal decision = %+v", decision)
+ }
+ withoutContract := supervisor.New(testRegistry(t), catalog.GoalContracts{}).Resolve(target, goal, nil, "")
+ if withoutContract.Kind == supervisor.DecisionTerminal {
+ t.Fatalf("synthetic state terminated without a compiled flow goal contract: %+v", withoutContract)
+ }
+ if target.Terminal.Value != model.TerminalNonterminal || target.Plan.Value != model.PlanAbsent || target.Publication.Value != model.PublicationNone {
+ t.Fatalf("fixture unexpectedly relied on StandardFlow terminal state: %+v", target)
+ }
+}
+
func TestIdempotencyReceiptCannotHideUncommittedRecoveryJournal(t *testing.T) {
// control-law: receipt-before-journal-commit-is-not-a-clean-replay
now := time.Unix(30, 0).UTC()
@@ -232,7 +349,7 @@ func TestIdempotencyReceiptCannotHideUncommittedRecoveryJournal(t *testing.T) {
recoveryObservation("recovery"),
}}
journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{}
- kernel, err := New(testRegistry(t), observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
+ kernel, err := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
if err != nil {
t.Fatal(err)
}
@@ -257,7 +374,7 @@ func TestApplyRejectsSnapshotDriftBeforeEffect(t *testing.T) {
now := time.Unix(30, 0).UTC()
observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "drifted")}}
journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, &fakeLock{}
- kernel, _ := New(testRegistry(t), observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
+ kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
_, err := kernel.Apply(context.Background(), request(now))
var stale StaleAdmissionError
if !errors.As(err, &stale) {
@@ -273,7 +390,7 @@ func TestApplyRollsBackFailedPostconditionAndDoesNotReceipt(t *testing.T) {
now := time.Unix(30, 0).UTC()
observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "unchanged")}}
journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{}
- kernel, _ := New(testRegistry(t), observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
+ kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
_, err := kernel.Apply(context.Background(), request(now))
var postcondition PostconditionError
if !errors.As(err, &postcondition) {
@@ -290,7 +407,7 @@ func TestApplyRequiresRecoveryWhenJournalFailsAfterEffect(t *testing.T) {
observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source")}}
journal := &fakeJournal{failMark: "verifying"}
effects, receipts, lock := &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectSettled}}, &memoryReceipts{}, &fakeLock{}
- kernel, _ := New(testRegistry(t), observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
+ kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
_, err := kernel.Apply(context.Background(), request(now))
if err == nil || !strings.Contains(err.Error(), "injected journal mark failure") {
t.Fatalf("error=%v, want injected post-effect journal failure", err)
@@ -305,7 +422,7 @@ func TestApplyPreservesUnknownExternalOutcomeForReconciliation(t *testing.T) {
now := time.Unix(30, 0).UTC()
observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "unchanged")}}
journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{result: ports.EffectResult{Settlement: ports.EffectUnknown}}, &memoryReceipts{}, &fakeLock{}
- kernel, _ := New(testRegistry(t), observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
+ kernel, _ := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
_, err := kernel.Apply(context.Background(), request(now))
var unknown ExternalOutcomeUnknownError
if !errors.As(err, &unknown) {
@@ -315,3 +432,25 @@ func TestApplyPreservesUnknownExternalOutcomeForReconciliation(t *testing.T) {
t.Fatalf("external uncertainty was not preserved: effects=%+v journal=%+v receipts=%d", effects, journal, len(receipts.values))
}
}
+
+func TestOwnedExternalExecutionErrorRequiresRecoveryWithoutRollback(t *testing.T) {
+ // control-law: a returned transport error cannot prove an external effect did not settle
+ now := time.Unix(30, 0).UTC()
+ registry := testRegistryWithAdvanceClass(t, catalog.EventOwnedExternal)
+ observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source")}}
+ journal, effects, receipts, lock := &fakeJournal{}, &fakeEffects{err: context.DeadlineExceeded}, &memoryReceipts{}, &fakeLock{}
+ kernel, err := New(registry, syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{lock}, journal, effects, receipts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ apply := request(now)
+ apply.Authority.Receipts[0].Class = catalog.AuthorityHuman
+ _, err = kernel.Apply(context.Background(), apply)
+ var unknown ExternalOutcomeUnknownError
+ if !errors.As(err, &unknown) || !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("error=%v, want unknown external outcome joined with deadline", err)
+ }
+ if effects.executions != 1 || effects.rollbacks != 0 || journal.recovery != 1 || journal.aborted != 0 || len(receipts.values) != 0 {
+ t.Fatalf("ambiguous external error was collapsed: effects=%+v journal=%+v receipts=%d", effects, journal, len(receipts.values))
+ }
+}
diff --git a/boatstack/internal/kernel/model/facet.go b/boatstack/internal/kernel/model/facet.go
index 62a282b..cad16b1 100644
--- a/boatstack/internal/kernel/model/facet.go
+++ b/boatstack/internal/kernel/model/facet.go
@@ -2,6 +2,7 @@ package model
import (
"fmt"
+ "regexp"
"sort"
"strings"
)
@@ -13,6 +14,7 @@ type FacetName string
const (
FacetPhase FacetName = "phase"
+ FacetProgram FacetName = "program"
FacetTopology FacetName = "topology"
FacetEngagement FacetName = "engagement"
FacetDelivery FacetName = "delivery"
@@ -32,23 +34,29 @@ const (
)
var controllingFacets = []FacetName{
- FacetPhase, FacetTopology, FacetEngagement, FacetDelivery, FacetWorkspace,
+ FacetPhase, FacetProgram, FacetTopology, FacetEngagement, FacetDelivery, FacetWorkspace,
FacetPlan, FacetConfiguration, FacetConfigurationPolicy, FacetRuntime, FacetPublication,
FacetVerification, FacetRecovery, FacetTransaction, FacetRecoveryInfo,
FacetTransactionInfo, FacetTerminal, FacetGoal,
}
-func ControllingFacets() []FacetName { return append([]FacetName(nil), controllingFacets...) }
+var namespacedFacet = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+){2,}$`)
-func (f FacetName) Valid() bool {
+func controllingFacet(facet FacetName) bool {
for _, candidate := range controllingFacets {
- if f == candidate {
+ if facet == candidate {
return true
}
}
return false
}
+func ControllingFacets() []FacetName { return append([]FacetName(nil), controllingFacets...) }
+
+func (f FacetName) Valid() bool {
+ return controllingFacet(f) || namespacedFacet.MatchString(string(f))
+}
+
// Facet returns the status and canonical scalar value used by catalog
// predicates. Composite contexts are represented by stable, sorted identity
// fields; their evidence remains in the snapshot fingerprint.
@@ -56,6 +64,8 @@ func (s Snapshot) Facet(name FacetName) (FactStatus, string, bool) {
switch name {
case FacetPhase:
return s.Phase.Status, string(s.Phase.Value), true
+ case FacetProgram:
+ return s.Program.Status, string(s.Program.Value), true
case FacetTopology:
return FactKnown, string(s.Invocation.Topology), true
case FacetEngagement:
@@ -95,6 +105,13 @@ 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:
- return "", "", false
+ if fact, ok := s.FlowFacts[string(name)]; ok {
+ return fact.Status, fact.Value, true
+ }
+ fact, ok := s.ExtensionFacts[string(name)]
+ if !ok {
+ return "", "", false
+ }
+ return fact.Status, fact.Value, true
}
}
diff --git a/boatstack/internal/kernel/model/state.go b/boatstack/internal/kernel/model/state.go
index d8178c4..75810e0 100644
--- a/boatstack/internal/kernel/model/state.go
+++ b/boatstack/internal/kernel/model/state.go
@@ -368,28 +368,57 @@ func (s TerminalStatus) Valid() bool {
}
}
+type ProgramState string
+
+const (
+ ProgramUnbound ProgramState = "unbound"
+ ProgramCurrent ProgramState = "current"
+ ProgramDrift ProgramState = "drift"
+)
+
+func (s ProgramState) Valid() bool {
+ switch s {
+ case ProgramUnbound, ProgramCurrent, ProgramDrift:
+ return true
+ default:
+ return false
+ }
+}
+
// Observation is the read-only plant result before canonical validation and
// fingerprinting.
type Observation struct {
- SchemaVersion int `json:"schema_version"`
- Invocation InvocationContext `json:"invocation"`
- Phase Fact[ProtocolPhase] `json:"phase"`
- Engagement Fact[EngagementState] `json:"engagement"`
- Delivery Fact[DeliveryState] `json:"delivery"`
- Workspace Fact[WorkspaceState] `json:"workspace"`
- Plan Fact[PlanState] `json:"plan"`
- Configuration Fact[ConfigurationState] `json:"configuration"`
- ConfigurationPolicy Fact[ConfigurationPolicy] `json:"configuration_policy"`
- Runtime Fact[RuntimeState] `json:"runtime"`
- Publication Fact[PublicationState] `json:"publication"`
- Verification Fact[VerificationState] `json:"verification"`
- Recovery Fact[RecoveryState] `json:"recovery"`
- Transaction Fact[TransactionState] `json:"transaction"`
- RecoveryInfo Fact[RecoveryContext] `json:"recovery_info"`
- TransactionInfo Fact[TransactionContext] `json:"transaction_info"`
- Terminal Fact[TerminalStatus] `json:"terminal"`
- Goal Fact[Goal] `json:"goal"`
- ObservedAt time.Time `json:"observed_at"`
+ SchemaVersion int `json:"schema_version"`
+ ProgramFingerprint string `json:"program_fingerprint,omitempty"`
+ RecordedProgramFingerprint string `json:"recorded_program_fingerprint,omitempty"`
+ Invocation InvocationContext `json:"invocation"`
+ Program Fact[ProgramState] `json:"program"`
+ Phase Fact[ProtocolPhase] `json:"phase"`
+ Engagement Fact[EngagementState] `json:"engagement"`
+ Delivery Fact[DeliveryState] `json:"delivery"`
+ Workspace Fact[WorkspaceState] `json:"workspace"`
+ Plan Fact[PlanState] `json:"plan"`
+ Configuration Fact[ConfigurationState] `json:"configuration"`
+ ConfigurationPolicy Fact[ConfigurationPolicy] `json:"configuration_policy"`
+ Runtime Fact[RuntimeState] `json:"runtime"`
+ Publication Fact[PublicationState] `json:"publication"`
+ Verification Fact[VerificationState] `json:"verification"`
+ Recovery Fact[RecoveryState] `json:"recovery"`
+ Transaction Fact[TransactionState] `json:"transaction"`
+ RecoveryInfo Fact[RecoveryContext] `json:"recovery_info"`
+ 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"`
+ ExtensionFacts map[string]Fact[string] `json:"extension_facts,omitempty"`
+ ObservedAt time.Time `json:"observed_at"`
+}
+
+// ExecutableRuntimeAdmitted reports whether repository-selected executable
+// observers may run for this exact compiled program.
+func (o Observation) ExecutableRuntimeAdmitted(programFingerprint string) bool {
+ return o.RecordedProgramFingerprint == programFingerprint &&
+ o.Configuration.Status == FactKnown && o.Configuration.Value == ConfigurationVerified
}
type Snapshot struct {
@@ -397,10 +426,34 @@ type Snapshot struct {
Fingerprint string `json:"fingerprint"`
}
+func CanonicalizeForProgram(observation Observation, programFingerprint string) (Snapshot, error) {
+ if len(programFingerprint) != 64 {
+ return Snapshot{}, fmt.Errorf("snapshot: invalid compiled program fingerprint")
+ }
+ observation.ProgramFingerprint = programFingerprint
+ state := ProgramUnbound
+ if observation.RecordedProgramFingerprint != "" {
+ state = ProgramDrift
+ if observation.RecordedProgramFingerprint == programFingerprint {
+ state = ProgramCurrent
+ }
+ }
+ observation.Program = Known(state, Evidence{
+ Source: "compiled-control-program", Fingerprint: programFingerprint, ObservedAt: observation.ObservedAt,
+ })
+ return Canonicalize(observation)
+}
+
func Canonicalize(observation Observation) (Snapshot, error) {
if observation.SchemaVersion != SnapshotSchemaVersion {
return Snapshot{}, fmt.Errorf("snapshot: schema version %d, want %d", observation.SchemaVersion, SnapshotSchemaVersion)
}
+ if observation.Program.Status == "" && observation.ProgramFingerprint == "" {
+ observation.Program = Known(ProgramUnbound, Evidence{Source: "control-program:unbound", Fingerprint: "unbound", ObservedAt: observation.ObservedAt})
+ }
+ if observation.ProgramFingerprint != "" && len(observation.ProgramFingerprint) != 64 {
+ return Snapshot{}, fmt.Errorf("snapshot: invalid program fingerprint")
+ }
if err := observation.Invocation.Validate(false); err != nil {
return Snapshot{}, err
}
@@ -408,6 +461,7 @@ func Canonicalize(observation Observation) (Snapshot, error) {
name string
err error
}{
+ {"program", observation.Program.Validate("program")},
{"phase", observation.Phase.Validate("phase")},
{"engagement", observation.Engagement.Validate("engagement")},
{"delivery", observation.Delivery.Validate("delivery")},
@@ -438,6 +492,7 @@ func Canonicalize(observation Observation) (Snapshot, error) {
known bool
valid bool
}{
+ {"program", observation.Program.Status == FactKnown, observation.Program.Value.Valid()},
{"engagement", observation.Engagement.Status == FactKnown, observation.Engagement.Value.Valid()},
{"delivery", observation.Delivery.Status == FactKnown, observation.Delivery.Value.Valid()},
{"workspace", observation.Workspace.Status == FactKnown, observation.Workspace.Value.Valid()},
@@ -498,6 +553,22 @@ func Canonicalize(observation Observation) (Snapshot, error) {
return Snapshot{}, fmt.Errorf("snapshot: invalid goal fact: %w", err)
}
}
+ for id, fact := range observation.FlowFacts {
+ if !FacetName(id).Valid() || controllingFacet(FacetName(id)) {
+ return Snapshot{}, fmt.Errorf("snapshot: invalid primary-flow fact id %q", id)
+ }
+ if err := fact.Validate("primary-flow fact " + id); err != nil {
+ return Snapshot{}, err
+ }
+ }
+ for id, fact := range observation.ExtensionFacts {
+ if !FacetName(id).Valid() || controllingFacet(FacetName(id)) {
+ return Snapshot{}, fmt.Errorf("snapshot: invalid extension fact id %q", id)
+ }
+ if err := fact.Validate("extension fact " + id); err != nil {
+ return Snapshot{}, err
+ }
+ }
if observation.ConfigurationPolicy.Status == FactKnown {
if err := observation.ConfigurationPolicy.Value.Validate(); err != nil {
return Snapshot{}, fmt.Errorf("snapshot: invalid configuration policy: %w", err)
@@ -517,6 +588,7 @@ func Canonicalize(observation Observation) (Snapshot, error) {
snapshot := Snapshot{Observation: observation}
projection := observation
projection.ObservedAt = time.Time{}
+ zeroEvidenceTimes(&projection.Program)
zeroEvidenceTimes(&projection.Phase)
zeroEvidenceTimes(&projection.Engagement)
zeroEvidenceTimes(&projection.Delivery)
@@ -533,6 +605,14 @@ func Canonicalize(observation Observation) (Snapshot, error) {
zeroEvidenceTimes(&projection.TransactionInfo)
zeroEvidenceTimes(&projection.Terminal)
zeroEvidenceTimes(&projection.Goal)
+ for id, fact := range projection.FlowFacts {
+ zeroEvidenceTimes(&fact)
+ projection.FlowFacts[id] = fact
+ }
+ for id, fact := range projection.ExtensionFacts {
+ zeroEvidenceTimes(&fact)
+ projection.ExtensionFacts[id] = fact
+ }
raw, err := json.Marshal(Snapshot{Observation: projection})
if err != nil {
return Snapshot{}, fmt.Errorf("snapshot: canonical encoding: %w", err)
diff --git a/boatstack/internal/kernel/model/state_test.go b/boatstack/internal/kernel/model/state_test.go
index 8d9d4d2..9607f0b 100644
--- a/boatstack/internal/kernel/model/state_test.go
+++ b/boatstack/internal/kernel/model/state_test.go
@@ -106,6 +106,7 @@ func TestEveryControllingFacetChangesCanonicalIdentity(t *testing.T) {
evidence := testEvidence()
mutations := map[FacetName]func(*Observation){
FacetPhase: func(o *Observation) { o.Phase = Known(PhaseActive, evidence) },
+ FacetProgram: func(o *Observation) { o.Program = Known(ProgramCurrent, evidence) },
FacetTopology: func(o *Observation) { o.Invocation.Topology = TopologyDetached },
FacetEngagement: func(o *Observation) { o.Engagement = Known(EngagementCommand, evidence) },
FacetDelivery: func(o *Observation) { o.Delivery = Known(DeliveryApproved, evidence) },
diff --git a/boatstack/internal/kernel/ports/ports.go b/boatstack/internal/kernel/ports/ports.go
index e3f6fa9..1af9c58 100644
--- a/boatstack/internal/kernel/ports/ports.go
+++ b/boatstack/internal/kernel/ports/ports.go
@@ -14,8 +14,9 @@ type Observer interface {
}
type ObservationRequest struct {
- Invocation model.InvocationContext
- IgnoreAdmissionID string
+ Invocation model.InvocationContext
+ IgnoreAdmissionID string
+ VerifyTransitionID catalog.TransitionID
}
type ControllerLayout struct {
@@ -72,6 +73,8 @@ type EffectResult struct {
}
type ResourceMutation struct {
+ Resource string `json:"resource"`
+ Owner string `json:"owner"`
Path string `json:"path"`
Prior []byte `json:"prior,omitempty"`
Target []byte `json:"target,omitempty"`
diff --git a/boatstack/internal/kernel/protocol/admission.go b/boatstack/internal/kernel/protocol/admission.go
index 0750e64..028cc50 100644
--- a/boatstack/internal/kernel/protocol/admission.go
+++ b/boatstack/internal/kernel/protocol/admission.go
@@ -16,6 +16,7 @@ type Admission struct {
ID string `json:"id"`
TransitionID catalog.TransitionID `json:"transition_id"`
TransitionVersion int `json:"transition_version"`
+ ProgramFingerprint string `json:"program_fingerprint"`
SnapshotFingerprint string `json:"snapshot_fingerprint"`
SourceRevision string `json:"source_revision,omitempty"`
WorktreeFingerprint string `json:"worktree_fingerprint,omitempty"`
@@ -40,7 +41,7 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T
if err := goal.Validate(); err != nil {
return Admission{}, err
}
- if snapshot.Fingerprint == "" || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) {
+ if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) {
return Admission{}, fmt.Errorf("transition %q is not admissible from snapshot %q", transition.ID, snapshot.Fingerprint)
}
if lifetime <= 0 {
@@ -59,7 +60,7 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T
return Admission{}, err
}
sourceRevision, worktreeFingerprint := gitBinding(snapshot)
- if transitionBindsSourceRevision(transition.ID) {
+ if transition.BindsSourceRevision {
declared, _ := parameters.Get("source_revision")
if sourceRevision == "" || worktreeFingerprint == "" || declared != sourceRevision {
return Admission{}, fmt.Errorf("transition %q must bind the current Git revision and worktree fingerprint", transition.ID)
@@ -76,7 +77,7 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T
}
a := Admission{
SchemaVersion: AdmissionSchemaVersion, TransitionID: transition.ID, TransitionVersion: transition.Version,
- SnapshotFingerprint: snapshot.Fingerprint, SourceRevision: sourceRevision, WorktreeFingerprint: worktreeFingerprint,
+ ProgramFingerprint: snapshot.ProgramFingerprint, SnapshotFingerprint: snapshot.Fingerprint, SourceRevision: sourceRevision, WorktreeFingerprint: worktreeFingerprint,
SourcePhase: snapshot.Phase.Value, Invocation: snapshot.Invocation, Goal: goal, Authority: authority.canonical(),
Evidence: append([]string(nil), transition.RequiredEvidence...), Parameters: parameters.Canonical(), IssuedAt: now.UTC(), ExpiresAt: now.Add(lifetime).UTC(),
}
@@ -113,6 +114,9 @@ func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, tra
if a.SnapshotFingerprint != snapshot.Fingerprint {
return fmt.Errorf("admission %q is stale: snapshot changed", a.ID)
}
+ if a.ProgramFingerprint != snapshot.ProgramFingerprint {
+ return fmt.Errorf("admission %q is bound to a different control program", a.ID)
+ }
if snapshot.Phase.Status != model.FactKnown || a.SourcePhase != snapshot.Phase.Value {
return fmt.Errorf("admission %q is bound to a different source phase", a.ID)
}
@@ -141,7 +145,7 @@ func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, tra
if a.SourceRevision != sourceRevision || a.WorktreeFingerprint != worktreeFingerprint {
return fmt.Errorf("admission %q Git binding changed", a.ID)
}
- if transitionBindsSourceRevision(transition.ID) {
+ if transition.BindsSourceRevision {
declared, _ := a.Parameters.Get("source_revision")
if declared != sourceRevision || sourceRevision == "" || worktreeFingerprint == "" {
return fmt.Errorf("admission %q is not bound to the current Git revision", a.ID)
@@ -167,11 +171,6 @@ func gitBinding(snapshot model.Snapshot) (string, string) {
return "", ""
}
-func transitionBindsSourceRevision(id catalog.TransitionID) bool {
- value := string(id)
- return strings.HasPrefix(value, "gate.") || id == "evidence.visual.attach" || id == "delivery.slice.advance"
-}
-
func validateRecoveryPermission(snapshot model.Snapshot, transition catalog.Transition) error {
if transition.Class != catalog.EventRecovery {
return nil
@@ -189,13 +188,8 @@ func validateRecoveryPermission(snapshot model.Snapshot, transition catalog.Tran
func validateProviderAuthorityBinding(authority AuthorityBundle, transition catalog.Transition, parameters Parameters) error {
var expected string
- switch transition.ID {
- case "publication.execute":
- expected, _ = parameters.Get("preview_fingerprint")
- case "publication.correct":
- expected, _ = parameters.Get("body_sha256")
- case "publication.reconcile":
- expected, _ = parameters.Get("publication_id")
+ if transition.AuthorityFingerprintParameter != "" {
+ expected, _ = parameters.Get(transition.AuthorityFingerprintParameter)
}
for _, receipt := range authority.Receipts {
if receipt.Class != catalog.AuthorityProvider {
@@ -224,8 +218,7 @@ func validatePolicyAuthority(snapshot model.Snapshot, transition catalog.Transit
return fmt.Errorf("transition %q is unavailable to disabled host %q", transition.ID, snapshot.Invocation.Host)
}
}
- requiresPolicy := transition.ID == "plan.approve" || transition.ID == "plan.approve-amendment" ||
- transition.ID == "gate.review.record" || transition.ID == "evidence.visual.attach"
+ requiresPolicy := transition.Policy.RequiredWhen != "" || transition.Policy.AuthorityRule != "" || transition.Policy.AvailabilityRule != ""
if !requiresPolicy {
return nil
}
@@ -233,13 +226,13 @@ func validatePolicyAuthority(snapshot model.Snapshot, transition catalog.Transit
return fmt.Errorf("transition %q requires known configuration policy", transition.ID)
}
policy := snapshot.ConfigurationPolicy.Value
- if transition.ID == "evidence.visual.attach" && policy.VisualEvidence == "off" {
+ if transition.Policy.AvailabilityRule == "visual-evidence-enabled" && policy.VisualEvidence == "off" {
return fmt.Errorf("transition %q is disabled by repository policy", transition.ID)
}
- if (transition.ID == "plan.approve" || transition.ID == "plan.approve-amendment") && policy.PlanApproval == "human" && !authority[catalog.AuthorityHuman] {
+ if transition.Policy.AuthorityRule == "plan-approval" && policy.PlanApproval == "human" && !authority[catalog.AuthorityHuman] {
return fmt.Errorf("transition %q requires human approval under repository policy", transition.ID)
}
- if transition.ID == "gate.review.record" && policy.IndependentReviewForHighRisk && policy.HighRiskChange && !authority[catalog.AuthorityHuman] {
+ if transition.Policy.AuthorityRule == "independent-high-risk-review" && policy.IndependentReviewForHighRisk && policy.HighRiskChange && !authority[catalog.AuthorityHuman] {
return fmt.Errorf("transition %q requires independent human review for a high-risk change", transition.ID)
}
return nil
@@ -265,7 +258,7 @@ func validateAuthorityEvidence(snapshot model.Snapshot, authority AuthorityBundl
}
func (a Admission) ValidateIdentity() error {
- if a.SchemaVersion != AdmissionSchemaVersion || a.ID == "" || a.TransitionID == "" || a.TransitionVersion < 1 || a.SnapshotFingerprint == "" || !a.SourcePhase.Valid() || a.IdempotencyKey == "" || a.IssuedAt.IsZero() || a.ExpiresAt.Before(a.IssuedAt) {
+ if a.SchemaVersion != AdmissionSchemaVersion || a.ID == "" || a.TransitionID == "" || a.TransitionVersion < 1 || len(a.ProgramFingerprint) != 64 || a.SnapshotFingerprint == "" || !a.SourcePhase.Valid() || a.IdempotencyKey == "" || a.IssuedAt.IsZero() || a.ExpiresAt.Before(a.IssuedAt) {
return fmt.Errorf("admission: invalid schema, identity, source, or lifetime")
}
if err := a.Invocation.Validate(true); err != nil {
diff --git a/boatstack/internal/kernel/protocol/config.go b/boatstack/internal/kernel/protocol/config.go
index 183283e..259fee6 100644
--- a/boatstack/internal/kernel/protocol/config.go
+++ b/boatstack/internal/kernel/protocol/config.go
@@ -7,6 +7,8 @@ import (
"encoding/json"
"fmt"
"io"
+ "path/filepath"
+ "regexp"
"sort"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
@@ -29,14 +31,30 @@ type PolicySettings struct {
ExternalEffectAuthority string `json:"external_effect_authority,omitempty"`
}
+// SubprocessExtensionSettings is a repository-selected, checksum-bound
+// additive capability. It cannot select or replace the primary flow.
+type SubprocessExtensionSettings struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Executable string `json:"executable"`
+ SHA256 string `json:"sha256"`
+ Manifest json.RawMessage `json:"manifest"`
+ Settings json.RawMessage `json:"settings,omitempty"`
+ DeadlineMillis int `json:"deadline_millis,omitempty"`
+ StdoutBytes int64 `json:"stdout_bytes,omitempty"`
+ StderrBytes int64 `json:"stderr_bytes,omitempty"`
+}
+
type ProjectConfig struct {
- SchemaVersion int `json:"schema_version"`
- Project ProjectSettings `json:"project"`
- Policy PolicySettings `json:"policy"`
- Hosts []string `json:"hosts"`
+ SchemaVersion int `json:"schema_version"`
+ Project ProjectSettings `json:"project"`
+ Policy PolicySettings `json:"policy"`
+ Hosts []string `json:"hosts"`
+ Extensions []SubprocessExtensionSettings `json:"extensions,omitempty"`
}
-var canonicalHosts = []string{"claude", "cli", "codex", "cursor", "gemini", "mcp"}
+var canonicalHosts = []string{"claude", "cli", "codex", "cursor", "gemini", "mcp", "sdk"}
+var extensionID = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$`)
func CanonicalHosts() []string { return append([]string(nil), canonicalHosts...) }
@@ -80,6 +98,28 @@ func ProjectConfigFingerprint(value []byte) (ProjectConfig, string, error) {
canonical := config
canonical.Hosts = append([]string(nil), config.Hosts...)
sort.Strings(canonical.Hosts)
+ canonical.Extensions = append([]SubprocessExtensionSettings(nil), config.Extensions...)
+ for index := range canonical.Extensions {
+ values := []struct {
+ name string
+ value *json.RawMessage
+ }{{"manifest", &canonical.Extensions[index].Manifest}, {"settings", &canonical.Extensions[index].Settings}}
+ for _, item := range values {
+ name, value := item.name, item.value
+ if len(*value) == 0 {
+ continue
+ }
+ var decoded any
+ if err := json.Unmarshal(*value, &decoded); err != nil {
+ return ProjectConfig{}, "", fmt.Errorf("canonicalize extension %q %s: %w", canonical.Extensions[index].ID, name, err)
+ }
+ *value, err = json.Marshal(decoded)
+ if err != nil {
+ return ProjectConfig{}, "", fmt.Errorf("canonicalize extension %q %s: %w", canonical.Extensions[index].ID, name, err)
+ }
+ }
+ }
+ sort.Slice(canonical.Extensions, func(i, j int) bool { return canonical.Extensions[i].ID < canonical.Extensions[j].ID })
if canonical.Policy.ExternalEffectAuthority == "" {
canonical.Policy.ExternalEffectAuthority = "human-or-autonomy-plus-provider"
}
@@ -125,5 +165,46 @@ func (c ProjectConfig) Validate() error {
if !seen["cli"] {
return fmt.Errorf("V2 project configuration must enable the canonical CLI surface")
}
+ seenExtensions := map[string]bool{}
+ for _, extension := range c.Extensions {
+ if !extensionID.MatchString(extension.ID) || extension.Version == "" || !filepath.IsAbs(extension.Executable) || filepath.Clean(extension.Executable) != extension.Executable || len(extension.SHA256) != 64 || len(extension.Manifest) == 0 {
+ return fmt.Errorf("subprocess extension requires semantic id, version, exact absolute executable, SHA-256, and declarative manifest")
+ }
+ if _, err := hex.DecodeString(extension.SHA256); err != nil {
+ return fmt.Errorf("subprocess extension %q has invalid SHA-256", extension.ID)
+ }
+ if seenExtensions[extension.ID] {
+ return fmt.Errorf("duplicated subprocess extension %q", extension.ID)
+ }
+ seenExtensions[extension.ID] = true
+ if extension.DeadlineMillis < 0 || extension.StdoutBytes < 0 || extension.StderrBytes < 0 {
+ return fmt.Errorf("subprocess extension %q has negative limits", extension.ID)
+ }
+ values := []struct {
+ name string
+ value json.RawMessage
+ }{{"manifest", extension.Manifest}, {"settings", extension.Settings}}
+ for _, item := range values {
+ name, value := item.name, item.value
+ if len(value) == 0 {
+ continue
+ }
+ var settings any
+ decoder := json.NewDecoder(bytes.NewReader(value))
+ decoder.UseNumber()
+ if err := decoder.Decode(&settings); err != nil {
+ return fmt.Errorf("subprocess extension %q %s is invalid JSON", extension.ID, name)
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return fmt.Errorf("subprocess extension %q %s contains trailing JSON", extension.ID, name)
+ }
+ if name == "manifest" {
+ if _, ok := settings.(map[string]any); !ok {
+ return fmt.Errorf("subprocess extension %q manifest must be a JSON object", extension.ID)
+ }
+ }
+ }
+ }
return nil
}
diff --git a/boatstack/internal/kernel/protocol/config_test.go b/boatstack/internal/kernel/protocol/config_test.go
index 8d1549d..e555307 100644
--- a/boatstack/internal/kernel/protocol/config_test.go
+++ b/boatstack/internal/kernel/protocol/config_test.go
@@ -1,6 +1,11 @@
package protocol
-import "testing"
+import (
+ "encoding/json"
+ "path/filepath"
+ "strings"
+ "testing"
+)
func TestProjectConfigurationIsStrictAndVersioned(t *testing.T) {
valid := []byte(`{"schema_version":2,"project":{"name":"product","default_branch":"main","commands":{}},"policy":{"plan_approval":"human","visual_evidence":"optional"},"hosts":["cli","codex"]}`)
@@ -20,6 +25,62 @@ func TestProjectConfigurationIsStrictAndVersioned(t *testing.T) {
}
}
+func TestRepositorySubprocessExtensionsAreStrictAndSemanticallyFingerprinted(t *testing.T) {
+ // control-law: repository-extension-selection-binds-exact-executable-and-settings
+ executable := filepath.Join(t.TempDir(), "extension")
+ base := ProjectConfig{
+ SchemaVersion: ConfigSchemaVersion,
+ Project: ProjectSettings{Name: "product", DefaultBranch: "main", Commands: map[string]string{}},
+ Policy: PolicySettings{PlanApproval: "human", VisualEvidence: "optional"},
+ Hosts: []string{"cli", "sdk"},
+ Extensions: []SubprocessExtensionSettings{{
+ ID: "example.guard", Version: "1.0.0", Executable: executable, SHA256: strings.Repeat("a", 64),
+ Manifest: json.RawMessage(`{"id":"example.guard","version":"1.0.0","protocol_version":1,"settings_schema":{"type":"object"},"privacy_classification":"metadata-only","telemetry_classification":"transition-receipt"}`),
+ Settings: json.RawMessage(`{"level":"strict","enabled":true}`), DeadlineMillis: 1000, StdoutBytes: 2048, StderrBytes: 1024,
+ }},
+ }
+ raw, err := json.Marshal(base)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, fingerprint, err := ProjectConfigFingerprint(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ reordered := base
+ reordered.Hosts = []string{"sdk", "cli"}
+ reordered.Extensions = append([]SubprocessExtensionSettings(nil), base.Extensions...)
+ reordered.Extensions[0].Settings = json.RawMessage(`{ "enabled" : true, "level" : "strict" }`)
+ raw, _ = json.Marshal(reordered)
+ _, reorderedFingerprint, err := ProjectConfigFingerprint(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if reorderedFingerprint != fingerprint {
+ t.Fatalf("representation changed extension policy fingerprint: %s != %s", reorderedFingerprint, fingerprint)
+ }
+
+ changed := base
+ changed.Extensions = append([]SubprocessExtensionSettings(nil), base.Extensions...)
+ changed.Extensions[0].SHA256 = strings.Repeat("b", 64)
+ raw, _ = json.Marshal(changed)
+ _, changedFingerprint, err := ProjectConfigFingerprint(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if changedFingerprint == fingerprint {
+ t.Fatal("executable identity change retained repository policy fingerprint")
+ }
+
+ invalid := base
+ invalid.Extensions = append([]SubprocessExtensionSettings(nil), base.Extensions...)
+ invalid.Extensions[0].Executable = "relative/extension"
+ raw, _ = json.Marshal(invalid)
+ if _, err := DecodeProjectConfig(raw); err == nil {
+ t.Fatal("relative subprocess executable was accepted")
+ }
+}
+
func TestProjectConfigurationFingerprintIsSemanticAndStrict(t *testing.T) {
one := []byte("{\n \"schema_version\": 2,\n \"project\": {\"name\": \"product\", \"default_branch\": \"main\", \"commands\": {\"test\": \"go test ./...\"}},\n \"policy\": {\"plan_approval\": \"human\", \"visual_evidence\": \"optional\"},\n \"hosts\": [\"codex\", \"cli\"]\n}\n")
two := []byte("{\r\n\"hosts\":[\"cli\",\"codex\"],\r\n\"policy\":{\"external_effect_authority\":\"human-or-autonomy-plus-provider\",\"visual_evidence\":\"optional\",\"plan_approval\":\"human\"},\r\n\"project\":{\"commands\":{\"test\":\"go test ./...\"},\"default_branch\":\"main\",\"name\":\"product\"},\r\n\"schema_version\":2\r\n}\r\n")
diff --git a/boatstack/internal/kernel/protocol/parameters_test.go b/boatstack/internal/kernel/protocol/parameters_test.go
index e6ccede..9878598 100644
--- a/boatstack/internal/kernel/protocol/parameters_test.go
+++ b/boatstack/internal/kernel/protocol/parameters_test.go
@@ -4,15 +4,15 @@ import (
"path/filepath"
"testing"
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
func TestEffectPathsAndGitNamesAreValidatedAtAdmissionBoundary(t *testing.T) {
- plan, _ := catalog.Default().Lookup("plan.create")
+ plan, _ := testprogram.StandardRegistry().Lookup("plan.create")
if err := (Parameters{{Name: "source_path", Value: "relative.md"}, {Name: "delivery_id", Value: "delivery"}}).Validate(plan); err == nil {
t.Fatal("relative source path was accepted")
}
- workspace, _ := catalog.Default().Lookup("workspace.cut")
+ workspace, _ := testprogram.StandardRegistry().Lookup("workspace.cut")
if err := (Parameters{{Name: "branch", Value: "--force"}, {Name: "base_ref", Value: "HEAD~1"}, {Name: "destination", Value: filepath.Join(t.TempDir(), "worktree")}}).Validate(workspace); err == nil {
t.Fatal("unsafe Git parameters were accepted")
}
diff --git a/boatstack/internal/kernel/protocol/policy_test.go b/boatstack/internal/kernel/protocol/policy_test.go
index ebf6fc6..7abaa0b 100644
--- a/boatstack/internal/kernel/protocol/policy_test.go
+++ b/boatstack/internal/kernel/protocol/policy_test.go
@@ -6,6 +6,7 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
func policySnapshot(policy model.ConfigurationPolicy) model.Snapshot {
@@ -21,7 +22,7 @@ func TestProviderAuthorityBindsExactExternalRequest(t *testing.T) {
ID: "provider", Class: catalog.AuthorityProvider, Subject: "github",
Fingerprint: "different-preview", IssuedAt: now, ExpiresAt: now.Add(time.Minute),
}}}
- transition, _ := catalog.Default().Lookup("publication.execute")
+ transition, _ := testprogram.StandardRegistry().Lookup("publication.execute")
parameters := Parameters{{Name: "preview_fingerprint", Value: "reviewed-preview"}}
if err := validateProviderAuthorityBinding(authority, transition, parameters); err == nil {
t.Fatal("provider authority for different preview was accepted")
@@ -37,7 +38,7 @@ func TestAdmissionPolicyRejectsRepositoryOnlyHighRiskReview(t *testing.T) {
PlanApproval: "human", IndependentReviewForHighRisk: true, HighRiskChange: true,
VisualEvidence: "optional", ExternalEffectAuthority: "human-or-autonomy-plus-provider", Hosts: []string{"cli"},
})
- transition := catalog.Transition{ID: "gate.review.record"}
+ transition, _ := testprogram.StandardRegistry().Lookup("gate.review.record")
if err := validatePolicyAuthority(snapshot, transition, catalog.AuthoritySet{catalog.AuthorityRepository: true}); err == nil {
t.Fatal("repository-only authority admitted a high-risk review")
}
@@ -51,7 +52,7 @@ func TestAdmissionPolicyRejectsDisabledVisualEvidence(t *testing.T) {
PlanApproval: "human", VisualEvidence: "off",
ExternalEffectAuthority: "human-or-autonomy-plus-provider", Hosts: []string{"cli"},
})
- transition := catalog.Transition{ID: "evidence.visual.attach"}
+ transition, _ := testprogram.StandardRegistry().Lookup("evidence.visual.attach")
if err := validatePolicyAuthority(snapshot, transition, catalog.AuthoritySet{catalog.AuthorityHuman: true}); err == nil {
t.Fatal("visual evidence attachment admitted while disabled")
}
diff --git a/boatstack/internal/kernel/protocol/receipt.go b/boatstack/internal/kernel/protocol/receipt.go
index 056e0aa..6f41b57 100644
--- a/boatstack/internal/kernel/protocol/receipt.go
+++ b/boatstack/internal/kernel/protocol/receipt.go
@@ -26,6 +26,7 @@ type TransitionReceipt struct {
Sequence uint64 `json:"sequence"`
TransitionID catalog.TransitionID `json:"transition_id"`
TransitionVersion int `json:"transition_version"`
+ ProgramFingerprint string `json:"program_fingerprint"`
AdmissionID string `json:"admission_id"`
GoalID string `json:"goal_id"`
GoalKind model.GoalKind `json:"goal_kind"`
@@ -61,7 +62,7 @@ func NewReceipt(flowID string, sequence uint64, admission Admission, transition
}
receipt := TransitionReceipt{
SchemaVersion: ReceiptSchemaVersion, FlowID: flowID, Sequence: sequence, TransitionID: transition.ID,
- TransitionVersion: transition.Version, AdmissionID: admission.ID, GoalID: admission.Goal.ID, GoalKind: admission.Goal.Kind, DeliveryID: admission.Goal.DeliveryID,
+ TransitionVersion: transition.Version, ProgramFingerprint: admission.ProgramFingerprint, AdmissionID: admission.ID, GoalID: admission.Goal.ID, GoalKind: admission.Goal.Kind, DeliveryID: admission.Goal.DeliveryID,
SourceFingerprint: admission.SnapshotFingerprint, TargetFingerprint: target.Fingerprint,
AuthorityClasses: classes, IdempotencyKey: admission.IdempotencyKey, Verifier: transition.Verifier,
Outcome: outcome, Recovery: transition.Interruption.Recovery, Terminal: terminal,
@@ -79,7 +80,7 @@ func NewReceipt(flowID string, sequence uint64, admission Admission, transition
}
func (r TransitionReceipt) Validate() error {
- if r.SchemaVersion != ReceiptSchemaVersion || r.ID == "" || r.FlowID == "" || r.Sequence == 0 || r.TransitionID == "" || r.TransitionVersion < 1 || r.AdmissionID == "" || r.GoalID == "" || !r.GoalKind.Valid() || r.DeliveryID == "" || r.SourceFingerprint == "" || r.TargetFingerprint == "" || r.IdempotencyKey == "" || r.Verifier == "" {
+ if r.SchemaVersion != ReceiptSchemaVersion || r.ID == "" || r.FlowID == "" || r.Sequence == 0 || r.TransitionID == "" || r.TransitionVersion < 1 || len(r.ProgramFingerprint) != 64 || r.AdmissionID == "" || r.GoalID == "" || !r.GoalKind.Valid() || r.DeliveryID == "" || r.SourceFingerprint == "" || r.TargetFingerprint == "" || r.IdempotencyKey == "" || r.Verifier == "" {
return fmt.Errorf("receipt has incomplete identity or evidence")
}
if r.StartedAt.IsZero() || r.CompletedAt.Before(r.StartedAt) || r.DurationNanoseconds != r.CompletedAt.Sub(r.StartedAt).Nanoseconds() {
diff --git a/boatstack/internal/kernel/supervisor/classify.go b/boatstack/internal/kernel/supervisor/classify.go
index caafc59..16f4fe9 100644
--- a/boatstack/internal/kernel/supervisor/classify.go
+++ b/boatstack/internal/kernel/supervisor/classify.go
@@ -5,8 +5,6 @@ import (
"encoding/hex"
"regexp"
"strings"
-
- "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
)
type commandPattern struct {
@@ -43,16 +41,15 @@ var (
)
var managedCommandPatterns = []struct {
- pattern *regexp.Regexp
- operation string
- transition catalog.TransitionID
+ pattern *regexp.Regexp
+ operation string
}{
- {regexp.MustCompile(`(?i)\bgit\s+worktree\s+remove\b`), "workspace.remove", "workspace.cleanup"},
- {regexp.MustCompile(`(?i)\bgh\s+pr\s+create\b`), "publication.create", "publication.execute"},
- {regexp.MustCompile(`(?i)\bgh\s+pr\s+edit\b`), "publication.edit", "publication.correct"},
- {regexp.MustCompile(`(?i)\bgh\s+pr\s+ready\b`), "publication.ready", "publication.correct"},
- {regexp.MustCompile(`(?i)\bgh\s+api\b[^\n;&|]*(?:/pulls\b|/pull-requests\b)[^\n;&|]*(?:\s-X\s*(?:POST|PATCH)|--request\s+(?:POST|PATCH))`), "publication.api-write", "publication.correct"},
- {regexp.MustCompile(`(?i)\bgit\s+push\b`), "publication.push", "publication.execute"},
+ {regexp.MustCompile(`(?i)\bgit\s+worktree\s+remove\b`), "workspace.remove"},
+ {regexp.MustCompile(`(?i)\bgh\s+pr\s+create\b`), "publication.create"},
+ {regexp.MustCompile(`(?i)\bgh\s+pr\s+edit\b`), "publication.edit"},
+ {regexp.MustCompile(`(?i)\bgh\s+pr\s+ready\b`), "publication.ready"},
+ {regexp.MustCompile(`(?i)\bgh\s+api\b[^\n;&|]*(?:/pulls\b|/pull-requests\b)[^\n;&|]*(?:\s-X\s*(?:POST|PATCH)|--request\s+(?:POST|PATCH))`), "publication.api-write"},
+ {regexp.MustCompile(`(?i)\bgit\s+push\b`), "publication.push"},
}
func ClassifyCommandIntent(command string) CommandIntent {
@@ -78,7 +75,7 @@ func ClassifyCommandIntent(command string) CommandIntent {
}
for _, candidate := range managedCommandPatterns {
if candidate.pattern.MatchString(normalized) {
- intent.Class, intent.Operation, intent.Transition = IntentManagedBypass, candidate.operation, candidate.transition
+ intent.Class, intent.Operation = IntentManagedBypass, candidate.operation
return intent
}
}
diff --git a/boatstack/internal/kernel/supervisor/guard.go b/boatstack/internal/kernel/supervisor/guard.go
index 6ba1002..6f99ac2 100644
--- a/boatstack/internal/kernel/supervisor/guard.go
+++ b/boatstack/internal/kernel/supervisor/guard.go
@@ -32,8 +32,8 @@ func (i CommandIntent) Validate() error {
return fmt.Errorf("%s intent cannot name a managed transition", i.Class)
}
case IntentManagedBypass:
- if i.Transition == "" {
- return fmt.Errorf("managed-bypass intent requires a transition")
+ if i.Transition != "" {
+ return fmt.Errorf("managed-bypass transition must be resolved from the compiled control program")
}
default:
return fmt.Errorf("invalid command intent class %q", i.Class)
@@ -66,7 +66,15 @@ func (s Supervisor) Guard(snapshot model.Snapshot, intent CommandIntent) GuardDe
decision.Reason = "ordinary repository operation is outside managed effect authority"
return decision
}
- decision.RequiredTransition = intent.Transition
+ transition, managed := s.registry.ManagedTransition(intent.Operation)
+ if !managed {
+ decision.Intent.Class = IntentOrdinary
+ decision.Allowed = true
+ decision.Reason = "operation is not managed by the compiled control program"
+ return decision
+ }
+ decision.Intent.Transition = transition.ID
+ decision.RequiredTransition = transition.ID
if snapshot.Engagement.Status != model.FactKnown {
decision.Reason = "managed-effect engagement is unresolved"
return decision
diff --git a/boatstack/internal/kernel/supervisor/guard_test.go b/boatstack/internal/kernel/supervisor/guard_test.go
new file mode 100644
index 0000000..d17e256
--- /dev/null
+++ b/boatstack/internal/kernel/supervisor/guard_test.go
@@ -0,0 +1,60 @@
+package supervisor
+
+import (
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+)
+
+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"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ active := model.Snapshot{
+ Fingerprint: "snapshot",
+ Observation: model.Observation{
+ Phase: model.Fact[model.ProtocolPhase]{Status: model.FactKnown, Value: model.PhaseActive},
+ Engagement: model.Fact[model.EngagementState]{Status: model.FactKnown, Value: model.EngagementActive},
+ },
+ }
+ supervisor := New(registry, catalog.GoalContracts{})
+ managed := supervisor.Guard(active, CommandIntent{Class: IntentManagedBypass, Operation: "artifact.publish", Fingerprint: "command"})
+ if managed.Allowed || managed.RequiredTransition != "synthetic.publish" || managed.Intent.Transition != "synthetic.publish" {
+ t.Fatalf("compiled managed operation was not routed through admission: %#v", managed)
+ }
+ unowned := supervisor.Guard(active, CommandIntent{Class: IntentManagedBypass, Operation: "publication.create", Fingerprint: "command"})
+ if !unowned.Allowed || unowned.RequiredTransition != "" || unowned.Intent.Class != IntentOrdinary {
+ t.Fatalf("custom program inherited StandardFlow command semantics: %#v", unowned)
+ }
+}
+
+func syntheticManagedTransition(id catalog.TransitionID, class catalog.EventClass, selection catalog.SelectionClass, recovery catalog.TransitionID) catalog.Transition {
+ policy := catalog.PolicyContract{}
+ if id == "synthetic.publish" {
+ policy.ManagedOperations = []string{"artifact.publish"}
+ }
+ return catalog.Transition{
+ ID: id, Version: 1,
+ Origin: catalog.TransitionOrigin{Kind: catalog.OriginPrimaryFlow, 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"},
+ Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot"},
+ OwnedResources: []string{"test.synthetic.state"}, Effect: catalog.EffectID(id), LocalEffects: []catalog.EffectID{catalog.EffectID(id)}, Idempotent: true,
+ Prescription: catalog.Prescription{Operation: string(id), ExpectedPostcondition: "synthetic-target"},
+ SourcePredicate: "synthetic-source", SourceConditions: []catalog.FacetCondition{{Facet: model.FacetProgram, Statuses: []model.FactStatus{model.FactKnown}}},
+ AdmissionPredicate: "exact-admission", TargetPredicate: "synthetic-target",
+ TargetConditions: []catalog.FacetCondition{{Facet: model.FacetProgram, Statuses: []model.FactStatus{model.FactKnown}}}, Verifier: "synthetic-verifier",
+ Interruption: catalog.InterruptionContract{
+ Points: []string{"after-effect"}, PartialState: []string{"state"}, Detection: "fresh-observation", ResumeContract: "resume",
+ RollbackContract: "rollback", CompensationContract: "none", Recovery: recovery, RecoveryAuthority: "repository-policy", ResumptionPredicate: "fresh-state",
+ },
+ Reversibility: catalog.Reversible, TerminalEffect: "none", PrivacyClassification: "metadata-only", TelemetryClassification: "receipt", CostClass: "synthetic",
+ Policy: policy, Priority: 1,
+ }
+}
diff --git a/boatstack/internal/kernel/supervisor/supervisor.go b/boatstack/internal/kernel/supervisor/supervisor.go
index f78bcaf..f73f87e 100644
--- a/boatstack/internal/kernel/supervisor/supervisor.go
+++ b/boatstack/internal/kernel/supervisor/supervisor.go
@@ -27,9 +27,14 @@ type Decision struct {
Reason string `json:"reason"`
}
-type Supervisor struct{ registry catalog.Registry }
+type Supervisor struct {
+ registry catalog.Registry
+ contracts catalog.GoalContracts
+}
-func New(registry catalog.Registry) Supervisor { return Supervisor{registry: registry} }
+func New(registry catalog.Registry, contracts catalog.GoalContracts) Supervisor {
+ return Supervisor{registry: registry, contracts: contracts}
+}
func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority catalog.AuthoritySet, requested catalog.TransitionID) Decision {
base := Decision{SnapshotFingerprint: snapshot.Fingerprint}
@@ -41,6 +46,17 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority
base.Kind, base.Reason = DecisionUnresolved, "terminal or phase evidence is not known"
return base
}
+ if snapshot.Program.Status != model.FactKnown {
+ base.Kind, base.Reason = DecisionUnresolved, "compiled control program evidence is not known"
+ return base
+ }
+ if snapshot.Program.Value == model.ProgramDrift {
+ transition, ok := s.registry.Lookup(requested)
+ if !ok || !transition.Policy.ReconcilesProgram {
+ base.Kind, base.Reason = DecisionUnresolved, "compiled control program drift requires explicit reconciliation"
+ return base
+ }
+ }
if requested != "" && snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal && requested != "goal.configure" {
base.Kind, base.Reason = DecisionRefused, "requested goal differs from configured goal; goal.configure is required"
return base
@@ -49,7 +65,7 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority
base.Kind, base.Reason = DecisionRefused, fmt.Sprintf("host %q is not enabled by repository policy", snapshot.Invocation.Host)
return base
}
- if requested == "" && snapshot.Terminal.Value == model.TerminalEstablished && terminalMatchesGoal(snapshot, goal) {
+ if requested == "" && s.contracts.Matches(snapshot, goal) {
base.Kind, base.Reason = DecisionTerminal, "configured terminal is established by current evidence"
return base
}
@@ -109,10 +125,11 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority
base.Kind, base.Reason = DecisionUnresolved, "no goal-progressing transition is safely selectable from current evidence"
return base
}
+ topClass := selectable[0].SelectionClass
topPriority := selectable[0].Priority
var top []catalog.Transition
for _, candidate := range selectable {
- if candidate.Priority == topPriority {
+ if candidate.SelectionClass == topClass && candidate.Priority == topPriority {
top = append(top, candidate)
}
}
@@ -133,17 +150,15 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority
}
func targetAlreadySatisfied(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition) bool {
- if transition.ID == "goal.configure" {
+ if transition.Policy.BindsRequestedGoal {
return snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value == goal
}
- if gate, ok := catalog.GateName(transition.ID); ok {
- return currentEvidenceRecorded(snapshot, "gate-evidence:"+gate+":")
+ if transition.Policy.RequiredWhen == "visual-evidence-required" &&
+ (snapshot.ConfigurationPolicy.Status != model.FactKnown || snapshot.ConfigurationPolicy.Value.VisualEvidence != "required") {
+ return true
}
- if transition.ID == "evidence.visual.attach" {
- if snapshot.ConfigurationPolicy.Status != model.FactKnown || snapshot.ConfigurationPolicy.Value.VisualEvidence != "required" {
- return true
- }
- return currentEvidenceRecorded(snapshot, "visual-evidence:")
+ if transition.Policy.CurrentEvidencePrefix != "" {
+ return currentEvidenceRecorded(snapshot, transition.Policy.CurrentEvidencePrefix)
}
return transition.TargetMatches(snapshot)
}
@@ -173,7 +188,7 @@ func authoritySatisfies(snapshot model.Snapshot, transition catalog.Transition,
if !authority.Satisfies(transition.Authority, transition.AuthorityAll) {
return false
}
- if transition.ID == "plan.approve" || transition.ID == "plan.approve-amendment" {
+ if transition.Policy.AuthorityRule == "plan-approval" {
if snapshot.ConfigurationPolicy.Status != model.FactKnown {
return false
}
@@ -181,7 +196,7 @@ func authoritySatisfies(snapshot model.Snapshot, transition catalog.Transition,
return false
}
}
- if transition.ID == "gate.review.record" {
+ if transition.Policy.AuthorityRule == "independent-high-risk-review" {
if snapshot.ConfigurationPolicy.Status != model.FactKnown {
return false
}
@@ -206,44 +221,14 @@ func policyAllows(snapshot model.Snapshot, transition catalog.Transition) (bool,
return false, fmt.Sprintf("recovery transition %q is not permitted for transaction %q", transition.ID, snapshot.RecoveryInfo.Value.TransactionID)
}
}
- if transition.ID != "evidence.visual.attach" {
+ if transition.Policy.AvailabilityRule == "" {
return true, ""
}
if snapshot.ConfigurationPolicy.Status != model.FactKnown {
return false, fmt.Sprintf("transition %q requires known configuration policy", transition.ID)
}
- if snapshot.ConfigurationPolicy.Value.VisualEvidence == "off" {
+ if transition.Policy.AvailabilityRule == "visual-evidence-enabled" && snapshot.ConfigurationPolicy.Value.VisualEvidence == "off" {
return false, "visual evidence is disabled by repository policy"
}
return true, ""
}
-
-func terminalMatchesGoal(snapshot model.Snapshot, goal model.Goal) bool {
- if snapshot.Goal.Status != model.FactKnown || snapshot.Goal.Value != goal {
- return false
- }
- switch goal.Kind {
- case model.GoalApprovedPlan:
- return snapshot.Plan.Status == model.FactKnown && snapshot.Plan.Value == model.PlanApproved
- case model.GoalVerified:
- return deliveryInputsCurrent(snapshot) &&
- snapshot.Delivery.Status == model.FactKnown && snapshot.Delivery.Value == model.DeliveryTerminal
- case model.GoalOpenPR:
- return deliveryInputsCurrent(snapshot) && snapshot.Publication.Status == model.FactKnown && snapshot.Publication.Value == model.PublicationOpen
- case model.GoalMerged:
- return snapshot.Publication.Status == model.FactKnown && snapshot.Publication.Value == model.PublicationMerged &&
- snapshot.Delivery.Status == model.FactKnown && snapshot.Delivery.Value == model.DeliveryTerminal &&
- snapshot.Workspace.Status == model.FactKnown && (snapshot.Workspace.Value == model.WorkspaceLanded || snapshot.Workspace.Value == model.WorkspaceAbsent)
- case model.GoalAbandoned:
- return snapshot.Delivery.Status == model.FactKnown && snapshot.Delivery.Value == model.DeliveryDiscarded &&
- snapshot.Workspace.Status == model.FactKnown && (snapshot.Workspace.Value == model.WorkspaceAbandoned || snapshot.Workspace.Value == model.WorkspaceAbsent)
- default:
- return false
- }
-}
-
-func deliveryInputsCurrent(snapshot model.Snapshot) bool {
- return snapshot.Verification.Status == model.FactKnown && snapshot.Verification.Value == model.VerificationCurrent &&
- snapshot.Configuration.Status == model.FactKnown && snapshot.Configuration.Value == model.ConfigurationVerified &&
- snapshot.Runtime.Status == model.FactKnown && snapshot.Runtime.Value == model.RuntimeVerified
-}
diff --git a/boatstack/internal/plant/observer.go b/boatstack/internal/plant/observer.go
index b1bba64..e930842 100644
--- a/boatstack/internal/plant/observer.go
+++ b/boatstack/internal/plant/observer.go
@@ -161,6 +161,13 @@ func (o Observer) Observe(ctx context.Context, request ports.ObservationRequest)
if pendingErr != nil {
return model.Observation{}, pendingErr
}
+ recordedProgramFingerprint := state.ProgramFingerprint
+ if pending.ProgramFingerprint != "" {
+ if recordedProgramFingerprint != "" && recordedProgramFingerprint != pending.ProgramFingerprint {
+ return model.Observation{}, fmt.Errorf("durable state and pending transaction bind different control programs")
+ }
+ recordedProgramFingerprint = pending.ProgramFingerprint
+ }
if pending.Conflicting {
evidence := append(append([]model.Evidence(nil), stateEvidence...), pending.Evidence...)
phase = model.PhaseUnresolved
@@ -191,7 +198,7 @@ func (o Observer) Observe(ctx context.Context, request ports.ObservationRequest)
goalFact = model.Fact[model.Goal]{Status: model.FactKnown, Value: state.Goal, Evidence: stateEvidence}
}
return model.Observation{
- SchemaVersion: model.SnapshotSchemaVersion, Invocation: current,
+ SchemaVersion: model.SnapshotSchemaVersion, RecordedProgramFingerprint: recordedProgramFingerprint, Invocation: current,
Phase: model.Fact[model.ProtocolPhase]{Status: model.FactKnown, Value: phase, Evidence: stateEvidence},
Engagement: model.Fact[model.EngagementState]{Status: model.FactKnown, Value: state.Engagement, Evidence: stateEvidence},
Delivery: model.Fact[model.DeliveryState]{Status: model.FactKnown, Value: delivery, Evidence: deliveryEvidence},
@@ -545,10 +552,11 @@ type pendingJournalHeader struct {
Status string `json:"status"`
Reason string `json:"reason"`
Admission struct {
- ID string `json:"id"`
- SourcePhase model.ProtocolPhase `json:"source_phase"`
- Invocation model.InvocationContext `json:"invocation"`
- Parameters protocol.Parameters `json:"parameters"`
+ ID string `json:"id"`
+ ProgramFingerprint string `json:"program_fingerprint"`
+ SourcePhase model.ProtocolPhase `json:"source_phase"`
+ Invocation model.InvocationContext `json:"invocation"`
+ Parameters protocol.Parameters `json:"parameters"`
} `json:"admission"`
Mutations []struct {
Path string `json:"path"`
@@ -558,12 +566,13 @@ type pendingJournalHeader struct {
}
type pendingJournalSet struct {
- Found bool
- Conflicting bool
- Evidence []model.Evidence
- Recovery model.RecoveryContext
- Transaction model.TransactionContext
- TransactionState model.TransactionState
+ Found bool
+ Conflicting bool
+ Evidence []model.Evidence
+ Recovery model.RecoveryContext
+ Transaction model.TransactionContext
+ TransactionState model.TransactionState
+ ProgramFingerprint string
}
type pendingJournalRecord struct {
@@ -610,7 +619,7 @@ func pendingJournalEvidence(root, ignoreAdmissionID string, now time.Time) (pend
return pendingJournalSet{}, err
}
class := catalog.EventClass(header.TransitionClass)
- if header.SchemaVersion != 2 || header.Admission.ID == "" || entry.Name() != header.Admission.ID+".pending" || header.TransitionID == "" || header.Status == "" || !class.Valid() || !class.Controllable() {
+ if header.SchemaVersion != 2 || header.Admission.ID == "" || len(header.Admission.ProgramFingerprint) != 64 || entry.Name() != header.Admission.ID+".pending" || header.TransitionID == "" || header.Status == "" || !class.Valid() || !class.Controllable() {
return pendingJournalSet{}, fmt.Errorf("invalid pending transaction journal %s", path)
}
if header.Admission.ID == ignoreAdmissionID {
@@ -644,8 +653,9 @@ func pendingJournalEvidence(root, ignoreAdmissionID string, now time.Time) (pend
}
set := pendingJournalSet{
Found: true, Evidence: []model.Evidence{evidence}, TransactionState: transactionState,
- Recovery: model.RecoveryContext{TransactionID: header.Admission.ID, Cause: cause, SourcePhase: header.Admission.SourcePhase, Permitted: permitted, BudgetRemaining: budget, Resumption: header.Admission.SourcePhase},
- Transaction: model.TransactionContext{ID: header.Admission.ID, TransitionID: header.TransitionID, Status: header.Status, ResourceDigests: resourceDigests, ExternalPossible: external},
+ ProgramFingerprint: header.Admission.ProgramFingerprint,
+ Recovery: model.RecoveryContext{TransactionID: header.Admission.ID, Cause: cause, SourcePhase: header.Admission.SourcePhase, Permitted: permitted, BudgetRemaining: budget, Resumption: header.Admission.SourcePhase},
+ Transaction: model.TransactionContext{ID: header.Admission.ID, TransitionID: header.TransitionID, Status: header.Status, ResourceDigests: resourceDigests, ExternalPossible: external},
}
rootID := header.Admission.ID
if class == catalog.EventRecovery {
@@ -687,6 +697,9 @@ func pendingJournalEvidence(root, ignoreAdmissionID string, now time.Time) (pend
base.Evidence = nil
base.Transaction.ResourceDigests = nil
for _, record := range records {
+ if record.set.ProgramFingerprint != base.ProgramFingerprint {
+ return conflictingPending(records), nil
+ }
base.Evidence = append(base.Evidence, record.set.Evidence...)
base.Transaction.ResourceDigests = append(base.Transaction.ResourceDigests, record.set.Transaction.ResourceDigests...)
}
diff --git a/boatstack/internal/plant/observer_test.go b/boatstack/internal/plant/observer_test.go
index a1b7230..54dbd8f 100644
--- a/boatstack/internal/plant/observer_test.go
+++ b/boatstack/internal/plant/observer_test.go
@@ -201,9 +201,10 @@ func TestRecoveryAttemptsExhaustToEscalationOnly(t *testing.T) {
"status": "recovery-required",
"reason": "simulated interruption",
"admission": map[string]any{
- "id": originalID,
- "source_phase": "ACTIVE",
- "invocation": map[string]any{"correlation_id": "prior-process"},
+ "id": originalID,
+ "program_fingerprint": strings.Repeat("a", 64),
+ "source_phase": "ACTIVE",
+ "invocation": map[string]any{"correlation_id": "prior-process"},
},
}
writeJSON := func(name string, value any) {
@@ -237,6 +238,9 @@ func TestRecoveryAttemptsExhaustToEscalationOnly(t *testing.T) {
if observed.Recovery.BudgetRemaining != wantBudget {
t.Fatalf("attempt %d budget=%d, want %d", attempt, observed.Recovery.BudgetRemaining, wantBudget)
}
+ if observed.ProgramFingerprint != strings.Repeat("a", 64) {
+ t.Fatalf("pending program fingerprint=%q", observed.ProgramFingerprint)
+ }
if attempt == 3 {
if len(observed.Recovery.Permitted) != 1 || observed.Recovery.Permitted[0] != "recovery.escalate" {
t.Fatalf("exhausted recovery permitted=%v, want escalation only", observed.Recovery.Permitted)
@@ -261,12 +265,12 @@ func TestInterruptedRecoveryAttemptCollapsesToEscalatableTransactionGroup(t *tes
originalID := "adm-original"
write(originalID+".pending", map[string]any{
"schema_version": 2, "transition_id": "plan.create", "transition_class": "owned-local", "status": "recovery-required",
- "admission": map[string]any{"id": originalID, "source_phase": "ACTIVE", "invocation": map[string]any{"correlation_id": "old-process"}},
+ "admission": map[string]any{"id": originalID, "program_fingerprint": strings.Repeat("a", 64), "source_phase": "ACTIVE", "invocation": map[string]any{"correlation_id": "old-process"}},
})
write("adm-nested.pending", map[string]any{
"schema_version": 2, "transition_id": "recovery.rollback", "transition_class": "recovery", "status": "verifying",
"admission": map[string]any{
- "id": "adm-nested", "source_phase": "RECOVERY", "invocation": map[string]any{"correlation_id": "old-process"},
+ "id": "adm-nested", "program_fingerprint": strings.Repeat("a", 64), "source_phase": "RECOVERY", "invocation": map[string]any{"correlation_id": "old-process"},
"parameters": []map[string]string{{"name": "transaction_id", "value": originalID}},
},
})
diff --git a/boatstack/internal/surfaces/artifacts_external_test.go b/boatstack/internal/surfaces/artifacts_external_test.go
new file mode 100644
index 0000000..52ac1cf
--- /dev/null
+++ b/boatstack/internal/surfaces/artifacts_external_test.go
@@ -0,0 +1,50 @@
+package surfaces_test
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/distribution"
+ "github.com/operatorstack/boatstack/boatstack/internal/surfaces"
+)
+
+func TestCheckedArchitectureArtifactsMatchCompiledStandardProgram(t *testing.T) {
+ program, err := distribution.StandardProgram(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ transitions := program.Transitions()
+ checks := map[string]string{
+ "boatstack-v2-transition-catalog.md": surfaces.RenderCatalogMarkdown(transitions),
+ "boatstack-v2-transition-catalog.mmd": surfaces.RenderCatalogMermaid(transitions),
+ "boatstack-standard-flow.mmd": surfaces.RenderStandardFlowMermaid(transitions),
+ }
+ safety, err := surfaces.RenderCatalogLocusSafety(transitions)
+ if err != nil {
+ t.Fatal(err)
+ }
+ liveness, err := surfaces.RenderCatalogLocusLiveness(transitions)
+ if err != nil {
+ t.Fatal(err)
+ }
+ checks["boatstack-v2-locus-safety.json"] = safety
+ checks["boatstack-v2-locus-liveness.json"] = liveness
+
+ _, file, _, ok := runtime.Caller(0)
+ if !ok {
+ t.Fatal("cannot locate checked architecture artifacts")
+ }
+ repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
+ for name, expected := range checks {
+ actual, err := os.ReadFile(filepath.Join(repositoryRoot, "docs", "architecture", name))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(actual) != expected {
+ t.Errorf("%s drifted; regenerate it with boatstack catalog", name)
+ }
+ }
+}
diff --git a/boatstack/internal/surfaces/catalog_render.go b/boatstack/internal/surfaces/catalog_render.go
index dde761a..3f86b2e 100644
--- a/boatstack/internal/surfaces/catalog_render.go
+++ b/boatstack/internal/surfaces/catalog_render.go
@@ -19,8 +19,8 @@ func RenderCatalogMarkdown(transitions []catalog.Transition) string {
counts[transition.Class]++
}
var output strings.Builder
- output.WriteString("\n")
- output.WriteString("# Boatstack V2 executable transition catalog\n\n")
+ output.WriteString("\n")
+ output.WriteString("# Boatstack compiled transition catalog\n\n")
fmt.Fprintf(&output, "Registry size: **%d** transitions. Event classes: authority %d; owned-local %d; owned-external %d; recovery %d; observed-external %d.\n\n",
len(ordered), counts[catalog.EventAuthority], counts[catalog.EventOwnedLocal], counts[catalog.EventOwnedExternal], counts[catalog.EventRecovery], counts[catalog.EventObservedExternal])
output.WriteString("Controlling facets: `")
@@ -32,8 +32,8 @@ func RenderCatalogMarkdown(transitions []catalog.Transition) string {
output.WriteString(string(facet))
}
output.WriteString("`.\n\n")
- output.WriteString("| Transition | Class | Source phases | Target phases | Authority | Parameters | Owned resources | Recovery |\n")
- output.WriteString("|---|---|---|---|---|---|---|---|\n")
+ output.WriteString("| Transition | Origin | Owner | Selection | Class | Source phases | Target phases | Authority | Parameters | Owned resources | Verifier | Recovery | Cost |\n")
+ output.WriteString("|---|---|---|---|---|---|---|---|---|---|---|---|---|\n")
for _, transition := range ordered {
authority := joinAuthorities(transition.Authority)
if len(transition.AuthorityAll) > 0 {
@@ -51,18 +51,36 @@ func RenderCatalogMarkdown(transitions []catalog.Transition) string {
if recovery == "" {
recovery = "-"
}
- fmt.Fprintf(&output, "| `%s` | %s | %s | %s | %s | %s | %s | `%s` |\n",
- transition.ID, transition.Class, joinPhases(transition.SourcePhases), joinPhases(transition.TargetPhases),
- authority, markdownList(parameters), markdownList(transition.OwnedResources), recovery)
+ origin := fmt.Sprintf("%s:`%s@%s`
`%s`", transition.Origin.Kind, transition.Origin.ID, transition.Origin.Version, transition.Origin.ManifestFingerprint)
+ fmt.Fprintf(&output, "| `%s` | %s | `%s` | %s | %s | %s | %s | %s | %s | %s | `%s` | `%s` | `%s` |\n",
+ transition.ID, origin, transition.Owner, transition.SelectionClass, transition.Class,
+ joinPhases(transition.SourcePhases), joinPhases(transition.TargetPhases), authority,
+ markdownList(parameters), markdownList(transition.OwnedResources), transition.Verifier, recovery, transition.CostClass)
}
output.WriteString("\n`*` 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`.\n")
return output.String()
}
-// RenderCatalogMermaid generates one inventory node per runtime transition,
-// grouped by event class. Phase sets are labels on the exact transition node,
-// avoiding a second hand-maintained graph.
+// RenderCatalogMermaid generates one connected phase-transition graph from the
+// runtime registry, avoiding a second hand-maintained graph.
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
+// 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 {
+ flow = append(flow, transition)
+ }
+ }
+ return renderCatalogMermaid(flow, "%% Generated from compiled PrimaryFlow declarations by surfaces.RenderStandardFlowMermaid. Do not edit.\n")
+}
+
+func renderCatalogMermaid(transitions []catalog.Transition, header string) string {
ordered := append([]catalog.Transition(nil), transitions...)
sort.Slice(ordered, func(i, j int) bool {
if ordered[i].Class != ordered[j].Class {
@@ -72,21 +90,53 @@ func RenderCatalogMermaid(transitions []catalog.Transition) string {
})
classes := []catalog.EventClass{catalog.EventAuthority, catalog.EventOwnedLocal, catalog.EventOwnedExternal, catalog.EventRecovery, catalog.EventObservedExternal}
var output strings.Builder
- output.WriteString("%% Generated from catalog.Default by surfaces.RenderCatalogMermaid. Do not edit.\n")
+ output.WriteString(header)
output.WriteString("flowchart TB\n")
+ phases := map[model.ProtocolPhase]bool{}
+ for _, transition := range ordered {
+ for _, phase := range append(append([]model.ProtocolPhase(nil), transition.SourcePhases...), transition.TargetPhases...) {
+ phases[phase] = true
+ }
+ }
+ orderedPhases := make([]model.ProtocolPhase, 0, len(phases))
+ for phase := range phases {
+ orderedPhases = append(orderedPhases, phase)
+ }
+ sort.Slice(orderedPhases, func(i, j int) bool { return orderedPhases[i] < orderedPhases[j] })
+ output.WriteString(" subgraph phases[\"protocol phases\"]\n")
+ for _, phase := range orderedPhases {
+ fmt.Fprintf(&output, " p_%s[\"%s\"]\n", strings.ReplaceAll(string(phase), "-", "_"), escapeMermaid(string(phase)))
+ }
+ output.WriteString(" end\n")
index := 0
+ type edgeSet struct {
+ node string
+ sources []model.ProtocolPhase
+ targets []model.ProtocolPhase
+ }
+ edges := make([]edgeSet, 0, len(ordered))
for _, class := range classes {
fmt.Fprintf(&output, " subgraph %s[\"%s\"]\n", strings.ReplaceAll(string(class), "-", "_"), class)
for _, transition := range ordered {
if transition.Class != class {
continue
}
- label := fmt.Sprintf("%s
%s → %s", transition.ID, strings.Join(phaseStrings(transition.SourcePhases), " | "), strings.Join(phaseStrings(transition.TargetPhases), " | "))
- fmt.Fprintf(&output, " t%02d[\"%s\"]\n", index, escapeMermaid(label))
+ node := fmt.Sprintf("t%02d", index)
+ label := fmt.Sprintf("%s
%s", transition.ID, transition.Class)
+ fmt.Fprintf(&output, " %s[\"%s\"]\n", node, escapeMermaid(label))
+ edges = append(edges, edgeSet{node: node, sources: transition.SourcePhases, targets: transition.TargetPhases})
index++
}
output.WriteString(" end\n")
}
+ for _, edge := range edges {
+ for _, phase := range edge.sources {
+ fmt.Fprintf(&output, " p_%s --> %s\n", strings.ReplaceAll(string(phase), "-", "_"), edge.node)
+ }
+ for _, phase := range edge.targets {
+ fmt.Fprintf(&output, " %s --> p_%s\n", edge.node, strings.ReplaceAll(string(phase), "-", "_"))
+ }
+ }
return output.String()
}
diff --git a/boatstack/internal/surfaces/locus_render.go b/boatstack/internal/surfaces/locus_render.go
index d78f6a8..58293ad 100644
--- a/boatstack/internal/surfaces/locus_render.go
+++ b/boatstack/internal/surfaces/locus_render.go
@@ -85,17 +85,17 @@ func renderCatalogLocus(transitions []catalog.Transition, safety bool) (string,
result := locusModel{
SchemaVersion: 1,
ID: "boatstack-v2-executable-catalog-liveness-v1",
- Subject: "Finite stable-phase abstraction generated from the executable Boatstack V2 registry. It contains one event for every runtime catalog entry and expands each declared source and target phase set. The 17-facet predicates, operating-system behavior, and external-provider truth remain executable evidence obligations rather than theorem assumptions.",
+ 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/internal/kernel/catalog/default.go", Note: "Executable registry, exact transition count, event classes, phase predicates, authority, and materialization."},
+ {Path: "boatstack/control/control.go", Note: "Compiler combines exact CoreSystem, PrimaryFlow, 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."},
- {Path: "boatstack/internal/kernel/reducer/reducer.go", Note: "Single executable reducer for every controllable semantic transition."},
- {Path: "boatstack/internal/kernel/catalog/completeness_test.go", Note: "Runtime facet/event classification, writer-boundary inventory, and reducer-completeness refusing tests."},
+ {Path: "boatstack/internal/effects/state_reducer.go", Note: "Admitted native effects reduce every controllable Standard distribution transition through one state adapter."},
+ {Path: "boatstack/flow/standard/completeness_test.go", Note: "Runtime facet/event classification, writer-boundary inventory, and reducer-completeness refusing tests."},
{Path: "boatstack/internal/kernel/engine/engine_test.go", Note: "Exact-admission, stale-snapshot, postcondition, interruption, idempotency, and unknown-outcome tests."},
{Path: "boatstack/internal/effects/prepared.go", Note: "Staged effect ordering, atomic resource application, rollback, and external settlement boundary."},
- {Path: "boatstack/internal/kernel/catalog/historical_test.go", Note: "Historical incidents resolved through the executable runtime supervisor."},
+ {Path: "boatstack/flow/standard/historical_test.go", Note: "Historical incidents resolved through the executable runtime supervisor."},
},
Spec: locusSpec{
Description: "Every reachable stable catalog phase retains a path to terminal, explicit frontier, or safe abandonment.",
diff --git a/boatstack/internal/surfaces/protocol.go b/boatstack/internal/surfaces/protocol.go
index 41106e7..36818a8 100644
--- a/boatstack/internal/surfaces/protocol.go
+++ b/boatstack/internal/surfaces/protocol.go
@@ -74,13 +74,6 @@ func (r Request) Validate(now time.Time) error {
return fmt.Errorf("apply/recover request requires flow and transition identity")
}
}
- if r.Operation == OperationRecover {
- switch r.TransitionID {
- case "recovery.resume", "recovery.rollback", "recovery.escalate", "runtime.reconcile", "configuration.reconcile", "workspace.reconcile", "publication.reconcile":
- default:
- return fmt.Errorf("recover operation requires a registered recovery transition")
- }
- }
if r.Operation == OperationGuard && (strings.TrimSpace(r.Command) == "" || len(r.Command) > 1<<20) {
return fmt.Errorf("guard operation requires a bounded command")
}
@@ -94,10 +87,22 @@ func (r Request) Validate(now time.Time) error {
}
type DoctorReport struct {
- Healthy bool `json:"healthy"`
- TransitionCount int `json:"transition_count"`
- Snapshot string `json:"snapshot,omitempty"`
- Detail string `json:"detail"`
+ Healthy bool `json:"healthy"`
+ 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"`
+ CoreTransitionCount int `json:"core_transition_count"`
+ FlowTransitionCount int `json:"flow_transition_count"`
+ ExtensionTransitionCount int `json:"extension_transition_count"`
+ TransitionCount int `json:"transition_count"`
+ EnabledExtensions []string `json:"enabled_extensions,omitempty"`
+ ProgramFingerprint string `json:"program_fingerprint"`
+ UnresolvedProgramDrift bool `json:"unresolved_program_drift"`
+ Snapshot string `json:"snapshot,omitempty"`
+ Detail string `json:"detail"`
}
type Response struct {
diff --git a/boatstack/internal/surfaces/render_test.go b/boatstack/internal/surfaces/render_test.go
index ad06b52..53fa722 100644
--- a/boatstack/internal/surfaces/render_test.go
+++ b/boatstack/internal/surfaces/render_test.go
@@ -2,10 +2,7 @@ package surfaces
import (
"encoding/json"
- "os"
- "path/filepath"
"reflect"
- "runtime"
"strings"
"testing"
@@ -13,11 +10,12 @@ import (
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/supervisor"
+ "github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
func TestShellRenderersConsumeOneCommandAST(t *testing.T) {
// control-law: shell-rendering-never-changes-transition-semantics
- transition, ok := catalog.Default().Lookup("plan.create")
+ transition, ok := testprogram.StandardRegistry().Lookup("plan.create")
if !ok {
t.Fatal("missing plan.create")
}
@@ -49,18 +47,21 @@ func TestShellRenderersConsumeOneCommandAST(t *testing.T) {
}
func TestCatalogArtifactsAreGeneratedFromEveryRuntimeTransition(t *testing.T) {
- registry := catalog.Default()
+ registry := testprogram.StandardRegistry()
markdown := RenderCatalogMarkdown(registry.All())
mermaid := RenderCatalogMermaid(registry.All())
for _, transition := range registry.All() {
- rowPrefix := "\n| `" + string(transition.ID) + "` | " + string(transition.Class) + " |"
+ rowPrefix := "\n| `" + string(transition.ID) + "` | " + string(transition.Origin.Kind) + ":"
if strings.Count(markdown, rowPrefix) != 1 {
t.Errorf("markdown does not contain transition %s exactly once", transition.ID)
}
- if strings.Count(mermaid, string(transition.ID)+"
") != 1 {
+ if strings.Count(mermaid, `["`+string(transition.ID)+`
`) != 1 {
t.Errorf("Mermaid does not contain transition %s exactly once", transition.ID)
}
}
+ if !strings.Contains(mermaid, " --> ") {
+ t.Fatal("Mermaid transition inventory is not connected to protocol phases")
+ }
if markdown != RenderCatalogMarkdown(registry.All()) || mermaid != RenderCatalogMermaid(registry.All()) {
t.Fatal("catalog artifact rendering is not deterministic")
}
@@ -68,7 +69,7 @@ func TestCatalogArtifactsAreGeneratedFromEveryRuntimeTransition(t *testing.T) {
func TestLocusModelsAreGeneratedFromEveryRuntimeTransition(t *testing.T) {
// control-law: formal-model-alphabet-is-the-runtime-catalog
- registry := catalog.Default()
+ registry := testprogram.StandardRegistry()
for _, render := range []func([]catalog.Transition) (string, error){RenderCatalogLocusSafety, RenderCatalogLocusLiveness} {
one, err := render(registry.All())
if err != nil {
@@ -101,41 +102,9 @@ func TestLocusModelsAreGeneratedFromEveryRuntimeTransition(t *testing.T) {
}
}
-func TestCheckedArchitectureArtifactsMatchExecutableCatalog(t *testing.T) {
- _, file, _, ok := runtime.Caller(0)
- if !ok {
- t.Fatal("cannot locate checked architecture artifacts")
- }
- repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
- checks := map[string]string{
- "boatstack-v2-transition-catalog.md": RenderCatalogMarkdown(catalog.Default().All()),
- "boatstack-v2-transition-catalog.mmd": RenderCatalogMermaid(catalog.Default().All()),
- }
- safety, err := RenderCatalogLocusSafety(catalog.Default().All())
- if err != nil {
- t.Fatal(err)
- }
- liveness, err := RenderCatalogLocusLiveness(catalog.Default().All())
- if err != nil {
- t.Fatal(err)
- }
- checks["boatstack-v2-locus-safety.json"] = safety
- checks["boatstack-v2-locus-liveness.json"] = liveness
- for name, expected := range checks {
- path := filepath.Join(repositoryRoot, "docs", "architecture", name)
- actual, err := os.ReadFile(path)
- if err != nil {
- t.Fatal(err)
- }
- if string(actual) != expected {
- t.Errorf("%s drifted; regenerate it with boatstack catalog", name)
- }
- }
-}
-
func TestEveryHostConsumesOneSemanticPrescription(t *testing.T) {
goal := model.Goal{ID: "goal", Kind: model.GoalVerified, DeliveryID: "delivery"}
- for _, transition := range catalog.Default().All() {
+ for _, transition := range testprogram.StandardRegistry().All() {
if !transition.Controllable() {
continue
}
@@ -162,7 +131,7 @@ func TestEveryHostConsumesOneSemanticPrescription(t *testing.T) {
}
func TestCanonicalHostsAreDataNotControllers(t *testing.T) {
- want := []string{"claude", "cli", "codex", "cursor", "gemini", "mcp"}
+ want := []string{"claude", "cli", "codex", "cursor", "gemini", "mcp", "sdk"}
if got := CanonicalHostNames(); !reflect.DeepEqual(got, want) {
t.Fatalf("hosts = %v, want %v", got, want)
}
@@ -177,7 +146,7 @@ func TestGuardClassifierIsShellNeutralAndPrivacyBounded(t *testing.T) {
}
}
managed := ClassifyCommandIntent("gh pr create --base main --head feature")
- if managed.Class != supervisor.IntentManagedBypass || managed.Transition != "publication.execute" {
+ if managed.Class != supervisor.IntentManagedBypass || managed.Operation != "publication.create" || managed.Transition != "" {
t.Fatalf("managed intent = %#v", managed)
}
ordinary := ClassifyCommandIntent("go test ./...")
diff --git a/boatstack/internal/testprogram/standard.go b/boatstack/internal/testprogram/standard.go
new file mode 100644
index 0000000..4c248a5
--- /dev/null
+++ b/boatstack/internal/testprogram/standard.go
@@ -0,0 +1,26 @@
+// Package testprogram assembles first-party definitions for compatibility and
+// parity tests. Product code must use distribution or an explicit compiler.
+package testprogram
+
+import (
+ "context"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+)
+
+// StandardRegistry compiles the real CoreSystem and StandardFlow declaration
+// bytes. It panics only in tests when a checked first-party manifest is invalid.
+func StandardRegistry() catalog.Registry {
+ program, err := control.Compile(context.Background(), control.CompileRequest{
+ KernelVersion: "test-kernel",
+ Core: core.System(),
+ Flow: standard.Definition(),
+ })
+ if err != nil {
+ panic(err)
+ }
+ return program.RuntimeRegistry()
+}
diff --git a/boatstack/v2_kernel.go b/boatstack/kernel.go
similarity index 55%
rename from boatstack/v2_kernel.go
rename to boatstack/kernel.go
index e2e0d47..ee15805 100644
--- a/boatstack/v2_kernel.go
+++ b/boatstack/kernel.go
@@ -8,6 +8,7 @@ import (
"os"
"time"
+ "github.com/operatorstack/boatstack/boatstack/control"
"github.com/operatorstack/boatstack/boatstack/internal/effects"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/engine"
@@ -25,51 +26,58 @@ var (
ChecksumsSHA256 = "development"
)
-// V2Kernel is a product facade over the authoritative engine and its concrete
-// plant/effect ports. It owns no independent durable or lifecycle state.
-type V2Kernel struct {
+// Kernel is the deterministic mechanism facade over one immutable compiled
+// ControlProgram and its concrete plant/effect ports. It owns no independent
+// delivery-flow policy or durable lifecycle state.
+type Kernel struct {
+ program control.ControlProgram
registry catalog.Registry
resolver plant.Resolver
- observer plant.Observer
+ observer ports.Observer
engine engine.Engine
clock effects.Clock
}
-func NewV2Kernel(externalStateRoot string) (V2Kernel, error) {
+func NewKernel(externalStateRoot string, program control.ControlProgram) (Kernel, error) {
+ if program.Fingerprint() == "" || program.TransitionCount() == 0 {
+ return Kernel{}, fmt.Errorf("Kernel requires an immutable compiled ControlProgram")
+ }
clock := effects.Clock{}
resolver, err := plant.NewResolver(externalStateRoot)
if err != nil {
- return V2Kernel{}, err
+ return Kernel{}, err
}
- observer, err := plant.NewObserver(resolver, clock)
+ baseObserver, err := plant.NewObserver(resolver, clock)
if err != nil {
- return V2Kernel{}, err
+ return Kernel{}, err
}
+ observer := programObserver{base: baseObserver, program: program}
locker, err := effects.NewLocker(resolver)
if err != nil {
- return V2Kernel{}, err
+ return Kernel{}, err
}
journal, err := effects.NewJournal(resolver, clock)
if err != nil {
- return V2Kernel{}, err
+ return Kernel{}, err
}
receipts, err := effects.NewReceiptStore(resolver, clock)
if err != nil {
- return V2Kernel{}, err
+ return Kernel{}, err
}
- driver, err := effects.NewDriver(resolver, clock, effects.NewNativeBoundary())
+ baseDriver, err := effects.NewProgramDriver(resolver, clock, effects.NewNativeBoundary(), program.ResourceOwnership())
if err != nil {
- return V2Kernel{}, err
+ return Kernel{}, err
}
- registry := catalog.Default()
- runtimeEngine, err := engine.New(registry, observer, clock, locker, journal, driver, receipts)
+ driver := programEffectDriver{base: baseDriver, program: program}
+ registry := program.RuntimeRegistry()
+ runtimeEngine, err := engine.New(registry, program.RuntimeGoalContracts(), program.Fingerprint(), observer, clock, locker, journal, driver, receipts)
if err != nil {
- return V2Kernel{}, err
+ return Kernel{}, err
}
- return V2Kernel{registry: registry, resolver: resolver, observer: observer, engine: runtimeEngine, clock: clock}, nil
+ return Kernel{program: program, registry: registry, resolver: resolver, observer: observer, engine: runtimeEngine, clock: clock}, nil
}
-func (k V2Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces.Response, error) {
+func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces.Response, error) {
response := surfaces.Response{SchemaVersion: surfaces.SchemaVersion, Operation: request.Operation}
if err := request.Validate(k.clock.Now()); err != nil {
response.Error = err.Error()
@@ -79,6 +87,14 @@ func (k V2Kernel) Handle(ctx context.Context, request surfaces.Request) (surface
response.Catalog = k.registry.All()
return response, nil
}
+ if request.Operation == surfaces.OperationRecover {
+ transition, ok := k.registry.Lookup(request.TransitionID)
+ if !ok || transition.Class != catalog.EventRecovery {
+ err := fmt.Errorf("recover operation requires a recovery transition from the compiled control program")
+ response.Error = err.Error()
+ return response, err
+ }
+ }
invocation, err := k.resolver.ResolveInvocation(ctx, request.Repository, request.Host, request.CorrelationID)
if err != nil {
response.Error = err.Error()
@@ -94,18 +110,26 @@ func (k V2Kernel) Handle(ctx context.Context, request surfaces.Request) (surface
switch request.Operation {
case surfaces.OperationResolve:
resolution, resolveErr := k.engine.Resolve(ctx, engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Requested: request.TransitionID})
+ response.Goal, response.Decision = resolution.Goal, &resolution.Decision
+ if resolution.Snapshot.Fingerprint != "" {
+ response.Snapshot = &resolution.Snapshot
+ }
if resolveErr != nil {
response.Error = resolveErr.Error()
return response, resolveErr
}
- response.Goal, response.Snapshot, response.Decision = resolution.Goal, &resolution.Snapshot, &resolution.Decision
return response, nil
case surfaces.OperationApply, surfaces.OperationRecover:
result, applyErr := k.engine.Apply(ctx, engine.ApplyRequest{
ResolveRequest: engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Requested: request.TransitionID},
FlowID: request.FlowID, Parameters: request.Parameters, IdempotencyKey: request.IdempotencyKey, AdmissionLifetime: 2 * time.Minute,
})
- response.Goal, response.Snapshot = result.Goal, &result.Target
+ response.Goal = result.Goal
+ if result.Target.Fingerprint != "" {
+ response.Snapshot = &result.Target
+ } else if result.Source.Fingerprint != "" {
+ response.Snapshot = &result.Source
+ }
if result.Decision.Kind != "" {
response.Decision = &result.Decision
}
@@ -118,34 +142,52 @@ func (k V2Kernel) Handle(ctx context.Context, request surfaces.Request) (surface
response.Replayed = result.Replayed
if applyErr != nil {
response.Error = applyErr.Error()
- if result.Target.Fingerprint == "" {
- response.Snapshot = &result.Source
- }
return response, applyErr
}
return response, nil
case surfaces.OperationDoctor:
+ summary := k.program.Summary()
+ extensionIDs := make([]string, 0, len(summary.Extensions))
+ for _, extension := range summary.Extensions {
+ extensionIDs = append(extensionIDs, extension.ID+"@"+extension.Version)
+ }
+ 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,
+ ExtensionTransitionCount: summary.ExtensionTransitionCount, TransitionCount: summary.TotalTransitionCount,
+ EnabledExtensions: extensionIDs, ProgramFingerprint: summary.ProgramFingerprint,
+ }
observation, observeErr := k.observer.Observe(ctx, ports.ObservationRequest{Invocation: invocation})
if observeErr != nil {
- response.Doctor = &surfaces.DoctorReport{Healthy: false, TransitionCount: k.registry.Len(), Detail: observeErr.Error()}
+ report.Healthy, report.Detail = false, observeErr.Error()
+ response.Doctor = &report
response.Error = observeErr.Error()
return response, observeErr
}
- snapshot, canonicalErr := model.Canonicalize(observation)
+ snapshot, canonicalErr := model.CanonicalizeForProgram(observation, k.program.Fingerprint())
if canonicalErr != nil {
- response.Doctor = &surfaces.DoctorReport{Healthy: false, TransitionCount: k.registry.Len(), Detail: canonicalErr.Error()}
+ report.Healthy, report.Detail = false, canonicalErr.Error()
+ response.Doctor = &report
response.Error = canonicalErr.Error()
return response, canonicalErr
}
response.Snapshot = &snapshot
- response.Doctor = &surfaces.DoctorReport{Healthy: k.registry.Len() == catalog.DefaultTransitionCount, TransitionCount: k.registry.Len(), Snapshot: snapshot.Fingerprint, Detail: "V2 kernel, observation, and catalog are valid"}
+ report.UnresolvedProgramDrift = snapshot.Program.Status != model.FactKnown || snapshot.Program.Value == model.ProgramDrift
+ report.Healthy = k.registry.Len() == summary.TotalTransitionCount && !report.UnresolvedProgramDrift
+ report.Snapshot = snapshot.Fingerprint
+ report.Detail = "Kernel, observation, and compiled control program are valid"
+ if report.UnresolvedProgramDrift {
+ report.Detail = "compiled control program drift requires explicit reconciliation"
+ }
+ response.Doctor = &report
return response, nil
case surfaces.OperationEvents:
layout, _, layoutErr := k.resolver.ResolveLayout(ctx, invocation)
if layoutErr != nil {
return response, layoutErr
}
- events, readErr := readV2Events(layout.EventPath)
+ events, readErr := readEvents(layout.EventPath)
if readErr != nil {
response.Error = readErr.Error()
return response, readErr
@@ -158,13 +200,13 @@ func (k V2Kernel) Handle(ctx context.Context, request surfaces.Request) (surface
response.Error = observeErr.Error()
return response, observeErr
}
- snapshot, canonicalErr := model.Canonicalize(observation)
+ snapshot, canonicalErr := model.CanonicalizeForProgram(observation, k.program.Fingerprint())
if canonicalErr != nil {
response.Error = canonicalErr.Error()
return response, canonicalErr
}
intent := surfaces.ClassifyCommandIntent(request.Command)
- guard := supervisor.New(k.registry).Guard(snapshot, intent)
+ guard := supervisor.New(k.registry, k.program.RuntimeGoalContracts()).Guard(snapshot, intent)
response.Snapshot, response.Guard = &snapshot, &guard
return response, nil
default:
@@ -172,24 +214,24 @@ func (k V2Kernel) Handle(ctx context.Context, request surfaces.Request) (surface
}
}
-func (k V2Kernel) deriveRepositoryAuthority(ctx context.Context, invocation model.InvocationContext, bundle protocol.AuthorityBundle) (protocol.AuthorityBundle, error) {
+func (k Kernel) deriveRepositoryAuthority(ctx context.Context, invocation model.InvocationContext, bundle protocol.AuthorityBundle) (protocol.AuthorityBundle, error) {
for _, receipt := range bundle.Receipts {
if receipt.Class == catalog.AuthorityRepository {
- return protocol.AuthorityBundle{}, fmt.Errorf("repository authority must be derived once by the V2 kernel")
+ return protocol.AuthorityBundle{}, fmt.Errorf("repository authority must be derived once by Kernel")
}
}
observation, err := k.observer.Observe(ctx, ports.ObservationRequest{Invocation: invocation})
if err != nil {
return protocol.AuthorityBundle{}, err
}
- snapshot, err := model.Canonicalize(observation)
+ snapshot, err := model.CanonicalizeForProgram(observation, k.program.Fingerprint())
if err != nil {
return protocol.AuthorityBundle{}, err
}
return protocol.DeriveRepositoryAuthority(snapshot, bundle, k.clock.Now())
}
-func readV2Events(path string) ([]map[string]any, error) {
+func readEvents(path string) ([]map[string]any, error) {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
diff --git a/boatstack/kernel_test.go b/boatstack/kernel_test.go
new file mode 100644
index 0000000..7aaf25f
--- /dev/null
+++ b/boatstack/kernel_test.go
@@ -0,0 +1,38 @@
+package boatstack_test
+
+import (
+ "context"
+ "strings"
+ "testing"
+ "time"
+
+ boatstack "github.com/operatorstack/boatstack/boatstack"
+ "github.com/operatorstack/boatstack/boatstack/distribution"
+ "github.com/operatorstack/boatstack/boatstack/internal/surfaces"
+)
+
+func TestRecoverSurfaceConsumesCompiledRegistryInsteadOfFixedFlowIDs(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 {
+ t.Fatal(err)
+ }
+ kernel, err := boatstack.NewKernel(t.TempDir(), program)
+ if err != nil {
+ t.Fatal(err)
+ }
+ request := surfaces.Request{
+ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationRecover,
+ Repository: "/repository-is-not-consulted", Host: "cli", CorrelationID: "compiled-recovery",
+ FlowID: "flow", TransitionID: "plan.create",
+ }
+ response, err := kernel.Handle(context.Background(), request)
+ if err == nil || !strings.Contains(err.Error(), "compiled control program") || response.Error == "" {
+ t.Fatalf("non-recovery transition crossed recover surface: response=%+v error=%v", response, err)
+ }
+
+ request.TransitionID = "example.extension.recover"
+ if err := request.Validate(time.Now()); err != nil {
+ t.Fatalf("surface schema rejected a recovery ID before compiled-registry validation: %v", err)
+ }
+}
diff --git a/boatstack/program_effects.go b/boatstack/program_effects.go
new file mode 100644
index 0000000..1203bbb
--- /dev/null
+++ b/boatstack/program_effects.go
@@ -0,0 +1,160 @@
+package boatstack
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/effects"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
+)
+
+type programEffectDriver struct {
+ base ports.EffectDriver
+ program control.ControlProgram
+}
+
+func (d programEffectDriver) Prepare(ctx context.Context, admission protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) {
+ 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 {
+ return d.base.Prepare(ctx, admission, transition)
+ }
+ if flow.Runtime == nil {
+ return nil, fmt.Errorf("primary-flow 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,
+ 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)
+ if invokeErr != nil {
+ return ports.EffectResult{}, invokeErr
+ }
+ if err := validateFlowResponse(flow, request.Operation, request.CorrelationID, response); err != nil {
+ return ports.EffectResult{}, err
+ }
+ return decodeExtensionSettlement(flow.Identity.ID, response.ExternalResult)
+ })
+ }
+ operation := control.FlowPlanLocalEffectOperation
+ if transition.Class == catalog.EventRecovery {
+ operation = control.FlowRecoverOperation
+ }
+ request.Operation = operation
+ response, invokeErr := flow.Runtime.InvokeFlow(ctx, request)
+ if invokeErr != nil {
+ return nil, invokeErr
+ }
+ if err := validateFlowResponse(flow, operation, admission.Invocation.Correlation, response); err != nil {
+ return nil, err
+ }
+ if err := validateProgramWrites(d.program, transition, flow.Identity.ID, response.Writes); err != nil {
+ return nil, err
+ }
+ return effects.NewFlowLocalPrepared(admission.Invocation.InvokingPath, flow.Identity.ID, response.Writes)
+ }
+ extension, ok := d.program.ExtensionByID(transition.Origin.ID)
+ if !ok || extension.Runtime == nil {
+ return nil, fmt.Errorf("extension runtime %q is unavailable", transition.Origin.ID)
+ }
+ parameters, err := json.Marshal(admission.Parameters)
+ if err != nil {
+ return nil, err
+ }
+ baseRequest := control.ExtensionRequest{
+ ProtocolVersion: control.ExtensionProtocolVersion, ExtensionID: extension.Identity.ID, ExtensionVersion: extension.Identity.Version,
+ ProgramFingerprint: admission.ProgramFingerprint, CorrelationID: admission.Invocation.Correlation,
+ RepositoryRoot: admission.Invocation.InvokingPath, TransitionID: transition.ID, Parameters: parameters, Settings: extension.Manifest.Settings,
+ }
+ if transition.Class == catalog.EventOwnedExternal {
+ return effects.NewExtensionExternalPrepared(func(executionContext context.Context) (ports.EffectResult, error) {
+ request := baseRequest
+ request.Operation = control.ExtensionExecuteExternalOperation
+ response, invokeErr := extension.Runtime.Invoke(executionContext, request)
+ if invokeErr != nil {
+ return ports.EffectResult{}, invokeErr
+ }
+ if err := validateExtensionResponse(extension, request.Operation, request.CorrelationID, response); err != nil {
+ return ports.EffectResult{}, err
+ }
+ return decodeExtensionSettlement(extension.Identity.ID, response.ExternalResult)
+ })
+ }
+ operation := control.ExtensionPlanLocalEffectOperation
+ if transition.Class == catalog.EventRecovery {
+ operation = control.ExtensionRecoverOperation
+ }
+ baseRequest.Operation = operation
+ response, err := extension.Runtime.Invoke(ctx, baseRequest)
+ if err != nil {
+ return nil, err
+ }
+ if err := validateExtensionResponse(extension, operation, admission.Invocation.Correlation, response); err != nil {
+ return nil, err
+ }
+ if err := validateProgramWrites(d.program, transition, extension.Identity.ID, response.Writes); err != nil {
+ return nil, err
+ }
+ return effects.NewExtensionLocalPrepared(admission.Invocation.InvokingPath, extension.Identity.ID, response.Writes)
+}
+
+func validateProgramWrites(program control.ControlProgram, transition catalog.Transition, owner string, writes []control.ResourceWrite) error {
+ allowed := map[string]bool{}
+ for _, resource := range transition.OwnedResources {
+ allowed[resource] = true
+ }
+ ownership := program.ResourceOwnership()
+ for _, write := range writes {
+ if !allowed[write.Resource] || ownership[write.Resource] != owner {
+ return fmt.Errorf("transition %q planned undeclared resource %q", transition.ID, write.Resource)
+ }
+ }
+ return nil
+}
+
+func decodeExtensionSettlement(owner string, raw []byte) (ports.EffectResult, error) {
+ var result struct {
+ Settlement ports.EffectSettlement `json:"settlement"`
+ Detail string `json:"detail,omitempty"`
+ }
+ if err := decodeStrictExtensionJSON(raw, &result); err != nil {
+ return ports.EffectResult{}, fmt.Errorf("component %q returned invalid external settlement", owner)
+ }
+ if result.Settlement != ports.EffectSettled && result.Settlement != ports.EffectUnknown {
+ return ports.EffectResult{}, fmt.Errorf("component %q returned invalid external settlement", owner)
+ }
+ return ports.EffectResult{Settlement: result.Settlement, Detail: result.Detail}, nil
+}
+
+func decodeStrictExtensionJSON(raw []byte, target any) error {
+ if len(raw) == 0 {
+ return fmt.Errorf("empty JSON")
+ }
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(target); err != nil {
+ return err
+ }
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ return fmt.Errorf("trailing JSON")
+ }
+ return nil
+}
diff --git a/boatstack/program_observer.go b/boatstack/program_observer.go
new file mode 100644
index 0000000..b7e62fc
--- /dev/null
+++ b/boatstack/program_observer.go
@@ -0,0 +1,227 @@
+package boatstack
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
+)
+
+type programObserver struct {
+ base ports.Observer
+ program control.ControlProgram
+}
+
+// ComponentRuntimeError preserves a bounded protocol error classification and
+// message without granting a component control over recovery policy.
+type ComponentRuntimeError struct {
+ Component string
+ Operation string
+ Class string
+ Message string
+}
+
+func (e ComponentRuntimeError) Error() string {
+ return fmt.Sprintf("%s %s reported %s: %s", e.Component, e.Operation, e.Class, e.Message)
+}
+
+func (o programObserver) Observe(ctx context.Context, request ports.ObservationRequest) (model.Observation, error) {
+ observation, err := o.base.Observe(ctx, request)
+ if err != nil {
+ return model.Observation{}, err
+ }
+ flow := o.program.Flow()
+ if flow.Manifest.RuntimeMode == control.FlowRuntimeProtocol {
+ if flow.Runtime == nil {
+ return model.Observation{}, fmt.Errorf("primary flow %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)
+ }
+ response, invokeErr := flow.Runtime.InvokeFlow(ctx, control.FlowRequest{
+ ProtocolVersion: control.FlowProtocolVersion, Operation: control.FlowObserveOperation,
+ FlowID: flow.Identity.ID, FlowVersion: 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)
+ }
+ if err := validateFlowResponse(flow, control.FlowObserveOperation, 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))
+ 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)
+ }
+ if _, exists := observation.FlowFacts[fact.ID]; exists {
+ return model.Observation{}, fmt.Errorf("primary-flow 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)
+ }
+ observation.FlowFacts[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}},
+ }
+ delete(declared, fact.ID)
+ }
+ if len(declared) != 0 {
+ return model.Observation{}, fmt.Errorf("primary flow %q omitted required observed facts", flow.Identity.ID)
+ }
+ }
+ // Extension observers receive the same core-plus-flow projection. Facts
+ // returned by one extension are collected into the final snapshot but are
+ // never exposed to another observer based on invocation order.
+ observation.ExtensionFacts = nil
+ extensionProjection := observation
+ for _, extension := range o.program.Extensions() {
+ if len(extension.Manifest.Facts) == 0 {
+ continue
+ }
+ // Repository-selected executable extensions remain inert until the
+ // observed configuration and recorded program identity prove that this
+ // exact composition has already crossed the Kernel admission boundary.
+ if extension.Manifest.ExecutableSHA256 != "" && !observation.ExecutableRuntimeAdmitted(o.program.Fingerprint()) {
+ continue
+ }
+ if extension.Runtime == nil {
+ return model.Observation{}, fmt.Errorf("extension %q observer is unavailable", extension.Identity.ID)
+ }
+ projection := extensionProjection
+ projection.ProgramFingerprint = o.program.Fingerprint()
+ snapshot, err := json.Marshal(projection)
+ if err != nil {
+ return model.Observation{}, fmt.Errorf("encode bounded extension observation: %w", err)
+ }
+ response, err := extension.Runtime.Invoke(ctx, control.ExtensionRequest{
+ ProtocolVersion: control.ExtensionProtocolVersion, Operation: control.ExtensionObserveOperation,
+ ExtensionID: extension.Identity.ID, ExtensionVersion: extension.Identity.Version,
+ ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation,
+ RepositoryRoot: request.Invocation.InvokingPath, Snapshot: snapshot, Settings: extension.Manifest.Settings,
+ })
+ if err != nil {
+ return model.Observation{}, fmt.Errorf("extension %q observation failed: %w", extension.Identity.ID, err)
+ }
+ if err := validateExtensionResponse(extension, control.ExtensionObserveOperation, request.Invocation.Correlation, response); err != nil {
+ return model.Observation{}, err
+ }
+ declared := make(map[string]bool, len(extension.Manifest.Facts))
+ for _, id := range extension.Manifest.Facts {
+ declared[id] = true
+ }
+ if observation.ExtensionFacts == nil {
+ observation.ExtensionFacts = map[string]model.Fact[string]{}
+ }
+ for _, fact := range response.Facts {
+ if !declared[fact.ID] {
+ return model.Observation{}, fmt.Errorf("extension %q returned undeclared fact %q", extension.Identity.ID, fact.ID)
+ }
+ if _, exists := observation.ExtensionFacts[fact.ID]; exists {
+ return model.Observation{}, fmt.Errorf("extension fact %q was returned more than once", fact.ID)
+ }
+ if !fact.Status.Valid() || fact.Fingerprint == "" {
+ return model.Observation{}, fmt.Errorf("extension %q returned invalid fact %q", extension.Identity.ID, fact.ID)
+ }
+ observation.ExtensionFacts[fact.ID] = model.Fact[string]{
+ Status: fact.Status, Value: fact.Value, Detail: fact.Detail,
+ Evidence: []model.Evidence{{Source: "extension:" + extension.Identity.ID, Fingerprint: fact.Fingerprint, ObservedAt: observation.ObservedAt}},
+ }
+ delete(declared, fact.ID)
+ }
+ if len(declared) != 0 {
+ return model.Observation{}, fmt.Errorf("extension %q omitted required observed facts", extension.Identity.ID)
+ }
+ }
+ if request.VerifyTransitionID != "" {
+ transition, ok := o.program.RuntimeRegistry().Lookup(request.VerifyTransitionID)
+ if ok && transition.Origin.Kind == catalog.OriginPrimaryFlow && flow.Manifest.RuntimeMode == control.FlowRuntimeProtocol {
+ 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,
+ 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 {
+ return model.Observation{}, err
+ }
+ if response.Verified == nil || !*response.Verified {
+ return model.Observation{}, fmt.Errorf("primary-flow verifier %q rejected the postcondition", transition.Verifier)
+ }
+ }
+ if ok && transition.Origin.Kind == catalog.OriginExtension {
+ extension, exists := o.program.ExtensionByID(transition.Origin.ID)
+ if !exists || extension.Runtime == nil {
+ return model.Observation{}, fmt.Errorf("extension verifier %q is unavailable", transition.Verifier)
+ }
+ snapshot, err := json.Marshal(observation)
+ if err != nil {
+ return model.Observation{}, err
+ }
+ response, err := extension.Runtime.Invoke(ctx, control.ExtensionRequest{
+ ProtocolVersion: control.ExtensionProtocolVersion, Operation: control.ExtensionVerifyOperation,
+ ExtensionID: extension.Identity.ID, ExtensionVersion: extension.Identity.Version,
+ ProgramFingerprint: o.program.Fingerprint(), CorrelationID: request.Invocation.Correlation,
+ RepositoryRoot: request.Invocation.InvokingPath, TransitionID: transition.ID, Snapshot: snapshot, Settings: extension.Manifest.Settings,
+ })
+ if err != nil {
+ return model.Observation{}, err
+ }
+ if err := validateExtensionResponse(extension, control.ExtensionVerifyOperation, request.Invocation.Correlation, response); err != nil {
+ return model.Observation{}, err
+ }
+ if response.Verified == nil || !*response.Verified {
+ return model.Observation{}, fmt.Errorf("extension verifier %q rejected the postcondition", transition.Verifier)
+ }
+ }
+ }
+ 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)
+ }
+ 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 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 nil
+}
+
+func validateExtensionResponse(extension control.CompiledExtension, operation control.ExtensionOperation, correlation string, response control.ExtensionResponse) error {
+ if response.ProtocolVersion != control.ExtensionProtocolVersion || response.Operation != operation ||
+ response.ExtensionID != extension.Identity.ID || response.ExtensionVersion != extension.Identity.Version ||
+ response.CorrelationID != correlation {
+ return fmt.Errorf("extension %q returned a mismatched protocol response", extension.Identity.ID)
+ }
+ if err := control.ValidateExtensionOperationResponse(operation, response); err != nil {
+ return fmt.Errorf("extension %q returned an invalid operation response: %w", extension.Identity.ID, err)
+ }
+ if response.ErrorClass != "" || response.Error != "" {
+ return ComponentRuntimeError{Component: fmt.Sprintf("extension %q", extension.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
new file mode 100644
index 0000000..75e1461
--- /dev/null
+++ b/boatstack/program_observer_test.go
@@ -0,0 +1,140 @@
+package boatstack
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/flow/standard"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
+ "github.com/operatorstack/boatstack/boatstack/internal/kernel/ports"
+)
+
+type fixedObservation struct{ value model.Observation }
+
+func (o fixedObservation) Observe(context.Context, ports.ObservationRequest) (model.Observation, error) {
+ return o.value, nil
+}
+
+func TestRuntimeErrorPreservesBoundedClassAndMessage(t *testing.T) {
+ extension := control.CompiledExtension{
+ Identity: control.ComponentIdentity{ID: "example.runtime", Version: "1.0.0"},
+ }
+ err := validateExtensionResponse(extension, control.ExtensionObserveOperation, "correlation", control.ExtensionResponse{
+ ProtocolVersion: control.ExtensionProtocolVersion, Operation: control.ExtensionObserveOperation,
+ ExtensionID: "example.runtime", ExtensionVersion: "1.0.0", CorrelationID: "correlation",
+ ErrorClass: "temporary", Error: "provider response was incomplete",
+ })
+ var runtimeErr ComponentRuntimeError
+ if !errors.As(err, &runtimeErr) || runtimeErr.Class != "temporary" || runtimeErr.Message != "provider response was incomplete" {
+ t.Fatalf("runtime error detail was lost: %#v (%v)", runtimeErr, err)
+ }
+}
+
+type isolatedObservationExtension struct {
+ id string
+ forbid string
+ sawFact *bool
+ executable bool
+ calls *int
+}
+
+func (e isolatedObservationExtension) ExtensionManifest(context.Context) (control.ExtensionManifest, error) {
+ manifest := control.ExtensionManifest{
+ ID: e.id, Version: "1.0.0", ProtocolVersion: control.ExtensionProtocolVersion,
+ SettingsSchema: json.RawMessage(`{"type":"object"}`), Facts: []string{e.id + ".fact"},
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
+ }
+ if e.executable {
+ manifest.ExecutableSHA256 = strings.Repeat("a", 64)
+ }
+ return manifest, nil
+}
+
+func (e isolatedObservationExtension) Runtime() control.ExtensionRuntime { return e }
+
+func (e isolatedObservationExtension) Invoke(_ context.Context, request control.ExtensionRequest) (control.ExtensionResponse, error) {
+ if e.calls != nil {
+ *e.calls++
+ }
+ var projection struct {
+ ExtensionFacts map[string]json.RawMessage `json:"extension_facts"`
+ }
+ if err := json.Unmarshal(request.Snapshot, &projection); err != nil {
+ return control.ExtensionResponse{}, err
+ }
+ _, *e.sawFact = projection.ExtensionFacts[e.forbid]
+ return control.ExtensionResponse{
+ ProtocolVersion: control.ExtensionProtocolVersion, Operation: request.Operation,
+ ExtensionID: e.id, ExtensionVersion: "1.0.0", CorrelationID: request.CorrelationID,
+ Facts: []control.ExtensionFact{{ID: e.id + ".fact", Status: control.FactKnown, Value: "observed", Fingerprint: e.id + "-fingerprint"}},
+ }, nil
+}
+
+func TestExecutableExtensionObservationWaitsForVerifiedProgramBinding(t *testing.T) {
+ // control-law: repository-selected-code-is-inert-before-exact-program-admission
+ var calls int
+ 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},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ invocation := model.InvocationContext{Correlation: "pre-admission-gate"}
+ base := model.Observation{
+ Invocation: invocation, ObservedAt: time.Unix(100, 0).UTC(),
+ Configuration: model.Known(model.ConfigurationVerified, model.Evidence{Source: "test", Fingerprint: "configuration", ObservedAt: time.Unix(100, 0).UTC()}),
+ }
+ observer := programObserver{base: fixedObservation{value: base}, program: program}
+ observed, err := observer.Observe(context.Background(), ports.ObservationRequest{Invocation: invocation})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if calls != 0 || len(observed.ExtensionFacts) != 0 {
+ t.Fatalf("unbound program executed extension: calls=%d facts=%#v", calls, observed.ExtensionFacts)
+ }
+ base.RecordedProgramFingerprint = program.Fingerprint()
+ observer.base = fixedObservation{value: base}
+ observed, err = observer.Observe(context.Background(), ports.ObservationRequest{Invocation: invocation})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if calls != 1 || observed.ExtensionFacts["example.external.fact"].Value != "observed" {
+ t.Fatalf("verified binding did not execute extension once: calls=%d facts=%#v", calls, observed.ExtensionFacts)
+ }
+}
+
+func TestExtensionObserversConsumeOneOrderIndependentProjection(t *testing.T) {
+ // control-law: extension-observation-order-cannot-create-cross-extension-facts
+ var alphaSawBeta, betaSawAlpha bool
+ 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(),
+ Extensions: []control.Extension{beta, alpha},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ invocation := model.InvocationContext{Correlation: "observation-order"}
+ observed, err := (programObserver{
+ base: fixedObservation{value: model.Observation{Invocation: invocation, ObservedAt: time.Unix(100, 0).UTC()}},
+ program: program,
+ }).Observe(context.Background(), ports.ObservationRequest{Invocation: invocation})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if alphaSawBeta || betaSawAlpha {
+ t.Fatalf("extension observer consumed another extension's invocation-order fact: alpha=%v beta=%v", alphaSawBeta, betaSawAlpha)
+ }
+ if len(observed.ExtensionFacts) != 2 || observed.ExtensionFacts["example.alpha.fact"].Value != "observed" || observed.ExtensionFacts["example.beta.fact"].Value != "observed" {
+ t.Fatalf("final layered observation lost extension facts: %#v", observed.ExtensionFacts)
+ }
+}
diff --git a/boatstack/references/config-schema.md b/boatstack/references/config-schema.md
index 5d88cd6..a718442 100644
--- a/boatstack/references/config-schema.md
+++ b/boatstack/references/config-schema.md
@@ -5,9 +5,17 @@ normative Go decoder is
`internal/kernel/protocol.DecodeProjectConfig`; the public example is
`project.example.json`.
-Top-level keys are `schema_version`, `project`, `policy`, and `hosts`.
+Top-level keys are `schema_version`, `project`, `policy`, `hosts`, and optional
+`extensions`.
Unknown keys and trailing JSON fail. Hosts are selected from `claude`, `cli`,
-`codex`, `cursor`, `gemini`, and `mcp`; `cli` is mandatory.
+`codex`, `cursor`, `gemini`, `mcp`, and `sdk`; `cli` is mandatory.
+
+Each `extensions` item selects only an additive subprocess extension and
+requires a semantic ID, exact version, symlink-free absolute executable path,
+exact SHA-256, optional strict JSON settings, and optional bounded deadline,
+stdout, and stderr limits. Repository configuration cannot replace the primary
+flow. A subprocess extension is a trusted executable boundary, not an OS
+sandbox.
Configuration changes use `configuration.mutate` with `config_path` and
`config_sha256`. That fingerprint is the SHA-256 of the strict decoded schema-2
diff --git a/boatstack/sdk/sdk.go b/boatstack/sdk/sdk.go
index f4c8ad3..cbc2522 100644
--- a/boatstack/sdk/sdk.go
+++ b/boatstack/sdk/sdk.go
@@ -1,11 +1,15 @@
-// Package sdk exposes the versioned Boatstack V2 surface protocol without
+// Package sdk exposes the versioned Boatstack surface protocol without
// exposing or duplicating the internal controller implementation.
package sdk
import (
"context"
+ "fmt"
boatstack "github.com/operatorstack/boatstack/boatstack"
+ "github.com/operatorstack/boatstack/boatstack/control"
+ "github.com/operatorstack/boatstack/boatstack/core"
+ "github.com/operatorstack/boatstack/boatstack/distribution"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
@@ -71,18 +75,131 @@ const (
DecisionUnresolved = supervisor.DecisionUnresolved
)
-// Client is the only supported in-process V2 entry point. It delegates every
-// decision and effect to the same kernel used by the CLI.
-type Client struct{ kernel boatstack.V2Kernel }
+const HostIdentity = "sdk"
-func New(externalStateRoot string) (Client, error) {
- kernel, err := boatstack.NewV2Kernel(externalStateRoot)
+type options struct {
+ flow control.FlowDefinition
+ extensions []control.Extension
+}
+
+type Option func(*options) error
+
+func WithFlow(flow control.FlowDefinition) Option {
+ return func(configuration *options) error {
+ if flow == nil {
+ return fmt.Errorf("SDK flow cannot be nil")
+ }
+ if configuration.flow != nil {
+ return fmt.Errorf("SDK accepts exactly one PrimaryFlow")
+ }
+ configuration.flow = flow
+ return nil
+ }
+}
+
+func WithExtension(extension control.Extension) Option {
+ return func(configuration *options) error {
+ if extension == nil {
+ return fmt.Errorf("SDK extension cannot be nil")
+ }
+ configuration.extensions = append(configuration.extensions, extension)
+ return nil
+ }
+}
+
+// Client is an immutable program factory plus the canonical request boundary.
+// It compiles one repository-scoped ControlProgram per request, allowing a
+// single Client to serve concurrent repositories with different extensions.
+type Client struct {
+ externalStateRoot string
+ standard bool
+ flow control.FlowDefinition
+ extensions []control.Extension
+}
+
+// New assembles the standard Boatstack distribution. Options may add
+// extensions but cannot replace StandardFlow.
+func New(externalStateRoot string, supplied ...Option) (Client, error) {
+ configuration, err := applyOptions(supplied)
if err != nil {
return Client{}, err
}
- return Client{kernel: kernel}, nil
+ if configuration.flow != 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 {
+ return Client{}, err
+ }
+ return Client{externalStateRoot: externalStateRoot, standard: true, extensions: append([]control.Extension(nil), configuration.extensions...)}, nil
+}
+
+// NewKernel is the low-level composition API. It never inserts StandardFlow;
+// callers must supply exactly one WithFlow 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 _, err := control.Compile(context.Background(), control.CompileRequest{
+ KernelVersion: boatstack.Version, Core: core.System(), Flow: configuration.flow,
+ Extensions: configuration.extensions,
+ }); err != nil {
+ return Client{}, err
+ }
+ return Client{externalStateRoot: externalStateRoot, flow: configuration.flow, extensions: append([]control.Extension(nil), configuration.extensions...)}, nil
+}
+
+func applyOptions(supplied []Option) (options, error) {
+ var configuration options
+ for _, option := range supplied {
+ if option == nil {
+ return options{}, fmt.Errorf("SDK option cannot be nil")
+ }
+ if err := option(&configuration); err != nil {
+ return options{}, err
+ }
+ }
+ return configuration, nil
}
func (c Client) Do(ctx context.Context, request Request) (Response, error) {
- return c.kernel.Handle(ctx, request)
+ if request.Host == "" {
+ request.Host = HostIdentity
+ }
+ repositoryRequest := distribution.RepositoryProgramRequest{
+ Repository: request.Repository, ExternalStateRoot: c.externalStateRoot, Host: request.Host,
+ CorrelationID: request.CorrelationID, Extensions: c.extensions,
+ }
+ if request.TransitionID == "installation.initialize" || request.TransitionID == "configuration.initialize" {
+ repositoryRequest.ConfigurationPath, _ = request.Parameters.Get("config_path")
+ repositoryRequest.ConfigurationFingerprint, _ = request.Parameters.Get("config_sha256")
+ }
+ var program control.ControlProgram
+ var err error
+ if c.standard {
+ program, err = distribution.StandardProgramForRepository(ctx, repositoryRequest)
+ } else {
+ var configured []control.Extension
+ var settings any
+ configured, settings, err = distribution.ConfiguredExtensions(ctx, repositoryRequest)
+ if err == nil {
+ 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,
+ Extensions: extensions, Settings: settings,
+ })
+ }
+ }
+ if err != nil {
+ return Response{}, err
+ }
+ kernel, err := boatstack.NewKernel(c.externalStateRoot, program)
+ if err != nil {
+ return Response{}, err
+ }
+ return kernel.Handle(ctx, request)
}
diff --git a/boatstack/sdk/sdk_test.go b/boatstack/sdk/sdk_test.go
index 610acbf..15b41ca 100644
--- a/boatstack/sdk/sdk_test.go
+++ b/boatstack/sdk/sdk_test.go
@@ -1,8 +1,11 @@
package sdk_test
import (
+ "context"
+ "encoding/json"
"testing"
+ "github.com/operatorstack/boatstack/boatstack/control"
"github.com/operatorstack/boatstack/boatstack/sdk"
)
@@ -19,3 +22,74 @@ func TestPublicProtocolCanBeConstructedWithoutInternalPackages(t *testing.T) {
t.Fatalf("public V2 aliases lost protocol identity: %#v", request)
}
}
+
+func TestLowLevelSDKRequiresAndAcceptsExactlyOneNonStandardPrimaryFlow(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")
+ }
+ flow := syntheticFlow{}
+ if _, err := sdk.NewKernel("", sdk.WithFlow(flow)); err != nil {
+ t.Fatalf("synthetic PrimaryFlow 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.New("", sdk.WithFlow(flow)); err == nil {
+ t.Fatal("standard SDK allowed StandardFlow replacement")
+ }
+}
+
+type syntheticFlow struct{}
+
+func (syntheticFlow) FlowRuntime() control.FlowRuntime { return syntheticRuntime{} }
+
+func (syntheticFlow) FlowManifest(context.Context) (control.PrimaryFlowManifest, error) {
+ const (
+ id = "synthetic.lifecycle"
+ fact = "synthetic.lifecycle.stage"
+ resource = "synthetic.lifecycle.state"
+ )
+ transition := func(id control.TransitionID, source, target string, priority int) control.Transition {
+ effect := control.EffectID(string(id) + "-effect")
+ verifier := string(id) + "-verifier"
+ return control.Transition{
+ ID: id, Version: 1, SelectionClass: control.SelectionFlowProgress, 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},
+ OwnedResources: []string{resource}, Effect: effect, LocalEffects: []control.EffectID{effect}, Idempotent: true,
+ Prescription: control.Prescription{Operation: string(id), ExpectedPostcondition: target},
+ SourcePredicate: "synthetic-source", AdmissionPredicate: "exact-admission", TargetPredicate: "synthetic-target",
+ SourceConditions: []control.FacetCondition{control.KnownCondition(control.FacetName(fact), source)},
+ TargetConditions: []control.FacetCondition{control.KnownCondition(control.FacetName(fact), target)}, Verifier: verifier,
+ Interruption: control.InterruptionContract{
+ Points: []string{"after-effect"}, PartialState: []string{"namespaced-flow-state"}, Detection: "fresh-flow-observation",
+ ResumeContract: "re-observe", RollbackContract: "restore-prior-bytes", CompensationContract: "not-required",
+ Recovery: "recovery.escalate", RecoveryAuthority: "repository-policy", ResumptionPredicate: "fresh-flow-fact",
+ },
+ Reversibility: control.Reversible, TerminalEffect: "compiled-goal-contract", PrivacyClassification: "metadata-only",
+ TelemetryClassification: "transition-receipt", CostClass: "synthetic", Priority: priority,
+ }
+ }
+ 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,
+ 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},
+ Effects: []string{string(verify.Effect), string(finish.Effect)}, Verifiers: []string{verify.Verifier, finish.Verifier},
+ ConfigurationSchema: json.RawMessage(`{"type":"object"}`),
+ PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
+ }, nil
+}
+
+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,
+ }, nil
+}
diff --git a/docs/architecture/boatstack-standard-flow.mmd b/docs/architecture/boatstack-standard-flow.mmd
new file mode 100644
index 0000000..bb0f5ee
--- /dev/null
+++ b/docs/architecture/boatstack-standard-flow.mmd
@@ -0,0 +1,162 @@
+%% Generated from compiled PrimaryFlow declarations by surfaces.RenderStandardFlowMermaid. Do not edit.
+flowchart TB
+ subgraph phases["protocol phases"]
+ p_ABANDONED["ABANDONED"]
+ p_ACTIVE["ACTIVE"]
+ p_DORMANT["DORMANT"]
+ p_FRONTIER["FRONTIER"]
+ p_OBSERVED["OBSERVED"]
+ p_RECOVERY["RECOVERY"]
+ p_TERMINAL["TERMINAL"]
+ p_UNRESOLVED["UNRESOLVED"]
+ end
+ subgraph authority["authority"]
+ t00["evidence.approval.revoke
authority"]
+ t01["plan.abandon
authority"]
+ t02["plan.approve
authority"]
+ t03["plan.approve-amendment
authority"]
+ t04["publication.abandon
authority"]
+ end
+ subgraph owned_local["owned-local"]
+ t05["delivery.slice.advance
owned-local"]
+ t06["evidence.visual.attach
owned-local"]
+ t07["gate.build.record
owned-local"]
+ t08["gate.change.record
owned-local"]
+ t09["gate.journey.record
owned-local"]
+ t10["gate.review.record
owned-local"]
+ t11["gate.test.record
owned-local"]
+ t12["plan.activate
owned-local"]
+ t13["plan.amend
owned-local"]
+ t14["plan.create
owned-local"]
+ t15["plan.invalidate
owned-local"]
+ t16["plan.validate
owned-local"]
+ t17["publication.observe
owned-local"]
+ t18["publication.preview
owned-local"]
+ t19["workspace.abandon
owned-local"]
+ t20["workspace.activate
owned-local"]
+ t21["workspace.cleanup
owned-local"]
+ t22["workspace.cut
owned-local"]
+ t23["workspace.publish
owned-local"]
+ t24["workspace.reap
owned-local"]
+ t25["workspace.sync
owned-local"]
+ end
+ subgraph owned_external["owned-external"]
+ t26["publication.correct
owned-external"]
+ t27["publication.execute
owned-external"]
+ end
+ subgraph recovery["recovery"]
+ t28["publication.reconcile
recovery"]
+ t29["workspace.reconcile
recovery"]
+ end
+ subgraph observed_external["observed-external"]
+ end
+ p_ACTIVE --> t00
+ p_FRONTIER --> t00
+ t00 --> p_FRONTIER
+ p_OBSERVED --> t01
+ p_ACTIVE --> t01
+ p_FRONTIER --> t01
+ t01 --> p_ABANDONED
+ p_ACTIVE --> t02
+ p_FRONTIER --> t02
+ t02 --> p_ACTIVE
+ t02 --> p_TERMINAL
+ p_ACTIVE --> t03
+ p_FRONTIER --> t03
+ t03 --> p_ACTIVE
+ p_ACTIVE --> t04
+ p_FRONTIER --> t04
+ t04 --> p_ABANDONED
+ p_ACTIVE --> t05
+ t05 --> p_ACTIVE
+ t05 --> p_TERMINAL
+ p_ACTIVE --> t06
+ t06 --> p_ACTIVE
+ t06 --> p_TERMINAL
+ p_ACTIVE --> t07
+ t07 --> p_ACTIVE
+ p_ACTIVE --> t08
+ t08 --> p_ACTIVE
+ p_ACTIVE --> t09
+ t09 --> p_ACTIVE
+ p_ACTIVE --> t10
+ t10 --> p_ACTIVE
+ t10 --> p_TERMINAL
+ p_ACTIVE --> t11
+ t11 --> p_ACTIVE
+ t11 --> p_TERMINAL
+ p_OBSERVED --> t12
+ p_ACTIVE --> t12
+ t12 --> p_ACTIVE
+ p_ACTIVE --> t13
+ p_FRONTIER --> t13
+ t13 --> p_ACTIVE
+ p_OBSERVED --> t14
+ p_ACTIVE --> t14
+ t14 --> p_ACTIVE
+ p_ACTIVE --> t15
+ p_OBSERVED --> t15
+ t15 --> p_FRONTIER
+ p_OBSERVED --> t16
+ p_ACTIVE --> t16
+ t16 --> p_ACTIVE
+ t16 --> p_FRONTIER
+ p_OBSERVED --> t17
+ p_ACTIVE --> t17
+ p_RECOVERY --> t17
+ p_UNRESOLVED --> t17
+ t17 --> p_ACTIVE
+ t17 --> p_TERMINAL
+ t17 --> p_FRONTIER
+ t17 --> p_UNRESOLVED
+ p_ACTIVE --> t18
+ t18 --> p_ACTIVE
+ p_ACTIVE --> t19
+ p_FRONTIER --> t19
+ t19 --> p_ABANDONED
+ p_OBSERVED --> t20
+ p_ACTIVE --> t20
+ t20 --> p_ACTIVE
+ p_OBSERVED --> t21
+ p_ACTIVE --> t21
+ p_TERMINAL --> t21
+ p_ABANDONED --> t21
+ t21 --> p_OBSERVED
+ t21 --> p_TERMINAL
+ t21 --> p_ABANDONED
+ p_OBSERVED --> t22
+ p_ACTIVE --> t22
+ t22 --> p_ACTIVE
+ p_ACTIVE --> t23
+ t23 --> p_ACTIVE
+ p_OBSERVED --> t24
+ p_TERMINAL --> t24
+ p_ABANDONED --> t24
+ t24 --> p_OBSERVED
+ t24 --> p_TERMINAL
+ t24 --> p_ABANDONED
+ p_ACTIVE --> t25
+ t25 --> p_ACTIVE
+ t25 --> p_FRONTIER
+ p_OBSERVED --> t26
+ p_ACTIVE --> t26
+ p_TERMINAL --> t26
+ t26 --> p_ACTIVE
+ t26 --> p_RECOVERY
+ p_ACTIVE --> t27
+ t27 --> p_ACTIVE
+ t27 --> p_RECOVERY
+ p_RECOVERY --> t28
+ p_UNRESOLVED --> t28
+ t28 --> p_ACTIVE
+ t28 --> p_TERMINAL
+ t28 --> p_FRONTIER
+ t28 --> p_UNRESOLVED
+ p_RECOVERY --> t29
+ p_UNRESOLVED --> t29
+ t29 --> p_DORMANT
+ t29 --> p_OBSERVED
+ t29 --> p_ACTIVE
+ t29 --> p_FRONTIER
+ t29 --> p_TERMINAL
+ t29 --> p_ABANDONED
diff --git a/docs/architecture/boatstack-v2-closure-report.md b/docs/architecture/boatstack-v2-closure-report.md
index 851d8b0..51abb65 100644
--- a/docs/architecture/boatstack-v2-closure-report.md
+++ b/docs/architecture/boatstack-v2-closure-report.md
@@ -1,5 +1,9 @@
# Boatstack V2 replacement closure
+> Historical replacement evidence for PR #186. The normative current
+> architecture and executable counts are defined by
+> [Boatstack programmable delivery control architecture](boatstack-v2-kernel.md).
+
Base revision: `c5b5e10cdcf4d97b645d705cb164e762acf93ff1`
Replacement mode: flag day; no V1 compatibility or state migration
diff --git a/docs/architecture/boatstack-v2-kernel.md b/docs/architecture/boatstack-v2-kernel.md
index e1b776d..ba775f1 100644
--- a/docs/architecture/boatstack-v2-kernel.md
+++ b/docs/architecture/boatstack-v2-kernel.md
@@ -1,14 +1,16 @@
-# Boatstack V2 authoritative delivery kernel
+# Boatstack programmable delivery control architecture
Status: normative implementation specification
-Base revision: `c5b5e10cdcf4d97b645d705cb164e762acf93ff1` (`origin/main`, including PR #185)
-Rewrite branch: `rewrite/v2-delivery-kernel`
-Scope: one flag-day rewrite and one final pull request; no merge is authorized by this document
+Base revision: `f7a5c9d1f2d15057f484371f348ee57311c0155e` (`origin/main`, after the V2 kernel replacement)
+Implementation branch: `feat/control-program-and-standard-flow`
+Scope: separate mechanism, system capabilities, primary delivery flow, optional
+extensions, and product surfaces in one final pull request; no merge is
+authorized by this document
> Boatstack V2 is a flag-day replacement. Existing machine-local state may be
> discarded and regenerated. No V1 runtime remains after cutover.
-This document is the source of truth for the V2 implementation. If code and this
+This document is the source of truth for the Boatstack implementation. If code and this
document disagree, the discrepancy is a release blocker: either the code must be
corrected or this document must be deliberately amended with matching tests.
The [replacement closure report](boatstack-v2-closure-report.md) binds its frozen
@@ -21,11 +23,11 @@ slices. They are logical ownership boundaries, not rollout phases.
| Slice | Domain | Structure | Goal | Operator | Immediate value |
| --- | --- | --- | --- | --- | --- |
-| 1. Authoritative kernel | Repository-local delivery control | One evidence-backed composite snapshot and one transition catalog | Every managed state has a safe path to progress, recovery, authority frontier, or terminal | Observe, resolve, admit, execute, verify, record, recover | One executable delivery law replaces distributed state and authority reconstruction |
-| 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 semantics | Decode, invoke, render | Hosts stop acting as independent controllers while useful workflows remain available |
+| 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 |
+| 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 `Snapshot` schema, the configured
-`Goal`, and the `Engine.Apply` operator. Canonical form for slice 2: one domain,
+Canonical form for slice 1: one domain, the `ControlProgram` and `Snapshot`
+schemas, the configured `Goal`, and the `Kernel.Handle` operator. Canonical form for slice 2: one domain,
the `SurfaceRequest`/`SurfaceResponse` schema, the same goal, and the adapter
projection operator.
@@ -44,7 +46,219 @@ which external outcomes can be proved, and which platform primitive provides
atomic replacement. A user decision is required only if a new transition would
change who may authorize an effect or what counts as a delivery terminal.
-## 1. Product contract
+Value emerges at the compilation boundary: the smallest valuable change is not
+a second workflow engine, but one deterministic program that preserves the V2
+effect protocol while moving delivery policy out of the mechanism. The two
+jointly shipped slices are therefore (1) program compilation and Kernel
+execution, and (2) standard distribution and surface projection.
+
+## 1. Program architecture
+
+Boatstack is a programmable supervisory control runtime for software delivery,
+with a first-party standard delivery flow. Its dependency direction is:
+
+```text
+kernel contracts
+ ^
+ |-- CoreSystem
+ |-- one PrimaryFlow
+ `-- zero or more Extensions
+ ^
+ |
+ distribution assembly
+ ^
+ |
+ SDK / CLI / hosts
+```
+
+The application assembles one immutable program before resolution:
+
+```text
+CoreSystem + PrimaryFlow + Extensions + RepositoryPolicy
+ -> Compile
+ -> ControlProgram
+ -> Kernel
+ -> observe -> resolve -> admit -> execute -> verify -> receipt -> recover
+```
+
+### Ownership
+
+- **Kernel** is the stable deterministic mechanism. It accepts an explicit
+ 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.
+- **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
+ 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
+ plan, approval, workspace, gate, evidence, publication, correction, and
+ abandonment behavior.
+- **Extensions** are additive. In-process extensions are trusted compiled Go
+ capabilities constrained by the compiler. Subprocess extensions are trusted
+ executable boundaries using a strict bounded JSON protocol; they are not OS
+ sandboxes. Extensions may add namespaced facts, resources, transitions,
+ recovery, and conjunctive goal obligations, but may not replace the flow,
+ weaken a goal contract, or mutate another owner's state.
+- **Surfaces** assemble or invoke a program and render typed results. They do
+ not decide lifecycle, terminal state, authority, or recovery.
+
+### ControlProgram
+
+`Compile` consumes an explicit CoreSystem definition, one PrimaryFlow 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,
+missing recovery contracts, dependency cycles, and goal constraints that are
+not conservative.
+
+The result is immutable and contains one transition registry, one goal-contract
+set, one resource-ownership map, compiled handlers, origin metadata, and one
+content fingerprint. The registry is the only runtime graph. There is no core,
+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 Extension interface {
+ ExtensionManifest(context.Context) (ExtensionManifest, error)
+}
+
+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
+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.
+
+The fingerprint covers the Kernel version; CoreSystem ID, version, manifest,
+and transitions; PrimaryFlow 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
+program-affecting repository policy. In this version that repository projection
+is exactly the checksum-bound extension composition; approval, host, visual,
+and risk policy remain controlling snapshot facts rather than catalog identity.
+The fingerprint is bound into snapshots, admissions,
+flow records, transition receipts, recovery journals, and telemetry. Once a
+flow admits its first transition, a different fingerprint is program drift and
+must fail closed until explicit reconciliation.
+
+### Compiled transition ownership
+
+The current 62-event Standard distribution is classified from compiled
+component declarations:
+
+| Owner | Families | Count |
+| --- | --- | ---: |
+| CoreSystem | `engagement.*`, `invocation.*`, `repository.*`, `runtime.*`, `configuration.*`, `installation.*`, `catalog.*`, `goal.*`, `recovery.*`, `external.*` | 32 |
+| StandardFlow | `plan.*`, `workspace.*`, `gate.*`, `evidence.*`, `delivery.*`, `publication.*` | 30 |
+| Extensions in the default distribution | none | 0 |
+| **Compiled total** | one registry | **62** |
+
+The CoreSystem ownership of `external.*` declares the event vocabulary and
+observation boundary; StandardFlow consumes the bounded publication and
+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
+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
+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
+flow; cross-component interruption references are resolved only after the one
+compiled registry exists.
+
+Command classification is similarly policy-neutral. A host classifier emits a
+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 base terminal contract. Extension obligations are conjoined with that
+contract, so for the same base state:
+
+```text
+Terminal(StandardFlow + Extension) subseteq Terminal(StandardFlow)
+```
+
+Only the Kernel evaluates the compiled terminal contract. A flow or extension
+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
+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
+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
+data, and their exact symlink-free executable path and SHA-256 are revalidated
+before every invocation.
+
+The compiled resource map assigns every mutable resource exactly one owner.
+Effect routing rejects undeclared effects, handlers, and writes before any
+mutation. StandardFlow and extension effects still pass through the same exact
+admission, journal, verification, recovery, and Kernel-written receipt path.
+
+### Standard and custom distributions
+
+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
+assemble `CoreSystem + another trusted flow + selected extensions` without
+forking Kernel and without parsing CLI output.
+
+```go
+standardClient, err := sdk.New(stateRoot, sdk.WithExtension(extension))
+customClient, err := sdk.NewKernel(
+ stateRoot,
+ sdk.WithFlow(primaryFlow),
+ sdk.WithExtension(extension),
+)
+```
+
+The first form always selects StandardFlow. The second form never inserts it.
+Both clients compile the repository-scoped program and delegate every request
+through `Client.Do`.
+
+The deterministic Kernel test target uses synthetic facts, flows, clocks,
+effects, journals, receipts, and verifiers. One unrelated `START -> VERIFY ->
+TERMINAL` flow proves that Kernel has no dependency on plan, workspace, PR, or
+publication semantics. StandardFlow parity, extension conformance, surface
+parity, and platform integration are separate test layers.
+
+## 2. Product contract
Boatstack is a repository-local supervisory controller for software delivery by
humans and coding agents. The agent writes software. Boatstack deterministically
@@ -275,13 +489,15 @@ gain event authority merely because they are commands.
## 7. Transition registry
-The initial V2 catalog contains **61 semantic events**. This count is generated
-from code and must remain synchronized with this table.
+The compiled Standard distribution contains **62 semantic events**. This count
+is generated from CoreSystem and StandardFlow declaration bytes and must remain
+synchronized with this table.
| Family | Count | Required IDs |
| --- | ---: | --- |
| Invocation and engagement | 6 | `engagement.begin`, `engagement.renew`, `engagement.release`, `invocation.rebind`, `repository.attach`, `repository.detach` |
| Installation, runtime, configuration | 8 | `runtime.hydrate`, `runtime.replace`, `runtime.reconcile`, `configuration.initialize`, `configuration.mutate`, `configuration.reconcile`, `installation.initialize`, `installation.update` |
+| Catalog identity | 1 | `catalog.reconcile` |
| Goal and plan | 9 | `goal.configure`, `plan.create`, `plan.validate`, `plan.approve`, `plan.activate`, `plan.amend`, `plan.approve-amendment`, `plan.invalidate`, `plan.abandon` |
| Workspace | 8 | `workspace.cut`, `workspace.sync`, `workspace.activate`, `workspace.publish`, `workspace.cleanup`, `workspace.reap`, `workspace.abandon`, `workspace.reconcile` |
| Delivery gates and evidence | 8 | `gate.build.record`, `gate.test.record`, `gate.review.record`, `gate.change.record`, `gate.journey.record`, `evidence.visual.attach`, `evidence.approval.revoke`, `delivery.slice.advance` |
@@ -306,6 +522,9 @@ prescription. The registry is executable runtime authority, not a shadow model.
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;
+it is not an independently maintained graph.
## 8. Supervisory control law
@@ -470,17 +689,21 @@ 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 |
-| `internal/kernel/catalog` | 61 transition declarations and catalog invariants | read-only registry; uniqueness/completeness/reachability verifier | model | effects implementations, surfaces |
-| `internal/kernel/supervisor` | admissible-set and deterministic outcome law | pure `Resolve`; exhaustive reachable-state/property tests | model, catalog | I/O, effects, surfaces |
+| `control` | stable CoreSystem, PrimaryFlow, 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 |
+| `internal/kernel/catalog` | transition, registry, and goal-contract mechanism and invariants | read-only registry; uniqueness and recovery-reference validation | model | CoreSystem or StandardFlow declarations, effects, surfaces |
+| `internal/kernel/supervisor` | admissible-set and deterministic outcome law | pure `Resolve`; synthetic mechanism tests through the engine, with StandardFlow parity outside Kernel packages | model, catalog | I/O, effects, surfaces |
| `internal/kernel/protocol` | prescriptions, admission, receipts, recovery records | typed codecs and content identity verifier | model, catalog | concrete I/O and surfaces |
| `internal/kernel/durable` | strict machine-state and detached-binding codecs | canonical encode/decode and invariant validation | model, catalog | observation, effects, surfaces |
| `internal/kernel/ports` | observer, clock, lock, journal, local/external effect ports | compile-time narrow interfaces and fakes | model, protocol | concrete adapters |
-| `internal/kernel/reducer` | the sole durable lifecycle reduction for admitted controllable events | `Apply` plus exhaustive catalog coverage tests | model, catalog, durable, protocol | I/O, plant, surfaces |
| `internal/kernel/engine` | observe-resolve-admit-execute-reobserve-verify-record orchestration | `Resolve`, `Apply`, `Recover`; protocol/conformance tests | model, catalog, supervisor, protocol, ports | concrete surfaces and host logic |
| `internal/plant` | Git/worktree identity, layout, configuration, runtime, durable-state and journal observation | one read-only composite observer; fact/fingerprint fixtures | model, protocol, ports, durable codecs | engine decisions, mutating effects, surfaces |
-| `internal/effects` | transactions, local/external effect drivers and recovery | port implementations; fault-injection/postcondition tests | model, catalog, durable, protocol, ports, reducer, shared supervisor command classifier | surfaces and independent lifecycle reduction |
+| `internal/effects` | transactions, local/external effect drivers, trusted StandardFlow native state adapters, and recovery | port implementations; exhaustive admitted-reducer coverage; fault-injection/postcondition tests | model, catalog, durable, protocol, ports, shared supervisor command classifier | surfaces and any decision graph independent of the compiled registry |
| `internal/surfaces` | request decoding and decision/prescription rendering | CLI/hook/host/SDK/MCP adapter protocol; golden parity tests | model, protocol, engine facade interfaces | plant/effect implementations, lifecycle logic |
-| top-level `boatstack` | product construction and stable V2 facade | dependency injection and public operations; end-to-end tests | engine, plant, effects, surfaces | independent durable state or alternate decisions |
+| top-level `boatstack` | stable Kernel facade over one explicit ControlProgram | dependency injection and public operations; end-to-end tests | control, engine, plant, effects, surfaces | StandardFlow, distribution assembly, independent durable state or alternate decisions |
+| `distribution` | Standard distribution composition and repository-scoped extension assembly | `StandardProgram` and `StandardProgramForRepository` | CoreSystem, StandardFlow, verified extensions, control | mutable global program state |
| `cmd/boatstack-helper` | process startup and command parsing | parse -> facade request -> render; command tests | top-level facade/surfaces | direct plant writes or workflow decisions |
| `sdk` | public Go aliases and client | schema-2 request/response and one facade delegate | top-level facade and public aliases | internal decision or effect implementations |
| `analysis` | passive retrospective API | bounded deterministic report | `internal/retromine` | lifecycle decisions or managed writes |
@@ -609,9 +832,9 @@ Capability analysis records three separate dispositions without modifying Locus:
The executable registry now deterministically generates the checked
[safety model](boatstack-v2-locus-safety.json) and
-[liveness model](boatstack-v2-locus-liveness.json). Both contain exactly the 61
+[liveness model](boatstack-v2-locus-liveness.json). Both contain exactly the 62
runtime events. The liveness abstraction expands the declared phase predicates
-to 363 inferred stable-phase edges over eight reachable phases; the safety
+to 427 inferred stable-phase edges over eight reachable phases; the safety
model adds one guarded counterfactual edge and `UNADMITTED_EFFECT` state.
Repository and Go tests reject byte drift or an alphabet mismatch.
@@ -619,33 +842,31 @@ Observed Locus runs over those generated artifacts produced:
| Claim/operator | Postimplementation result | Disposition |
| --- | --- | --- |
-| `verification.safety-reachability` | `UNADMITTED_EFFECT` is unreachable; result `res-e90ff62f70169697c81e851f44fc4f423a0c26ca2d589f69b59b663200f4913e` | accepted finite-model result; advisory claim |
-| `verification.guard-essentiality` | `exact-admission` is essential; removing it admits `DORMANT --publication.execute--> UNADMITTED_EFFECT`; result `res-b095833a4f708aedec42f62f3c10ab9634ae102e88f31522ca4aa97f1d6c2481` | accepted finite-model result; advisory claim |
-| `control.nonblockingness` | all eight reachable stable phases are coreachable; no blocking states; result `res-f8ca50706925277190a6c0fba49a3ac44665d0a894203ea88c321434a7a4e549` | accepted finite-model result; advisory claim |
-| event-completeness obligation | source inventory, sole reducer, generated-model parity, and repository-contract refusing tests were accepted as complete | closes the declared event alphabet obligation |
-
-Safety/guard derivation
-`drv-bfc66012cb01e28ea3e1ef7407acf433d2340a166a14a4120a09a27fc77f1859`
-and liveness derivation
-`drv-a9534324fdc7e93fcfba3fd6919cbe9513a7dff9e97428f66f46b5489c39c9a4`
-remain advisory because the applicable Locus operators are currently candidate
-capabilities and because the model deliberately names real-system unknowns.
-The verified liveness frontier returned `current_claim: advisory`,
-`termination: blocked`, and no `EvidenceAction`; there is no honest additional
-action to invent inside V2.
-
-This closes the finite stable-phase abstraction, not the whole live system.
-The source-phase by target-phase expansion is conservative. Exact 17-facet
-predicates, reducer branches, operating-system interruption behavior, and
-external-provider truth are separately closed by executable unit, fault,
-integration, historical, repository, and platform tests.
+| `verification.trace-refinement` | the programmable ControlProgram protocol refines the preimplementation Kernel protocol with no distinguishing trace | accepted finite-model result; advisory claim |
+| `verification.conservative-feature-extension` | the reference release-note extension is conservative across all six checks with no violation | accepted bounded-extension result; advisory claim |
+| `verification.safety-reachability` | `UNADMITTED_EFFECT` is unreachable; result `res-6e8d6372eea4a7aaf2fcfa8ce5fcc91271b1f99a208f34006ba952d9825343a1` | accepted finite-model result; advisory claim |
+| `verification.guard-essentiality` | `exact-admission` is essential; removing it admits `DORMANT --publication.execute--> UNADMITTED_EFFECT`; result `res-10a40fec10fa47086e1e5ef83ee3434cffb461e30166961e4cc18d08812f2caa` | accepted finite-model result; advisory claim |
+| `control.nonblockingness` | all eight reachable stable phases are coreachable; no blocking states; result `res-a6a465d2e50cd830e0b95777f631ca9254d475757a6941c4d6922b2b7ff701f4` | accepted finite-model result; advisory claim |
+| `practice.zca-projection` | both shipped slices cover all nine declared facets and all 14 bounded Go-module sites | accepted source-bound projection; no runtime authority granted |
+| declared-slice completeness | every declared event-completeness obligation and the conservative-extension facet obligation were accepted as complete | closes the modeled source, writer, command, lifecycle, reducer, and generated-artifact inventories |
+
+The content-addressed Locus results and derivations are archived in Observatory.
+They remain advisory because each model deliberately names facts outside its
+bounded source slice rather than treating them as assumptions.
+
+This closes the finite stable-phase abstraction and the declared Go-module
+event surface, not every possible host integration. The source-phase by
+target-phase expansion is conservative. Exact 18-facet predicates, reducer
+branches, arbitrary third-party extension executables, fresh coding-host
+execution, operating-system interruption behavior, and external-provider truth
+remain executable integration evidence rather than whole-host formal proof.
## 18. Complete V2 replacement work order
This is one atomic branch and one final PR. The order controls build safety, not
rollout compatibility.
-1. Freeze this specification against base `c5b5e10...` and record historical
+1. Freeze this specification against base `f7a5c9d1f2d15057f484371f348ee57311c0155e` and record historical
fixtures.
2. Add slice 1 model, catalog, supervisor, protocol, ports, engine, generated
graph, and formal/property tests.
diff --git a/docs/architecture/boatstack-v2-locus-liveness.json b/docs/architecture/boatstack-v2-locus-liveness.json
index 4ca0ab6..37e8ced 100644
--- a/docs/architecture/boatstack-v2-locus-liveness.json
+++ b/docs/architecture/boatstack-v2-locus-liveness.json
@@ -1,11 +1,11 @@
{
"schema_version": 1,
"id": "boatstack-v2-executable-catalog-liveness-v1",
- "subject": "Finite stable-phase abstraction generated from the executable Boatstack V2 registry. It contains one event for every runtime catalog entry and expands each declared source and target phase set. The 17-facet predicates, operating-system behavior, and external-provider truth remain executable evidence obligations rather than theorem assumptions.",
+ "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": [
{
- "path": "boatstack/internal/kernel/catalog/default.go",
- "note": "Executable registry, exact transition count, event classes, phase predicates, authority, and materialization."
+ "path": "boatstack/control/control.go",
+ "note": "Compiler combines exact CoreSystem, PrimaryFlow, extension, contract, and ownership declarations into one immutable runtime registry."
},
{
"path": "docs/architecture/boatstack-v2-transition-catalog.md",
@@ -20,11 +20,11 @@
"note": "Single apply path across lock, journal, effect, fresh observation, target predicate, receipt, and recovery."
},
{
- "path": "boatstack/internal/kernel/reducer/reducer.go",
- "note": "Single executable reducer for every controllable semantic transition."
+ "path": "boatstack/internal/effects/state_reducer.go",
+ "note": "Admitted native effects reduce every controllable Standard distribution transition through one state adapter."
},
{
- "path": "boatstack/internal/kernel/catalog/completeness_test.go",
+ "path": "boatstack/flow/standard/completeness_test.go",
"note": "Runtime facet/event classification, writer-boundary inventory, and reducer-completeness refusing tests."
},
{
@@ -36,7 +36,7 @@
"note": "Staged effect ordering, atomic resource application, rollback, and external settlement boundary."
},
{
- "path": "boatstack/internal/kernel/catalog/historical_test.go",
+ "path": "boatstack/flow/standard/historical_test.go",
"note": "Historical incidents resolved through the executable runtime supervisor."
}
],
@@ -70,6 +70,12 @@
}
],
"events": [
+ {
+ "id": "catalog.reconcile",
+ "controllable": true,
+ "observable": true,
+ "basis": "observed"
+ },
{
"id": "configuration.initialize",
"controllable": true,
@@ -438,6 +444,710 @@
}
],
"transitions": [
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
{
"from": "OBSERVED",
"event": "configuration.initialize",
diff --git a/docs/architecture/boatstack-v2-locus-safety.json b/docs/architecture/boatstack-v2-locus-safety.json
index 32d4c0f..17989d4 100644
--- a/docs/architecture/boatstack-v2-locus-safety.json
+++ b/docs/architecture/boatstack-v2-locus-safety.json
@@ -1,11 +1,11 @@
{
"schema_version": 1,
"id": "boatstack-v2-executable-catalog-safety-v1",
- "subject": "Finite stable-phase abstraction generated from the executable Boatstack V2 registry. It contains one event for every runtime catalog entry and expands each declared source and target phase set. The 17-facet predicates, operating-system behavior, and external-provider truth remain executable evidence obligations rather than theorem assumptions.",
+ "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": [
{
- "path": "boatstack/internal/kernel/catalog/default.go",
- "note": "Executable registry, exact transition count, event classes, phase predicates, authority, and materialization."
+ "path": "boatstack/control/control.go",
+ "note": "Compiler combines exact CoreSystem, PrimaryFlow, extension, contract, and ownership declarations into one immutable runtime registry."
},
{
"path": "docs/architecture/boatstack-v2-transition-catalog.md",
@@ -20,11 +20,11 @@
"note": "Single apply path across lock, journal, effect, fresh observation, target predicate, receipt, and recovery."
},
{
- "path": "boatstack/internal/kernel/reducer/reducer.go",
- "note": "Single executable reducer for every controllable semantic transition."
+ "path": "boatstack/internal/effects/state_reducer.go",
+ "note": "Admitted native effects reduce every controllable Standard distribution transition through one state adapter."
},
{
- "path": "boatstack/internal/kernel/catalog/completeness_test.go",
+ "path": "boatstack/flow/standard/completeness_test.go",
"note": "Runtime facet/event classification, writer-boundary inventory, and reducer-completeness refusing tests."
},
{
@@ -36,7 +36,7 @@
"note": "Staged effect ordering, atomic resource application, rollback, and external settlement boundary."
},
{
- "path": "boatstack/internal/kernel/catalog/historical_test.go",
+ "path": "boatstack/flow/standard/historical_test.go",
"note": "Historical incidents resolved through the executable runtime supervisor."
}
],
@@ -73,6 +73,12 @@
}
],
"events": [
+ {
+ "id": "catalog.reconcile",
+ "controllable": true,
+ "observable": true,
+ "basis": "observed"
+ },
{
"id": "configuration.initialize",
"controllable": true,
@@ -441,6 +447,710 @@
}
],
"transitions": [
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "OBSERVED",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ACTIVE",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "RECOVERY",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "FRONTIER",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "UNRESOLVED",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "TERMINAL",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "DORMANT",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "RECOVERY",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "UNRESOLVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "TERMINAL",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "ABANDONED",
+ "event": "catalog.reconcile",
+ "to": "ABANDONED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
{
"from": "OBSERVED",
"event": "configuration.initialize",
diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md
index 505cd6c..6811882 100644
--- a/docs/architecture/boatstack-v2-transition-catalog.md
+++ b/docs/architecture/boatstack-v2-transition-catalog.md
@@ -1,72 +1,73 @@
-
-# Boatstack V2 executable transition catalog
+
+# Boatstack compiled transition catalog
-Registry size: **61** transitions. Event classes: authority 9; owned-local 30; owned-external 2; recovery 7; observed-external 13.
+Registry size: **62** transitions. Event classes: authority 9; owned-local 31; owned-external 2; recovery 7; observed-external 13.
-Controlling facets: `phase`, `topology`, `engagement`, `delivery`, `workspace`, `plan`, `configuration`, `configuration-policy`, `runtime`, `publication`, `verification`, `recovery`, `transaction`, `recovery-info`, `transaction-info`, `terminal`, `goal`.
+Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `workspace`, `plan`, `configuration`, `configuration-policy`, `runtime`, `publication`, `verification`, `recovery`, `transaction`, `recovery-info`, `transaction-info`, `terminal`, `goal`.
-| Transition | Class | Source phases | Target phases | Authority | Parameters | Owned resources | Recovery |
-|---|---|---|---|---|---|---|---|
-| `configuration.initialize` | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `config_path*`, `config_sha256*` | `configuration` | `configuration.reconcile` |
-| `configuration.mutate` | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `config_path*`, `config_sha256*` | `configuration` | `configuration.reconcile` |
-| `configuration.reconcile` | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `transaction_id*` | `configuration` | `recovery.escalate` |
-| `delivery.slice.advance` | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `slice_id*`, `source_revision*` | `delivery-state` | `recovery.resume` |
-| `engagement.begin` | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | - | `engagement` | `recovery.resume` |
-| `engagement.release` | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | - | `engagement` | `recovery.resume` |
-| `engagement.renew` | authority | ACTIVE | ACTIVE | repository-policy/autonomy | - | `engagement` | `recovery.resume` |
-| `evidence.approval.revoke` | authority | ACTIVE / FRONTIER | FRONTIER | human | - | `approval` | `recovery.resume` |
-| `evidence.visual.attach` | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `recovery.resume` |
-| `external.branch-changed` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `-` |
-| `external.ci-completed` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `-` |
-| `external.configuration-drifted` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | `-` |
-| `external.files-changed` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `-` |
-| `external.head-changed` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `-` |
-| `external.host-interrupted` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | `-` |
-| `external.lease-expired` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | `-` |
-| `external.pr-closed` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | `-` |
-| `external.pr-merged` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `-` |
-| `external.pr-opened` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `-` |
-| `external.pr-updated` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `-` |
-| `external.provider-unavailable` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | `-` |
-| `external.runtime-disappeared` | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | `-` |
-| `gate.build.record` | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `recovery.resume` |
-| `gate.change.record` | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `recovery.resume` |
-| `gate.journey.record` | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `recovery.resume` |
-| `gate.review.record` | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `recovery.resume` |
-| `gate.test.record` | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `recovery.resume` |
-| `goal.configure` | authority | OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `goal_kind*`, `delivery_id*` | `goal` | `recovery.resume` |
-| `installation.initialize` | owned-local | DORMANT / OBSERVED | OBSERVED | human | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `runtime.reconcile` |
-| `installation.update` | owned-local | OBSERVED / ACTIVE | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `installation` | `runtime.reconcile` |
-| `invocation.rebind` | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | - | `identity-binding` | `recovery.resume` |
-| `plan.abandon` | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | - | `plan` | `recovery.resume` |
-| `plan.activate` | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | - | `delivery-state` | `recovery.resume` |
-| `plan.amend` | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `recovery.resume` |
-| `plan.approve` | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `recovery.resume` |
-| `plan.approve-amendment` | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `recovery.resume` |
-| `plan.create` | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `recovery.resume` |
-| `plan.invalidate` | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | - | `plan-evidence` | `recovery.resume` |
-| `plan.validate` | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | - | `plan-evidence` | `recovery.resume` |
-| `publication.abandon` | authority | ACTIVE / FRONTIER | ABANDONED | human | - | `publication` | `recovery.resume` |
-| `publication.correct` | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `publication.reconcile` |
-| `publication.execute` | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `preview_fingerprint*` | `publication` | `publication.reconcile` |
-| `publication.observe` | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `publication_id*` | `publication-evidence` | `recovery.resume` |
-| `publication.preview` | owned-local | ACTIVE | ACTIVE | repository-policy | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `recovery.resume` |
-| `publication.reconcile` | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `publication_id*`, `transaction_id*` | `publication` | `recovery.escalate` |
-| `recovery.escalate` | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `transaction_id*` | `recovery-journal` | `recovery.escalate` |
-| `recovery.resume` | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `transaction_id*` | `recovery-journal` | `recovery.escalate` |
-| `recovery.rollback` | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `recovery-journal` | `recovery.escalate` |
-| `repository.attach` | owned-local | DORMANT / OBSERVED | OBSERVED | human | `topology*`, `config_authority*` | `repository-binding` | `recovery.resume` |
-| `repository.detach` | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | - | `repository-binding` | `recovery.resume` |
-| `runtime.hydrate` | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `runtime.reconcile` |
-| `runtime.reconcile` | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `recovery.escalate` |
-| `runtime.replace` | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `runtime.reconcile` |
-| `workspace.abandon` | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `branch*` | `workspace` | `recovery.resume` |
-| `workspace.activate` | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace` | `recovery.resume` |
-| `workspace.cleanup` | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `branch*` | `workspace` | `recovery.escalate` |
-| `workspace.cut` | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `branch*`, `base_ref*`, `destination*` | `workspace` | `workspace.reconcile` |
-| `workspace.publish` | owned-local | ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace-state` | `recovery.resume` |
-| `workspace.reap` | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `branch*` | `workspace` | `recovery.escalate` |
-| `workspace.reconcile` | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `workspace` | `recovery.escalate` |
-| `workspace.sync` | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `branch*` | `workspace` | `recovery.resume` |
+| Transition | Origin | Owner | Selection | Class | Source phases | Target phases | Authority | Parameters | Owned resources | Verifier | Recovery | Cost |
+|---|---|---|---|---|---|---|---|---|---|---|---|---|
+| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` |
+| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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` |
+| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` |
+| `external.files-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.files-changed` | `-` | `declared-neutral` |
+| `external.head-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.head-changed` | `-` | `declared-neutral` |
+| `external.host-interrupted` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | `verifier:fresh-observation:external.host-interrupted` | `-` | `declared-neutral` |
+| `external.lease-expired` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | `verifier:fresh-observation:external.lease-expired` | `-` | `declared-neutral` |
+| `external.pr-closed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | `verifier:fresh-observation:external.pr-closed` | `-` | `declared-neutral` |
+| `external.pr-merged` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-merged` | `-` | `declared-neutral` |
+| `external.pr-opened` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-opened` | `-` | `declared-neutral` |
+| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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` |
+| `goal.configure` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / 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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` |
+| `installation.update` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` |
+| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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` |
+| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `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` |
+| `repository.attach` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` |
+| `repository.detach` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` |
+| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` |
+| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` |
+| `workspace.abandon` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `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/boatstack-v2-transition-catalog.mmd b/docs/architecture/boatstack-v2-transition-catalog.mmd
index 9fff145..2dd67b2 100644
--- a/docs/architecture/boatstack-v2-transition-catalog.mmd
+++ b/docs/architecture/boatstack-v2-transition-catalog.mmd
@@ -1,73 +1,397 @@
-%% Generated from catalog.Default by surfaces.RenderCatalogMermaid. Do not edit.
+%% Generated from the compiled ControlProgram registry by surfaces.RenderCatalogMermaid. Do not edit.
flowchart TB
+ subgraph phases["protocol phases"]
+ p_ABANDONED["ABANDONED"]
+ p_ACTIVE["ACTIVE"]
+ p_DORMANT["DORMANT"]
+ p_FRONTIER["FRONTIER"]
+ p_OBSERVED["OBSERVED"]
+ p_RECOVERY["RECOVERY"]
+ p_TERMINAL["TERMINAL"]
+ p_UNRESOLVED["UNRESOLVED"]
+ end
subgraph authority["authority"]
- t00["engagement.begin
DORMANT | OBSERVED → OBSERVED | ACTIVE"]
- t01["engagement.release
ACTIVE | FRONTIER → DORMANT"]
- t02["engagement.renew
ACTIVE → ACTIVE"]
- t03["evidence.approval.revoke
ACTIVE | FRONTIER → FRONTIER"]
- t04["goal.configure
OBSERVED | ACTIVE | FRONTIER | TERMINAL | ABANDONED → OBSERVED | ACTIVE | FRONTIER"]
- t05["plan.abandon
OBSERVED | ACTIVE | FRONTIER → ABANDONED"]
- t06["plan.approve
ACTIVE | FRONTIER → ACTIVE | TERMINAL"]
- t07["plan.approve-amendment
ACTIVE | FRONTIER → ACTIVE"]
- t08["publication.abandon
ACTIVE | FRONTIER → ABANDONED"]
+ t00["engagement.begin
authority"]
+ t01["engagement.release
authority"]
+ t02["engagement.renew
authority"]
+ t03["evidence.approval.revoke
authority"]
+ t04["goal.configure
authority"]
+ t05["plan.abandon
authority"]
+ t06["plan.approve
authority"]
+ t07["plan.approve-amendment
authority"]
+ t08["publication.abandon
authority"]
end
subgraph owned_local["owned-local"]
- t09["configuration.initialize
OBSERVED → OBSERVED | TERMINAL"]
- t10["configuration.mutate
OBSERVED | ACTIVE | FRONTIER | UNRESOLVED → OBSERVED | ACTIVE | TERMINAL"]
- t11["delivery.slice.advance
ACTIVE → ACTIVE | TERMINAL"]
- t12["evidence.visual.attach
ACTIVE → ACTIVE | TERMINAL"]
- t13["gate.build.record
ACTIVE → ACTIVE"]
- t14["gate.change.record
ACTIVE → ACTIVE"]
- t15["gate.journey.record
ACTIVE → ACTIVE"]
- t16["gate.review.record
ACTIVE → ACTIVE | TERMINAL"]
- t17["gate.test.record
ACTIVE → ACTIVE | TERMINAL"]
- t18["installation.initialize
DORMANT | OBSERVED → OBSERVED"]
- t19["installation.update
OBSERVED | ACTIVE → OBSERVED | ACTIVE | TERMINAL"]
- t20["invocation.rebind
OBSERVED | UNRESOLVED → OBSERVED"]
- t21["plan.activate
OBSERVED | ACTIVE → ACTIVE"]
- t22["plan.amend
ACTIVE | FRONTIER → ACTIVE"]
- t23["plan.create
OBSERVED | ACTIVE → ACTIVE"]
- t24["plan.invalidate
ACTIVE | OBSERVED → FRONTIER"]
- t25["plan.validate
OBSERVED | ACTIVE → ACTIVE | FRONTIER"]
- t26["publication.observe
OBSERVED | ACTIVE | RECOVERY | UNRESOLVED → ACTIVE | TERMINAL | FRONTIER | UNRESOLVED"]
- t27["publication.preview
ACTIVE → ACTIVE"]
- t28["repository.attach
DORMANT | OBSERVED → OBSERVED"]
- t29["repository.detach
DORMANT | OBSERVED | FRONTIER → DORMANT"]
- t30["runtime.hydrate
OBSERVED | RECOVERY | UNRESOLVED → OBSERVED | ACTIVE | TERMINAL"]
- t31["runtime.replace
OBSERVED | RECOVERY → OBSERVED | TERMINAL"]
- t32["workspace.abandon
ACTIVE | FRONTIER → ABANDONED"]
- t33["workspace.activate
OBSERVED | ACTIVE → ACTIVE"]
- t34["workspace.cleanup
OBSERVED | ACTIVE | TERMINAL | ABANDONED → OBSERVED | TERMINAL | ABANDONED"]
- t35["workspace.cut
OBSERVED | ACTIVE → ACTIVE"]
- t36["workspace.publish
ACTIVE → ACTIVE"]
- t37["workspace.reap
OBSERVED | TERMINAL | ABANDONED → OBSERVED | TERMINAL | ABANDONED"]
- t38["workspace.sync
ACTIVE → ACTIVE | FRONTIER"]
+ t09["catalog.reconcile
owned-local"]
+ t10["configuration.initialize
owned-local"]
+ t11["configuration.mutate
owned-local"]
+ t12["delivery.slice.advance
owned-local"]
+ t13["evidence.visual.attach
owned-local"]
+ t14["gate.build.record
owned-local"]
+ t15["gate.change.record
owned-local"]
+ t16["gate.journey.record
owned-local"]
+ t17["gate.review.record
owned-local"]
+ t18["gate.test.record
owned-local"]
+ t19["installation.initialize
owned-local"]
+ t20["installation.update
owned-local"]
+ t21["invocation.rebind
owned-local"]
+ t22["plan.activate
owned-local"]
+ t23["plan.amend
owned-local"]
+ t24["plan.create
owned-local"]
+ t25["plan.invalidate
owned-local"]
+ t26["plan.validate
owned-local"]
+ t27["publication.observe
owned-local"]
+ t28["publication.preview
owned-local"]
+ t29["repository.attach
owned-local"]
+ t30["repository.detach
owned-local"]
+ t31["runtime.hydrate
owned-local"]
+ t32["runtime.replace
owned-local"]
+ t33["workspace.abandon
owned-local"]
+ t34["workspace.activate
owned-local"]
+ t35["workspace.cleanup
owned-local"]
+ t36["workspace.cut
owned-local"]
+ t37["workspace.publish
owned-local"]
+ t38["workspace.reap
owned-local"]
+ t39["workspace.sync
owned-local"]
end
subgraph owned_external["owned-external"]
- t39["publication.correct
OBSERVED | ACTIVE | TERMINAL → ACTIVE | RECOVERY"]
- t40["publication.execute
ACTIVE → ACTIVE | RECOVERY"]
+ t40["publication.correct
owned-external"]
+ t41["publication.execute
owned-external"]
end
subgraph recovery["recovery"]
- t41["configuration.reconcile
RECOVERY | UNRESOLVED → OBSERVED | FRONTIER | TERMINAL"]
- t42["publication.reconcile
RECOVERY | UNRESOLVED → ACTIVE | TERMINAL | FRONTIER | UNRESOLVED"]
- t43["recovery.escalate
RECOVERY | UNRESOLVED → FRONTIER"]
- t44["recovery.resume
RECOVERY → DORMANT | OBSERVED | ACTIVE | FRONTIER | TERMINAL | ABANDONED"]
- t45["recovery.rollback
RECOVERY → DORMANT | OBSERVED | ACTIVE | FRONTIER | TERMINAL | ABANDONED"]
- t46["runtime.reconcile
RECOVERY | UNRESOLVED → OBSERVED | FRONTIER | TERMINAL"]
- t47["workspace.reconcile
RECOVERY | UNRESOLVED → DORMANT | OBSERVED | ACTIVE | FRONTIER | TERMINAL | ABANDONED"]
+ t42["configuration.reconcile
recovery"]
+ t43["publication.reconcile
recovery"]
+ t44["recovery.escalate
recovery"]
+ t45["recovery.resume
recovery"]
+ t46["recovery.rollback
recovery"]
+ t47["runtime.reconcile
recovery"]
+ t48["workspace.reconcile
recovery"]
end
subgraph observed_external["observed-external"]
- t48["external.branch-changed
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED"]
- t49["external.ci-completed
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED | ACTIVE | TERMINAL"]
- t50["external.configuration-drifted
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED | UNRESOLVED"]
- t51["external.files-changed
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED"]
- t52["external.head-changed
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED"]
- t53["external.host-interrupted
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → RECOVERY"]
- t54["external.lease-expired
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → DORMANT | FRONTIER"]
- t55["external.pr-closed
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED | ACTIVE | FRONTIER"]
- t56["external.pr-merged
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED | ACTIVE | TERMINAL"]
- t57["external.pr-opened
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED | ACTIVE | TERMINAL"]
- t58["external.pr-updated
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED | ACTIVE | TERMINAL"]
- t59["external.provider-unavailable
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → UNRESOLVED | RECOVERY"]
- t60["external.runtime-disappeared
DORMANT | OBSERVED | ACTIVE | RECOVERY | FRONTIER | UNRESOLVED → OBSERVED | RECOVERY"]
+ t49["external.branch-changed
observed-external"]
+ t50["external.ci-completed
observed-external"]
+ t51["external.configuration-drifted
observed-external"]
+ t52["external.files-changed
observed-external"]
+ t53["external.head-changed
observed-external"]
+ t54["external.host-interrupted
observed-external"]
+ t55["external.lease-expired
observed-external"]
+ t56["external.pr-closed
observed-external"]
+ t57["external.pr-merged
observed-external"]
+ t58["external.pr-opened
observed-external"]
+ t59["external.pr-updated
observed-external"]
+ t60["external.provider-unavailable
observed-external"]
+ t61["external.runtime-disappeared
observed-external"]
end
+ p_DORMANT --> t00
+ p_OBSERVED --> t00
+ t00 --> p_OBSERVED
+ t00 --> p_ACTIVE
+ p_ACTIVE --> t01
+ p_FRONTIER --> t01
+ t01 --> p_DORMANT
+ p_ACTIVE --> t02
+ t02 --> p_ACTIVE
+ p_ACTIVE --> t03
+ p_FRONTIER --> t03
+ t03 --> p_FRONTIER
+ p_OBSERVED --> t04
+ p_ACTIVE --> t04
+ p_FRONTIER --> t04
+ p_TERMINAL --> t04
+ p_ABANDONED --> t04
+ t04 --> p_OBSERVED
+ t04 --> p_ACTIVE
+ t04 --> p_FRONTIER
+ p_OBSERVED --> t05
+ p_ACTIVE --> t05
+ p_FRONTIER --> t05
+ t05 --> p_ABANDONED
+ p_ACTIVE --> t06
+ p_FRONTIER --> t06
+ t06 --> p_ACTIVE
+ t06 --> p_TERMINAL
+ p_ACTIVE --> t07
+ p_FRONTIER --> t07
+ t07 --> p_ACTIVE
+ p_ACTIVE --> t08
+ p_FRONTIER --> t08
+ t08 --> p_ABANDONED
+ p_DORMANT --> t09
+ p_OBSERVED --> t09
+ p_ACTIVE --> t09
+ p_RECOVERY --> t09
+ p_FRONTIER --> t09
+ p_UNRESOLVED --> t09
+ p_TERMINAL --> t09
+ p_ABANDONED --> t09
+ t09 --> p_DORMANT
+ t09 --> p_OBSERVED
+ t09 --> p_ACTIVE
+ t09 --> p_RECOVERY
+ t09 --> p_FRONTIER
+ t09 --> p_UNRESOLVED
+ t09 --> p_TERMINAL
+ t09 --> p_ABANDONED
+ p_OBSERVED --> t10
+ t10 --> p_OBSERVED
+ t10 --> p_TERMINAL
+ p_OBSERVED --> t11
+ p_ACTIVE --> t11
+ p_FRONTIER --> t11
+ p_UNRESOLVED --> t11
+ t11 --> p_OBSERVED
+ t11 --> p_ACTIVE
+ t11 --> p_TERMINAL
+ p_ACTIVE --> t12
+ t12 --> p_ACTIVE
+ t12 --> p_TERMINAL
+ p_ACTIVE --> t13
+ t13 --> p_ACTIVE
+ t13 --> p_TERMINAL
+ p_ACTIVE --> t14
+ t14 --> p_ACTIVE
+ p_ACTIVE --> t15
+ t15 --> p_ACTIVE
+ p_ACTIVE --> t16
+ t16 --> p_ACTIVE
+ p_ACTIVE --> t17
+ t17 --> p_ACTIVE
+ t17 --> p_TERMINAL
+ p_ACTIVE --> t18
+ t18 --> p_ACTIVE
+ t18 --> p_TERMINAL
+ p_DORMANT --> t19
+ p_OBSERVED --> t19
+ t19 --> p_OBSERVED
+ p_OBSERVED --> t20
+ p_ACTIVE --> t20
+ t20 --> p_OBSERVED
+ t20 --> p_ACTIVE
+ t20 --> p_TERMINAL
+ p_OBSERVED --> t21
+ p_UNRESOLVED --> t21
+ t21 --> p_OBSERVED
+ p_OBSERVED --> t22
+ p_ACTIVE --> t22
+ t22 --> p_ACTIVE
+ p_ACTIVE --> t23
+ p_FRONTIER --> t23
+ t23 --> p_ACTIVE
+ p_OBSERVED --> t24
+ p_ACTIVE --> t24
+ t24 --> p_ACTIVE
+ p_ACTIVE --> t25
+ p_OBSERVED --> t25
+ t25 --> p_FRONTIER
+ p_OBSERVED --> t26
+ p_ACTIVE --> t26
+ t26 --> p_ACTIVE
+ t26 --> p_FRONTIER
+ p_OBSERVED --> t27
+ p_ACTIVE --> t27
+ p_RECOVERY --> t27
+ p_UNRESOLVED --> t27
+ t27 --> p_ACTIVE
+ t27 --> p_TERMINAL
+ t27 --> p_FRONTIER
+ t27 --> p_UNRESOLVED
+ p_ACTIVE --> t28
+ t28 --> p_ACTIVE
+ p_DORMANT --> t29
+ p_OBSERVED --> t29
+ t29 --> p_OBSERVED
+ p_DORMANT --> t30
+ p_OBSERVED --> t30
+ p_FRONTIER --> t30
+ t30 --> p_DORMANT
+ p_OBSERVED --> t31
+ p_RECOVERY --> t31
+ p_UNRESOLVED --> t31
+ t31 --> p_OBSERVED
+ t31 --> p_ACTIVE
+ t31 --> p_TERMINAL
+ p_OBSERVED --> t32
+ p_RECOVERY --> t32
+ t32 --> p_OBSERVED
+ t32 --> p_TERMINAL
+ p_ACTIVE --> t33
+ p_FRONTIER --> t33
+ t33 --> p_ABANDONED
+ p_OBSERVED --> t34
+ p_ACTIVE --> t34
+ t34 --> p_ACTIVE
+ p_OBSERVED --> t35
+ p_ACTIVE --> t35
+ p_TERMINAL --> t35
+ p_ABANDONED --> t35
+ t35 --> p_OBSERVED
+ t35 --> p_TERMINAL
+ t35 --> p_ABANDONED
+ p_OBSERVED --> t36
+ p_ACTIVE --> t36
+ t36 --> p_ACTIVE
+ p_ACTIVE --> t37
+ t37 --> p_ACTIVE
+ p_OBSERVED --> t38
+ p_TERMINAL --> t38
+ p_ABANDONED --> t38
+ t38 --> p_OBSERVED
+ t38 --> p_TERMINAL
+ t38 --> p_ABANDONED
+ p_ACTIVE --> t39
+ t39 --> p_ACTIVE
+ t39 --> p_FRONTIER
+ p_OBSERVED --> t40
+ p_ACTIVE --> t40
+ p_TERMINAL --> t40
+ t40 --> p_ACTIVE
+ t40 --> p_RECOVERY
+ p_ACTIVE --> t41
+ t41 --> p_ACTIVE
+ t41 --> p_RECOVERY
+ p_RECOVERY --> t42
+ p_UNRESOLVED --> t42
+ t42 --> p_OBSERVED
+ t42 --> p_FRONTIER
+ t42 --> p_TERMINAL
+ p_RECOVERY --> t43
+ p_UNRESOLVED --> t43
+ t43 --> p_ACTIVE
+ t43 --> p_TERMINAL
+ t43 --> p_FRONTIER
+ t43 --> p_UNRESOLVED
+ p_RECOVERY --> t44
+ p_UNRESOLVED --> t44
+ t44 --> p_FRONTIER
+ p_RECOVERY --> t45
+ t45 --> p_DORMANT
+ t45 --> p_OBSERVED
+ t45 --> p_ACTIVE
+ t45 --> p_FRONTIER
+ t45 --> p_TERMINAL
+ t45 --> p_ABANDONED
+ p_RECOVERY --> t46
+ t46 --> p_DORMANT
+ t46 --> p_OBSERVED
+ t46 --> p_ACTIVE
+ t46 --> p_FRONTIER
+ t46 --> p_TERMINAL
+ t46 --> p_ABANDONED
+ p_RECOVERY --> t47
+ p_UNRESOLVED --> t47
+ t47 --> p_OBSERVED
+ t47 --> p_FRONTIER
+ t47 --> p_TERMINAL
+ p_RECOVERY --> t48
+ p_UNRESOLVED --> t48
+ t48 --> p_DORMANT
+ t48 --> p_OBSERVED
+ t48 --> p_ACTIVE
+ t48 --> p_FRONTIER
+ t48 --> p_TERMINAL
+ t48 --> p_ABANDONED
+ p_DORMANT --> t49
+ p_OBSERVED --> t49
+ p_ACTIVE --> t49
+ p_RECOVERY --> t49
+ p_FRONTIER --> t49
+ p_UNRESOLVED --> t49
+ t49 --> p_OBSERVED
+ p_DORMANT --> t50
+ p_OBSERVED --> t50
+ p_ACTIVE --> t50
+ p_RECOVERY --> t50
+ p_FRONTIER --> t50
+ p_UNRESOLVED --> t50
+ t50 --> p_OBSERVED
+ t50 --> p_ACTIVE
+ t50 --> p_TERMINAL
+ p_DORMANT --> t51
+ p_OBSERVED --> t51
+ p_ACTIVE --> t51
+ p_RECOVERY --> t51
+ p_FRONTIER --> t51
+ p_UNRESOLVED --> t51
+ t51 --> p_OBSERVED
+ t51 --> p_UNRESOLVED
+ p_DORMANT --> t52
+ p_OBSERVED --> t52
+ p_ACTIVE --> t52
+ p_RECOVERY --> t52
+ p_FRONTIER --> t52
+ p_UNRESOLVED --> t52
+ t52 --> p_OBSERVED
+ p_DORMANT --> t53
+ p_OBSERVED --> t53
+ p_ACTIVE --> t53
+ p_RECOVERY --> t53
+ p_FRONTIER --> t53
+ p_UNRESOLVED --> t53
+ t53 --> p_OBSERVED
+ p_DORMANT --> t54
+ p_OBSERVED --> t54
+ p_ACTIVE --> t54
+ p_RECOVERY --> t54
+ p_FRONTIER --> t54
+ p_UNRESOLVED --> t54
+ t54 --> p_RECOVERY
+ p_DORMANT --> t55
+ p_OBSERVED --> t55
+ p_ACTIVE --> t55
+ p_RECOVERY --> t55
+ p_FRONTIER --> t55
+ p_UNRESOLVED --> t55
+ t55 --> p_DORMANT
+ t55 --> p_FRONTIER
+ p_DORMANT --> t56
+ p_OBSERVED --> t56
+ p_ACTIVE --> t56
+ p_RECOVERY --> t56
+ p_FRONTIER --> t56
+ p_UNRESOLVED --> t56
+ t56 --> p_OBSERVED
+ t56 --> p_ACTIVE
+ t56 --> p_FRONTIER
+ p_DORMANT --> t57
+ p_OBSERVED --> t57
+ p_ACTIVE --> t57
+ p_RECOVERY --> t57
+ p_FRONTIER --> t57
+ p_UNRESOLVED --> t57
+ t57 --> p_OBSERVED
+ t57 --> p_ACTIVE
+ t57 --> p_TERMINAL
+ p_DORMANT --> t58
+ p_OBSERVED --> t58
+ p_ACTIVE --> t58
+ p_RECOVERY --> t58
+ p_FRONTIER --> t58
+ p_UNRESOLVED --> t58
+ t58 --> p_OBSERVED
+ t58 --> p_ACTIVE
+ t58 --> p_TERMINAL
+ p_DORMANT --> t59
+ p_OBSERVED --> t59
+ p_ACTIVE --> t59
+ p_RECOVERY --> t59
+ p_FRONTIER --> t59
+ p_UNRESOLVED --> t59
+ t59 --> p_OBSERVED
+ t59 --> p_ACTIVE
+ t59 --> p_TERMINAL
+ p_DORMANT --> t60
+ p_OBSERVED --> t60
+ p_ACTIVE --> t60
+ p_RECOVERY --> t60
+ p_FRONTIER --> t60
+ p_UNRESOLVED --> t60
+ t60 --> p_UNRESOLVED
+ t60 --> p_RECOVERY
+ p_DORMANT --> t61
+ p_OBSERVED --> t61
+ p_ACTIVE --> t61
+ p_RECOVERY --> t61
+ p_FRONTIER --> t61
+ p_UNRESOLVED --> t61
+ t61 --> p_OBSERVED
+ t61 --> p_RECOVERY
diff --git a/docs/configuration.md b/docs/configuration.md
index 0ef3d79..bd372f4 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -23,7 +23,7 @@ hosts, trailing JSON, and missing required fields fail closed.
"visual_evidence": "optional",
"external_effect_authority": "human-or-autonomy-plus-provider"
},
- "hosts": ["cli", "cursor", "codex", "claude", "gemini", "mcp"]
+ "hosts": ["cli", "cursor", "codex", "claude", "gemini", "mcp", "sdk"]
}
```
@@ -49,6 +49,47 @@ refuses attachment. A host omitted from `hosts` cannot request managed
transitions. If the configured default branch cannot be inspected, the
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:
+
+```json
+{
+ "extensions": [
+ {
+ "id": "example.security",
+ "version": "1.0.0",
+ "executable": "/absolute/symlink-free/path/security-extension",
+ "sha256": "<64 lowercase hexadecimal characters>",
+ "manifest": {
+ "id": "example.security",
+ "version": "1.0.0",
+ "protocol_version": 1,
+ "settings_schema": {"type": "object", "additionalProperties": false},
+ "privacy_classification": "metadata-only",
+ "telemetry_classification": "transition-receipt"
+ },
+ "settings": {"profile": "strict"},
+ "deadline_millis": 5000,
+ "stdout_bytes": 1048576,
+ "stderr_bytes": 65536
+ }
+ ]
+}
+```
+
+The declarative manifest is compiled without starting the executable. Once the
+exact configuration and ControlProgram binding is current, the executable is
+invoked directly without a shell from a private copy of the exact bytes hashed
+for that invocation. It receives only the bounded versioned JSON protocol and
+fixed locale variables. Crossing either output bound cancels the subprocess
+immediately and fails the operation closed. It is a trusted executable
+boundary, not an OS sandbox.
+Changing its set, version, executable bytes, settings, or limits changes the
+ControlProgram fingerprint and therefore fails closed as program drift for an
+active flow.
+
`project.commands` names the repository's canonical product checks. Build and
test gate transitions execute those exact repository-owned commands inside the
effect boundary and install evidence only after a zero exit status. The command
diff --git a/docs/generated-files.md b/docs/generated-files.md
index b8be4e9..ba08f98 100644
--- a/docs/generated-files.md
+++ b/docs/generated-files.md
@@ -32,18 +32,21 @@ falls back to it.
## Generated architecture evidence
-The executable transition registry deterministically generates four checked
+The executable transition registry deterministically generates five checked
architecture artifacts:
| Artifact | Regeneration command |
|---|---|
| `docs/architecture/boatstack-v2-transition-catalog.md` | `boatstack-helper catalog --format markdown` |
| `docs/architecture/boatstack-v2-transition-catalog.mmd` | `boatstack-helper catalog --format mermaid` |
+| `docs/architecture/boatstack-standard-flow.mmd` | `boatstack-helper catalog --format standard-flow-mermaid` |
| `docs/architecture/boatstack-v2-locus-safety.json` | `boatstack-helper catalog --format locus-safety` |
| `docs/architecture/boatstack-v2-locus-liveness.json` | `boatstack-helper catalog --format locus-liveness` |
Repository and Go tests compare every checked byte with a fresh render and
-require both Locus alphabets to equal all 61 executable catalog transitions.
+require both Locus alphabets to equal all 62 executable catalog transitions.
+The StandardFlow graph contains exactly the 30 transitions whose compiled
+origin is the primary flow.
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/docs/public-claims.json b/docs/public-claims.json
index 8b84c5b..4bb608c 100644
--- a/docs/public-claims.json
+++ b/docs/public-claims.json
@@ -13,11 +13,11 @@
"readable_evidence": "architecture/boatstack-v2-kernel.md#14-package-and-dependency-architecture",
"implementation": [
"../boatstack/internal/kernel/engine/engine.go",
- "../boatstack/internal/kernel/catalog/default.go",
- "../boatstack/v2_kernel.go"
+ "../boatstack/control/control.go",
+ "../boatstack/kernel.go"
],
"verification": [
- "../boatstack/internal/kernel/catalog/completeness_test.go",
+ "../boatstack/flow/standard/completeness_test.go",
"../boatstack/internal/kernel/engine/engine_test.go"
],
"last_verified_version": "v2.0.0"
@@ -80,7 +80,7 @@
"../boatstack/internal/surfaces/guard.go"
],
"verification": [
- "../boatstack/internal/kernel/supervisor/supervisor_test.go",
+ "../boatstack/flow/standard/supervisor_parity_test.go",
"../boatstack/internal/surfaces/render_test.go"
],
"last_verified_version": "v2.0.0"
@@ -110,7 +110,7 @@
"readable_evidence": "architecture/boatstack-v2-kernel.md#16-process-telemetry-contract",
"implementation": [
"../boatstack/internal/effects/receipts.go",
- "../boatstack/v2_kernel.go"
+ "../boatstack/kernel.go"
],
"verification": [
"../boatstack/internal/effects/integration_test.go"
@@ -125,31 +125,33 @@
"implementation": [
"../boatstack/internal/kernel/model/state.go",
"../boatstack/internal/kernel/supervisor/supervisor.go",
- "../boatstack/internal/kernel/reducer/reducer.go"
+ "../boatstack/internal/effects/state_reducer.go"
],
"verification": [
- "../boatstack/internal/kernel/supervisor/supervisor_test.go",
+ "../boatstack/flow/standard/supervisor_parity_test.go",
"../boatstack/internal/kernel/protocol/policy_test.go",
"../boatstack/internal/plant/observer_test.go",
- "../boatstack/internal/kernel/reducer/reducer_test.go",
+ "../boatstack/internal/effects/state_reducer_test.go",
"../boatstack/internal/effects/integration_test.go"
],
"last_verified_version": "v2.0.0"
},
{
"id": "formal-live-system-closure",
- "public_claim": "The generated 61-event stable-phase abstraction satisfies the checked safety and liveness properties; executable tests separately bind catalog completeness, facets, reducer branches, operating-system behavior, and provider outcomes.",
+ "public_claim": "The generated 62-event stable-phase abstraction satisfies the checked safety and liveness properties; executable tests separately bind catalog completeness, facets, reducer branches, operating-system behavior, and provider outcomes.",
"status": "advisory",
"readable_evidence": "architecture/boatstack-v2-kernel.md#17-test-and-formal-property-strategy",
"implementation": [
"architecture/boatstack-v2-transition-catalog.md",
"architecture/boatstack-v2-locus-safety.json",
"architecture/boatstack-v2-locus-liveness.json",
- "../boatstack/internal/kernel/catalog/default.go"
+ "../boatstack/control/control.go",
+ "../boatstack/core/transitions.json",
+ "../boatstack/flow/standard/transitions.json"
],
"verification": [
- "../boatstack/internal/kernel/catalog/completeness_test.go",
- "../boatstack/internal/kernel/catalog/historical_test.go",
+ "../boatstack/flow/standard/completeness_test.go",
+ "../boatstack/flow/standard/historical_test.go",
"../boatstack/internal/surfaces/render_test.go",
"../boatstack/internal/kernel/engine/engine_test.go",
"../boatstack/internal/effects/integration_test.go"
diff --git a/project.example.json b/project.example.json
index 7bc7eb0..4e947f6 100644
--- a/project.example.json
+++ b/project.example.json
@@ -33,6 +33,7 @@
"codex",
"claude",
"gemini",
- "mcp"
+ "mcp",
+ "sdk"
]
}
diff --git a/release-notes/2026-08-11-programmable-control-program.md b/release-notes/2026-08-11-programmable-control-program.md
new file mode 100644
index 0000000..3bfafc7
--- /dev/null
+++ b/release-notes/2026-08-11-programmable-control-program.md
@@ -0,0 +1,3 @@
+### Separate Kernel mechanism from delivery policy
+
+Boatstack now compiles one immutable CoreSystem, one explicit primary delivery flow, and optional conservative extensions into a fingerprinted ControlProgram before the Kernel resolves or applies any transition. The default distribution retains StandardFlow behavior, while public SDK contracts can supply another trusted flow and checksum-verified subprocess extensions without modifying Kernel mechanism code. Subprocess manifests are declarative, component settings are schema-validated, executable bytes remain identity-bound through invocation, and uncertain external outcomes enter recovery instead of being reported as rolled back.