diff --git a/.github/reviews/kernel-explicit-selection-semantics.receipt.json b/.github/reviews/kernel-explicit-selection-semantics.receipt.json new file mode 100644 index 00000000..ba80202f --- /dev/null +++ b/.github/reviews/kernel-explicit-selection-semantics.receipt.json @@ -0,0 +1,4 @@ +{ + "reviewed_tree": "f4ff1a7f1becf21742022c0c19114df6bd7366e2", + "program_fingerprint": "3ca3397ff275d89bdb6d5c934b86b51d3cbdfab0ee628c47fe94d1d4f5767155" +} diff --git a/boatstack/cmd/boatstack-reviewer/program.go b/boatstack/cmd/boatstack-reviewer/program.go index aa8f6ff3..7a78a7e0 100644 --- a/boatstack/cmd/boatstack-reviewer/program.go +++ b/boatstack/cmd/boatstack-reviewer/program.go @@ -66,6 +66,8 @@ func compileReviewProgram(policy Policy) (kernel.Program, error) { RequiredCapabilities: []kernel.Capability{capabilitySubmit}, OwnedFacets: []string{facetRound}, Operation: transitionConverge, + SelectionRank: 1, + Selection: kernel.SelectionImplicit, Priority: 10, }, { @@ -77,6 +79,8 @@ func compileReviewProgram(policy Policy) (kernel.Program, error) { RequiredCapabilities: []kernel.Capability{capabilitySubmit}, OwnedFacets: []string{facetRound}, Operation: transitionRecord, + SelectionRank: 1, + Selection: kernel.SelectionImplicit, Priority: 20, }, { @@ -88,6 +92,8 @@ func compileReviewProgram(policy Policy) (kernel.Program, error) { RequiredCapabilities: []kernel.Capability{capabilitySubmit}, OwnedFacets: []string{facetRound}, Operation: transitionEscalate, + SelectionRank: 1, + Selection: kernel.SelectionImplicit, Priority: 30, }, { @@ -99,6 +105,8 @@ func compileReviewProgram(policy Policy) (kernel.Program, error) { RequiredCapabilities: []kernel.Capability{capabilityHuman}, OwnedFacets: []string{facetRound}, Operation: transitionReopen, + SelectionRank: 1, + Selection: kernel.SelectionImplicit, Priority: 40, }, { @@ -110,6 +118,8 @@ func compileReviewProgram(policy Policy) (kernel.Program, error) { RequiredCapabilities: []kernel.Capability{capabilityRecover}, OwnedFacets: []string{facetRound}, Operation: transitionRecover, + SelectionRank: 1, + Selection: kernel.SelectionImplicit, Priority: 5, Recovers: []string{ transitionConverge, diff --git a/boatstack/conformance/behavior/behavior.go b/boatstack/conformance/behavior/behavior.go index 4e373936..65756010 100644 --- a/boatstack/conformance/behavior/behavior.go +++ b/boatstack/conformance/behavior/behavior.go @@ -159,12 +159,6 @@ func Laws() []Law { // Every method operates through the backend's real production resolve and // apply path; adapters normalize evidence only at this boundary. type Backend interface { - // Unsupported reports why a law cannot be expressed through this - // backend's production path without changing production semantics. An - // empty string means the law runs. A non-empty reason is a recorded, - // precisely evidenced semantic mismatch, never a silent skip. - Unsupported(law Law) string - ResolveUntargeted(t testing.TB) Resolution ResolveTargeted(t testing.TB, role TransitionRole) Resolution Apply(t testing.TB, prescription Prescription) ApplyOutcome @@ -183,8 +177,7 @@ type Backend interface { // explicit-only transition is the only admissible candidate, so an // implementation that ever promoted explicit-only transitions to // implicit progress would be caught, not shadowed by a higher-ranked - // candidate. Backends that record LawExplicitOnly as unsupported never - // receive this call. + // candidate. IsolateExplicitOnly(t testing.TB) // Drift operators mutate the plant or control identity after a @@ -211,10 +204,11 @@ type Backend interface { // Factory constructs one isolated backend per law. type Factory func(testing.TB) Backend -// RunSharedLaws executes every shared law against the backend factory. -func RunSharedLaws(t *testing.T, factory Factory) { - t.Helper() - runners := map[Law]func(*testing.T, Backend){ +// runners is the canonical one-to-one binding between shared laws and their +// executable bodies. Every registered backend runs every runner; no backend +// may exempt itself from a shared control law. +func runners() map[Law]func(*testing.T, Backend) { + return map[Law]func(*testing.T, Backend){ LawSharedSelection: lawSharedSelection, LawExplicitOnly: lawExplicitOnly, LawAuthorityMissing: lawAuthorityMissing, @@ -234,14 +228,22 @@ func RunSharedLaws(t *testing.T, factory Factory) { LawReachesTarget: lawReachesTarget, LawReadOnlyResolution: lawReadOnlyResolution, } +} + +// RunSharedLaws executes every shared law against the backend factory. There +// is deliberately no skip or exemption path: a backend that cannot express a +// shared law fails it. +func RunSharedLaws(t *testing.T, factory Factory) { + t.Helper() + bodies := runners() for _, law := range Laws() { law := law + body, ok := bodies[law] + if !ok { + t.Fatalf("shared law %q has no runner", law) + } t.Run(string(law), func(t *testing.T) { - backend := factory(t) - if reason := backend.Unsupported(law); reason != "" { - t.Skipf("recorded semantic mismatch, not silent absence: %s", reason) - } - runners[law](t, backend) + body(t, factory(t)) }) } } diff --git a/boatstack/conformance/behavior/integrity_test.go b/boatstack/conformance/behavior/integrity_test.go new file mode 100644 index 00000000..d6454ffd --- /dev/null +++ b/boatstack/conformance/behavior/integrity_test.go @@ -0,0 +1,36 @@ +package behavior + +import "testing" + +// TestSharedLawRunnerIntegrity proves the harness cannot silently narrow the +// shared contract: the canonical law list and the runner table are one +// bijection, and RunSharedLaws executes exactly that list for every +// registered backend with no skip or exemption path. +func TestSharedLawRunnerIntegrity(t *testing.T) { + laws := Laws() + if len(laws) != 18 { + t.Fatalf("canonical shared law list has %d laws, want 18", len(laws)) + } + seen := make(map[Law]bool, len(laws)) + for _, law := range laws { + if seen[law] { + t.Fatalf("law %q is duplicated in the canonical list", law) + } + seen[law] = true + } + bodies := runners() + for _, law := range laws { + body, ok := bodies[law] + if !ok || body == nil { + t.Fatalf("law %q has no runner", law) + } + } + for law := range bodies { + if !seen[law] { + t.Fatalf("runner %q exists outside the canonical law list", law) + } + } + if len(bodies) != len(laws) { + t.Fatalf("runner table has %d entries, want exactly one per law (%d)", len(bodies), len(laws)) + } +} diff --git a/boatstack/conformance/behavior/kernel_backend_test.go b/boatstack/conformance/behavior/kernel_backend_test.go index 07f2cafa..3929264c 100644 --- a/boatstack/conformance/behavior/kernel_backend_test.go +++ b/boatstack/conformance/behavior/kernel_backend_test.go @@ -76,17 +76,12 @@ func (b *kernelBackend) newRuntime(t testing.TB, program kernel.Program, classif return runtime } -func (b *kernelBackend) Unsupported(law behavior.Law) string { - if law == behavior.LawExplicitOnly { - return "kernel.Runtime cannot express an explicit-only transition: kernel.Transition declares no selection class and Runtime.resolve marks every admissible candidate selectable; explicit-only selection exists only at the kernel.Relate relation layer and in the software-delivery catalog" - } - return "" -} - func (b *kernelBackend) TransitionID(role behavior.TransitionRole) string { switch role { case behavior.RoleAdvance: return b.fixture.Scenario.AdvanceTransitions[0] + case behavior.RoleExplicitOnly: + return b.fixture.Scenario.ExplicitOnlyTransition case behavior.RoleRecovery: return b.fixture.Scenario.RecoveryTransition default: @@ -252,10 +247,8 @@ func (b *kernelBackend) DriftObjectiveBinding(testing.TB) { b.objective = b.fixture.Scenario.RevisedObjective } -// IsolateExplicitOnly is unreachable: this backend records LawExplicitOnly -// as an unsupported semantic mismatch, so the law body never runs here. -func (b *kernelBackend) IsolateExplicitOnly(t testing.TB) { - t.Fatalf("kernel.Runtime cannot express an explicit-only transition; LawExplicitOnly is a recorded mismatch") +func (b *kernelBackend) IsolateExplicitOnly(testing.TB) { + b.fixture.Scenario.IsolateExplicitOnly() } func (b *kernelBackend) RetargetInstance(testing.TB) { diff --git a/boatstack/conformance/behavior/software_backend_test.go b/boatstack/conformance/behavior/software_backend_test.go index f9977f0e..fb08e8e4 100644 --- a/boatstack/conformance/behavior/software_backend_test.go +++ b/boatstack/conformance/behavior/software_backend_test.go @@ -481,7 +481,7 @@ func softwareRegistryVariant(t testing.TB, advanceEffect catalog.EffectID, advan RequiredEvidence: []string{"snapshot"}, OwnedResources: []string{"state"}, OwnedFacets: []model.StateFacet{model.StateFacetControl}, StateEffect: catalog.StateEffect{Kind: catalog.StateEffectAssignments, Assignments: []catalog.StateAssignment{{Facet: "phase", Value: &activePhase}}}, Effect: effect, LocalEffects: []catalog.EffectID{effect}, Idempotent: true, - Prescription: catalog.Prescription{Operation: string(id), ExpectedPostcondition: "active"}, + Prescription: catalog.Prescription{Operation: string(id), ExpectedPostcondition: "active"}, SourcePredicate: "observed", AdmissionPredicate: "exact-admission", TargetPredicate: "active", Verifier: "fresh-active", SourceConditions: []catalog.FacetCondition{{Facet: softwareStageFacet, Statuses: []model.FactStatus{model.FactKnown}, Values: sourceStages}}, TargetConditions: []catalog.FacetCondition{{Facet: softwareStageFacet, Statuses: []model.FactStatus{model.FactKnown}, Values: []string{"terminal"}}}, @@ -501,7 +501,7 @@ func softwareRegistryVariant(t testing.TB, advanceEffect catalog.EffectID, advan 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: catalog.EffectID(softwareRecoverID), LocalEffects: []catalog.EffectID{catalog.EffectID(softwareRecoverID)}, Idempotent: true, - Prescription: catalog.Prescription{Operation: string(softwareRecoverID), ExpectedPostcondition: "frontier"}, + Prescription: catalog.Prescription{Operation: string(softwareRecoverID), 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)}}}, @@ -574,8 +574,6 @@ func (b *softwareBackend) newEngine(t testing.TB, program protocol.ProgramIdenti return built } -func (b *softwareBackend) Unsupported(behavior.Law) string { return "" } - func (b *softwareBackend) TransitionID(role behavior.TransitionRole) string { switch role { case behavior.RoleAdvance: diff --git a/boatstack/delivery/control.go b/boatstack/delivery/control.go index 8cccd0a3..5b3aca41 100644 --- a/boatstack/delivery/control.go +++ b/boatstack/delivery/control.go @@ -610,11 +610,15 @@ func compileSupervisoryProgram(runtime ProgramRuntimeManifest, compatibility, do capabilities = append(capabilities, general.Capability("objective.bind")) facets = append(facets, "supervisor.objective") } + selection := general.SelectionExplicitOnly + if transition.ImplicitlySelectable() { + selection = general.SelectionImplicit + } projected = append(projected, general.Transition{ ID: string(transition.ID), SourceModes: []string{"software-delivery"}, TargetMode: "software-delivery", ObjectiveScope: transition.Policy.ObjectiveScope, ObjectiveMutation: mutation, RequiredCapabilities: capabilities, OwnedFacets: facets, - Operation: string(transition.ID), Priority: transition.SelectionClass.Rank()*1000 + transition.Priority, + Operation: string(transition.ID), SelectionRank: transition.SelectionClass.Rank(), Selection: selection, Priority: transition.Priority, Recovers: recoveredBy[transition.ID], }) } diff --git a/boatstack/kernel/conformance/conformance.go b/boatstack/kernel/conformance/conformance.go index 5cb0e915..34f85e79 100644 --- a/boatstack/kernel/conformance/conformance.go +++ b/boatstack/kernel/conformance/conformance.go @@ -36,30 +36,32 @@ type Snapshot struct { // Scenario maps domain-specific fixture operations onto domain-neutral laws. // The suite never infers these roles from transition or operation names. type Scenario struct { - InstanceID string - Objective kernel.Objective - RevisedObjective kernel.Objective - ConflictingObjective kernel.Objective - AlternateProgram kernel.Program - Authority kernel.Authority - BindTransition string - AdvanceTransitions []string - MaintenanceTransition string - RecoveryTransition string - RecoveryCapability kernel.Capability - ExtraCapability kernel.Capability - ChangeObservation func() - RebindObjective func(kernel.Objective) - BumpStateRevision func() - RetargetProgram func(kernel.ProgramIdentity) - AdvanceClock func(time.Duration) - IndependentLocker func() kernel.Locker - VerifyCommitted func(Snapshot, Snapshot, kernel.Receipt) error - InterruptNextOperator func() - PanicNextOperator func() - FailNextCommit func() - RetargetInstance func(string) - Snapshot func() Snapshot + InstanceID string + Objective kernel.Objective + RevisedObjective kernel.Objective + ConflictingObjective kernel.Objective + AlternateProgram kernel.Program + Authority kernel.Authority + BindTransition string + AdvanceTransitions []string + MaintenanceTransition string + ExplicitOnlyTransition string + IsolateExplicitOnly func() + RecoveryTransition string + RecoveryCapability kernel.Capability + ExtraCapability kernel.Capability + ChangeObservation func() + RebindObjective func(kernel.Objective) + BumpStateRevision func() + RetargetProgram func(kernel.ProgramIdentity) + AdvanceClock func(time.Duration) + IndependentLocker func() kernel.Locker + VerifyCommitted func(Snapshot, Snapshot, kernel.Receipt) error + InterruptNextOperator func() + PanicNextOperator func() + FailNextCommit func() + RetargetInstance func(string) + Snapshot func() Snapshot } // KernelConformance binds kernel ports to explicit scenario roles. New must @@ -732,7 +734,7 @@ func (suite KernelConformance) fixture(t testing.TB, setup Setup) KernelConforma t.Fatal("kernel conformance requires a fresh fixture factory") } fixture := suite.New(t, setup) - if fixture.Domain == nil || fixture.Operator == nil || fixture.CapabilityClassifier == nil || fixture.Store == nil || fixture.Locker == nil || fixture.Clock == nil || fixture.Scenario.Snapshot == nil || fixture.Scenario.ChangeObservation == nil || fixture.Scenario.RebindObjective == nil || fixture.Scenario.BumpStateRevision == nil || fixture.Scenario.RetargetProgram == nil || fixture.Scenario.AdvanceClock == nil || fixture.Scenario.IndependentLocker == nil || fixture.Scenario.VerifyCommitted == nil || fixture.Scenario.InterruptNextOperator == nil || fixture.Scenario.PanicNextOperator == nil || fixture.Scenario.FailNextCommit == nil || fixture.Scenario.RetargetInstance == nil || fixture.Scenario.InstanceID == "" || fixture.Scenario.BindTransition == "" || len(fixture.Scenario.AdvanceTransitions) == 0 || fixture.Scenario.MaintenanceTransition == "" || fixture.Scenario.RecoveryTransition == "" || fixture.Scenario.RecoveryCapability.Validate() != nil || fixture.Scenario.ExtraCapability.Validate() != nil { + if fixture.Domain == nil || fixture.Operator == nil || fixture.CapabilityClassifier == nil || fixture.Store == nil || fixture.Locker == nil || fixture.Clock == nil || fixture.Scenario.Snapshot == nil || fixture.Scenario.ChangeObservation == nil || fixture.Scenario.RebindObjective == nil || fixture.Scenario.BumpStateRevision == nil || fixture.Scenario.RetargetProgram == nil || fixture.Scenario.AdvanceClock == nil || fixture.Scenario.IndependentLocker == nil || fixture.Scenario.VerifyCommitted == nil || fixture.Scenario.InterruptNextOperator == nil || fixture.Scenario.PanicNextOperator == nil || fixture.Scenario.FailNextCommit == nil || fixture.Scenario.RetargetInstance == nil || fixture.Scenario.InstanceID == "" || fixture.Scenario.BindTransition == "" || len(fixture.Scenario.AdvanceTransitions) == 0 || fixture.Scenario.MaintenanceTransition == "" || fixture.Scenario.ExplicitOnlyTransition == "" || fixture.Scenario.IsolateExplicitOnly == nil || fixture.Scenario.RecoveryTransition == "" || fixture.Scenario.RecoveryCapability.Validate() != nil || fixture.Scenario.ExtraCapability.Validate() != nil { t.Fatal("kernel conformance fixture is incomplete") } if fixture.Scenario.RevisedObjective.Validate() != nil || fixture.Scenario.RevisedObjective.ID != fixture.Scenario.Objective.ID || fixture.Scenario.RevisedObjective.Revision <= fixture.Scenario.Objective.Revision || fixture.Scenario.RevisedObjective.Fingerprint == fixture.Scenario.Objective.Fingerprint { diff --git a/boatstack/kernel/conformance/conformance_test.go b/boatstack/kernel/conformance/conformance_test.go index 4d749952..294ded9f 100644 --- a/boatstack/kernel/conformance/conformance_test.go +++ b/boatstack/kernel/conformance/conformance_test.go @@ -349,7 +349,7 @@ func integerRecoveryCycleFixture() KernelConformance { ID: "counter.recover-cycle", SourceModes: []string{"zero", "one", "two"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, - Operation: "counter.reset", Priority: 1, Recovers: []string{"counter.increment-first", "counter.increment-second", "counter.reset"}, + Operation: "counter.reset", SelectionRank: 1, Selection: kernel.SelectionImplicit, Priority: 1, Recovers: []string{"counter.increment-first", "counter.increment-second", "counter.reset"}, }) program, err := kernel.CompileProgram(base.ID, base.Version, base.RuntimeCompatibility, base.InitialMode, base.MarkedModes, transitions) if err != nil { diff --git a/boatstack/kernel/conformance/integer.go b/boatstack/kernel/conformance/integer.go index bf19db6c..22b5916a 100644 --- a/boatstack/kernel/conformance/integer.go +++ b/boatstack/kernel/conformance/integer.go @@ -42,6 +42,8 @@ func (d *IntegerDomain) Admissible(_ context.Context, evaluation kernel.Evaluati return evaluation.Objective != nil && observed.Value < 2, "exact objective is present and value is below target", nil case "counter.reset": return observed.Value > 0 || evaluation.State.Recovery != nil, "value is nonzero or recovery remains active", nil + case "counter.hold": + return true, "hold preserves the counter and is admissible from every declared source mode", nil default: return false, "unknown transition", nil } @@ -70,6 +72,10 @@ func (d *IntegerDomain) Verify(_ context.Context, evaluation kernel.Evaluation, if after.Value != 0 { return fmt.Errorf("reset postcondition failed") } + case "counter.hold": + if before.Value != after.Value { + return fmt.Errorf("hold changed domain state") + } } return nil } @@ -123,6 +129,7 @@ func (o IntegerOperator) Execute(_ context.Context, operation kernel.Operation) } case "counter.reset": o.Domain.value = 0 + case "counter.hold": default: return kernel.Effect{}, fmt.Errorf("unknown operation") } @@ -144,6 +151,8 @@ func (IntegerCapabilities) RequiredCapabilities(transition kernel.Transition) ([ return []kernel.Capability{"counter.increment"}, nil case "counter.reset": return []kernel.Capability{"counter.reset"}, nil + case "counter.hold": + return []kernel.Capability{"counter.hold"}, nil default: return nil, fmt.Errorf("unclassified operation %q", transition.Operation) } @@ -226,6 +235,12 @@ func (s *MemoryStateStore) retarget(instanceID string) { s.state.InstanceID = instanceID } +func (s *MemoryStateStore) isolate() { + s.mu.Lock() + defer s.mu.Unlock() + s.state.Mode = "isolated" +} + func (s *MemoryStateStore) bumpRevision() { s.mu.Lock() defer s.mu.Unlock() @@ -284,12 +299,13 @@ func (c *FixedClock) advance(duration time.Duration) { // IntegerProgram compiles the reference control program. func IntegerProgram() (kernel.Program, error) { return kernel.CompileProgram("integer-control", "1.0.0", "kernel-v1", "unbound", []string{"two"}, []kernel.Transition{ - {ID: "objective.bind", SourceModes: []string{"unbound"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveNone, ObjectiveMutation: kernel.BindObjectiveMutation, RequiredCapabilities: []kernel.Capability{"objective.bind"}, OwnedFacets: []string{"supervisor.objective"}, Operation: "objective.bind", Priority: 5}, - {ID: "counter.increment-first", SourceModes: []string{"zero"}, TargetMode: "one", ObjectiveScope: kernel.ObjectiveBoundExact, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, - {ID: "counter.increment-second", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: kernel.ObjectiveBoundExact, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 10}, - {ID: "counter.reset", SourceModes: []string{"one", "two"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 20}, - {ID: "objective.recover", SourceModes: []string{"unbound"}, TargetMode: "unbound", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"objective.bind"}}, - {ID: "counter.recover", SourceModes: []string{"zero", "one", "two"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"counter.increment-first", "counter.increment-second", "counter.reset"}}, + {ID: "objective.bind", SourceModes: []string{"unbound"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveNone, ObjectiveMutation: kernel.BindObjectiveMutation, RequiredCapabilities: []kernel.Capability{"objective.bind"}, OwnedFacets: []string{"supervisor.objective"}, Operation: "objective.bind", SelectionRank: 1, Selection: kernel.SelectionImplicit, Priority: 5}, + {ID: "counter.increment-first", SourceModes: []string{"zero"}, TargetMode: "one", ObjectiveScope: kernel.ObjectiveBoundExact, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", SelectionRank: 1, Selection: kernel.SelectionImplicit, Priority: 10}, + {ID: "counter.increment-second", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: kernel.ObjectiveBoundExact, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", SelectionRank: 1, Selection: kernel.SelectionImplicit, Priority: 10}, + {ID: "counter.reset", SourceModes: []string{"one", "two"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", SelectionRank: 1, Selection: kernel.SelectionImplicit, Priority: 20}, + {ID: "counter.hold", SourceModes: []string{"isolated", "zero"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.hold"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.hold", SelectionRank: 6, Selection: kernel.SelectionExplicitOnly, Priority: 30}, + {ID: "objective.recover", SourceModes: []string{"unbound"}, TargetMode: "unbound", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", SelectionRank: 1, Selection: kernel.SelectionImplicit, Priority: 1, Recovers: []string{"objective.bind"}}, + {ID: "counter.recover", SourceModes: []string{"isolated", "zero", "one", "two"}, TargetMode: "zero", ObjectiveScope: kernel.ObjectiveOptionalPreserve, ObjectiveMutation: kernel.PreserveObjective, RequiredCapabilities: []kernel.Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", SelectionRank: 1, Selection: kernel.SelectionImplicit, Priority: 1, Recovers: []string{"counter.hold", "counter.increment-first", "counter.increment-second", "counter.reset"}}, }) } @@ -347,7 +363,7 @@ func newIntegerFixture(setup Setup) KernelConformance { state.Mode, state.ObjectiveBinding, value = "one", &binding, 1 } now := time.Date(2026, 8, 12, 10, 0, 0, 0, time.UTC) - authority := kernel.Authority{Receipts: []kernel.AuthorityReceipt{{ID: "human-counter", Subject: "fixture", Fingerprint: "fixture-authority", Capabilities: []kernel.Capability{"counter.audit", "counter.increment", "counter.reset", "objective.bind"}, IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(24 * time.Hour)}}} + authority := kernel.Authority{Receipts: []kernel.AuthorityReceipt{{ID: "human-counter", Subject: "fixture", Fingerprint: "fixture-authority", Capabilities: []kernel.Capability{"counter.audit", "counter.hold", "counter.increment", "counter.reset", "objective.bind"}, IssuedAt: now.Add(-time.Minute), ExpiresAt: now.Add(24 * time.Hour)}}} domain := &IntegerDomain{value: value, executions: map[string]int{}} receipts := &MemoryReceipts{} store := &MemoryStateStore{state: state, receipts: receipts} @@ -362,24 +378,26 @@ func newIntegerFixture(setup Setup) KernelConformance { Program: program, } fixture.Scenario = Scenario{ - InstanceID: state.InstanceID, - Objective: objective, - RevisedObjective: revised, - ConflictingObjective: conflicting, - AlternateProgram: alternateProgram, - Authority: authority, - BindTransition: "objective.bind", - AdvanceTransitions: []string{"counter.increment-first", "counter.increment-second"}, - MaintenanceTransition: "counter.reset", - RecoveryTransition: "counter.recover", - RecoveryCapability: "counter.reset", - ExtraCapability: "counter.audit", - ChangeObservation: domain.changeObservation, - RebindObjective: store.rebind, - BumpStateRevision: store.bumpRevision, - RetargetProgram: store.retargetProgram, - AdvanceClock: clock.advance, - IndependentLocker: func() kernel.Locker { return &MemoryLocker{} }, + InstanceID: state.InstanceID, + Objective: objective, + RevisedObjective: revised, + ConflictingObjective: conflicting, + AlternateProgram: alternateProgram, + Authority: authority, + BindTransition: "objective.bind", + AdvanceTransitions: []string{"counter.increment-first", "counter.increment-second"}, + MaintenanceTransition: "counter.reset", + ExplicitOnlyTransition: "counter.hold", + IsolateExplicitOnly: store.isolate, + RecoveryTransition: "counter.recover", + RecoveryCapability: "counter.reset", + ExtraCapability: "counter.audit", + ChangeObservation: domain.changeObservation, + RebindObjective: store.rebind, + BumpStateRevision: store.bumpRevision, + RetargetProgram: store.retargetProgram, + AdvanceClock: clock.advance, + IndependentLocker: func() kernel.Locker { return &MemoryLocker{} }, VerifyCommitted: func(before, after Snapshot, receipt kernel.Receipt) error { return verifyIntegerCommitted(program, before, after, receipt) }, @@ -453,6 +471,10 @@ func verifyIntegerCommitted(program kernel.Program, before, after Snapshot, rece if result.Value != 0 { return fmt.Errorf("reset left value at %d", result.Value) } + case "counter.hold": + if result.Value != prior.Value { + return fmt.Errorf("hold changed value from %d to %d", prior.Value, result.Value) + } default: return fmt.Errorf("unsupported operation %q", transition.Operation) } diff --git a/boatstack/kernel/program.go b/boatstack/kernel/program.go index e3f1a1af..07cb04d3 100644 --- a/boatstack/kernel/program.go +++ b/boatstack/kernel/program.go @@ -14,10 +14,27 @@ type Transition struct { RequiredCapabilities []Capability `json:"required_capabilities"` OwnedFacets []string `json:"owned_facets"` Operation string `json:"operation"` + SelectionRank int `json:"selection_rank"` + Selection SelectionMode `json:"selection"` Priority int `json:"priority"` Recovers []string `json:"recovers,omitempty"` } +// SelectionMode declares how the canonical relation may choose a transition. +// An implicitly selectable transition competes for untargeted progress; an +// explicit-only transition stays admissible but is prescribed only when a +// request names it exactly. +type SelectionMode string + +const ( + SelectionImplicit SelectionMode = "implicit" + SelectionExplicitOnly SelectionMode = "explicit-only" +) + +func (m SelectionMode) valid() bool { + return m == SelectionImplicit || m == SelectionExplicitOnly +} + type ObjectiveMutation string const ( @@ -34,6 +51,9 @@ func (t Transition) validate() error { if !qualifiedSemanticID.MatchString(t.ID) || len(t.SourceModes) == 0 || t.TargetMode == "" || !t.ObjectiveScope.Valid() || !t.ObjectiveMutation.valid() || !qualifiedSemanticID.MatchString(t.Operation) || t.Priority < 1 { return fmt.Errorf("transition %q has incomplete identity, modes, objective scope, operation, or priority", t.ID) } + if t.SelectionRank < 1 || !t.Selection.valid() { + return fmt.Errorf("transition %q requires a positive selection rank and an explicit selection mode (%q or %q)", t.ID, SelectionImplicit, SelectionExplicitOnly) + } if len(t.RequiredCapabilities) == 0 || len(t.OwnedFacets) == 0 { return fmt.Errorf("transition %q requires explicit capabilities and owned facets", t.ID) } diff --git a/boatstack/kernel/program_test.go b/boatstack/kernel/program_test.go index 5609afbb..7d5aa692 100644 --- a/boatstack/kernel/program_test.go +++ b/boatstack/kernel/program_test.go @@ -12,9 +12,9 @@ func TestProgramFingerprintCanonicalizesSemanticSets(t *testing.T) { ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.write", "state.inspect"}, OwnedFacets: []string{"counter.value", "counter.audit"}, - Operation: "counter.advance", Priority: 1, + Operation: "counter.advance", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1, }, - {ID: "recover", SourceModes: []string{"idle", "ready"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.write"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.recover", Priority: 2, Recovers: []string{"advance"}}, + {ID: "recover", SourceModes: []string{"idle", "ready"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.write"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.recover", SelectionRank: 1, Selection: SelectionImplicit, Priority: 2, Recovers: []string{"advance"}}, }) if err != nil { t.Fatal(err) @@ -25,9 +25,9 @@ func TestProgramFingerprintCanonicalizesSemanticSets(t *testing.T) { ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.inspect", "state.write"}, OwnedFacets: []string{"counter.audit", "counter.value"}, - Operation: "counter.advance", Priority: 1, + Operation: "counter.advance", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1, }, - {ID: "recover", SourceModes: []string{"ready", "idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.write"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.recover", Priority: 2, Recovers: []string{"advance"}}, + {ID: "recover", SourceModes: []string{"ready", "idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"state.write"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.recover", SelectionRank: 1, Selection: SelectionImplicit, Priority: 2, Recovers: []string{"advance"}}, }) if err != nil { t.Fatal(err) @@ -39,8 +39,8 @@ func TestProgramFingerprintCanonicalizesSemanticSets(t *testing.T) { func TestProgramRejectsRecoveryThatCannotRunFromRecoveredSourceMode(t *testing.T) { _, err := CompileProgram("blocked-recovery", "1", "kernel-v1", "one", []string{"done"}, []Transition{ - {ID: "increment", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 1}, - {ID: "recover", SourceModes: []string{"zero"}, TargetMode: "zero", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"increment"}}, + {ID: "increment", SourceModes: []string{"one"}, TargetMode: "two", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1}, + {ID: "recover", SourceModes: []string{"zero"}, TargetMode: "zero", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1, Recovers: []string{"increment"}}, }) if err == nil || !strings.Contains(err.Error(), `cannot recover "increment" from source mode "one"`) { t.Fatalf("compile error = %v", err) @@ -49,8 +49,8 @@ func TestProgramRejectsRecoveryThatCannotRunFromRecoveredSourceMode(t *testing.T func TestProgramRejectsObjectiveDependentRecovery(t *testing.T) { _, err := CompileProgram("blocked-objective-recovery", "1", "kernel-v1", "idle", []string{"done"}, []Transition{ - {ID: "advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 1}, - {ID: "recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", Priority: 1, Recovers: []string{"advance"}}, + {ID: "advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1}, + {ID: "recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1, Recovers: []string{"advance"}}, }) if err == nil || !strings.Contains(err.Error(), `recovery transition "recover" must preserve objective state without requiring an exact objective`) { t.Fatalf("compile error = %v", err) @@ -60,17 +60,59 @@ func TestProgramRejectsObjectiveDependentRecovery(t *testing.T) { func TestProgramRejectsTransitionWithoutDeclaredRecovery(t *testing.T) { _, err := CompileProgram("unrecoverable", "1", "kernel-v1", "idle", []string{"done"}, []Transition{{ ID: "advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, - RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", Priority: 1, + RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1, }}) if err == nil || !strings.Contains(err.Error(), `transition "advance" has no declared recovery`) { t.Fatalf("compile error = %v", err) } } +func TestProgramRejectsTransitionWithoutExplicitSelection(t *testing.T) { + compile := func(mutate func(*Transition)) error { + transitions := []Transition{ + {ID: "advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1}, + {ID: "recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", SelectionRank: 1, Selection: SelectionImplicit, Priority: 2, Recovers: []string{"advance", "recover"}}, + } + mutate(&transitions[0]) + _, err := CompileProgram("selection-contract", "1", "kernel-v1", "idle", []string{"done"}, transitions) + return err + } + for name, mutate := range map[string]func(*Transition){ + "zero rank": func(transition *Transition) { transition.SelectionRank = 0 }, + "negative rank": func(transition *Transition) { transition.SelectionRank = -1 }, + "absent selection mode": func(transition *Transition) { transition.Selection = "" }, + "unknown selection mode": func(transition *Transition) { transition.Selection = "sometimes" }, + } { + if err := compile(mutate); err == nil || !strings.Contains(err.Error(), "requires a positive selection rank and an explicit selection mode") { + t.Fatalf("%s: compile error = %v", name, err) + } + } +} + +func TestProgramFingerprintBindsSelectionSemantics(t *testing.T) { + compile := func(rank int, mode SelectionMode) Program { + program, err := CompileProgram("selection-identity", "1", "kernel-v1", "idle", []string{"done"}, []Transition{ + {ID: "advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.increment", SelectionRank: rank, Selection: mode, Priority: 1}, + {ID: "recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveNone, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "counter.reset", SelectionRank: 1, Selection: SelectionImplicit, Priority: 2, Recovers: []string{"advance", "recover"}}, + }) + if err != nil { + t.Fatal(err) + } + return program + } + base := compile(1, SelectionImplicit) + if changedRank := compile(2, SelectionImplicit); changedRank.Fingerprint == base.Fingerprint { + t.Fatal("changing selection rank did not change the program fingerprint") + } + if changedMode := compile(1, SelectionExplicitOnly); changedMode.Fingerprint == base.Fingerprint { + t.Fatal("changing selection mode did not change the program fingerprint") + } +} + func TestProgramAcceptsQualifiedTransitionIdentities(t *testing.T) { program, err := CompileProgram("qualified", "1", "kernel-v1", "idle", []string{"done"}, []Transition{ - {ID: "example-program/advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "example-program/advance", Priority: 1}, - {ID: "example-program/recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "example-program/recover", Priority: 2, Recovers: []string{"example-program/advance", "example-program/recover"}}, + {ID: "example-program/advance", SourceModes: []string{"idle"}, TargetMode: "done", ObjectiveScope: ObjectiveBoundExact, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.increment"}, OwnedFacets: []string{"counter.value"}, Operation: "example-program/advance", SelectionRank: 1, Selection: SelectionImplicit, Priority: 1}, + {ID: "example-program/recover", SourceModes: []string{"idle"}, TargetMode: "idle", ObjectiveScope: ObjectiveOptionalPreserve, ObjectiveMutation: PreserveObjective, RequiredCapabilities: []Capability{"counter.reset"}, OwnedFacets: []string{"counter.value"}, Operation: "example-program/recover", SelectionRank: 1, Selection: SelectionImplicit, Priority: 2, Recovers: []string{"example-program/advance", "example-program/recover"}}, }) if err != nil { t.Fatal(err) diff --git a/boatstack/kernel/runtime.go b/boatstack/kernel/runtime.go index 1fbac809..ed41e468 100644 --- a/boatstack/kernel/runtime.go +++ b/boatstack/kernel/runtime.go @@ -227,7 +227,7 @@ func (r Runtime) resolve(ctx context.Context, state ControlState, observation Ob transitions := map[string]Transition{} var candidates []RelationCandidate for _, transition := range r.program.Transitions { - candidateTrace := CandidateTrace{TransitionID: transition.ID, Priority: transition.Priority} + candidateTrace := CandidateTrace{TransitionID: transition.ID, Rank: transition.SelectionRank, Priority: transition.Priority} if request.Requested != "" && transition.ID != request.Requested { candidateTrace.Disposition = DispositionIrrelevantToRequest trace.Candidates = append(trace.Candidates, candidateTrace) @@ -277,9 +277,11 @@ func (r Runtime) resolve(ctx context.Context, state ControlState, observation Ob if capabilityErr != nil { return Resolution{}, capabilityErr } - candidateTrace.Selectable, candidateTrace.Survived = true, true + selectable := transition.Selection == SelectionImplicit + candidateTrace.Selection = EvaluationTrace{Evaluated: true, Satisfied: selectable, Reason: fmt.Sprintf("transition declares selection mode %q", transition.Selection)} + candidateTrace.Selectable, candidateTrace.Survived = selectable, true transitions[transition.ID] = transition - candidates = append(candidates, RelationCandidate{ID: transition.ID, Priority: transition.Priority, Selectable: true, RequiredAll: required}) + candidates = append(candidates, RelationCandidate{ID: transition.ID, Rank: transition.SelectionRank, Priority: transition.Priority, Selectable: selectable, RequiredAll: required}) trace.Candidates = append(trace.Candidates, candidateTrace) } var relationTraces []CandidateTrace diff --git a/docs/architecture/kernel.md b/docs/architecture/kernel.md index de75108f..e7b7235d 100644 --- a/docs/architecture/kernel.md +++ b/docs/architecture/kernel.md @@ -43,6 +43,8 @@ The general kernel never imports the software-delivery implementation. - external `Objective` and durable exact `ObjectiveBinding`; - objective scopes: `none`, `optional-preserve`, and `bound-exact`; - explicit objective bind and clear mutations; +- explicit per-transition selection: a positive selection rank and a + selection mode (`implicit` or `explicit-only`), alongside priority; - one transition relation used by resolve and apply; - state, program, objective, observation, and authority freshness; - trusted minimum-capability classification; @@ -104,6 +106,21 @@ predicate, and authority. Apply reloads the state and observation under the instance lock, verifies prescription freshness, and calls the same relation. It cannot use a separate deterministic legality rule. +Every transition declares its selection behavior explicitly: a positive +selection rank and a selection mode. Resolve copies both directly into the +relation candidate, so an `explicit-only` transition stays admissible but is +never chosen by untargeted resolution; it is prescribed only when a request +names it exactly. Implicit candidates order by selection rank, then priority, +then identity; equal preference returns a frontier; capability authority is +compared only after selection filtering. Because rank and mode are part of the +canonical program representation, changing either changes the program +fingerprint and stales prior prescriptions before any effect. + +The software-delivery projection maps its catalog selection classes onto +these generic fields one-to-one — class rank to selection rank, implicit +selectability to selection mode, priority to priority — without encoding one +axis inside another and without placing software vocabulary in the kernel. + Software delivery uses the same relation through its domain adapter. Its advanced journal and reversible effect machinery remain domain-owned, while the prescription uses the same `kernel.Freshness` CAS identity as the generic diff --git a/release-notes/2026-08-23-kernel-explicit-selection-semantics.md b/release-notes/2026-08-23-kernel-explicit-selection-semantics.md new file mode 100644 index 00000000..32448dd3 --- /dev/null +++ b/release-notes/2026-08-23-kernel-explicit-selection-semantics.md @@ -0,0 +1,18 @@ +### Generic kernel transitions carry explicit selection semantics with zero conformance exemptions + +Every kernel transition now declares a positive selection rank and an explicit +selection mode — implicitly selectable or explicit-request-only — and the +generic runtime copies both directly into the canonical selection relation +instead of marking every admissible transition selectable. The +software-delivery projection maps its selection classes onto these fields +one-to-one, replacing the previous compression of rank into a single priority +number, so selection semantics survive exactly from a compiled domain program +into the shared relation. Rank and selection mode are part of the canonical +program identity: changing either changes the program fingerprint and stales +prior prescriptions before any effect. + +With the mismatch closed, the explicit-only shared behavioral law now executes +against both registered runtimes, and the conformance harness no longer offers +any backend-controlled exemption or skip path: all eighteen shared control +laws run for every registered backend, and a new integrity check pins the +one-to-one binding between the canonical law list and its runners.