@@ -11,7 +11,6 @@ import {
1111 WORLD_GAME_STATE_SYSTEM_ID ,
1212 WORLD_GAME_STATE_TRANSITION_NAMES
1313} from './constants.js' ;
14- import { createPromotionGate } from '../promotion/createPromotionGate.js' ;
1514import { createInitialWorldGameState } from './initialState.js' ;
1615import { createSelectorRegistry } from './selectors.js' ;
1716import { createTransitionRegistry } from './transitions.js' ;
@@ -77,6 +76,172 @@ function resolveFrameFromMeta(meta, payload) {
7776 return null ;
7877}
7978
79+ function asFiniteNumber ( value , fallback = 0 ) {
80+ const numeric = Number ( value ) ;
81+ return Number . isFinite ( numeric ) ? numeric : fallback ;
82+ }
83+
84+ function asPositiveInteger ( value , fallback = 1 ) {
85+ const numeric = Math . floor ( asFiniteNumber ( value , fallback ) ) ;
86+ return numeric >= 1 ? numeric : fallback ;
87+ }
88+
89+ function normalizeRequiredCriteria ( requiredCriteria ) {
90+ if ( ! Array . isArray ( requiredCriteria ) ) return [ ] ;
91+ const seen = new Set ( ) ;
92+ const out = [ ] ;
93+ for ( let i = 0 ; i < requiredCriteria . length ; i += 1 ) {
94+ const key = String ( requiredCriteria [ i ] || '' ) . trim ( ) ;
95+ if ( ! key || seen . has ( key ) ) continue ;
96+ seen . add ( key ) ;
97+ out . push ( key ) ;
98+ }
99+ return out ;
100+ }
101+
102+ function normalizeCriteriaMap ( criteria , requiredCriteria ) {
103+ const normalized = { } ;
104+ if ( isPlainObject ( criteria ) ) {
105+ const keys = Object . keys ( criteria ) ;
106+ for ( let i = 0 ; i < keys . length ; i += 1 ) {
107+ normalized [ String ( keys [ i ] ) ] = Boolean ( criteria [ keys [ i ] ] ) ;
108+ }
109+ }
110+ for ( let i = 0 ; i < requiredCriteria . length ; i += 1 ) {
111+ const key = requiredCriteria [ i ] ;
112+ if ( ! ( key in normalized ) ) normalized [ key ] = false ;
113+ }
114+ return normalized ;
115+ }
116+
117+ function createInlinePromotionGate ( {
118+ now,
119+ requiredCriteria,
120+ stabilityWindowFrames,
121+ initiallyPromoted,
122+ logger
123+ } ) {
124+ const criteriaKeys = normalizeRequiredCriteria ( requiredCriteria ) ;
125+ const windowFrames = asPositiveInteger ( stabilityWindowFrames , 3 ) ;
126+ let promoted = Boolean ( initiallyPromoted ) ;
127+ let stableFrames = promoted ? windowFrames : 0 ;
128+ let lastReason = promoted ? 'ALREADY_PROMOTED' : 'AWAITING_CRITERIA' ;
129+ let lastEvaluation = null ;
130+ const metrics = {
131+ evaluations : 0 ,
132+ stableEvaluations : 0 ,
133+ blockedEvaluations : 0 ,
134+ rollbackAborts : 0 ,
135+ promotions : promoted ? 1 : 0 ,
136+ lastEvaluationAtMs : null ,
137+ lastPromotionAtMs : promoted ? Number ( now ( ) ) : null
138+ } ;
139+
140+ function getMetrics ( ) {
141+ return {
142+ ...metrics ,
143+ promoted,
144+ stableFrames,
145+ stabilityWindowFrames : windowFrames
146+ } ;
147+ }
148+
149+ function getState ( ) {
150+ return {
151+ promoted,
152+ stableFrames,
153+ stabilityWindowFrames : windowFrames ,
154+ lastReason,
155+ lastEvaluation : lastEvaluation ? cloneDeep ( lastEvaluation ) : null
156+ } ;
157+ }
158+
159+ function evaluate ( { criteria = { } , rollbackTriggered = false , transitionName = '' , frame = null } = { } ) {
160+ metrics . evaluations += 1 ;
161+ const timestampMs = Number ( now ( ) ) ;
162+ metrics . lastEvaluationAtMs = timestampMs ;
163+
164+ const normalizedCriteria = normalizeCriteriaMap ( criteria , criteriaKeys ) ;
165+ const normalizedKeys = Object . keys ( normalizedCriteria ) ;
166+ const unmet = [ ] ;
167+ for ( let i = 0 ; i < normalizedKeys . length ; i += 1 ) {
168+ const key = normalizedKeys [ i ] ;
169+ if ( ! normalizedCriteria [ key ] ) unmet . push ( key ) ;
170+ }
171+ const hasCriteria = normalizedKeys . length > 0 ;
172+ const allCriteriaMet = hasCriteria && unmet . length === 0 ;
173+ let promotedNow = false ;
174+
175+ if ( rollbackTriggered && ! promoted ) {
176+ stableFrames = 0 ;
177+ lastReason = 'ROLLBACK_ABORTED_PROMOTION' ;
178+ metrics . rollbackAborts += 1 ;
179+ metrics . blockedEvaluations += 1 ;
180+ } else if ( promoted ) {
181+ stableFrames = windowFrames ;
182+ lastReason = 'ALREADY_PROMOTED' ;
183+ metrics . stableEvaluations += 1 ;
184+ } else if ( ! hasCriteria ) {
185+ stableFrames = 0 ;
186+ lastReason = 'PROMOTION_CRITERIA_MISSING' ;
187+ metrics . blockedEvaluations += 1 ;
188+ } else if ( ! allCriteriaMet ) {
189+ stableFrames = 0 ;
190+ lastReason = 'PROMOTION_CRITERIA_UNMET' ;
191+ metrics . blockedEvaluations += 1 ;
192+ } else {
193+ stableFrames += 1 ;
194+ metrics . stableEvaluations += 1 ;
195+ lastReason = stableFrames >= windowFrames ? 'PROMOTION_READY' : 'PROMOTION_STABILIZING' ;
196+ if ( stableFrames >= windowFrames ) {
197+ promoted = true ;
198+ promotedNow = true ;
199+ metrics . promotions += 1 ;
200+ metrics . lastPromotionAtMs = timestampMs ;
201+ lastReason = 'PROMOTED' ;
202+ }
203+ }
204+
205+ const readiness = promoted ? 'authoritative' : ( allCriteriaMet ? 'stabilizing' : 'passive' ) ;
206+ const evaluation = {
207+ transitionName : String ( transitionName || '' ) ,
208+ frame : frame !== undefined && frame !== null ? Number ( frame ) : null ,
209+ timestampMs,
210+ readiness,
211+ promoted,
212+ promotedNow,
213+ rollbackTriggered : Boolean ( rollbackTriggered ) ,
214+ stability : {
215+ currentFrames : stableFrames ,
216+ requiredFrames : windowFrames
217+ } ,
218+ criteria : {
219+ values : normalizedCriteria ,
220+ unmet,
221+ allMet : allCriteriaMet
222+ } ,
223+ reason : lastReason ,
224+ metrics : getMetrics ( )
225+ } ;
226+ lastEvaluation = cloneDeep ( evaluation ) ;
227+
228+ if ( typeof logger === 'function' ) {
229+ logger (
230+ `[promotion-gate] readiness=${ readiness } promoted=${ String ( promoted ) } ` +
231+ `stable=${ stableFrames } /${ windowFrames } reason=${ lastReason } `
232+ ) ;
233+ }
234+
235+ return evaluation ;
236+ }
237+
238+ return {
239+ evaluate,
240+ getMetrics,
241+ getState
242+ } ;
243+ }
244+
80245function createWorldGameStateSystem ( options = { } ) {
81246 const now = typeof options . now === 'function' ? options . now : ( ) => Date . now ( ) ;
82247 const initialPassiveMode = options . passiveMode !== undefined ? Boolean ( options . passiveMode ) : true ;
@@ -111,7 +276,7 @@ function createWorldGameStateSystem(options = {}) {
111276 ? promotionGateConfig . logger
112277 : null ;
113278 const promotionGate = promotionGateConfig
114- ? createPromotionGate ( {
279+ ? createInlinePromotionGate ( {
115280 now,
116281 requiredCriteria : promotionGateConfig . requiredCriteria ,
117282 stabilityWindowFrames : promotionGateConfig . stabilityWindowFrames ,
@@ -477,26 +642,6 @@ function createWorldGameStateSystem(options = {}) {
477642 return featureGates ;
478643 }
479644
480- function getMode ( ) {
481- return activeMode ;
482- }
483-
484- function getPromotionReadiness ( ) {
485- if ( ! promotionGate ) return null ;
486- const state = promotionGate . getState ( ) ;
487- return {
488- mode : activeMode ,
489- promoted : ! isPassiveModeActive ( ) ,
490- stableFrames : state . stableFrames ,
491- requiredStableFrames : state . stabilityWindowFrames ,
492- reason : state . lastReason
493- } ;
494- }
495-
496- function getPromotionMetrics ( ) {
497- return promotionGate ? promotionGate . getMetrics ( ) : null ;
498- }
499-
500645 const publicApi = {
501646 getSnapshot,
502647 getReadonlyView,
@@ -505,10 +650,7 @@ function createWorldGameStateSystem(options = {}) {
505650 applyExternalSnapshotPatch,
506651 getTransitionNames,
507652 getSelectorNames,
508- getFeatureGates,
509- getMode,
510- getPromotionReadiness,
511- getPromotionMetrics
653+ getFeatureGates
512654 } ;
513655
514656 function getPublicApi ( ) {
0 commit comments