@@ -229,3 +229,195 @@ exports.resolveStringConfig = function resolveStringConfig({ envVar, configValue
229229 }
230230 return defaultValue ;
231231} ;
232+
233+ // ============================================================================
234+ // Dynamic Config Resolution
235+ // ============================================================================
236+
237+ /**
238+ * @enum {string}
239+ */
240+ const SOURCE = {
241+ ENV : 'ENV' ,
242+ IN_CODE : 'IN_CODE' ,
243+ AGENT : 'AGENT' ,
244+ DEFAULT : 'DEFAULT'
245+ } ;
246+
247+ /**
248+ * Configuration source priority levels
249+ * Higher number = higher priority in precedence resolution
250+ *
251+ * To change precedence, simply modify the numbers here.
252+ * For example, to make AGENT higher priority than IN_CODE:
253+ * AGENT: 3, IN_CODE: 2
254+ *
255+ * @type {Record<string, number> }
256+ */
257+ const SOURCE_PRIORITY = {
258+ [ SOURCE . ENV ] : 4 ,
259+ [ SOURCE . IN_CODE ] : 3 ,
260+ [ SOURCE . AGENT ] : 2 ,
261+ [ SOURCE . DEFAULT ] : 1
262+ } ;
263+ /**
264+ * @typedef {Object } TypeSchema
265+ * @property {function(any): any } coerce
266+ * @property {function(any): boolean } validate
267+ */
268+
269+ /** @type {Object.<string, TypeSchema> } */
270+ const TypeSchemas = {
271+ STR : {
272+ coerce : v => ( v != null ? String ( v ) . trim ( ) : null ) ,
273+ validate : v => typeof v === 'string' && v . length > 0 && v !== 'null' && v !== 'undefined'
274+ } ,
275+ NUM : {
276+ coerce : v => ( v !== '' ? Number ( v ) : NaN ) ,
277+ validate : v => typeof v === 'number' && ! isNaN ( v )
278+ } ,
279+ BOOL : {
280+ coerce : v => {
281+ if ( typeof v === 'boolean' ) return v ;
282+ if ( v === 'true' || v === '1' ) return true ;
283+ if ( v === 'false' || v === '0' ) return false ;
284+ return null ;
285+ } ,
286+ validate : v => typeof v === 'boolean'
287+ }
288+ } ;
289+
290+ /**
291+ * @typedef {Object } ConfigEntry
292+ * @property {any } value - The resolved configuration value
293+ * @property {string } source - The source name (ENV, IN_CODE, AGENT, DEFAULT)
294+ */
295+
296+ /**
297+ * Central configuration state store
298+ * @type {Object.<string, ConfigEntry> }
299+ */
300+ const configStore = { } ;
301+
302+ /**
303+ * Resolves a configuration value during initial normalization
304+ * Follows the priority order: ENV > IN_CODE > DEFAULT
305+ *
306+ * @param {Object } params
307+ * @param {string } params.key - Configuration key name
308+ * @param {string } params.envKey - Environment variable name
309+ * @param {any } params.inCodeValue - User-provided in-code value
310+ * @param {any } params.defaultValue - Default fallback value
311+ * @param {'STR'|'NUM'|'BOOL' } [params.type='STR'] - Value type
312+ * @returns {any } The resolved configuration value
313+ */
314+ exports . get = function get ( { key, envKey, inCodeValue, defaultValue, type = 'STR' } ) {
315+ const schema = TypeSchemas [ type ] ;
316+ if ( ! schema ) {
317+ logger . warn ( `Unknown type "${ type } " for config key "${ key } ". Defaulting to STR.` ) ;
318+ return defaultValue ;
319+ }
320+
321+ // Resolution order: ENV > IN_CODE > DEFAULT
322+ const sources = [
323+ { name : SOURCE . ENV , value : process . env [ envKey ] } ,
324+ { name : SOURCE . IN_CODE , value : inCodeValue } ,
325+ { name : SOURCE . DEFAULT , value : defaultValue }
326+ ] ;
327+
328+ // eslint-disable-next-line no-restricted-syntax
329+ for ( const source of sources ) {
330+ if ( source . value === undefined || source . value === null ) {
331+ continue ;
332+ }
333+
334+ const coerced = schema . coerce ( source . value ) ;
335+ if ( schema . validate ( coerced ) ) {
336+ configStore [ key ] = {
337+ value : coerced ,
338+ source : source . name
339+ } ;
340+
341+ logger . debug ( `[config] Resolved "${ key } " from ${ source . name } : ${ JSON . stringify ( coerced ) } )` ) ;
342+
343+ return coerced ;
344+ }
345+
346+ logger . warn ( `[config] Invalid ${ type } value for "${ key } " from ${ source . name } : ${ JSON . stringify ( source . value ) } ` ) ;
347+ }
348+
349+ logger . debug ( `[config] No valid value found for "${ key } ", using default: ${ JSON . stringify ( defaultValue ) } ` ) ;
350+ return defaultValue ;
351+ } ;
352+
353+ /**
354+ * Updates a configuration value dynamically (e.g., from agent)
355+ * Respects the precedence hierarchy
356+ *
357+ * @param {Object } params
358+ * @param {string } params.key - Configuration key name
359+ * @param {any } params.newValue - New value to set
360+ * @param {string } params.sourceName - Source name (must be a valid SOURCE key)
361+ * @param {'STR'|'NUM'|'BOOL' } [params.type='STR'] - Value type
362+ * @returns {any } The current configuration value (may be unchanged if update was rejected)
363+ */
364+ exports . update = function update ( { key, newValue, sourceName, type = 'STR' } ) {
365+ const schema = TypeSchemas [ type ] ;
366+ if ( ! schema ) {
367+ logger . warn ( `Unknown type "${ type } " for config key "${ key } ". Update rejected.` ) ;
368+ return configStore [ key ] ?. value || null ;
369+ }
370+
371+ const current = configStore [ key ] ;
372+ const incomingPriority = SOURCE_PRIORITY [ sourceName ] ;
373+
374+ if ( incomingPriority === undefined ) {
375+ logger . warn ( `Invalid source name "${ sourceName } " for config key "${ key } ". Update rejected.` ) ;
376+ return current ?. value || null ;
377+ }
378+
379+ if ( current ) {
380+ const currentPriority = SOURCE_PRIORITY [ current . source ] ;
381+ if ( incomingPriority <= currentPriority ) {
382+ logger . info (
383+ `[config] Rejected "${ key } " update from ${ sourceName } (priority: ${ incomingPriority } ): ` +
384+ `${ current . source } (priority: ${ currentPriority } ) has higher or equal precedence. ` +
385+ `Current value: ${ JSON . stringify ( current . value ) } `
386+ ) ;
387+ return current . value ;
388+ }
389+ }
390+
391+ const coerced = schema . coerce ( newValue ) ;
392+ if ( ! schema . validate ( coerced ) ) {
393+ logger . warn (
394+ `[config] Rejected "${ key } " update from ${ sourceName } : Invalid ${ type } value: ${ JSON . stringify ( newValue ) } `
395+ ) ;
396+ return current ?. value || null ;
397+ }
398+
399+ configStore [ key ] = {
400+ value : coerced ,
401+ source : sourceName
402+ } ;
403+
404+ logger . info (
405+ `[config] Updated "${ key } " from ${ sourceName } : ${ JSON . stringify ( coerced ) } (priority: ${ incomingPriority } )`
406+ ) ;
407+
408+ return coerced ;
409+ } ;
410+
411+ /**
412+ * @param {string } key
413+ * @returns {ConfigEntry|null }
414+ */
415+ exports . getConfigEntry = function getConfigEntry ( key ) {
416+ return configStore [ key ] || null ;
417+ } ;
418+
419+ exports . clearConfigStore = function clearConfigStore ( ) {
420+ Object . keys ( configStore ) . forEach ( key => {
421+ delete configStore [ key ] ;
422+ } ) ;
423+ } ;
0 commit comments