From 333159f82eaa6c9dfe2b34b0494391a04d994e34 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Thu, 13 Aug 2026 10:07:14 +0100 Subject: [PATCH 1/3] Make flow control law repository-owned --- .github/tests/test_detached_supervision.py | 14 +- .github/tests/test_repository_contract.py | 6 +- README.md | 2 +- .../cmd/boatstack-helper/flow_runtime.go | 32 ++-- .../cmd/boatstack-helper/flow_runtime_test.go | 22 +-- boatstack/cmd/boatstack-helper/main.go | 11 +- boatstack/cmd/boatstack-helper/main_test.go | 2 +- .../frontend_conformance_test.go | 55 +++++++ boatstack/core/transitions.json | 68 ++++----- boatstack/delivery/control.go | 42 ++--- boatstack/delivery/control_test.go | 12 +- boatstack/delivery/program_manifest.go | 8 +- boatstack/delivery/program_manifest_test.go | 4 +- .../extension/releasenote/releasenote.go | 6 +- boatstack/flow/softwaredelivery/bindings.go | 4 + boatstack/flow/softwaredelivery/definition.go | 115 ++++++++------ .../flow/softwaredelivery/definition_test.go | 53 ++++++- boatstack/flow/standard/standard.go | 6 +- boatstack/flow/standard/standard_test.go | 4 +- .../flow/standard/supervisor_parity_test.go | 12 +- boatstack/flow/standard/transitions.json | 60 ++++---- .../catalog/objective_contract.go | 26 ++-- .../softwaredelivery/catalog/transition.go | 14 +- .../durable/state_facet_test.go | 2 +- .../effects/cas_integration_test.go | 4 +- .../effects/integration_test.go | 32 ++-- .../softwaredelivery/effects/journal.go | 2 +- .../softwaredelivery/effects/receipts.go | 2 +- .../softwaredelivery/effects/receipts_test.go | 11 +- .../softwaredelivery/effects/recovery_test.go | 2 +- .../effects/state_facet_test.go | 6 +- .../softwaredelivery/effects/state_reducer.go | 14 +- .../effects/state_reducer_test.go | 26 ++-- .../softwaredelivery/engine/engine.go | 4 +- .../softwaredelivery/engine/engine_test.go | 10 +- .../engine/maintenance_replay_test.go | 8 +- .../internal/softwaredelivery/model/facet.go | 2 +- .../softwaredelivery/model/objective.go | 47 +++--- .../softwaredelivery/model/state_test.go | 2 +- .../softwaredelivery/plant/observer.go | 11 +- .../protocol/maintenance_objective_test.go | 4 +- .../softwaredelivery/protocol/receipt.go | 11 +- .../protocol/receipt_capability_test.go | 2 +- .../protocol/receipt_fact_test.go | 5 +- .../softwaredelivery/supervisor/guard_test.go | 2 +- .../softwaredelivery/surfaces/render.go | 2 +- .../softwaredelivery/surfaces/render_test.go | 4 +- boatstack/program_effects_test.go | 10 +- boatstack/sdk/sdk.go | 2 +- boatstack/sdk/sdk_test.go | 14 +- .../product-delivery-a.flow.ts | 31 ++++ .../product-delivery-b.flow.ts | 41 +++++ .../product-delivery-c.flow.ts | 36 +++++ .../testdata/v2-scenarios/historical.json | 44 +++--- .../boatstack-v2-transition-catalog.md | 126 +++++++-------- docs/configuration.md | 2 +- docs/getting-started.md | 20 +-- .../boatstack-software-delivery/src/index.ts | 144 +++--------------- .../boatstack/bin/boatstack-flow-frontend.mjs | 22 +++ ...08-13-repository-owned-flow-control-law.md | 3 + 60 files changed, 740 insertions(+), 548 deletions(-) create mode 100644 boatstack/testdata/control-programs/product-delivery-a.flow.ts create mode 100644 boatstack/testdata/control-programs/product-delivery-b.flow.ts create mode 100644 boatstack/testdata/control-programs/product-delivery-c.flow.ts create mode 100644 release-notes/2026-08-13-repository-owned-flow-control-law.md diff --git a/.github/tests/test_detached_supervision.py b/.github/tests/test_detached_supervision.py index 3fbde9c..de89958 100644 --- a/.github/tests/test_detached_supervision.py +++ b/.github/tests/test_detached_supervision.py @@ -118,7 +118,7 @@ def porcelain(self, repository: Path | None = None) -> str: @staticmethod def objective_flags() -> tuple[str, ...]: return ( - "--objective-id", "bootstrap", "--objective-kind", "approved-plan", + "--objective-id", "bootstrap", "--target-id", "approved-plan", "--delivery", "bootstrap", ) @@ -207,7 +207,7 @@ def test_detached_installation_and_engaged_guard_use_the_same_kernel(self) -> No self.apply_prescribed( "objective.bind", "--repo", self.repo, *self.objective_flags(), "--human", "contract", - "--param", "objective_kind=approved-plan", "--param", "delivery_id=bootstrap", + "--param", "target_id=approved-plan", "--param", "delivery_id=bootstrap", ) self.apply_prescribed( "engagement.begin", "--repo", self.repo, @@ -232,7 +232,7 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) - # control-law: codex-mode-authority-survives-observation-and-effects objective = ( "--objective-id", "codex-driver-authority-triggers", - "--objective-kind", "open-or-updated-pr", + "--target-id", "open-or-updated-pr", "--delivery", "codex-driver-authority-triggers", ) flow = ("--run-id", "flow-codex-driver-authority-triggers") @@ -258,7 +258,7 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) - self.apply_prescribed( "objective.bind", "--repo", self.repo, *objective, *flow, "--human", "contract", - "--param", "objective_kind=open-or-updated-pr", + "--param", "target_id=open-or-updated-pr", "--param", "delivery_id=codex-driver-authority-triggers", ) self.apply_prescribed( @@ -322,7 +322,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali # control-law: retained-repository-source-crosses-maintenance-receipt-once objective = ( "--objective-id", "preserve-repository-authority-context", - "--objective-kind", "open-or-updated-pr", + "--target-id", "open-or-updated-pr", "--delivery", "preserve-repository-authority-context", ) flow = ("--run-id", "flow-preserve-repository-authority-context") @@ -391,7 +391,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali configured = self.apply_prescribed( "objective.bind", "--repo", self.repo, *objective, *flow, *actor, - "--param", "objective_kind=open-or-updated-pr", + "--param", "target_id=open-or-updated-pr", "--param", "delivery_id=preserve-repository-authority-context", ) self.assertEqual(configured["receipt"]["transition_id"], "objective.bind") @@ -449,7 +449,7 @@ def test_repository_authority_rematerialization_fails_closed_without_verified_co result = self.run_helper( "next", "--repo", root, "--objective-id", "unverified-authority", - "--objective-kind", "open-or-updated-pr", + "--target-id", "open-or-updated-pr", "--delivery", "unverified-authority", "--run-id", "flow-unverified-authority", "--human", "contract", "--repository-authority", diff --git a/.github/tests/test_repository_contract.py b/.github/tests/test_repository_contract.py index 1068141..2b9a149 100644 --- a/.github/tests/test_repository_contract.py +++ b/.github/tests/test_repository_contract.py @@ -839,12 +839,12 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) - self.assertEqual(doctor["snapshot"]["runtime"]["value"], "verified") objective = ( - "--objective-id", "bootstrap", "--objective-kind", "approved-plan", + "--objective-id", "bootstrap", "--target-id", "approved-plan", "--delivery", "bootstrap", ) self.apply_prescribed( launcher, "objective.bind", "--repo", repository, *objective, - "--human", "contract", "--param", "objective_kind=approved-plan", + "--human", "contract", "--param", "target_id=approved-plan", "--param", "delivery_id=bootstrap", env=env, ) self.apply_prescribed( @@ -956,7 +956,7 @@ def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> No "contract", "--objective-id", "bootstrap", - "--objective-kind", + "--target-id", "approved-plan", "--delivery", "bootstrap", diff --git a/README.md b/README.md index a85a3bc..3376092 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ The public protocol is deliberately small: ```sh # Observe or resolve. These commands do not mutate managed state. boatstack status --repo . --format json -boatstack next --repo . --objective-id --objective-kind \ +boatstack next --repo . --objective-id --target-id \ --delivery --format json # Resolve one repository-owned entry. diff --git a/boatstack/cmd/boatstack-helper/flow_runtime.go b/boatstack/cmd/boatstack-helper/flow_runtime.go index dab593e..40b5254 100644 --- a/boatstack/cmd/boatstack-helper/flow_runtime.go +++ b/boatstack/cmd/boatstack-helper/flow_runtime.go @@ -100,8 +100,11 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions, return commandOptions{}, fmt.Errorf("FLOW_ACTIVE_RUN_INVALID: active abandonment has no committed run identity") } options.repository = repository - if options.objectiveKind == "" { - options.objectiveKind = string(objective) + if options.targetID == "" { + options.targetID = string(objective.TargetID) + } + if options.trustedObjectiveClass == "" { + options.trustedObjectiveClass = string(objective.TrustedClass) } if options.deliveryID == "" { options.deliveryID = deliveryID @@ -110,7 +113,7 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions, if options.objectiveID == "" { options.objectiveID = expectedObjectiveID } - if options.objectiveKind != string(objective) || options.deliveryID != deliveryID || options.objectiveID != expectedObjectiveID { + if options.targetID != string(objective.TargetID) || options.trustedObjectiveClass != string(objective.TrustedClass) || options.deliveryID != deliveryID || options.objectiveID != expectedObjectiveID { return commandOptions{}, fmt.Errorf("FLOW_CONTEXT_MISMATCH: objective or delivery changed across the run") } parameters, err := parseParameters(options.parameters) @@ -118,7 +121,7 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions, return commandOptions{}, err } for name, expected := range map[string]string{ - "objective_kind": string(objective), + "target_id": string(objective.TargetID), "delivery_id": deliveryID, "source_path": plan, "source_fingerprint": planFingerprint, @@ -129,7 +132,7 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions, } switch options.transitionID { case "objective.bind": - if err := bindResolvedParameter(&options, parameters, "objective_kind", string(objective)); err != nil { + if err := bindResolvedParameter(&options, parameters, "target_id", string(objective.TargetID)); err != nil { return commandOptions{}, err } if err := bindResolvedParameter(&options, parameters, "delivery_id", deliveryID); err != nil { @@ -149,8 +152,8 @@ func bindFlowEntry(ctx context.Context, options commandOptions) (commandOptions, return options, nil } -func bindActiveFlowContext(ctx context.Context, repository string, options commandOptions, entryObjective model.ObjectiveKind) (commandOptions, error) { - if options.runID != "" && entryObjective != model.ObjectiveAbandoned { +func bindActiveFlowContext(ctx context.Context, repository string, options commandOptions, entryObjective softwareflow.EntryObjective) (commandOptions, error) { + if options.runID != "" && entryObjective.TrustedClass != model.ObjectiveAbandoned { return options, nil } resolver, err := plant.NewResolver("") @@ -201,13 +204,13 @@ func bindActiveFlowContext(ctx context.Context, repository string, options comma if !found || !strings.HasPrefix(receipt.FlowID, "run-") { return commandOptions{}, fmt.Errorf("FLOW_ACTIVE_RUN_INVALID: active objective has no committed run identity") } - if active.Kind == entryObjective && strings.HasPrefix(active.ID, prefix) { + if active.TargetID == entryObjective.TargetID && strings.HasPrefix(active.ID, prefix) { options.runID, options.deliveryID = receipt.FlowID, active.DeliveryID - options.objectiveID, options.objectiveKind = active.ID, string(active.Kind) + options.objectiveID, options.targetID, options.trustedObjectiveClass = active.ID, string(active.TargetID), string(active.TrustedObjectiveClass()) options.activeFlowBound = true return options, nil } - if entryObjective == model.ObjectiveAbandoned { + if entryObjective.TrustedClass == model.ObjectiveAbandoned { repositoryIdentity, identityErr := flowRepositoryIdentity(repository) if identityErr != nil { return commandOptions{}, identityErr @@ -252,7 +255,7 @@ func bindRPCFlowEntry(ctx context.Context, request surfaces.Request) (surfaces.R bound, err := bindFlowEntry(ctx, commandOptions{ repository: request.Repository, host: request.Host, programID: request.ProgramID, entryID: request.EntryID, flowProgramFingerprint: request.ProgramFingerprint, - runID: request.FlowID, objectiveID: request.Objective.ID, objectiveKind: string(request.Objective.Kind), deliveryID: request.Objective.DeliveryID, + runID: request.FlowID, objectiveID: request.Objective.ID, targetID: string(request.Objective.TargetID), trustedObjectiveClass: string(request.Objective.TrustedObjectiveClass()), deliveryID: request.Objective.DeliveryID, transitionID: string(request.TransitionID), parameters: parameterFlags, }) if err != nil { @@ -266,14 +269,15 @@ func bindRPCFlowEntry(ctx context.Context, request surfaces.Request) (surfaces.R request.ProgramFingerprint = bound.flowProgramFingerprint request.FlowID = bound.runID request.Objective.ID = bound.objectiveID - request.Objective.Kind = model.ObjectiveKind(bound.objectiveKind) + request.Objective.TargetID = model.TargetID(bound.targetID) + request.Objective.TrustedClass = model.TargetID(bound.trustedObjectiveClass) request.Objective.DeliveryID = bound.deliveryID request.Parameters = parameters return request, nil } -func resolveBoundPlan(repository string, entry controlprogram.Entry, entryObjective model.ObjectiveKind, options commandOptions) (string, string, error) { - if options.activeFlowBound && entryObjective == model.ObjectiveAbandoned { +func resolveBoundPlan(repository string, entry controlprogram.Entry, entryObjective softwareflow.EntryObjective, options commandOptions) (string, string, error) { + if options.activeFlowBound && entryObjective.TrustedClass == model.ObjectiveAbandoned { return "", options.deliveryID, nil } if options.runID == "" && options.deliveryID == "" { diff --git a/boatstack/cmd/boatstack-helper/flow_runtime_test.go b/boatstack/cmd/boatstack-helper/flow_runtime_test.go index fe7b67f..f5267b1 100644 --- a/boatstack/cmd/boatstack-helper/flow_runtime_test.go +++ b/boatstack/cmd/boatstack-helper/flow_runtime_test.go @@ -209,7 +209,7 @@ func TestFlowRunIdentitySurvivesWorkspaceTransfer(t *testing.T) { resumed, err := bindFlowEntry(context.Background(), commandOptions{ repository: destination, programID: "product-delivery", entryID: "run", host: "codex", flowProgramFingerprint: initial.flowProgramFingerprint, runID: initial.runID, - deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, objectiveID: initial.objectiveID, + deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: initial.objectiveID, transitionID: "plan.create", }) if err != nil { @@ -777,13 +777,13 @@ func TestFlowEntryBindsStableRunAndResumesManagedPlan(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.HasPrefix(initial.runID, "run-") || initial.deliveryID != "delivery-one" || initial.objectiveKind != "open-or-updated-pr" || len(initial.parameters) != 0 { + if !strings.HasPrefix(initial.runID, "run-") || initial.deliveryID != "delivery-one" || initial.targetID != "published-pr" || initial.trustedObjectiveClass != "open-or-updated-pr" || len(initial.parameters) != 0 { t.Fatalf("initial Flow context = %#v", initial) } for _, transitionID := range []string{"objective.bind", "plan.create"} { preManaged, err := bindFlowEntry(context.Background(), commandOptions{ repository: repository, programID: "product-delivery", entryID: "run", runID: initial.runID, host: "codex", - deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, objectiveID: initial.objectiveID, transitionID: transitionID, + deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: initial.objectiveID, transitionID: transitionID, }) if err != nil { t.Fatalf("pre-materialization %s binding failed: %v", transitionID, err) @@ -803,7 +803,7 @@ func TestFlowEntryBindsStableRunAndResumesManagedPlan(t *testing.T) { writeFixture(t, repository, ".boatstack/plans/inbox/unrelated.md", []byte("other plan")) resumed, err := bindFlowEntry(context.Background(), commandOptions{ repository: repository, programID: "product-delivery", entryID: "run", runID: initial.runID, host: "codex", - deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, objectiveID: initial.objectiveID, transitionID: "plan.create", + deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: initial.objectiveID, transitionID: "plan.create", }) if err != nil { t.Fatal(err) @@ -822,7 +822,9 @@ func TestFlowEntryBindsStableRunAndResumesManagedPlan(t *testing.T) { func TestRepositoryNamedAbandonmentEntryUsesCompiledObjective(t *testing.T) { entry := controlprogram.Entry{ID: "cancel", Target: "safely-abandoned"} - plan, delivery, err := resolveBoundPlan(t.TempDir(), entry, model.ObjectiveAbandoned, commandOptions{ + plan, delivery, err := resolveBoundPlan(t.TempDir(), entry, softwareflow.EntryObjective{ + TargetID: model.TargetID("safely-abandoned"), TrustedClass: model.ObjectiveAbandoned, + }, commandOptions{ entryID: "cancel", activeFlowBound: true, deliveryID: "delivery-one", }) if err != nil { @@ -845,7 +847,7 @@ func TestFlowEntryRejectsSelectedPlanContentSubstitution(t *testing.T) { writeFixture(t, repository, planPath, []byte("plan B")) _, err = bindFlowEntry(context.Background(), commandOptions{ repository: repository, programID: "product-delivery", entryID: "run", runID: initial.runID, host: "codex", - deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, objectiveID: initial.objectiveID, transitionID: "plan.create", + deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: initial.objectiveID, transitionID: "plan.create", }) if err == nil || !strings.Contains(err.Error(), "FLOW_RUN_MISMATCH") { t.Fatalf("plan substitution result = %v", err) @@ -865,7 +867,7 @@ func TestFlowEntryPreservesSelectedPlanFilenameBeforeMaterialization(t *testing. } resumed, err := bindFlowEntry(context.Background(), commandOptions{ repository: repository, programID: "product-delivery", entryID: "run", runID: initial.runID, host: "codex", - deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, objectiveID: initial.objectiveID, transitionID: "plan.create", + deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: initial.objectiveID, transitionID: "plan.create", }) if err != nil { t.Fatal(err) @@ -898,7 +900,7 @@ func TestFlowEntryRejectsAmbiguousPlanFilenameOnResume(t *testing.T) { } _, err = bindFlowEntry(context.Background(), commandOptions{ repository: repository, programID: "product-delivery", entryID: "run", runID: initial.runID, host: "codex", - deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, objectiveID: initial.objectiveID, transitionID: "plan.create", + deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: initial.objectiveID, transitionID: "plan.create", }) if err == nil || !strings.Contains(err.Error(), "FLOW_INPUT_INVALID") { t.Fatalf("ambiguous resume result = %v", err) @@ -915,7 +917,7 @@ func TestFlowEntryRejectsObjectiveSubstitutionWithinRun(t *testing.T) { } _, err = bindFlowEntry(context.Background(), commandOptions{ repository: repository, programID: "product-delivery", entryID: "run", host: "codex", - runID: initial.runID, deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, + runID: initial.runID, deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: "objective-substituted", transitionID: "objective.bind", }) if err == nil || !strings.Contains(err.Error(), "FLOW_CONTEXT_MISMATCH") { @@ -947,7 +949,7 @@ func TestFlowEntryRejectsManagedPlanSymlinkEscape(t *testing.T) { } _, err = bindFlowEntry(context.Background(), commandOptions{ repository: repository, programID: "product-delivery", entryID: "run", runID: initial.runID, host: "codex", - deliveryID: initial.deliveryID, objectiveKind: initial.objectiveKind, objectiveID: initial.objectiveID, + deliveryID: initial.deliveryID, targetID: initial.targetID, objectiveID: initial.objectiveID, }) if err == nil || !strings.Contains(err.Error(), "regular non-symlink") { t.Fatalf("managed symlink result = %v", err) diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index f095f8f..5d96583 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -41,7 +41,8 @@ type commandOptions struct { repository string format string objectiveID string - objectiveKind string + targetID string + trustedObjectiveClass string deliveryID string programID string flowProgramFingerprint string @@ -272,12 +273,12 @@ func parseOptions(command string, arguments []string, transition catalog.Transit flags.SetOutput(os.Stderr) options := commandOptions{format: "json", transitionID: string(transition), host: "cli"} if defaults != nil { - options.objectiveKind, options.deliveryID, options.objectiveID = defaults["objective-kind"], defaults["delivery"], defaults["objective-id"] + options.targetID, options.deliveryID, options.objectiveID = defaults["target-id"], defaults["delivery"], defaults["objective-id"] } flags.StringVar(&options.repository, "repo", ".", "explicit invoking repository or worktree") flags.StringVar(&options.format, "format", options.format, "json, text, or jsonl") flags.StringVar(&options.objectiveID, "objective-id", options.objectiveID, "configured objective identity") - flags.StringVar(&options.objectiveKind, "objective-kind", options.objectiveKind, "approved-plan, verified-implementation, open-or-updated-pr, merged-delivery, or safely-abandoned") + flags.StringVar(&options.targetID, "target-id", options.targetID, "program-scoped marked target identity") flags.StringVar(&options.deliveryID, "delivery", options.deliveryID, "delivery identity") flags.StringVar(&options.programID, "flow", "", "repository Control Program identity") flags.StringVar(&options.entryID, "entry", "", "named Flow entry") @@ -495,8 +496,8 @@ func buildRequest(operation surfaces.Operation, options commandOptions) (surface correlation = fmt.Sprintf("cli-%d-%d", os.Getpid(), now.UnixNano()) } objective := model.Objective{} - if options.objectiveKind != "" || options.objectiveID != "" || options.deliveryID != "" { - objective = model.Objective{ID: options.objectiveID, Kind: model.ObjectiveKind(options.objectiveKind), DeliveryID: options.deliveryID} + if options.targetID != "" || options.objectiveID != "" || options.deliveryID != "" { + objective = model.Objective{ID: options.objectiveID, TargetID: model.TargetID(options.targetID), TrustedClass: model.TargetID(options.trustedObjectiveClass), DeliveryID: options.deliveryID} if err := objective.Validate(); err != nil { return surfaces.Request{}, err } diff --git a/boatstack/cmd/boatstack-helper/main_test.go b/boatstack/cmd/boatstack-helper/main_test.go index 1653620..19b27aa 100644 --- a/boatstack/cmd/boatstack-helper/main_test.go +++ b/boatstack/cmd/boatstack-helper/main_test.go @@ -95,7 +95,7 @@ func TestHumanPublicationConfirmationBindsExactPreviewFingerprint(t *testing.T) // control-law: publication-authority-confirms-exact-preview-bytes now := time.Now().UTC() options := commandOptions{humanActor: "reviewer", transitionID: "publication.execute"} - objective := model.Objective{ID: "publish", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + objective := model.Objective{ID: "publish", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"} one, err := loadAuthority(options, "correlation", objective, protocol.Parameters{{Name: "preview_fingerprint", Value: strings.Repeat("a", 64)}}, now) if err != nil { t.Fatal(err) diff --git a/boatstack/controlprogram/frontend_conformance_test.go b/boatstack/controlprogram/frontend_conformance_test.go index 61dc8e1..7050489 100644 --- a/boatstack/controlprogram/frontend_conformance_test.go +++ b/boatstack/controlprogram/frontend_conformance_test.go @@ -2,6 +2,7 @@ package controlprogram_test import ( "bytes" + "context" "os" "os/exec" "path/filepath" @@ -11,6 +12,7 @@ import ( "testing" "github.com/operatorstack/boatstack/boatstack/controlprogram" + softwareflow "github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery" ) func TestTypeScriptDSLAndRawIRHaveOneCanonicalFingerprint(t *testing.T) { @@ -55,6 +57,59 @@ func TestTypeScriptDSLAndRawIRHaveOneCanonicalFingerprint(t *testing.T) { } } +func TestRepositoryOwnedSoftwareDeliveryFlowsShareOneRuntime(t *testing.T) { + // control-law: repositories-own-entry-target-and-transition-policy + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("cannot locate frontend fixtures") + } + moduleRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..")) + frontend := filepath.Join(filepath.Dir(moduleRoot), "node_modules", ".bin", "boatstack-flow-frontend") + if runtime.GOOS == "windows" { + frontend += ".cmd" + } + if _, err := os.Stat(frontend); err != nil { + t.Skip("Flow frontend dependencies are not installed") + } + resolver, err := softwareflow.NewResolver(context.Background()) + if err != nil { + t.Fatal(err) + } + cases := []struct { + fixture string + entries int + transitions int + }{ + {"product-delivery-a.flow.ts", 1, 1}, + {"product-delivery-b.flow.ts", 2, 2}, + {"product-delivery-c.flow.ts", 1, 6}, + } + for _, test := range cases { + t.Run(test.fixture, func(t *testing.T) { + source := filepath.Join(moduleRoot, "testdata", "control-programs", test.fixture) + frontendRaw, commandErr := exec.Command(frontend, source).CombinedOutput() + if commandErr != nil { + t.Fatalf("compile repository Flow: %v\n%s", commandErr, frontendRaw) + } + compiled, compileErr := controlprogram.Load(bytes.NewReader(frontendRaw), resolver) + if compileErr != nil { + t.Fatal(compileErr) + } + definition, definitionErr := softwareflow.NewDefinition(compiled, resolver) + if definitionErr != nil { + t.Fatal(definitionErr) + } + manifest, manifestErr := definition.RuntimeManifest(context.Background()) + if manifestErr != nil { + t.Fatal(manifestErr) + } + if len(compiled.Document.Entries) != test.entries || len(manifest.Transitions) != test.transitions { + t.Fatalf("entries=%d transitions=%d", len(compiled.Document.Entries), len(manifest.Transitions)) + } + }) + } +} + func TestTypeScriptFrontendRejectsRepositoryCodeWithoutExecutingIt(t *testing.T) { // control-law: authoring-frontends-parse-repository-declarations-without-module-execution _, file, _, ok := runtime.Caller(0) diff --git a/boatstack/core/transitions.json b/boatstack/core/transitions.json index 0933c9d..4679de5 100644 --- a/boatstack/core/transitions.json +++ b/boatstack/core/transitions.json @@ -19,7 +19,7 @@ "OBSERVED", "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -222,7 +222,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -426,7 +426,7 @@ "target_phases": [ "DORMANT" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -631,7 +631,7 @@ "target_phases": [ "OBSERVED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -797,7 +797,7 @@ "target_phases": [ "OBSERVED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -984,7 +984,7 @@ "target_phases": [ "DORMANT" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1165,7 +1165,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1356,7 +1356,7 @@ "OBSERVED", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1549,7 +1549,7 @@ "FRONTIER", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1714,7 +1714,7 @@ "OBSERVED", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1900,7 +1900,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2113,7 +2113,7 @@ "FRONTIER", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2263,7 +2263,7 @@ "target_phases": [ "OBSERVED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2478,7 +2478,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2722,7 +2722,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2961,7 +2961,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3148,7 +3148,7 @@ "ACTIVE", "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3190,7 +3190,7 @@ "idempotent": true, "parameters": [ { - "name": "objective_kind", + "name": "target_id", "required": true, "secret": false }, @@ -3298,7 +3298,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3453,7 +3453,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3603,7 +3603,7 @@ "target_phases": [ "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3795,7 +3795,7 @@ "target_phases": [ "OBSERVED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3897,7 +3897,7 @@ "target_phases": [ "OBSERVED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3999,7 +3999,7 @@ "target_phases": [ "OBSERVED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4102,7 +4102,7 @@ "OBSERVED", "RECOVERY" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4213,7 +4213,7 @@ "OBSERVED", "UNRESOLVED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4324,7 +4324,7 @@ "DORMANT", "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4434,7 +4434,7 @@ "target_phases": [ "RECOVERY" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4551,7 +4551,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4655,7 +4655,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4762,7 +4762,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4869,7 +4869,7 @@ "ACTIVE", "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4977,7 +4977,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -5083,7 +5083,7 @@ "UNRESOLVED", "RECOVERY" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", diff --git a/boatstack/delivery/control.go b/boatstack/delivery/control.go index 9741cb4..ed83c49 100644 --- a/boatstack/delivery/control.go +++ b/boatstack/delivery/control.go @@ -41,7 +41,7 @@ type StateAssignment = catalog.StateAssignment type StatePrecondition = catalog.StatePrecondition type StateValueReference = catalog.StateValueReference type StateFacet = model.StateFacet -type ObjectiveKind = model.ObjectiveKind +type TargetID = model.TargetID type ProtocolPhase = model.ProtocolPhase type FactStatus = model.FactStatus type FacetName = model.FacetName @@ -179,7 +179,7 @@ type ProgramRuntimeManifest struct { Version string `json:"version"` ProtocolVersion int `json:"protocol_version"` RuntimeMode ProgramRuntimeMode `json:"runtime_mode"` - SupportedObjectives []ObjectiveKind `json:"supported_objectives"` + SupportedTargets []TargetID `json:"supported_objectives"` ObjectiveContracts []ObjectiveContract `json:"objective_contracts"` Transitions []Transition `json:"transitions"` Facts []string `json:"facts,omitempty"` @@ -195,8 +195,8 @@ type ProgramRuntimeManifest struct { } type ObjectiveConstraint struct { - ObjectiveKind ObjectiveKind `json:"objective_kind"` - Conditions []FacetCondition `json:"conditions"` + TargetID TargetID `json:"target_id"` + Conditions []FacetCondition `json:"conditions"` } type ExtensionManifest struct { @@ -417,7 +417,7 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error return ControlProgram{}, err } - extensionConditions := map[model.ObjectiveKind][]catalog.FacetCondition{} + extensionConditions := map[model.TargetID][]catalog.FacetCondition{} compiledExtensions := make([]compiledExtension, 0, len(request.Extensions)) extensionIdentities := make([]ComponentIdentity, 0, len(request.Extensions)) extensionCount := 0 @@ -461,7 +461,7 @@ func Compile(ctx context.Context, request CompileRequest) (ControlProgram, error } compiledExtensions = append(compiledExtensions, compiledExtension{manifest: manifest, identity: identity, runtime: runtime}) for _, constraint := range manifest.ObjectiveConstraints { - extensionConditions[constraint.ObjectiveKind] = append(extensionConditions[constraint.ObjectiveKind], constraint.Conditions...) + extensionConditions[constraint.TargetID] = append(extensionConditions[constraint.TargetID], constraint.Conditions...) } for _, resource := range manifest.OwnedResources { if !strings.HasPrefix(resource, manifest.ID+".") { @@ -631,7 +631,7 @@ func validateCore(manifest CoreSystemManifest) error { func validateProgramRuntime(manifest ProgramRuntimeManifest) error { if !componentID.MatchString(manifest.ID) || manifest.Version == "" || manifest.ProtocolVersion != ProgramRuntimeProtocolVersion || (manifest.RuntimeMode != ProgramRuntimeNative && manifest.RuntimeMode != ProgramRuntimeProtocol) || - len(manifest.Transitions) == 0 || len(manifest.SupportedObjectives) == 0 || + len(manifest.Transitions) == 0 || len(manifest.SupportedTargets) == 0 || len(manifest.Capabilities) == 0 || !validJSONObject(manifest.ConfigurationSchema) || manifest.PrivacyClassification == "" || manifest.TelemetryClassification == "" { @@ -644,18 +644,18 @@ func validateProgramRuntime(manifest ProgramRuntimeManifest) error { if err := validateDeclaredSchema(manifest.ConfigurationSchema, manifest.Settings, "ProgramRuntime "+manifest.ID+" configuration"); err != nil { return err } - supported := map[ObjectiveKind]bool{} - for _, objective := range manifest.SupportedObjectives { + supported := map[TargetID]bool{} + for _, objective := range manifest.SupportedTargets { if !objective.Valid() || supported[objective] { return fmt.Errorf("ProgramRuntime has invalid or duplicate objective %q", objective) } supported[objective] = true } for _, contract := range manifest.ObjectiveContracts { - if !supported[contract.ObjectiveKind] { - return fmt.Errorf("ProgramRuntime objective contract %q is not supported", contract.ObjectiveKind) + if !supported[contract.TargetID] { + return fmt.Errorf("ProgramRuntime objective contract %q is not supported", contract.TargetID) } - delete(supported, contract.ObjectiveKind) + delete(supported, contract.TargetID) } if len(supported) != 0 { return fmt.Errorf("ProgramRuntime does not define every supported objective contract") @@ -827,9 +827,9 @@ func validateExtension(manifest ExtensionManifest, seen, reserved map[string]boo return fmt.Errorf("extension %q cannot depend on itself", manifest.ID) } } - constrainedFacets := map[ObjectiveKind]map[FacetName][]FacetCondition{} + constrainedFacets := map[TargetID]map[FacetName][]FacetCondition{} for _, constraint := range manifest.ObjectiveConstraints { - if !constraint.ObjectiveKind.Valid() || len(constraint.Conditions) == 0 { + if !constraint.TargetID.Valid() || len(constraint.Conditions) == 0 { return fmt.Errorf("extension %q has invalid objective constraint", manifest.ID) } for _, condition := range constraint.Conditions { @@ -841,10 +841,10 @@ func validateExtension(manifest ExtensionManifest, seen, reserved map[string]boo return fmt.Errorf("extension %q has invalid objective-condition status %q", manifest.ID, status) } } - if constrainedFacets[constraint.ObjectiveKind] == nil { - constrainedFacets[constraint.ObjectiveKind] = map[FacetName][]FacetCondition{} + if constrainedFacets[constraint.TargetID] == nil { + constrainedFacets[constraint.TargetID] = map[FacetName][]FacetCondition{} } - constrainedFacets[constraint.ObjectiveKind][condition.Facet] = append(constrainedFacets[constraint.ObjectiveKind][condition.Facet], condition) + constrainedFacets[constraint.TargetID][condition.Facet] = append(constrainedFacets[constraint.TargetID][condition.Facet], condition) } } declaredRecovery := map[TransitionID]bool{} @@ -877,10 +877,10 @@ func validateExtension(manifest ExtensionManifest, seen, reserved map[string]boo } } if transition.SelectionClass == SelectionObjectiveRequired { - if len(transition.ObjectiveKinds) == 0 { + if len(transition.TargetIDs) == 0 { return fmt.Errorf("extension transition %q is implicitly selectable without an explicit constrained objective", transition.ID) } - for _, objective := range transition.ObjectiveKinds { + for _, objective := range transition.TargetIDs { discharges := false for _, target := range transition.TargetConditions { for _, obligation := range constrainedFacets[objective][target.Facet] { @@ -1062,7 +1062,7 @@ func validateDependencies(extensions []compiledExtension) error { func cloneTransition(value Transition) Transition { value.SourcePhases = append([]model.ProtocolPhase(nil), value.SourcePhases...) value.TargetPhases = append([]model.ProtocolPhase(nil), value.TargetPhases...) - value.ObjectiveKinds = append([]model.ObjectiveKind(nil), value.ObjectiveKinds...) + value.TargetIDs = append([]model.TargetID(nil), value.TargetIDs...) value.RequiredIdentity = append([]string(nil), value.RequiredIdentity...) value.Authority = append([]catalog.AuthorityClass(nil), value.Authority...) value.AuthorityAll = append([]catalog.AuthorityClass(nil), value.AuthorityAll...) @@ -1110,7 +1110,7 @@ func cloneCoreManifest(value CoreSystemManifest) CoreSystemManifest { } func cloneRuntimeManifest(value ProgramRuntimeManifest) ProgramRuntimeManifest { - value.SupportedObjectives = append([]ObjectiveKind(nil), value.SupportedObjectives...) + value.SupportedTargets = append([]TargetID(nil), value.SupportedTargets...) value.ObjectiveContracts = append([]ObjectiveContract(nil), value.ObjectiveContracts...) for index := range value.ObjectiveContracts { value.ObjectiveContracts[index].Conditions = cloneConditions(value.ObjectiveContracts[index].Conditions) diff --git a/boatstack/delivery/control_test.go b/boatstack/delivery/control_test.go index b14b6d2..6e77252 100644 --- a/boatstack/delivery/control_test.go +++ b/boatstack/delivery/control_test.go @@ -260,7 +260,7 @@ func TestExtensionCompilationRejectsBoundaryViolations(t *testing.T) { value.ObjectiveConstraints[0].Conditions[0].Statuses = []delivery.FactStatus{"invented"} }, "objective-selection-without-matching-obligation": func(value *delivery.ExtensionManifest) { - value.Transitions[0].ObjectiveKinds = []delivery.ObjectiveKind{delivery.ObjectiveVerified} + value.Transitions[0].TargetIDs = []delivery.TargetID{delivery.ObjectiveVerified} }, "objective-selection-does-not-discharge-obligation": func(value *delivery.ExtensionManifest) { value.Transitions[0].TargetConditions[0].Values = []string{"missing"} @@ -336,7 +336,7 @@ func TestExtensionObjectiveConditionsAreConjunctive(t *testing.T) { } conditionCount := func(program delivery.ControlProgram) int { for _, contract := range program.RuntimeObjectiveContracts().All() { - if contract.ObjectiveKind == delivery.ObjectiveVerified { + if contract.TargetID == delivery.ObjectiveVerified { return len(contract.Conditions) } } @@ -345,15 +345,15 @@ func TestExtensionObjectiveConditionsAreConjunctive(t *testing.T) { if conditionCount(extended) != conditionCount(base) { t.Fatalf("release-note extension unexpectedly changed the verified objective") } - for _, objective := range []delivery.ObjectiveKind{delivery.ObjectiveOpenPR, delivery.ObjectiveMerged} { + for _, objective := range []delivery.TargetID{delivery.ObjectiveOpenPR, delivery.ObjectiveMerged} { baseCount, extendedCount := 0, 0 for _, contract := range base.RuntimeObjectiveContracts().All() { - if contract.ObjectiveKind == objective { + if contract.TargetID == objective { baseCount = len(contract.Conditions) } } for _, contract := range extended.RuntimeObjectiveContracts().All() { - if contract.ObjectiveKind == objective { + if contract.TargetID == objective { extendedCount = len(contract.Conditions) } } @@ -410,7 +410,7 @@ func cloneManifest(t *testing.T, value delivery.ExtensionManifest) delivery.Exte func (e declarationOnlyExtension) ExtensionManifest(context.Context) (delivery.ExtensionManifest, error) { var constraints []delivery.ObjectiveConstraint if len(e.objectiveConditions) != 0 { - constraints = []delivery.ObjectiveConstraint{{ObjectiveKind: delivery.ObjectiveOpenPR, Conditions: append([]delivery.FacetCondition(nil), e.objectiveConditions...)}} + constraints = []delivery.ObjectiveConstraint{{TargetID: delivery.ObjectiveOpenPR, Conditions: append([]delivery.FacetCondition(nil), e.objectiveConditions...)}} } return delivery.ExtensionManifest{ ID: e.id, Version: "1.0.0", ProtocolVersion: delivery.ExtensionProtocolVersion, diff --git a/boatstack/delivery/program_manifest.go b/boatstack/delivery/program_manifest.go index 6bf3449..e11bfad 100644 --- a/boatstack/delivery/program_manifest.go +++ b/boatstack/delivery/program_manifest.go @@ -51,7 +51,7 @@ type ProgramTransition struct { Class EventClass `json:"class"` SourcePhases []ProtocolPhase `json:"source_phases"` TargetPhases []ProtocolPhase `json:"target_phases"` - ObjectiveKinds []ObjectiveKind `json:"objective_kinds,omitempty"` + TargetIDs []TargetID `json:"target_ids,omitempty"` RequiredIdentity []string `json:"required_identity"` Authority []AuthorityClass `json:"authority"` AuthorityAll []AuthorityClass `json:"authority_all,omitempty"` @@ -276,7 +276,7 @@ func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (Co return ControlProgram{}, invalidProgram(fmt.Sprintf("objective_contracts[%d].conditions", index), err.Error()) } } - sort.Slice(contracts, func(i, j int) bool { return contracts[i].ObjectiveKind < contracts[j].ObjectiveKind }) + sort.Slice(contracts, func(i, j int) bool { return contracts[i].TargetID < contracts[j].TargetID }) objectiveContracts, err := catalog.NewObjectiveContracts(contracts, nil) if err != nil { return ControlProgram{}, invalidProgram("objective_contracts", err.Error()) @@ -335,7 +335,7 @@ func ValidateProgram(manifest ProgramManifest, runtime RuntimeCompatibility) (Co func (value ProgramTransition) runtimeTransition() Transition { return Transition{ ID: value.ID, Version: value.Version, SelectionClass: value.SelectionClass, Class: value.Class, - SourcePhases: value.SourcePhases, TargetPhases: value.TargetPhases, ObjectiveKinds: value.ObjectiveKinds, + SourcePhases: value.SourcePhases, TargetPhases: value.TargetPhases, TargetIDs: value.TargetIDs, RequiredIdentity: value.RequiredIdentity, Authority: value.Authority, AuthorityAll: value.AuthorityAll, RequiredCapabilities: value.RequiredCapabilities, RequiredEvidence: value.RequiredEvidence, OwnedResources: value.OwnedResources, Effect: value.Effect, @@ -448,7 +448,7 @@ func normalizeProgramTransition(value Transition) (Transition, error) { if err != nil { return Transition{}, err } - value.ObjectiveKinds, err = uniqueSorted(value.ObjectiveKinds, func(v ObjectiveKind) string { return string(v) }) + value.TargetIDs, err = uniqueSorted(value.TargetIDs, func(v TargetID) string { return string(v) }) if err != nil { return Transition{}, err } diff --git a/boatstack/delivery/program_manifest_test.go b/boatstack/delivery/program_manifest_test.go index e880b1b..a9ab76c 100644 --- a/boatstack/delivery/program_manifest_test.go +++ b/boatstack/delivery/program_manifest_test.go @@ -314,7 +314,7 @@ func programFixture() delivery.ProgramManifest { advance.Class = delivery.EventOwnedLocal advance.SourcePhases = []delivery.ProtocolPhase{delivery.PhaseActive} advance.TargetPhases = []delivery.ProtocolPhase{delivery.PhaseTerminal} - advance.ObjectiveKinds = []delivery.ObjectiveKind{delivery.ObjectiveVerified} + advance.TargetIDs = []delivery.TargetID{delivery.ObjectiveVerified} advance.Authority = []delivery.AuthorityClass{delivery.AuthorityHuman, delivery.AuthorityRepository} advance.RequiredCapabilities = []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityProductMutate} advance.Effect = "program.advance" @@ -334,7 +334,7 @@ func programFixture() delivery.ProgramManifest { Effects: []string{"program.advance", "program.recover"}, Verifiers: []string{"program.current", "program.terminal"}, CapabilitySurface: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityProductMutate}, }, - OwnedResources: []string{"program.state"}, ObjectiveContracts: []delivery.ObjectiveContract{{ObjectiveKind: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, "terminal")}}}, + OwnedResources: []string{"program.state"}, ObjectiveContracts: []delivery.ObjectiveContract{{TargetID: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, "terminal")}}}, Transitions: []delivery.ProgramTransition{advance, recovery}, } } diff --git a/boatstack/extension/releasenote/releasenote.go b/boatstack/extension/releasenote/releasenote.go index 081d8d9..c0c79a1 100644 --- a/boatstack/extension/releasenote/releasenote.go +++ b/boatstack/extension/releasenote/releasenote.go @@ -39,7 +39,7 @@ func (Extension) ExtensionManifest(context.Context) (delivery.ExtensionManifest, transition := catalog.Transition{ ID: Transition, Version: 1, Class: catalog.EventOwnedLocal, SelectionClass: catalog.SelectionObjectiveRequired, SourcePhases: []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, TargetPhases: []model.ProtocolPhase{model.PhaseActive, model.PhaseTerminal}, - ObjectiveKinds: []model.ObjectiveKind{model.ObjectiveOpenPR, model.ObjectiveMerged}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id", "controller-id", "topology", "host", "correlation-id"}, + TargetIDs: []model.TargetID{model.ObjectiveOpenPR, model.ObjectiveMerged}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id", "controller-id", "topology", "host", "correlation-id"}, Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredEvidence: []string{"snapshot-fingerprint", "objective", "facet:" + FactID}, OwnedResources: []string{Resource}, Effect: Effect, LocalEffects: []catalog.EffectID{Effect}, Idempotent: true, OwnedFacets: []model.StateFacet{model.StateFacetControl}, StateEffect: catalog.StateEffect{Kind: catalog.StateEffectAssignments}, @@ -62,8 +62,8 @@ func (Extension) ExtensionManifest(context.Context) (delivery.ExtensionManifest, Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}, } transition.RequiredCapabilities = []catalog.Capability{catalog.CapabilityRepositoryWrite, catalog.CapabilityCommandExecute} - constraint := func(objective model.ObjectiveKind) delivery.ObjectiveConstraint { - return delivery.ObjectiveConstraint{ObjectiveKind: objective, Conditions: []catalog.FacetCondition{known(model.FacetName(FactID), "verified", "not-required")}} + constraint := func(objective model.TargetID) delivery.ObjectiveConstraint { + return delivery.ObjectiveConstraint{TargetID: objective, Conditions: []catalog.FacetCondition{known(model.FacetName(FactID), "verified", "not-required")}} } return delivery.ExtensionManifest{ ID: ID, Version: Version, ProtocolVersion: delivery.ExtensionProtocolVersion, diff --git a/boatstack/flow/softwaredelivery/bindings.go b/boatstack/flow/softwaredelivery/bindings.go index 76a2131..0967ad0 100644 --- a/boatstack/flow/softwaredelivery/bindings.go +++ b/boatstack/flow/softwaredelivery/bindings.go @@ -92,6 +92,10 @@ func (r Resolver) Transition(reference string) (delivery.Transition, bool) { return delivery.Transition{}, false } transition, ok := r.transitions[id] + transition.TargetIDs = append([]delivery.TargetID(nil), transition.TargetIDs...) + transition.SourceConditions = append([]delivery.FacetCondition(nil), transition.SourceConditions...) + transition.TargetConditions = append([]delivery.FacetCondition(nil), transition.TargetConditions...) + transition.RequiredCapabilities = append([]delivery.Capability(nil), transition.RequiredCapabilities...) return transition, ok } diff --git a/boatstack/flow/softwaredelivery/definition.go b/boatstack/flow/softwaredelivery/definition.go index 6375e37..7aa4b87 100644 --- a/boatstack/flow/softwaredelivery/definition.go +++ b/boatstack/flow/softwaredelivery/definition.go @@ -1,12 +1,10 @@ package softwaredelivery import ( - "bytes" "context" "encoding/json" "fmt" "sort" - "strings" "github.com/operatorstack/boatstack/boatstack/controlprogram" "github.com/operatorstack/boatstack/boatstack/delivery" @@ -21,6 +19,15 @@ type Definition struct { resolver Resolver } +// EntryObjective binds a repository-owned target identity and terminal +// predicate to the trusted software-delivery objective class whose operators +// may make progress toward it. +type EntryObjective struct { + TargetID model.TargetID + TrustedClass model.TargetID + Contract delivery.ObjectiveContract +} + func NewDefinition(compiled controlprogram.Compiled, resolver Resolver) (Definition, error) { if _, err := ObjectiveForEntry(context.Background(), compiled, resolver, compiled.Document.Entries[0].ID); err != nil { return Definition{}, err @@ -39,14 +46,14 @@ func (d Definition) RuntimeManifest(ctx context.Context) (delivery.ProgramRuntim for _, operator := range d.compiled.Document.Operators { operatorByID[operator.ID] = operator } - objectives := map[model.ObjectiveKind]bool{} - contracts := map[model.ObjectiveKind]delivery.ObjectiveContract{} + objectives := map[model.TargetID]EntryObjective{} + contracts := map[model.TargetID]delivery.ObjectiveContract{} for _, entry := range d.compiled.Document.Entries { - kind, contract, objectiveErr := objectiveContractForEntry(d.compiled, base, entry.ID) + objective, objectiveErr := objectiveContractForEntry(d.compiled, base, entry.ID) if objectiveErr != nil { return delivery.ProgramRuntimeManifest{}, objectiveErr } - objectives[kind], contracts[kind] = true, contract + objectives[objective.TargetID], contracts[objective.TargetID] = objective, objective.Contract } selected := make([]delivery.Transition, 0, len(d.compiled.Document.Transitions)) @@ -78,23 +85,23 @@ func (d Definition) RuntimeManifest(ctx context.Context) (delivery.ProgramRuntim transition.SourceConditions = append(transition.SourceConditions, guard...) transition.TargetConditions = append(transition.TargetConditions, target...) transition.Priority = declaration.Priority - trustedObjectives := append([]model.ObjectiveKind(nil), transition.ObjectiveKinds...) - transition.ObjectiveKinds = transition.ObjectiveKinds[:0] - for objective := range objectives { - if containsAll(trustedObjectives, []model.ObjectiveKind{objective}) { - transition.ObjectiveKinds = append(transition.ObjectiveKinds, objective) + trustedObjectives := append([]model.TargetID(nil), transition.TargetIDs...) + transition.TargetIDs = transition.TargetIDs[:0] + for targetID, objective := range objectives { + if containsAll(trustedObjectives, []model.TargetID{objective.TrustedClass}) { + transition.TargetIDs = append(transition.TargetIDs, targetID) } } - if len(transition.ObjectiveKinds) == 0 { - return delivery.ProgramRuntimeManifest{}, fmt.Errorf("transition %q supports none of the declared entry objectives", declaration.ID) + if len(transition.TargetIDs) == 0 { + return delivery.ProgramRuntimeManifest{}, fmt.Errorf("transition %q supports none of the declared entry targets", declaration.ID) } - if transition.ID == "plan.abandon" && objectives[model.ObjectiveAbandoned] { + if transition.ID == "plan.abandon" && hasTrustedClass(objectives, model.ObjectiveAbandoned) { // A repository Flow that explicitly exposes a safely-abandoned entry // makes abandonment progress for that objective only. Human authority // remains mandatory and other objectives cannot select this transition. transition.SelectionClass = delivery.SelectionProgramProgress } - sort.Slice(transition.ObjectiveKinds, func(i, j int) bool { return transition.ObjectiveKinds[i] < transition.ObjectiveKinds[j] }) + sort.Slice(transition.TargetIDs, func(i, j int) bool { return transition.TargetIDs[i] < transition.TargetIDs[j] }) selected = append(selected, transition) } if len(selected) == 0 { @@ -106,33 +113,32 @@ func (d Definition) RuntimeManifest(ctx context.Context) (delivery.ProgramRuntim selected[index].RequiredCapabilities = delivery.KernelEffectCapabilities(selected[index]) capabilities = delivery.UnionCapabilities(capabilities, selected[index].RequiredCapabilities) } - supported := make([]model.ObjectiveKind, 0, len(objectives)) + supported := make([]model.TargetID, 0, len(objectives)) objectiveContracts := make([]delivery.ObjectiveContract, 0, len(contracts)) for objective := range objectives { supported = append(supported, objective) objectiveContracts = append(objectiveContracts, contracts[objective]) } sort.Slice(supported, func(i, j int) bool { return supported[i] < supported[j] }) - sort.Slice(objectiveContracts, func(i, j int) bool { return objectiveContracts[i].ObjectiveKind < objectiveContracts[j].ObjectiveKind }) + sort.Slice(objectiveContracts, func(i, j int) bool { return objectiveContracts[i].TargetID < objectiveContracts[j].TargetID }) settings, _ := json.Marshal(map[string]string{"flow_id": d.compiled.Document.Program.ID, "flow_fingerprint": d.compiled.Fingerprint}) base.Version = standard.Version + "+flow." + d.compiled.Fingerprint[:12] - base.SupportedObjectives = supported + base.SupportedTargets = supported base.ObjectiveContracts = objectiveContracts base.Transitions, base.OwnedResources, base.Effects, base.Verifiers = selected, resources, effects, verifiers base.Capabilities, base.RecoveryTransitions, base.Settings = capabilities, recoveries, settings return base, nil } -func ObjectiveForEntry(ctx context.Context, compiled controlprogram.Compiled, resolver Resolver, entryID string) (delivery.ObjectiveKind, error) { +func ObjectiveForEntry(ctx context.Context, compiled controlprogram.Compiled, resolver Resolver, entryID string) (EntryObjective, error) { base, err := standard.Definition().RuntimeManifest(ctx) if err != nil { - return "", err + return EntryObjective{}, err } - kind, _, err := objectiveContractForEntry(compiled, base, entryID) - return kind, err + return objectiveContractForEntry(compiled, base, entryID) } -func objectiveContractForEntry(compiled controlprogram.Compiled, base delivery.ProgramRuntimeManifest, entryID string) (model.ObjectiveKind, delivery.ObjectiveContract, error) { +func objectiveContractForEntry(compiled controlprogram.Compiled, base delivery.ProgramRuntimeManifest, entryID string) (EntryObjective, error) { var targetID string for _, entry := range compiled.Document.Entries { if entry.ID == entryID { @@ -141,7 +147,10 @@ func objectiveContractForEntry(compiled controlprogram.Compiled, base delivery.P } } if targetID == "" { - return "", delivery.ObjectiveContract{}, fmt.Errorf("unknown Flow entry %q", entryID) + return EntryObjective{}, fmt.Errorf("unknown Flow entry %q", entryID) + } + if !model.TargetID(targetID).Valid() { + return EntryObjective{}, fmt.Errorf("entry %q target identity is invalid", entryID) } var predicate controlprogram.Predicate for _, target := range compiled.Document.Targets { @@ -152,15 +161,48 @@ func objectiveContractForEntry(compiled controlprogram.Compiled, base delivery.P } conditions, err := conjunctiveConditions(predicate) if err != nil { - return "", delivery.ObjectiveContract{}, fmt.Errorf("entry %q target is not a software-delivery marked state: %w", entryID, err) + return EntryObjective{}, fmt.Errorf("entry %q target is not a software-delivery marked state: %w", entryID, err) } - normalizedTarget := canonicalConditions(conditions) + var matches []delivery.ObjectiveContract for _, contract := range base.ObjectiveContracts { - if bytes.Equal(normalizedTarget, canonicalConditions(contract.Conditions)) { - return contract.ObjectiveKind, contract, nil + if conditionsStrengthen(conditions, contract.Conditions) { + matches = append(matches, contract) } } - return "", delivery.ObjectiveContract{}, fmt.Errorf("entry %q target does not match a trusted software-delivery marked state", entryID) + if len(matches) != 1 { + return EntryObjective{}, fmt.Errorf("entry %q target must strengthen exactly one trusted software-delivery marked state", entryID) + } + target := model.TargetID(targetID) + return EntryObjective{ + TargetID: target, + TrustedClass: matches[0].TargetID, + Contract: delivery.ObjectiveContract{TargetID: target, Conditions: conditions}, + }, nil +} + +func conditionsStrengthen(repository, trusted []delivery.FacetCondition) bool { + for _, required := range trusted { + found := false + for _, declared := range repository { + if declared.Facet == required.Facet && conditionImplies(declared, required) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +func hasTrustedClass(objectives map[model.TargetID]EntryObjective, class model.TargetID) bool { + for _, objective := range objectives { + if objective.TrustedClass == class { + return true + } + } + return false } func conjunctiveConditions(predicate controlprogram.Predicate) ([]delivery.FacetCondition, error) { @@ -191,21 +233,6 @@ func conjunctiveConditions(predicate controlprogram.Predicate) ([]delivery.Facet return nil, fmt.Errorf("only true, fact, and all predicates are supported") } -func canonicalConditions(values []delivery.FacetCondition) []byte { - copy := append([]delivery.FacetCondition(nil), values...) - for index := range copy { - sort.Slice(copy[index].Statuses, func(i, j int) bool { return copy[index].Statuses[i] < copy[index].Statuses[j] }) - sort.Strings(copy[index].Values) - } - sort.Slice(copy, func(i, j int) bool { - left, _ := json.Marshal(copy[i]) - right, _ := json.Marshal(copy[j]) - return strings.Compare(string(left), string(right)) < 0 - }) - encoded, _ := json.Marshal(copy) - return encoded -} - func requireImpliedTarget(trusted, repository []delivery.FacetCondition) error { for _, candidate := range repository { implied := false diff --git a/boatstack/flow/softwaredelivery/definition_test.go b/boatstack/flow/softwaredelivery/definition_test.go index 095a8c1..01112f3 100644 --- a/boatstack/flow/softwaredelivery/definition_test.go +++ b/boatstack/flow/softwaredelivery/definition_test.go @@ -113,6 +113,53 @@ func TestRepositoryTargetMustBeImpliedByTrustedPostcondition(t *testing.T) { } } +func TestRepositoryTargetIdentityAndStrengtheningBecomeRuntimeContract(t *testing.T) { + // control-law: repository-marked-target-is-the-runtime-terminal-contract + truth := true + compiled, resolver := compiledFlow(t, controlprogram.Predicate{True: &truth}) + document := compiled.Document + document.Facets = append(document.Facets, controlprogram.Facet{ID: "release-policy", Kind: "string"}) + document.Targets[0].ID = "release-ready" + document.Targets[0].Predicate.All = append(document.Targets[0].Predicate.All, fact("release-policy", "satisfied")) + document.Entries[0].ID, document.Entries[0].Target = "deliver", "release-ready" + repositoryOwned, err := controlprogram.Compile(document, resolver) + if err != nil { + t.Fatal(err) + } + definition, err := softwareflow.NewDefinition(repositoryOwned, resolver) + if err != nil { + t.Fatal(err) + } + manifest, err := definition.RuntimeManifest(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(manifest.SupportedTargets) != 1 || manifest.SupportedTargets[0] != "release-ready" { + t.Fatalf("runtime targets = %#v", manifest.SupportedTargets) + } + if len(manifest.ObjectiveContracts) != 1 || manifest.ObjectiveContracts[0].TargetID != "release-ready" || len(manifest.ObjectiveContracts[0].Conditions) != 5 { + t.Fatalf("runtime terminal contracts = %#v", manifest.ObjectiveContracts) + } + if len(manifest.Transitions) != 1 || len(manifest.Transitions[0].TargetIDs) != 1 || manifest.Transitions[0].TargetIDs[0] != "release-ready" { + t.Fatalf("target-conditioned transition = %#v", manifest.Transitions) + } +} + +func TestRepositoryTargetCannotWeakenTrustedTerminalLaw(t *testing.T) { + // control-law: repository-targets-may-strengthen-but-never-weaken-domain-safety + truth := true + compiled, resolver := compiledFlow(t, controlprogram.Predicate{True: &truth}) + document := compiled.Document + document.Targets[0].Predicate = fact("publication", "open") + weakened, err := controlprogram.Compile(document, resolver) + if err != nil { + t.Fatal(err) + } + if _, err = softwareflow.NewDefinition(weakened, resolver); err == nil || !strings.Contains(err.Error(), "strengthen exactly one trusted") { + t.Fatalf("weakened target result = %v", err) + } +} + func TestRepositoryTransitionMustMatchTrustedBindingIdentity(t *testing.T) { truth := true compiled, resolver := compiledFlow(t, controlprogram.Predicate{True: &truth}) @@ -131,7 +178,7 @@ func TestRepositoryTransitionMustMatchTrustedBindingIdentity(t *testing.T) { } } -func TestRepositoryTransitionCannotWidenTrustedObjectiveKinds(t *testing.T) { +func TestRepositoryTransitionCannotWidenTrustedTargetIDs(t *testing.T) { truth := true compiled, resolver := compiledFlow(t, controlprogram.Predicate{True: &truth}) widened := compiled.Document @@ -146,7 +193,7 @@ func TestRepositoryTransitionCannotWidenTrustedObjectiveKinds(t *testing.T) { if err == nil { _, err = definition.RuntimeManifest(context.Background()) } - if err == nil || !strings.Contains(err.Error(), "supports none of the declared entry objectives") { + if err == nil || !strings.Contains(err.Error(), "supports none of the declared entry targets") { t.Fatalf("widened objective result = %v", err) } } @@ -193,7 +240,7 @@ func TestAbandonmentEntryMakesTrustedAbandonmentObjectiveProgress(t *testing.T) } for _, transition := range manifest.Transitions { if transition.ID == "plan.abandon" { - if transition.SelectionClass != delivery.SelectionProgramProgress || len(transition.ObjectiveKinds) != 1 || transition.ObjectiveKinds[0] != delivery.ObjectiveAbandoned { + if transition.SelectionClass != delivery.SelectionProgramProgress || len(transition.TargetIDs) != 1 || transition.TargetIDs[0] != delivery.ObjectiveAbandoned { t.Fatalf("abandonment transition = %#v", transition) } if transition.Priority != 31 { diff --git a/boatstack/flow/standard/standard.go b/boatstack/flow/standard/standard.go index 2987356..1d89159 100644 --- a/boatstack/flow/standard/standard.go +++ b/boatstack/flow/standard/standard.go @@ -39,7 +39,7 @@ func (definition) RuntimeManifest(context.Context) (delivery.ProgramRuntimeManif } return delivery.ProgramRuntimeManifest{ ID: ID, Version: Version, ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, RuntimeMode: delivery.ProgramRuntimeNative, - SupportedObjectives: []delivery.ObjectiveKind{ + SupportedTargets: []delivery.TargetID{ model.ObjectiveApprovedPlan, model.ObjectiveVerified, model.ObjectiveOpenPR, model.ObjectiveMerged, model.ObjectiveAbandoned, }, @@ -108,8 +108,8 @@ func declarations(transitions []delivery.Transition) ([]string, []string, []stri return resources, effects, verifiers, recoveries } -func contract(objective model.ObjectiveKind, conditions ...delivery.FacetCondition) delivery.ObjectiveContract { - return delivery.ObjectiveContract{ObjectiveKind: objective, Conditions: conditions} +func contract(objective model.TargetID, conditions ...delivery.FacetCondition) delivery.ObjectiveContract { + return delivery.ObjectiveContract{TargetID: objective, Conditions: conditions} } func known(facet model.FacetName, values ...string) delivery.FacetCondition { diff --git a/boatstack/flow/standard/standard_test.go b/boatstack/flow/standard/standard_test.go index c25e98f..09482b4 100644 --- a/boatstack/flow/standard/standard_test.go +++ b/boatstack/flow/standard/standard_test.go @@ -27,10 +27,10 @@ func TestManifestOwnsOnlyStandardDeliverySemantics(t *testing.T) { if len(manifest.ObjectiveContracts) != 5 { t.Fatalf("objective contracts = %d, want 5", len(manifest.ObjectiveContracts)) } - for _, objective := range []delivery.ObjectiveKind{delivery.ObjectiveApprovedPlan, delivery.ObjectiveVerified, delivery.ObjectiveOpenPR, delivery.ObjectiveMerged, delivery.ObjectiveAbandoned} { + for _, objective := range []delivery.TargetID{delivery.ObjectiveApprovedPlan, delivery.ObjectiveVerified, delivery.ObjectiveOpenPR, delivery.ObjectiveMerged, delivery.ObjectiveAbandoned} { found := false for _, contract := range manifest.ObjectiveContracts { - found = found || contract.ObjectiveKind == objective + found = found || contract.TargetID == objective } if !found { t.Errorf("missing objective contract %s", objective) diff --git a/boatstack/flow/standard/supervisor_parity_test.go b/boatstack/flow/standard/supervisor_parity_test.go index d1fa1fd..b0c4081 100644 --- a/boatstack/flow/standard/supervisor_parity_test.go +++ b/boatstack/flow/standard/supervisor_parity_test.go @@ -58,7 +58,7 @@ func snapshotFor(t *testing.T, phase model.ProtocolPhase, terminal model.Termina } func objectiveFor() model.Objective { - return model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} + return model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"} } func recanonicalize(t *testing.T, snapshot model.Snapshot) model.Snapshot { @@ -73,7 +73,7 @@ func recanonicalize(t *testing.T, snapshot model.Snapshot) model.Snapshot { func openPRSnapshot(t *testing.T, recordedGates ...string) (model.Snapshot, model.Objective) { t.Helper() snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) - objective := model.Objective{ID: "objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + objective := model.Objective{ID: "objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"} evidence := snapshot.Verification.Evidence[0] snapshot.Objective = model.Known(objective, evidence) snapshot.Plan = model.Known(model.PlanLocked, evidence) @@ -118,7 +118,7 @@ func TestTerminalEvidenceForOldObjectiveDoesNotTerminateNewObjective(t *testing. snapshot.Workspace = model.Known(model.WorkspacePublished, snapshot.Workspace.Evidence[0]) snapshot.Publication = model.Known(model.PublicationOpen, snapshot.Publication.Evidence[0]) snapshot = recanonicalize(t, snapshot) - newObjective := model.Objective{ID: "next-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "next-delivery"} + newObjective := model.Objective{ID: "next-objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "next-delivery"} decision := s.Resolve(snapshot, newObjective, catalog.AuthoritySet{catalog.AuthorityHuman: true}, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "objective.bind" { t.Fatalf("untargeted terminal replacement decision=%#v, want exact new-objective configuration", decision) @@ -130,7 +130,7 @@ func TestUntargetedResolutionReconfiguresDifferentObjectiveAndSkipsSatisfiedObje snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal) authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} - newObjective := model.Objective{ID: "new-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + newObjective := model.Objective{ID: "new-objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"} decision := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, newObjective, authority, "") if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "objective.bind" { t.Fatalf("different-objective decision = %#v, want objective.bind", decision) @@ -147,7 +147,7 @@ func TestUntargetedResolutionReconfiguresDifferentObjectiveAndSkipsSatisfiedObje func TestDormantBootstrapObjectiveReconfiguresBeforeEngagement(t *testing.T) { // control-law: a retained bootstrap objective cannot be bypassed by engagement snapshot := snapshotFor(t, model.PhaseDormant, model.TerminalNonterminal) - requested := model.Objective{ID: "basic-project", Kind: model.ObjectiveApprovedPlan, DeliveryID: "basic-project"} + requested := model.Objective{ID: "basic-project", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "basic-project"} authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true} untargeted := New(testprogram.StandardRegistry(), testObjectiveContracts()).Resolve(snapshot, requested, authority, "") @@ -178,7 +178,7 @@ func TestDisabledHostIsRefusedBeforeUntargetedSelection(t *testing.T) { func TestPublicationObservationRemainsSelectableForVolatileExternalState(t *testing.T) { // control-law: a nonterminal provider observation is evidence, not permanent progress snapshot, objective := openPRSnapshot(t, "build", "test", "review", "change", "journey") - objective.Kind = model.ObjectiveMerged + objective.TargetID = model.ObjectiveMerged snapshot.Objective = model.Known(objective, snapshot.Objective.Evidence[0]) snapshot.Publication = model.Known(model.PublicationOpen, snapshot.Publication.Evidence[0]) snapshot = recanonicalize(t, snapshot) diff --git a/boatstack/flow/standard/transitions.json b/boatstack/flow/standard/transitions.json index cf19b8d..f9eeed3 100644 --- a/boatstack/flow/standard/transitions.json +++ b/boatstack/flow/standard/transitions.json @@ -18,7 +18,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -262,7 +262,7 @@ "ACTIVE", "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -482,7 +482,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -708,7 +708,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "verified-implementation", "open-or-updated-pr", "merged-delivery" @@ -925,7 +925,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1169,7 +1169,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1408,7 +1408,7 @@ "target_phases": [ "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -1616,7 +1616,7 @@ "target_phases": [ "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "safely-abandoned" ], "required_identity": [ @@ -1803,7 +1803,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2072,7 +2072,7 @@ "ACTIVE", "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2295,7 +2295,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2516,7 +2516,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "open-or-updated-pr", "merged-delivery" ], @@ -2738,7 +2738,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -2960,7 +2960,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3175,7 +3175,7 @@ "target_phases": [ "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "safely-abandoned" ], "required_identity": [ @@ -3418,7 +3418,7 @@ "TERMINAL", "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3587,7 +3587,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -3823,7 +3823,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4059,7 +4059,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4303,7 +4303,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4538,7 +4538,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -4774,7 +4774,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -5008,7 +5008,7 @@ "target_phases": [ "FRONTIER" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -5207,7 +5207,7 @@ "ACTIVE", "TERMINAL" ], - "objective_kinds": [ + "target_ids": [ "approved-plan", "verified-implementation", "open-or-updated-pr", @@ -5446,7 +5446,7 @@ "target_phases": [ "ACTIVE" ], - "objective_kinds": [ + "target_ids": [ "open-or-updated-pr", "merged-delivery" ], @@ -5695,7 +5695,7 @@ "ACTIVE", "RECOVERY" ], - "objective_kinds": [ + "target_ids": [ "open-or-updated-pr", "merged-delivery" ], @@ -5955,7 +5955,7 @@ "FRONTIER", "UNRESOLVED" ], - "objective_kinds": [ + "target_ids": [ "open-or-updated-pr", "merged-delivery" ], @@ -6177,7 +6177,7 @@ "FRONTIER", "UNRESOLVED" ], - "objective_kinds": [ + "target_ids": [ "open-or-updated-pr", "merged-delivery" ], @@ -6382,7 +6382,7 @@ "ACTIVE", "RECOVERY" ], - "objective_kinds": [ + "target_ids": [ "open-or-updated-pr", "merged-delivery" ], @@ -6639,7 +6639,7 @@ "target_phases": [ "ABANDONED" ], - "objective_kinds": [ + "target_ids": [ "safely-abandoned" ], "required_identity": [ diff --git a/boatstack/internal/softwaredelivery/catalog/objective_contract.go b/boatstack/internal/softwaredelivery/catalog/objective_contract.go index 3fb3770..8cdbc90 100644 --- a/boatstack/internal/softwaredelivery/catalog/objective_contract.go +++ b/boatstack/internal/softwaredelivery/catalog/objective_contract.go @@ -11,11 +11,11 @@ import ( // Extension conditions are conjunctive and therefore can only narrow the // terminal set. type ObjectiveContract struct { - ObjectiveKind model.ObjectiveKind `json:"objective_kind"` - Conditions []FacetCondition `json:"conditions"` + TargetID model.TargetID `json:"target_id"` + Conditions []FacetCondition `json:"conditions"` } -type ObjectiveContracts map[model.ObjectiveKind]ObjectiveContract +type ObjectiveContracts map[model.TargetID]ObjectiveContract func (c ObjectiveContracts) Clone() ObjectiveContracts { result := make(ObjectiveContracts, len(c)) @@ -26,29 +26,29 @@ func (c ObjectiveContracts) Clone() ObjectiveContracts { return result } -func NewObjectiveContracts(base []ObjectiveContract, extension map[model.ObjectiveKind][]FacetCondition) (ObjectiveContracts, error) { +func NewObjectiveContracts(base []ObjectiveContract, extension map[model.TargetID][]FacetCondition) (ObjectiveContracts, error) { contracts := make(ObjectiveContracts, len(base)) for _, contract := range base { - if !contract.ObjectiveKind.Valid() || len(contract.Conditions) == 0 { + if !contract.TargetID.Valid() || len(contract.Conditions) == 0 { return nil, fmt.Errorf("objective contract requires a valid objective and conditions") } - if _, exists := contracts[contract.ObjectiveKind]; exists { - return nil, fmt.Errorf("duplicate objective contract %q", contract.ObjectiveKind) + if _, exists := contracts[contract.TargetID]; exists { + return nil, fmt.Errorf("duplicate objective contract %q", contract.TargetID) } conditions := append([]FacetCondition(nil), contract.Conditions...) - conditions = append(conditions, extension[contract.ObjectiveKind]...) + conditions = append(conditions, extension[contract.TargetID]...) for _, condition := range conditions { if !condition.Facet.Valid() || len(condition.Statuses) == 0 { - return nil, fmt.Errorf("objective %q has invalid terminal condition", contract.ObjectiveKind) + return nil, fmt.Errorf("objective %q has invalid terminal condition", contract.TargetID) } for _, status := range condition.Statuses { if !status.Valid() { - return nil, fmt.Errorf("objective %q has invalid terminal status %q", contract.ObjectiveKind, status) + return nil, fmt.Errorf("objective %q has invalid terminal status %q", contract.TargetID, status) } } } contract.Conditions = conditions - contracts[contract.ObjectiveKind] = contract + contracts[contract.TargetID] = contract } for objective := range extension { if _, exists := contracts[objective]; !exists { @@ -62,7 +62,7 @@ func (c ObjectiveContracts) Matches(snapshot model.Snapshot, objective model.Obj if snapshot.Objective.Status != model.FactKnown || snapshot.Objective.Value != objective { return false } - contract, ok := c[objective.Kind] + contract, ok := c[objective.TargetID] if !ok { return false } @@ -80,6 +80,6 @@ func (c ObjectiveContracts) All() []ObjectiveContract { contract.Conditions = append([]FacetCondition(nil), contract.Conditions...) result = append(result, contract) } - sort.Slice(result, func(i, j int) bool { return result[i].ObjectiveKind < result[j].ObjectiveKind }) + sort.Slice(result, func(i, j int) bool { return result[i].TargetID < result[j].TargetID }) return result } diff --git a/boatstack/internal/softwaredelivery/catalog/transition.go b/boatstack/internal/softwaredelivery/catalog/transition.go index aefb78c..b924e52 100644 --- a/boatstack/internal/softwaredelivery/catalog/transition.go +++ b/boatstack/internal/softwaredelivery/catalog/transition.go @@ -287,7 +287,7 @@ type Transition struct { Class EventClass `json:"class"` SourcePhases []model.ProtocolPhase `json:"source_phases"` TargetPhases []model.ProtocolPhase `json:"target_phases"` - ObjectiveKinds []model.ObjectiveKind `json:"objective_kinds,omitempty"` + TargetIDs []model.TargetID `json:"target_ids,omitempty"` RequiredIdentity []string `json:"required_identity"` Authority []AuthorityClass `json:"authority"` AuthorityAll []AuthorityClass `json:"authority_all,omitempty"` @@ -343,11 +343,11 @@ func (t Transition) SupportsObjective(objective model.Objective) bool { if t.Policy.ObjectiveScope == ObjectiveScopeOptionalPreserve { return true } - if len(t.ObjectiveKinds) == 0 { + if len(t.TargetIDs) == 0 { return true } - for _, kind := range t.ObjectiveKinds { - if kind == objective.Kind { + for _, kind := range t.TargetIDs { + if kind == objective.TargetID { return true } } @@ -478,8 +478,8 @@ func validateTransition(t Transition) error { return fmt.Errorf("%s: invalid phase %q", t.ID, phase) } } - objectiveKinds := map[model.ObjectiveKind]bool{} - for _, objective := range t.ObjectiveKinds { + objectiveKinds := map[model.TargetID]bool{} + for _, objective := range t.TargetIDs { if !objective.Valid() || objectiveKinds[objective] { return fmt.Errorf("%s: objective kinds must be valid and unique", t.ID) } @@ -676,7 +676,7 @@ func cloneTransitions(values []Transition) []Transition { func cloneTransition(value Transition) Transition { value.SourcePhases = append([]model.ProtocolPhase(nil), value.SourcePhases...) value.TargetPhases = append([]model.ProtocolPhase(nil), value.TargetPhases...) - value.ObjectiveKinds = append([]model.ObjectiveKind(nil), value.ObjectiveKinds...) + value.TargetIDs = append([]model.TargetID(nil), value.TargetIDs...) value.RequiredIdentity = append([]string(nil), value.RequiredIdentity...) value.Authority = append([]AuthorityClass(nil), value.Authority...) value.AuthorityAll = append([]AuthorityClass(nil), value.AuthorityAll...) diff --git a/boatstack/internal/softwaredelivery/durable/state_facet_test.go b/boatstack/internal/softwaredelivery/durable/state_facet_test.go index 75d707d..f93b89b 100644 --- a/boatstack/internal/softwaredelivery/durable/state_facet_test.go +++ b/boatstack/internal/softwaredelivery/durable/state_facet_test.go @@ -25,7 +25,7 @@ func TestChangedFacetsPreservesExactDomainValues(t *testing.T) { before := facetFixture() before.ProgramFingerprint = "program-a" before.RuntimeVersion = "runtime-a" - before.Objective = model.Objective{ID: "objective-a", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery-a"} + before.Objective = model.Objective{ID: "objective-a", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery-a"} after := before after.RuntimeVersion = "runtime-b" after.Revision++ diff --git a/boatstack/internal/softwaredelivery/effects/cas_integration_test.go b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go index f90f7b8..155854c 100644 --- a/boatstack/internal/softwaredelivery/effects/cas_integration_test.go +++ b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go @@ -58,7 +58,7 @@ func TestConcurrentApplyConsumesOneRevisionExactlyOnce(t *testing.T) { ID: "cas-human", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "cas-human-proof", IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour), }}} - objective := model.Objective{ID: "cas-objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "cas-delivery"} + objective := model.Objective{ID: "cas-objective", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "cas-delivery"} request := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "cas-concurrent", FlowID: "flow-cas", Objective: objective, TransitionID: "installation.initialize", Authority: human, @@ -190,7 +190,7 @@ func TestProgramChangeInvalidatesPriorPrescriptionBeforeEffects(t *testing.T) { now := time.Now().UTC() request := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-cas", - FlowID: "flow-program-cas", Objective: model.Objective{ID: "program-cas", Kind: model.ObjectiveApprovedPlan, DeliveryID: "program-cas"}, TransitionID: "installation.initialize", + FlowID: "flow-program-cas", Objective: model.Objective{ID: "program-cas", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "program-cas"}, TransitionID: "installation.initialize", Authority: protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ID: "program-cas-human", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "human", IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)}}}, Parameters: protocol.Parameters{ {Name: "source_revision", Value: "program-cas"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, diff --git a/boatstack/internal/softwaredelivery/effects/integration_test.go b/boatstack/internal/softwaredelivery/effects/integration_test.go index 1c677fa..904b0e0 100644 --- a/boatstack/internal/softwaredelivery/effects/integration_test.go +++ b/boatstack/internal/softwaredelivery/effects/integration_test.go @@ -164,7 +164,7 @@ func TestConcreteBoundaryAppliesAndReceiptsOneTransition(t *testing.T) { if err != nil { t.Fatal(err) } - objective := model.Objective{ID: "objective-1", Kind: model.ObjectiveVerified, DeliveryID: "delivery-1"} + objective := model.Objective{ID: "objective-1", TargetID: model.ObjectiveVerified, DeliveryID: "delivery-1"} authority := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "authority-1", Class: catalog.AuthorityHuman, Subject: invocation.RepositoryID, Fingerprint: "human-fingerprint", IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), @@ -236,7 +236,7 @@ func TestDeclaredStateEffectAppliesAndReceiptsWithoutTransitionDispatch(t *testi if err != nil { t.Fatal(err) } - objective := model.Objective{ID: "declared-state-objective", Kind: model.ObjectiveVerified, DeliveryID: "declared-state-delivery"} + objective := model.Objective{ID: "declared-state-objective", TargetID: model.ObjectiveVerified, DeliveryID: "declared-state-delivery"} human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "declared-state-human", Class: catalog.AuthorityHuman, Subject: invocation.RepositoryID, Fingerprint: "explicit-human", IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), @@ -276,7 +276,7 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing if err != nil { t.Fatal(err) } - objective := model.Objective{ID: "external-config-objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "external-config"} + objective := model.Objective{ID: "external-config-objective", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "external-config"} now := time.Now().UTC() human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "external-config-human", Class: catalog.AuthorityHuman, Subject: "integration", Fingerprint: "explicit-human", @@ -309,7 +309,7 @@ func TestExternalConfigurationAuthorityTransfersAcrossAttachAndDetach(t *testing {Name: "source_revision", Value: "external-config-fixture"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: initialPath}, {Name: "config_sha256", Value: configFingerprint(t, initialConfig)}, }) - apply("objective.bind", human, false, protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) + apply("objective.bind", human, false, protocol.Parameters{{Name: "target_id", Value: string(objective.TargetID)}, {Name: "delivery_id", Value: objective.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 { @@ -370,7 +370,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { if err != nil { t.Fatal(err) } - objective := model.Objective{ID: "program-drift", Kind: model.ObjectiveApprovedPlan, DeliveryID: "program-drift"} + objective := model.Objective{ID: "program-drift", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "program-drift"} now := time.Now().UTC() human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "program-drift-human", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "explicit-program-reconciliation", @@ -505,7 +505,7 @@ func TestProgramDriftRequiresAtomicInstallationReconciliation(t *testing.T) { } updateRequest := surfaces.Request{ SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationApply, Repository: repository, Host: "cli", CorrelationID: "program-current-update", - FlowID: "flow-program-drift", Objective: model.Objective{ID: "ignored-command-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "ignored"}, + FlowID: "flow-program-drift", Objective: model.Objective{ID: "ignored-command-objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "ignored"}, TransitionID: "installation.update", Authority: human, Parameters: protocol.Parameters{ {Name: "source_revision", Value: "program-current"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, @@ -540,7 +540,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test if err != nil { t.Fatal(err) } - objective := model.Objective{ID: "extension-receipt", Kind: model.ObjectiveOpenPR, DeliveryID: "extension-receipt"} + objective := model.Objective{ID: "extension-receipt", TargetID: model.ObjectiveOpenPR, DeliveryID: "extension-receipt"} now := time.Now().UTC() authority := func(class catalog.AuthorityClass) protocol.AuthorityBundle { fingerprint, subject := "explicit-human", "integration" @@ -587,7 +587,7 @@ func TestReferenceExtensionUsesKernelAdmissionVerificationAndReceiptPath(t *test if initialized.Receipt == nil || initialized.Receipt.AuthorityFingerprint == "" || len(initialized.Receipt.AuthoritySources) != 1 || len(initialized.Receipt.RequiredCapabilities) == 0 || len(initialized.Receipt.GrantedCapabilities) == 0 || len(initialized.Receipt.ExercisedCapabilities) != 0 || len(initialized.Receipt.CommittedEffects) == 0 || initialized.Receipt.Verification.Result != protocol.VerificationSatisfied { t.Fatalf("receipt lost capability or authority provenance: %#v", initialized.Receipt) } - apply("objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) + apply("objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "target_id", Value: string(objective.TargetID)}, {Name: "delivery_id", Value: objective.DeliveryID}}) apply("engagement.begin", authority(catalog.AuthorityRepository), nil) planPath := filepath.Join(t.TempDir(), "plan.md") planRaw := []byte("# Extension plan\n") @@ -712,12 +712,12 @@ func TestConcreteWorkflowPreservesConfigurationProofAndObjectiveTerminals(t *tes if err := os.WriteFile(configPath, configRaw, 0o600); err != nil { t.Fatal(err) } - approvedObjective := model.Objective{ID: "objective-approved", Kind: model.ObjectiveApprovedPlan, DeliveryID: "delivery-workflow"} + approvedObjective := model.Objective{ID: "objective-approved", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "delivery-workflow"} apply(approvedObjective, "installation.initialize", authority(catalog.AuthorityHuman), protocol.Parameters{ {Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configPath}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, }) - apply(approvedObjective, "objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "objective_kind", Value: string(approvedObjective.Kind)}, {Name: "delivery_id", Value: approvedObjective.DeliveryID}}) + apply(approvedObjective, "objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "target_id", Value: string(approvedObjective.TargetID)}, {Name: "delivery_id", Value: approvedObjective.DeliveryID}}) apply(approvedObjective, "engagement.begin", authority(catalog.AuthorityRepository), nil) updatedConfigPath := filepath.Join(t.TempDir(), "project-v2-updated.json") @@ -744,8 +744,8 @@ func TestConcreteWorkflowPreservesConfigurationProofAndObjectiveTerminals(t *tes t.Fatalf("approved-plan terminal not established: %#v", approved.Target) } - verifiedObjective := model.Objective{ID: "objective-verified", Kind: model.ObjectiveVerified, DeliveryID: approvedObjective.DeliveryID} - apply(verifiedObjective, "objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "objective_kind", Value: string(verifiedObjective.Kind)}, {Name: "delivery_id", Value: verifiedObjective.DeliveryID}}) + verifiedObjective := model.Objective{ID: "objective-verified", TargetID: model.ObjectiveVerified, DeliveryID: approvedObjective.DeliveryID} + apply(verifiedObjective, "objective.bind", authority(catalog.AuthorityHuman), protocol.Parameters{{Name: "target_id", Value: string(verifiedObjective.TargetID)}, {Name: "delivery_id", Value: verifiedObjective.DeliveryID}}) apply(verifiedObjective, "plan.activate", authority(catalog.AuthorityHuman), nil) head := strings.TrimSpace(commandOutput(t, repository, "git", "rev-parse", "HEAD")) gateParameters := func(name string) protocol.Parameters { @@ -816,7 +816,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) if err != nil { t.Fatal(err) } - objective := model.Objective{ID: "objective-workspace", Kind: model.ObjectiveMerged, DeliveryID: "delivery-workspace"} + objective := model.Objective{ID: "objective-workspace", TargetID: model.ObjectiveMerged, DeliveryID: "delivery-workspace"} human := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "human-workspace", Class: catalog.AuthorityHuman, Subject: "operator", Fingerprint: "human-workspace-proof", IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), @@ -850,7 +850,7 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) {Name: "source_revision", Value: "integration-revision"}, {Name: "runtime_version", Value: runtimeVersion}, {Name: "runtime_sha256", Value: digestBytes(runtimeRaw)}, {Name: "config_path", Value: configSource}, {Name: "config_sha256", Value: configFingerprint(t, configRaw)}, }) - apply(sourceInvocation, "objective.bind", human, protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) + apply(sourceInvocation, "objective.bind", human, protocol.Parameters{{Name: "target_id", Value: string(objective.TargetID)}, {Name: "delivery_id", Value: objective.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 { @@ -917,8 +917,8 @@ func TestWorkspaceCutTransfersAuthorityToExactDestinationWorktree(t *testing.T) if err := os.WriteFile(destinationConfigPath, destinationConfig, 0o600); err != nil { t.Fatal(err) } - objective = model.Objective{ID: "objective-workspace-abandon", Kind: model.ObjectiveAbandoned, DeliveryID: "delivery-workspace"} - apply(destinationInvocation, "objective.bind", human, protocol.Parameters{{Name: "objective_kind", Value: string(objective.Kind)}, {Name: "delivery_id", Value: objective.DeliveryID}}) + objective = model.Objective{ID: "objective-workspace-abandon", TargetID: model.ObjectiveAbandoned, DeliveryID: "delivery-workspace"} + apply(destinationInvocation, "objective.bind", human, protocol.Parameters{{Name: "target_id", Value: string(objective.TargetID)}, {Name: "delivery_id", Value: objective.DeliveryID}}) abandoned := apply(destinationInvocation, "workspace.abandon", human, protocol.Parameters{{Name: "branch", Value: "feature/v2-workspace-transfer"}}) if abandoned.Target.Terminal.Value != model.TerminalEstablished || abandoned.Target.Workspace.Value != model.WorkspaceAbandoned { t.Fatalf("workspace abandonment did not establish its configured terminal: %#v", abandoned.Target) diff --git a/boatstack/internal/softwaredelivery/effects/journal.go b/boatstack/internal/softwaredelivery/effects/journal.go index 94dc0bd..0eacef7 100644 --- a/boatstack/internal/softwaredelivery/effects/journal.go +++ b/boatstack/internal/softwaredelivery/effects/journal.go @@ -140,7 +140,7 @@ func readJournal(path string) (journalRecord, error) { if receipt.PrescriptionID != admission.PrescriptionID || receipt.TransitionVersion != admission.TransitionVersion || receipt.Program.Fingerprint != admission.ExpectedProgramFingerprint || receipt.PriorStateRevision != admission.ExpectedStateRevision || receipt.SourceFingerprint != admission.ExpectedSnapshotFingerprint || receipt.AuthorityFingerprint != admission.AuthorityFingerprint || !slices.Equal(receipt.RequiredCapabilities, admission.RequiredCapabilities) || - !slices.Equal(receipt.GrantedCapabilities, admission.GrantedCapabilities) || receipt.ObjectiveID != admission.Objective.ID || receipt.ObjectiveKind != admission.Objective.Kind || + !slices.Equal(receipt.GrantedCapabilities, admission.GrantedCapabilities) || receipt.ObjectiveID != admission.Objective.ID || receipt.TargetID != admission.Objective.TargetID || receipt.TrustedClass != admission.Objective.TrustedClass || receipt.DeliveryID != admission.Objective.DeliveryID || receipt.ObjectiveScope != admission.ObjectiveScope || receipt.ObjectiveStatus != admission.ObjectiveStatus { return journalRecord{}, fmt.Errorf("committed transition fact in %s does not match its exact admission", path) } diff --git a/boatstack/internal/softwaredelivery/effects/receipts.go b/boatstack/internal/softwaredelivery/effects/receipts.go index 8468f73..2c05756 100644 --- a/boatstack/internal/softwaredelivery/effects/receipts.go +++ b/boatstack/internal/softwaredelivery/effects/receipts.go @@ -124,7 +124,7 @@ func sameStateLineage(left, right model.InvocationContext) bool { func matchesObjectiveBinding(receipt protocol.TransitionReceipt, objective model.Objective, maximumRevision uint64) bool { return receipt.TransitionID == "objective.bind" && strings.HasPrefix(receipt.FlowID, "run-") && - receipt.ObjectiveID == objective.ID && receipt.ObjectiveKind == objective.Kind && receipt.DeliveryID == objective.DeliveryID && + receipt.ObjectiveID == objective.ID && receipt.TargetID == objective.TargetID && receipt.TrustedClass == objective.TrustedClass && receipt.DeliveryID == objective.DeliveryID && receipt.ResultingStateRevision <= maximumRevision } diff --git a/boatstack/internal/softwaredelivery/effects/receipts_test.go b/boatstack/internal/softwaredelivery/effects/receipts_test.go index 4cc4dd0..e55e4c9 100644 --- a/boatstack/internal/softwaredelivery/effects/receipts_test.go +++ b/boatstack/internal/softwaredelivery/effects/receipts_test.go @@ -8,9 +8,9 @@ import ( ) func TestActiveFlowIdentityComesFromObjectiveBindingReceipt(t *testing.T) { - objective := model.Objective{ID: "objective-product-delivery-run-one", Kind: model.ObjectiveOpenPR, DeliveryID: "one"} - binding := protocol.TransitionReceipt{ID: "binding", FlowID: "run-original", TransitionID: "objective.bind", ObjectiveID: objective.ID, ObjectiveKind: objective.Kind, DeliveryID: objective.DeliveryID, ResultingStateRevision: 4} - maintenance := protocol.TransitionReceipt{ID: "maintenance", FlowID: "run-maintenance", TransitionID: "installation.reconcile-update", ObjectiveID: objective.ID, ObjectiveKind: objective.Kind, DeliveryID: objective.DeliveryID, ResultingStateRevision: 5} + objective := model.Objective{ID: "objective-product-delivery-run-one", TargetID: "published-pr", TrustedClass: model.ObjectiveOpenPR, DeliveryID: "one"} + binding := protocol.TransitionReceipt{ID: "binding", FlowID: "run-original", TransitionID: "objective.bind", ObjectiveID: objective.ID, TargetID: objective.TargetID, TrustedClass: objective.TrustedClass, DeliveryID: objective.DeliveryID, ResultingStateRevision: 4} + maintenance := protocol.TransitionReceipt{ID: "maintenance", FlowID: "run-maintenance", TransitionID: "installation.reconcile-update", ObjectiveID: objective.ID, TargetID: objective.TargetID, TrustedClass: objective.TrustedClass, DeliveryID: objective.DeliveryID, ResultingStateRevision: 5} if !matchesObjectiveBinding(binding, objective, 5) { t.Fatal("objective binding receipt was not recognized") } @@ -20,6 +20,11 @@ func TestActiveFlowIdentityComesFromObjectiveBindingReceipt(t *testing.T) { if matchesObjectiveBinding(binding, objective, 3) { t.Fatal("future objective binding receipt was accepted") } + wrongClass := binding + wrongClass.TrustedClass = model.ObjectiveVerified + if matchesObjectiveBinding(wrongClass, objective, 5) { + t.Fatal("receipt from another trusted target class was accepted") + } } func TestActiveFlowIdentityRequiresExactWorktreeLineage(t *testing.T) { diff --git a/boatstack/internal/softwaredelivery/effects/recovery_test.go b/boatstack/internal/softwaredelivery/effects/recovery_test.go index 6785292..d62dbda 100644 --- a/boatstack/internal/softwaredelivery/effects/recovery_test.go +++ b/boatstack/internal/softwaredelivery/effects/recovery_test.go @@ -91,7 +91,7 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing. if err != nil { t.Fatal(err) } - objective := model.Objective{ID: "objective-recovery", Kind: model.ObjectiveVerified, DeliveryID: "delivery-recovery"} + objective := model.Objective{ID: "objective-recovery", TargetID: model.ObjectiveVerified, DeliveryID: "delivery-recovery"} authority := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ ID: "recovery-human", Class: catalog.AuthorityHuman, Subject: "fixture", Fingerprint: "recovery-human-fingerprint", IssuedAt: clock.Now().Add(-time.Minute), ExpiresAt: clock.Now().Add(time.Hour), diff --git a/boatstack/internal/softwaredelivery/effects/state_facet_test.go b/boatstack/internal/softwaredelivery/effects/state_facet_test.go index 00208bf..a93a50c 100644 --- a/boatstack/internal/softwaredelivery/effects/state_facet_test.go +++ b/boatstack/internal/softwaredelivery/effects/state_facet_test.go @@ -52,7 +52,7 @@ func requireOwnedChange(t *testing.T, transition catalog.Transition, before, aft func TestStateFacetIsolationMatrix(t *testing.T) { base := ownershipState() - knownObjective := model.Objective{ID: "objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} + knownObjective := model.Objective{ID: "objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"} fixtures := []struct { name string transition catalog.Transition @@ -84,7 +84,7 @@ func TestMaintenancePreservesAbsentAndKnownObjectiveExactly(t *testing.T) { transition := transitionFixture("installation.update", catalog.OriginCoreSystem, false) transition.Policy.ObjectiveScope = catalog.ObjectiveScopeOptionalPreserve transition.TargetPhases = []model.ProtocolPhase{model.PhaseDormant} - for _, objective := range []model.Objective{{}, {ID: "known", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"}} { + for _, objective := range []model.Objective{{}, {ID: "known", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"}} { state := ownershipState() state.Objective = objective admission := protocol.Admission{ObjectiveStatus: model.FactAbsent} @@ -103,7 +103,7 @@ func TestMaintenancePreservesAbsentAndKnownObjectiveExactly(t *testing.T) { func TestRecoveryCannotReplayFacetOutsideInterruptedTransition(t *testing.T) { before := ownershipState() after := before - after.Objective = model.Objective{ID: "invented", Kind: model.ObjectiveApprovedPlan, DeliveryID: "invented"} + after.Objective = model.Objective{ID: "invented", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "invented"} prior, _ := durable.EncodeState(before) target, _ := durable.EncodeState(after) record := journalRecord{TransitionID: "installation.update", AllowedStateFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetInstallation}, Mutations: []ports.ResourceMutation{{Path: "/controller/state.json", PriorExists: true, Prior: prior, Target: target, StateFacets: []model.StateFacet{model.StateFacetControl, model.StateFacetProduct}}}} diff --git a/boatstack/internal/softwaredelivery/effects/state_reducer.go b/boatstack/internal/softwaredelivery/effects/state_reducer.go index 0e7dae6..f1a82ad 100644 --- a/boatstack/internal/softwaredelivery/effects/state_reducer.go +++ b/boatstack/internal/softwaredelivery/effects/state_reducer.go @@ -345,9 +345,9 @@ func applyCatalogReconcile(state *durable.State, admission protocol.Admission, _ func applyObjectiveBind(state *durable.State, admission protocol.Admission, _ catalog.Transition) error { wasActive := state.Phase == model.PhaseActive - kind, _ := admission.Parameters.Get("objective_kind") + kind, _ := admission.Parameters.Get("target_id") delivery, _ := admission.Parameters.Get("delivery_id") - if kind != string(admission.Objective.Kind) || delivery != admission.Objective.DeliveryID { + if kind != string(admission.Objective.TargetID) || delivery != admission.Objective.DeliveryID { return fmt.Errorf("objective parameters do not match admitted objective") } if state.Objective.DeliveryID != "" && state.Objective.DeliveryID != admission.Objective.DeliveryID { @@ -380,7 +380,7 @@ func resetDeliveryState(state *durable.State) { func applyPlanApprove(state *durable.State, admission protocol.Admission, _ catalog.Transition) error { state.Plan, state.Delivery, state.Phase = model.PlanApproved, model.DeliveryApproved, model.PhaseActive - if admission.Objective.Kind == model.ObjectiveApprovedPlan { + if admission.Objective.TrustedObjectiveClass() == model.ObjectiveApprovedPlan { establishTerminal(state, model.PhaseTerminal) } return nil @@ -440,7 +440,7 @@ func applyVisualEvidence(state *durable.State, admission protocol.Admission, _ c upsertGate(state, durable.GateEvidence{Gate: "visual", Revision: revision, Fingerprint: fingerprint}) state.SourceRevision, state.WorktreeFingerprint = admission.SourceRevision, admission.WorktreeFingerprint state.Terminal = model.TerminalNonterminal - if admission.Objective.Kind == model.ObjectiveVerified { + if admission.Objective.TrustedObjectiveClass() == model.ObjectiveVerified { state.Delivery = model.DeliveryActive } state.Verification, state.Phase = model.VerificationCurrent, model.PhaseActive @@ -458,9 +458,9 @@ func applyPublicationObservation(state *durable.State, admission protocol.Admiss state.Phase = model.PhaseUnresolved } else if state.Publication == model.PublicationClosedUnmerged { state.Phase = model.PhaseFrontier - } else if admission.Objective.Kind == model.ObjectiveOpenPR && state.Publication == model.PublicationOpen { + } else if admission.Objective.TrustedObjectiveClass() == model.ObjectiveOpenPR && state.Publication == model.PublicationOpen { establishTerminal(state, model.PhaseTerminal) - } else if admission.Objective.Kind == model.ObjectiveMerged && state.Publication == model.PublicationMerged { + } else if admission.Objective.TrustedObjectiveClass() == model.ObjectiveMerged && state.Publication == model.PublicationMerged { state.Workspace, state.Delivery = model.WorkspaceLanded, model.DeliveryTerminal establishTerminal(state, model.PhaseTerminal) } else { @@ -535,7 +535,7 @@ func hasGates(state durable.State, names ...string) bool { } func verifiedObjectiveSatisfied(state durable.State, objective model.Objective) bool { - if objective.Kind != model.ObjectiveVerified || !hasGates(state, "build", "test", "review") { + if objective.TrustedObjectiveClass() != model.ObjectiveVerified || !hasGates(state, "build", "test", "review") { return false } return state.VisualEvidencePolicy != "required" || hasGates(state, "visual") diff --git a/boatstack/internal/softwaredelivery/effects/state_reducer_test.go b/boatstack/internal/softwaredelivery/effects/state_reducer_test.go index 991974a..dc1f88c 100644 --- a/boatstack/internal/softwaredelivery/effects/state_reducer_test.go +++ b/boatstack/internal/softwaredelivery/effects/state_reducer_test.go @@ -14,7 +14,7 @@ import ( func TestRequiredVisualEvidenceParticipatesInVerifiedTerminal(t *testing.T) { // control-law: repository-visual-policy-is-terminal-authority-not-decoration - objective := model.Objective{ID: "visual-objective", Kind: model.ObjectiveVerified, DeliveryID: "visual-delivery"} + objective := model.Objective{ID: "visual-objective", TargetID: model.ObjectiveVerified, DeliveryID: "visual-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseActive, Engagement: model.EngagementActive, Delivery: model.DeliveryActive, Workspace: model.WorkspaceActive, @@ -51,7 +51,7 @@ func TestRequiredVisualEvidenceParticipatesInVerifiedTerminal(t *testing.T) { func TestPublicationCorrectionRequiresIndependentObservationForTerminal(t *testing.T) { // control-law: external-writer-cannot-self-certify-provider-state - objective := model.Objective{ID: "publication-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "publication-delivery"} + objective := model.Objective{ID: "publication-objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "publication-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseTerminal, Engagement: model.EngagementActive, Delivery: model.DeliveryTerminal, Workspace: model.WorkspacePublished, @@ -79,14 +79,14 @@ func TestPublicationCorrectionRequiresIndependentObservationForTerminal(t *testi func TestWorkspaceReapPreservesEstablishedTerminalPhase(t *testing.T) { for _, fixture := range []struct { - objectiveKind model.ObjectiveKind + objectiveKind model.TargetID delivery model.DeliveryState phase model.ProtocolPhase }{ {objectiveKind: model.ObjectiveMerged, delivery: model.DeliveryTerminal, phase: model.PhaseTerminal}, {objectiveKind: model.ObjectiveAbandoned, delivery: model.DeliveryDiscarded, phase: model.PhaseAbandoned}, } { - objective := model.Objective{ID: "cleanup-objective", Kind: fixture.objectiveKind, DeliveryID: "cleanup-delivery"} + objective := model.Objective{ID: "cleanup-objective", TargetID: fixture.objectiveKind, DeliveryID: "cleanup-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: fixture.phase, Engagement: model.EngagementActive, Delivery: fixture.delivery, Workspace: model.WorkspaceAbandoned, @@ -104,8 +104,8 @@ func TestWorkspaceReapPreservesEstablishedTerminalPhase(t *testing.T) { } func TestEscalatedRecoveryCanOnlyBeReconfiguredTowardExplicitAbandonment(t *testing.T) { - original := model.Objective{ID: "delivery-objective", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} - abandoned := model.Objective{ID: "abandon-objective", Kind: model.ObjectiveAbandoned, DeliveryID: "delivery"} + original := model.Objective{ID: "delivery-objective", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"} + abandoned := model.Objective{ID: "abandon-objective", TargetID: model.ObjectiveAbandoned, DeliveryID: "delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseFrontier, Engagement: model.EngagementActive, Delivery: model.DeliveryPublished, Workspace: model.WorkspacePublished, @@ -116,7 +116,7 @@ func TestEscalatedRecoveryCanOnlyBeReconfiguredTowardExplicitAbandonment(t *test } configure, _ := testprogram.StandardRegistry().Lookup("objective.bind") configureAdmission := protocol.Admission{Objective: abandoned, Parameters: protocol.Parameters{ - {Name: "objective_kind", Value: string(abandoned.Kind)}, {Name: "delivery_id", Value: abandoned.DeliveryID}, + {Name: "target_id", Value: string(abandoned.TargetID)}, {Name: "delivery_id", Value: abandoned.DeliveryID}, }} if err := applyStateTransition(&state, configureAdmission, configure); err != nil { t.Fatal(err) @@ -134,8 +134,8 @@ func TestEscalatedRecoveryCanOnlyBeReconfiguredTowardExplicitAbandonment(t *test } func TestObjectiveBindStartsDifferentDeliveryFromCleanProductState(t *testing.T) { - prior := model.Objective{ID: "prior", Kind: model.ObjectiveAbandoned, DeliveryID: "prior-delivery"} - next := model.Objective{ID: "next", Kind: model.ObjectiveOpenPR, DeliveryID: "next-delivery"} + prior := model.Objective{ID: "prior", TargetID: model.ObjectiveAbandoned, DeliveryID: "prior-delivery"} + next := model.Objective{ID: "next", TargetID: model.ObjectiveOpenPR, DeliveryID: "next-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseTerminal, Engagement: model.EngagementCommand, Delivery: model.DeliveryTerminal, Workspace: model.WorkspacePublished, @@ -146,7 +146,7 @@ func TestObjectiveBindStartsDifferentDeliveryFromCleanProductState(t *testing.T) PreviewFingerprint: "old-preview", Gates: []durable.GateEvidence{{Gate: "test", Revision: "old", Fingerprint: "old-test"}}, } transition, _ := testprogram.StandardRegistry().Lookup("objective.bind") - admission := protocol.Admission{Objective: next, Parameters: protocol.Parameters{{Name: "objective_kind", Value: string(next.Kind)}, {Name: "delivery_id", Value: next.DeliveryID}}} + admission := protocol.Admission{Objective: next, Parameters: protocol.Parameters{{Name: "target_id", Value: string(next.TargetID)}, {Name: "delivery_id", Value: next.DeliveryID}}} if err := applyStateTransition(&state, admission, transition); err != nil { t.Fatal(err) } @@ -156,8 +156,8 @@ func TestObjectiveBindStartsDifferentDeliveryFromCleanProductState(t *testing.T) } func TestObjectiveBindRejectsDifferentDeliveryBeforeSafeTerminal(t *testing.T) { - prior := model.Objective{ID: "prior", Kind: model.ObjectiveOpenPR, DeliveryID: "prior-delivery"} - next := model.Objective{ID: "next", Kind: model.ObjectiveOpenPR, DeliveryID: "next-delivery"} + prior := model.Objective{ID: "prior", TargetID: model.ObjectiveOpenPR, DeliveryID: "prior-delivery"} + next := model.Objective{ID: "next", TargetID: model.ObjectiveOpenPR, DeliveryID: "next-delivery"} state := durable.State{ SchemaVersion: durable.StateSchemaVersion, RepositoryID: "repo", GitCommonID: "git", WorktreeID: "worktree", Revision: 1, Phase: model.PhaseActive, Engagement: model.EngagementActive, Delivery: model.DeliveryActive, Workspace: model.WorkspaceActive, @@ -166,7 +166,7 @@ func TestObjectiveBindRejectsDifferentDeliveryBeforeSafeTerminal(t *testing.T) { Objective: prior, } transition, _ := testprogram.StandardRegistry().Lookup("objective.bind") - admission := protocol.Admission{Objective: next, Parameters: protocol.Parameters{{Name: "objective_kind", Value: string(next.Kind)}, {Name: "delivery_id", Value: next.DeliveryID}}} + admission := protocol.Admission{Objective: next, Parameters: protocol.Parameters{{Name: "target_id", Value: string(next.TargetID)}, {Name: "delivery_id", Value: next.DeliveryID}}} if err := applyStateTransition(&state, admission, transition); err == nil || !strings.Contains(err.Error(), "prior delivery") { t.Fatalf("different active delivery bind result = %v", err) } diff --git a/boatstack/internal/softwaredelivery/engine/engine.go b/boatstack/internal/softwaredelivery/engine/engine.go index fa90a91..cb1da80 100644 --- a/boatstack/internal/softwaredelivery/engine/engine.go +++ b/boatstack/internal/softwaredelivery/engine/engine.go @@ -513,7 +513,7 @@ func validateReplayRequest(prior protocol.TransitionReceipt, request ApplyReques return fmt.Errorf("idempotency receipt belongs to a different prescription") } if prior.ObjectiveScope != catalog.ObjectiveScopeOptionalPreserve && request.Objective.Validate() == nil { - if prior.ObjectiveID != request.Objective.ID || prior.ObjectiveKind != request.Objective.Kind || prior.DeliveryID != request.Objective.DeliveryID { + if prior.ObjectiveID != request.Objective.ID || prior.TargetID != request.Objective.TargetID || prior.TrustedClass != request.Objective.TrustedClass || prior.DeliveryID != request.Objective.DeliveryID { return fmt.Errorf("idempotency receipt belongs to a different configured objective") } } @@ -530,7 +530,7 @@ func validateReplayObjectiveState(prior protocol.TransitionReceipt, snapshot mod switch prior.ObjectiveStatus { case model.FactKnown: if snapshot.Objective.Status != model.FactKnown || snapshot.Objective.Value.ID != prior.ObjectiveID || - snapshot.Objective.Value.Kind != prior.ObjectiveKind || snapshot.Objective.Value.DeliveryID != prior.DeliveryID { + snapshot.Objective.Value.TargetID != prior.TargetID || snapshot.Objective.Value.TrustedClass != prior.TrustedClass || snapshot.Objective.Value.DeliveryID != prior.DeliveryID { return fmt.Errorf("idempotency receipt objective binding no longer matches current state") } case model.FactAbsent: diff --git a/boatstack/internal/softwaredelivery/engine/engine_test.go b/boatstack/internal/softwaredelivery/engine/engine_test.go index d11370c..6c3ca1d 100644 --- a/boatstack/internal/softwaredelivery/engine/engine_test.go +++ b/boatstack/internal/softwaredelivery/engine/engine_test.go @@ -26,8 +26,8 @@ var syntheticProgram = protocol.ProgramIdentity{ID: "test.synthetic", Version: " func syntheticObjectiveContracts(t *testing.T) catalog.ObjectiveContracts { t.Helper() contracts, err := catalog.NewObjectiveContracts([]catalog.ObjectiveContract{{ - ObjectiveKind: model.ObjectiveVerified, - Conditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}}, + TargetID: model.ObjectiveVerified, + Conditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}}, }}, nil) if err != nil { t.Fatal(err) @@ -192,7 +192,7 @@ func observation(phase model.ProtocolPhase, fingerprint string) model.Observatio ConfigurationPolicy: model.Known(model.ConfigurationPolicy{PlanApproval: "human", VisualEvidence: "optional", ExternalEffectAuthority: "human-or-autonomy-plus-provider", Hosts: []string{"cli"}}, configurationEvidence), Publication: model.Known(model.PublicationNone, e), Verification: model.Known(model.VerificationUnverified, e), Recovery: model.Known(model.RecoveryNone, e), Transaction: model.Known(model.TransactionNone, e), RecoveryInfo: model.Absent[model.RecoveryContext]("none", e), TransactionInfo: model.Absent[model.TransactionContext]("none", e), - Terminal: model.Known(model.TerminalNonterminal, e), Objective: model.Known(model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"}, e), ObservedAt: time.Unix(20, 0).UTC(), + Terminal: model.Known(model.TerminalNonterminal, e), Objective: model.Known(model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"}, e), ObservedAt: time.Unix(20, 0).UTC(), ProgramFacts: map[string]model.Fact[string]{"test.synthetic.stage": model.Known(stage, e)}, } } @@ -282,7 +282,7 @@ func request(t *testing.T, now time.Time) ApplyRequest { t.Fatal(err) } return ApplyRequest{ResolveRequest: ResolveRequest{ - Invocation: invocation, Objective: model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"}, Requested: "test.advance", + Invocation: invocation, Objective: model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"}, Requested: "test.advance", Authority: authorityBundle, }, FlowID: "flow", Prescription: prescription, AdmissionLifetime: time.Minute} } @@ -439,7 +439,7 @@ func TestProjectionFailureCannotUndoCanonicalTransitionFact(t *testing.T) { func TestSyntheticStartVerifyTerminalContractNeedsNoStandardFlowFacet(t *testing.T) { // control-law: kernel-terminal-is-defined-only-by-the-compiled-control-program-contract - objective := model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} + objective := model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"} source, err := model.CanonicalizeForProgram(observation(model.PhaseObserved, "source"), syntheticProgramFingerprint) if err != nil { t.Fatal(err) diff --git a/boatstack/internal/softwaredelivery/engine/maintenance_replay_test.go b/boatstack/internal/softwaredelivery/engine/maintenance_replay_test.go index a2fa41d..6d5e9e3 100644 --- a/boatstack/internal/softwaredelivery/engine/maintenance_replay_test.go +++ b/boatstack/internal/softwaredelivery/engine/maintenance_replay_test.go @@ -10,8 +10,8 @@ import ( func TestMaintenanceReplayBindsDurableObjectiveState(t *testing.T) { // control-law: maintenance-replay-preserves-verified-objective-state - configured := model.Objective{ID: "configured", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} - commandObjective := model.Objective{ID: "command", Kind: model.ObjectiveApprovedPlan, DeliveryID: "other"} + configured := model.Objective{ID: "configured", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"} + commandObjective := model.Objective{ID: "command", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "other"} request := ApplyRequest{ResolveRequest: ResolveRequest{Objective: commandObjective}, FlowID: "flow"} tests := []struct { @@ -21,9 +21,9 @@ func TestMaintenanceReplayBindsDurableObjectiveState(t *testing.T) { wantErr bool }{ {name: "absent survives command objective and retry", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactAbsent}, fact: model.Fact[model.Objective]{Status: model.FactAbsent}}, - {name: "known survives conflicting command objective and retry", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactKnown, ObjectiveID: configured.ID, ObjectiveKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: configured}}, + {name: "known survives conflicting command objective and retry", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactKnown, ObjectiveID: configured.ID, TargetID: configured.TargetID, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: configured}}, {name: "absent cannot replay after product objective appears", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactAbsent}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: configured}, wantErr: true}, - {name: "known cannot replay after product objective changes", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactKnown, ObjectiveID: configured.ID, ObjectiveKind: configured.Kind, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: commandObjective}, wantErr: true}, + {name: "known cannot replay after product objective changes", receipt: protocol.TransitionReceipt{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve, ObjectiveStatus: model.FactKnown, ObjectiveID: configured.ID, TargetID: configured.TargetID, DeliveryID: configured.DeliveryID}, fact: model.Fact[model.Objective]{Status: model.FactKnown, Value: commandObjective}, wantErr: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/boatstack/internal/softwaredelivery/model/facet.go b/boatstack/internal/softwaredelivery/model/facet.go index 578b5f1..039d41c 100644 --- a/boatstack/internal/softwaredelivery/model/facet.go +++ b/boatstack/internal/softwaredelivery/model/facet.go @@ -103,7 +103,7 @@ func (s Snapshot) Facet(name FacetName) (FactStatus, string, bool) { return s.Terminal.Status, string(s.Terminal.Value), true case FacetObjective: value := s.Objective.Value - return s.Objective.Status, strings.Join([]string{value.ID, string(value.Kind), value.DeliveryID, value.EvidenceFingerprint, fmt.Sprint(value.FrontierIsStop)}, "|"), true + return s.Objective.Status, strings.Join([]string{value.ID, string(value.TargetID), string(value.TrustedObjectiveClass()), value.DeliveryID, value.EvidenceFingerprint, fmt.Sprint(value.FrontierIsStop)}, "|"), true default: if fact, ok := s.ProgramFacts[string(name)]; ok { return fact.Status, fact.Value, true diff --git a/boatstack/internal/softwaredelivery/model/objective.go b/boatstack/internal/softwaredelivery/model/objective.go index cc37d72..dcae839 100644 --- a/boatstack/internal/softwaredelivery/model/objective.go +++ b/boatstack/internal/softwaredelivery/model/objective.go @@ -5,41 +5,44 @@ import ( "regexp" ) -type ObjectiveKind string +type TargetID string + +var safeObjectiveIdentity = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) const ( - ObjectiveApprovedPlan ObjectiveKind = "approved-plan" - ObjectiveVerified ObjectiveKind = "verified-implementation" - ObjectiveOpenPR ObjectiveKind = "open-or-updated-pr" - ObjectiveMerged ObjectiveKind = "merged-delivery" - ObjectiveAbandoned ObjectiveKind = "safely-abandoned" + ObjectiveApprovedPlan TargetID = "approved-plan" + ObjectiveVerified TargetID = "verified-implementation" + ObjectiveOpenPR TargetID = "open-or-updated-pr" + ObjectiveMerged TargetID = "merged-delivery" + ObjectiveAbandoned TargetID = "safely-abandoned" ) -func (k ObjectiveKind) Valid() bool { - switch k { - case ObjectiveApprovedPlan, ObjectiveVerified, ObjectiveOpenPR, ObjectiveMerged, ObjectiveAbandoned: - return true - default: - return false - } +func (k TargetID) Valid() bool { + return safeObjectiveIdentity.MatchString(string(k)) } type Objective struct { - ID string `json:"id"` - Kind ObjectiveKind `json:"kind"` - DeliveryID string `json:"delivery_id"` - EvidenceFingerprint string `json:"evidence_fingerprint,omitempty"` - FrontierIsStop bool `json:"frontier_is_stop,omitempty"` + ID string `json:"id"` + TargetID TargetID `json:"target_id"` + TrustedClass TargetID `json:"trusted_class,omitempty"` + DeliveryID string `json:"delivery_id"` + EvidenceFingerprint string `json:"evidence_fingerprint,omitempty"` + FrontierIsStop bool `json:"frontier_is_stop,omitempty"` } -var safeObjectiveIdentity = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) - func (g Objective) Validate() error { if !safeObjectiveIdentity.MatchString(g.ID) || !safeObjectiveIdentity.MatchString(g.DeliveryID) { return fmt.Errorf("objective: id and delivery identity must be safe semantic segments") } - if !g.Kind.Valid() { - return fmt.Errorf("objective: invalid kind %q", g.Kind) + if !g.TargetID.Valid() || (g.TrustedClass != "" && !g.TrustedClass.Valid()) { + return fmt.Errorf("objective: invalid target %q or trusted class %q", g.TargetID, g.TrustedClass) } return nil } + +func (g Objective) TrustedObjectiveClass() TargetID { + if g.TrustedClass != "" { + return g.TrustedClass + } + return g.TargetID +} diff --git a/boatstack/internal/softwaredelivery/model/state_test.go b/boatstack/internal/softwaredelivery/model/state_test.go index ed7c4ec..d2c16e6 100644 --- a/boatstack/internal/softwaredelivery/model/state_test.go +++ b/boatstack/internal/softwaredelivery/model/state_test.go @@ -136,7 +136,7 @@ func TestEveryControllingFacetChangesCanonicalIdentity(t *testing.T) { }, FacetTerminal: func(o *Observation) { o.Terminal = Known(TerminalStale, evidence) }, FacetObjective: func(o *Observation) { - o.Objective = Known(Objective{ID: "objective", Kind: ObjectiveVerified, DeliveryID: "delivery"}, evidence) + o.Objective = Known(Objective{ID: "objective", TargetID: ObjectiveVerified, DeliveryID: "delivery"}, evidence) }, } for _, facet := range ControllingFacets() { diff --git a/boatstack/internal/softwaredelivery/plant/observer.go b/boatstack/internal/softwaredelivery/plant/observer.go index e2a46d4..5ea58cc 100644 --- a/boatstack/internal/softwaredelivery/plant/observer.go +++ b/boatstack/internal/softwaredelivery/plant/observer.go @@ -191,21 +191,22 @@ func (o Observer) Observe(ctx context.Context, request ports.ObservationRequest) recoveryInfoFact := model.Absent[model.RecoveryContext]("no recovery context", stateEvidence...) transactionInfoFact := model.Absent[model.TransactionContext]("no active transaction", stateEvidence...) terminal := artifactTerminal - requiresCurrentImplementation := state.Objective.Kind == model.ObjectiveVerified || state.Objective.Kind == model.ObjectiveOpenPR + class := state.Objective.TrustedObjectiveClass() + requiresCurrentImplementation := class == model.ObjectiveVerified || class == model.ObjectiveOpenPR currentDeliveryInvalid := verification != model.VerificationCurrent || configuration != model.ConfigurationVerified || runtimeState != model.RuntimeVerified if requiresCurrentImplementation && (terminal == model.TerminalStale || (terminal == model.TerminalEstablished && currentDeliveryInvalid)) { terminal, phase, delivery = model.TerminalStale, model.PhaseActive, model.DeliveryActive if runtimeState == model.RuntimeAbsent { phase = model.PhaseObserved } - } else if state.Objective.Kind == model.ObjectiveApprovedPlan && terminal == model.TerminalStale { + } else if class == model.ObjectiveApprovedPlan && terminal == model.TerminalStale { phase, delivery = model.PhaseActive, model.DeliveryPlanning } - if state.Objective.Kind == model.ObjectiveMerged && state.Publication == model.PublicationMerged && state.Delivery == model.DeliveryTerminal && + if class == model.ObjectiveMerged && state.Publication == model.PublicationMerged && state.Delivery == model.DeliveryTerminal && (state.Workspace == model.WorkspaceLanded || state.Workspace == model.WorkspaceAbsent) { terminal, phase = model.TerminalEstablished, model.PhaseTerminal } - if state.Objective.Kind == model.ObjectiveAbandoned && state.Delivery == model.DeliveryDiscarded && + if class == model.ObjectiveAbandoned && state.Delivery == model.DeliveryDiscarded && (state.Workspace == model.WorkspaceAbandoned || state.Workspace == model.WorkspaceAbsent) { terminal, phase = model.TerminalEstablished, model.PhaseAbandoned } @@ -600,7 +601,7 @@ func observeRepositoryArtifacts(layout ports.ControllerLayout, state durable.Sta verification, terminal = model.VerificationStale, model.TerminalStale } } - if terminal == model.TerminalEstablished && state.Objective.Kind == model.ObjectiveVerified && state.VisualEvidencePolicy == "required" && !hasVisual { + if terminal == model.TerminalEstablished && state.Objective.TrustedObjectiveClass() == model.ObjectiveVerified && state.VisualEvidencePolicy == "required" && !hasVisual { verification, terminal = model.VerificationUnresolved, model.TerminalStale } return plan, verification, terminal, planEvidence, verificationEvidence, nil diff --git a/boatstack/internal/softwaredelivery/protocol/maintenance_objective_test.go b/boatstack/internal/softwaredelivery/protocol/maintenance_objective_test.go index 63e7678..467f731 100644 --- a/boatstack/internal/softwaredelivery/protocol/maintenance_objective_test.go +++ b/boatstack/internal/softwaredelivery/protocol/maintenance_objective_test.go @@ -10,8 +10,8 @@ import ( func TestMaintenanceObjectiveBindingUsesOnlyDurableProductState(t *testing.T) { // control-law: maintenance-admission-is-independent-from-command-product-intent transition := catalog.Transition{ID: "installation.update", Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeOptionalPreserve}} - configured := model.Objective{ID: "configured", Kind: model.ObjectiveOpenPR, DeliveryID: "delivery"} - conflicting := model.Objective{ID: "command", Kind: model.ObjectiveApprovedPlan, DeliveryID: "other"} + configured := model.Objective{ID: "configured", TargetID: model.ObjectiveOpenPR, DeliveryID: "delivery"} + conflicting := model.Objective{ID: "command", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "other"} tests := []struct { name string diff --git a/boatstack/internal/softwaredelivery/protocol/receipt.go b/boatstack/internal/softwaredelivery/protocol/receipt.go index 7789f74..62e3dfe 100644 --- a/boatstack/internal/softwaredelivery/protocol/receipt.go +++ b/boatstack/internal/softwaredelivery/protocol/receipt.go @@ -109,7 +109,8 @@ type TransitionReceipt struct { PriorStateRevision uint64 `json:"prior_state_revision"` ResultingStateRevision uint64 `json:"resulting_state_revision"` ObjectiveID string `json:"objective_id"` - ObjectiveKind model.ObjectiveKind `json:"objective_kind"` + TargetID model.TargetID `json:"target_id"` + TrustedClass model.TargetID `json:"trusted_class,omitempty"` DeliveryID string `json:"delivery_id"` ObjectiveScope catalog.ObjectiveScope `json:"objective_scope,omitempty"` ObjectiveStatus model.FactStatus `json:"objective_status,omitempty"` @@ -181,7 +182,7 @@ func NewReceipt(flowID string, sequence uint64, program ProgramIdentity, admissi TransitionID: transition.ID, TransitionVersion: transition.Version, PrescriptionID: admission.PrescriptionID, AdmissionID: admission.ID, PriorStateRevision: admission.ExpectedStateRevision, ResultingStateRevision: target.StateRevision, - ObjectiveID: admission.Objective.ID, ObjectiveKind: admission.Objective.Kind, DeliveryID: admission.Objective.DeliveryID, + ObjectiveID: admission.Objective.ID, TargetID: admission.Objective.TargetID, TrustedClass: admission.Objective.TrustedClass, DeliveryID: admission.Objective.DeliveryID, ObjectiveScope: admission.ObjectiveScope, ObjectiveStatus: admission.ObjectiveStatus, ObjectiveBindingFingerprint: resultingObjectiveBindingFingerprint, SourceFingerprint: admission.ExpectedSnapshotFingerprint, TargetFingerprint: target.Fingerprint, @@ -289,17 +290,17 @@ func (r TransitionReceipt) Validate() error { if r.ObjectiveScope == catalog.ObjectiveScopeOptionalPreserve { switch r.ObjectiveStatus { case model.FactKnown: - if r.ObjectiveID == "" || !r.ObjectiveKind.Valid() || r.DeliveryID == "" { + if r.ObjectiveID == "" || !r.TargetID.Valid() || (r.TrustedClass != "" && !r.TrustedClass.Valid()) || r.DeliveryID == "" { return fmt.Errorf("maintenance receipt has incomplete known objective binding") } case model.FactAbsent: - if r.ObjectiveID != "" || r.ObjectiveKind != "" || r.DeliveryID != "" { + if r.ObjectiveID != "" || r.TargetID != "" || r.DeliveryID != "" { return fmt.Errorf("maintenance receipt invents product intent from verified absence") } default: return fmt.Errorf("maintenance receipt requires known or verified-absent objective status") } - } else if r.ObjectiveID == "" || !r.ObjectiveKind.Valid() || r.DeliveryID == "" { + } else if r.ObjectiveID == "" || !r.TargetID.Valid() || (r.TrustedClass != "" && !r.TrustedClass.Valid()) || r.DeliveryID == "" { return fmt.Errorf("receipt has incomplete objective identity") } if r.StartedAt.IsZero() || r.CommittedAt.Before(r.StartedAt) || r.DurationNanoseconds != r.CommittedAt.Sub(r.StartedAt).Nanoseconds() || r.Verification.VerifiedAt.After(r.CommittedAt) { diff --git a/boatstack/internal/softwaredelivery/protocol/receipt_capability_test.go b/boatstack/internal/softwaredelivery/protocol/receipt_capability_test.go index f399f31..4b4a6d1 100644 --- a/boatstack/internal/softwaredelivery/protocol/receipt_capability_test.go +++ b/boatstack/internal/softwaredelivery/protocol/receipt_capability_test.go @@ -20,7 +20,7 @@ func TestReceiptRejectsRehashedAuthorityProvenanceTampering(t *testing.T) { ID: "admission", PrescriptionID: "prescription", ExpectedStateRevision: 1, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), ExpectedObjectiveBindingFingerprint: strings.Repeat("d", 64), - Objective: model.Objective{ID: "objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "delivery"}, + Objective: model.Objective{ID: "objective", TargetID: model.ObjectiveApprovedPlan, DeliveryID: "delivery"}, ObjectiveScope: catalog.ObjectiveScopeBoundExact, Authority: authority, AuthorityFingerprint: authorityFingerprint, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, diff --git a/boatstack/internal/softwaredelivery/protocol/receipt_fact_test.go b/boatstack/internal/softwaredelivery/protocol/receipt_fact_test.go index 79fa85d..67cfb46 100644 --- a/boatstack/internal/softwaredelivery/protocol/receipt_fact_test.go +++ b/boatstack/internal/softwaredelivery/protocol/receipt_fact_test.go @@ -22,7 +22,7 @@ func committedReceiptFixture(t *testing.T) (TransitionReceipt, Admission, catalo ID: "adm-fixture", PrescriptionID: "prx-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), ExpectedObjectiveBindingFingerprint: strings.Repeat("d", 64), - Objective: model.Objective{ID: "objective", Kind: model.ObjectiveApprovedPlan, DeliveryID: "delivery"}, + Objective: model.Objective{ID: "objective", TargetID: "approved-plan-for-repo", TrustedClass: model.ObjectiveApprovedPlan, DeliveryID: "delivery"}, ObjectiveScope: catalog.ObjectiveScopeBoundExact, Authority: authority, AuthorityFingerprint: authorityFingerprint, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, GrantedCapabilities: authority.GrantedCapabilities(now), EffectiveCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, @@ -67,6 +67,9 @@ func TestCommittedTransitionFactBindsProgramTransitionStateAuthorityEffectsAndVe if receipt.TransitionID != transition.ID || receipt.PriorStateRevision != 41 || receipt.ResultingStateRevision != 42 { t.Fatalf("transition/revision fact = %#v", receipt) } + if receipt.TargetID != admission.Objective.TargetID || receipt.TrustedClass != admission.Objective.TrustedClass { + t.Fatalf("target identity = %#v", receipt) + } if receipt.AuthorityFingerprint != admission.AuthorityFingerprint || len(receipt.AuthoritySources) != 1 || len(receipt.RequiredCapabilities) != 1 || len(receipt.GrantedCapabilities) == 0 { t.Fatalf("authority fact = %#v", receipt) } diff --git a/boatstack/internal/softwaredelivery/supervisor/guard_test.go b/boatstack/internal/softwaredelivery/supervisor/guard_test.go index d6eef9c..a477398 100644 --- a/boatstack/internal/softwaredelivery/supervisor/guard_test.go +++ b/boatstack/internal/softwaredelivery/supervisor/guard_test.go @@ -43,7 +43,7 @@ func syntheticManagedTransition(id catalog.TransitionID, class catalog.EventClas Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: "manifest"}, Owner: "test.synthetic", SelectionClass: selection, Class: class, SourcePhases: []model.ProtocolPhase{model.PhaseActive}, TargetPhases: []model.ProtocolPhase{model.PhaseActive}, - ObjectiveKinds: []model.ObjectiveKind{model.ObjectiveVerified}, RequiredIdentity: []string{"repository-id"}, + TargetIDs: []model.TargetID{model.ObjectiveVerified}, RequiredIdentity: []string{"repository-id"}, Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, DeclaredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"test.synthetic.state"}, OwnedFacets: []model.StateFacet{model.StateFacetControl}, StateEffect: catalog.StateEffect{Kind: catalog.StateEffectAssignments}, diff --git a/boatstack/internal/softwaredelivery/surfaces/render.go b/boatstack/internal/softwaredelivery/surfaces/render.go index eb28259..898f776 100644 --- a/boatstack/internal/softwaredelivery/surfaces/render.go +++ b/boatstack/internal/softwaredelivery/surfaces/render.go @@ -44,7 +44,7 @@ func PrescriptionCommand(transition catalog.Transition, prescription protocol.Pr arguments = append(arguments, "--effective-capability", string(capability)) } if objective.Validate() == nil { - arguments = append(arguments, "--objective-kind", string(objective.Kind), "--delivery", objective.DeliveryID, "--objective-id", objective.ID) + arguments = append(arguments, "--target-id", string(objective.TargetID), "--delivery", objective.DeliveryID, "--objective-id", objective.ID) } canonical := parameters.Canonical() for _, parameter := range canonical { diff --git a/boatstack/internal/softwaredelivery/surfaces/render_test.go b/boatstack/internal/softwaredelivery/surfaces/render_test.go index 07a9831..67bb544 100644 --- a/boatstack/internal/softwaredelivery/surfaces/render_test.go +++ b/boatstack/internal/softwaredelivery/surfaces/render_test.go @@ -20,7 +20,7 @@ func TestShellRenderersConsumeOneCommandAST(t *testing.T) { if !ok { t.Fatal("missing plan.create") } - objective := model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} + objective := model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"} parameters := protocol.Parameters{{Name: "source_path", Value: "/tmp/O'Brien plan.md"}, {Name: "delivery_id", Value: "delivery"}} prescription := protocol.Prescription{ ID: "prx-fixture", Freshness: general.Freshness{ExpectedInstanceID: "repo-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), ExpectedObjectiveBindingFingerprint: strings.Repeat("c", 64), AuthorityFingerprint: "auth-fixture"}, @@ -118,7 +118,7 @@ func TestLocusModelsAreGeneratedFromEveryRuntimeTransition(t *testing.T) { } func TestEveryHostConsumesOneSemanticPrescription(t *testing.T) { - objective := model.Objective{ID: "objective", Kind: model.ObjectiveVerified, DeliveryID: "delivery"} + objective := model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"} prescription := protocol.Prescription{ID: "prx-fixture", Freshness: general.Freshness{ExpectedInstanceID: "repo-fixture", ExpectedStateRevision: 41, ExpectedProgramFingerprint: strings.Repeat("a", 64), ExpectedSnapshotFingerprint: strings.Repeat("b", 64), ExpectedObjectiveBindingFingerprint: strings.Repeat("c", 64)}} for _, transition := range testprogram.StandardRegistry().All() { if !transition.Controllable() { diff --git a/boatstack/program_effects_test.go b/boatstack/program_effects_test.go index 7e27b2e..ac0bc2e 100644 --- a/boatstack/program_effects_test.go +++ b/boatstack/program_effects_test.go @@ -88,7 +88,7 @@ func (protocolStateRuntime) RuntimeManifest(context.Context) (delivery.ProgramRu publish := delivery.Transition{ ID: publishID, Version: 1, SelectionClass: delivery.SelectionProgramProgress, Class: delivery.EventOwnedLocal, SourcePhases: []delivery.ProtocolPhase{delivery.PhaseActive}, TargetPhases: []delivery.ProtocolPhase{delivery.PhaseActive}, - ObjectiveKinds: []delivery.ObjectiveKind{delivery.ObjectiveVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"}, + TargetIDs: []delivery.TargetID{delivery.ObjectiveVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"}, Authority: []delivery.AuthorityClass{delivery.AuthorityHuman, delivery.AuthorityRepository}, RequiredCapabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityProductMutate}, RequiredEvidence: []string{"snapshot", "objective"}, OwnedResources: []string{resource}, OwnedFacets: []delivery.StateFacet{delivery.StateFacetControl, delivery.StateFacetProduct}, @@ -105,9 +105,9 @@ func (protocolStateRuntime) RuntimeManifest(context.Context) (delivery.ProgramRu } return delivery.ProgramRuntimeManifest{ ID: programID, Version: "1.0.0", ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, RuntimeMode: delivery.ProgramRuntimeProtocol, - SupportedObjectives: []delivery.ObjectiveKind{delivery.ObjectiveVerified}, - ObjectiveContracts: []delivery.ObjectiveContract{{ObjectiveKind: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, string(model.DeliveryPublished))}}}, - Transitions: []delivery.Transition{publish, recover}, OwnedResources: []string{resource}, + SupportedTargets: []delivery.TargetID{delivery.ObjectiveVerified}, + ObjectiveContracts: []delivery.ObjectiveContract{{TargetID: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetDelivery, string(model.DeliveryPublished))}}}, + Transitions: []delivery.Transition{publish, recover}, OwnedResources: []string{resource}, Effects: []string{string(publishEffect), string(recoverEffect)}, Verifiers: []string{"fixture.state.published", "fixture.state.recovered"}, Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute, delivery.CapabilityProductMutate}, ConfigurationSchema: json.RawMessage(`{"type":"object"}`), PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", @@ -191,7 +191,7 @@ func TestProgramRuntimeProtocolCommitsDeclaredStateEffectBeforeReceipt(t *testin state.Phase, state.Engagement, state.Delivery = model.PhaseActive, model.EngagementActive, model.DeliveryActive state.Configuration, state.ConfigFingerprint = model.ConfigurationVerified, "configuration-fingerprint" state.PlanApprovalPolicy, state.VisualEvidencePolicy, state.ExternalEffectPolicy, state.EnabledHosts = "human", "optional", "human-or-autonomy-plus-provider", []string{"cli"} - state.Objective = model.Objective{ID: "protocol-objective", Kind: model.ObjectiveVerified, DeliveryID: "protocol-delivery"} + state.Objective = model.Objective{ID: "protocol-objective", TargetID: model.ObjectiveVerified, DeliveryID: "protocol-delivery"} encoded, err := durable.EncodeState(state) if err != nil { t.Fatal(err) diff --git a/boatstack/sdk/sdk.go b/boatstack/sdk/sdk.go index 23c7911..7e2022a 100644 --- a/boatstack/sdk/sdk.go +++ b/boatstack/sdk/sdk.go @@ -36,7 +36,7 @@ type Response = surfaces.Response type DoctorReport = surfaces.DoctorReport type ProgramChange = surfaces.ProgramChange type Objective = model.Objective -type ObjectiveKind = model.ObjectiveKind +type TargetID = model.TargetID type StateFacet = model.StateFacet const ( diff --git a/boatstack/sdk/sdk_test.go b/boatstack/sdk/sdk_test.go index 3b9f36e..e993ff4 100644 --- a/boatstack/sdk/sdk_test.go +++ b/boatstack/sdk/sdk_test.go @@ -17,9 +17,9 @@ func TestPublicProtocolCanBeConstructedWithoutInternalPackages(t *testing.T) { Repository: t.TempDir(), Host: "mcp", CorrelationID: "correlation", - Objective: sdk.Objective{ID: "objective", Kind: sdk.ObjectiveVerified, DeliveryID: "delivery"}, + Objective: sdk.Objective{ID: "objective", TargetID: sdk.ObjectiveVerified, DeliveryID: "delivery"}, } - if request.Objective.Kind != sdk.ObjectiveVerified || request.Operation != sdk.OperationResolve { + if request.Objective.TargetID != sdk.ObjectiveVerified || request.Operation != sdk.OperationResolve { t.Fatalf("public V2 aliases lost protocol identity: %#v", request) } } @@ -37,7 +37,7 @@ func TestSDKPreservesCapabilityAdmissionProtocol(t *testing.T) { } func TestSDKSerializesTheSameDurableTransitionFactAsTheSurface(t *testing.T) { - raw := []byte(`{"schema_version":5,"operation":"apply","receipt":{"schema_version":8,"kind":"transition-committed","id":"trc-fact","flow_id":"flow","sequence":1,"program":{"id":"product-delivery","version":"1.0.0","fingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"transition_id":"product-delivery/build.begin","transition_version":1,"prescription_id":"prx","admission_id":"adm","prior_state_revision":41,"resulting_state_revision":42,"objective_id":"objective","objective_kind":"verified","delivery_id":"delivery","objective_scope":"bound-exact","objective_binding_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","source_fingerprint":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","target_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","authority_fingerprint":"auth","authority_sources":[],"required_capabilities":["repository.write"],"granted_capabilities":["repository.write"],"committed_effects":[{"kind":"resource-mutation","effect_id":"build.begin","owner":"product-delivery","resource":"state","target":"/state","operation":"update","prior_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","resulting_fingerprint":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}],"changed_state_facets":["control","product"],"verification":{"verifier":"build-active","expected_postcondition":"active","result":"satisfied","evidence_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","verified_at":"2026-08-12T00:00:00Z"},"idempotency_key":"idem","terminal":"nonterminal","started_at":"2026-08-12T00:00:00Z","committed_at":"2026-08-12T00:00:01Z","duration_nanoseconds":1000000000}}`) + raw := []byte(`{"schema_version":5,"operation":"apply","receipt":{"schema_version":8,"kind":"transition-committed","id":"trc-fact","flow_id":"flow","sequence":1,"program":{"id":"product-delivery","version":"1.0.0","fingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"transition_id":"product-delivery/build.begin","transition_version":1,"prescription_id":"prx","admission_id":"adm","prior_state_revision":41,"resulting_state_revision":42,"objective_id":"objective","target_id":"verified","delivery_id":"delivery","objective_scope":"bound-exact","objective_binding_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","source_fingerprint":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","target_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","authority_fingerprint":"auth","authority_sources":[],"required_capabilities":["repository.write"],"granted_capabilities":["repository.write"],"committed_effects":[{"kind":"resource-mutation","effect_id":"build.begin","owner":"product-delivery","resource":"state","target":"/state","operation":"update","prior_fingerprint":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","resulting_fingerprint":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}],"changed_state_facets":["control","product"],"verification":{"verifier":"build-active","expected_postcondition":"active","result":"satisfied","evidence_fingerprint":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","verified_at":"2026-08-12T00:00:00Z"},"idempotency_key":"idem","terminal":"nonterminal","started_at":"2026-08-12T00:00:00Z","committed_at":"2026-08-12T00:00:01Z","duration_nanoseconds":1000000000}}`) var response sdk.Response if err := json.Unmarshal(raw, &response); err != nil { t.Fatal(err) @@ -94,7 +94,7 @@ func (syntheticFlow) RuntimeManifest(context.Context) (delivery.ProgramRuntimeMa return delivery.Transition{ ID: id, Version: 1, SelectionClass: delivery.SelectionProgramProgress, Class: delivery.EventOwnedLocal, SourcePhases: []delivery.ProtocolPhase{delivery.PhaseObserved, delivery.PhaseActive}, TargetPhases: []delivery.ProtocolPhase{delivery.PhaseObserved, delivery.PhaseActive}, - ObjectiveKinds: []delivery.ObjectiveKind{delivery.ObjectiveVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"}, + TargetIDs: []delivery.TargetID{delivery.ObjectiveVerified}, RequiredIdentity: []string{"repository-id", "git-common-id", "worktree-id"}, Authority: []delivery.AuthorityClass{delivery.AuthorityRepository}, RequiredCapabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, RequiredEvidence: []string{"snapshot", "objective", "facet:" + fact}, OwnedResources: []string{resource}, OwnedFacets: []delivery.StateFacet{delivery.StateFacetControl}, StateEffect: delivery.StateEffect{Kind: delivery.StateEffectAssignments}, Effect: effect, LocalEffects: []delivery.EffectID{effect}, Idempotent: true, @@ -115,9 +115,9 @@ func (syntheticFlow) RuntimeManifest(context.Context) (delivery.ProgramRuntimeMa finish := transition("synthetic.lifecycle.finish", "verify", "terminal", 2) return delivery.ProgramRuntimeManifest{ ID: id, Version: "1.0.0", ProtocolVersion: delivery.ProgramRuntimeProtocolVersion, RuntimeMode: delivery.ProgramRuntimeProtocol, - SupportedObjectives: []delivery.ObjectiveKind{delivery.ObjectiveVerified}, - ObjectiveContracts: []delivery.ObjectiveContract{{ObjectiveKind: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetName(fact), "terminal")}}}, - Transitions: []delivery.Transition{verify, finish}, Facts: []string{fact}, OwnedResources: []string{resource}, + SupportedTargets: []delivery.TargetID{delivery.ObjectiveVerified}, + ObjectiveContracts: []delivery.ObjectiveContract{{TargetID: delivery.ObjectiveVerified, Conditions: []delivery.FacetCondition{delivery.KnownCondition(delivery.FacetName(fact), "terminal")}}}, + Transitions: []delivery.Transition{verify, finish}, Facts: []string{fact}, OwnedResources: []string{resource}, Effects: []string{string(verify.Effect), string(finish.Effect)}, Verifiers: []string{verify.Verifier, finish.Verifier}, Capabilities: []delivery.Capability{delivery.CapabilityRepositoryWrite, delivery.CapabilityCommandExecute}, ConfigurationSchema: json.RawMessage(`{"type":"object"}`), diff --git a/boatstack/testdata/control-programs/product-delivery-a.flow.ts b/boatstack/testdata/control-programs/product-delivery-a.flow.ts new file mode 100644 index 0000000..1a84f46 --- /dev/null +++ b/boatstack/testdata/control-programs/product-delivery-a.flow.ts @@ -0,0 +1,31 @@ +import { all, defineFlow, entry, fact, marked } from "@operatorstack/boatstack"; +import { + inbox, + planInboxResolver, + softwareDeliveryEvidence, + softwareDeliveryFacets, + trustedOperators, + trustedTransitions, + type TrustedStep, +} from "@operatorstack/boatstack-software-delivery"; + +const lifecycle = [ + { id: "publication.observe", priority: 77 }, +] satisfies TrustedStep[]; + +export default defineFlow({ + id: "product-delivery-a", + version: "1", + declarations: { input_resolvers: [planInboxResolver] }, + facets: softwareDeliveryFacets, + evidence: softwareDeliveryEvidence, + operators: trustedOperators(lifecycle), + transitions: trustedTransitions(lifecycle), + targets: [marked("published-pr", all( + fact("verification", ["current"]), + fact("configuration", ["verified"]), + fact("runtime", ["verified"]), + fact("publication", ["open"]), + ))], + entries: [entry("run", "published-pr", [inbox(".boatstack/plans/inbox")])], +}); diff --git a/boatstack/testdata/control-programs/product-delivery-b.flow.ts b/boatstack/testdata/control-programs/product-delivery-b.flow.ts new file mode 100644 index 0000000..641ada9 --- /dev/null +++ b/boatstack/testdata/control-programs/product-delivery-b.flow.ts @@ -0,0 +1,41 @@ +import { all, defineFlow, entry, fact, marked } from "@operatorstack/boatstack"; +import { + inbox, + planInboxResolver, + softwareDeliveryEvidence, + softwareDeliveryFacets, + trustedOperators, + trustedTransitions, + type TrustedStep, +} from "@operatorstack/boatstack-software-delivery"; + +const lifecycle = [ + { id: "publication.observe", priority: 77 }, + { id: "plan.abandon", priority: 31 }, +] satisfies TrustedStep[]; + +export default defineFlow({ + id: "product-delivery-b", + version: "1", + declarations: { input_resolvers: [planInboxResolver] }, + facets: softwareDeliveryFacets, + evidence: softwareDeliveryEvidence, + operators: trustedOperators(lifecycle), + transitions: trustedTransitions(lifecycle), + targets: [ + marked("published-pr", all( + fact("verification", ["current"]), + fact("configuration", ["verified"]), + fact("runtime", ["verified"]), + fact("publication", ["open"]), + )), + marked("safely-abandoned", all( + fact("delivery", ["discarded"]), + fact("workspace", ["abandoned", "absent"]), + )), + ], + entries: [ + entry("deliver", "published-pr", [inbox(".boatstack/plans/inbox")]), + entry("cancel", "safely-abandoned", [inbox(".boatstack/plans/inbox")]), + ], +}); diff --git a/boatstack/testdata/control-programs/product-delivery-c.flow.ts b/boatstack/testdata/control-programs/product-delivery-c.flow.ts new file mode 100644 index 0000000..a6b974d --- /dev/null +++ b/boatstack/testdata/control-programs/product-delivery-c.flow.ts @@ -0,0 +1,36 @@ +import { all, defineFlow, entry, fact, marked } from "@operatorstack/boatstack"; +import { + inbox, + planInboxResolver, + softwareDeliveryEvidence, + softwareDeliveryFacets, + trustedOperators, + trustedTransitions, + type TrustedStep, +} from "@operatorstack/boatstack-software-delivery"; + +const lifecycle = [ + { id: "gate.test.record", priority: 62 }, + { id: "gate.review.record", priority: 63 }, + { id: "gate.change.record", priority: 64 }, + { id: "publication.preview", priority: 72 }, + { id: "publication.execute", priority: 76 }, + { id: "publication.observe", priority: 77 }, +] satisfies TrustedStep[]; + +export default defineFlow({ + id: "product-delivery-c", + version: "1", + declarations: { input_resolvers: [planInboxResolver] }, + facets: softwareDeliveryFacets, + evidence: softwareDeliveryEvidence, + operators: trustedOperators(lifecycle), + transitions: trustedTransitions(lifecycle), + targets: [marked("published-pr", all( + fact("verification", ["current"]), + fact("configuration", ["verified"]), + fact("runtime", ["verified"]), + fact("publication", ["open"]), + ))], + entries: [entry("run", "published-pr", [inbox(".boatstack/plans/inbox")])], +}); diff --git a/boatstack/testdata/v2-scenarios/historical.json b/boatstack/testdata/v2-scenarios/historical.json index dace21a..9ed5eb5 100644 --- a/boatstack/testdata/v2-scenarios/historical.json +++ b/boatstack/testdata/v2-scenarios/historical.json @@ -41,7 +41,7 @@ }, "requested_objective": { "id": "objective-stale-worktree-runtime-selection", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-stale-worktree-runtime-selection" }, "event": "runtime.hydrate", @@ -98,7 +98,7 @@ }, "requested_objective": { "id": "objective-detached-bootstrap-hydration", - "kind": "approved-plan", + "target_id": "approved-plan", "delivery_id": "delivery-detached-bootstrap-hydration" }, "event": "runtime.hydrate", @@ -155,7 +155,7 @@ }, "requested_objective": { "id": "objective-workspace-transition-deadlock", - "kind": "safely-abandoned", + "target_id": "safely-abandoned", "delivery_id": "delivery-workspace-transition-deadlock" }, "event": "workspace.abandon", @@ -212,7 +212,7 @@ }, "requested_objective": { "id": "objective-cleanup-before-publication", - "kind": "open-or-updated-pr", + "target_id": "open-or-updated-pr", "delivery_id": "delivery-cleanup-before-publication" }, "event": "workspace.cleanup", @@ -270,7 +270,7 @@ }, "requested_objective": { "id": "objective-saved-plan-ambient-restriction", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-saved-plan-ambient-restriction" }, "event": "plan.activate", @@ -327,7 +327,7 @@ }, "requested_objective": { "id": "objective-detached-command-admission-mismatch", - "kind": "approved-plan", + "target_id": "approved-plan", "delivery_id": "delivery-detached-command-admission-mismatch" }, "event": "plan.validate", @@ -384,7 +384,7 @@ }, "requested_objective": { "id": "objective-split-bootstrap-command-authority", - "kind": "approved-plan", + "target_id": "approved-plan", "delivery_id": "delivery-split-bootstrap-command-authority" }, "event": "plan.create", @@ -441,7 +441,7 @@ }, "requested_objective": { "id": "objective-configuration-projection-drift", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-configuration-projection-drift" }, "event": "configuration.mutate", @@ -498,7 +498,7 @@ }, "requested_objective": { "id": "objective-partial-delivery-vs-merged-projection", - "kind": "merged-delivery", + "target_id": "merged-delivery", "delivery_id": "delivery-partial-delivery-vs-merged-projection" }, "event": "workspace.cleanup", @@ -556,7 +556,7 @@ }, "requested_objective": { "id": "objective-dormant-repository-interference", - "kind": "approved-plan", + "target_id": "approved-plan", "delivery_id": "delivery-dormant-repository-interference" }, "event": "plan.create", @@ -613,7 +613,7 @@ }, "requested_objective": { "id": "objective-configuration-mutation-self-invalidation", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-configuration-mutation-self-invalidation" }, "event": "configuration.mutate", @@ -670,7 +670,7 @@ }, "requested_objective": { "id": "objective-cross-shard-deterministic-resolution", - "kind": "approved-plan", + "target_id": "approved-plan", "delivery_id": "delivery-cross-shard-deterministic-resolution" }, "event": "plan.validate", @@ -727,7 +727,7 @@ }, "requested_objective": { "id": "objective-unpublished-equal-main-not-landed", - "kind": "merged-delivery", + "target_id": "merged-delivery", "delivery_id": "delivery-unpublished-equal-main-not-landed" }, "event": "workspace.cleanup", @@ -784,7 +784,7 @@ }, "requested_objective": { "id": "objective-closed-unmerged-not-cleanup-eligible", - "kind": "merged-delivery", + "target_id": "merged-delivery", "delivery_id": "delivery-closed-unmerged-not-cleanup-eligible" }, "event": "workspace.cleanup", @@ -841,7 +841,7 @@ }, "requested_objective": { "id": "objective-bindd-merged-terminal", - "kind": "merged-delivery", + "target_id": "merged-delivery", "delivery_id": "delivery-configured-merged-terminal" }, "event": "", @@ -898,7 +898,7 @@ }, "requested_objective": { "id": "objective-active-workspace-preserved", - "kind": "merged-delivery", + "target_id": "merged-delivery", "delivery_id": "delivery-active-workspace-preserved" }, "event": "workspace.cleanup", @@ -955,7 +955,7 @@ }, "requested_objective": { "id": "objective-amendment-deadlock", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-amendment-deadlock" }, "event": "plan.approve-amendment", @@ -1013,7 +1013,7 @@ }, "requested_objective": { "id": "objective-ambiguous-detached-controller-alias", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-ambiguous-detached-controller-alias" }, "event": "workspace.cleanup", @@ -1071,7 +1071,7 @@ }, "requested_objective": { "id": "objective-activation-wrong-worktree-identity", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-activation-wrong-worktree-identity" }, "event": "plan.activate", @@ -1128,7 +1128,7 @@ }, "requested_objective": { "id": "objective-runtime-publication-before-lock-release", - "kind": "open-or-updated-pr", + "target_id": "open-or-updated-pr", "delivery_id": "delivery-runtime-publication-before-lock-release" }, "event": "publication.preview", @@ -1186,7 +1186,7 @@ }, "requested_objective": { "id": "objective-recovery-outranks-slice-position", - "kind": "verified-implementation", + "target_id": "verified-implementation", "delivery_id": "delivery-recovery-outranks-slice-position" }, "event": "recovery.resume", @@ -1243,7 +1243,7 @@ }, "requested_objective": { "id": "objective-unknown-provider-is-not-complete", - "kind": "open-or-updated-pr", + "target_id": "open-or-updated-pr", "delivery_id": "delivery-unknown-provider-is-not-complete" }, "event": "publication.execute", diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md index 33c66ed..b847da7 100644 --- a/docs/architecture/boatstack-v2-transition-catalog.md +++ b/docs/architecture/boatstack-v2-transition-catalog.md @@ -7,68 +7,68 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w | Transition | Origin | Owner | Selection | Class | Source phases | Target phases | Authority | Required capabilities | Parameters | Owned resources | Verifier | Recovery | Cost | |---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `repository.write` | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` | -| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` | -| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` | -| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `repository.write` | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` | -| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` | -| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` | -| `engagement.release` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` | -| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` | -| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | `product.mutate`, `repository.write` | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` | -| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` | -| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.branch-changed` | `-` | `declared-neutral` | -| `external.ci-completed` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.ci-completed` | `-` | `declared-neutral` | -| `external.configuration-drifted` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` | -| `external.files-changed` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.files-changed` | `-` | `declared-neutral` | -| `external.head-changed` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | - | `verifier:fresh-observation:external.head-changed` | `-` | `declared-neutral` | -| `external.host-interrupted` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | - | `verifier:fresh-observation:external.host-interrupted` | `-` | `declared-neutral` | -| `external.lease-expired` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | - | `verifier:fresh-observation:external.lease-expired` | `-` | `declared-neutral` | -| `external.pr-closed` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | - | `verifier:fresh-observation:external.pr-closed` | `-` | `declared-neutral` | -| `external.pr-merged` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-merged` | `-` | `declared-neutral` | -| `external.pr-opened` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-opened` | `-` | `declared-neutral` | -| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | - | `verifier:fresh-observation:external.pr-updated` | `-` | `declared-neutral` | -| `external.provider-unavailable` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.provider-unavailable` | `-` | `declared-neutral` | -| `external.runtime-disappeared` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` | -| `gate.build.record` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` | -| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` | -| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` | -| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` | -| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` | -| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` | -| `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` | -| `installation.update` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` | -| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | `repository.write` | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` | -| `objective.bind` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `product.mutate`, `repository.write` | `objective_kind*`, `delivery_id*` | `objective` | `verifier:fresh-observation:objective.bind` | `recovery.resume` | `declared-neutral` | -| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` | -| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` | -| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` | -| `plan.approve` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | -| `plan.approve-amendment` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | -| `plan.create` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | -| `plan.invalidate` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | -| `plan.validate` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | -| `publication.abandon` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | -| `publication.correct` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | -| `publication.execute` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | -| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` | -| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `publication.prepare`, `repository.write` | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` | -| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `command.execute`, `product.mutate`, `repository.write` | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` | -| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` | -| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` | -| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` | -| `repository.attach` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` | -| `repository.detach` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | `product.mutate`, `repository.write` | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` | -| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` | -| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` | -| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`888b71d9c3d401472edfb234dc78f48eeca0308d915e7bf7e2a52b418164b463` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` | -| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` | -| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` | -| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` | -| `workspace.cut` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | -| `workspace.publish` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | -| `workspace.reap` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | -| `workspace.reconcile` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `product.mutate`, `repository.write` | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | -| `workspace.sync` | control-program:`boatstack.standard@1.0.0`
`94432db5a3760b8b884a55f5d1fff6f63b1a492e4c80651d8ef9fe0dd336370c` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` | +| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `repository.write` | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` | +| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` | +| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` | +| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `repository.write` | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` | +| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` | +| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` | +| `engagement.release` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` | +| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` | +| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | `product.mutate`, `repository.write` | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` | +| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` | +| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` | +| `gate.build.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` | +| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` | +| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` | +| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` | +| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` | +| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` | +| `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` | +| `installation.update` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` | +| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | `repository.write` | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` | +| `objective.bind` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `product.mutate`, `repository.write` | `target_id*`, `delivery_id*` | `objective` | `verifier:fresh-observation:objective.bind` | `recovery.resume` | `declared-neutral` | +| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` | +| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` | +| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` | +| `plan.approve` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | +| `plan.approve-amendment` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | +| `plan.create` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | +| `plan.invalidate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | +| `plan.validate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | +| `publication.abandon` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | +| `publication.correct` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | +| `publication.execute` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | +| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` | +| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `publication.prepare`, `repository.write` | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` | +| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `command.execute`, `product.mutate`, `repository.write` | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` | +| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` | +| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` | +| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` | +| `repository.attach` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` | +| `repository.detach` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | `product.mutate`, `repository.write` | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` | +| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` | +| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` | +| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` | +| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` | +| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` | +| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` | +| `workspace.cut` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | +| `workspace.publish` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | +| `workspace.reap` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | +| `workspace.reconcile` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `product.mutate`, `repository.write` | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | +| `workspace.sync` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `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/configuration.md b/docs/configuration.md index e4a6efe..9b90f2a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -122,7 +122,7 @@ configuration source by first-match path discovery. ```sh boatstack attach --repo . --human alice \ - --objective-id bootstrap --objective-kind approved-plan --delivery bootstrap \ + --objective-id bootstrap --target-id approved-plan --delivery bootstrap \ --param topology=detached --param config_authority=external ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index 4e91368..5e14f34 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -32,9 +32,9 @@ This example targets a verified implementation: ```sh boatstack objective-bind --repo . \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout --human alice \ - --param objective_kind=verified-implementation \ + --param target_id=verified-implementation \ --param delivery_id=search-timeout ``` @@ -45,11 +45,11 @@ the current, independently hashed `.boatstack/project.json`. ```sh boatstack next --repo . --transition engagement.begin \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout --repository-authority --format json boatstack apply --repo . --transition engagement.begin --run-id search-timeout \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout --repository-authority \ --correlation --prescription-id \ --expected-state-revision \ @@ -69,13 +69,13 @@ time; both are read-only. ```sh boatstack plan-create --repo . \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout --human alice \ --param source_path=/absolute/path/to/plan.md \ --param delivery_id=search-timeout boatstack plan-validate --repo . \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout --repository-authority ``` @@ -84,12 +84,12 @@ bytes: ```sh boatstack plan-approve --repo . \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout --human alice \ --param plan_fingerprint= --param actor=alice boatstack plan-activate --repo . \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout --human alice ``` @@ -116,7 +116,7 @@ status installs the evidence and receipt. Command output is never persisted: ```sh boatstack record-build --repo . --repository-authority \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout \ --param source_revision="$(git rev-parse HEAD)" \ --param evidence_path=/absolute/path/to/build-evidence.json \ @@ -137,7 +137,7 @@ parks the source checkout: ```sh boatstack workspace-cut --repo . --human alice \ - --objective-id search-timeout --objective-kind verified-implementation \ + --objective-id search-timeout --target-id verified-implementation \ --delivery search-timeout \ --param branch=feature/search-timeout \ --param base_ref=origin/main \ diff --git a/packages/boatstack-software-delivery/src/index.ts b/packages/boatstack-software-delivery/src/index.ts index 19045b9..1754eb9 100644 --- a/packages/boatstack-software-delivery/src/index.ts +++ b/packages/boatstack-software-delivery/src/index.ts @@ -1,18 +1,12 @@ import { - all, always, - entry, - fact, facet, - marked, operator, transition, - type EntryDefinition, type EntryInputDefinition, + type EvidenceDefinition, type FacetDefinition, - type FlowDefinition, type OperatorDefinition, - type TargetDefinition, type TransitionDefinition, } from "@operatorstack/boatstack"; @@ -52,63 +46,20 @@ export const softwareDeliveryFacets: FacetDefinition[] = [ "worktree_fingerprint", ].map((id) => facet(id, "string")); -export const publishedPR: TargetDefinition = marked( - "published-pr", - all( - fact("verification", ["current"]), - fact("configuration", ["verified"]), - fact("runtime", ["verified"]), - fact("publication", ["open"]), - ), - "A provider-observed open or updated pull request", -); - -export const safelyAbandoned: TargetDefinition = marked( - "safely-abandoned", - all( - fact("delivery", ["discarded"]), - fact("workspace", ["abandoned", "absent"]), - ), - "The selected delivery is explicitly and safely abandoned", -); +export const softwareDeliveryEvidence: EvidenceDefinition[] = [ + { id: "plan-evidence", subject: "plan", kind: "artifact" }, + { + id: "publication-evidence", + subject: "publication", + kind: "provider-observation", + }, +]; export interface TrustedStep { id: string; priority: number; } -export const runToPublishedPR: TrustedStep[] = [ - { id: "plan.create", priority: 35 }, - { id: "plan.validate", priority: 40 }, - { id: "plan.invalidate", priority: 41 }, - { id: "plan.amend", priority: 42 }, - { id: "plan.approve", priority: 45 }, - { id: "plan.approve-amendment", priority: 46 }, - { id: "plan.activate", priority: 50 }, - { id: "workspace.cut", priority: 52 }, - { id: "workspace.activate", priority: 53 }, - { id: "workspace.sync", priority: 58 }, - { id: "gate.build.record", priority: 61 }, - { id: "gate.test.record", priority: 62 }, - { id: "gate.review.record", priority: 63 }, - { id: "gate.change.record", priority: 64 }, - { id: "gate.journey.record", priority: 64 }, - { id: "evidence.visual.attach", priority: 66 }, - { id: "delivery.slice.advance", priority: 68 }, - { id: "publication.preview", priority: 72 }, - { id: "workspace.publish", priority: 75 }, - { id: "publication.execute", priority: 76 }, - { id: "publication.observe", priority: 77 }, - { id: "publication.correct", priority: 80 }, - { id: "workspace.reconcile", priority: 2 }, - { id: "publication.reconcile", priority: 1 }, -]; - -export const runWithAbandonment: TrustedStep[] = [ - ...runToPublishedPR, - { id: "plan.abandon", priority: 31 }, -]; - export function inbox(path: string): EntryInputDefinition { return { id: "plan", @@ -119,71 +70,26 @@ export function inbox(path: string): EntryInputDefinition { }; } -export function trustedOperators(steps: TrustedStep[]): OperatorDefinition[] { - return steps.map((step) => - operator(step.id, { - binding: { reference: `${bindingPrefix}${step.id}`, version: "1" }, - }), - ); +export function trustedOperator(step: TrustedStep): OperatorDefinition { + return operator(step.id, { + binding: { reference: `${bindingPrefix}${step.id}`, version: "1" }, + }); } -export function trustedTransitions( - steps: TrustedStep[], -): TransitionDefinition[] { - return steps.map((step) => - transition(step.id, step.id, { - guard: always, - target: always, - priority: step.priority, - }), - ); -} - -export function productDeliveryFlow(input: { - id: string; - version: string; - steps?: TrustedStep[]; - entries: EntryDefinition[]; - description?: string; -}): FlowDefinition { - const steps = input.steps ?? runToPublishedPR; - return { - id: input.id, - version: input.version, - description: input.description, - declarations: { input_resolvers: [planInboxResolver] }, - facets: softwareDeliveryFacets, - evidence: [ - { id: "plan-evidence", subject: "plan", kind: "artifact" }, - { - id: "publication-evidence", - subject: "publication", - kind: "provider-observation", - }, - ], - operators: trustedOperators(steps), - transitions: trustedTransitions(steps), - targets: input.entries.some((value) => value.target === safelyAbandoned.id) - ? [publishedPR, safelyAbandoned] - : [publishedPR], - entries: input.entries, - }; +export function trustedOperators(steps: TrustedStep[]): OperatorDefinition[] { + return steps.map(trustedOperator); } -export function runEntry(path = ".boatstack/plans/inbox"): EntryDefinition { - return entry( - "run", - "published-pr", - [inbox(path)], - "Implement one approved repository plan and publish a pull request", - ); +export function trustedTransition(step: TrustedStep): TransitionDefinition { + return transition(step.id, step.id, { + guard: always, + target: always, + priority: step.priority, + }); } -export function abandonEntry(path = ".boatstack/plans/inbox"): EntryDefinition { - return entry( - "abandon", - "safely-abandoned", - [inbox(path)], - "Explicitly abandon the selected delivery before starting different work", - ); +export function trustedTransitions( + steps: TrustedStep[], +): TransitionDefinition[] { + return steps.map(trustedTransition); } diff --git a/packages/boatstack/bin/boatstack-flow-frontend.mjs b/packages/boatstack/bin/boatstack-flow-frontend.mjs index 22143ef..afe8d05 100755 --- a/packages/boatstack/bin/boatstack-flow-frontend.mjs +++ b/packages/boatstack/bin/boatstack-flow-frontend.mjs @@ -41,6 +41,8 @@ async function compile(input) { throw new Error("Flow source contains invalid TypeScript syntax"); } const imports = new Map(); + const locals = new Map(); + const declarations = []; let exported; for (const statement of sourceFile.statements) { @@ -68,6 +70,13 @@ async function compile(input) { exported = statement.expression; continue; } + if (ts.isVariableStatement(statement)) { + if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) { + throw new Error("Flow local declarations must be const"); + } + declarations.push(statement.declarationList); + continue; + } if (ts.isEmptyStatement(statement)) continue; throw new Error("Flow source may contain only trusted imports and one default export"); } @@ -84,6 +93,7 @@ async function compile(input) { if (node.kind === ts.SyntaxKind.NullKeyword) return null; if (ts.isIdentifier(node)) { if (node.text === "undefined") return undefined; + if (locals.has(node.text)) return structuredClone(locals.get(node.text)); if (!imports.has(node.text)) throw new Error(`Flow identifier is not a trusted SDK import: ${node.text}`); return structuredClone(imports.get(node.text)); } @@ -111,6 +121,18 @@ async function compile(input) { throw new Error(`Flow expression is not declarative: ${ts.SyntaxKind[node.kind]}`); }; + for (const declarationList of declarations) { + for (const declaration of declarationList.declarations) { + if (!ts.isIdentifier(declaration.name) || !declaration.initializer) { + throw new Error("Flow const declarations require one static identifier and initializer"); + } + if (locals.has(declaration.name.text) || imports.has(declaration.name.text)) { + throw new Error(`Flow identifier is declared more than once: ${declaration.name.text}`); + } + locals.set(declaration.name.text, evaluate(declaration.initializer)); + } + } + const value = evaluate(exported); if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error("Flow default export must lower to a Control Program IR object"); diff --git a/release-notes/2026-08-13-repository-owned-flow-control-law.md b/release-notes/2026-08-13-repository-owned-flow-control-law.md new file mode 100644 index 0000000..b4116d5 --- /dev/null +++ b/release-notes/2026-08-13-repository-owned-flow-control-law.md @@ -0,0 +1,3 @@ +### Make Flow lifecycle policy repository-owned + +Flow targets now remain repository-defined runtime identities, and the software-delivery SDK exports trusted mechanisms instead of built-in run or abandonment lifecycle presets. From 88c6a8a7d224637a19f22bd6466bfbd86e127456 Mon Sep 17 00:00:00 2001 From: bigboateng Date: Thu, 13 Aug 2026 10:24:56 +0100 Subject: [PATCH 2/3] Version target-bound wire schemas --- boatstack/delivery/control.go | 2 +- boatstack/delivery/program_manifest_test.go | 1 + boatstack/internal/softwaredelivery/durable/state.go | 2 +- .../softwaredelivery/durable/state_schema_test.go | 10 ++++++++++ .../softwaredelivery/effects/cas_integration_test.go | 3 ++- boatstack/internal/softwaredelivery/effects/journal.go | 2 +- .../softwaredelivery/effects/journal_schema_test.go | 4 ++-- .../internal/softwaredelivery/effects/recovery_test.go | 4 ++-- boatstack/internal/softwaredelivery/model/state.go | 2 +- .../internal/softwaredelivery/model/state_test.go | 8 ++++++++ .../internal/softwaredelivery/protocol/admission.go | 2 +- .../internal/softwaredelivery/protocol/journal.go | 2 +- .../internal/softwaredelivery/protocol/receipt.go | 2 +- .../internal/softwaredelivery/surfaces/protocol.go | 2 +- 14 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 boatstack/internal/softwaredelivery/durable/state_schema_test.go diff --git a/boatstack/delivery/control.go b/boatstack/delivery/control.go index ed83c49..9797d3e 100644 --- a/boatstack/delivery/control.go +++ b/boatstack/delivery/control.go @@ -132,7 +132,7 @@ const ( FacetObjective = model.FacetObjective ) -const ProgramSchemaVersion = 4 +const ProgramSchemaVersion = 5 func KernelEffectCapabilities(transition Transition) []Capability { return catalog.KernelEffectCapabilities(transition) diff --git a/boatstack/delivery/program_manifest_test.go b/boatstack/delivery/program_manifest_test.go index a9ab76c..d9d2dbe 100644 --- a/boatstack/delivery/program_manifest_test.go +++ b/boatstack/delivery/program_manifest_test.go @@ -179,6 +179,7 @@ func TestProgramManifestNamespaceAndCompatibilityBoundary(t *testing.T) { code delivery.ProgramErrorCode }{ {"unsupported-schema", func(value *delivery.ProgramManifest) { value.SchemaVersion = delivery.ProgramSchemaVersion + 1 }, runtimeFixture(), delivery.ProgramSchemaUnsupported}, + {"prior-schema", func(value *delivery.ProgramManifest) { value.SchemaVersion = delivery.ProgramSchemaVersion - 1 }, runtimeFixture(), delivery.ProgramSchemaUnsupported}, {"invalid-schema", func(value *delivery.ProgramManifest) { value.SchemaVersion = 0 }, runtimeFixture(), delivery.ProgramInvalid}, {"runtime-too-old", func(value *delivery.ProgramManifest) { value.RequiresRuntime = ">=2.0.0" }, runtimeFixture(), delivery.RuntimeTooOld}, {"malformed-runtime", func(value *delivery.ProgramManifest) { value.RequiresRuntime = "^1" }, runtimeFixture(), delivery.ProgramInvalid}, diff --git a/boatstack/internal/softwaredelivery/durable/state.go b/boatstack/internal/softwaredelivery/durable/state.go index 1780c28..e45cd99 100644 --- a/boatstack/internal/softwaredelivery/durable/state.go +++ b/boatstack/internal/softwaredelivery/durable/state.go @@ -12,7 +12,7 @@ import ( "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) -const StateSchemaVersion = 3 +const StateSchemaVersion = 4 type GateEvidence struct { Gate string `json:"gate"` diff --git a/boatstack/internal/softwaredelivery/durable/state_schema_test.go b/boatstack/internal/softwaredelivery/durable/state_schema_test.go new file mode 100644 index 0000000..dd214ac --- /dev/null +++ b/boatstack/internal/softwaredelivery/durable/state_schema_test.go @@ -0,0 +1,10 @@ +package durable + +import "testing" + +func TestStateRejectsPriorObjectiveSchema(t *testing.T) { + state := State{SchemaVersion: StateSchemaVersion - 1} + if err := state.Validate(); err == nil { + t.Fatal("prior objective state schema was accepted") + } +} diff --git a/boatstack/internal/softwaredelivery/effects/cas_integration_test.go b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go index 155854c..63779ac 100644 --- a/boatstack/internal/softwaredelivery/effects/cas_integration_test.go +++ b/boatstack/internal/softwaredelivery/effects/cas_integration_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "os" "path/filepath" "strings" @@ -142,7 +143,7 @@ func TestConcurrentApplyConsumesOneRevisionExactlyOnce(t *testing.T) { } committedRaw, err := os.ReadFile(committedJournals[0]) if err != nil || !bytes.Contains(committedRaw, []byte(committed.Receipt.ID)) || !bytes.Contains(committedRaw, []byte("committed_effects")) || - !bytes.Contains(committedRaw, []byte(`"schema_version": 8`)) || !bytes.Contains(committedRaw, []byte(`"allowed_state_facets"`)) { + !bytes.Contains(committedRaw, []byte(fmt.Sprintf(`"schema_version": %d`, protocol.JournalSchemaVersion))) || !bytes.Contains(committedRaw, []byte(`"allowed_state_facets"`)) { t.Fatalf("committed journal lacks its complete transition fact: %v %q", err, committedRaw) } // Simulate a crash after canonical commit but before the passive receipt diff --git a/boatstack/internal/softwaredelivery/effects/journal.go b/boatstack/internal/softwaredelivery/effects/journal.go index 0eacef7..87e7e98 100644 --- a/boatstack/internal/softwaredelivery/effects/journal.go +++ b/boatstack/internal/softwaredelivery/effects/journal.go @@ -37,7 +37,7 @@ type journalRecord struct { Admission protocol.Admission `json:"admission"` TransitionID catalog.TransitionID `json:"transition_id"` TransitionClass catalog.EventClass `json:"transition_class"` - // AllowedStateFacets preserves the current schema-8 record shape. Recovery + // AllowedStateFacets preserves the declared record shape. Recovery // authority is reconstructed from Admission.RequiredCapabilities instead. AllowedStateFacets []model.StateFacet `json:"allowed_state_facets"` ReconcilesProgram bool `json:"reconciles_program,omitempty"` diff --git a/boatstack/internal/softwaredelivery/effects/journal_schema_test.go b/boatstack/internal/softwaredelivery/effects/journal_schema_test.go index 1faac72..0c487fd 100644 --- a/boatstack/internal/softwaredelivery/effects/journal_schema_test.go +++ b/boatstack/internal/softwaredelivery/effects/journal_schema_test.go @@ -7,7 +7,7 @@ import ( ) func TestInstallationUpdateKeepsCurrentJournalSchema(t *testing.T) { - if protocol.JournalSchemaVersion != 8 { - t.Fatalf("journal schema = %d, want current schema 8 for in-flight installation updates", protocol.JournalSchemaVersion) + if protocol.JournalSchemaVersion != 9 { + t.Fatalf("journal schema = %d, want current schema 9 for target-bound transaction records", protocol.JournalSchemaVersion) } } diff --git a/boatstack/internal/softwaredelivery/effects/recovery_test.go b/boatstack/internal/softwaredelivery/effects/recovery_test.go index d62dbda..833b784 100644 --- a/boatstack/internal/softwaredelivery/effects/recovery_test.go +++ b/boatstack/internal/softwaredelivery/effects/recovery_test.go @@ -161,8 +161,8 @@ func TestRestartRecoveryRestoresPriorStateAndCommitsRecoveryRevision(t *testing. if err != nil { t.Fatal(err) } - if !bytes.Contains(basePending, []byte(`"schema_version": 8`)) || !bytes.Contains(basePending, []byte(`"allowed_state_facets"`)) { - t.Fatalf("pending update journal is not current-main schema 8: %s", basePending) + if !bytes.Contains(basePending, []byte(`"schema_version": 9`)) || !bytes.Contains(basePending, []byte(`"allowed_state_facets"`)) { + t.Fatalf("pending update journal is not target-bound schema 9: %s", basePending) } restartedInvocation, err := resolver.ResolveInvocation(ctx, repository, "cli", "after-restart") diff --git a/boatstack/internal/softwaredelivery/model/state.go b/boatstack/internal/softwaredelivery/model/state.go index 66fe73e..7a7d616 100644 --- a/boatstack/internal/softwaredelivery/model/state.go +++ b/boatstack/internal/softwaredelivery/model/state.go @@ -9,7 +9,7 @@ import ( "time" ) -const SnapshotSchemaVersion = 4 +const SnapshotSchemaVersion = 5 type ProtocolPhase string diff --git a/boatstack/internal/softwaredelivery/model/state_test.go b/boatstack/internal/softwaredelivery/model/state_test.go index d2c16e6..b65fbf2 100644 --- a/boatstack/internal/softwaredelivery/model/state_test.go +++ b/boatstack/internal/softwaredelivery/model/state_test.go @@ -61,6 +61,14 @@ func TestCanonicalizeIsDeterministicAndControlSensitive(t *testing.T) { } } +func TestCanonicalizeRejectsPriorObjectiveSchema(t *testing.T) { + observation := testObservation(PhaseObserved) + observation.SchemaVersion = SnapshotSchemaVersion - 1 + if _, err := Canonicalize(observation); err == nil { + t.Fatal("prior objective snapshot schema was accepted") + } +} + func TestCanonicalizeRejectsKnownFactWithoutEvidence(t *testing.T) { // control-law: controlling-facts-carry-evidence observation := testObservation(PhaseObserved) diff --git a/boatstack/internal/softwaredelivery/protocol/admission.go b/boatstack/internal/softwaredelivery/protocol/admission.go index 2bb4759..31463f8 100644 --- a/boatstack/internal/softwaredelivery/protocol/admission.go +++ b/boatstack/internal/softwaredelivery/protocol/admission.go @@ -9,7 +9,7 @@ import ( "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) -const AdmissionSchemaVersion = 5 +const AdmissionSchemaVersion = 6 type Admission struct { SchemaVersion int `json:"schema_version"` diff --git a/boatstack/internal/softwaredelivery/protocol/journal.go b/boatstack/internal/softwaredelivery/protocol/journal.go index 261a231..4f00243 100644 --- a/boatstack/internal/softwaredelivery/protocol/journal.go +++ b/boatstack/internal/softwaredelivery/protocol/journal.go @@ -2,4 +2,4 @@ package protocol // JournalSchemaVersion identifies the transaction record that embeds an exact // prescription-bound admission. -const JournalSchemaVersion = 8 +const JournalSchemaVersion = 9 diff --git a/boatstack/internal/softwaredelivery/protocol/receipt.go b/boatstack/internal/softwaredelivery/protocol/receipt.go index 62e3dfe..83d0d79 100644 --- a/boatstack/internal/softwaredelivery/protocol/receipt.go +++ b/boatstack/internal/softwaredelivery/protocol/receipt.go @@ -12,7 +12,7 @@ import ( "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) -const ReceiptSchemaVersion = 8 +const ReceiptSchemaVersion = 9 type TransitionFactKind string diff --git a/boatstack/internal/softwaredelivery/surfaces/protocol.go b/boatstack/internal/softwaredelivery/surfaces/protocol.go index e10067f..61b8a89 100644 --- a/boatstack/internal/softwaredelivery/surfaces/protocol.go +++ b/boatstack/internal/softwaredelivery/surfaces/protocol.go @@ -14,7 +14,7 @@ import ( "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" ) -const SchemaVersion = 6 +const SchemaVersion = 7 var flowContextIdentity = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) From 8f83c4f369f626c3fc514ba85c36b03562bd8eae Mon Sep 17 00:00:00 2001 From: bigboateng Date: Thu, 13 Aug 2026 10:41:54 +0100 Subject: [PATCH 3/3] Preserve trusted bootstrap paths for repository targets --- boatstack/flow/softwaredelivery/definition.go | 2 +- .../flow/softwaredelivery/definition_test.go | 11 ++++ .../catalog/objective_contract.go | 16 ++++- .../softwaredelivery/catalog/transition.go | 6 +- .../softwaredelivery/supervisor/supervisor.go | 7 ++- .../boatstack-v2-transition-catalog.md | 60 +++++++++---------- 6 files changed, 67 insertions(+), 35 deletions(-) diff --git a/boatstack/flow/softwaredelivery/definition.go b/boatstack/flow/softwaredelivery/definition.go index 7aa4b87..b7721eb 100644 --- a/boatstack/flow/softwaredelivery/definition.go +++ b/boatstack/flow/softwaredelivery/definition.go @@ -176,7 +176,7 @@ func objectiveContractForEntry(compiled controlprogram.Compiled, base delivery.P return EntryObjective{ TargetID: target, TrustedClass: matches[0].TargetID, - Contract: delivery.ObjectiveContract{TargetID: target, Conditions: conditions}, + Contract: delivery.ObjectiveContract{TargetID: target, TrustedClass: matches[0].TargetID, Conditions: conditions}, }, nil } diff --git a/boatstack/flow/softwaredelivery/definition_test.go b/boatstack/flow/softwaredelivery/definition_test.go index 01112f3..c14c430 100644 --- a/boatstack/flow/softwaredelivery/definition_test.go +++ b/boatstack/flow/softwaredelivery/definition_test.go @@ -9,6 +9,7 @@ import ( "github.com/operatorstack/boatstack/boatstack/core" "github.com/operatorstack/boatstack/boatstack/delivery" softwareflow "github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" ) func compiledFlow(t *testing.T, guard controlprogram.Predicate) (controlprogram.Compiled, softwareflow.Resolver) { @@ -65,6 +66,16 @@ func TestTrustedFlowLowersThroughStandardStateEffectBoundary(t *testing.T) { if program.Summary().RuntimeTransitionCount != 1 { t.Fatalf("runtime transition count = %d", program.Summary().RuntimeTransitionCount) } + objective := model.Objective{ID: "objective", TargetID: "published-pr", TrustedClass: model.ObjectiveOpenPR, DeliveryID: "delivery"} + bootstrap, ok := program.RuntimeRegistry().Lookup("objective.bind") + if !ok || !bootstrap.SupportsObjective(objective) || !program.RuntimeObjectiveContracts().Accepts(objective) { + t.Fatalf("repository target lost trusted bootstrap correlation: transition=%#v found=%v", bootstrap, ok) + } + spoofed := objective + spoofed.TrustedClass = model.ObjectiveApprovedPlan + if program.RuntimeObjectiveContracts().Accepts(spoofed) { + t.Fatal("repository target accepted a caller-supplied trusted class") + } } func TestRepositoryGuardCanOnlyStrengthenTrustedBinding(t *testing.T) { diff --git a/boatstack/internal/softwaredelivery/catalog/objective_contract.go b/boatstack/internal/softwaredelivery/catalog/objective_contract.go index 8cdbc90..66cb581 100644 --- a/boatstack/internal/softwaredelivery/catalog/objective_contract.go +++ b/boatstack/internal/softwaredelivery/catalog/objective_contract.go @@ -11,8 +11,9 @@ import ( // Extension conditions are conjunctive and therefore can only narrow the // terminal set. type ObjectiveContract struct { - TargetID model.TargetID `json:"target_id"` - Conditions []FacetCondition `json:"conditions"` + TargetID model.TargetID `json:"target_id"` + TrustedClass model.TargetID `json:"trusted_class"` + Conditions []FacetCondition `json:"conditions"` } type ObjectiveContracts map[model.TargetID]ObjectiveContract @@ -32,6 +33,12 @@ func NewObjectiveContracts(base []ObjectiveContract, extension map[model.TargetI if !contract.TargetID.Valid() || len(contract.Conditions) == 0 { return nil, fmt.Errorf("objective contract requires a valid objective and conditions") } + if contract.TrustedClass == "" { + contract.TrustedClass = contract.TargetID + } + if !contract.TrustedClass.Valid() { + return nil, fmt.Errorf("objective contract %q requires a valid trusted class", contract.TargetID) + } if _, exists := contracts[contract.TargetID]; exists { return nil, fmt.Errorf("duplicate objective contract %q", contract.TargetID) } @@ -58,6 +65,11 @@ func NewObjectiveContracts(base []ObjectiveContract, extension map[model.TargetI return contracts, nil } +func (c ObjectiveContracts) Accepts(objective model.Objective) bool { + contract, ok := c[objective.TargetID] + return ok && contract.TrustedClass == objective.TrustedObjectiveClass() +} + func (c ObjectiveContracts) Matches(snapshot model.Snapshot, objective model.Objective) bool { if snapshot.Objective.Status != model.FactKnown || snapshot.Objective.Value != objective { return false diff --git a/boatstack/internal/softwaredelivery/catalog/transition.go b/boatstack/internal/softwaredelivery/catalog/transition.go index b924e52..a4a84a2 100644 --- a/boatstack/internal/softwaredelivery/catalog/transition.go +++ b/boatstack/internal/softwaredelivery/catalog/transition.go @@ -346,8 +346,12 @@ func (t Transition) SupportsObjective(objective model.Objective) bool { if len(t.TargetIDs) == 0 { return true } + targetID := objective.TargetID + if t.Origin.Kind == OriginCoreSystem { + targetID = objective.TrustedObjectiveClass() + } for _, kind := range t.TargetIDs { - if kind == objective.TargetID { + if kind == targetID { return true } } diff --git a/boatstack/internal/softwaredelivery/supervisor/supervisor.go b/boatstack/internal/softwaredelivery/supervisor/supervisor.go index cc9b8f6..434a070 100644 --- a/boatstack/internal/softwaredelivery/supervisor/supervisor.go +++ b/boatstack/internal/softwaredelivery/supervisor/supervisor.go @@ -42,10 +42,15 @@ func New(registry catalog.Registry, contracts catalog.ObjectiveContracts) Superv func (s Supervisor) Resolve(snapshot model.Snapshot, objective model.Objective, authority catalog.AuthoritySet, requested catalog.TransitionID) Decision { base := Decision{SnapshotFingerprint: snapshot.Fingerprint} objectiveAbsent := snapshot.Objective.Status == model.FactAbsent - if (objective.Validate() != nil && !objectiveAbsent) || snapshot.Fingerprint == "" { + objectiveProvided := objective.ID != "" || objective.TargetID != "" || objective.TrustedClass != "" || objective.DeliveryID != "" + if (objectiveProvided && objective.Validate() != nil) || (!objectiveProvided && !objectiveAbsent) || snapshot.Fingerprint == "" { base.Kind, base.Reason = DecisionUnresolved, "objective or canonical snapshot is invalid" return base } + if objectiveProvided && !s.contracts.Accepts(objective) { + base.Kind, base.Reason = DecisionRefused, "objective target and trusted class do not match the compiled program" + return base + } if snapshot.Terminal.Status != model.FactKnown || snapshot.Phase.Status != model.FactKnown { base.Kind, base.Reason = DecisionUnresolved, "terminal or phase evidence is not known" return base diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md index b847da7..eaa9046 100644 --- a/docs/architecture/boatstack-v2-transition-catalog.md +++ b/docs/architecture/boatstack-v2-transition-catalog.md @@ -11,12 +11,12 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w | `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` | | `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `repository.write` | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` | | `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `repository.write` | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` | -| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` | +| `delivery.slice.advance` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` | | `engagement.begin` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` | | `engagement.release` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` | | `engagement.renew` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | `product.mutate`, `repository.write` | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` | -| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | `product.mutate`, `repository.write` | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` | -| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` | +| `evidence.approval.revoke` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | `product.mutate`, `repository.write` | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` | +| `evidence.visual.attach` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` | | `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` | @@ -30,30 +30,30 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w | `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `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`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` | -| `gate.build.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` | -| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` | -| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` | -| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` | -| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` | +| `gate.build.record` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` | +| `gate.change.record` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` | +| `gate.journey.record` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` | +| `gate.review.record` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` | +| `gate.test.record` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` | | `installation.initialize` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` | | `installation.reconcile-update` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `accept_obligation_change*` | `installation` | `verifier:fresh-observation:installation.reconcile-update` | `recovery.rollback` | `declared-neutral` | | `installation.update` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` | | `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | `repository.write` | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` | | `objective.bind` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `product.mutate`, `repository.write` | `target_id*`, `delivery_id*` | `objective` | `verifier:fresh-observation:objective.bind` | `recovery.resume` | `declared-neutral` | -| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` | -| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` | -| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` | -| `plan.approve` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | -| `plan.approve-amendment` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | -| `plan.create` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | -| `plan.invalidate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | -| `plan.validate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | -| `publication.abandon` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | -| `publication.correct` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | -| `publication.execute` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | -| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` | -| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `publication.prepare`, `repository.write` | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` | -| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `command.execute`, `product.mutate`, `repository.write` | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` | +| `plan.abandon` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` | +| `plan.activate` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` | +| `plan.amend` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` | +| `plan.approve` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` | +| `plan.approve-amendment` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` | +| `plan.create` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `product.mutate`, `repository.write` | `source_path*`, `delivery_id*`, `source_fingerprint` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` | +| `plan.invalidate` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` | +| `plan.validate` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | `product.mutate`, `repository.write` | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` | +| `publication.abandon` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` | +| `publication.correct` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` | +| `publication.execute` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `command.execute`, `product.mutate`, `publication.publish`, `repository.write` | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` | +| `publication.observe` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `command.execute`, `product.mutate`, `repository.write` | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` | +| `publication.preview` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `publication.prepare`, `repository.write` | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` | +| `publication.reconcile` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `command.execute`, `product.mutate`, `repository.write` | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` | | `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` | | `recovery.resume` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` | | `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `repository.write` | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` | @@ -62,13 +62,13 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w | `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | OBJECTIVE_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` | | `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` | | `runtime.replace` | core-system:`boatstack.core@1.0.0`
`753278c89b6b249319a2be379fbea290d8f026ffb4fb13a5ec61fb93e632e1b7` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `repository.write` | `source_revision*`, `runtime_version*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` | -| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` | -| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` | -| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` | -| `workspace.cut` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | -| `workspace.publish` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | -| `workspace.reap` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | -| `workspace.reconcile` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `product.mutate`, `repository.write` | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | -| `workspace.sync` | control-program:`boatstack.standard@1.0.0`
`957310ec96369e38a231b62c118bd13155aa51146332ac4e8f49914b1223c4ba` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` | +| `workspace.abandon` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` | +| `workspace.activate` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` | +| `workspace.cleanup` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` | +| `workspace.cut` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` | +| `workspace.publish` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `product.mutate`, `repository.write` | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` | +| `workspace.reap` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `command.execute`, `product.mutate`, `repository.write` | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` | +| `workspace.reconcile` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | PROGRAM_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `product.mutate`, `repository.write` | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` | +| `workspace.sync` | control-program:`boatstack.standard@1.0.0`
`67b063fc0720a3a4d82d2e97639d473d13737922f0f799b11ab5790fc3dc6980` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `command.execute`, `product.mutate`, `repository.write` | `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`.