diff --git a/.github/reviews/convergence-boundary.receipt.json b/.github/reviews/convergence-boundary.receipt.json index 1d54eb69..981e7801 100644 --- a/.github/reviews/convergence-boundary.receipt.json +++ b/.github/reviews/convergence-boundary.receipt.json @@ -1,4 +1,4 @@ { - "reviewed_tree": "638048ded4504ed3df431637e803348cc2100340", - "program_fingerprint": "ddbd77f1cbc842b3dffcb8e54b53ddcc624a998fb9c5a9ace15028580ab87967" + "reviewed_tree": "cdfe3ed9fd234a119274ac44f54583e993b2e073", + "program_fingerprint": "2277c979a06ee984c09aa32b2ed3d8886f1ae685a647274185a71a75a1a3961c" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5140e145..8aa5ce04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,11 +29,16 @@ jobs: cache-dependency-path: boatstack/go.sum - name: Build TypeScript frontends and documentation run: npm ci && npm run test:flow-sdk && npm run docs:check - - name: Prove frontend canonical equivalence + # Required mode forbids the frontend-absent skip path: every + # frontend-dependent conformance proof (canonical equivalence, sugar + # equivalence, and the no-execution/import/expression boundaries) must + # actually run here, because the plain test jobs never install the + # frontend and would skip them silently. + - name: Prove frontend conformance in required mode working-directory: boatstack env: BOATSTACK_REQUIRE_FLOW_FRONTEND: '1' - run: go test ./controlprogram -run 'TestTypeScriptDSLAndRawIRHaveOneCanonicalFingerprint|TestRepositoryOwnedSoftwareDeliveryFlowsShareOneRuntime|TestSoftwareDeliverySugar' + run: go test ./controlprogram -run 'TestTypeScriptDSLAndRawIRHaveOneCanonicalFingerprint|TestRepositoryOwnedSoftwareDeliveryFlowsShareOneRuntime|TestSoftwareDeliverySugar|TestTypeScriptFrontend|TestDomainNeutral' component: name: component-${{ matrix.name }} diff --git a/boatstack/delivery_controller_work_loop_test.go b/boatstack/delivery_controller_work_loop_test.go new file mode 100644 index 00000000..7cd9217d --- /dev/null +++ b/boatstack/delivery_controller_work_loop_test.go @@ -0,0 +1,330 @@ +package boatstack + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/catalog" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/effects" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/engine" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/foregroundwork" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/model" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/plant" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/ports" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/protocol" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/supervisor" + "github.com/operatorstack/boatstack/boatstack/internal/softwaredelivery/surfaces" + general "github.com/operatorstack/boatstack/boatstack/kernel" +) + +const workLoopProgramFingerprint = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +var workLoopProgramIdentity = protocol.ProgramIdentity{ID: "test.synthetic", Version: "1.0.0", Fingerprint: workLoopProgramFingerprint} + +func workLoopObservation() model.Observation { + e := model.Evidence{Source: "fixture", Fingerprint: "source", ObservedAt: time.Unix(20, 0).UTC()} + configurationEvidence := model.Evidence{Source: "configuration:/repo/.boatstack/project.json", Fingerprint: "config-fingerprint", ObservedAt: time.Unix(20, 0).UTC()} + return model.Observation{ + SchemaVersion: model.SnapshotSchemaVersion, StateRevision: 1, + Phase: model.Known(model.PhaseObserved, e), Engagement: model.Known(model.EngagementActive, e), Delivery: model.Known(model.DeliveryActive, e), Workspace: model.Known(model.WorkspaceActive, e), + Plan: model.Known(model.PlanApproved, e), Configuration: model.Known(model.ConfigurationVerified, configurationEvidence), Runtime: model.Known(model.RuntimeVerified, e), + 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", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"}, e), ObservedAt: time.Unix(20, 0).UTC(), + ProgramFacts: map[string]model.Fact[string]{"test.synthetic.stage": model.Known("start", e)}, + } +} + +// workLoopObserver projects one fixed plant observation onto whichever +// invocation identity the controller resolved, so every resolve sees the same +// canonical state. +type workLoopObserver struct{} + +func (workLoopObserver) Observe(_ context.Context, request ports.ObservationRequest) (model.Observation, error) { + observation := workLoopObservation() + observation.Invocation = request.Invocation + return observation, nil +} + +type workLoopPrepared struct{ transition catalog.Transition } + +func (p workLoopPrepared) Manifest() []ports.ResourceMutation { return nil } +func (p workLoopPrepared) ChangedStateFacets() []model.StateFacet { + return []model.StateFacet{model.StateFacetControl} +} +func (p workLoopPrepared) CommittedEffects() []protocol.EffectFact { + return []protocol.EffectFact{{ + Kind: protocol.EffectResourceMutation, EffectID: p.transition.Effect, Owner: p.transition.Owner, Resource: p.transition.OwnedResources[0], + Target: "/test/state.json", Operation: "update", PriorFingerprint: strings.Repeat("1", 64), ResultingFingerprint: strings.Repeat("2", 64), + }} +} +func (p workLoopPrepared) VerificationInvocation() (model.InvocationContext, bool) { + return model.InvocationContext{}, false +} +func (p workLoopPrepared) Execute(context.Context) (ports.EffectResult, error) { + return ports.EffectResult{Settlement: ports.EffectSettled}, nil +} +func (p workLoopPrepared) Rollback(context.Context) error { return nil } + +type workLoopDriver struct{} + +func (workLoopDriver) Prepare(_ context.Context, _ protocol.Admission, transition catalog.Transition) (ports.PreparedEffect, error) { + return workLoopPrepared{transition: transition}, nil +} + +func workLoopWorkContract(t *testing.T) *catalog.WorkContract { + t.Helper() + instructionDigest := sha256.Sum256([]byte("Inspect the incident.")) + work := &catalog.WorkContract{ + ID: "diagnose", InstructionPath: "instructions.md", InstructionSHA256: hex.EncodeToString(instructionDigest[:]), InstructionContent: "Inspect the incident.", + Outputs: []catalog.WorkOutput{{ID: "diagnosis", Path: "diagnosis.md", MediaType: "text/markdown", Required: true, MaxBytes: 1024}}, + } + fingerprint, err := general.Fingerprint(struct { + ID string `json:"id"` + InstructionPath string `json:"instruction_path"` + InstructionSHA256 string `json:"instruction_sha256"` + InstructionContent string `json:"instruction_content"` + Inputs []catalog.WorkInput `json:"inputs,omitempty"` + Outputs []catalog.WorkOutput `json:"outputs"` + }{work.ID, work.InstructionPath, work.InstructionSHA256, work.InstructionContent, work.Inputs, work.Outputs}) + if err != nil { + t.Fatal(err) + } + work.Fingerprint = fingerprint + return work +} + +func workLoopRegistry(t *testing.T) catalog.Registry { + t.Helper() + identity := []string{"repository-id", "git-common-id", "worktree-id"} + interruption := func(recovery catalog.TransitionID) catalog.InterruptionContract { + return catalog.InterruptionContract{ + Points: []string{"after-effect"}, PartialState: []string{"effect-possibly-installed"}, Detection: "test-observation", + ResumeContract: "test-resume", RollbackContract: "test-rollback", CompensationContract: "not-required", + Recovery: recovery, RecoveryAuthority: "test-authority", ResumptionPredicate: "test-resumption", + } + } + activePhase := string(model.PhaseActive) + frontierPhase := string(model.PhaseFrontier) + escalatedRecovery := string(model.RecoveryEscalated) + registry, err := catalog.New([]catalog.Transition{{ + ID: "test.advance", Version: 1, Class: catalog.EventOwnedLocal, + Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: workLoopProgramFingerprint}, Owner: "test.synthetic", SelectionClass: catalog.SelectionProgramProgress, + SourcePhases: []model.ProtocolPhase{model.PhaseObserved}, TargetPhases: []model.ProtocolPhase{model.PhaseActive}, + RequiredIdentity: identity, Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, DeclaredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite, catalog.CapabilityCommandExecute}, RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"state", "foreground-work-diagnose"}, OwnedFacets: []model.StateFacet{model.StateFacetControl}, StateEffect: catalog.StateEffect{Kind: catalog.StateEffectAssignments, Assignments: []catalog.StateAssignment{{Facet: "phase", Value: &activePhase}}}, Effect: "test.advance", LocalEffects: []catalog.EffectID{"test.advance"}, Idempotent: true, + Work: workLoopWorkContract(t), + Prescription: catalog.Prescription{Operation: "test.advance", ExpectedPostcondition: "active"}, SourcePredicate: "observed", AdmissionPredicate: "exact-admission", TargetPredicate: "active", Verifier: "fresh-active", + SourceConditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"start"}}}, + TargetConditions: []catalog.FacetCondition{{Facet: model.FacetName("test.synthetic.stage"), Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}}, + Interruption: interruption("test.recover"), Reversibility: catalog.Reversible, TerminalEffect: "none", + PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "test", Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}, Priority: 1, + }, { + ID: "test.recover", Version: 1, Class: catalog.EventRecovery, + Origin: catalog.TransitionOrigin{Kind: catalog.OriginControlProgram, ID: "test.synthetic", Version: "1.0.0", ManifestFingerprint: workLoopProgramFingerprint}, Owner: "test.synthetic", SelectionClass: catalog.SelectionProgramRecovery, + SourcePhases: []model.ProtocolPhase{model.PhaseRecovery}, TargetPhases: []model.ProtocolPhase{model.PhaseFrontier}, + RequiredIdentity: identity, Authority: []catalog.AuthorityClass{catalog.AuthorityRepository}, RequiredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite}, DeclaredCapabilities: []catalog.Capability{catalog.CapabilityRepositoryWrite, catalog.CapabilityCommandExecute}, RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"state"}, OwnedFacets: []model.StateFacet{model.StateFacetControl}, StateEffect: catalog.StateEffect{Kind: catalog.StateEffectAssignments, Assignments: []catalog.StateAssignment{{Facet: "phase", Value: &frontierPhase}, {Facet: "recovery", Value: &escalatedRecovery}}}, Effect: "test.recover", LocalEffects: []catalog.EffectID{"test.recover"}, Idempotent: true, + Prescription: catalog.Prescription{Operation: "test.recover", ExpectedPostcondition: "frontier"}, SourcePredicate: "recovery", AdmissionPredicate: "exact-recovery-admission", TargetPredicate: "frontier", Verifier: "fresh-frontier", + SourceConditions: []catalog.FacetCondition{{Facet: model.FacetRecovery, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.RecoveryReconcile)}}}, + TargetConditions: []catalog.FacetCondition{{Facet: model.FacetRecovery, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{string(model.RecoveryEscalated)}}}, + Interruption: interruption("test.recover"), Reversibility: catalog.Reversible, TerminalEffect: "none", + PrivacyClassification: "metadata-only", TelemetryClassification: "transition-receipt", CostClass: "test", Policy: catalog.PolicyContract{ObjectiveScope: catalog.ObjectiveScopeBoundExact}, Priority: 2, + }}) + if err != nil { + t.Fatal(err) + } + return registry +} + +func workLoopController(t *testing.T) (DeliveryController, string) { + t.Helper() + repository := t.TempDir() + command := exec.Command("git", "init", "-q", repository) + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("initialize fixture repository: %v\n%s", err, output) + } + resolver, err := plant.NewResolver(t.TempDir()) + if err != nil { + t.Fatal(err) + } + clock := effects.Clock{} + locker, err := effects.NewLocker(resolver) + if err != nil { + t.Fatal(err) + } + workManager, err := foregroundwork.NewManager(resolver, locker, clock, effects.NewRuntimeStore()) + if err != nil { + t.Fatal(err) + } + journal, err := effects.NewJournal(resolver, clock) + if err != nil { + t.Fatal(err) + } + receipts, err := effects.NewReceiptStore(resolver, clock) + if err != nil { + t.Fatal(err) + } + registry := workLoopRegistry(t) + contracts, err := catalog.NewObjectiveContracts([]catalog.ObjectiveContract{{ + 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) + } + observer := workLoopObserver{} + runtimeEngine, err := engine.New(registry, contracts, workLoopProgramIdentity, observer, clock, locker, journal, workLoopDriver{}, receipts) + if err != nil { + t.Fatal(err) + } + controller := DeliveryController{registry: registry, resolver: resolver, observer: observer, engine: runtimeEngine, clock: clock, work: workManager} + return controller, repository +} + +func TestForegroundWorkSuspendsAnswersAndResumesThroughSurface(t *testing.T) { + // control-law: foreground work suspends one resolve, binds answers to the + // exact current question, refuses apply until completed, and resumes the + // same run with the committed work evidence. + controller, repository := workLoopController(t) + ctx := context.Background() + now := time.Now().UTC() + authority := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ + ID: "auth", Class: catalog.AuthorityRepository, Subject: "repo", Fingerprint: "config-fingerprint", + IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour), + }}} + base := surfaces.Request{ + SchemaVersion: surfaces.SchemaVersion, Repository: repository, Host: "cli", CorrelationID: "work-loop", + FlowID: "run-one", + } + + resolve := base + resolve.Operation = surfaces.OperationResolve + resolve.ProgramID, resolve.ProgramFingerprint, resolve.EntryID = "test.synthetic", workLoopProgramFingerprint, "entry" + resolve.Objective = model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"} + resolve.Authority = authority + suspended, err := controller.Handle(ctx, resolve) + if err != nil { + t.Fatal(err) + } + if suspended.Work == nil || suspended.Work.Status != foregroundwork.StatusRequested || suspended.Decision == nil || suspended.Decision.Kind != supervisor.DecisionCandidate { + t.Fatalf("resolve did not suspend on foreground work: work=%+v decision=%+v", suspended.Work, suspended.Decision) + } + if suspended.Prescription != nil { + t.Fatalf("suspended resolve minted a prescription: %+v", suspended.Prescription) + } + + question := base + question.Operation = surfaces.OperationWorkInputRequired + question.WorkID, question.WorkQuestionPrompt = "diagnose", "Which incident is being diagnosed?" + asked, err := controller.Handle(ctx, question) + if err != nil { + t.Fatal(err) + } + if asked.Work == nil || asked.Work.Status != foregroundwork.StatusInputRequired || asked.Work.Question == nil { + t.Fatalf("work question was not recorded: %+v", asked.Work) + } + + staleAnswer := base + staleAnswer.Operation = surfaces.OperationWorkAnswer + staleAnswer.WorkID, staleAnswer.WorkQuestionID, staleAnswer.WorkAnswer = "diagnose", "question-not-current", []byte(`"incident-7"`) + if _, err := controller.Handle(ctx, staleAnswer); err == nil || !strings.Contains(err.Error(), "does not match the current question") { + t.Fatalf("stale answer result = %v, want current-question refusal", err) + } + + answer := staleAnswer + answer.WorkQuestionID = asked.Work.Question.ID + answered, err := controller.Handle(ctx, answer) + if err != nil { + t.Fatal(err) + } + if answered.Work == nil || answered.Work.Status != foregroundwork.StatusRequested || len(answered.Work.Answers) != 1 { + t.Fatalf("answer was not bound: %+v", answered.Work) + } + + prescriptionSnapshot := model.Snapshot{Observation: model.Observation{Invocation: model.InvocationContext{RepositoryID: "repo-fixture"}, StateRevision: 1, ProgramFingerprint: workLoopProgramFingerprint}, Fingerprint: strings.Repeat("a", 64)} + projection := protocol.CapabilityProjection{AuthorityFingerprint: "auth-test", Required: []catalog.Capability{catalog.CapabilityRepositoryWrite}, Effective: []catalog.Capability{catalog.CapabilityRepositoryWrite}} + apply := base + apply.Operation = surfaces.OperationApply + apply.TransitionID = "test.advance" + apply.Authority = authority + apply.Prescription, err = protocol.NewPrescription(prescriptionSnapshot, catalog.Transition{ID: apply.TransitionID}, projection) + if err != nil { + t.Fatal(err) + } + if _, err := controller.Handle(ctx, apply); err == nil || !strings.Contains(err.Error(), "requires completed foreground work") { + t.Fatalf("apply before completion = %v, want completed-work refusal", err) + } + + diagnosis := filepath.Join(answered.Work.Request.StagingRoot, "diagnosis.md") + if err := os.WriteFile(diagnosis, []byte("Cause: overload."), 0o600); err != nil { + t.Fatal(err) + } + complete := base + complete.Operation = surfaces.OperationWorkComplete + complete.WorkID = "diagnose" + completed, err := controller.Handle(ctx, complete) + if err != nil { + t.Fatal(err) + } + if completed.Work == nil || completed.Work.Status != foregroundwork.StatusCompleted || completed.Work.Result == nil { + t.Fatalf("completion was not recorded: %+v", completed.Work) + } + + resumed, err := controller.Handle(ctx, resolve) + if err != nil { + t.Fatal(err) + } + if resumed.Decision == nil || resumed.Decision.Kind != supervisor.DecisionPrescribed || resumed.Prescription == nil { + t.Fatalf("completed work did not resume resolution: decision=%+v prescription=%+v", resumed.Decision, resumed.Prescription) + } + if resumed.Prescription.WorkResultFingerprint != completed.Work.Result.ResultFingerprint { + t.Fatalf("resumed prescription is not bound to the committed work result: %q vs %q", resumed.Prescription.WorkResultFingerprint, completed.Work.Result.ResultFingerprint) + } + if resumed.Work == nil || resumed.Work.Request.Fingerprint != suspended.Work.Request.Fingerprint { + t.Fatalf("resume did not reuse the exact originating work request: %+v", resumed.Work) + } +} + +func TestForegroundWorkBlockCrossesSurfaceBoundary(t *testing.T) { + // control-law: each foreground-work mutation crosses one typed operation boundary + controller, repository := workLoopController(t) + ctx := context.Background() + now := time.Now().UTC() + authority := protocol.AuthorityBundle{Receipts: []protocol.AuthorityReceipt{{ + ID: "auth", Class: catalog.AuthorityRepository, Subject: "repo", Fingerprint: "config-fingerprint", + IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour), + }}} + resolve := surfaces.Request{ + SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationResolve, + Repository: repository, Host: "cli", CorrelationID: "work-block", FlowID: "run-two", + ProgramID: "test.synthetic", ProgramFingerprint: workLoopProgramFingerprint, EntryID: "entry", + Objective: model.Objective{ID: "objective", TargetID: model.ObjectiveVerified, DeliveryID: "delivery"}, + Authority: authority, + } + suspended, err := controller.Handle(ctx, resolve) + if err != nil { + t.Fatal(err) + } + if suspended.Work == nil || suspended.Work.Status != foregroundwork.StatusRequested { + t.Fatalf("resolve did not suspend on foreground work: %+v", suspended.Work) + } + block := surfaces.Request{ + SchemaVersion: surfaces.SchemaVersion, Operation: surfaces.OperationWorkBlock, + Repository: repository, Host: "cli", CorrelationID: "work-block", FlowID: "run-two", + WorkID: "diagnose", WorkBlockReason: "repository access is unavailable", + } + blocked, err := controller.Handle(ctx, block) + if err != nil { + t.Fatal(err) + } + if blocked.Work == nil || blocked.Work.Status != foregroundwork.StatusBlocked || blocked.Work.BlockReason == "" { + t.Fatalf("work block was not recorded: %+v", blocked.Work) + } +} diff --git a/boatstack/flow/softwaredelivery/artifact_projection_test.go b/boatstack/flow/softwaredelivery/artifact_projection_test.go new file mode 100644 index 00000000..d0ec3fd2 --- /dev/null +++ b/boatstack/flow/softwaredelivery/artifact_projection_test.go @@ -0,0 +1,121 @@ +package softwaredelivery_test + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/operatorstack/boatstack/boatstack/controlprogram" + softwareflow "github.com/operatorstack/boatstack/boatstack/flow/softwaredelivery" + "github.com/operatorstack/boatstack/boatstack/internal/hostprojection" +) + +func artifactFactPredicate(facet, value string) controlprogram.Predicate { + return controlprogram.Predicate{Fact: &controlprogram.FactPredicate{Facet: facet, Statuses: []string{"known"}, Values: []string{value}}} +} + +func artifactBoundaryProgram() controlprogram.Document { + mitigated := "mitigated" + return controlprogram.Document{ + Schema: controlprogram.SchemaName, SchemaRevision: controlprogram.SchemaRevision, + Program: controlprogram.Program{ID: "incident-response", Version: "1", Description: "human text"}, + Description: "incident control program", + Declarations: controlprogram.Declarations{ + Capabilities: []string{"service.restart"}, Authorities: []string{"incident-commander"}, + Effects: []string{"service.restart"}, Verifiers: []string{"healthcheck"}, InputResolvers: []string{"incident.input"}, + }, + Facets: []controlprogram.Facet{ + {ID: "service", Kind: "enum", Values: []string{"healthy", "degraded"}, Description: "service health"}, + {ID: "incident", Kind: "enum", Values: []string{"open", "mitigated"}}, + }, + Evidence: []controlprogram.Evidence{{ID: "healthcheck", Subject: "service", Kind: "observation", Description: "observed health"}}, + Operators: []controlprogram.Operator{{ + ID: "restart", Capabilities: []string{"service.restart"}, Authority: controlprogram.AuthorityRequirement{AnyOf: []string{"incident-commander"}}, + Effects: []string{"service.restart"}, Verifier: "healthcheck", Recovery: "restart", + Description: "restart the service", ExecutionContext: "preserve", + StateEffect: &controlprogram.StateEffect{Kind: "assignments", Assignments: []controlprogram.StateAssignment{{Facet: "incident", Value: &mitigated}}}, + }}, + Transitions: []controlprogram.Transition{{ + ID: "restart", Operator: "restart", Priority: 10, + Guard: artifactFactPredicate("incident", "open"), Target: artifactFactPredicate("incident", "mitigated"), Description: "restart service", + }}, + Targets: []controlprogram.Target{{ID: "mitigated", Predicate: artifactFactPredicate("incident", "mitigated"), Description: "incident mitigated"}}, + Entries: []controlprogram.Entry{{ID: "respond", Target: "mitigated", Description: "respond to incident", Inputs: []controlprogram.EntryInput{{ID: "incident", Type: "json", Required: true, Resolver: "incident.input", Config: json.RawMessage(`{"a":1}`)}}}}, + } +} + +func TestArtifactBindsEveryCanonicalHostProjectionExactly(t *testing.T) { + // control-law: runtime-admits-only-an-exact-source-lock-artifact-projection + // for every canonical host, including Cursor and Gemini, through the same + // artifact boundary that verifies committed projection bytes. + compiled, err := controlprogram.Compile(artifactBoundaryProgram(), nil) + if err != nil { + t.Fatal(err) + } + generated, err := softwareflow.GenerateProjections(compiled, hostprojection.CanonicalIDs()) + if err != nil { + t.Fatal(err) + } + hostPaths := map[hostprojection.ID]string{} + for _, host := range hostprojection.CanonicalIDs() { + paths, err := hostprojection.FlowPaths(host, "incident-response-respond") + if err != nil { + t.Fatal(err) + } + for _, path := range paths { + if _, exists := generated[path]; !exists { + t.Fatalf("host %s projection %s was not generated", host, path) + } + if strings.HasSuffix(path, ".md") && !strings.HasSuffix(path, ".gitattributes") { + hostPaths[host] = path + } + } + } + repository := t.TempDir() + sourcePath, lockPath := "flow.ts", "package-lock.json" + source, lock := []byte("declarative source"), []byte("dependency lock") + files := map[string][]byte{sourcePath: source, lockPath: lock} + for path, content := range generated { + files[path] = content + } + for path, content := range files { + absolute := filepath.Join(repository, filepath.FromSlash(path)) + if err := os.MkdirAll(filepath.Dir(absolute), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(absolute, content, 0o600); err != nil { + t.Fatal(err) + } + } + artifact, _, err := controlprogram.NewArtifact(compiled, controlprogram.ArtifactInput{ + CompilerVersion: "compiler-1", SourcePath: sourcePath, Source: source, + DependencyLockPath: lockPath, DependencyLock: lock, + Projections: hostprojection.CanonicalIDs(), GeneratedProjections: generated, + }) + if err != nil { + t.Fatal(err) + } + if _, err := controlprogram.CheckArtifact(repository, artifact, "compiler-1", nil, hostprojection.CanonicalIDs(), softwareflow.GenerateProjections); err != nil { + t.Fatalf("exact four-host artifact was refused: %v", err) + } + for _, host := range []hostprojection.ID{hostprojection.Cursor, hostprojection.Gemini} { + path := hostPaths[host] + absolute := filepath.Join(repository, filepath.FromSlash(path)) + if err := os.WriteFile(absolute, append(generated[path], []byte("\ntampered")...), 0o600); err != nil { + t.Fatal(err) + } + _, err := controlprogram.CheckArtifact(repository, artifact, "compiler-1", nil, hostprojection.CanonicalIDs(), softwareflow.GenerateProjections) + if err == nil || !strings.Contains(err.Error(), path) || !strings.Contains(err.Error(), "does not match compiled program") { + t.Fatalf("tampered %s projection was admitted: %v", host, err) + } + if err := os.WriteFile(absolute, generated[path], 0o600); err != nil { + t.Fatal(err) + } + } + narrowed := append([]hostprojection.ID(nil), hostprojection.Codex, hostprojection.Claude) + if _, err := controlprogram.CheckArtifact(repository, artifact, "compiler-1", nil, narrowed, softwareflow.GenerateProjections); err == nil || !strings.Contains(err.Error(), "FLOW_PROJECTION_SELECTION_STALE") { + t.Fatalf("narrowed projection selection was admitted: %v", err) + } +} diff --git a/boatstack/internal/softwaredelivery/engine/engine_test.go b/boatstack/internal/softwaredelivery/engine/engine_test.go index 95ca5b1c..dea5a6a8 100644 --- a/boatstack/internal/softwaredelivery/engine/engine_test.go +++ b/boatstack/internal/softwaredelivery/engine/engine_test.go @@ -1095,3 +1095,80 @@ func TestOwnedExternalExecutionErrorRequiresRecoveryWithoutRollback(t *testing.T t.Fatalf("ambiguous external error was collapsed: effects=%+v journal=%+v receipts=%d", effects, journal, len(receipts.values)) } } + +func TestForgedRepositoryAuthorityIsRefusedBeforeEffects(t *testing.T) { + // control-law: missing-expired-changed-or-forged-authority-fails-before-effects + now := time.Unix(30, 0).UTC() + observer := &sequenceObserver{items: []model.Observation{ + observation(model.PhaseObserved, "source"), + observation(model.PhaseObserved, "source"), + }} + journal, effectsPort, receipts, lock := &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, &fakeLock{} + kernel, err := New( + testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, + observer, fixedClock{now}, fakeLocker{lock}, journal, effectsPort, receipts, + ) + if err != nil { + t.Fatal(err) + } + apply := request(t, now) + apply.Authority.Receipts[0].Fingerprint = "forged-configuration-fingerprint" + + resolved, err := kernel.Resolve(context.Background(), apply.ResolveRequest) + if err != nil { + t.Fatal(err) + } + if resolved.Decision.Kind != supervisor.DecisionRefused || !strings.Contains(resolved.Decision.Reason, "not bound to current configuration evidence") { + t.Fatalf("forged authority resolution = %+v, want configuration-evidence refusal", resolved.Decision) + } + if resolved.Prescription.ID != "" { + t.Fatalf("forged authority minted a prescription: %+v", resolved.Prescription) + } + + if _, applyErr := kernel.Apply(context.Background(), apply); applyErr == nil { + t.Fatal("apply with forged repository authority succeeded") + } + if effectsPort.transition.ID != "" || effectsPort.executions != 0 || journal.begun != 0 || len(receipts.values) != 0 { + t.Fatalf("forged authority crossed the effect boundary: prepared=%q effects=%d journal=%d receipts=%d", effectsPort.transition.ID, effectsPort.executions, journal.begun, len(receipts.values)) + } +} + +func TestTargetedAndUntargetedResolutionShareOnePrescription(t *testing.T) { + // control-law: targeted-and-untargeted-resolution-use-one-canonical-selection-relation + now := time.Unix(30, 0).UTC() + observer := &sequenceObserver{items: []model.Observation{ + observation(model.PhaseObserved, "source"), + observation(model.PhaseObserved, "source"), + }} + kernel, err := New( + testRegistry(t), syntheticObjectiveContracts(t), syntheticProgram, + observer, fixedClock{now}, + fakeLocker{&fakeLock{}}, &fakeJournal{}, &fakeEffects{}, &memoryReceipts{}, + ) + if err != nil { + t.Fatal(err) + } + req := request(t, now).ResolveRequest + req.Requested = "" + untargeted, err := kernel.Resolve(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if untargeted.Decision.Kind != supervisor.DecisionPrescribed || untargeted.Prescription.ID == "" { + t.Fatalf("untargeted resolution = %+v, want PRESCRIBED with a prescription", untargeted.Decision) + } + req.Requested = "test.advance" + targeted, err := kernel.Resolve(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if targeted.Decision.Kind != supervisor.DecisionPrescribed || targeted.Prescription.ID == "" { + t.Fatalf("targeted resolution = %+v, want PRESCRIBED with a prescription", targeted.Decision) + } + if untargeted.Prescription.ID != targeted.Prescription.ID { + t.Fatalf("targeted and untargeted resolution derived different prescriptions: %q vs %q", untargeted.Prescription.ID, targeted.Prescription.ID) + } + if untargeted.Admission.TransitionID != targeted.Admission.TransitionID || untargeted.Admission.PrescriptionID != targeted.Admission.PrescriptionID { + t.Fatalf("targeted and untargeted resolution admitted different transitions: %+v vs %+v", untargeted.Admission, targeted.Admission) + } +} diff --git a/boatstack/kernel/relation_test.go b/boatstack/kernel/relation_test.go index bd1713cc..a6df08d8 100644 --- a/boatstack/kernel/relation_test.go +++ b/boatstack/kernel/relation_test.go @@ -33,6 +33,31 @@ func TestRelationTargetedAndUntargetedUseSameCandidates(t *testing.T) { } } +func TestRelationExplicitOnlyCandidateNeverAdvancesImplicitly(t *testing.T) { + // control-law: explicit-only-transitions-never-become-implicit-progress + candidates := []RelationCandidate{ + {ID: "explicit-control", Rank: 1, Priority: 1, Selectable: false}, + {ID: "routine-advance", Rank: 2, Priority: 1, Selectable: true}, + } + untargeted, trace := RelateWithTrace(RelationInput{Candidates: candidates}) + if untargeted.Kind != Prescribed || untargeted.Transition != "routine-advance" { + t.Fatalf("untargeted selection = %#v", untargeted) + } + for _, candidate := range trace { + if candidate.TransitionID == "explicit-control" && candidate.Survived { + t.Fatalf("explicit-only candidate survived untargeted selection: %#v", trace) + } + } + targeted := Relate(RelationInput{Requested: "explicit-control", Candidates: candidates}) + if targeted.Kind != Prescribed || targeted.Transition != "explicit-control" { + t.Fatalf("explicit request = %#v", targeted) + } + onlyExplicit := Relate(RelationInput{Candidates: candidates[:1]}) + if onlyExplicit.Kind != Unresolved { + t.Fatalf("explicit-only field must not progress implicitly: %#v", onlyExplicit) + } +} + func TestRelationReportsEqualPreferenceAndMarkedState(t *testing.T) { tied, trace := RelateWithTrace(RelationInput{Candidates: []RelationCandidate{ {ID: "a", Priority: 1, Selectable: true}, diff --git a/release-notes/2026-08-23-delivery-capability-regression-contracts.md b/release-notes/2026-08-23-delivery-capability-regression-contracts.md new file mode 100644 index 00000000..595f29c1 --- /dev/null +++ b/release-notes/2026-08-23-delivery-capability-regression-contracts.md @@ -0,0 +1,3 @@ +### Software-delivery capabilities frozen as regression contracts + +The delivery pipeline's load-bearing behaviors are now pinned by explicit regression tests at their real boundaries, so later refactors must preserve capabilities rather than merely compile. New contracts prove that an explicit-only transition never becomes implicit progress (skipped untargeted, prescribable when requested, unresolved when it is the only candidate), that targeted and untargeted resolution derive the same prescription through the engine's public resolve path, that forged repository authority is refused at resolve time and never reaches the journal or effect ports, that every canonical host projection is byte-bound into the control-program artifact and any tampered projection fails closed as stale, and that the foreground work loop suspends, binds answers to the exact requested revision, rejects stale answers, refuses apply while work is incomplete, and resumes only its originating execution through the delivery surface. CI now runs every frontend-dependent conformance proof in required mode, closing a gap where the no-execution boundary could skip silently when the frontend was not installed.