@ -110,7 +110,7 @@ export function buildConfigState(cat: Cat): ConfigState {
* // state: { 'ct:cat/ob:bmw/at:color': 'rojo' }
* // resultado: { 'ct:cat': { 'ob:bmw': { 'at:color': 'rojo' } } }
* /
function buildNestedState ( state : ConfigState ) : Record < string , unknown > {
export function buildNestedState ( state : ConfigState ) : Record < string , unknown > {
const root : Record < string , unknown > = { } ;
for ( const [ key , value ] of Object . entries ( state ) ) {
@ -414,6 +414,28 @@ function applyOptionLogic(
return state ;
}
/ * *
* Eval ú a las rules de options de un att y aplica la l ó gica resultante
* al EffectiveState . Factorizado para evitar duplicaci ó n en evaluateObj .
* /
function applyOptionsForAtt (
att : Att ,
attPath : string ,
opts : Opt [ ] ,
ctx : EvalContext ,
resolvedEngine : JsonLogicEngine ,
effectiveState : EffectiveState ,
) : void {
if ( att . value . domain . kind !== 'option' || att . value . domain . mode !== 'list' ) return ;
const { validOptIds , invalidOptIds } = evaluateOptions (
att . value . domain as AttOptionDomain & { mode : 'list' } , opts , ctx , resolvedEngine ,
) ;
const current = effectiveState . get ( attPath ) ? ? defaultEffectiveState ( ) ;
effectiveState . set ( attPath , applyOptionLogic (
att . value as AttValueOption , validOptIds , invalidOptIds , current ,
) ) ;
}
export function evaluateAtt (
att : Att ,
prefix : string ,
@ -515,16 +537,7 @@ export function evaluateObj(
if ( ! effectiveState . has ( attPath ) ) {
effectiveState . set ( attPath , defaultEffectiveState ( ) ) ;
}
// Evaluar options si el dominio es option/list
if ( att . value . domain . kind === 'option' && att . value . domain . mode === 'list' ) {
const { validOptIds , invalidOptIds } = evaluateOptions (
att . value . domain as AttOptionDomain & { mode : 'list' } , opts , ctx , resolvedEngine ,
) ;
const current = effectiveState . get ( attPath ) ? ? defaultEffectiveState ( ) ;
effectiveState . set ( attPath , applyOptionLogic (
att . value as AttValueOption , validOptIds , invalidOptIds , current ,
) ) ;
}
applyOptionsForAtt ( att , attPath , opts , ctx , resolvedEngine , effectiveState ) ;
}
for ( const sec of obj . secs ) {
@ -544,16 +557,7 @@ export function evaluateObj(
if ( ! effectiveState . has ( attPath ) ) {
effectiveState . set ( attPath , defaultEffectiveState ( ) ) ;
}
// Evaluar options si el dominio es option/list
if ( att . value . domain . kind === 'option' && att . value . domain . mode === 'list' ) {
const { validOptIds , invalidOptIds } = evaluateOptions (
att . value . domain as AttOptionDomain & { mode : 'list' } , opts , ctx , resolvedEngine ,
) ;
const current = effectiveState . get ( attPath ) ? ? defaultEffectiveState ( ) ;
effectiveState . set ( attPath , applyOptionLogic (
att . value as AttValueOption , validOptIds , invalidOptIds , current ,
) ) ;
}
applyOptionsForAtt ( att , attPath , opts , ctx , resolvedEngine , effectiveState ) ;
}
}
@ -595,6 +599,19 @@ export function evaluateCat(cat: Cat, engine: JsonLogicEngine | null = null): Ev
const nestedState = buildNestedState ( state ) ;
const nextEffective = new Map < string , AttEffectiveState > ( ) ;
// Evaluar atributos directos del Cat
const catCtx : EvalContext = { state , nestedState , prefix : cat.id } ;
const catValuePriorities = new Map < string , { priority : number } > ( ) ;
for ( const att of cat . atts ) {
const attPath = joinPath ( cat . id , att . id ) ;
if ( att . rules ) {
applyRuleSet ( att . rules , catCtx , nextEffective , attPath , cat . id , catValuePriorities , resolvedEngine ) ;
}
if ( ! nextEffective . has ( attPath ) ) {
nextEffective . set ( attPath , defaultEffectiveState ( ) ) ;
}
}
for ( const obj of cat . objs ) {
const objResult = evaluateObj ( obj , cat . id , state , resolvedEngine , cat . opts ? ? [ ] , nestedState ) ;
for ( const [ k , v ] of objResult ) {
@ -619,62 +636,35 @@ export function evaluateCat(cat: Cat, engine: JsonLogicEngine | null = null): Ev
}
// Si no hay valores derivados que hayan cambiado el estado → convergió
if ( JSON . stringify ( nextState ) === JSON . stringify ( state ) ) break ;
const keys = Object . keys ( nextState ) ;
if ( ! keys . some ( k = > nextState [ k ] !== state [ k ] ) ) break ;
state = nextState ;
}
// ── Capa 0: validación de dominio contra effectiveState final ──────────
const errors : string [ ] = [ ] ;
const warnings : string [ ] = [ ] ;
const errors = new Set < string > ( ) ;
const warnings = new Set < string > ( ) ;
// Validar atributos directos del Cat
for ( const att of cat . atts ) {
const path = joinPath ( cat . id , att . id ) ;
validateAttPath ( path , att , effectiveState , errors , warnings ) ;
}
for ( const obj of cat . objs ) {
const allAtts = collectAtts ( obj , cat . id ) ;
for ( const { path , att } of allAtts ) {
const es = effectiveState . get ( path ) ? ? defaultEffectiveState ( ) ;
// Atributo forbidden con valor → error
if ( es . state === 'forbidden' && att . value . value !== undefined && att . value . value !== null ) {
errors . push ( path ) ;
continue ;
}
// Atributo hidden o forbidden → no validar valor
if ( es . state === 'hidden' || es . state === 'forbidden' ) continue ;
// Valor en dominio efectivo (aplicar forbiddenValues)
const effectiveValue = es . derivedValue ? ? att . value ;
const domainResult = validateAttValue ( effectiveValue ) ;
if ( ! domainResult . valid ) {
errors . push ( path ) ;
continue ;
}
// Valor en forbiddenValues
const rawValue = effectiveValue . value ;
const values = Array . isArray ( rawValue ) ? rawValue : [ rawValue ] ;
const forbidden = values . some ( v = > es . forbiddenValues . includes ( v as DomainValueRef ) ) ;
if ( forbidden ) {
errors . push ( path ) ;
continue ;
}
// Mensajes de validación acumulados
for ( const msg of es . messages ) {
if ( msg . level === 'error' ) errors . push ( path ) ;
if ( msg . level === 'warning' ) warnings . push ( path ) ;
}
validateAttPath ( path , att , effectiveState , errors , warnings ) ;
}
}
return {
effectiveState ,
valid : errors. length === 0 ,
errors : [ . . . new Set ( errors ) ] ,
warnings : [ . . . new Set ( warnings ) ] ,
valid : errors.size === 0 ,
errors : [ . . . errors ] ,
warnings : [ . . . warnings ] ,
iterations ,
} ;
}
@ -683,6 +673,43 @@ export function evaluateCat(cat: Cat, engine: JsonLogicEngine | null = null): Ev
// INTERNAL HELPERS
// ─────────────────────────────────────────────────────────────────────────────
/** Valida un Att individual contra el EffectiveState y acumula errors/warnings */
function validateAttPath (
path : string ,
att : Att ,
effectiveState : EffectiveState ,
errors : Set < string > ,
warnings : Set < string > ,
) : void {
const es = effectiveState . get ( path ) ? ? defaultEffectiveState ( ) ;
// Atributo forbidden con valor → error
if ( es . state === 'forbidden' && att . value . value !== undefined && att . value . value !== null ) {
errors . add ( path ) ;
return ;
}
// Atributo hidden o forbidden → no validar valor
if ( es . state === 'hidden' || es . state === 'forbidden' ) return ;
// Valor en dominio efectivo
const effectiveValue = es . derivedValue ? ? att . value ;
const domainResult = validateAttValue ( effectiveValue ) ;
if ( ! domainResult . valid ) { errors . add ( path ) ; return ; }
// Valor en forbiddenValues
const rawValue = effectiveValue . value ;
const values = Array . isArray ( rawValue ) ? rawValue : [ rawValue ] ;
const forbidden = values . some ( v = > es . forbiddenValues . includes ( v as DomainValueRef ) ) ;
if ( forbidden ) { errors . add ( path ) ; return ; }
// Mensajes de validación acumulados
for ( const msg of es . messages ) {
if ( msg . level === 'error' ) errors . add ( path ) ;
if ( msg . level === 'warning' ) warnings . add ( path ) ;
}
}
/** Recorre un Obj y devuelve todos sus Att con su path absoluto */
function collectAtts ( obj : Obj , catId : string ) : Array < { path : string ; att : Att } > {
const result : Array < { path : string ; att : Att } > = [ ] ;