|
| 1 | +/- |
| 2 | +Copyright (c) 2026 Lean FRO, LLC. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Authors: Kim Morrison |
| 5 | +-/ |
| 6 | + |
| 7 | +module |
| 8 | + |
| 9 | +public import HexInterval.Experiment.Policy |
| 10 | + |
| 11 | +@[expose] public section |
| 12 | + |
| 13 | +/-! |
| 14 | +# Bounded package features for interval-search policies |
| 15 | +
|
| 16 | +An interval or function package may know that one live offer is more useful |
| 17 | +than another, even though the generic scheduler must not know how to interpret |
| 18 | +its facts or operations. This experiment gives such packages a stateless, |
| 19 | +versioned feature callback. A callback receives one immutable policy snapshot |
| 20 | +and one engine-owned offer; the decorator accepts only bounded integer output. |
| 21 | +
|
| 22 | +Callbacks are evaluated while a view is decorated; they are never retained in |
| 23 | +the policy state. The decorated offer keeps the complete engine-owned offer, |
| 24 | +so choosing it still goes through the ordinary freshness and semantic-key |
| 25 | +checks. A stale or dishonest feature can therefore change search order, but |
| 26 | +cannot authorize an engine transition, enter proof evidence, or bypass replay |
| 27 | +validation of the trace that search eventually generates. |
| 28 | +-/ |
| 29 | + |
| 30 | +namespace Hex.Interval.Experiment.PolicyFeature |
| 31 | + |
| 32 | +open Propagator Propagator.Policy |
| 33 | + |
| 34 | +/-- Versioned identity of one independently registered feature provider. |
| 35 | +Changing the meaning of any local feature requires a new provider version. -/ |
| 36 | +structure ProviderKey where |
| 37 | + family : Nat |
| 38 | + version : Nat |
| 39 | + deriving DecidableEq, Repr |
| 40 | + |
| 41 | +/-- A provider-local feature before its owner is attached by the registry. -/ |
| 42 | +structure LocalFeature where |
| 43 | + key : Nat |
| 44 | + value : Int |
| 45 | + deriving DecidableEq, Repr |
| 46 | + |
| 47 | +/-- Globally unambiguous feature address and its bounded integer value. -/ |
| 48 | +structure Feature where |
| 49 | + provider : ProviderKey |
| 50 | + key : Nat |
| 51 | + value : Int |
| 52 | + deriving DecidableEq, Repr |
| 53 | + |
| 54 | +/-- Exact immutable input to a feature provider. |
| 55 | +
|
| 56 | +The request deliberately contains no package cache and grants no mutation |
| 57 | +authority. A goal-oriented provider may close over a fixed frontend goal; |
| 58 | +the provider version must change when the interpretation of that configuration |
| 59 | +changes. -/ |
| 60 | +structure Request (Fact : Type) where |
| 61 | + scope : ScopeId |
| 62 | + serial : Nat |
| 63 | + programVersion : Nat |
| 64 | + facts : Snapshot Fact |
| 65 | + remaining : EngineBudgetView |
| 66 | + incomplete : Bool |
| 67 | + offer : OfferView |
| 68 | + |
| 69 | +/-- Stateless companion contribution from one interval or frontend package. |
| 70 | +Returning an empty array means this provider contributes nothing for the offer. -/ |
| 71 | +structure Provider (Fact : Type) where |
| 72 | + key : ProviderKey |
| 73 | + features : Request Fact -> Array LocalFeature |
| 74 | + |
| 75 | +/-- Every accepted output/count dimension which a provider can make large has |
| 76 | +an independent cap. Provider identities are opaque registry addresses. -/ |
| 77 | +structure Limits where |
| 78 | + maxProviders : Nat |
| 79 | + maxProviderChecks : Nat |
| 80 | + maxFeaturesPerProvider : Nat |
| 81 | + maxFeaturesPerOffer : Nat |
| 82 | + maxTotalFeatures : Nat |
| 83 | + maxFeatureKey : Nat |
| 84 | + maxFeatureValue : Nat |
| 85 | + deriving DecidableEq, Repr |
| 86 | + |
| 87 | +/-- Exact rejection reason. Decoration is transactional: errors return no |
| 88 | +partially featured view. -/ |
| 89 | +inductive Error where |
| 90 | + | providerLimit |
| 91 | + | duplicateProvider (key : ProviderKey) |
| 92 | + | providerCheckLimit |
| 93 | + | providerFeatureLimit (provider : ProviderKey) (offer : OfferId) |
| 94 | + | offerFeatureLimit (offer : OfferId) |
| 95 | + | totalFeatureLimit |
| 96 | + | featureKeyLimit (provider : ProviderKey) (key : Nat) |
| 97 | + | featureValueLimit (provider : ProviderKey) (key : Nat) (value : Int) |
| 98 | + | duplicateFeature (provider : ProviderKey) (key : Nat) (offer : OfferId) |
| 99 | + deriving DecidableEq, Repr |
| 100 | + |
| 101 | +/-- Immutable provider order established by successful registry assembly. -/ |
| 102 | +structure Registry (Fact : Type) where |
| 103 | + private mk :: |
| 104 | + providers : Array (Provider Fact) |
| 105 | + |
| 106 | +private def makeRegistry (providers : Array (Provider Fact)) : Registry Fact := |
| 107 | + { providers } |
| 108 | + |
| 109 | +namespace Registry |
| 110 | + |
| 111 | +private def duplicateProvider? (providers : List (Provider Fact)) : Option ProviderKey := |
| 112 | + match providers with |
| 113 | + | [] => none |
| 114 | + | provider :: rest => |
| 115 | + if rest.any (fun other => other.key == provider.key) then |
| 116 | + some provider.key |
| 117 | + else |
| 118 | + duplicateProvider? rest |
| 119 | + |
| 120 | +/-- Assemble providers in caller order after bounding and checking identities. -/ |
| 121 | +opaque buildWithin (limits : Limits) (providers : Array (Provider Fact)) : |
| 122 | + Except Error (Registry Fact) := |
| 123 | + if limits.maxProviders < providers.size then |
| 124 | + .error .providerLimit |
| 125 | + else |
| 126 | + match duplicateProvider? providers.toList with |
| 127 | + | some key => .error (.duplicateProvider key) |
| 128 | + | none => .ok (makeRegistry providers) |
| 129 | + |
| 130 | +end Registry |
| 131 | + |
| 132 | +/-- One engine offer decorated with package data. `base` is returned unchanged |
| 133 | +when a policy selects this item. -/ |
| 134 | +structure FeaturedOffer where |
| 135 | + base : OfferView |
| 136 | + features : Array Feature |
| 137 | + |
| 138 | +/-- Deterministic callback/output counts reported by one successful decoration. -/ |
| 139 | +structure Metrics where |
| 140 | + providerChecks : Nat |
| 141 | + emittedFeatures : Nat |
| 142 | + deriving DecidableEq, Repr |
| 143 | + |
| 144 | +/-- A policy view plus aligned featured offers. The base view remains the |
| 145 | +freshness authority. -/ |
| 146 | +structure View (Fact : Type) where |
| 147 | + base : Policy.View Fact |
| 148 | + offers : Array FeaturedOffer |
| 149 | + metrics : Metrics |
| 150 | + |
| 151 | +private def request (view : Policy.View Fact) (offer : OfferView) : Request Fact := |
| 152 | + { scope := view.scope |
| 153 | + serial := view.serial |
| 154 | + programVersion := view.programVersion |
| 155 | + facts := view.facts |
| 156 | + remaining := view.remaining |
| 157 | + incomplete := view.incomplete |
| 158 | + offer } |
| 159 | + |
| 160 | +private def validValue (limit : Nat) (value : Int) : Bool := |
| 161 | + value.natAbs <= limit |
| 162 | + |
| 163 | +/-- Attach package features in stable offer-major, provider-major, local order. |
| 164 | +
|
| 165 | +The complete provider/offer cross product is preflight-counted before any |
| 166 | +callback is entered. Returned arrays are then checked incrementally against |
| 167 | +per-provider, per-offer, whole-view, key, and value limits. -/ |
| 168 | +opaque decorate (limits : Limits) (registry : Registry Fact) |
| 169 | + (view : Policy.View Fact) : Except Error (View Fact) := do |
| 170 | + if limits.maxProviders < registry.providers.size then throw .providerLimit |
| 171 | + let checks := registry.providers.size * view.offers.size |
| 172 | + if limits.maxProviderChecks < checks then throw .providerCheckLimit |
| 173 | + let mut decorated := #[] |
| 174 | + let mut total := 0 |
| 175 | + for offer in view.offers do |
| 176 | + let mut features := #[] |
| 177 | + for provider in registry.providers do |
| 178 | + let emitted := provider.features (request view offer) |
| 179 | + if limits.maxFeaturesPerProvider < emitted.size then |
| 180 | + throw (.providerFeatureLimit provider.key offer.id) |
| 181 | + for feature in emitted do |
| 182 | + if limits.maxFeatureKey < feature.key then |
| 183 | + throw (.featureKeyLimit provider.key feature.key) |
| 184 | + if !validValue limits.maxFeatureValue feature.value then |
| 185 | + throw (.featureValueLimit provider.key feature.key feature.value) |
| 186 | + if features.any (fun old => |
| 187 | + old.provider == provider.key && old.key == feature.key) then |
| 188 | + throw (.duplicateFeature provider.key feature.key offer.id) |
| 189 | + if limits.maxFeaturesPerOffer <= features.size then |
| 190 | + throw (.offerFeatureLimit offer.id) |
| 191 | + if limits.maxTotalFeatures <= total then |
| 192 | + throw .totalFeatureLimit |
| 193 | + features := features.push |
| 194 | + { provider := provider.key, key := feature.key, value := feature.value } |
| 195 | + total := total + 1 |
| 196 | + decorated := decorated.push { base := offer, features } |
| 197 | + pure |
| 198 | + { base := view |
| 199 | + offers := decorated |
| 200 | + metrics := { providerChecks := checks, emittedFeatures := total } } |
| 201 | + |
| 202 | +/-- Exact lookup used by policies which understand a configured feature key. |
| 203 | +Unknown features remain inert. -/ |
| 204 | +def FeaturedOffer.feature? (offer : FeaturedOffer) |
| 205 | + (provider : ProviderKey) (key : Nat) : Option Int := |
| 206 | + (offer.features.find? fun feature => |
| 207 | + feature.provider == provider && feature.key == key).map (fun feature => feature.value) |
| 208 | + |
| 209 | +end Hex.Interval.Experiment.PolicyFeature |
0 commit comments