diff --git a/.github/tests/test_detached_supervision.py b/.github/tests/test_detached_supervision.py index ca87217..c5fc49b 100644 --- a/.github/tests/test_detached_supervision.py +++ b/.github/tests/test_detached_supervision.py @@ -187,6 +187,11 @@ def test_detached_installation_and_engaged_guard_use_the_same_kernel(self) -> No ), ) + self.helper_json( + "apply", "--repo", self.repo, "--transition", "goal.configure", + *self.goal_flags(), "--human", "contract", + "--param", "goal_kind=approved-plan", "--param", "delivery_id=bootstrap", + ) self.helper_json( "apply", "--repo", self.repo, "--transition", "engagement.begin", *self.goal_flags(), "--repository-authority", @@ -233,6 +238,12 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) - "init", "--repo", self.repo, *goal, *flow, "--human", "contract", "--param", f"config_path={config}", ) + self.helper_json( + "apply", "--repo", self.repo, "--transition", "goal.configure", + *goal, *flow, "--human", "contract", + "--param", "goal_kind=open-or-updated-pr", + "--param", "delivery_id=codex-driver-authority-triggers", + ) self.helper_json( "apply", "--repo", self.repo, "--transition", "engagement.begin", *goal, *flow, "--repository-authority", @@ -355,6 +366,14 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali for field in ('"admission"', '"receipt"', '"snapshot"', '"target_fingerprint"', '"recovery"'): self.assertIn(field, initialized_process.stdout) + configured = self.helper_json( + "apply", "--repo", self.repo, "--transition", "goal.configure", + *goal, *flow, *actor, + "--param", "goal_kind=open-or-updated-pr", + "--param", "delivery_id=preserve-repository-authority-context", + ) + self.assertEqual(configured["receipt"]["transition_id"], "goal.configure") + engagement = self.helper_json( "next", "--repo", self.repo, *goal, *flow, *actor, "--repository-authority", diff --git a/.github/tests/test_repository_contract.py b/.github/tests/test_repository_contract.py index ac67b1c..0472cda 100644 --- a/.github/tests/test_repository_contract.py +++ b/.github/tests/test_repository_contract.py @@ -452,6 +452,12 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) - "--goal-id", "bootstrap", "--goal-kind", "approved-plan", "--delivery", "bootstrap", ) + self.run_command( + launcher, "apply", "--repo", repository, + "--transition", "goal.configure", *goal, + "--human", "contract", "--param", "goal_kind=approved-plan", + "--param", "delivery_id=bootstrap", env=env, + ) self.run_command( launcher, "apply", "--repo", repository, "--transition", "engagement.begin", *goal, diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index d53776d..1db995d 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -200,11 +200,7 @@ func classifyCommand(command string) (surfaces.Operation, catalog.TransitionID, return surfaces.OperationGuard, "", nil, nil } if transition, ok := aliases[command]; ok { - defaults := map[string]string{} - if command == "init" { - defaults["goal-kind"], defaults["delivery"], defaults["goal-id"] = string(model.GoalApprovedPlan), "bootstrap", "bootstrap" - } - return surfaces.OperationApply, transition, defaults, nil + return surfaces.OperationApply, transition, nil, nil } return "", "", nil, fmt.Errorf("unknown command %q", command) } diff --git a/boatstack/control/control.go b/boatstack/control/control.go index 0411137..65d3e66 100644 --- a/boatstack/control/control.go +++ b/boatstack/control/control.go @@ -25,6 +25,7 @@ type AuthorityClass = catalog.AuthorityClass type FacetCondition = catalog.FacetCondition type SelectionClass = catalog.SelectionClass type GoalContract = catalog.GoalContract +type GoalScope = catalog.GoalScope type EffectID = catalog.EffectID type Prescription = catalog.Prescription type ParameterSpec = catalog.ParameterSpec @@ -56,6 +57,8 @@ const ( SelectionExplicitOnly = catalog.SelectionExplicitOnly SelectionObservedExternal = catalog.SelectionObservedExternal + GoalScopeOptionalPreserve = catalog.GoalScopeOptionalPreserve + GoalApprovedPlan = model.GoalApprovedPlan GoalVerified = model.GoalVerified GoalOpenPR = model.GoalOpenPR diff --git a/boatstack/core/transitions.json b/boatstack/core/transitions.json index 1817655..783d370 100644 --- a/boatstack/core/transitions.json +++ b/boatstack/core/transitions.json @@ -699,7 +699,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 15, "allows_identity_rebind": true }, @@ -869,7 +869,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 12, "allows_identity_rebind": true }, @@ -1211,7 +1211,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 20 }, { @@ -1392,7 +1392,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 25 }, { @@ -1548,7 +1548,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 4 }, { @@ -1720,7 +1720,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 22 }, { @@ -1925,7 +1925,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 60 }, { @@ -2067,7 +2067,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 3 }, { @@ -2261,7 +2261,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 11 }, { @@ -2417,7 +2417,8 @@ { "facet": "goal", "statuses": [ - "known" + "known", + "absent" ] }, { @@ -2470,7 +2471,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 65 }, { @@ -2541,7 +2542,7 @@ {"facet": "recovery", "statuses": ["known"], "values": ["none"]}, {"facet": "transaction", "statuses": ["known"], "values": ["none"]}, {"facet": "terminal", "statuses": ["known"], "values": ["nonterminal", "stale", "established"]}, - {"facet": "goal", "statuses": ["known"]}, + {"facet": "goal", "statuses": ["known", "absent"]}, {"facet": "configuration", "statuses": ["known"], "values": ["verified"]} ], "admission_predicate": "predicate:exact-admission:installation.reconcile-update", @@ -2567,7 +2568,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {"reconciles_program": true}, + "policy": {"reconciles_program": true, "goal_scope": "optional-preserve"}, "priority": 1 }, { @@ -2750,7 +2751,8 @@ "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", "policy": { - "reconciles_program": true + "reconciles_program": true, + "goal_scope": "optional-preserve" }, "priority": 1 }, @@ -3038,7 +3040,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 2 }, { @@ -3182,7 +3184,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 3 }, { @@ -3321,7 +3323,7 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": {"goal_scope": "optional-preserve"}, "priority": 5 }, { diff --git a/boatstack/internal/effects/artifacts.go b/boatstack/internal/effects/artifacts.go index a847135..5b44d22 100644 --- a/boatstack/internal/effects/artifacts.go +++ b/boatstack/internal/effects/artifacts.go @@ -89,9 +89,13 @@ type publicationPreview struct { func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admission, transition catalog.Transition, state *durable.State) ([]ports.ResourceMutation, error) { var mutations []ports.ResourceMutation - deliveryID, err := safeSegment(admission.Goal.DeliveryID, "delivery identity") - if err != nil { - return nil, err + var deliveryID string + if transitionUsesDeliveryArtifacts(transition.ID) { + var err error + deliveryID, err = safeSegment(admission.Goal.DeliveryID, "delivery identity") + if err != nil { + return nil, err + } } artifactRoot := filepath.Join(layout.RepositoryRoot, ".boatstack") switch transition.ID { @@ -291,6 +295,18 @@ func prepareArtifacts(layout ports.ControllerLayout, admission protocol.Admissio return mutations, nil } +func transitionUsesDeliveryArtifacts(id catalog.TransitionID) bool { + switch id { + case "plan.create", "plan.amend", "plan.validate", "plan.approve", "plan.approve-amendment", + "evidence.approval.revoke", "gate.build.record", "gate.test.record", "gate.review.record", + "gate.change.record", "gate.journey.record", "evidence.visual.attach", "publication.preview", + "publication.execute", "publication.correct": + return true + default: + return false + } +} + func loadPublicationPreview(path string) (publicationPreview, error) { raw, err := os.ReadFile(path) if err != nil { diff --git a/boatstack/internal/effects/integration_test.go b/boatstack/internal/effects/integration_test.go index 0ddadf4..0ae2af4 100644 --- a/boatstack/internal/effects/integration_test.go +++ b/boatstack/internal/effects/integration_test.go @@ -184,6 +184,7 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing {Name: "source_revision", Value: "external-config-fixture"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: initialPath}, {Name: "config_sha256", Value: configFingerprint(t, initialConfig)}, }) + apply("goal.configure", human, false, protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}}) apply("repository.attach", human, false, protocol.Parameters{{Name: "topology", Value: "detached"}, {Name: "config_authority", Value: "external"}}) resolver, err := plant.NewResolver(externalRoot) if err != nil { @@ -273,6 +274,9 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { if initialized.Receipt == nil || initialized.Receipt.ProgramFingerprint != oldProgram.Fingerprint() { t.Fatalf("initial receipt did not freeze old program: %#v", initialized.Receipt) } + if initialized.Snapshot == nil || initialized.Snapshot.Goal.Status != model.FactAbsent || initialized.Receipt.GoalStatus != model.FactAbsent || initialized.Receipt.GoalID != "" { + t.Fatalf("installation initialization invented product intent: %#v", initialized) + } newProgram := testProgram() newKernel, err := boatstack.NewKernel(externalRoot, newProgram) @@ -342,6 +346,9 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { reconciled.Snapshot.Program.Value != model.ProgramCurrent || reconciled.Snapshot.Phase.Value != model.PhaseObserved { t.Fatalf("reconciliation did not establish exact program identity: %#v", reconciled) } + if reconciled.Snapshot.Goal.Status != model.FactAbsent || reconciled.Receipt.GoalStatus != model.FactAbsent || reconciled.Receipt.GoalID != "" { + t.Fatalf("reconcile-update invented product intent: %#v", reconciled) + } afterSuccess, err := os.ReadFile(layout.StatePath) if err != nil { t.Fatal(err) @@ -356,6 +363,20 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { if !bytes.Equal(afterSuccess, afterReplay) { t.Fatal("rejected repeated reconciliation mutated durable state") } + updated, err := newKernel.Handle(ctx, surfaces.Request{ + SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-current-update", + FlowID: "flow-program-drift", Goal: model.Goal{ID: "ignored-command-goal", Kind: model.GoalOpenPR, DeliveryID: "ignored"}, + TransitionID: "installation.update", Authority: human, + Parameters: protocol.Parameters{ + {Name: "source_revision", Value: "program-current"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, + }, + }) + if err != nil { + t.Fatalf("current-program update after reconciliation: %v", err) + } + if updated.Snapshot == nil || updated.Snapshot.Goal.Status != model.FactAbsent || updated.Receipt == nil || updated.Receipt.GoalStatus != model.FactAbsent || updated.Receipt.GoalID != "" { + t.Fatalf("reconcile to update composition invented product intent: %#v", updated) + } } func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *testing.T) { @@ -420,6 +441,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test {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("goal.configure", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}}) apply("engagement.begin", authority(catalog.AuthorityRepository), nil) planPath := filepath.Join(t.TempDir(), "plan.md") planRaw := []byte("# Extension plan\n") @@ -547,6 +569,7 @@ func TestConcreteWorkflowPreservesConfigurationProofAndGoalTerminals(t *testing. {Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, }) + apply(approvedGoal, "goal.configure", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "goal_kind", Value: string(approvedGoal.Kind)}, {Name: "delivery_id", Value: approvedGoal.DeliveryID}}) apply(approvedGoal, "engagement.begin", authority(catalog.AuthorityRepository), nil) updatedConfigPath := filepath.Join(t.TempDir(), "project-v2-updated.json") @@ -677,6 +700,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) {Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_path", Value: executable}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configSource}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, }) + apply(sourceInvocation, "goal.configure", human, protocol.Parameters{{Name: "goal_kind", Value: string(goal.Kind)}, {Name: "delivery_id", Value: goal.DeliveryID}}) run(t, repository, "git", "add", ".boatstack/project.json") run(t, repository, "git", "commit", "-q", "-m", "install V2 configuration") repositoryAuthority := func(path string) protocol.AuthorityBundle { @@ -768,7 +792,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) if err != nil { t.Fatal(err) } - if strings.Count(strings.TrimSpace(string(eventBytes)), "\n")+1 != 7 { + if strings.Count(strings.TrimSpace(string(eventBytes)), "\n")+1 != 8 { t.Fatalf("shared flow telemetry lost a cross-worktree transition: %s", eventBytes) } } diff --git a/boatstack/internal/effects/receipts.go b/boatstack/internal/effects/receipts.go index b07016c..a4c14e3 100644 --- a/boatstack/internal/effects/receipts.go +++ b/boatstack/internal/effects/receipts.go @@ -136,6 +136,8 @@ type processEvent struct { Sequence uint64 `json:"sequence"` Timestamp time.Time `json:"timestamp"` GoalID string `json:"goal_id"` + GoalScope string `json:"goal_scope,omitempty"` + GoalStatus string `json:"goal_status,omitempty"` TransitionID string `json:"transition_id"` ProgramFingerprint string `json:"program_fingerprint"` SourceFingerprint string `json:"source_fingerprint"` @@ -181,6 +183,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, + GoalScope: string(receipt.GoalScope), GoalStatus: string(receipt.GoalStatus), 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 070bc4e..a735176 100644 --- a/boatstack/internal/effects/recovery_test.go +++ b/boatstack/internal/effects/recovery_test.go @@ -164,7 +164,8 @@ func TestRestartRecoveryRollsBackExactPriorBytesAndArchivesJournal(t *testing.T) if err != nil { t.Fatal(err) } - if result.Target.Phase.Value != model.PhaseDormant || result.Target.Recovery.Value != model.RecoveryNone || result.Receipt.ID == "" { + if result.Target.Phase.Value != model.PhaseDormant || result.Target.Recovery.Value != model.RecoveryNone || result.Target.Goal.Status != model.FactAbsent || + result.Receipt.ID == "" || result.Receipt.GoalStatus != model.FactAbsent || result.Receipt.GoalID != "" { t.Fatalf("rollback target=%#v receipt=%q", result.Target, result.Receipt.ID) } layout, _, _ := resolver.ResolveLayout(ctx, restartedInvocation) diff --git a/boatstack/internal/effects/state_reducer.go b/boatstack/internal/effects/state_reducer.go index dfcaacf..5eb3cfa 100644 --- a/boatstack/internal/effects/state_reducer.go +++ b/boatstack/internal/effects/state_reducer.go @@ -11,11 +11,17 @@ import ( 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": + if transition.Policy.GoalScope == catalog.GoalScopeOptionalPreserve { + if configured && state.Goal != admission.Goal { + return fmt.Errorf("transition %q must preserve the exact configured product goal", transition.ID) + } + if !configured && admission.Goal.Validate() == nil { + return fmt.Errorf("transition %q cannot create product intent from verified absence", transition.ID) + } + } else if transition.ID == "goal.configure" { state.Goal = admission.Goal configured = true - default: + } else { if configured && state.Goal != admission.Goal { return fmt.Errorf("transition %q cannot replace configured goal; use goal.configure", transition.ID) } diff --git a/boatstack/internal/kernel/catalog/transition.go b/boatstack/internal/kernel/catalog/transition.go index ae3a954..f229b1f 100644 --- a/boatstack/internal/kernel/catalog/transition.go +++ b/boatstack/internal/kernel/catalog/transition.go @@ -176,15 +176,22 @@ type InterruptionContract struct { ResumptionPredicate string `json:"resumption_predicate"` } +type GoalScope string + +const GoalScopeOptionalPreserve GoalScope = "optional-preserve" + +func (s GoalScope) Valid() bool { return s == "" || s == GoalScopeOptionalPreserve } + 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"` - RechecksExternalState bool `json:"rechecks_external_state,omitempty"` + 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"` + RechecksExternalState bool `json:"rechecks_external_state,omitempty"` + GoalScope GoalScope `json:"goal_scope,omitempty"` } // FacetCondition is an executable, serializable predicate over one canonical @@ -280,6 +287,9 @@ func (t Transition) ImplicitlySelectable() bool { } func (t Transition) SupportsGoal(goal model.Goal) bool { + if t.Policy.GoalScope == GoalScopeOptionalPreserve { + return true + } if len(t.GoalKinds) == 0 { return true } @@ -309,6 +319,9 @@ func (t Transition) SourceMatches(snapshot model.Snapshot) bool { return false } } + if t.Policy.GoalScope == GoalScopeOptionalPreserve && snapshot.Goal.Status != model.FactKnown && snapshot.Goal.Status != model.FactAbsent { + return false + } return true } @@ -485,6 +498,12 @@ func validateTransition(t Transition) error { } managedOperations[operation] = true } + if !t.Policy.GoalScope.Valid() { + return fmt.Errorf("%s: invalid goal scope %q", t.ID, t.Policy.GoalScope) + } + if t.Policy.GoalScope == GoalScopeOptionalPreserve && t.Policy.BindsRequestedGoal { + return fmt.Errorf("%s: optional-preserve maintenance cannot bind a requested product goal", t.ID) + } 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) } diff --git a/boatstack/internal/kernel/engine/engine.go b/boatstack/internal/kernel/engine/engine.go index 01d8415..a88ee2b 100644 --- a/boatstack/internal/kernel/engine/engine.go +++ b/boatstack/internal/kernel/engine/engine.go @@ -66,11 +66,20 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution return Resolution{}, fmt.Errorf("observer returned a different invocation identity") } goal := request.Goal - if err := goal.Validate(); err != nil { - if snapshot.Goal.Status != model.FactKnown { + if transition, requested := e.registry.Lookup(request.Requested); requested && transition.Policy.GoalScope == catalog.GoalScopeOptionalPreserve { + goal, err = protocol.GoalForTransition(snapshot, request.Goal, transition) + if err != nil { + return Resolution{}, err + } + } else if err := goal.Validate(); err != nil { + switch snapshot.Goal.Status { + case model.FactKnown: + goal = snapshot.Goal.Value + case model.FactAbsent: + goal = model.Goal{} + default: return Resolution{}, fmt.Errorf("no valid requested or configured goal: %w", err) } - goal = snapshot.Goal.Value } now := e.clock.Now() if err := request.Authority.Validate(now); err != nil { @@ -78,6 +87,13 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution } decision := e.control.Resolve(snapshot, goal, request.Authority.Set(now), request.Requested) if decision.Kind == supervisor.DecisionPrescribed && decision.Transition != nil { + goal, err = protocol.GoalForTransition(snapshot, goal, *decision.Transition) + if err != nil { + decision.Kind = supervisor.DecisionRefused + decision.Reason = err.Error() + decision.Transition = nil + return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil + } if applicabilityErr := protocol.ValidateApplicability(snapshot, goal, *decision.Transition, request.Authority, request.Parameters, now); applicabilityErr != nil { if protocol.IsMissingParameter(applicabilityErr) { decision.Kind = supervisor.DecisionCandidate @@ -182,6 +198,9 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe if canonicalErr != nil { return result, canonicalErr } + if err := validateReplayGoalState(prior, snapshot); err != nil { + return result, err + } if !replayStateSettled(snapshot) { return result, ReplayRecoveryError{ReceiptID: prior.ID} } @@ -233,6 +252,9 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe if canonicalErr != nil { return result, canonicalErr } + if err := validateReplayGoalState(prior, current); err != nil { + return result, err + } if !replayStateSettled(current) { return result, ReplayRecoveryError{ReceiptID: prior.ID} } @@ -264,6 +286,9 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe if err := validateReplayRequest(prior, request, e.programFingerprint); err != nil { return result, err } + if err := validateReplayGoalState(prior, lockedSnapshot); err != nil { + return result, err + } if !replayStateSettled(lockedSnapshot) { return result, ReplayRecoveryError{ReceiptID: prior.ID} } @@ -370,8 +395,10 @@ func validateReplayRequest(prior protocol.TransitionReceipt, request ApplyReques if prior.FlowID != request.FlowID { return fmt.Errorf("idempotency receipt belongs to flow %q, not %q", prior.FlowID, request.FlowID) } - if request.Goal.Validate() == nil && (prior.GoalID != request.Goal.ID || prior.GoalKind != request.Goal.Kind || prior.DeliveryID != request.Goal.DeliveryID) { - return fmt.Errorf("idempotency receipt belongs to a different configured goal") + if prior.GoalScope != catalog.GoalScopeOptionalPreserve && request.Goal.Validate() == nil { + if prior.GoalID != request.Goal.ID || prior.GoalKind != request.Goal.Kind || prior.DeliveryID != request.Goal.DeliveryID { + return fmt.Errorf("idempotency receipt belongs to a different configured goal") + } } if request.Requested != "" && prior.TransitionID != request.Requested { return fmt.Errorf("idempotency receipt belongs to transition %q, not %q", prior.TransitionID, request.Requested) @@ -379,6 +406,26 @@ func validateReplayRequest(prior protocol.TransitionReceipt, request ApplyReques return nil } +func validateReplayGoalState(prior protocol.TransitionReceipt, snapshot model.Snapshot) error { + if prior.GoalScope != catalog.GoalScopeOptionalPreserve { + return nil + } + switch prior.GoalStatus { + case model.FactKnown: + if snapshot.Goal.Status != model.FactKnown || snapshot.Goal.Value.ID != prior.GoalID || + snapshot.Goal.Value.Kind != prior.GoalKind || snapshot.Goal.Value.DeliveryID != prior.DeliveryID { + return fmt.Errorf("idempotency receipt product-goal binding no longer matches current state") + } + case model.FactAbsent: + if snapshot.Goal.Status != model.FactAbsent { + return fmt.Errorf("idempotency receipt preserved an absent product goal, but current state is %q", snapshot.Goal.Status) + } + default: + return fmt.Errorf("idempotency receipt has invalid preserved product-goal status %q", prior.GoalStatus) + } + return nil +} + func replayStateSettled(snapshot model.Snapshot) bool { return snapshot.Phase.Status == model.FactKnown && snapshot.Phase.Value != model.PhaseRecovery && snapshot.Recovery.Status == model.FactKnown && snapshot.Recovery.Value == model.RecoveryNone && diff --git a/boatstack/internal/kernel/engine/maintenance_replay_test.go b/boatstack/internal/kernel/engine/maintenance_replay_test.go new file mode 100644 index 0000000..330f9a8 --- /dev/null +++ b/boatstack/internal/kernel/engine/maintenance_replay_test.go @@ -0,0 +1,44 @@ +package engine + +import ( + "testing" + + "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" + "github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol" +) + +func TestMaintenanceReplayBindsDurableGoalState(t *testing.T) { + // control-law: maintenance-replay-preserves-verified-product-goal-state + configured := model.Goal{ID: "configured", Kind: model.GoalOpenPR, DeliveryID: "delivery"} + commandGoal := model.Goal{ID: "command", Kind: model.GoalApprovedPlan, DeliveryID: "other"} + request := ApplyRequest{ResolveRequest: ResolveRequest{Goal: commandGoal}, FlowID: "flow"} + + tests := []struct { + name string + receipt protocol.TransitionReceipt + fact model.Fact[model.Goal] + wantErr bool + }{ + {name: "absent survives command goal and retry", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactAbsent}, fact: model.Fact[model.Goal]{Status: model.FactAbsent}}, + {name: "known survives conflicting command goal and retry", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactKnown, GoalID: configured.ID, GoalKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}}, + {name: "absent cannot replay after product goal appears", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactAbsent}, fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}, wantErr: true}, + {name: "known cannot replay after product goal changes", receipt: protocol.TransitionReceipt{GoalScope: catalog.GoalScopeOptionalPreserve, GoalStatus: model.FactKnown, GoalID: configured.ID, GoalKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: commandGoal}, wantErr: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.receipt.FlowID = request.FlowID + test.receipt.ProgramFingerprint = syntheticProgramFingerprint + if err := validateReplayRequest(test.receipt, request, syntheticProgramFingerprint); err != nil { + t.Fatalf("command goal affected maintenance replay identity: %v", err) + } + err := validateReplayGoalState(test.receipt, model.Snapshot{Observation: model.Observation{Goal: test.fact}}) + if test.wantErr && err == nil { + t.Fatal("changed durable product-goal state was accepted for replay") + } + if !test.wantErr && err != nil { + t.Fatalf("unchanged durable product-goal state rejected: %v", err) + } + }) + } +} diff --git a/boatstack/internal/kernel/protocol/admission.go b/boatstack/internal/kernel/protocol/admission.go index 082322a..d1ce452 100644 --- a/boatstack/internal/kernel/protocol/admission.go +++ b/boatstack/internal/kernel/protocol/admission.go @@ -25,6 +25,8 @@ type Admission struct { SourcePhase model.ProtocolPhase `json:"source_phase"` Invocation model.InvocationContext `json:"invocation"` Goal model.Goal `json:"goal"` + GoalScope catalog.GoalScope `json:"goal_scope,omitempty"` + GoalStatus model.FactStatus `json:"goal_status,omitempty"` Authority AuthorityBundle `json:"authority"` Parameters Parameters `json:"parameters,omitempty"` Evidence []string `json:"evidence"` @@ -34,6 +36,11 @@ type Admission struct { } func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, authority AuthorityBundle, parameters Parameters, now time.Time, lifetime time.Duration) (Admission, error) { + var err error + goal, err = GoalForTransition(snapshot, goal, transition) + if err != nil { + return Admission{}, err + } if err := ValidateApplicability(snapshot, goal, transition, authority, parameters, now); err != nil { return Admission{}, err } @@ -44,9 +51,12 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T a := Admission{ SchemaVersion: AdmissionSchemaVersion, TransitionID: transition.ID, TransitionVersion: transition.Version, ProgramFingerprint: snapshot.ProgramFingerprint, SnapshotFingerprint: snapshot.Fingerprint, SourceRevision: sourceRevision, WorktreeFingerprint: worktreeFingerprint, - SourcePhase: snapshot.Phase.Value, Invocation: snapshot.Invocation, Goal: goal, Authority: authority.canonical(), + SourcePhase: snapshot.Phase.Value, Invocation: snapshot.Invocation, Goal: goal, GoalScope: transition.Policy.GoalScope, Authority: authority.canonical(), Evidence: append([]string(nil), transition.RequiredEvidence...), Parameters: parameters.Canonical(), IssuedAt: now.UTC(), ExpiresAt: now.Add(lifetime).UTC(), } + if transition.Policy.GoalScope == catalog.GoalScopeOptionalPreserve { + a.GoalStatus = snapshot.Goal.Status + } if snapshot.RecordedProgramFingerprint != "" && snapshot.RecordedProgramFingerprint != snapshot.ProgramFingerprint { a.PriorProgramFingerprint = snapshot.RecordedProgramFingerprint delta, err := ProgramDeltaFingerprint(snapshot.RecordedProgramFingerprint, snapshot.ProgramFingerprint) @@ -75,6 +85,28 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T return a, nil } +// GoalForTransition binds maintenance to verified durable product-goal state. +// Command-scoped product intent is deliberately irrelevant to maintenance. +func GoalForTransition(snapshot model.Snapshot, requested model.Goal, transition catalog.Transition) (model.Goal, error) { + if transition.Policy.GoalScope != catalog.GoalScopeOptionalPreserve { + if err := requested.Validate(); err != nil { + return model.Goal{}, err + } + return requested, nil + } + switch snapshot.Goal.Status { + case model.FactKnown: + if err := snapshot.Goal.Value.Validate(); err != nil { + return model.Goal{}, fmt.Errorf("transition %q has invalid configured product-goal evidence: %w", transition.ID, err) + } + return snapshot.Goal.Value, nil + case model.FactAbsent: + return model.Goal{}, nil + default: + return model.Goal{}, fmt.Errorf("transition %q requires known or verified-absent product goal evidence", transition.ID) + } +} + // ValidateApplicability is the deterministic transition law shared by // resolution and admission. A transition that fails here must never be // reported as prescribed for the same snapshot and context. @@ -85,7 +117,9 @@ func ValidateApplicability(snapshot model.Snapshot, goal model.Goal, transition if err := snapshot.Invocation.Validate(true); err != nil { return err } - if err := goal.Validate(); err != nil { + var err error + goal, err = GoalForTransition(snapshot, goal, transition) + if err != nil { return err } if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) { @@ -135,6 +169,9 @@ func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, tra if a.TransitionID != transition.ID || a.TransitionVersion != transition.Version { return fmt.Errorf("admission %q is bound to a different transition", a.ID) } + if a.GoalScope != transition.Policy.GoalScope { + return fmt.Errorf("admission %q is bound to a different product-goal scope", a.ID) + } if a.SnapshotFingerprint != snapshot.Fingerprint { return fmt.Errorf("admission %q is stale: snapshot changed", a.ID) } @@ -163,6 +200,9 @@ func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, tra if a.Goal != goal { return fmt.Errorf("admission %q is bound to a different goal", a.ID) } + if a.GoalScope == catalog.GoalScopeOptionalPreserve && (a.GoalStatus != snapshot.Goal.Status || (a.GoalStatus == model.FactKnown && a.Goal != snapshot.Goal.Value)) { + return fmt.Errorf("admission %q is stale: product goal changed", a.ID) + } if err := a.Authority.Validate(now); err != nil { return err } @@ -316,7 +356,23 @@ func (a Admission) ValidateIdentity() error { if err := a.Invocation.Validate(true); err != nil { return err } - if err := a.Goal.Validate(); err != nil { + if !a.GoalScope.Valid() { + return fmt.Errorf("admission has invalid product-goal scope %q", a.GoalScope) + } + if a.GoalScope == catalog.GoalScopeOptionalPreserve { + switch a.GoalStatus { + case model.FactKnown: + if err := a.Goal.Validate(); err != nil { + return err + } + case model.FactAbsent: + if a.Goal.Validate() == nil { + return fmt.Errorf("maintenance admission cannot bind product intent to verified absence") + } + default: + return fmt.Errorf("maintenance admission requires known or verified-absent product goal status") + } + } else if err := a.Goal.Validate(); err != nil { return err } identity := a diff --git a/boatstack/internal/kernel/protocol/maintenance_goal_test.go b/boatstack/internal/kernel/protocol/maintenance_goal_test.go new file mode 100644 index 0000000..8a5e7b4 --- /dev/null +++ b/boatstack/internal/kernel/protocol/maintenance_goal_test.go @@ -0,0 +1,42 @@ +package protocol + +import ( + "testing" + + "github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/kernel/model" +) + +func TestMaintenanceGoalBindingUsesOnlyDurableProductState(t *testing.T) { + // control-law: maintenance-admission-is-independent-from-command-product-intent + transition := catalog.Transition{ID: "installation.update", Policy: catalog.PolicyContract{GoalScope: catalog.GoalScopeOptionalPreserve}} + configured := model.Goal{ID: "configured", Kind: model.GoalOpenPR, DeliveryID: "delivery"} + conflicting := model.Goal{ID: "command", Kind: model.GoalApprovedPlan, DeliveryID: "other"} + + tests := []struct { + name string + fact model.Fact[model.Goal] + request model.Goal + want model.Goal + wantFail bool + }{ + {name: "goal absent", fact: model.Fact[model.Goal]{Status: model.FactAbsent}, request: conflicting}, + {name: "goal known and preserved", fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}, want: configured}, + {name: "conflicting command-scoped goal ignored", fact: model.Fact[model.Goal]{Status: model.FactKnown, Value: configured}, request: conflicting, want: configured}, + {name: "unknown goal fails closed", fact: model.Fact[model.Goal]{Status: model.FactUnknown}, request: conflicting, wantFail: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := GoalForTransition(model.Snapshot{Observation: model.Observation{Goal: test.fact}}, test.request, transition) + if test.wantFail { + if err == nil { + t.Fatalf("unknown product-goal evidence produced %#v", got) + } + return + } + if err != nil || got != test.want { + t.Fatalf("goal binding = %#v, %v; want %#v", got, err, test.want) + } + }) + } +} diff --git a/boatstack/internal/kernel/protocol/receipt.go b/boatstack/internal/kernel/protocol/receipt.go index 453e999..3be4c3e 100644 --- a/boatstack/internal/kernel/protocol/receipt.go +++ b/boatstack/internal/kernel/protocol/receipt.go @@ -36,6 +36,8 @@ type TransitionReceipt struct { GoalID string `json:"goal_id"` GoalKind model.GoalKind `json:"goal_kind"` DeliveryID string `json:"delivery_id"` + GoalScope catalog.GoalScope `json:"goal_scope,omitempty"` + GoalStatus model.FactStatus `json:"goal_status,omitempty"` SourceFingerprint string `json:"source_fingerprint"` TargetFingerprint string `json:"target_fingerprint"` AuthorityClasses []string `json:"authority_classes"` @@ -68,6 +70,7 @@ func NewReceipt(flowID string, sequence uint64, admission Admission, transition receipt := TransitionReceipt{ SchemaVersion: ReceiptSchemaVersion, FlowID: flowID, Sequence: sequence, TransitionID: transition.ID, TransitionVersion: transition.Version, ProgramFingerprint: admission.ProgramFingerprint, AdmissionID: admission.ID, GoalID: admission.Goal.ID, GoalKind: admission.Goal.Kind, DeliveryID: admission.Goal.DeliveryID, + GoalScope: admission.GoalScope, GoalStatus: admission.GoalStatus, SourceFingerprint: admission.SnapshotFingerprint, TargetFingerprint: target.Fingerprint, AuthorityClasses: classes, IdempotencyKey: admission.IdempotencyKey, Verifier: transition.Verifier, Outcome: outcome, Recovery: transition.Interruption.Recovery, Terminal: terminal, @@ -95,9 +98,28 @@ 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 || len(r.ProgramFingerprint) != 64 || 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.SourceFingerprint == "" || r.TargetFingerprint == "" || r.IdempotencyKey == "" || r.Verifier == "" { return fmt.Errorf("receipt has incomplete identity or evidence") } + if !r.GoalScope.Valid() { + return fmt.Errorf("receipt has invalid product-goal scope %q", r.GoalScope) + } + if r.GoalScope == catalog.GoalScopeOptionalPreserve { + switch r.GoalStatus { + case model.FactKnown: + if r.GoalID == "" || !r.GoalKind.Valid() || r.DeliveryID == "" { + return fmt.Errorf("maintenance receipt has incomplete known product-goal binding") + } + case model.FactAbsent: + if r.GoalID != "" || r.GoalKind != "" || r.DeliveryID != "" { + return fmt.Errorf("maintenance receipt invents product intent from verified absence") + } + default: + return fmt.Errorf("maintenance receipt requires known or verified-absent product-goal status") + } + } else if r.GoalID == "" || !r.GoalKind.Valid() || r.DeliveryID == "" { + return fmt.Errorf("receipt has incomplete product-goal identity") + } if r.StartedAt.IsZero() || r.CompletedAt.Before(r.StartedAt) || r.DurationNanoseconds != r.CompletedAt.Sub(r.StartedAt).Nanoseconds() { return fmt.Errorf("receipt has invalid timing evidence") } diff --git a/boatstack/internal/kernel/supervisor/supervisor.go b/boatstack/internal/kernel/supervisor/supervisor.go index fc776cb..872e4d7 100644 --- a/boatstack/internal/kernel/supervisor/supervisor.go +++ b/boatstack/internal/kernel/supervisor/supervisor.go @@ -40,7 +40,8 @@ func New(registry catalog.Registry, contracts catalog.GoalContracts) Supervisor func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority catalog.AuthoritySet, requested catalog.TransitionID) Decision { base := Decision{SnapshotFingerprint: snapshot.Fingerprint} - if err := goal.Validate(); err != nil || snapshot.Fingerprint == "" { + goalAbsent := snapshot.Goal.Status == model.FactAbsent + if (goal.Validate() != nil && !goalAbsent) || snapshot.Fingerprint == "" { base.Kind, base.Reason = DecisionUnresolved, "goal or canonical snapshot is invalid" return base } diff --git a/boatstack/internal/surfaces/render.go b/boatstack/internal/surfaces/render.go index 134b4e3..409ffc4 100644 --- a/boatstack/internal/surfaces/render.go +++ b/boatstack/internal/surfaces/render.go @@ -25,7 +25,10 @@ type CommandAST struct { } func PrescriptionCommand(transition catalog.Transition, repository string, goal model.Goal, flowID string, parameters protocol.Parameters) CommandAST { - arguments := []string{"apply", "--repo", repository, "--transition", string(transition.ID), "--goal-kind", string(goal.Kind), "--delivery", goal.DeliveryID, "--goal-id", goal.ID, "--flow", flowID} + arguments := []string{"apply", "--repo", repository, "--transition", string(transition.ID), "--flow", flowID} + if goal.Validate() == nil { + arguments = append(arguments, "--goal-kind", string(goal.Kind), "--delivery", goal.DeliveryID, "--goal-id", goal.ID) + } canonical := parameters.Canonical() for _, parameter := range canonical { arguments = append(arguments, "--param", parameter.Name+"="+parameter.Value) diff --git a/boatstack/sdk/sdk.go b/boatstack/sdk/sdk.go index 0b13156..8c3150b 100644 --- a/boatstack/sdk/sdk.go +++ b/boatstack/sdk/sdk.go @@ -48,9 +48,12 @@ const ( type TransitionID = catalog.TransitionID type Transition = catalog.Transition +type GoalScope = catalog.GoalScope type AuthorityClass = catalog.AuthorityClass const ( + GoalScopeOptionalPreserve = catalog.GoalScopeOptionalPreserve + AuthorityRepository = catalog.AuthorityRepository AuthorityHuman = catalog.AuthorityHuman AuthorityAutonomy = catalog.AuthorityAutonomy diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md index 2b42308..b468370 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 | Parameters | Owned resources | Verifier | Recovery | Cost | |---|---|---|---|---|---|---|---|---|---|---|---|---| -| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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` | +| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` | +| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `goal_kind*`, `delivery_id*` | `goal` | `verifier:fresh-observation:goal.configure` | `recovery.resume` | `declared-neutral` | -| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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.reconcile-update` | core-system:`boatstack.core@1.0.0`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `source_revision*`, `runtime_path*`, `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | 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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` | +| `goal.configure` | core-system:`boatstack.core@1.0.0`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `goal_kind*`, `delivery_id*` | `goal` | `verifier:fresh-observation:goal.configure` | `recovery.resume` | `declared-neutral` | +| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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.reconcile-update` | core-system:`boatstack.core@1.0.0`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `source_revision*`, `runtime_path*`, `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | 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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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` | @@ -54,14 +54,14 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w | `publication.observe` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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`
`5f63de0d16895f5f45cfbb85779bcadc4243b81bf336440e4516824d74f4aed8` | `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` | +| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`1dd9892b2fdd1e835d8cc15e3a26584b7ad21fea0c1cdaa404e1211841b86e4c` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `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` | diff --git a/release-notes/2026-08-11-maintenance-goal-isolation.md b/release-notes/2026-08-11-maintenance-goal-isolation.md new file mode 100644 index 0000000..728a938 --- /dev/null +++ b/release-notes/2026-08-11-maintenance-goal-isolation.md @@ -0,0 +1,3 @@ +### Keep maintenance independent from product intent + +Boatstack maintenance now binds the verified durable product-goal state as preserved context instead of requiring or creating a delivery goal. Updates and their recovery paths preserve a known goal, preserve verified absence, reject unknown goal evidence, and keep command-scoped product intent outside maintenance admission.