diff --git a/boatstack/delivery/program_manifest_test.go b/boatstack/delivery/program_manifest_test.go
index 56fab49..e880b1b 100644
--- a/boatstack/delivery/program_manifest_test.go
+++ b/boatstack/delivery/program_manifest_test.go
@@ -316,6 +316,7 @@ func programFixture() delivery.ProgramManifest {
advance.TargetPhases = []delivery.ProtocolPhase{delivery.PhaseTerminal}
advance.ObjectiveKinds = []delivery.ObjectiveKind{delivery.ObjectiveVerified}
advance.Authority = []delivery.AuthorityClass{delivery.AuthorityHuman, delivery.AuthorityRepository}
+ advance.RequiredCapabilities = []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityProductMutate}
advance.Effect = "program.advance"
advance.LocalEffects = []delivery.EffectID{"program.advance"}
advance.Prescription = delivery.Prescription{Operation: "advance", Arguments: []string{"--exact"}, ExpectedPostcondition: "terminal"}
@@ -331,7 +332,7 @@ func programFixture() delivery.ProgramManifest {
SchemaVersion: delivery.ProgramSchemaVersion, ProgramID: "test-program", ProgramVersion: "1", RequiresRuntime: ">=1.0.0",
Capabilities: delivery.ProgramCapabilities{
Effects: []string{"program.advance", "program.recover"}, Verifiers: []string{"program.current", "program.terminal"},
- CapabilitySurface: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute},
+ CapabilitySurface: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityProductMutate},
},
OwnedResources: []string{"program.state"}, ObjectiveContracts: []delivery.ObjectiveContract{{ObjectiveKind: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, "terminal")}}},
Transitions: []delivery.ProgramTransition{advance, recovery},
@@ -339,7 +340,7 @@ func programFixture() delivery.ProgramManifest {
}
func runtimeFixture() delivery.RuntimeCompatibility {
- return delivery.RuntimeCompatibility{Version: "v1.2.3", Effects: []string{"program.advance", "program.recover", "alternate.effect"}, Verifiers: []string{"program.current", "program.terminal", "alternate.verifier"}, Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityHumanApprove}}
+ return delivery.RuntimeCompatibility{Version: "v1.2.3", Effects: []string{"program.advance", "program.recover", "alternate.effect"}, Verifiers: []string{"program.current", "program.terminal", "alternate.verifier"}, Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityProductMutate, delivery.CapabilityHumanApprove}}
}
func loadManifest(t *testing.T, manifest delivery.ProgramManifest) delivery.ControlProgram {
diff --git a/boatstack/internal/softwaredelivery/catalog/capability.go b/boatstack/internal/softwaredelivery/catalog/capability.go
index 473d1fd..e7671de 100644
--- a/boatstack/internal/softwaredelivery/catalog/capability.go
+++ b/boatstack/internal/softwaredelivery/catalog/capability.go
@@ -3,7 +3,8 @@ package catalog
import (
"fmt"
"sort"
- "strings"
+
+ "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model"
)
// Capability names a kernel-enforced class of effect. A declaration narrows
@@ -131,17 +132,17 @@ func KernelEffectCapabilities(transition Transition) []Capability {
if transition.RuntimeExecution {
required[CapabilityCommandExecute] = true
}
+ for _, facet := range transition.OwnedFacets {
+ if facet == model.StateFacetProduct {
+ required[CapabilityProductMutate] = true
+ }
+ }
id := string(transition.Effect)
switch id {
case "gate.build.record", "gate.test.record", "workspace.cut", "workspace.sync", "workspace.cleanup", "workspace.reap",
"publication.observe", "publication.reconcile", "publication.execute", "publication.correct":
required[CapabilityCommandExecute] = true
}
- if strings.HasPrefix(id, "objective.") || strings.HasPrefix(id, "plan.") || strings.HasPrefix(id, "workspace.") ||
- strings.HasPrefix(id, "gate.") || strings.HasPrefix(id, "evidence.") || strings.HasPrefix(id, "delivery.") ||
- strings.HasPrefix(id, "publication.") {
- required[CapabilityProductMutate] = true
- }
if id == "publication.preview" {
required[CapabilityPublicationPrepare] = true
}
diff --git a/boatstack/internal/softwaredelivery/catalog/capability_test.go b/boatstack/internal/softwaredelivery/catalog/capability_test.go
index a2eecac..11072e1 100644
--- a/boatstack/internal/softwaredelivery/catalog/capability_test.go
+++ b/boatstack/internal/softwaredelivery/catalog/capability_test.go
@@ -1,6 +1,10 @@
package catalog
-import "testing"
+import (
+ "testing"
+
+ "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model"
+)
func TestCapabilityVocabularyFailsClosed(t *testing.T) {
if _, err := NormalizeCapabilities("test", []Capability{"production.nuke"}); err == nil {
@@ -15,6 +19,7 @@ func TestKernelEffectClassificationCannotBeWeakenedByTransitionDeclaration(t *te
// control-law: repository-authored requirements cannot under-classify a kernel effect
transition := Transition{
ID: "program/publish", Class: EventOwnedExternal, Effect: "publication.execute",
+ OwnedFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct},
RequiredCapabilities: []Capability{CapabilityRepositoryWrite},
DeclaredCapabilities: []Capability{CapabilityRepositoryWrite, CapabilityCommandExecute, CapabilityProductMutate, CapabilityPublicationPublish},
}
@@ -26,6 +31,18 @@ func TestKernelEffectClassificationCannotBeWeakenedByTransitionDeclaration(t *te
}
}
+func TestProductOwnershipRequiresProductMutationForArbitraryEffect(t *testing.T) {
+ transition := Transition{
+ ID: "program/advance", Class: EventOwnedLocal, Effect: "acme.advance",
+ OwnedFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct},
+ RequiredCapabilities: []Capability{CapabilityRepositoryWrite},
+ }
+ required := NewCapabilitySet(RequiredCapabilities(transition)...)
+ if !required[CapabilityProductMutate] {
+ t.Fatalf("product-owning arbitrary effect was under-classified: %v", required.Sorted())
+ }
+}
+
func TestCapabilityClassesHaveNoImplicitHierarchy(t *testing.T) {
granted := AuthorityCapabilities(AuthoritySet{AuthorityProvider: true})
if !granted[CapabilityPublicationPublish] || granted[CapabilityPublicationPrepare] || granted[CapabilityRepositoryWrite] {
diff --git a/boatstack/internal/softwaredelivery/catalog/state_effect_closure_test.go b/boatstack/internal/softwaredelivery/catalog/state_effect_closure_test.go
index c6267e7..3962f56 100644
--- a/boatstack/internal/softwaredelivery/catalog/state_effect_closure_test.go
+++ b/boatstack/internal/softwaredelivery/catalog/state_effect_closure_test.go
@@ -126,6 +126,19 @@ func TestDeclarativeAssignmentsRejectApplyTimeOnlyValueConstraints(t *testing.T)
}
}
+func TestStateAssignmentMustSatisfyEveryTargetCondition(t *testing.T) {
+ assignment := literalAssignment("delivery", string(model.DeliveryPublished))
+ transition := Transition{
+ TargetConditions: []FacetCondition{
+ {Facet: model.FacetDelivery, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.DeliveryPublished)}},
+ {Facet: model.FacetDelivery, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.DeliveryTerminal)}},
+ },
+ }
+ if stateAssignmentMatchesTarget(transition, assignment) {
+ t.Fatal("assignment matched only the first of two target conditions")
+ }
+}
+
func closureTransition(source FacetCondition, assignments ...StateAssignment) Transition {
transition := Transition{
ID: "test.transition", SourcePhases: []model.ProtocolPhase{model.PhaseActive}, TargetPhases: []model.ProtocolPhase{model.PhaseActive},
diff --git a/boatstack/internal/softwaredelivery/catalog/state_facet.go b/boatstack/internal/softwaredelivery/catalog/state_facet.go
index 3044c52..2944321 100644
--- a/boatstack/internal/softwaredelivery/catalog/state_facet.go
+++ b/boatstack/internal/softwaredelivery/catalog/state_facet.go
@@ -157,6 +157,26 @@ func DurableStateFacetPolicy(transition Transition) (StateFacetPolicy, error) {
return StateFacetPolicy{Reads: append([]model.StateFacet(nil), allStateFacets...), Writes: writes}, nil
}
+// RecoveryStateFacets reconstructs the interrupted transition's write envelope
+// from admission-bound capability identity. Privileged core facets remain
+// closed over the fixed transition IDs that own them; repository-authored
+// programs can recover only control and admitted product mutations.
+func RecoveryStateFacets(id TransitionID, required []Capability) []model.StateFacet {
+ switch id {
+ case "runtime.hydrate", "runtime.replace", "runtime.reconcile", "installation.update":
+ return []model.StateFacet{model.StateFacetControl, model.StateFacetInstallation}
+ case "installation.initialize", "installation.reconcile-update":
+ return []model.StateFacet{model.StateFacetControl, model.StateFacetInstallation, model.StateFacetProgram}
+ case "repository.attach", "catalog.reconcile":
+ return []model.StateFacet{model.StateFacetControl, model.StateFacetProgram}
+ }
+ writes := []model.StateFacet{model.StateFacetControl}
+ if NewCapabilitySet(required...).ContainsAll([]Capability{CapabilityProductMutate}) {
+ writes = append(writes, model.StateFacetProduct)
+ }
+ return writes
+}
+
func containsStateFacet(values []model.StateFacet, wanted model.StateFacet) bool {
for _, value := range values {
if value == wanted {
diff --git a/boatstack/internal/softwaredelivery/catalog/transition.go b/boatstack/internal/softwaredelivery/catalog/transition.go
index 507b538..aefb78c 100644
--- a/boatstack/internal/softwaredelivery/catalog/transition.go
+++ b/boatstack/internal/softwaredelivery/catalog/transition.go
@@ -858,22 +858,35 @@ func stateAssignmentMatchesTarget(t Transition, assignment StateAssignment) bool
if target.Facet != facet {
continue
}
- if len(target.Statuses) != 1 || target.Statuses[0] != model.FactKnown {
+ if !containsFactStatus(target.Statuses, model.FactKnown) {
return false
}
if len(target.Values) == 0 {
- return true
+ continue
}
+ matched := false
for _, value := range target.Values {
if value == *assignment.Value {
- return true
+ matched = true
+ break
}
}
- return false
+ if !matched {
+ return false
+ }
}
return true
}
+func containsFactStatus(values []model.FactStatus, wanted model.FactStatus) bool {
+ for _, value := range values {
+ if value == wanted {
+ return true
+ }
+ }
+ return false
+}
+
func cloneConditions(values []FacetCondition) []FacetCondition {
result := make([]FacetCondition, len(values))
for index, value := range values {
diff --git a/boatstack/internal/softwaredelivery/effects/cas_integration_test.go b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go
index f0817f6..f90f7b8 100644
--- a/boatstack/internal/softwaredelivery/effects/cas_integration_test.go
+++ b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go
@@ -141,7 +141,8 @@ func TestConcurrentApplyConsumesOneRevisionExactlyOnce(t *testing.T) {
t.Fatalf("canonical committed journal count=%d err=%v", len(committedJournals), err)
}
committedRaw, err := os.ReadFile(committedJournals[0])
- if err != nil || !bytes.Contains(committedRaw, []byte(committed.Receipt.ID)) || !bytes.Contains(committedRaw, []byte("committed_effects")) {
+ if err != nil || !bytes.Contains(committedRaw, []byte(committed.Receipt.ID)) || !bytes.Contains(committedRaw, []byte("committed_effects")) ||
+ !bytes.Contains(committedRaw, []byte(`"schema_version": 8`)) || !bytes.Contains(committedRaw, []byte(`"allowed_state_facets"`)) {
t.Fatalf("committed journal lacks its complete transition fact: %v %q", err, committedRaw)
}
// Simulate a crash after canonical commit but before the passive receipt
diff --git a/boatstack/internal/softwaredelivery/effects/journal.go b/boatstack/internal/softwaredelivery/effects/journal.go
index f64d8cf..94dc0bd 100644
--- a/boatstack/internal/softwaredelivery/effects/journal.go
+++ b/boatstack/internal/softwaredelivery/effects/journal.go
@@ -33,10 +33,12 @@ func NewJournal(resolver ports.InvocationResolver, clock ports.Clock) (*Journal,
}
type journalRecord struct {
- SchemaVersion int `json:"schema_version"`
- Admission protocol.Admission `json:"admission"`
- TransitionID catalog.TransitionID `json:"transition_id"`
- TransitionClass catalog.EventClass `json:"transition_class"`
+ SchemaVersion int `json:"schema_version"`
+ Admission protocol.Admission `json:"admission"`
+ TransitionID catalog.TransitionID `json:"transition_id"`
+ TransitionClass catalog.EventClass `json:"transition_class"`
+ // AllowedStateFacets preserves the current schema-8 record shape. Recovery
+ // authority is reconstructed from Admission.RequiredCapabilities instead.
AllowedStateFacets []model.StateFacet `json:"allowed_state_facets"`
ReconcilesProgram bool `json:"reconciles_program,omitempty"`
Status string `json:"status"`
diff --git a/boatstack/internal/softwaredelivery/effects/journal_schema_test.go b/boatstack/internal/softwaredelivery/effects/journal_schema_test.go
index 0b86489..1faac72 100644
--- a/boatstack/internal/softwaredelivery/effects/journal_schema_test.go
+++ b/boatstack/internal/softwaredelivery/effects/journal_schema_test.go
@@ -1,27 +1,13 @@
package effects
import (
- "os"
- "path/filepath"
- "strings"
"testing"
-)
-func TestPriorJournalSchemaRequiresExplicitReset(t *testing.T) {
- path := filepath.Join(t.TempDir(), "adm-prior.pending")
- raw := []byte(`{"schema_version":7,"admission":{"id":"adm-prior"},"transition_id":"plan.create","transition_class":"owned-local","status":"begun"}`)
- if err := os.WriteFile(path, raw, 0o600); err != nil {
- t.Fatal(err)
- }
+ "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol"
+)
- if _, err := readJournal(path); err == nil || !strings.Contains(err.Error(), "invalid transaction journal") {
- t.Fatalf("read prior journal schema error = %v, want explicit invalid journal refusal", err)
- }
- got, err := os.ReadFile(path)
- if err != nil {
- t.Fatal(err)
- }
- if string(got) != string(raw) {
- t.Fatalf("prior journal changed during refusal:\n got %s\nwant %s", got, raw)
+func TestInstallationUpdateKeepsCurrentJournalSchema(t *testing.T) {
+ if protocol.JournalSchemaVersion != 8 {
+ t.Fatalf("journal schema = %d, want current schema 8 for in-flight installation updates", protocol.JournalSchemaVersion)
}
}
diff --git a/boatstack/internal/softwaredelivery/effects/recovery.go b/boatstack/internal/softwaredelivery/effects/recovery.go
index f8f5074..2a1da05 100644
--- a/boatstack/internal/softwaredelivery/effects/recovery.go
+++ b/boatstack/internal/softwaredelivery/effects/recovery.go
@@ -147,7 +147,7 @@ func recoveryStateFacets(record journalRecord, recovery catalog.TransitionID, in
if err != nil {
return nil, err
}
- allowed := model.UnionStateFacets(record.AllowedStateFacets, []model.StateFacet{model.StateFacetControl})
+ allowed := catalog.RecoveryStateFacets(record.TransitionID, record.Admission.RequiredCapabilities)
if _, err := validateAllowedStateFacets(recovery, staged, allowed); err != nil {
return nil, err
}
diff --git a/boatstack/internal/softwaredelivery/effects/recovery_test.go b/boatstack/internal/softwaredelivery/effects/recovery_test.go
index c5bb5be..6785292 100644
--- a/boatstack/internal/softwaredelivery/effects/recovery_test.go
+++ b/boatstack/internal/softwaredelivery/effects/recovery_test.go
@@ -1,6 +1,7 @@
package effects
import (
+ "bytes"
"context"
"os"
"os/exec"
@@ -152,6 +153,17 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing.
if err := journalBeforeRestart.RequireRecovery(ctx, admission.ID, "simulated process loss after effect"); err != nil {
t.Fatal(err)
}
+ basePendingPath, err := journalBeforeRestart.pendingPath(ctx, admission)
+ if err != nil {
+ t.Fatal(err)
+ }
+ basePending, err := os.ReadFile(basePendingPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Contains(basePending, []byte(`"schema_version": 8`)) || !bytes.Contains(basePending, []byte(`"allowed_state_facets"`)) {
+ t.Fatalf("pending update journal is not current-main schema 8: %s", basePending)
+ }
restartedInvocation, err := resolver.ResolveInvocation(ctx, repository, "cli", "after-restart")
if err != nil {
diff --git a/boatstack/internal/softwaredelivery/effects/state_facet_test.go b/boatstack/internal/softwaredelivery/effects/state_facet_test.go
index df563e7..00208bf 100644
--- a/boatstack/internal/softwaredelivery/effects/state_facet_test.go
+++ b/boatstack/internal/softwaredelivery/effects/state_facet_test.go
@@ -131,7 +131,7 @@ func TestRecoveryReportsActualStateDeltaInsteadOfInterruptedEnvelope(t *testing.
staged.UpdatedAt = time.Unix(101, 0).UTC()
prior, _ := durable.EncodeState(before)
target, _ := durable.EncodeState(staged)
- record := journalRecord{TransitionID: "plan.create", AllowedStateFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct}, Mutations: []ports.ResourceMutation{{
+ record := journalRecord{TransitionID: "plan.create", AllowedStateFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct}, Admission: protocol.Admission{RequiredCapabilities: []catalog.Capability{catalog.CapabilityProductMutate}}, Mutations: []ports.ResourceMutation{{
Path: "/controller/state.json", PriorExists: true, Prior: prior, Target: target,
StateFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct},
}}}
@@ -151,6 +151,28 @@ func TestRecoveryReportsActualStateDeltaInsteadOfInterruptedEnvelope(t *testing.
}
}
+func TestRecoveryCannotWidenFacetsOutsideAdmissionCapabilities(t *testing.T) {
+ before := ownershipState()
+ after := before
+ after.Plan = model.PlanApproved
+ prior, _ := durable.EncodeState(before)
+ target, _ := durable.EncodeState(after)
+ record := journalRecord{
+ TransitionID: "repository-program/advance",
+ AllowedStateFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct},
+ Admission: protocol.Admission{RequiredCapabilities: []catalog.Capability{
+ catalog.CapabilityRepositoryWrite,
+ }},
+ Mutations: []ports.ResourceMutation{{
+ Path: "/controller/state.json", PriorExists: true, Prior: prior, Target: target,
+ StateFacets: []model.StateFacet{model.StateFacetProduct},
+ }},
+ }
+ if _, err := recoveryStateFacets(record, "recovery.resume", model.InvocationContext{}, nil); err == nil || !strings.Contains(err.Error(), "FACET_OWNERSHIP_VIOLATION") {
+ t.Fatalf("recovery widened admission-bound facets: %v", err)
+ }
+}
+
func TestJournalRejectsReceiptFacetMismatch(t *testing.T) {
err := validateCommittedMutationFacts(catalog.EventOwnedLocal, []ports.ResourceMutation{{StateFacets: []model.StateFacet{model.StateFacetControl}}}, []model.StateFacet{model.StateFacetProduct}, nil)
if err == nil || !strings.Contains(err.Error(), "do not match staged mutation facets") {
diff --git a/boatstack/program_effects_test.go b/boatstack/program_effects_test.go
index 3a0efbc..7e27b2e 100644
--- a/boatstack/program_effects_test.go
+++ b/boatstack/program_effects_test.go
@@ -90,7 +90,7 @@ func (protocolStateRuntime) RuntimeManifest(context.Context) (delivery.ProgramRu
SourcePhases: []delivery.ProtocolPhase{delivery.PhaseActive}, TargetPhases: []delivery.ProtocolPhase{delivery.PhaseActive},
ObjectiveKinds: []delivery.ObjectiveKind{delivery.ObjectiveVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"},
Authority: []delivery.AuthorityClass{delivery.AuthorityHuman, delivery.AuthorityRepository},
- RequiredCapabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, RequiredEvidence: []string{"snapshot", "objective"},
+ RequiredCapabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityProductMutate}, RequiredEvidence: []string{"snapshot", "objective"},
OwnedResources: []string{resource}, OwnedFacets: []delivery.StateFacet{delivery.StateFacetControl, delivery.StateFacetProduct},
StateEffect: delivery.StateEffect{Kind: delivery.StateEffectAssignments, Assignments: []delivery.StateAssignment{{Facet: "delivery", Value: &published}}},
Effect: publishEffect, LocalEffects: []delivery.EffectID{publishEffect}, Idempotent: true,
@@ -109,7 +109,7 @@ func (protocolStateRuntime) RuntimeManifest(context.Context) (delivery.ProgramRu
ObjectiveContracts: []delivery.ObjectiveContract{{ObjectiveKind: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, string(model.DeliveryPublished))}}},
Transitions: []delivery.Transition{publish, recover}, OwnedResources: []string{resource},
Effects: []string{string(publishEffect), string(recoverEffect)}, Verifiers: []string{"fixture.state.published", "fixture.state.recovered"},
- Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute},
+ Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityProductMutate},
ConfigurationSchema: json.RawMessage(`{"type":"object"}`), PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt",
}, nil
}
diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md
index 92ae93f..c29513f 100644
--- a/docs/architecture/boatstack-v2-transition-catalog.md
+++ b/docs/architecture/boatstack-v2-transition-catalog.md
@@ -7,39 +7,39 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w
| Transition | Origin | Owner | Selection | Class | Source phases | Target phases | Authority | Required capabilities | Parameters | Owned resources | Verifier | Recovery | Cost |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
-| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `repository.write` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` |
-| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` |
-| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `repository.write` | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `repository.write` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` |
+| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` |
+| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `repository.write` | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` |
| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` |
-| `engagement.release` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` |
-| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` |
+| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` |
+| `engagement.release` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` |
+| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` |
| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | `product.mutate`, `repository.write` | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` |
| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `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`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` |
+| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `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` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` |
| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` |
| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` |
| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` |
| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` |
-| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` |
-| `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` |
-| `installation.update` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` |
-| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | `repository.write` | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` |
-| `objective.bind` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `product.mutate`, `repository.write` | `objective_kind*`, `delivery_id*` | `objective` | `verifier:fresh-observation:objective.bind` | `recovery.resume` | `declared-neutral` |
+| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` |
+| `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` |
+| `installation.update` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` |
+| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | `repository.write` | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` |
+| `objective.bind` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `product.mutate`, `repository.write` | `objective_kind*`, `delivery_id*` | `objective` | `verifier:fresh-observation:objective.bind` | `recovery.resume` | `declared-neutral` |
| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` |
| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` |
| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` |
@@ -54,14 +54,14 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w
| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` |
| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `publication.prepare`, `repository.write` | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` |
| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `command.execute`, `product.mutate`, `repository.write` | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` |
-| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` |
-| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` |
-| `repository.attach` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` |
-| `repository.detach` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | `repository.write` | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` |
-| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` |
-| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`85ee13e3604eee43bde7cbdfa48de4a5e15b47ea2987f915f0a9463be5312ab6` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` |
+| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` |
+| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` |
+| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` |
+| `repository.attach` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` |
+| `repository.detach` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | `product.mutate`, `repository.write` | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` |
+| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` |
+| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` |
| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` |
| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` |
| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`43b24be10c7c809c0198207dc0fecb6551669b0a15568e0362a074ee118f83f7` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` |
diff --git a/release-notes/2026-08-12-declarative-state-effect-boundaries.md b/release-notes/2026-08-12-declarative-state-effect-boundaries.md
new file mode 100644
index 0000000..ba87bc3
--- /dev/null
+++ b/release-notes/2026-08-12-declarative-state-effect-boundaries.md
@@ -0,0 +1,10 @@
+### Bind declarative effects to admitted control law
+
+Product-state effects now require product mutation authority based on their
+owned facets. Declarative assignments must satisfy every target condition for
+the affected facet, and recovery reconstructs its write boundary from the
+admitted transition instead of mutable journal data.
+
+The declarative state-effect change keeps the existing journal schema, so an
+installation update can resume its pending transaction after runtime
+activation.