diff --git a/.github/tests/test_detached_supervision.py b/.github/tests/test_detached_supervision.py index 6ef2943..ca87217 100644 --- a/.github/tests/test_detached_supervision.py +++ b/.github/tests/test_detached_supervision.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json import os import subprocess @@ -247,7 +248,7 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) - "next", "--repo", self.repo, *goal, *flow, "--human", "contract", "--repository-authority", ) - self.assertEqual(progressing["decision"]["kind"], "PRESCRIBED") + self.assertEqual(progressing["decision"]["kind"], "CANDIDATE") self.assertEqual(progressing["decision"]["transition"]["id"], "plan.create") plan = Path(self.work.name) / "source-plan.md" @@ -301,7 +302,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali prescribed = self.helper_json( "next", "--repo", self.repo, *goal, *flow, *actor, ) - self.assertEqual(prescribed["decision"]["kind"], "PRESCRIBED") + self.assertEqual(prescribed["decision"]["kind"], "CANDIDATE") self.assertEqual( prescribed["decision"]["transition"]["id"], "installation.initialize" ) @@ -321,6 +322,28 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali } ) ) + canonical_config = json.loads(config.read_text()) + canonical_config["hosts"] = sorted(canonical_config["hosts"]) + canonical_config["policy"]["external_effect_authority"] = ( + "human-or-autonomy-plus-provider" + ) + config_fingerprint = hashlib.sha256( + json.dumps(canonical_config, separators=(",", ":")).encode() + ).hexdigest() + bound_initialization = self.helper_json( + "next", "--repo", self.repo, + "--transition", "installation.initialize", *goal, *flow, *actor, + "--param", f"source_revision={self._git(self.repo, 'rev-parse', 'HEAD').stdout.strip()}", + "--param", f"runtime_path={self.binary.resolve()}", + "--param", f"runtime_sha256={hashlib.sha256(self.binary.read_bytes()).hexdigest()}", + "--param", f"config_path={config}", + "--param", f"config_sha256={config_fingerprint}", + ) + self.assertEqual(bound_initialization["decision"]["kind"], "PRESCRIBED") + self.assertEqual( + bound_initialization["decision"]["transition"]["id"], + "installation.initialize", + ) initialized_process = self.run_helper( "init", "--repo", self.repo, *goal, *flow, *actor, "--param", f"config_path={config}", @@ -357,9 +380,20 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali "next", "--repo", self.repo, *goal, *flow, *actor, "--repository-authority", ) - self.assertEqual(plan["decision"]["kind"], "PRESCRIBED") + self.assertEqual(plan["decision"]["kind"], "CANDIDATE") self.assertEqual(plan["decision"]["transition"]["id"], "plan.create") + plan_source = Path(self.work.name) / "retained-authority-plan.md" + plan_source.write_text("# Retained authority\n\nContinue in one operation context.\n") + bound = self.helper_json( + "next", "--repo", self.repo, "--transition", "plan.create", + *goal, *flow, *actor, "--repository-authority", + "--param", f"source_path={plan_source}", + "--param", "delivery_id=preserve-repository-authority-context", + ) + self.assertEqual(bound["decision"]["kind"], "PRESCRIBED") + self.assertEqual(bound["decision"]["transition"]["id"], "plan.create") + def test_repository_authority_rematerialization_fails_closed_without_verified_config(self) -> None: # control-law: repository-authority-requires-exact-verified-fingerprint root = Path(self.work.name) / "unverified" diff --git a/boatstack/core/transitions.json b/boatstack/core/transitions.json index 7eeb7f2..f12414d 100644 --- a/boatstack/core/transitions.json +++ b/boatstack/core/transitions.json @@ -2672,6 +2672,7 @@ "class": "authority", "source_phases": [ "OBSERVED", + "DORMANT", "ACTIVE", "FRONTIER", "TERMINAL", diff --git a/boatstack/flow/standard/supervisor_parity_test.go b/boatstack/flow/standard/supervisor_parity_test.go index 4b0b3f0..bd35233 100644 --- a/boatstack/flow/standard/supervisor_parity_test.go +++ b/boatstack/flow/standard/supervisor_parity_test.go @@ -140,6 +140,60 @@ func TestUntargetedResolutionReconfiguresDifferentGoalAndSkipsSatisfiedGoal(t *t } } +func TestDormantBootstrapGoalReconfiguresBeforeEngagement(t *testing.T) { + // control-law: a retained bootstrap goal cannot be bypassed by engagement + snapshot := snapshotFor(t, model.PhaseDormant, model.TerminalNonterminal) + requested := model.Goal{ID: "basic-project", Kind: model.GoalApprovedPlan, DeliveryID: "basic-project"} + authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} + + untargeted := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, "") + if untargeted.Kind != DecisionPrescribed || untargeted.Transition == nil || untargeted.Transition.ID != "goal.configure" { + t.Fatalf("untargeted decision = %#v, want goal.configure", untargeted) + } + targeted := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, untargeted.Transition.ID) + if targeted.Kind != DecisionPrescribed || targeted.Transition == nil || targeted.Transition.ID != untargeted.Transition.ID { + t.Fatalf("targeted decision = %#v, want parity with %#v", targeted, untargeted) + } + engagement := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, "engagement.begin") + if engagement.Kind != DecisionRefused { + t.Fatalf("engagement decision = %#v, want refusal until goal.configure", engagement) + } +} + +func TestDisabledHostIsRefusedBeforeUntargetedSelection(t *testing.T) { + // control-law: host policy applies before both targeted and untargeted selection + snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) + snapshot.Invocation.Host = "codex" + snapshot = recanonicalize(t, snapshot) + decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + if decision.Kind != DecisionRefused || decision.Transition != nil { + t.Fatalf("disabled-host decision = %#v, want REFUSED", decision) + } +} + +func TestPublicationObservationRemainsSelectableForVolatileExternalState(t *testing.T) { + // control-law: a nonterminal provider observation is evidence, not permanent progress + snapshot, goal := openPRSnapshot(t, "build", "test", "review", "change", "journey") + goal.Kind = model.GoalMerged + snapshot.Goal = model.Known(goal, snapshot.Goal.Evidence[0]) + snapshot.Publication = model.Known(model.PublicationOpen, snapshot.Publication.Evidence[0]) + snapshot = recanonicalize(t, snapshot) + var transitions []catalog.Transition + for _, transition := range testprogram.StandardRegistry().All() { + if transition.ID == "publication.observe" || transition.Class == catalog.EventRecovery { + transitions = append(transitions, transition) + } + } + registry, err := catalog.New(transitions) + if err != nil { + t.Fatal(err) + } + decision := New(registry, testGoalContracts()).Resolve(snapshot, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "") + if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "publication.observe" { + t.Fatalf("volatile publication decision = %#v, want publication.observe", decision) + } +} + func TestUntargetedResolutionExcludesExplicitControlTransitions(t *testing.T) { // control-law: untargeted-resolution-cannot-invent-repair-or-slice-intent snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) diff --git a/boatstack/flow/standard/transitions.json b/boatstack/flow/standard/transitions.json index 49be596..a9553a8 100644 --- a/boatstack/flow/standard/transitions.json +++ b/boatstack/flow/standard/transitions.json @@ -5583,7 +5583,9 @@ "privacy_classification": "metadata-only", "telemetry_classification": "transition-receipt", "cost_class": "declared-neutral", - "policy": {}, + "policy": { + "rechecks_external_state": true + }, "priority": 77 }, { diff --git a/boatstack/internal/effects/host_skills.go b/boatstack/internal/effects/host_skills.go index af56d79..438dab5 100644 --- a/boatstack/internal/effects/host_skills.go +++ b/boatstack/internal/effects/host_skills.go @@ -92,8 +92,10 @@ materialized authority receipts. %s -Begin each cycle with an untargeted authority-bearing `+"`next`"+`. Apply only the -stable transition ID from the immediately preceding prescription and only its +Begin each cycle with an untargeted authority-bearing `+"`next`"+`. A `+"`CANDIDATE`"+` +identifies the next transition but is not permission to apply it: bind only its +declared parameters and re-resolve that exact transition. Apply only the stable +transition ID from the immediately preceding `+"`PRESCRIBED`"+` result and only its declared parameters. Preserve the complete apply response and stderr, including admission, receipt, postcondition, error, recovery, and transaction fields. Re-resolve with the same context after every complete receipt. diff --git a/boatstack/internal/effects/host_skills_test.go b/boatstack/internal/effects/host_skills_test.go index 2aa487b..76cc293 100644 --- a/boatstack/internal/effects/host_skills_test.go +++ b/boatstack/internal/effects/host_skills_test.go @@ -50,7 +50,7 @@ func TestHostSkillProjectionPreservesAuthorityBoundaries(t *testing.T) { for _, contract := range []string{ "authority-free\n`FRONTIER`", "command-scoped context", "every `next`, `apply`, `recover`, and re-resolution", "requested authority sources separately from currently\nmaterialized authority receipts", - "complete apply response and stderr", "authority-bearing `FRONTIER`", "Never synthesize missing\nauthority", + "complete apply response and stderr", "authority-bearing `FRONTIER`", "Never synthesize missing\nauthority", "`CANDIDATE`", "immediately preceding `PRESCRIBED`", "every requested authority source is materialized\nor conclusively rejected against the post-receipt state", } { if !strings.Contains(value, contract) { diff --git a/boatstack/internal/kernel/catalog/transition.go b/boatstack/internal/kernel/catalog/transition.go index 7485211..ae3a954 100644 --- a/boatstack/internal/kernel/catalog/transition.go +++ b/boatstack/internal/kernel/catalog/transition.go @@ -184,6 +184,7 @@ type PolicyContract struct { 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"` } // FacetCondition is an executable, serializable predicate over one canonical diff --git a/boatstack/internal/kernel/engine/engine.go b/boatstack/internal/kernel/engine/engine.go index e0d24f0..01d8415 100644 --- a/boatstack/internal/kernel/engine/engine.go +++ b/boatstack/internal/kernel/engine/engine.go @@ -40,6 +40,7 @@ type ResolveRequest struct { Invocation model.InvocationContext Goal model.Goal Authority protocol.AuthorityBundle + Parameters protocol.Parameters Requested catalog.TransitionID } @@ -76,6 +77,30 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution return Resolution{}, err } decision := e.control.Resolve(snapshot, goal, request.Authority.Set(now), request.Requested) + if decision.Kind == supervisor.DecisionPrescribed && decision.Transition != nil { + if applicabilityErr := protocol.ValidateApplicability(snapshot, goal, *decision.Transition, request.Authority, request.Parameters, now); applicabilityErr != nil { + if protocol.IsMissingParameter(applicabilityErr) { + decision.Kind = supervisor.DecisionCandidate + decision.Reason = applicabilityErr.Error() + "; bind the declared parameters and re-resolve this transition" + decision.Candidates = []catalog.TransitionID{decision.Transition.ID} + } else { + decision.Kind = supervisor.DecisionRefused + decision.Reason = applicabilityErr.Error() + decision.Transition = nil + } + } else { + admission, admissionErr := protocol.NewAdmission(snapshot, goal, *decision.Transition, request.Authority, request.Parameters, now, 2*time.Minute) + if admissionErr != nil { + decision.Kind = supervisor.DecisionUnresolved + decision.Reason = admissionErr.Error() + decision.Transition = nil + } else if _, preflightErr := e.effects.Prepare(ctx, admission, *decision.Transition); preflightErr != nil { + decision.Kind = supervisor.DecisionUnresolved + decision.Reason = fmt.Sprintf("transition %q failed deterministic effect preflight: %v", admission.TransitionID, preflightErr) + decision.Transition = nil + } + } + } return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil } @@ -170,6 +195,7 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe if request.AdmissionLifetime <= 0 { request.AdmissionLifetime = 2 * time.Minute } + request.ResolveRequest.Parameters = request.Parameters resolution, err := e.Resolve(ctx, request.ResolveRequest) result.Source, result.Goal, result.Decision = resolution.Snapshot, resolution.Goal, resolution.Decision if err != nil { diff --git a/boatstack/internal/kernel/engine/engine_test.go b/boatstack/internal/kernel/engine/engine_test.go index 94f22f2..9c4e56b 100644 --- a/boatstack/internal/kernel/engine/engine_test.go +++ b/boatstack/internal/kernel/engine/engine_test.go @@ -105,9 +105,13 @@ type fakeEffects struct { executions, rollbacks int result ports.EffectResult err error + prepareErr error } func (e *fakeEffects) Prepare(context.Context, protocol.Admission, catalog.Transition) (ports.PreparedEffect, error) { + if e.prepareErr != nil { + return nil, e.prepareErr + } return e, nil } func (e *fakeEffects) Manifest() []ports.ResourceMutation { return nil } @@ -274,6 +278,66 @@ func TestRequiredObserverFailureReturnsTypedUnresolvedDecision(t *testing.T) { } } +func TestResolutionDoesNotPrescribeBeforeRequiredParametersAreBound(t *testing.T) { + // control-law: a selected transition is only a candidate until deterministic admission inputs are complete + now := time.Unix(30, 0).UTC() + transitions := testRegistry(t).All() + for index := range transitions { + if transitions[index].ID == "test.advance" { + transitions[index].Parameters = []catalog.ParameterSpec{{Name: "value", Required: true}} + } + } + registry, err := catalog.New(transitions) + if err != nil { + t.Fatal(err) + } + observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source")}} + kernel, err := New(registry, syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}) + if err != nil { + t.Fatal(err) + } + req := request(now).ResolveRequest + req.Requested = "" + candidate, err := kernel.Resolve(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if candidate.Decision.Kind != supervisor.DecisionCandidate || candidate.Decision.Transition == nil || candidate.Decision.Transition.ID != "test.advance" { + t.Fatalf("incomplete resolution = %+v, want CANDIDATE", candidate.Decision) + } + req.Requested = "test.advance" + req.Parameters = protocol.Parameters{{Name: "value", Value: "bound"}} + prescribed, err := kernel.Resolve(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if prescribed.Decision.Kind != supervisor.DecisionPrescribed || prescribed.Decision.Transition == nil || prescribed.Decision.Transition.ID != "test.advance" { + t.Fatalf("complete resolution = %+v, want PRESCRIBED", prescribed.Decision) + } +} + +func TestResolutionDoesNotPrescribeAnEffectThatDeterministicPreflightRejects(t *testing.T) { + // control-law: effect preparation cannot introduce a deterministic apply-only refusal + now := time.Unix(30, 0).UTC() + effects := &fakeEffects{prepareErr: errors.New("malformed artifact")} + observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source")}} + journal := &fakeJournal{} + kernel, err := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, &memoryReceipts{}) + if err != nil { + t.Fatal(err) + } + resolved, err := kernel.Resolve(context.Background(), request(now).ResolveRequest) + if err != nil { + t.Fatal(err) + } + if resolved.Decision.Kind != supervisor.DecisionUnresolved || resolved.Decision.Transition != nil || !strings.Contains(resolved.Decision.Reason, "malformed artifact") { + t.Fatalf("preflight decision = %+v, want typed UNRESOLVED without prescription", resolved.Decision) + } + if effects.executions != 0 || journal.begun != 0 { + t.Fatalf("preflight crossed mutation boundary: effects=%d journals=%d", effects.executions, journal.begun) + } +} + func TestApplyCrossesAdmissionEffectVerificationAndReceiptBoundary(t *testing.T) { // control-law: synthetic-flow-crosses-exact-admission-and-postcondition-without-standard-flow now := time.Unix(30, 0).UTC() diff --git a/boatstack/internal/kernel/ports/ports.go b/boatstack/internal/kernel/ports/ports.go index 1af9c58..0c80bb5 100644 --- a/boatstack/internal/kernel/ports/ports.go +++ b/boatstack/internal/kernel/ports/ports.go @@ -92,6 +92,8 @@ type PreparedEffect interface { } type EffectDriver interface { + // Prepare is a side-effect-free preflight. It may read exact plant state and + // construct a mutation manifest, but it must not execute or install it. Prepare(context.Context, protocol.Admission, catalog.Transition) (PreparedEffect, error) } diff --git a/boatstack/internal/kernel/protocol/admission.go b/boatstack/internal/kernel/protocol/admission.go index 028cc50..0f3ebef 100644 --- a/boatstack/internal/kernel/protocol/admission.go +++ b/boatstack/internal/kernel/protocol/admission.go @@ -32,49 +32,13 @@ 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) { - if !transition.Controllable() { - return Admission{}, fmt.Errorf("transition %q is uncontrollable and cannot be admitted", transition.ID) - } - if err := snapshot.Invocation.Validate(true); err != nil { - return Admission{}, err - } - if err := goal.Validate(); err != nil { + if err := ValidateApplicability(snapshot, goal, transition, authority, parameters, now); err != nil { return Admission{}, err } - if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) { - return Admission{}, fmt.Errorf("transition %q is not admissible from snapshot %q", transition.ID, snapshot.Fingerprint) - } if lifetime <= 0 { return Admission{}, fmt.Errorf("admission lifetime must be positive") } - if err := authority.Validate(now); err != nil { - return Admission{}, err - } - if err := validateAuthorityEvidence(snapshot, authority); err != nil { - return Admission{}, err - } - if err := parameters.Validate(transition); err != nil { - return Admission{}, err - } - if err := validateProviderAuthorityBinding(authority, transition, parameters); err != nil { - return Admission{}, err - } sourceRevision, worktreeFingerprint := gitBinding(snapshot) - if transition.BindsSourceRevision { - declared, _ := parameters.Get("source_revision") - if sourceRevision == "" || worktreeFingerprint == "" || declared != sourceRevision { - return Admission{}, fmt.Errorf("transition %q must bind the current Git revision and worktree fingerprint", transition.ID) - } - } - if !authority.Set(now).Satisfies(transition.Authority, transition.AuthorityAll) { - return Admission{}, fmt.Errorf("transition %q lacks required authority", transition.ID) - } - if err := validatePolicyAuthority(snapshot, transition, authority.Set(now)); err != nil { - return Admission{}, err - } - if err := validateRecoveryPermission(snapshot, transition); err != nil { - return Admission{}, err - } a := Admission{ SchemaVersion: AdmissionSchemaVersion, TransitionID: transition.ID, TransitionVersion: transition.Version, ProgramFingerprint: snapshot.ProgramFingerprint, SnapshotFingerprint: snapshot.Fingerprint, SourceRevision: sourceRevision, WorktreeFingerprint: worktreeFingerprint, @@ -101,6 +65,56 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T return a, nil } +// 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. +func ValidateApplicability(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, authority AuthorityBundle, parameters Parameters, now time.Time) error { + if !transition.Controllable() { + return fmt.Errorf("transition %q is uncontrollable and cannot be admitted", transition.ID) + } + if err := snapshot.Invocation.Validate(true); err != nil { + return err + } + if err := goal.Validate(); err != nil { + return err + } + if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) { + return fmt.Errorf("transition %q is not admissible from snapshot %q", transition.ID, snapshot.Fingerprint) + } + if snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal && !transition.Policy.BindsRequestedGoal { + return fmt.Errorf("transition %q cannot replace configured goal; goal.configure is required", transition.ID) + } + if err := authority.Validate(now); err != nil { + return err + } + if err := validateAuthorityEvidence(snapshot, authority); err != nil { + return err + } + if err := parameters.Validate(transition); err != nil { + return err + } + if err := validateProviderAuthorityBinding(authority, transition, parameters); err != nil { + return err + } + sourceRevision, worktreeFingerprint := gitBinding(snapshot) + if transition.BindsSourceRevision { + declared, _ := parameters.Get("source_revision") + if sourceRevision == "" || worktreeFingerprint == "" || declared != sourceRevision { + return fmt.Errorf("transition %q must bind the current Git revision and worktree fingerprint", transition.ID) + } + } + if !authority.Set(now).Satisfies(transition.Authority, transition.AuthorityAll) { + return fmt.Errorf("transition %q lacks required authority", transition.ID) + } + if err := validatePolicyAuthority(snapshot, transition, authority.Set(now)); err != nil { + return err + } + if err := validateRecoveryPermission(snapshot, transition); err != nil { + return err + } + return nil +} + func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, now time.Time) error { if err := a.ValidateIdentity(); err != nil { return err diff --git a/boatstack/internal/kernel/protocol/parameters.go b/boatstack/internal/kernel/protocol/parameters.go index 5c2214d..f619614 100644 --- a/boatstack/internal/kernel/protocol/parameters.go +++ b/boatstack/internal/kernel/protocol/parameters.go @@ -1,6 +1,7 @@ package protocol import ( + "errors" "fmt" "path/filepath" "sort" @@ -15,6 +16,20 @@ type Parameter struct { type Parameters []Parameter +type MissingParameterError struct { + Transition catalog.TransitionID + Parameter string +} + +func (e MissingParameterError) Error() string { + return fmt.Sprintf("transition %q requires parameter %q", e.Transition, e.Parameter) +} + +func IsMissingParameter(err error) bool { + var missing MissingParameterError + return errors.As(err, &missing) +} + func (p Parameters) Canonical() Parameters { result := append(Parameters(nil), p...) sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) @@ -63,7 +78,7 @@ func (p Parameters) Validate(transition catalog.Transition) error { } for _, spec := range transition.Parameters { if spec.Required && !seen[spec.Name] { - return fmt.Errorf("transition %q requires parameter %q", transition.ID, spec.Name) + return MissingParameterError{Transition: transition.ID, Parameter: spec.Name} } } return nil diff --git a/boatstack/internal/kernel/supervisor/supervisor.go b/boatstack/internal/kernel/supervisor/supervisor.go index f73f87e..c12b79b 100644 --- a/boatstack/internal/kernel/supervisor/supervisor.go +++ b/boatstack/internal/kernel/supervisor/supervisor.go @@ -12,6 +12,7 @@ type DecisionKind string const ( DecisionPrescribed DecisionKind = "PRESCRIBED" + DecisionCandidate DecisionKind = "CANDIDATE" DecisionTerminal DecisionKind = "TERMINAL" DecisionFrontier DecisionKind = "FRONTIER" DecisionBlocked DecisionKind = "BLOCKED" @@ -57,11 +58,7 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority return base } } - if requested != "" && snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal && requested != "goal.configure" { - base.Kind, base.Reason = DecisionRefused, "requested goal differs from configured goal; goal.configure is required" - return base - } - if requested != "" && snapshot.ConfigurationPolicy.Status == model.FactKnown && !hostEnabled(snapshot.ConfigurationPolicy.Value.Hosts, snapshot.Invocation.Host) { + if snapshot.ConfigurationPolicy.Status == model.FactKnown && !hostEnabled(snapshot.ConfigurationPolicy.Value.Hosts, snapshot.Invocation.Host) { base.Kind, base.Reason = DecisionRefused, fmt.Sprintf("host %q is not enabled by repository policy", snapshot.Invocation.Host) return base } @@ -70,6 +67,15 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority return base } admissible := s.registry.Admissible(snapshot, goal) + if snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal { + filtered := admissible[:0] + for _, candidate := range admissible { + if candidate.Policy.BindsRequestedGoal { + filtered = append(filtered, candidate) + } + } + admissible = filtered + } if snapshot.Phase.Value == model.PhaseRecovery { filtered := admissible[:0] for _, candidate := range admissible { @@ -150,6 +156,9 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority } func targetAlreadySatisfied(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition) bool { + if transition.Policy.RechecksExternalState { + return false + } if transition.Policy.BindsRequestedGoal { return snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value == goal } diff --git a/boatstack/internal/plant/observer.go b/boatstack/internal/plant/observer.go index e930842..a7f901c 100644 --- a/boatstack/internal/plant/observer.go +++ b/boatstack/internal/plant/observer.go @@ -646,7 +646,7 @@ func pendingJournalEvidence(root, ignoreAdmissionID string, now time.Time) (pend if budget < 0 { budget = 0 } - permitted := recoveryContract(header.TransitionID, external, budget) + permitted := recoveryContract(header.TransitionID, external, len(header.Mutations) > 0, budget) cause := header.Reason if cause == "" { cause = "process ended before transition receipt" @@ -739,7 +739,7 @@ func conflictingPending(records []pendingJournalRecord) pendingJournalSet { return result } -func recoveryContract(transitionID string, external bool, budget int) []string { +func recoveryContract(transitionID string, external, staged bool, budget int) []string { if budget == 0 { return []string{"recovery.escalate"} } @@ -756,6 +756,9 @@ func recoveryContract(transitionID string, external bool, budget int) []string { case "workspace.cleanup", "workspace.reap": return []string{"recovery.escalate"} default: + if !staged { + return []string{"recovery.rollback", "recovery.escalate"} + } return []string{"recovery.resume", "recovery.rollback", "recovery.escalate"} } } diff --git a/boatstack/internal/plant/observer_test.go b/boatstack/internal/plant/observer_test.go index 54dbd8f..bb5e380 100644 --- a/boatstack/internal/plant/observer_test.go +++ b/boatstack/internal/plant/observer_test.go @@ -249,6 +249,19 @@ func TestRecoveryAttemptsExhaustToEscalationOnly(t *testing.T) { } } +func TestRecoveryWithoutStagedManifestCannotPrescribeResume(t *testing.T) { + // control-law: recovery selection cannot promise a replay that prepare will reject + permitted := recoveryContract("plan.create", false, false, 3) + for _, transition := range permitted { + if transition == "recovery.resume" { + t.Fatalf("unstaged recovery permits resume: %v", permitted) + } + } + if len(permitted) != 2 || permitted[0] != "recovery.rollback" || permitted[1] != "recovery.escalate" { + t.Fatalf("unstaged recovery contract = %v", permitted) + } +} + func TestInterruptedRecoveryAttemptCollapsesToEscalatableTransactionGroup(t *testing.T) { // control-law: recovery-of-recovery-does-not-create-an-unselectable-conflict root := t.TempDir() diff --git a/boatstack/kernel.go b/boatstack/kernel.go index ee15805..fe18b47 100644 --- a/boatstack/kernel.go +++ b/boatstack/kernel.go @@ -109,7 +109,7 @@ func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces. } switch request.Operation { case surfaces.OperationResolve: - resolution, resolveErr := k.engine.Resolve(ctx, engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Requested: request.TransitionID}) + resolution, resolveErr := k.engine.Resolve(ctx, engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Parameters: request.Parameters, Requested: request.TransitionID}) response.Goal, response.Decision = resolution.Goal, &resolution.Decision if resolution.Snapshot.Fingerprint != "" { response.Snapshot = &resolution.Snapshot diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index ccf843b..c6c3735 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -17,8 +17,9 @@ Event families: - recovery; - observed external plant changes. -The supervisor returns one of `PRESCRIBED`, `TERMINAL`, `FRONTIER`, -`BLOCKED`, `REFUSED`, or `UNRESOLVED`. Only `PRESCRIBED` can produce an +The supervisor returns one of `CANDIDATE`, `PRESCRIBED`, `TERMINAL`, `FRONTIER`, +`BLOCKED`, `REFUSED`, or `UNRESOLVED`. `CANDIDATE` identifies the deterministic +next transition while required parameters remain unbound. Only `PRESCRIBED` can produce an admission. Only an independently verified postcondition can produce a receipt. Untargeted resolution excludes transitions whose target is already established and transitions that encode separate maintenance, repair, abandonment, or diff --git a/docs/architecture/boatstack-v2-kernel.md b/docs/architecture/boatstack-v2-kernel.md index ba775f1..f782f3a 100644 --- a/docs/architecture/boatstack-v2-kernel.md +++ b/docs/architecture/boatstack-v2-kernel.md @@ -531,6 +531,7 @@ it is not an independently maintained graph. `supervisor.Resolve(snapshot, goal, authority, optionalObservedEvent)` is pure and deterministic. It evaluates the executable registry and returns exactly one: +- `CANDIDATE`: one deterministic next transition still needs declared parameters; - `PRESCRIBED`: one exact next transition and prescription; - `TERMINAL`: goal predicate established by current terminal evidence; - `FRONTIER`: a genuine human/reasoning authority decision is required; @@ -547,6 +548,9 @@ repository presence is not engagement; a saved plan is not active authority. Resolution never fabricates progress. If several controllable events remain equally admissible after declared deterministic priority, the answer is `FRONTIER` or `UNRESOLVED`, never map-order selection or first-match behavior. +Before `PRESCRIBED`, resolution also runs the effect driver's side-effect-free +preflight over the exact admission context; deterministic artifact, durable-state, +or recovery refusals therefore cannot first appear at apply. ## 9. Admission and authority model diff --git a/docs/architecture/boatstack-v2-locus-liveness.json b/docs/architecture/boatstack-v2-locus-liveness.json index 37e8ced..31796d9 100644 --- a/docs/architecture/boatstack-v2-locus-liveness.json +++ b/docs/architecture/boatstack-v2-locus-liveness.json @@ -3403,6 +3403,39 @@ ], "basis": "inferred" }, + { + "from": "DORMANT", + "event": "goal.configure", + "to": "OBSERVED", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, + { + "from": "DORMANT", + "event": "goal.configure", + "to": "ACTIVE", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, + { + "from": "DORMANT", + "event": "goal.configure", + "to": "FRONTIER", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, { "from": "ACTIVE", "event": "goal.configure", diff --git a/docs/architecture/boatstack-v2-locus-safety.json b/docs/architecture/boatstack-v2-locus-safety.json index 17989d4..5d0a7e3 100644 --- a/docs/architecture/boatstack-v2-locus-safety.json +++ b/docs/architecture/boatstack-v2-locus-safety.json @@ -3406,6 +3406,39 @@ ], "basis": "inferred" }, + { + "from": "DORMANT", + "event": "goal.configure", + "to": "OBSERVED", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, + { + "from": "DORMANT", + "event": "goal.configure", + "to": "ACTIVE", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, + { + "from": "DORMANT", + "event": "goal.configure", + "to": "FRONTIER", + "evidence": [ + 0, + 1, + 4 + ], + "basis": "inferred" + }, { "from": "ACTIVE", "event": "goal.configure", diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md index 6811882..4bac11a 100644 --- a/docs/architecture/boatstack-v2-transition-catalog.md +++ b/docs/architecture/boatstack-v2-transition-catalog.md @@ -7,67 +7,67 @@ 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`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` | -| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` | -| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` | -| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` | -| `delivery.slice.advance` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` | -| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` | -| `engagement.release` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` | -| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` | -| `evidence.approval.revoke` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` | -| `evidence.visual.attach` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` | -| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.branch-changed` | `-` | `declared-neutral` | -| `external.ci-completed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.ci-completed` | `-` | `declared-neutral` | -| `external.configuration-drifted` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` | -| `external.files-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.files-changed` | `-` | `declared-neutral` | -| `external.head-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.head-changed` | `-` | `declared-neutral` | -| `external.host-interrupted` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | `verifier:fresh-observation:external.host-interrupted` | `-` | `declared-neutral` | -| `external.lease-expired` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | `verifier:fresh-observation:external.lease-expired` | `-` | `declared-neutral` | -| `external.pr-closed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | `verifier:fresh-observation:external.pr-closed` | `-` | `declared-neutral` | -| `external.pr-merged` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-merged` | `-` | `declared-neutral` | -| `external.pr-opened` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-opened` | `-` | `declared-neutral` | -| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-updated` | `-` | `declared-neutral` | -| `external.provider-unavailable` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.provider-unavailable` | `-` | `declared-neutral` | -| `external.runtime-disappeared` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` | -| `gate.build.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` | -| `gate.change.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` | -| `gate.journey.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` | -| `gate.review.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` | -| `gate.test.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` | -| `goal.configure` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `goal_kind*`, `delivery_id*` | `goal` | `verifier:fresh-observation:goal.configure` | `recovery.resume` | `declared-neutral` | -| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` | -| `installation.update` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` | -| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` | -| `plan.abandon` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` | -| `plan.activate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` | -| `plan.amend` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` | -| `plan.approve` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | -| `plan.approve-amendment` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | -| `plan.create` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | -| `plan.invalidate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | -| `plan.validate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | -| `publication.abandon` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | -| `publication.correct` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | -| `publication.execute` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | -| `publication.observe` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` | -| `publication.preview` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` | -| `publication.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` | -| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` | -| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` | -| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` | -| `repository.attach` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` | -| `repository.detach` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` | -| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` | -| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` | -| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` | -| `workspace.abandon` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` | -| `workspace.activate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` | -| `workspace.cleanup` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` | -| `workspace.cut` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | -| `workspace.publish` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | -| `workspace.reap` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | -| `workspace.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | -| `workspace.sync` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` | +| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | GOAL_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` | +| `installation.update` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` | +| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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` | +| `plan.approve` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | +| `plan.approve-amendment` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | +| `plan.create` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | +| `plan.invalidate` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | +| `plan.validate` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | +| `publication.abandon` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | +| `publication.correct` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | +| `publication.execute` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | +| `publication.observe` | primary-flow:`boatstack.standard@1.0.0`
`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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `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` | +| `workspace.cut` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | +| `workspace.publish` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | +| `workspace.reap` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | +| `workspace.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | +| `workspace.sync` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` | `*` marks a required parameter. OR authority is shown with `/`; mandatory authority clauses are shown with `AND`. Source and target facet predicates remain in the canonical JSON returned by `boatstack catalog --format json`. diff --git a/docs/architecture/boatstack-v2-transition-catalog.mmd b/docs/architecture/boatstack-v2-transition-catalog.mmd index 2dd67b2..164403a 100644 --- a/docs/architecture/boatstack-v2-transition-catalog.mmd +++ b/docs/architecture/boatstack-v2-transition-catalog.mmd @@ -95,6 +95,7 @@ flowchart TB p_FRONTIER --> t03 t03 --> p_FRONTIER p_OBSERVED --> t04 + p_DORMANT --> t04 p_ACTIVE --> t04 p_FRONTIER --> t04 p_TERMINAL --> t04 diff --git a/release-notes/2026-08-11-transition-law-consistency.md b/release-notes/2026-08-11-transition-law-consistency.md new file mode 100644 index 0000000..1ef5ee7 --- /dev/null +++ b/release-notes/2026-08-11-transition-law-consistency.md @@ -0,0 +1,3 @@ +### Keep prescriptions consistent with deterministic apply checks + +Boatstack now applies one shared applicability law and side-effect-free effect preflight before reporting a transition as prescribed. Required parameters are surfaced as a candidate before admission, retained goals are reconfigured before engagement, recovery omits impossible resume actions, and volatile provider observations remain selectable until the configured goal is established.