\ No newline at end of file
diff --git a/src/vice/adapters/svelte/vice_store_svelte.test.ts b/src/vice/adapters/svelte/vice_store_svelte.test.ts
deleted file mode 100644
index 9ff41f8..0000000
--- a/src/vice/adapters/svelte/vice_store_svelte.test.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · ViceStore Tests
-// ─────────────────────────────────────────────────────────────────────────────
-
-import { describe, it, expect, beforeEach } from 'vitest'
-import { fixture } from '@/vice/libs//fixture.ts'
-import { createViceStore } from './ViceStore.svelte.ts'
-import type { ViceStore } from './ViceStore.svelte.ts'
-
-let vice: ViceStore
-
-beforeEach(() => {
- vice = createViceStore(fixture, 'en_bmw430')
-})
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Inicialización
-// ─────────────────────────────────────────────────────────────────────────────
-
-describe('inicialización', () => {
- it('DEBUG — violaciones tras seleccionar op_coupe', () => {
- vice.select('en_bmw430/at_version', 'op_coupe' as any)
- console.log('consistent:', vice.consistent)
- console.log('violations:', vice.violations)
- console.log('state attributes:')
- for (const [path, attr] of vice.state.attributes) {
- if (attr.violations.length > 0 || !attr.active) {
- console.log(' ', path, '| active:', attr.active, '| domain:', [...attr.domain], '| violations:', attr.violations)
- }
- }
- expect(true).toBe(true)
- })
-
- it('loadResult.valid = true para fixture válido', () => {
- expect(vice.loadResult.valid).toBe(true)
- })
-
- it('lanza si el catálogo tiene errores de diseño', () => {
- const bad = {
- ...fixture,
- entities: {
- 'en_bad': {
- id: 'en_bad' as any,
- code: 'bad',
- name: { es: 'Bad' },
- description: { es: '' },
- attributes: [{
- id: 'at_x' as any,
- code: 'x',
- type: 'required' as const,
- name: { es: 'X' },
- description: { es: '' },
- display: {},
- default: 'op_inexistente' as any,
- options: [{ optionId: 'op_blanco' as any }],
- }],
- groups: {},
- },
- },
- }
- expect(() => createViceStore(bad as any, 'en_bad' as any)).toThrow('[VICE]')
- })
-
- it('estado inicial es consistente', () => {
- expect(vice.consistent).toBe(true)
- })
-
- it('estado inicial sin violaciones', () => {
- expect(vice.hasViolations).toBe(false)
- })
-
- it('at_version empieza con default op_berlina', () => {
- expect(vice.currentOption('en_bmw430/at_version')).toBe('op_berlina')
- })
-
- it('gr_extras empieza inactivo', () => {
- expect(vice.isActive('en_bmw430/gr_extras')).toBe(false)
- })
-
- it('gr_motor empieza activo', () => {
- expect(vice.isActive('en_bmw430/gr_motor')).toBe(true)
- })
-})
-
-// ─────────────────────────────────────────────────────────────────────────────
-// select
-// ─────────────────────────────────────────────────────────────────────────────
-
-describe('select', () => {
-
- it('devuelve ok=true para selección válida', () => {
- const result = vice.select('en_bmw430/at_version', 'op_coupe' as any)
- expect(result.ok).toBe(true)
- })
-
- it('actualiza state tras selección', () => {
- vice.select('en_bmw430/at_version', 'op_coupe' as any)
- expect(vice.currentOption('en_bmw430/at_version')).toBe('op_coupe')
- })
-
- it('propaga — seleccionar coupe activa gr_extras', () => {
- vice.select('en_bmw430/at_version', 'op_coupe' as any)
- expect(vice.isActive('en_bmw430/gr_extras')).toBe(true)
- })
-
- it('propaga — seleccionar electrico restringe potencia', () => {
- vice.select('en_bmw430/gr_motor/at_combustible', 'op_electrico' as any)
- const domain = vice.domain('en_bmw430/gr_motor/at_potencia')
- expect(domain.has('op_150cv' as any)).toBe(false)
- expect(domain.has('op_190cv' as any)).toBe(false)
- })
-
- it('devuelve ok=false con INVALID_PATH', () => {
- const result = vice.select('en_bmw430/at_inexistente', 'op_x' as any)
- expect(result.ok).toBe(false)
- if (!result.ok) expect(result.reason).toBe('INVALID_PATH')
- })
-
- it('devuelve ok=false con INACTIVE_ATTRIBUTE', () => {
- const result = vice.select('en_bmw430/gr_tecnologia/at_sonido', 'op_harman' as any)
- expect(result.ok).toBe(false)
- if (!result.ok) expect(result.reason).toBe('INACTIVE_ATTRIBUTE')
- })
-
- it('devuelve ok=false con OPTION_NOT_IN_DOMAIN', () => {
- vice.select('en_bmw430/gr_motor/at_combustible', 'op_electrico' as any)
- const result = vice.select('en_bmw430/gr_motor/at_potencia', 'op_150cv' as any)
- expect(result.ok).toBe(false)
- if (!result.ok) expect(result.reason).toBe('OPTION_NOT_IN_DOMAIN')
- })
-
- it('guarda lastError tras selección fallida', () => {
- vice.select('en_bmw430/at_inexistente', 'op_x' as any)
- expect(vice.lastError).not.toBeNull()
- expect(vice.lastError?.ok).toBe(false)
- })
-
- it('limpia lastError tras selección exitosa', () => {
- vice.select('en_bmw430/at_inexistente', 'op_x' as any)
- vice.select('en_bmw430/at_version', 'op_coupe' as any)
- expect(vice.lastError).toBeNull()
- })
-
- it('guarda lastResult tras selección exitosa', () => {
- vice.select('en_bmw430/at_version', 'op_coupe' as any)
- expect(vice.lastResult).not.toBeNull()
- expect(vice.lastResult?.consistent).toBe(false)
- })
-})
-
-// ─────────────────────────────────────────────────────────────────────────────
-// deselect
-// ─────────────────────────────────────────────────────────────────────────────
-
-describe('deselect', () => {
-
- it('devuelve ok=false con INVALID_PATH', () => {
- const result = vice.deselect('en_bmw430/at_inexistente')
- expect(result.ok).toBe(false)
- if (!result.ok) expect(result.reason).toBe('INVALID_PATH')
- })
-
- it('pone current = null tras deselect', () => {
- vice.deselect('en_bmw430/at_version')
- expect(vice.currentOption('en_bmw430/at_version')).toBeNull()
- })
-})
-
-// ─────────────────────────────────────────────────────────────────────────────
-// reset
-// ─────────────────────────────────────────────────────────────────────────────
-
-describe('reset', () => {
-
- it('vuelve al estado inicial', () => {
- vice.select('en_bmw430/at_version', 'op_coupe' as any)
- vice.select('en_bmw430/gr_motor/at_combustible', 'op_electrico' as any)
- vice.reset()
-
- expect(vice.currentOption('en_bmw430/at_version')).toBe('op_berlina')
- expect(vice.currentOption('en_bmw430/gr_motor/at_combustible')).toBe('op_gasolina')
- expect(vice.isActive('en_bmw430/gr_extras')).toBe(false)
- })
-
- it('limpia lastResult y lastError tras reset', () => {
- vice.select('en_bmw430/at_version', 'op_coupe' as any)
- vice.reset()
- expect(vice.lastResult).toBeNull()
- expect(vice.lastError).toBeNull()
- })
-})
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Helpers
-// ─────────────────────────────────────────────────────────────────────────────
-
-describe('helpers', () => {
-
- it('getAttribute devuelve el estado del atributo', () => {
- const attr = vice.getAttribute('en_bmw430/at_version')
- expect(attr).not.toBeNull()
- expect(attr?.attrId).toBe('at_version')
- })
-
- it('getAttribute devuelve null para path inexistente', () => {
- expect(vice.getAttribute('en_bmw430/at_inexistente')).toBeNull()
- })
-
- it('getGroup devuelve el estado del grupo', () => {
- const group = vice.getGroup('en_bmw430/gr_motor')
- expect(group).not.toBeNull()
- expect(group?.groupId).toBe('gr_motor')
- })
-
- it('domain devuelve set vacío para path inexistente', () => {
- expect(vice.domain('en_bmw430/at_inexistente').size).toBe(0)
- })
-
- it('violations agrupa todas las violaciones con path', () => {
- expect(Array.isArray(vice.violations)).toBe(true)
- })
-})
-
diff --git a/src/vice/editor/comps/AttributeDetail.svelte b/src/vice/editor/comps/AttributeDetail.svelte
deleted file mode 100644
index b19645a..0000000
--- a/src/vice/editor/comps/AttributeDetail.svelte
+++ /dev/null
@@ -1,378 +0,0 @@
-
-
-
-
-
-
-
{attrDisplayName}
- {attr.id}
-
-
-
-
-
-
-
- Tipo
-
- {#each (['required', 'optional', 'fixed'] as const) as t}
-
- {/each}
-
-
- {#if attr.type === 'required'}El usuario debe elegir. null = configuración incompleta.
- {:else if attr.type === 'optional'}El usuario activa o desactiva.
- {:else}Valor constante. Puede ser fuente de constraints.{/if}
-
-
-
-
-
- Comportamiento
- {#each ([
- ['uiVisible', 'Visible en UI', '— Mostrar en el configurador'],
- ['affectsVisual', 'Afecta a la imagen', '— Cambiar esta opción actualiza el render'],
- ['affectsPrice', 'Afecta al precio', '— Cambiar esta opción recalcula el precio'],
- ] as const) as [key, label, hint]}
-
- {/each}
-
-
-
- {#if hasOptions(attr)}
- {@const attrWithOpts = attr as RequiredAttribute}
-
-
- Opciones ({attrWithOpts.options.length})
-
-
- {#if attrWithOpts.options.length === 0}
- Sin opciones — añade desde la librería
- {:else}
-
- {#each attrWithOpts.options as attrOpt (attrOpt.optionId)}
- {@const price = optPrice(attrOpt.optionId)}
-
-
- {optName(attrOpt.optionId)}
-
- {editor.collection.options[attrOpt.optionId]?.code ?? ''}
-
-
-
- {#if price}
- {price}
- {/if}
-
-
-
- {/each}
-
- {/if}
-
-
-
-
-
- {#if optPickerOpen}
-
-
-
-
-
-
- {#if filteredPicker.length === 0}
- -
- {allOptions.length === 0 ? 'No hay opciones — créalas en el panel Opciones' : 'Sin resultados'}
-
- {:else}
- {#each filteredPicker as opt (opt.id)}
- -
-
-
- {/each}
- {/if}
-
-
- {/if}
-
-
-
- {#if attrWithOpts.options.length > 0}
-
-
-
-
- {/if}
-
- {/if}
-
-
- {#if attr.type === 'fixed'}
- {@const fixedAttr = attr as FixedAttribute}
-
- Valor fijo
-
-
- El valor constante no puede ser modificado por el usuario.
-
-
- {/if}
-
-
- {#if entity}
-
-
- Reglas
-
- ({((attr as any).rules ?? []).length})
-
-
-
-
- {:else}
-
- Reglas
-
- Los atributos de catálogo no tienen reglas — son el nivel raíz.
-
-
- {/if}
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/AttributeList.svelte b/src/vice/editor/comps/AttributeList.svelte
deleted file mode 100644
index 73b47fa..0000000
--- a/src/vice/editor/comps/AttributeList.svelte
+++ /dev/null
@@ -1,141 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Atributos ({group.attributes.length})
-
-
-
-
-
- {#if group.attributes.length === 0}
-
-
Sin atributos
-
Suelo, color, material...
-
-
- {:else}
- {#each group.attributes as attr (attr.id)}
- {@const isActive = editor.activeAttrId === attr.id}
- {@const hasError = editor.validation.errors.some(e => e.path.includes(attr.id))}
- {@const hasWarn = editor.validation.warnings.some(e => e.path.includes(attr.id))}
- {@const name = ling(attr.name as Record
, langs[0])}
- editor.setActiveAttribute(attr.id)}
- onkeydown={(e) => e.key === 'Enter' && editor.setActiveAttribute(attr.id)}
- class="
- w-full text-left px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer
- transition-colors group border-l-2
- {isActive
- ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500'
- : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}
- "
- >
-
- {typeLabel[attr.type] ?? attr.type}
-
-
- {#if hasError}
-
- {:else if hasWarn}
-
- {/if}
-
-
- {#if name}{name}{:else}Sin nombre{/if}
-
-
- {#if attr.type === 'required' || attr.type === 'optional'}
-
- {attr.options?.length ?? 0}
-
- {/if}
-
-
-
- {/each}
- {/if}
-
-
-
-
-
- {#if editor.activeAttribute}
-
- {:else}
-
- Selecciona un atributo para editarlo
-
- {/if}
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/CollectionPanel.svelte b/src/vice/editor/comps/CollectionPanel.svelte
deleted file mode 100644
index ca68c2a..0000000
--- a/src/vice/editor/comps/CollectionPanel.svelte
+++ /dev/null
@@ -1,339 +0,0 @@
-
-
-
-
-
-
-
Configuración de la colección
- {col.id}
-
-
-
-
- {#each [
- { label: 'Entidades', value: stats.entities },
- { label: 'Grupos', value: stats.groups },
- { label: 'Atributos', value: stats.attributes },
- { label: 'Opciones', value: stats.options },
- ] as stat}
-
- {stat.value}
- {stat.label}
-
- {/each}
-
-
-
-
- Identificación
-
-
-
- patch('code', e.currentTarget.value)}
- placeholder="viviendas_vice"
- class="field-input font-mono text-xs" />
-
-
-
-
-
-
- {#each col.langs as lang}
-
- {lang}
- patchI18n('name', lang, e.currentTarget.value)}
- placeholder={lang === 'es' ? 'Viviendas VICE' : ''}
- class="field-input flex-1" />
-
- {/each}
-
-
-
-
-
-
-
- {#each col.langs as lang}
-
- {lang}
-
-
- {/each}
-
-
-
-
-
-
- Idiomas soportados
-
- {#each ALL_LANGS as lang}
- {@const active = col.langs.includes(lang)}
- {@const required = lang === 'es'}
-
- {/each}
-
- * Español es obligatorio y no puede desactivarse
-
-
-
-
-
-
- Atributos de contexto
- ({col.attributes.length})
-
-
-
-
- {#if col.attributes.length === 0}
-
- Sin atributos — añade variables de contexto como país, normativa fiscal, año...
-
- {:else}
-
- {#each col.attributes as attr}
- {@const lit = asLiteral(attr)}
-
- {/each}
-
- {/if}
-
-
-
-
-
-
- {#if editor.validation.errors.filter(e => e.path.startsWith('collection')).length > 0}
-
- Errores
- {#each editor.validation.errors.filter(e => e.path.startsWith('collection')) as error}
-
-
{error.message}
-
{error.path}
-
- {/each}
-
- {/if}
-
-
-
- Importar / Resetear
-
-
-
-
-
- {#if showImport}
-
-
- {#if importError}
-
{importError}
- {/if}
-
-
-
-
-
- {/if}
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/ConditionBuilder.svelte b/src/vice/editor/comps/ConditionBuilder.svelte
deleted file mode 100644
index 4e7423d..0000000
--- a/src/vice/editor/comps/ConditionBuilder.svelte
+++ /dev/null
@@ -1,431 +0,0 @@
-
-
-
-
-
-
-
Condición
-
- {#if !rawMode && activeClauseCount === 0}
- siempre activa
- {/if}
- {#if parseFailed}
- JsonLogic complejo
- {:else}
-
- {/if}
-
-
-
- {#if rawMode || parseFailed}
-
-
-
-
- JsonLogic — variables son AttrIDs. Vacío = siempre activa.
-
-
- {:else}
-
-
-
-
- {#if draft.clauses.length > 1}
-
- Combinar con
- {#each (['and', 'or'] as const) as l}
-
- {/each}
-
- {/if}
-
-
- {#each draft.clauses as clause, i (i)}
- {@const kind = getAttrKind(clause.attrId)}
- {@const ops = OPS[kind]}
- {@const options = getAttrOptions(clause.attrId)}
- {@const multi = clause.op === 'in' || clause.op === '!in'}
-
-
- {/each}
-
-
-
-
- {/if}
-
-
diff --git a/src/vice/editor/comps/EntitiesPanel.svelte b/src/vice/editor/comps/EntitiesPanel.svelte
deleted file mode 100644
index cea9847..0000000
--- a/src/vice/editor/comps/EntitiesPanel.svelte
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {#if editor.activeEntity}
-
-
-
- {/if}
-
-
-
- {#if editor.activeGroup}
-
- {:else if editor.activeAttrId && editor.activeEntity}
-
-
- {:else if editor.activeEntity}
-
- Selecciona un grupo o un atributo de entidad
-
- {:else}
-
- Selecciona o crea una entidad
-
- {/if}
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/EntityAttributeList.svelte b/src/vice/editor/comps/EntityAttributeList.svelte
deleted file mode 100644
index 51623b1..0000000
--- a/src/vice/editor/comps/EntityAttributeList.svelte
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
-
-
- {#each sections as section (section.key)}
-
-
-
-
- {section.label}
- ({section.attrs.length})
-
-
-
-
-
- {#if section.attrs.length === 0}
-
- Sin atributos
-
- {:else}
- {#each section.attrs as attr (attr.id)}
- {@const isActive = editor.activeAttrId === attr.id}
- {@const hasError = editor.validation.errors.some(e => e.path.includes(attr.id))}
- {@const hasWarn = editor.validation.warnings.some(e => e.path.includes(attr.id))}
- {@const name = ling(attr.name as Record
, langs[0])}
- editor.setActiveAttribute(attr.id)}
- onkeydown={(e) => e.key === 'Enter' && editor.setActiveAttribute(attr.id)}
- class="
- w-full text-left px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer
- transition-colors group border-l-2
- {isActive
- ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500'
- : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}
- "
- >
-
- {typeLabel[attr.type] ?? attr.type}
-
-
- {#if hasError}
-
- {:else if hasWarn}
-
- {/if}
-
-
- {#if name}{name}{:else}Sin nombre{/if}
-
-
- {#if attr.type === 'required' || attr.type === 'optional'}
-
- {(attr as any).options?.length ?? 0}
-
- {/if}
-
-
-
- {/each}
- {/if}
-
- {/each}
-
-
-
-
-
- {#if editor.activeAttribute}
-
- {:else}
-
- Selecciona un atributo para editarlo
-
- {/if}
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/EntityList.svelte b/src/vice/editor/comps/EntityList.svelte
deleted file mode 100644
index 232097e..0000000
--- a/src/vice/editor/comps/EntityList.svelte
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
- Entidades ({entities.length})
-
-
-
-
-
- {#if entities.length === 0}
-
-
Sin entidades
-
- Una entidad es un producto configurable — apartamento, coche, habitación...
-
-
-
- {:else}
-
- {/if}
-
diff --git a/src/vice/editor/comps/EntityMetaForm.svelte b/src/vice/editor/comps/EntityMetaForm.svelte
deleted file mode 100644
index 16c83af..0000000
--- a/src/vice/editor/comps/EntityMetaForm.svelte
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
diff --git a/src/vice/editor/comps/GroupList.svelte b/src/vice/editor/comps/GroupList.svelte
deleted file mode 100644
index fdf119c..0000000
--- a/src/vice/editor/comps/GroupList.svelte
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-
-
-
-
- {#if showMeta}
-
-
-
- {/if}
-
-
-
-
-
-
-
- Grupos ({groups.length})
-
-
-
-
-
-
-
- {#if groups.length === 0}
-
- Sin grupos
-
-
- {:else}
-
- {#each groups as group (group.id)}
- -
-
editor.setActiveGroup(group.id)}
- onkeydown={(e) => e.key === 'Enter' && editor.setActiveGroup(group.id)}
- class="
- w-full text-left px-4 py-2.5 flex items-center gap-2 text-sm cursor-pointer
- transition-colors group
- {editor.activeGroupId === group.id
- ? 'bg-indigo-600/20 text-indigo-300'
- : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'}
- "
- >
-
- {group.kind === 'fixed' ? '◉' : '◈'}
-
-
- {#if editor.validation.errors.some(e => e.path.includes(group.id))}
-
- {:else if editor.validation.warnings.some(e => e.path.includes(group.id))}
-
- {/if}
-
-
- {#if ling(group.name as Record)}
- {ling(group.name as Record)}
- {:else}
- Sin nombre
- {/if}
-
-
- {group.attributes.length}
-
-
-
-
- {/each}
-
- {/if}
-
-
-
-
- Attrs entidad ({entity.attributes.length})
-
-
-
-
- {#if entity.attributes.length === 0}
-
- Sin atributos de entidad
-
- {:else}
- {#each entity.attributes as attr (attr.id)}
- {@const isActive = editor.activeAttrId === attr.id}
- {@const name = ling(attr.name as Record
)}
- editor.setActiveEntityAttribute(attr.id)}
- onkeydown={(e) => e.key === 'Enter' && editor.setActiveEntityAttribute(attr.id)}
- class="
- w-full text-left px-4 py-2 flex items-center gap-2 text-sm cursor-pointer
- transition-colors group border-l-2
- {isActive
- ? 'bg-amber-600/20 text-amber-300 border-amber-500'
- : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200 border-transparent'}
- "
- >
-
- {typeLabel[attr.type] ?? attr.type}
-
-
- {#if name}{name}{:else}Sin nombre{/if}
-
-
-
- {/each}
- {/if}
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/GroupMetaForm.svelte b/src/vice/editor/comps/GroupMetaForm.svelte
deleted file mode 100644
index 282e02d..0000000
--- a/src/vice/editor/comps/GroupMetaForm.svelte
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
- patch('code', e.currentTarget.value)}
- placeholder="salon"
- class="field-input font-mono text-xs {!group.code ? 'border-red-800' : ''}"
- />
-
-
-
-
-
-
-
-
-
-
- {#each langs as lang (lang)}
-
-
- patchI18n('name', lang, e.currentTarget.value)}
- placeholder={lang === 'es' ? 'Salón' : 'Living room'}
- class="field-input {lang === 'es' && !group.name.es ? 'border-red-800' : ''}"
- />
-
- {/each}
-
-
diff --git a/src/vice/editor/comps/OptionDetail.svelte b/src/vice/editor/comps/OptionDetail.svelte
deleted file mode 100644
index 1ebcc7a..0000000
--- a/src/vice/editor/comps/OptionDetail.svelte
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
-
-
-
{ling.ts(opt.name) || 'Opción sin nombre'}
-
-
- {opt.id}
-
-
-
-
-
- Identificación
-
-
-
-
- patch('code', e.currentTarget.value)}
- placeholder="parquet_roble"
- class="field-input font-mono text-xs" />
-
-
-
-
-
-
- {#each langs as lang}
-
- {lang}
- patchI18n('name', lang, e.currentTarget.value)}
- class="field-input flex-1" />
-
- {/each}
-
-
-
-
-
-
-
- {#each langs as lang}
-
- {lang}
-
-
- {/each}
-
-
-
-
-
-
- Precio
-
-
- {#each (['fixed', 'dynamic'] as const) as type}
-
- {/each}
-
-
-
-
-
- setBaseAmount(Number(e.currentTarget.value))}
- min="0" step="0.01" class="field-input pr-8" />
- €
-
-
-
- {#if pricingType === 'dynamic'}
-
- El precio dinámico usa una expresión JsonLogic. Edítala en el JSON exportado.
-
- {/if}
-
-
-
-
-
-
- {#if usages.length > 0}
-
-
- Usada en ({usages.length})
-
-
- {#each usages as use}
-
- {use.entityName}
- ›
- {use.groupName}
- ›
- {use.attrName}
-
- {/each}
-
-
- {/if}
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/OptionsPanel.svelte b/src/vice/editor/comps/OptionsPanel.svelte
deleted file mode 100644
index 50154a3..0000000
--- a/src/vice/editor/comps/OptionsPanel.svelte
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Opciones ({options.length})
-
-
-
-
-
-
-
-
-
-
-
- {#if options.length === 0}
-
-
Sin opciones
-
Parquet, mármol, lacado blanco...
-
- {:else if filtered.length === 0}
-
Sin resultados
- {:else}
- {#each [...grouped] as [tag, opts]}
-
- {tag}
-
- {#each opts as opt (opt.id)}
-
editor.setActiveOption(opt.id)}
- onkeydown={(e) => e.key === 'Enter' && editor.setActiveOption(opt.id)}
- class="px-3 py-2 flex items-center gap-2 text-sm cursor-pointer transition-colors group
- {editor.activeOptionId === opt.id
- ? 'bg-indigo-600/20 text-indigo-300'
- : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200'}"
- >
- {#if editor.validation.errors.some(e => e.path.startsWith(`options.${opt.id}`))}
-
- {:else if editor.validation.warnings.some(e => e.path.startsWith(`options.${opt.id}`))}
-
- {:else}
-
- {/if}
-
- {ling.ts(opt.name) || 'Sin nombre'}
-
- {#if opt.pricing && !opt.pricing.dynamicExpression}
- {opt.pricing.baseAmount}€
- {/if}
-
-
-
- {/each}
- {/each}
- {/if}
-
-
-
-
-
- {#if editor.activeOption}
-
- {:else}
-
-
Selecciona o crea una opción
-
- Las opciones son los valores seleccionables — parquet, mármol, lacado blanco...
- Se definen una vez y se reutilizan en múltiples atributos.
-
-
-
- {/if}
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/RuleEditor.svelte b/src/vice/editor/comps/RuleEditor.svelte
deleted file mode 100644
index 0bbb71c..0000000
--- a/src/vice/editor/comps/RuleEditor.svelte
+++ /dev/null
@@ -1,240 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {#if groupActions.includes(rule.action.type)}
-
- {:else}
-
- {/if}
-
-
-
-
-
-
- {#if needsValues && targetAttrOptions.length > 0}
-
- {#each targetAttrOptions as opt (opt.id)}
- {@const active = (rule.action.values ?? []).includes(opt.id)}
-
- {/each}
- {#if needsValues && targetAttrOptions.length === 0}
- Selecciona un atributo target primero
- {/if}
-
- {/if}
-
-
-
- onchange({ condition: jl })}
- />
-
-
-
-
-
- {#each langs as lang (lang)}
- )?.[lang] ?? ''}
- oninput={(e) => patchMessage(lang, e.currentTarget.value)}
- placeholder="Esta opción requiere..."
- class="field-input text-xs"
- />
- {/each}
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/RuleList.svelte b/src/vice/editor/comps/RuleList.svelte
deleted file mode 100644
index 5e89cd6..0000000
--- a/src/vice/editor/comps/RuleList.svelte
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
- {#if rules.length === 0}
-
Sin reglas
- {:else}
- {#each rules as rule (rule.id)}
-
updateRule(rule.id!, patch)}
- ondelete={() => deleteRule(rule.id!)}
- />
- {/each}
- {/if}
-
-
-
-
diff --git a/src/vice/editor/comps/Sidebar.svelte b/src/vice/editor/comps/Sidebar.svelte
deleted file mode 100644
index c41a76f..0000000
--- a/src/vice/editor/comps/Sidebar.svelte
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
diff --git a/src/vice/editor/comps/TopBar.svelte b/src/vice/editor/comps/TopBar.svelte
deleted file mode 100644
index 69faeaf..0000000
--- a/src/vice/editor/comps/TopBar.svelte
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
- VICE
- |
-
- {ling.ts(editor.collection.name) || 'Nueva colección'}
-
- {#if editor.isDirty}
- ● Sin guardar
- {/if}
-
-
-
-
-
-
- {#if !editor.validation.valid}
-
-
- {editor.validation.errors.length} error{editor.validation.errors.length !== 1 ? 'es' : ''}
-
- {:else}
-
-
- Válido
-
- {/if}
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/comps/ViceEditor.svelte b/src/vice/editor/comps/ViceEditor.svelte
deleted file mode 100644
index 4479317..0000000
--- a/src/vice/editor/comps/ViceEditor.svelte
+++ /dev/null
@@ -1,191 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/src/vice/editor/layouts/EditorLayout.svelte b/src/vice/editor/layouts/EditorLayout.svelte
deleted file mode 100644
index c55b2e9..0000000
--- a/src/vice/editor/layouts/EditorLayout.svelte
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {#if editor.activePanel === 'catalog'}
-
- CatalogPanel — en construcción
- {:else if editor.activePanel === 'objects'}
-
- ObjectsPanel — en construcción
- {:else if editor.activePanel === 'options'}
-
- OptionsPanel — en construcción
- {:else if editor.activePanel === 'rules'}
-
- RulesPanel — en construcción
- {:else if editor.activePanel === 'preview'}
-
- PreviewPanel — en construcción
- {:else if editor.activePanel === 'export'}
-
- ExportPanel — en construcción
- {/if}
-
-
-
-
diff --git a/src/vice/editor/stores/EditorStore.svelte.tsa b/src/vice/editor/stores/EditorStore.svelte.tsa
deleted file mode 100644
index dd188ac..0000000
--- a/src/vice/editor/stores/EditorStore.svelte.tsa
+++ /dev/null
@@ -1,606 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Editor Store
-//
-// Store principal del editor de catálogos.
-// Gestiona el estado de edición de una Collection completa.
-// Usa runas de Svelte 5 ($state, $derived).
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type {
- Attribute,
- AttributeOption,
- AttrID,
- Collection,
- CollectionID,
- Entity,
- EntityID,
- FixedGroup,
- Group,
- GroupID,
- OptionalGroup,
- OptionDefinition,
- OptionID,
- RequiredAttribute,
-} from '@/vice/libs/types'
-import type {LingString} from "@/ling";
-
-// ─────────────────────────────────────────────────────────────────────────────
-// HELPERS
-// ─────────────────────────────────────────────────────────────────────────────
-
-function uid(prefix: string): string {
- return `${prefix}_${Math.random().toString(36).slice(2, 8)}`
-}
-
-function emptyLing(es = '', en = ''): LingString {
- return { es, en }
-}
-
-function emptyCollection(): Collection {
- return {
- id: uid('col') as CollectionID,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- attributes: [],
- options: {} as Record,
- entities: {} as Record,
- }
-}
-
-function emptyEntity(id: EntityID): Entity {
- return {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- attributes: [],
- groups: {} as Record,
- rules: [],
- }
-}
-
-function emptyGroup(id: GroupID, kind: 'fixed' | 'optional' = 'fixed'): Group {
- const base = {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- attributes: [],
- }
- if (kind === 'optional') {
- return { ...base, kind: 'optional', rules: [] } satisfies OptionalGroup
- }
- return { ...base, kind: 'fixed' } satisfies FixedGroup
-}
-
-function emptyAttribute(id: AttrID): RequiredAttribute {
- return {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- type: 'required',
- default: '' as OptionID,
- options: [],
- rules: [],
- display: {},
- }
-}
-
-function emptyOption(id: OptionID): OptionDefinition {
- return {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- tags: [],
- }
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// VALIDATION
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface EditorIssue {
- severity: 'error' | 'warning'
- message: string
- path: string
-}
-
-export interface EditorValidation {
- valid: boolean
- errors: EditorIssue[]
- warnings: EditorIssue[]
-}
-
-function validateCollection(collection: Collection): EditorValidation {
- const errors: EditorIssue[] = []
- const warnings: EditorIssue[] = []
-
- // Catálogo
- if (!collection.code) errors.push({ severity: 'error', message: 'El catálogo necesita un código', path: 'collection.code' })
- if (!getLing(collection.name)) errors.push({ severity: 'error', message: 'El catálogo necesita un nombre', path: 'collection.name' })
-
- // Opciones globales
- for (const opt of Object.values(collection.options)) {
- if (!opt.code) errors.push({ severity: 'error', message: `Opción "${opt.id}" sin código`, path: `options.${opt.id}.code` })
- if (!getLing(opt.name)) errors.push({ severity: 'error', message: `Opción "${opt.id}" sin nombre`, path: `options.${opt.id}.name` })
- }
-
- // Entidades
- for (const entity of Object.values(collection.entities)) {
- if (!entity.code) errors.push({ severity: 'error', message: `Entidad "${entity.id}" sin código`, path: `entities.${entity.id}.code` })
- if (!getLing(entity.name)) errors.push({ severity: 'error', message: `Entidad "${entity.id}" sin nombre`, path: `entities.${entity.id}.name` })
-
- // Grupos
- for (const [groupId, group] of Object.entries(entity.groups)) {
- if (!group.code) errors.push({ severity: 'error', message: `Grupo "${groupId}" sin código`, path: `entities.${entity.id}.groups.${groupId}.code` })
- if (!getLing(group.name)) errors.push({ severity: 'error', message: `Grupo "${groupId}" sin nombre`, path: `entities.${entity.id}.groups.${groupId}.name` })
-
- // Atributos del grupo
- for (const attr of group.attributes) {
- validateAttribute(attr, `entities.${entity.id}.groups.${groupId}`, collection, errors, warnings)
- }
- }
-
- // Atributos globales de la entidad
- for (const attr of entity.attributes) {
- validateAttribute(attr, `entities.${entity.id}`, collection, errors, warnings)
- }
- }
-
- // Atributos globales del catálogo
- for (const attr of collection.attributes) {
- validateAttribute(attr, 'collection', collection, errors, warnings)
- }
-
- return { valid: errors.length === 0, errors, warnings }
-}
-
-function validateAttribute(
- attr: Attribute,
- scope: string,
- collection: Collection,
- errors: EditorIssue[],
- warnings: EditorIssue[],
-): void {
- const path = `${scope}.attributes.${attr.id}`
- if (!attr.code) errors.push({ severity: 'error', message: `Atributo "${attr.id}" sin código`, path: `${path}.code` })
- if (!getLing(attr.name)) errors.push({ severity: 'error', message: `Atributo "${attr.id}" sin nombre`, path: `${path}.name` })
-
- if (attr.type !== 'fixed') {
- if (attr.options.length === 0) {
- warnings.push({ severity: 'warning', message: `Atributo "${attr.id}" sin opciones`, path: `${path}.options` })
- } else {
- // Verificar que el default existe en las opciones
- const hasDefault = attr.options.some(o => o.optionId === attr.default)
- if (!hasDefault) {
- errors.push({ severity: 'error', message: `Atributo "${attr.id}": el valor por defecto no está en las opciones`, path: `${path}.default` })
- }
- // Verificar que todas las opciones existen en la librería
- for (const attrOpt of attr.options) {
- if (!collection.options[attrOpt.optionId]) {
- errors.push({ severity: 'error', message: `Atributo "${attr.id}": opción "${attrOpt.optionId}" no existe en la librería`, path: `${path}.options.${attrOpt.optionId}` })
- }
- }
- }
- }
-}
-
-function getLing(l: LingString): string {
- return typeof l === 'string' ? l : (l as any)?.es ?? ''
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// EDITOR STORE
-// ─────────────────────────────────────────────────────────────────────────────
-
-export function createEditorStore(initial?: Collection) {
-
- // ── State ──────────────────────────────────────────────────────────────────
-
- let collection = $state(initial ?? emptyCollection())
- let activeEntityId = $state(null)
- let activeGroupId = $state(null)
- let activeAttrId = $state(null)
- let activeOptionId = $state(null)
- let activeRuleIdx = $state(null)
-
- // ── Derived ────────────────────────────────────────────────────────────────
-
- const validation = $derived(validateCollection(collection))
- const activeEntity = $derived(activeEntityId ? collection.entities[activeEntityId] ?? null : null)
- const activeGroup = $derived(
- activeEntity && activeGroupId ? (activeEntity.groups[activeGroupId] ?? null) : null
- )
- const activeAttribute = $derived(() => {
- if (!activeAttrId) return null
- if (activeGroup) return activeGroup.attributes.find(a => a.id === activeAttrId) ?? null
- if (activeEntity) return activeEntity.attributes.find(a => a.id === activeAttrId) ?? null
- return collection.attributes.find(a => a.id === activeAttrId) ?? null
- })
- const activeOption = $derived(activeOptionId ? collection.options[activeOptionId] ?? null : null)
-
- // ── Navigation ─────────────────────────────────────────────────────────────
-
- function setActiveEntity(id: EntityID | null) {
- activeEntityId = id
- activeGroupId = null
- activeAttrId = null
- }
-
- function setActiveGroup(id: GroupID | null) {
- activeGroupId = id
- activeAttrId = null
- }
-
- function setActiveAttribute(id: AttrID | null) {
- activeAttrId = id
- }
-
- function setActiveOption(id: OptionID | null) {
- activeOptionId = id
- }
-
- // ── Collection meta ────────────────────────────────────────────────────────
-
- function updateCollectionMeta(patch: Partial>) {
- collection = { ...collection, ...patch }
- }
-
- // ── Options library ────────────────────────────────────────────────────────
-
- function addOption(): OptionID {
- const id = uid('op') as OptionID
- collection = {
- ...collection,
- options: { ...collection.options, [id]: emptyOption(id) },
- }
- activeOptionId = id
- return id
- }
-
- function updateOption(id: OptionID, patch: Partial) {
- const opt = collection.options[id]
- if (!opt) return
- collection = {
- ...collection,
- options: { ...collection.options, [id]: { ...opt, ...patch } },
- }
- }
-
- function deleteOption(id: OptionID) {
- const { [id]: _, ...rest } = collection.options
- collection = { ...collection, options: rest as Record }
- if (activeOptionId === id) activeOptionId = null
- }
-
- function duplicateOption(id: OptionID): OptionID {
- const opt = collection.options[id]
- if (!opt) return id
- const newId = uid('op') as OptionID
- const name = typeof opt.name === 'string'
- ? opt.name + ' (copia)'
- : { ...(opt.name as any), es: ((opt.name as any).es ?? '') + ' (copia)' }
- collection = {
- ...collection,
- options: { ...collection.options, [newId]: { ...opt, id: newId, code: opt.code + '_copy', name } },
- }
- activeOptionId = newId
- return newId
- }
-
- // ── Entities ───────────────────────────────────────────────────────────────
-
- function addEntity(): EntityID {
- const id = uid('en') as EntityID
- collection = {
- ...collection,
- entities: { ...collection.entities, [id]: emptyEntity(id) },
- }
- activeEntityId = id
- activeGroupId = null
- activeAttrId = null
- return id
- }
-
- function updateEntity(id: EntityID, patch: Record) {
- const entity = collection.entities[id]
- if (!entity) return
- collection = {
- ...collection,
- entities: { ...collection.entities, [id]: { ...entity, ...patch } as Entity },
- }
- }
-
- function deleteEntity(id: EntityID) {
- const { [id]: _, ...rest } = collection.entities
- collection = { ...collection, entities: rest as Record }
- if (activeEntityId === id) setActiveEntity(null)
- }
-
- // ── Groups ─────────────────────────────────────────────────────────────────
-
- function addGroup(entityId: EntityID, kind: 'fixed' | 'optional' = 'fixed'): GroupID {
- const entity = collection.entities[entityId]
- if (!entity) return '' as GroupID
- const id = uid('gr') as GroupID
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: { ...entity.groups, [id]: emptyGroup(id, kind) },
- },
- },
- }
- activeGroupId = id
- activeAttrId = null
- return id
- }
-
- function updateGroup(entityId: EntityID, groupId: GroupID, patch: Record) {
- const entity = collection.entities[entityId]
- const group = entity?.groups[groupId]
- if (!entity || !group) return
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: { ...entity.groups, [groupId]: { ...group, ...patch } as Group },
- },
- },
- }
- }
-
- function deleteGroup(entityId: EntityID, groupId: GroupID) {
- const entity = collection.entities[entityId]
- if (!entity) return
- const { [groupId]: _, ...rest } = entity.groups
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: { ...entity, groups: rest as Record },
- },
- }
- if (activeGroupId === groupId) { activeGroupId = null; activeAttrId = null }
- }
-
- // ── Attributes ─────────────────────────────────────────────────────────────
-
- function addAttribute(entityId: EntityID, groupId?: GroupID): AttrID {
- const id = uid('at') as AttrID
- const attr = emptyAttribute(id)
-
- if (groupId) {
- collection = produceAttr(collection, entityId, groupId, id, () => attr)
- // produceAttr filtra por attrId que no existe aún — necesitamos append
- const entity = collection.entities[entityId]
- const group = entity?.groups[groupId]
- if (entity && group) {
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: {
- ...entity.groups,
- [groupId]: { ...group, attributes: [...group.attributes, attr] },
- },
- },
- },
- }
- }
- } else {
- const entity = collection.entities[entityId]
- if (entity) {
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: { ...entity, attributes: [...entity.attributes, attr] },
- },
- }
- }
- }
-
- activeAttrId = id
- return id
- }
-
- function updateAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, patch: Record) {
- collection = produceAttr(collection, entityId, groupId, attrId, attr => ({ ...attr, ...patch }) as Attribute)
- }
-
- function deleteAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID) {
- collection = produceAttr(collection, entityId, groupId, attrId, null)
- if (activeAttrId === attrId) activeAttrId = null
- }
-
- // ── Attribute options ──────────────────────────────────────────────────────
-
- function addOptionToAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, optionId: OptionID) {
- collection = produceAttr(collection, entityId, groupId, attrId, attr => {
- if (attr.type === 'fixed') return attr
- if (attr.options.some(o => o.optionId === optionId)) return attr
- const newOpt: AttributeOption = { optionId }
- const options = [...attr.options, newOpt]
- // Si es la primera opción, ponerla como default
- const updated = { ...attr, options }
- if (!attr.default && options.length === 1) (updated as any).default = optionId
- return updated
- })
- }
-
- function removeOptionFromAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, optionId: OptionID) {
- collection = produceAttr(collection, entityId, groupId, attrId, attr => {
- if (attr.type === 'fixed') return attr
- const options = attr.options.filter(o => o.optionId !== optionId)
- const updated = { ...attr, options }
- if ((attr as any).default === optionId) (updated as any).default = options[0]?.optionId ?? ''
- return updated
- })
- }
-
- // ── Catalog attributes ─────────────────────────────────────────────────────
-
- function addCatalogAttribute(): AttrID {
- const id = uid('at') as AttrID
- const attr = emptyAttribute(id)
- collection = { ...collection, attributes: [...collection.attributes, attr] }
- activeAttrId = id
- activeEntityId = null
- activeGroupId = null
- return id
- }
-
- function updateCatalogAttribute(attrId: AttrID, patch: Record) {
- collection = {
- ...collection,
- attributes: collection.attributes.map(a =>
- a.id === attrId ? { ...a, ...patch } as Attribute : a
- ),
- }
- }
-
- function deleteCatalogAttribute(attrId: AttrID) {
- collection = { ...collection, attributes: collection.attributes.filter(a => a.id !== attrId) }
- if (activeAttrId === attrId) activeAttrId = null
- }
-
- // ── Import / Export ────────────────────────────────────────────────────────
-
- function exportToJson(): string {
- return JSON.stringify(collection, null, 2)
- }
-
- function importFromJson(json: string) {
- collection = JSON.parse(json)
- activeEntityId = null
- activeGroupId = null
- activeAttrId = null
- activeOptionId = null
- }
-
- function reset() {
- collection = emptyCollection()
- activeEntityId = null
- activeGroupId = null
- activeAttrId = null
- activeOptionId = null
- }
-
- // ── Public API ─────────────────────────────────────────────────────────────
-
- return {
- // State (getters)
- get collection() { return collection },
- get validation() { return validation },
- get activeEntityId() { return activeEntityId },
- get activeGroupId() { return activeGroupId },
- get activeAttrId() { return activeAttrId },
- get activeOptionId() { return activeOptionId },
- get activeEntity() { return activeEntity },
- get activeGroup() { return activeGroup },
- get activeAttribute() { return activeAttribute() },
- get activeOption() { return activeOption },
-
- // Navigation
- setActiveEntity,
- setActiveGroup,
- setActiveAttribute,
- setActiveOption,
-
- // Collection
- updateCollectionMeta,
-
- // Options
- addOption,
- updateOption,
- deleteOption,
- duplicateOption,
-
- // Entities
- addEntity,
- updateEntity,
- deleteEntity,
-
- // Groups
- addGroup,
- updateGroup,
- deleteGroup,
-
- // Attributes
- addAttribute,
- updateAttribute,
- deleteAttribute,
- addCatalogAttribute,
- updateCatalogAttribute,
- deleteCatalogAttribute,
-
- // Attribute options
- addOptionToAttribute,
- removeOptionFromAttribute,
-
- // Import/Export
- exportToJson,
- importFromJson,
- reset,
- }
-}
-
-export type EditorStore = ReturnType
-
-// ─────────────────────────────────────────────────────────────────────────────
-// HELPERS INTERNOS — produce inmutable
-// ─────────────────────────────────────────────────────────────────────────────
-
-
-
-function produceAttr(
- collection: Collection,
- entityId: EntityID,
- groupId: GroupID | undefined,
- attrId: AttrID,
- fn: ((attr: Attribute) => Attribute) | null,
-): Collection {
- const entity = collection.entities[entityId]
- if (!entity) return collection
-
- function mapAttrs(attrs: Attribute[]): Attribute[] {
- if (fn === null) return attrs.filter(a => a.id !== attrId)
- return attrs.map(a => a.id === attrId ? fn(a) as Attribute : a)
- }
-
- if (groupId) {
- const group = entity.groups[groupId]
- if (!group) return collection
- return {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: {
- ...entity.groups,
- [groupId]: { ...group, attributes: mapAttrs(group.attributes) },
- },
- },
- },
- }
- }
-
- return {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: { ...entity, attributes: mapAttrs(entity.attributes) },
- },
- }
-}
\ No newline at end of file
diff --git a/src/vice/editor/stores/editor.svelte.ts b/src/vice/editor/stores/editor.svelte.ts
deleted file mode 100644
index f19f9d2..0000000
--- a/src/vice/editor/stores/editor.svelte.ts
+++ /dev/null
@@ -1,812 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Editor Store
-//
-// Store principal del editor de catálogos.
-// Gestiona el estado de edición de una Collection completa.
-// Usa runas de Svelte 5 ($state, $derived).
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type {
- Collection, CollectionID,
- Entity, EntityID,
- Group, GroupID,
- Attribute, AttrID,
- AttributeOption,
- OptionDefinition, OptionID,
- Rule,
- EntityPath,
- EntityPathKey,
- LiteralValue,
-} from '@/vice/libs/types'
-import type {FixedGroup, OptionalGroup} from "@/vice/libs";
-import type {LingString} from "@/ling/lib";
-
-// ─────────────────────────────────────────────────────────────────────────────
-// HELPERS
-// ─────────────────────────────────────────────────────────────────────────────
-
-function uid(prefix: string): string {
- return `${prefix}_${Math.random().toString(36).slice(2, 8)}`
-}
-
-function emptyLing(es = '', en = ''): LingString {
- return { es, en }
-}
-
-function emptyCollection(): Collection {
- return {
- id: uid('col') as CollectionID,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- langs: ['es'],
- attributes: [],
- options: {} as Record,
- entities: {} as Record,
- }
-}
-
-function emptyEntity(id: EntityID): Entity {
- return {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- attributes: [],
- groups: {} as Record,
- rules: [],
- }
-}
-
-function emptyGroup(id: GroupID, kind: 'fixed' | 'optional' = 'fixed'): Group {
- const base = {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- attributes: [],
- }
- if (kind === 'optional') {
- return { ...base, kind: 'optional', rules: [] } satisfies OptionalGroup
- }
- return { ...base, kind: 'fixed' } satisfies FixedGroup
-}
-
-function emptyAttribute(id: AttrID): import('@/vice/libs/types.ts').RequiredAttribute {
- return {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- type: 'required',
- domain: { kind: 'options', options: [], default: '' as OptionID },
- rules: [],
- display: {},
- }
-}
-
-function emptyFixedAttribute(id: AttrID): import('@/vice/libs/types.ts').FixedAttribute {
- return {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- type: 'fixed',
- domain: { kind: 'fixed', value: { kind: 'string', value: '' } },
- display: {},
- }
-}
-
-function emptyOption(id: OptionID): OptionDefinition {
- return {
- id,
- code: '',
- name: emptyLing(),
- description: emptyLing(),
- tags: [],
- }
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// VALIDATION
-// ─────────────────────────────────────────────────────────────────────────────
-
-export type EditorPanel = 'catalog' | 'objects' | 'options' | 'rules' | 'preview' | 'export'
-
-export interface EditorIssue {
- severity: 'error' | 'warning'
- message: string
- path: string
-}
-
-export interface EditorValidation {
- valid: boolean
- errors: EditorIssue[]
- warnings: EditorIssue[]
-}
-
-function validateCollection(collection: Collection): EditorValidation {
- const errors: EditorIssue[] = []
- const warnings: EditorIssue[] = []
-
- // Catálogo
- if (!collection.code) errors.push({ severity: 'error', message: 'El catálogo necesita un código', path: 'collection.code' })
-
- // Validar nombre en todos los langs declarados
- const name = collection.name as any
- for (const lang of collection.langs) {
- if (!name?.[lang]) {
- errors.push({ severity: 'error', message: `El catálogo necesita nombre en "${lang}"`, path: `collection.name.${lang}` })
- }
- }
-
- // Opciones globales
- for (const opt of Object.values(collection.options)) {
- if (!opt.code) errors.push({ severity: 'error', message: `Opción "${opt.id}" sin código`, path: `options.${opt.id}.code` })
-
- const optName = opt.name as any
- for (const lang of collection.langs) {
- if (!optName?.[lang]) {
- warnings.push({ severity: 'warning', message: `Opción "${opt.id}" sin nombre en "${lang}"`, path: `options.${opt.id}.name.${lang}` })
- }
- }
- }
-
- // Entidades
- for (const entity of Object.values(collection.entities)) {
- if (!entity.code) errors.push({ severity: 'error', message: `Entidad "${entity.id}" sin código`, path: `entities.${entity.id}.code` })
-
- const entityName = entity.name as any
- for (const lang of collection.langs) {
- if (!entityName?.[lang]) {
- warnings.push({ severity: 'warning', message: `Entidad "${entity.id}" sin nombre en "${lang}"`, path: `entities.${entity.id}.name.${lang}` })
- }
- }
-
- // Grupos
- for (const [groupId, group] of Object.entries(entity.groups)) {
- if (!group.code) errors.push({ severity: 'error', message: `Grupo "${groupId}" sin código`, path: `entities.${entity.id}.groups.${groupId}.code` })
-
- const groupName = group.name as any
- for (const lang of collection.langs) {
- if (!groupName?.[lang]) {
- warnings.push({ severity: 'warning', message: `Grupo "${groupId}" sin nombre en "${lang}"`, path: `entities.${entity.id}.groups.${groupId}.name.${lang}` })
- }
- }
-
- // Atributos del grupo
- for (const attr of group.attributes) {
- validateAttribute(attr, `entities.${entity.id}.groups.${groupId}`, collection, errors, warnings)
- }
- }
-
- // Atributos globales de la entidad
- for (const attr of entity.attributes) {
- validateAttribute(attr, `entities.${entity.id}`, collection, errors, warnings)
- }
- }
-
- // Atributos globales del catálogo
- for (const attr of collection.attributes) {
- validateAttribute(attr, 'collection', collection, errors, warnings)
- }
-
- return { valid: errors.length === 0, errors, warnings }
-}
-
-function validateAttribute(
- attr: Attribute,
- scope: string,
- collection: Collection,
- errors: EditorIssue[],
- warnings: EditorIssue[],
-): void {
- const path = `${scope}.attributes.${attr.id}`
- if (!attr.code) errors.push({ severity: 'error', message: `Atributo "${attr.id}" sin código`, path: `${path}.code` })
-
- const attrName = attr.name as any
- for (const lang of collection.langs) {
- if (!attrName?.[lang]) {
- warnings.push({ severity: 'warning', message: `Atributo "${attr.id}" sin nombre en "${lang}"`, path: `${path}.name.${lang}` })
- }
- }
-
- if (attr.type !== 'fixed') {
- const domain = attr.domain
- if (domain.kind === 'options') {
- if (domain.options.length === 0) {
- warnings.push({ severity: 'warning', message: `Atributo "${attr.id}" sin opciones`, path: `${path}.domain.options` })
- } else {
- const hasDefault = domain.options.some(o => o.optionId === domain.default)
- if (!hasDefault) {
- errors.push({ severity: 'error', message: `Atributo "${attr.id}": el valor por defecto no está en las opciones`, path: `${path}.domain.default` })
- }
- for (const attrOpt of domain.options) {
- if (!collection.options[attrOpt.optionId]) {
- errors.push({ severity: 'error', message: `Atributo "${attr.id}": opción "${attrOpt.optionId}" no existe en la librería`, path: `${path}.domain.options.${attrOpt.optionId}` })
- }
- }
- }
- } else if (domain.kind === 'literals') {
- if (domain.literals.length === 0) {
- warnings.push({ severity: 'warning', message: `Atributo "${attr.id}" sin literales en el dominio`, path: `${path}.domain.literals` })
- }
- }
- }
-}
-
-function getLing(l: LingString): string {
- return typeof l === 'string' ? l : (l as any)?.es ?? ''
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// EDITOR STORE
-// ─────────────────────────────────────────────────────────────────────────────
-
-export function createEditorStore(initial?: Collection) {
-
- // ── State ──────────────────────────────────────────────────────────────────
-
- let collection = $state(initial ?? emptyCollection())
- let activeEntityId = $state(null)
- let activeGroupId = $state(null)
- let activeAttrId = $state(null)
- let activeOptionId = $state(null)
- let activeRuleIdx = $state(null)
- let activePanel = $state('objects')
- let isDirty = $state(false)
-
- function markDirty() { isDirty = true }
-
- // ── Derived ────────────────────────────────────────────────────────────────
-
- const validation = $derived(validateCollection(collection))
- const canPreview = $derived(
- validation.valid && Object.keys(collection.entities).length > 0
- )
- const activeEntity = $derived(activeEntityId ? collection.entities[activeEntityId] ?? null : null)
- const activeGroup = $derived(
- activeEntity && activeGroupId ? (activeEntity.groups[activeGroupId] ?? null) : null
- )
- const activeAttribute = $derived((() => {
- if (!activeAttrId) return null
- if (activeGroup) return activeGroup.attributes.find(a => a.id === activeAttrId) ?? null
- if (activeEntity) return activeEntity.attributes.find(a => a.id === activeAttrId) ?? null
- return collection.attributes.find(a => a.id === activeAttrId) ?? null
- })())
- const activeOption = $derived(activeOptionId ? collection.options[activeOptionId] ?? null : null)
-
- // ── Navigation ─────────────────────────────────────────────────────────────
-
- function setPanel(panel: EditorPanel) {
- activePanel = panel
- }
-
- function setActiveEntity(id: EntityID | null) {
- activeEntityId = id
- activeGroupId = null
- activeAttrId = null
- }
-
- function setActiveGroup(id: GroupID | null) {
- activeGroupId = id
- activeAttrId = null
- }
-
- function setActiveAttribute(id: AttrID | null) {
- activeAttrId = id
- }
-
- function setActiveEntityAttribute(id: AttrID | null) {
- activeGroupId = null
- activeAttrId = id
- }
-
- function setActiveOption(id: OptionID | null) {
- activeOptionId = id
- }
-
- // ── Collection meta ────────────────────────────────────────────────────────
-
- function updateCollectionMeta(patch: Partial>) {
- collection = { ...collection, ...patch }
- isDirty = true
- }
-
- // ── Options library ────────────────────────────────────────────────────────
-
- function addOption(): OptionID {
- const id = uid('op') as OptionID
- collection = {
- ...collection,
- options: { ...collection.options, [id]: emptyOption(id) },
- }
- activeOptionId = id
- markDirty()
- return id
- }
-
- function updateOption(id: OptionID, patch: Partial) {
- const opt = collection.options[id]
- if (!opt) return
- collection = {
- ...collection,
- options: { ...collection.options, [id]: { ...opt, ...patch } },
- }
- markDirty()
- }
-
- function deleteOption(id: OptionID) {
- const { [id]: _, ...rest } = collection.options
- collection = { ...collection, options: rest as Record }
- if (activeOptionId === id) activeOptionId = null
- markDirty()
- }
-
- function duplicateOption(id: OptionID): OptionID {
- const opt = collection.options[id]
- if (!opt) return id
- const newId = uid('op') as OptionID
- const name = typeof opt.name === 'string'
- ? opt.name + ' (copia)'
- : { ...(opt.name as any), es: ((opt.name as any).es ?? '') + ' (copia)' }
- collection = {
- ...collection,
- options: { ...collection.options, [newId]: { ...opt, id: newId, code: opt.code + '_copy', name } },
- }
- activeOptionId = newId
- return newId
- }
-
- // ── Entities ───────────────────────────────────────────────────────────────
-
- function addEntity(): EntityID {
- const id = uid('en') as EntityID
- collection = {
- ...collection,
- entities: { ...collection.entities, [id]: emptyEntity(id) },
- }
- activeEntityId = id
- activeGroupId = null
- activeAttrId = null
- markDirty()
- return id
- }
-
- function updateEntity(id: EntityID, patch: Record) {
- const entity = collection.entities[id]
- if (!entity) return
- collection = {
- ...collection,
- entities: { ...collection.entities, [id]: { ...entity, ...patch } as Entity },
- }
- markDirty()
- }
-
- function deleteEntity(id: EntityID) {
- const { [id]: _, ...rest } = collection.entities
- collection = { ...collection, entities: rest as Record }
- if (activeEntityId === id) setActiveEntity(null)
- markDirty()
- }
-
- // ── Groups ─────────────────────────────────────────────────────────────────
-
- function addGroup(entityId: EntityID, kind: 'fixed' | 'optional' = 'fixed'): GroupID {
- const entity = collection.entities[entityId]
- if (!entity) return '' as GroupID
- const id = uid('gr') as GroupID
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: { ...entity.groups, [id]: emptyGroup(id, kind) },
- },
- },
- }
- activeGroupId = id
- activeAttrId = null
- markDirty()
- return id
- }
-
- function updateGroup(entityId: EntityID, groupId: GroupID, patch: Record) {
- const entity = collection.entities[entityId]
- const group = entity?.groups[groupId]
- if (!entity || !group) return
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: { ...entity.groups, [groupId]: { ...group, ...patch } as Group },
- },
- },
- }
- markDirty()
- }
-
- function deleteGroup(entityId: EntityID, groupId: GroupID) {
- const entity = collection.entities[entityId]
- if (!entity) return
- const { [groupId]: _, ...rest } = entity.groups
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: { ...entity, groups: rest as Record },
- },
- }
- if (activeGroupId === groupId) { activeGroupId = null; activeAttrId = null }
- markDirty()
- }
-
- // ── Attributes ─────────────────────────────────────────────────────────────
-
- function addAttribute(entityId: EntityID, groupId?: GroupID): AttrID {
- const id = uid('at') as AttrID
- const attr = emptyAttribute(id)
-
- if (groupId) {
- const entity = collection.entities[entityId]
- const group = entity?.groups[groupId]
- if (entity && group) {
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: {
- ...entity.groups,
- [groupId]: { ...group, attributes: [...group.attributes, attr] },
- },
- },
- },
- }
- }
- } else {
- const entity = collection.entities[entityId]
- if (entity) {
- collection = {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: { ...entity, attributes: [...entity.attributes, attr] },
- },
- }
- }
- }
-
- activeAttrId = id
- markDirty()
- return id
- }
-
- function updateAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, patch: Record) {
- collection = produceAttr(collection, entityId, groupId, attrId, attr => ({ ...attr, ...patch }) as Attribute)
- markDirty()
- }
-
- function deleteAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID) {
- collection = produceAttr(collection, entityId, groupId, attrId, null)
- if (activeAttrId === attrId) activeAttrId = null
- markDirty()
- }
-
- // ── Attribute options ──────────────────────────────────────────────────────
-
- function addOptionToAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, optionId: OptionID) {
- collection = produceAttr(collection, entityId, groupId, attrId, attr => {
- if (attr.type === 'fixed' || attr.type === 'literal') return attr
- if (attr.options.some(o => o.optionId === optionId)) return attr
- const newOpt: AttributeOption = { optionId }
- const options = [...attr.options, newOpt]
- const updated = { ...attr, options }
- if (!attr.default && options.length === 1) (updated as any).default = optionId
- return updated
- })
- markDirty()
- }
-
- function removeOptionFromAttribute(entityId: EntityID, groupId: GroupID | undefined, attrId: AttrID, optionId: OptionID) {
- collection = produceAttr(collection, entityId, groupId, attrId, attr => {
- if (attr.type === 'fixed' || attr.type === 'literal' ) return attr
- const options = attr.options.filter(o => o.optionId !== optionId)
- const updated = { ...attr, options }
- if ((attr as any).default === optionId) (updated as any).default = options[0]?.optionId ?? ''
- return updated
- })
- markDirty()
- }
-
- // ── Catalog attributes ─────────────────────────────────────────────────────
-
- function addCatalogAttribute(): AttrID {
- const id = uid('at') as AttrID
- const attr = emptyLiteralAttribute(id)
- collection = { ...collection, attributes: [...collection.attributes, attr] }
- activeAttrId = id
- activeEntityId = null
- activeGroupId = null
- markDirty()
- return id
- }
-
- function updateCatalogAttribute(attrId: AttrID, patch: Record) {
- collection = {
- ...collection,
- attributes: collection.attributes.map(a =>
- a.id === attrId ? { ...a, ...patch } as Attribute : a
- ),
- }
- }
-
- function deleteCatalogAttribute(attrId: AttrID) {
- collection = { ...collection, attributes: collection.attributes.filter(a => a.id !== attrId) }
- if (activeAttrId === attrId) activeAttrId = null
- }
-
- // ── Rules ──────────────────────────────────────────────────────────────────
-
- function addRule(
- entityId: EntityID,
- groupId: GroupID | undefined,
- attrId: AttrID | undefined,
- rule: Rule,
- ) {
- if (attrId) {
- collection = produceAttr(collection, entityId, groupId, attrId, (a) => ({
- ...a,
- rules: [...((a as any).rules ?? []), rule],
- }))
- } else if (groupId) {
- collection = produceGroup(collection, entityId, groupId, (g) => ({
- ...g,
- rules: [...((g as any).rules ?? []), rule],
- }))
- } else {
- collection = produceEntity(collection, entityId, (e) => ({
- ...e,
- rules: [...(e.rules ?? []), rule],
- }))
- }
- isDirty = true
- }
-
- function updateRule(
- entityId: EntityID,
- groupId: GroupID | undefined,
- attrId: AttrID | undefined,
- ruleId: string,
- patch: Partial,
- ) {
- const mapRules = (rules: Rule[]) =>
- rules.map(r => (r.id === ruleId ? { ...r, ...patch } : r))
-
- if (attrId) {
- collection = produceAttr(collection, entityId, groupId, attrId, (a) => ({
- ...a,
- rules: mapRules((a as any).rules ?? []),
- }))
- } else if (groupId) {
- collection = produceGroup(collection, entityId, groupId, (g) => ({
- ...g,
- rules: mapRules((g as any).rules ?? []),
- }))
- } else {
- collection = produceEntity(collection, entityId, (e) => ({
- ...e,
- rules: mapRules(e.rules ?? []),
- }))
- }
- isDirty = true
- }
-
- function deleteRule(
- entityId: EntityID,
- groupId: GroupID | undefined,
- attrId: AttrID | undefined,
- ruleId: string,
- ) {
- const filterRules = (rules: Rule[]) =>
- rules.filter(r => r.id !== ruleId)
-
- if (attrId) {
- collection = produceAttr(collection, entityId, groupId, attrId, (a) => ({
- ...a,
- rules: filterRules((a as any).rules ?? []),
- }))
- } else if (groupId) {
- collection = produceGroup(collection, entityId, groupId, (g) => ({
- ...g,
- rules: filterRules((g as any).rules ?? []),
- }))
- } else {
- collection = produceEntity(collection, entityId, (e) => ({
- ...e,
- rules: filterRules(e.rules ?? []),
- }))
- }
- isDirty = true
- }
-
- // ── Import / Export ────────────────────────────────────────────────────────
-
- function exportToJson(): string {
- return JSON.stringify(collection, null, 2)
- }
-
- function importFromJson(json: string) {
- const parsed = JSON.parse(json)
- collection = parsed
- activeEntityId = null
- activeGroupId = null
- activeAttrId = null
- activeOptionId = null
- isDirty = false
- }
-
- function reset() {
- collection = emptyCollection()
- activeEntityId = null
- activeGroupId = null
- activeAttrId = null
- activeOptionId = null
- isDirty = false
- }
-
- // ── Public API ─────────────────────────────────────────────────────────────
-
- return {
- // State (getters)
- get collection() { return collection },
- get validation() { return validation },
- get activePanel() { return activePanel },
- get isDirty() { return isDirty },
- get canPreview() { return canPreview },
- get activeEntityId() { return activeEntityId },
- get activeGroupId() { return activeGroupId },
- get activeAttrId() { return activeAttrId },
- get activeOptionId() { return activeOptionId },
- get activeEntity() { return activeEntity },
- get activeGroup() { return activeGroup },
- get activeAttribute() { return activeAttribute },
- get activeOption() { return activeOption },
-
- // Navigation
- setPanel,
- setActiveEntity,
- setActiveGroup,
- setActiveAttribute,
- setActiveEntityAttribute,
- setActiveOption,
-
- // Collection
- updateCollectionMeta,
-
- // Options
- addOption,
- updateOption,
- deleteOption,
- duplicateOption,
-
- // Entities
- addEntity,
- updateEntity,
- deleteEntity,
-
- // Groups
- addGroup,
- updateGroup,
- deleteGroup,
-
- // Attributes
- addAttribute,
- updateAttribute,
- deleteAttribute,
- addCatalogAttribute,
- updateCatalogAttribute,
- deleteCatalogAttribute,
-
- // Attribute options
- addOptionToAttribute,
- removeOptionFromAttribute,
-
- // Rules
- addRule,
- updateRule,
- deleteRule,
-
- // Import/Export
- exportToJson,
- importFromJson,
- reset,
- }
-}
-
-export type EditorStore = ReturnType
-
-// ─────────────────────────────────────────────────────────────────────────────
-// HELPERS INTERNOS — produce inmutable
-// ─────────────────────────────────────────────────────────────────────────────
-
-function produceAttr(
- collection: Collection,
- entityId: EntityID,
- groupId: GroupID | undefined,
- attrId: AttrID,
- fn: ((attr: Attribute) => Attribute) | null,
-): Collection {
- const entity = collection.entities[entityId]
- if (!entity) return collection
-
- function mapAttrs(attrs: Attribute[]): Attribute[] {
- if (fn === null) return attrs.filter(a => a.id !== attrId)
- return attrs.map(a => a.id === attrId ? fn(a) as Attribute : a)
- }
-
- if (groupId) {
- const group = entity.groups[groupId]
- if (!group) return collection
- return {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: {
- ...entity.groups,
- [groupId]: { ...group, attributes: mapAttrs(group.attributes) },
- },
- },
- },
- }
- }
-
- return {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: { ...entity, attributes: mapAttrs(entity.attributes) },
- },
- }
-}
-
-function produceEntity(
- collection: Collection,
- entityId: EntityID,
- fn: (entity: Entity) => Entity,
-): Collection {
- const entity = collection.entities[entityId]
- if (!entity) return collection
- return {
- ...collection,
- entities: { ...collection.entities, [entityId]: fn(entity) },
- }
-}
-
-function produceGroup(
- collection: Collection,
- entityId: EntityID,
- groupId: GroupID,
- fn: (group: Group) => Group,
-): Collection {
- const entity = collection.entities[entityId]
- if (!entity) return collection
- const group = entity.groups[groupId]
- if (!group) return collection
- return {
- ...collection,
- entities: {
- ...collection.entities,
- [entityId]: {
- ...entity,
- groups: { ...entity.groups, [groupId]: fn(group) },
- },
- },
- }
-}
\ No newline at end of file
diff --git a/src/vice/libs/engine.ts b/src/vice/libs/engine.ts
deleted file mode 100644
index a412634..0000000
--- a/src/vice/libs/engine.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Engine Implementation
-//
-// Implementación de ViceEngine.
-// Junta GacGraphBuilder, GacValidator, GacEngine y Session.
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type { Collection, EntityID } from './types.ts'
-import type { ViceEngine, ViceSession, LoadResult } from './engine_types.ts'
-import { GacGraphBuilder } from './graph_builder.ts'
-import { GacValidator } from './validator.ts'
-import { GacEngine } from './gac.ts'
-import { Session } from './session.ts'
-import type { GacGraph } from './gac.ts'
-import type {JsonLogic} from "@/tsat/lib/types";
-
-// ─────────────────────────────────────────────────────────────────────────────
-// JSON LOGIC EVALUATOR
-// Implementación mínima — reemplazable por json-logic-js en producción.
-// ─────────────────────────────────────────────────────────────────────────────
-
-function evalLogic(logic: JsonLogic, data: Record): unknown {
- if (logic === null || typeof logic !== 'object') return logic
- if (Array.isArray(logic)) return logic
-
- const obj = logic as Record
-
- if ('var' in obj) {
- const key = obj['var'] as string
- return key in data ? data[key] : null
- }
- if ('==' in obj) {
- const [a, b] = obj['=='] as [JsonLogic, JsonLogic]
- return evalLogic(a, data) === evalLogic(b, data)
- }
- if ('!=' in obj) {
- const [a, b] = obj['!='] as [JsonLogic, JsonLogic]
- return evalLogic(a, data) !== evalLogic(b, data)
- }
- if ('>' in obj) {
- const [a, b] = obj['>'] as [JsonLogic, JsonLogic]
- return (evalLogic(a, data) as number) > (evalLogic(b, data) as number)
- }
- if ('>=' in obj) {
- const [a, b] = obj['>='] as [JsonLogic, JsonLogic]
- return (evalLogic(a, data) as number) >= (evalLogic(b, data) as number)
- }
- if ('<' in obj) {
- const [a, b] = obj['<'] as [JsonLogic, JsonLogic]
- return (evalLogic(a, data) as number) < (evalLogic(b, data) as number)
- }
- if ('<=' in obj) {
- const [a, b] = obj['<='] as [JsonLogic, JsonLogic]
- return (evalLogic(a, data) as number) <= (evalLogic(b, data) as number)
- }
- if ('and' in obj) {
- return (obj['and'] as JsonLogic[]).every(c => Boolean(evalLogic(c, data)))
- }
- if ('or' in obj) {
- return (obj['or'] as JsonLogic[]).some(c => Boolean(evalLogic(c, data)))
- }
- if ('!' in obj) {
- return !evalLogic(obj['!'] as JsonLogic, data)
- }
- if ('in' in obj) {
- const [val, arr] = obj['in'] as [JsonLogic, JsonLogic]
- const resolved = evalLogic(arr, data)
- return Array.isArray(resolved) && resolved.includes(evalLogic(val, data))
- }
-
- return false
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// ENGINE IMPL
-// ─────────────────────────────────────────────────────────────────────────────
-
-export class ViceEngineImpl implements ViceEngine {
-
- private gacEngine = new GacEngine(evalLogic)
- private graph: GacGraph | null = null
- private result: LoadResult | null = null
-
- load(collection: Collection, entityId: EntityID): LoadResult {
- const graph = new GacGraphBuilder().build(collection, entityId)
- const validator = new GacValidator()
- const validation = validator.validate(graph)
-
- this.graph = graph
- this.result = {
- valid: validation.valid,
- issues: validation.issues,
- entityId,
- }
-
- return this.result
- }
-
- createSession(): ViceSession {
- if (!this.graph) throw new Error('[VICE] No graph loaded — call load() first')
- if (!this.result) throw new Error('[VICE] No result — call load() first')
- if (!this.result.valid) {
- const errors = this.result.issues
- .filter(i => i.severity === 'error')
- .map(i => i.message)
- .join('\n')
- throw new Error(`[VICE] Cannot create session — graph has errors:\n${errors}`)
- }
-
- return new Session(this.graph, this.gacEngine)
- }
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// FACTORY
-// ─────────────────────────────────────────────────────────────────────────────
-
-export function createViceEngine(): ViceEngine {
- return new ViceEngineImpl()
-}
-
diff --git a/src/vice/libs/engine_types.ts b/src/vice/libs/engine_types.ts
deleted file mode 100644
index 872fcb9..0000000
--- a/src/vice/libs/engine_types.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Engine — API pública
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type { Collection, EntityID, OptionID, EntityPathKey } from './types.ts'
-import type { ValidationResult } from './validator.ts'
-import type { SessionState, SelectResult, DeselectResult } from './state_types.ts'
-
-export interface LoadResult {
- valid: boolean
- issues: ValidationResult['issues']
- entityId: EntityID
-}
-
-export interface ViceSession {
- select(path: EntityPathKey, optionId: OptionID, quantity?: number): SelectResult
- deselect(path: EntityPathKey): DeselectResult
- getState(): SessionState
- reset(): SessionState
-}
-
-export interface ViceEngine {
- load(collection: Collection, entityId: EntityID): LoadResult
- createSession(): ViceSession
-}
-
-export type { ViceEngine as default }
\ No newline at end of file
diff --git a/src/vice/libs/gac.ts b/src/vice/libs/gac.ts
deleted file mode 100644
index a180132..0000000
--- a/src/vice/libs/gac.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · GAC (Generalized Arc Consistency)
-//
-// Responsabilidades:
-// - Definir las estructuras del grafo (GacGraph, GacState)
-// - Propagar cambios de dominio mediante GAC
-// - Devolver el estado arc-consistent resultante
-//
-// No conoce Collection, Session ni ningún tipo de runtime externo.
-// Opera exclusivamente sobre GacGraph y GacState.
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type { LingString } from '@/ling/lib'
-import type {
- AttrID,
- GroupID,
- EntityID,
- OptionID,
- EntityPathKey,
- RuleAction,
- AttributeType,
- Severity,
- OptionSelection,
-} from './types.ts'
-import type {JsonLogic} from "@/jslg/lib";
-
-// ─────────────────────────────────────────────────────────────────────────────
-// VARIABLE
-// Un atributo en el grafo. Su dominio se reduce por GAC.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface GacVariable {
- path: EntityPathKey
- attrId: AttrID
- type: AttributeType
- fullDomain: ReadonlySet // dominio del catálogo — nunca muta
- defaultOption: OptionID
- dependants: EntityPathKey[] // paths de constraints que dependen de esta variable
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// GROUP
-// Un grupo en el grafo. Solo importa su kind para saber el estado inicial.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface GacGroup {
- path: EntityPathKey
- groupId: GroupID
- kind: 'fixed' | 'optional'
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// CONSTRAINT
-// Una regla compilada y lista para evaluar. Inmutable.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface GacConstraint {
- id: string
- scope: EntityPathKey
- dependsOn: EntityPathKey[] // variables que condition referencia
- condition: JsonLogic | null // null = siempre activa
- action: RuleAction
- message: LingString
- severity: Severity
- mode: 'after' | 'override'
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// GAC GRAPH
-// Construido una vez en load(). Inmutable en runtime.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface GacGraph {
- entityId: EntityID
- variables: Map
- groups: Map
- constraints: Map
- /** Índice inverso: variable/group → IDs de constraints que la referencian en dependsOn */
- dependantIndex: Map>
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// GAC STATE
-// Copia mutable por sesión. Nunca muta GacGraph.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface GacVariableState {
- path: EntityPathKey
- domain: Set // dominio reducido por GAC
- current: OptionSelection | null
- active: boolean
- violations: GacViolation[]
-}
-
-export interface GacGroupState {
- path: EntityPathKey
- active: boolean
-}
-
-export interface GacViolation {
- constraintId: string
- optionId?: OptionID
- reason: LingString
- severity: Severity
-}
-
-export interface GacState {
- variables: Map
- groups: Map
- consistent: boolean
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// GAC RESULT
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface GacResult {
- state: GacState
- consistent: boolean
- active: Set // IDs de constraints activos en esta propagación
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// JSON LOGIC EVALUATOR
-// Inyectado desde fuera — el motor no depende de la librería directamente.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export type JsonLogicEvaluator = (logic: JsonLogic, data: Record) => unknown
-
-// ─────────────────────────────────────────────────────────────────────────────
-// GAC ENGINE
-// ─────────────────────────────────────────────────────────────────────────────
-
-export class GacEngine {
-
- constructor(private readonly evalLogic: JsonLogicEvaluator) {}
-
- // ── Propagación ─────────────────────────────────────────────────────────────
-
- propagate(graph: GacGraph, state: GacState): GacResult {
- const active = new Set()
- const queue = new Set(graph.constraints.keys())
-
- while (queue.size > 0) {
- const cid = this.dequeue(queue)
- const constraint = graph.constraints.get(cid)
- if (!constraint) continue
-
- if (!this.evalCondition(constraint, state)) continue
-
- active.add(cid)
-
- const reduced = this.applyAction(constraint, state, graph)
- const targetPath = this.resolveTarget(constraint, graph)
-
- if (reduced && targetPath) {
- for (const dep of graph.dependantIndex.get(targetPath) ?? new Set()) {
- queue.add(dep)
- }
- }
-
- // dominio vacío = estado inconsistente
- if (targetPath) {
- const vs = state.variables.get(targetPath)
- if (vs && vs.domain.size === 0) state.consistent = false
- }
- }
-
- return { state, consistent: state.consistent, active }
- }
-
- // ── Evaluación de condición ──────────────────────────────────────────────────
-
- private evalCondition(constraint: GacConstraint, state: GacState): boolean {
- if (!constraint.condition) return true
- const data = this.buildContext(constraint.dependsOn, state)
- return Boolean(this.evalLogic(constraint.condition, data))
- }
-
- private buildContext(paths: EntityPathKey[], state: GacState): Record {
- const data: Record = {}
- for (const path of paths) {
- const vs = state.variables.get(path)
- if (!vs) continue
- const attrId = path.split('/').pop() ?? path
- data[attrId] = vs.current?.optionId ?? null
- if (vs.current?.quantity !== undefined) {
- data[`${attrId}:quantity`] = vs.current.quantity
- }
- }
- return data
- }
-
- // ── Aplicación de acción ─────────────────────────────────────────────────────
-
- private applyAction(constraint: GacConstraint, state: GacState, graph: GacGraph): boolean {
- const targetPath = this.resolveTarget(constraint, graph)
- if (!targetPath) return false
-
- const { type, values } = constraint.action
-
- // activate / deactivate pueden apuntar a grupo o atributo opcional
- if (type === 'activate' || type === 'deactivate') {
- const gs = state.groups.get(targetPath)
- if (gs) {
- const before = gs.active
- gs.active = type === 'activate'
- return gs.active !== before
- }
- const vs = state.variables.get(targetPath)
- if (!vs) return false
- const before = vs.active
- vs.active = type === 'activate'
- if (!vs.active) vs.domain.clear()
- return vs.active !== before
- }
-
- const vs = state.variables.get(targetPath)
- if (!vs) return false
-
- const before = vs.domain.size
-
- switch (type) {
- case 'forbid': {
- for (const optId of values ?? []) {
- vs.domain.delete(optId)
- if (vs.current?.optionId === optId) {
- vs.violations.push({ constraintId: constraint.id, optionId: optId, reason: constraint.message, severity: constraint.severity })
- }
- }
- break
- }
- case 'allow': {
- const allowed = new Set(values ?? [])
- for (const optId of [...vs.domain]) {
- if (!allowed.has(optId)) {
- vs.domain.delete(optId)
- if (vs.current?.optionId === optId) {
- vs.violations.push({ constraintId: constraint.id, optionId: optId, reason: constraint.message, severity: constraint.severity })
- }
- }
- }
- break
- }
- case 'require': {
- if (!vs.current) {
- vs.violations.push({ constraintId: constraint.id, reason: constraint.message, severity: constraint.severity })
- }
- if ((values?.length ?? 0) > 0) {
- const required = new Set(values)
- if (vs.current && !required.has(vs.current.optionId)) {
- vs.violations.push({ constraintId: constraint.id, optionId: vs.current.optionId, reason: constraint.message, severity: constraint.severity })
- }
- }
- break
- }
- case 'suggest': {
- const suggested = new Set(values ?? [])
- if (vs.current && !suggested.has(vs.current.optionId)) {
- vs.violations.push({ constraintId: constraint.id, optionId: vs.current.optionId, reason: constraint.message, severity: 'info' })
- }
- break
- }
- }
-
- return vs.domain.size < before
- }
-
- // ── resolveTarget ────────────────────────────────────────────────────────────
-
- private resolveTarget(constraint: GacConstraint, graph: GacGraph): EntityPathKey | null {
- const target = constraint.action.target
- if (!target) return null
- for (const path of graph.variables.keys()) {
- if (path.endsWith(`/${target}`) || path === target) return path
- }
- for (const path of graph.groups.keys()) {
- if (path.endsWith(`/${target}`) || path === target) return path
- }
- return null
- }
-
- private dequeue(queue: Set): string {
- const first = queue.values().next().value as string
- queue.delete(first)
- return first
- }
-
- // ── Estado ───────────────────────────────────────────────────────────────────
-
- initState(graph: GacGraph): GacState {
- const variables = new Map()
- const groups = new Map()
-
- for (const [path, v] of graph.variables) {
- variables.set(path, {
- path,
- domain: new Set(v.fullDomain),
- current: v.type === 'required' ? { optionId: v.defaultOption } : null,
- active: v.type !== 'optional',
- violations: [],
- })
- }
-
- for (const [path, g] of graph.groups) {
- groups.set(path, { path, active: g.kind === 'fixed' })
- }
-
- return { variables, groups, consistent: true }
- }
-
- applySelection(state: GacState, path: EntityPathKey, optionId: OptionID, quantity?: number): GacState {
- const next = this.cloneState(state)
- const vs = next.variables.get(path)
- if (!vs) return next
- vs.current = { optionId, ...(quantity !== undefined ? { quantity } : {}) }
- vs.active = true
- vs.violations = []
- return next
- }
-
- applyDeselection(state: GacState, path: EntityPathKey): GacState {
- const next = this.cloneState(state)
- const vs = next.variables.get(path)
- if (!vs) return next
- vs.current = null
- vs.active = false
- vs.violations = []
- return next
- }
-
- cloneState(state: GacState): GacState {
- const variables = new Map()
- const groups = new Map()
- for (const [path, v] of state.variables) {
- variables.set(path, { ...v, domain: new Set(v.domain), violations: [...v.violations] })
- }
- for (const [path, g] of state.groups) {
- groups.set(path, { ...g })
- }
- return { variables, groups, consistent: state.consistent }
- }
-}
\ No newline at end of file
diff --git a/src/vice/libs/graph_builder.ts b/src/vice/libs/graph_builder.ts
deleted file mode 100644
index 2f4111f..0000000
--- a/src/vice/libs/graph_builder.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Graph Builder
-//
-// Construye el GacGraph desde el modelo del catálogo.
-// Se ejecuta una vez en load() — el grafo es inmutable en runtime.
-//
-// Responsabilidades:
-// - Recorrer Collection → Entity → Group → Attribute → Rule
-// - Crear GacVariable por cada Attribute
-// - Crear GacGroup por cada Group
-// - Crear GacConstraint por cada Rule en cualquier nivel
-// - Inferir Rule.dependsOn desde JsonLogic si no está declarado
-// - Construir dependantIndex (grafo inverso)
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type {
- Collection,
- Entity,
- Group,
- Attribute,
- Rule,
- EntityID,
- GroupID,
- EntityPath,
- EntityPathKey,
-} from './types.ts'
-
-import type {
- GacGraph,
- GacVariable,
- GacGroup,
- GacConstraint,
-} from './gac.ts'
-import type {JsonLogic} from "@/tsat/lib/types";
-
-// ─────────────────────────────────────────────────────────────────────────────
-// HELPERS
-// ─────────────────────────────────────────────────────────────────────────────
-
-function toKey(path: EntityPath): EntityPathKey {
- return path.join('/')
-}
-
-/**
- * Extrae las referencias { var: 'at_xxx' } de un JsonLogic.
- * Usado para inferir Rule.dependsOn cuando no está declarado.
- */
-function extractVars(logic: JsonLogic, found: Set = new Set()): Set {
- if (logic === null || typeof logic !== 'object') return found
- if (Array.isArray(logic)) {
- for (const item of logic) extractVars(item as JsonLogic, found)
- return found
- }
- const obj = logic as Record
- if ('var' in obj && typeof obj['var'] === 'string') {
- found.add(obj['var'])
- return found
- }
- for (const val of Object.values(obj)) {
- extractVars(val as JsonLogic, found)
- }
- return found
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// BUILDER
-// ─────────────────────────────────────────────────────────────────────────────
-
-export class GacGraphBuilder {
-
- private variables = new Map()
- private groups = new Map()
- private constraints = new Map()
- private constraintSeq = 0
-
- build(collection: Collection, entityId: EntityID): GacGraph {
- const entity = collection.entities[entityId]
- if (!entity) throw new Error(`[VICE] Entity "${entityId}" not found`)
-
- // 1 — Atributos globales del catálogo (contexto — fuente de constraints)
- for (const attr of collection.attributes) {
- this.registerVariable(attr, [attr.id])
- }
-
- // 2 — Atributos globales de la entidad
- for (const attr of entity.attributes) {
- this.registerVariable(attr, [entityId, attr.id])
- }
-
- // 3 — Reglas de la entidad
- for (const rule of entity.rules ?? []) {
- this.registerConstraint(rule, [entityId])
- }
-
- // 4 — Grupos y sus atributos
- for (const [groupId, group] of Object.entries(entity.groups) as [GroupID, Group][]) {
- const groupPath: EntityPath = [entityId, groupId]
-
- // Registrar grupo
- this.groups.set(toKey(groupPath), {
- path: toKey(groupPath),
- groupId: groupId as GroupID,
- kind: group.kind,
- })
-
- // Atributos del grupo
- for (const attr of group.attributes) {
- this.registerVariable(attr, [entityId, groupId, attr.id])
- }
-
- // Reglas del grupo (solo OptionalGroup)
- if (group.kind === 'optional') {
- for (const rule of group.rules ?? []) {
- this.registerConstraint(rule, groupPath)
- }
- }
- }
-
- // 5 — Inferir dependsOn desde JsonLogic donde no está declarado
- this.resolveInferredDependsOn()
-
- // 6 — Construir dependantIndex
- const dependantIndex = this.buildDependantIndex()
-
- return {
- entityId,
- variables: this.variables,
- groups: this.groups,
- constraints: this.constraints,
- dependantIndex,
- }
- }
-
- // ── Registro de variables ──────────────────────────────────────────────────
-
- private registerVariable(attr: Attribute, path: EntityPath): void {
- const key = toKey(path)
-
- const fullDomain = this.extractDomain(attr)
- const defaultOption = this.extractDefault(attr)
-
- this.variables.set(key, {
- path : key,
- attrId: attr.id,
- type: attr.type,
- fullDomain: new Set(fullDomain),
- defaultOption,
- dependants: (attr.dependants ?? []).map(toKey),
- })
-
- // Reglas del atributo
- const rules = attr.type !== 'fixed' ? (attr.rules ?? []) : []
- for (const rule of rules) {
- this.registerConstraint(rule, path)
- }
-
- // Reglas de las opciones
- if (attr.type === 'optional' || attr.type === 'required') {
- for (const attrOption of attr.options) {
- for (const rule of attrOption.rules ?? []) {
- this.registerConstraint(rule, [...path, attrOption.optionId])
- }
- }
- }
- }
-
- // ── Registro de constraints ────────────────────────────────────────────────
-
- private registerConstraint(rule: Rule, scope: EntityPath): void {
- const id = rule.id ?? (`cn_auto_${++this.constraintSeq}` as any)
-
- const dependsOn = rule.dependsOn
- ? rule.dependsOn.map(toKey)
- : [] // se infiere en resolveInferredDependsOn()
-
- this.constraints.set(id, {
- id,
- scope: toKey(scope),
- dependsOn,
- condition: rule.condition ?? null,
- action: rule.action,
- message: rule.message,
- severity: rule.severity,
- mode: rule.mode ?? 'after',
- })
- }
-
- // ── Inferencia de dependsOn ────────────────────────────────────────────────
-
- private resolveInferredDependsOn(): void {
- for (const [id, constraint] of this.constraints) {
- if (constraint.dependsOn.length > 0) continue // ya declarado
- if (!constraint.condition) continue // sin condición = sin dependencias
-
- const varNames = extractVars(constraint.condition)
- const resolved: EntityPathKey[] = []
-
- for (const varName of varNames) {
- // Buscar variable cuyo attrId coincide con varName
- for (const [key, variable] of this.variables) {
- if (variable.attrId === varName) {
- resolved.push(key)
- break
- }
- }
- }
-
- this.constraints.set(id, { ...constraint, dependsOn: resolved })
- }
- }
-
- // ── dependantIndex ─────────────────────────────────────────────────────────
-
- private buildDependantIndex(): Map> {
- const index = new Map>()
-
- // Desde dependsOn de cada constraint
- for (const [constraintId, constraint] of this.constraints) {
- for (const varPath of constraint.dependsOn) {
- if (!index.has(varPath)) index.set(varPath, new Set())
- index.get(varPath)!.add(constraintId)
- }
- }
-
- // Desde dependants declarados en las variables
- for (const [varPath, variable] of this.variables) {
- for (const dep of variable.dependants) {
- if (!index.has(varPath)) index.set(varPath, new Set())
- index.get(varPath)!.add(dep)
- }
- }
-
- return index
- }
-
- // ── Helpers de extracción ──────────────────────────────────────────────────
-
- private extractDomain(attr: Attribute) {
- if (attr.type === 'fixed') return [attr.option.optionId]
- if (attr.type === 'optional') return attr.options.map(o => o.optionId)
- if (attr.type === 'required') return attr.options.map(o => o.optionId)
- return []
- }
-
- private extractDefault(attr: Attribute) {
- if (attr.type === 'fixed') return attr.option.optionId
- if (attr.type === 'optional') return attr.default
- if (attr.type === 'required') return attr.default
- const _: never = attr
- throw new Error('[VICE] Unknown attribute type')
- }
-}
\ No newline at end of file
diff --git a/src/vice/libs/index.ts b/src/vice/libs/index.ts
deleted file mode 100644
index f026ac6..0000000
--- a/src/vice/libs/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-export * from './types.ts';
-export * from './engine_types.ts';
-export * from './state_types.ts'
-export * from './validator_types.ts';
-
-
diff --git a/src/vice/libs/session.ts b/src/vice/libs/session.ts
deleted file mode 100644
index 284d2bc..0000000
--- a/src/vice/libs/session.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Session
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type {OptionID, EntityPathKey} from './types.ts'
-import type {GacGraph, GacState} from './gac.ts'
-import type {SessionState, PropagationResult, SelectResult, DeselectResult} from './state_types.ts'
-import type {ViceSession} from './engine_types.ts'
-import {GacEngine} from './gac.ts'
-
-
-export class Session implements ViceSession {
-
- private state: GacState
-
- constructor(
- private readonly graph: GacGraph,
- private readonly engine: GacEngine,
- ) {
- this.state = engine.initState(graph)
- this.state = engine.propagate(graph, this.state).state
- }
-
- select(path: EntityPathKey, optionId: OptionID, quantity?: number): SelectResult {
- const vs = this.state.variables.get(path)
- if (!vs) return {ok: false, reason: 'INVALID_PATH'}
- if (!vs.active) return {ok: false, reason: 'INACTIVE_ATTRIBUTE'}
- if (!vs.domain.has(optionId)) return {ok: false, reason: 'OPTION_NOT_IN_DOMAIN'}
-
- const prev = this.state
- const next = this.engine.applySelection(this.state, path, optionId, quantity)
- const result = this.engine.propagate(this.graph, next)
- this.state = result.state
-
- return {ok: true, result: this.toPropagationResult(result.state, prev)}
- }
-
- deselect(path: EntityPathKey): DeselectResult {
- const vs = this.state.variables.get(path)
- if (!vs) return {ok: false, reason: 'INVALID_PATH'}
- if (!vs.active) return {ok: false, reason: 'INACTIVE_ATTRIBUTE'}
-
- const prev = this.state
- const next = this.engine.applyDeselection(this.state, path)
- const result = this.engine.propagate(this.graph, next)
- this.state = result.state
-
- return {ok: true, result: this.toPropagationResult(result.state, prev)}
- }
-
- getState(): SessionState {
- return this.toSessionState(this.state)
- }
-
- reset(): SessionState {
- this.state = this.engine.initState(this.graph)
- this.state = this.engine.propagate(this.graph, this.state).state
- return this.toSessionState(this.state)
- }
-
- private toSessionState(state: GacState): SessionState {
- const attributes = new Map()
- const groups = new Map()
-
- for (const [path, vs] of state.variables) {
- const variable = this.graph.variables.get(path)
- if (!variable) continue
- attributes.set(path, {
- path,
- attrId: variable.attrId,
- active: vs.active,
- domain: vs.domain,
- current: vs.current,
- violations: vs.violations.map(v => ({
- constraintId: v.constraintId,
- optionId: v.optionId,
- reason: v.reason,
- severity: v.severity,
- })),
- })
- }
-
- for (const [path, gs] of state.groups) {
- const group = this.graph.groups.get(path)
- if (!group) continue
- groups.set(path, {path, groupId: group.groupId, active: gs.active})
- }
-
- return {entityId: this.graph.entityId, attributes, groups, consistent: state.consistent}
- }
-
- private toPropagationResult(next: GacState, prev: GacState): PropagationResult {
- const changed = new Set()
-
- for (const [path, vs] of next.variables) {
- const prevVs = prev.variables.get(path)
- if (!prevVs) {
- changed.add(path);
- continue
- }
- if (
- vs.active !== prevVs.active ||
- vs.current?.optionId !== prevVs.current?.optionId ||
- !setsEqual(vs.domain, prevVs.domain)
- ) changed.add(path)
- }
-
- for (const [path, gs] of next.groups) {
- const prevGs = prev.groups.get(path)
- if (!prevGs || gs.active !== prevGs.active) changed.add(path)
- }
-
- return {state: this.toSessionState(next), consistent: next.consistent, changed}
- }
-}
-
-function setsEqual(a: Set, b: Set): boolean {
- if (a.size !== b.size) return false
- for (const v of a) if (!b.has(v)) return false
- return true
-}
\ No newline at end of file
diff --git a/src/vice/libs/state_types.ts b/src/vice/libs/state_types.ts
deleted file mode 100644
index 8a6fb4f..0000000
--- a/src/vice/libs/state_types.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Runtime State
-//
-// Tipos del estado en runtime expuestos hacia fuera.
-// Son la traducción del GacState interno al contrato público de la sesión.
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type { LingString } from '@/ling/lib'
-import type {
- AttrID,
- GroupID,
- OptionID,
- EntityPathKey,
- Severity,
- OptionSelection,
-} from './types.ts'
-
-// ─────────────────────────────────────────────────────────────────────────────
-// VIOLATION
-// Una restricción activa sobre un atributo o una opción concreta.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface Violation {
- constraintId: string
- optionId?: OptionID // undefined = violación sobre el atributo completo
- reason: LingString
- severity: Severity
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// ATTRIBUTE STATE
-// Estado de un atributo en un momento dado.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface AttributeState {
- path: EntityPathKey
- attrId: AttrID
- active: boolean
- domain: ReadonlySet // opciones disponibles tras GAC
- current: OptionSelection | null
- violations: Violation[]
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// GROUP STATE
-// Estado de un grupo en un momento dado.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface GroupState {
- path: EntityPathKey
- groupId: GroupID
- active: boolean
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// SESSION STATE
-// Snapshot completo de una sesión — serializable.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface SessionState {
- entityId: string
- attributes: Map
- groups: Map
- consistent: boolean
-}
-
-// ─────────────────────────────────────────────────────────────────────────────
-// PROPAGATION RESULT
-// Lo que devuelve cada select() — qué cambió.
-// ─────────────────────────────────────────────────────────────────────────────
-
-export interface PropagationResult {
- state: SessionState
- consistent: boolean
- /** Paths de atributos cuyo dominio o estado cambió en esta propagación */
- changed: Set
-}
-
-
-// ─────────────────────────────────────────────────────────────────────────────
-// SELECT / DESELECT RESULT
-// ─────────────────────────────────────────────────────────────────────────────
-
-export type SelectErrorReason =
- | 'INVALID_PATH'
- | 'INACTIVE_ATTRIBUTE'
- | 'OPTION_NOT_IN_DOMAIN'
-
-export type SelectResult =
- | { ok: true; result: PropagationResult }
- | { ok: false; reason: SelectErrorReason }
-
-export type DeselectResult =
- | { ok: true; result: PropagationResult }
- | { ok: false; reason: 'INVALID_PATH' | 'INACTIVE_ATTRIBUTE' }
\ No newline at end of file
diff --git a/src/vice/libs/validator.ts b/src/vice/libs/validator.ts
deleted file mode 100644
index 64e7576..0000000
--- a/src/vice/libs/validator.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// VICE · Validator
-//
-// Validación design time del GacGraph.
-// Se ejecuta después de GacGraphBuilder.build() en load().
-// Solo opera sobre GacGraph — no conoce Collection.
-//
-// Errores (bloquean la sesión):
-// - MISSING_REFERENCE — target o dependsOn inexistente en el grafo
-// - INVALID_DEFAULT — default no está en el dominio
-// - EMPTY_DOMAIN — atributo sin opciones
-// - DUPLICATE_ID — ID repetido en el mismo scope
-//
-// Warnings (no bloquean):
-// - DEAD_OPTION — opción prohibida incondicionalmente
-// - REDUNDANT_RULE — regla que nunca puede activarse
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type { GacGraph } from './gac.ts'
-import type {DesignIssue, ValidationResult} from "@/vice/libs/validator_types.ts";
-
-
-
-// ─────────────────────────────────────────────────────────────────────────────
-// VALIDATOR
-// ─────────────────────────────────────────────────────────────────────────────
-
-export class GacValidator {
-
- validate(graph: GacGraph): ValidationResult {
- const issues: DesignIssue[] = []
-
- this.checkMissingReferences(graph, issues)
- this.checkInvalidDefaults(graph, issues)
- this.checkEmptyDomains(graph, issues)
- this.checkDeadOptions(graph, issues)
- this.checkRedundantRules(graph, issues)
-
- return {
- valid: !issues.some(i => i.severity === 'error'),
- issues,
- }
- }
-
- // ── MISSING_REFERENCE ──────────────────────────────────────────────────────
-
- private checkMissingReferences(graph: GacGraph, issues: DesignIssue[]): void {
- for (const constraint of graph.constraints.values()) {
-
- // target debe existir como variable o grupo
- const tid = constraint.action.target
- if (tid) {
- const found =
- [...graph.variables.keys()].some(k => k.endsWith(`/${tid}`) || k === tid) ||
- [...graph.groups.keys()].some(k => k.endsWith(`/${tid}`) || k === tid)
-
- if (!found) {
- issues.push({
- code: 'MISSING_REFERENCE',
- severity: 'error',
- message: `Rule "${constraint.id}" references unknown target: "${tid}"`,
- path: constraint.scope,
- ruleId: constraint.id,
- })
- }
- }
-
- // dependsOn deben existir como variables
- for (const dep of constraint.dependsOn) {
- if (!graph.variables.has(dep)) {
- issues.push({
- code: 'MISSING_REFERENCE',
- severity: 'error',
- message: `Rule "${constraint.id}" references unknown dependency: "${dep}"`,
- path: constraint.scope,
- ruleId: constraint.id,
- })
- }
- }
- }
- }
-
- // ── INVALID_DEFAULT ────────────────────────────────────────────────────────
-
- private checkInvalidDefaults(graph: GacGraph, issues: DesignIssue[]): void {
- for (const v of graph.variables.values()) {
- if (!v.fullDomain.has(v.defaultOption)) {
- issues.push({
- code: 'INVALID_DEFAULT',
- severity: 'error',
- message: `Attribute "${v.attrId}" default "${v.defaultOption}" is not in its domain`,
- path: v.path,
- })
- }
- }
- }
-
- // ── EMPTY_DOMAIN ───────────────────────────────────────────────────────────
-
- private checkEmptyDomains(graph: GacGraph, issues: DesignIssue[]): void {
- for (const v of graph.variables.values()) {
- if (v.fullDomain.size === 0) {
- issues.push({
- code: 'EMPTY_DOMAIN',
- severity: 'error',
- message: `Attribute "${v.attrId}" has no options`,
- path: v.path,
- })
- }
- }
- }
-
- // ── DEAD_OPTION ────────────────────────────────────────────────────────────
- // Una opción está muerta si una regla incondicional la prohíbe siempre.
-
- private checkDeadOptions(graph: GacGraph, issues: DesignIssue[]): void {
- for (const v of graph.variables.values()) {
- for (const optionId of v.fullDomain) {
- const dead = [...graph.constraints.values()].some(c => {
- const tid = c.action.target
- if (!tid) return false
- const targetsThis = v.path.endsWith(`/${tid}`) || v.path === tid
- return targetsThis &&
- !c.condition &&
- c.action.type === 'forbid' &&
- c.action.values?.includes(optionId)
- })
-
- if (dead) {
- issues.push({
- code: 'DEAD_OPTION',
- severity: 'warning',
- message: `Option "${optionId}" in "${v.attrId}" is unconditionally forbidden`,
- path: v.path,
- })
- }
- }
- }
- }
-
- // ── REDUNDANT_RULE ─────────────────────────────────────────────────────────
- // Una regla es redundante si alguna de sus dependencias tiene dominio vacío.
-
- private checkRedundantRules(graph: GacGraph, issues: DesignIssue[]): void {
- for (const c of graph.constraints.values()) {
- if (!c.condition) continue
-
- const hasDead = c.dependsOn.some(dep => {
- const v = graph.variables.get(dep)
- return v && v.fullDomain.size === 0
- })
-
- if (hasDead) {
- issues.push({
- code: 'REDUNDANT_RULE',
- severity: 'warning',
- message: `Rule "${c.id}" can never activate — depends on an empty domain`,
- path: c.scope,
- ruleId: c.id,
- })
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/vice/libs/validator_types.ts b/src/vice/libs/validator_types.ts
deleted file mode 100644
index 3d57626..0000000
--- a/src/vice/libs/validator_types.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// ─────────────────────────────────────────────────────────────────────────────
-// TIPOS
-// ─────────────────────────────────────────────────────────────────────────────
-
-import type {EntityPathKey} from "@/vice/libs/types.ts";
-
-export type DesignIssueCode =
- | 'MISSING_REFERENCE'
- | 'INVALID_DEFAULT'
- | 'EMPTY_DOMAIN'
- | 'DUPLICATE_ID'
- | 'DEAD_OPTION'
- | 'REDUNDANT_RULE'
-
-export interface DesignIssue {
- code: DesignIssueCode
- severity: 'error' | 'warning'
- message: string
- path: EntityPathKey // dónde ocurre el problema
- ruleId?: string
-}
-
-export interface ValidationResult {
- valid: boolean // false si hay algún error
- issues: DesignIssue[]
-}
-