|
|
|
|
@ -0,0 +1,320 @@
|
|
|
|
|
# CDX-212 — `Soma.services` registry + `Sium.widgets` registry con 3-layer override
|
|
|
|
|
|
|
|
|
|
> v2.3 refactor estructural de Soma hacia un **service registry tipado** (preparatorio para futuras extensiones) + introducción de **`SiumService.widgets`** como home de los widget defaults que hoy cada demo reinventa. Cierra el bug `[object Object]` en color/booking/kitchen-sink y hace que cualquier consumer futuro de `colorValue()`/`dateValue()`/`timeValue()` obtenga el UI correcto sin declarar widgets.
|
|
|
|
|
|
|
|
|
|
- **Owner**: Codex | **Phase**: fase-14 (primera de v2.3)
|
|
|
|
|
- **Allowed files** (scope grande, unifica dos ideas en una entrega):
|
|
|
|
|
- **Soma services refactor**:
|
|
|
|
|
- `src/uix/soma/core/services.ts` (NUEVO — interfaces `SomaServices`, `SomaService`, `SiumService`)
|
|
|
|
|
- `src/uix/soma/core/soma.svelte.ts` (refactor — `readonly services: SomaServices` + getters retrocompat)
|
|
|
|
|
- `src/uix/soma/components/internal/soma.svelte` (props pasan por a través de `services`)
|
|
|
|
|
- `src/uix/soma/test/soma.test.ts` (si existe, actualizar; si no, crear mínimos)
|
|
|
|
|
- **Sium widgets**:
|
|
|
|
|
- `src/lib/sium/svelte/widgets.ts` (NUEVO — `WidgetRegistry`, `defaultSiumWidgets`, `createDefaultSiumService`)
|
|
|
|
|
- `src/lib/sium/svelte/sium.svelte.ts` (extender `SiumOpts` con `widgets?: WidgetRegistry`)
|
|
|
|
|
- `src/lib/sium/svelte/sium-provider.svelte` (prop `widgets` opcional)
|
|
|
|
|
- `src/lib/sium/svelte/index.ts` (re-export `defaultSiumWidgets`, `WidgetRegistry`, `SiumService` si procede)
|
|
|
|
|
- **Form.AutoFields resolution**:
|
|
|
|
|
- `src/uix/soma/components/form/components/form-auto-fields.svelte` (3-layer `getWidget()`: instance → subtree → global)
|
|
|
|
|
- `src/uix/soma/components/form/components/form-auto-fields.svelte.test.ts` (tests por capa)
|
|
|
|
|
- **Demo sweep**:
|
|
|
|
|
- `src/routes/test/sium/+layout.svelte` (si hace falta configurar Soma con services custom — probablemente no)
|
|
|
|
|
- `src/routes/test/sium/color/+page.svelte` (quitar widget snippets inline; los defaults de Soma.services.sium los cubren)
|
|
|
|
|
- `src/routes/test/sium/booking/+page.svelte` (igual — date/time se renderizan desde default)
|
|
|
|
|
- `src/routes/test/sium/kitchen-sink/+page.svelte` (quitar widget snippets salvo los `-preview` que son demo-específicos; usar los canónicos)
|
|
|
|
|
- **Docs**:
|
|
|
|
|
- `src/lib/sium/IMPLEMENTATION_STATUS.md` (añadir fase-14 + decisiones nuevas 11-13)
|
|
|
|
|
- `src/lib/sium/GUIDE.md` (sección nueva: "Widget registry + override chain")
|
|
|
|
|
- `src/lib/sium/COOKBOOK.md` (receta: "Override a default widget for one kind")
|
|
|
|
|
- `src/uix/soma/SOMA_ARCHITECTURE.md` (añadir sección "Services registry")
|
|
|
|
|
- **Forbidden**:
|
|
|
|
|
- `src/lib/ext/app/**` — App se queda flat (primitivas universales). El registry vive en Soma, no en App.
|
|
|
|
|
- `src/lib/sium/core/**` — sium core no conoce widgets (son presentation concern).
|
|
|
|
|
|
|
|
|
|
## Contexto: qué está mal hoy
|
|
|
|
|
|
|
|
|
|
### Gap 1 — Widget registration es per-call, no per-framework
|
|
|
|
|
|
|
|
|
|
Cada `<Form.AutoFields>` recibe `widgets={...}` como prop. No hay registry global. Resultado:
|
|
|
|
|
- `kitchen-sink` y `color` declaran `colorWidgetSnippet` inline.
|
|
|
|
|
- `booking` (con `CalendarDate` + `Time`) debería declarar date/time snippets — no lo hace, muestra `[object Object]`.
|
|
|
|
|
- Cualquier consumer del framework que use `colorValue()` fuera de los demos sufre lo mismo.
|
|
|
|
|
|
|
|
|
|
Es un anti-patrón idéntico al que CDX-210 cerró para `isColorValue` / `isCalendarDate`: laguna de framework que los consumers rellenan con copia-pega.
|
|
|
|
|
|
|
|
|
|
### Gap 2 — Soma's service bag es flat y hardcoded
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// uix/soma/core/soma.svelte.ts — estado actual
|
|
|
|
|
export class Soma {
|
|
|
|
|
readonly app: App;
|
|
|
|
|
get langs() { return this.app.langs; }
|
|
|
|
|
get presentation() { return this.app.presentation; }
|
|
|
|
|
get logger() { return this.app.logger; }
|
|
|
|
|
// ... 7 getters más
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Cada service añadido requiere:
|
|
|
|
|
1. Prop getter nuevo.
|
|
|
|
|
2. Probablemente también en `App` (si delega).
|
|
|
|
|
3. Callsites que los consumen tienen que conocer el getter exacto.
|
|
|
|
|
|
|
|
|
|
No es extensible: un plugin de terceros no puede añadir `Soma.analytics` sin patchear `Soma` directamente.
|
|
|
|
|
|
|
|
|
|
## Goal
|
|
|
|
|
|
|
|
|
|
### 1. `SomaServices` como interface ABIERTA (typed registry, pero extensible via module augmentation)
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// uix/soma/core/services.ts — NUEVO
|
|
|
|
|
export interface SomaService {
|
|
|
|
|
// marker base — cada service la extiende con sus métodos
|
|
|
|
|
readonly __type?: string; // opcional, útil para debug
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SomaServices {
|
|
|
|
|
langs: AppLangs;
|
|
|
|
|
presentation: AppPresentation;
|
|
|
|
|
logger: AppLogger;
|
|
|
|
|
nums?: AppNums;
|
|
|
|
|
money?: AppMoney;
|
|
|
|
|
dates?: AppDates;
|
|
|
|
|
units?: AppUnits;
|
|
|
|
|
sium: SiumService;
|
|
|
|
|
// ↑ OPEN INTERFACE. Librerías extienden vía:
|
|
|
|
|
// declare module '$soma/core/services' {
|
|
|
|
|
// interface SomaServices { analytics: AnalyticsService }
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SiumService extends SomaService {
|
|
|
|
|
readonly widgets: WidgetRegistry;
|
|
|
|
|
// Futuro: defaultValidationBehaviour?, errorDisplayPrefs?, etc.
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. Soma refactor: `services` interno + getters retrocompat
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// uix/soma/core/soma.svelte.ts
|
|
|
|
|
import type { SomaServices, SomaOptions } from './services';
|
|
|
|
|
import { createDefaultSiumService } from '$lib/sium/svelte/widgets';
|
|
|
|
|
|
|
|
|
|
export class Soma {
|
|
|
|
|
readonly services: SomaServices;
|
|
|
|
|
readonly portalTo: string | HTMLElement | undefined;
|
|
|
|
|
readonly app: App;
|
|
|
|
|
|
|
|
|
|
private constructor(app: App, opts: SomaOptions) {
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.portalTo = opts.portalTo;
|
|
|
|
|
this.services = {
|
|
|
|
|
langs: app.langs,
|
|
|
|
|
presentation: app.presentation,
|
|
|
|
|
logger: app.logger,
|
|
|
|
|
nums: app.nums,
|
|
|
|
|
money: app.money,
|
|
|
|
|
dates: app.dates,
|
|
|
|
|
units: app.units,
|
|
|
|
|
sium: opts.services?.sium ?? createDefaultSiumService(),
|
|
|
|
|
...opts.services // consumer override + additions
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Backward-compat shortcuts — no breaking change ──
|
|
|
|
|
get langs() { return this.services.langs; }
|
|
|
|
|
get presentation() { return this.services.presentation; }
|
|
|
|
|
get logger() { return this.services.logger; }
|
|
|
|
|
get nums() { return this.services.nums; }
|
|
|
|
|
get money() { return this.services.money; }
|
|
|
|
|
get dates() { return this.services.dates; }
|
|
|
|
|
get units() { return this.services.units; }
|
|
|
|
|
get sium() { return this.services.sium; }
|
|
|
|
|
|
|
|
|
|
static create(opts: SomaOptions = {}): Soma {
|
|
|
|
|
const app = App.get();
|
|
|
|
|
const instance = new Soma(app, opts);
|
|
|
|
|
return _ctx.set(instance);
|
|
|
|
|
}
|
|
|
|
|
// ... get() / require() sin cambios
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. `WidgetRegistry` + `defaultSiumWidgets`
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// lib/sium/svelte/widgets.ts — NUEVO
|
|
|
|
|
import type { Snippet, Component } from 'svelte';
|
|
|
|
|
import type { Schema } from '../core';
|
|
|
|
|
|
|
|
|
|
export type WidgetProps = {
|
|
|
|
|
value: unknown;
|
|
|
|
|
errors: string[];
|
|
|
|
|
required: boolean;
|
|
|
|
|
path: Array<string | number>;
|
|
|
|
|
schema: Schema<unknown, unknown>;
|
|
|
|
|
onValueChange: (next: unknown) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type WidgetEntry =
|
|
|
|
|
| string
|
|
|
|
|
| Component<Record<string, unknown>>
|
|
|
|
|
| Snippet<[WidgetProps]>
|
|
|
|
|
| { snippet: Snippet<[WidgetProps]> };
|
|
|
|
|
|
|
|
|
|
export type WidgetRegistry = Record<string, WidgetEntry>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Canonical widget mappings from sium composite kinds to their Soma components.
|
|
|
|
|
* Soma.services.sium.widgets defaults to these; consumers can override via
|
|
|
|
|
* `<Sium.Provider widgets={...}>` (subtree) or `<Form.AutoFields widgets={...}>`
|
|
|
|
|
* (per-instance).
|
|
|
|
|
*/
|
|
|
|
|
export const defaultSiumWidgets: WidgetRegistry = {
|
|
|
|
|
'kind:color': colorWidget, // wraps <ColorPicker.Provider>
|
|
|
|
|
'kind:date': dateWidget, // wraps <DateField.Provider>
|
|
|
|
|
'kind:time': timeWidget, // wraps <TimeField.Provider>
|
|
|
|
|
// TODO post-v2.3: 'kind:date-range', 'kind:time-range' cuando los composites estén
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export interface SiumService {
|
|
|
|
|
readonly widgets: WidgetRegistry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createDefaultSiumService(
|
|
|
|
|
overrides?: { widgets?: WidgetRegistry }
|
|
|
|
|
): SiumService {
|
|
|
|
|
return {
|
|
|
|
|
widgets: { ...defaultSiumWidgets, ...overrides?.widgets }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Dónde viven los widget snippets concretos** (los `colorWidget`, `dateWidget`, `timeWidget`): en `lib/sium/svelte/widgets.ts` al principio. Si crecen, extraer a `lib/sium/svelte/widgets/` dir con un file por widget.
|
|
|
|
|
|
|
|
|
|
### 4. `<Sium.Provider>` acepta override de widgets
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// lib/sium/svelte/sium.svelte.ts
|
|
|
|
|
interface SiumOpts {
|
|
|
|
|
schema: StandardSchemaV1;
|
|
|
|
|
widgets?: WidgetRegistry; // subtree-level override
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class SiumProvider {
|
|
|
|
|
readonly schema: StandardSchemaV1;
|
|
|
|
|
readonly widgets: WidgetRegistry; // merged: global defaults ∪ subtree
|
|
|
|
|
|
|
|
|
|
private constructor(opts: SiumOpts) {
|
|
|
|
|
this.schema = opts.schema;
|
|
|
|
|
// Merge subtree widgets onto global Soma.services.sium.widgets
|
|
|
|
|
const globalWidgets = Soma.get()?.services.sium.widgets ?? {};
|
|
|
|
|
this.widgets = { ...globalWidgets, ...(opts.widgets ?? {}) };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 5. `Form.AutoFields` resuelve widgets en 3 capas
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// form-auto-fields.svelte
|
|
|
|
|
function getWidget(schemaInfo) {
|
|
|
|
|
// 1. Per-instance: <Form.AutoFields widgets={...}>
|
|
|
|
|
const instanceWidgets = propsWidgets ?? {};
|
|
|
|
|
// 2. Subtree: <Sium.Provider widgets={...}>
|
|
|
|
|
const siumProvider = SiumProvider.get();
|
|
|
|
|
const subtreeWidgets = siumProvider?.widgets ?? {};
|
|
|
|
|
// 3. Global: Soma.services.sium.widgets
|
|
|
|
|
const globalWidgets = Soma.get()?.services.sium.widgets ?? {};
|
|
|
|
|
|
|
|
|
|
const merged = { ...globalWidgets, ...subtreeWidgets, ...instanceWidgets };
|
|
|
|
|
|
|
|
|
|
// Existing 3-key lookup: meta.widget → channel:X → kind:X
|
|
|
|
|
if (resolved.widgetKey && merged[resolved.widgetKey]) return merged[resolved.widgetKey];
|
|
|
|
|
if (resolved.channelKey && merged[resolved.channelKey]) return merged[resolved.channelKey];
|
|
|
|
|
return merged[resolved.kindKey];
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Tests
|
|
|
|
|
|
|
|
|
|
### Services registry (soma.test.ts)
|
|
|
|
|
|
|
|
|
|
1. `Soma.create()` sin opts → `services.sium` existe con `defaultSiumWidgets`.
|
|
|
|
|
2. `Soma.create({ services: { sium: customService } })` → `services.sium` es el custom.
|
|
|
|
|
3. `Soma.create({ services: { sium: {...}, analytics: mockAnalytics } })` — con module augmentation en test, `services.analytics` se accede tipado.
|
|
|
|
|
4. Backward-compat: `soma.langs.t(...)` sigue funcionando = `soma.services.langs.t(...)`.
|
|
|
|
|
|
|
|
|
|
### Widget registry (form-auto-fields.svelte.test.ts nuevos)
|
|
|
|
|
|
|
|
|
|
1. Schema con `colorValue()` sin widgets prop ni Sium.Provider widgets → renderiza ColorPicker (default global aplica).
|
|
|
|
|
2. `<Form.AutoFields widgets={{ 'kind:color': customColorWidget }}>` → override per-instance aplica sobre default.
|
|
|
|
|
3. `<Sium.Provider widgets={{ 'kind:color': subtreeWidget }}>` + `<Form.AutoFields>` sin widgets → subtree gana sobre default.
|
|
|
|
|
4. Los tres niveles coexisten con orden correcto: instance > subtree > global.
|
|
|
|
|
|
|
|
|
|
## Constraints
|
|
|
|
|
|
|
|
|
|
- **No breaking**. Los getters viejos `Soma.langs`, etc. siguen funcionando como shortcuts a `soma.services.langs`. Demos existentes que pasan `widgets={...}` siguen igual.
|
|
|
|
|
- **`App` se queda flat**. No meter services en App. App es la capa de primitivas universales (langs, presentation, logger); Soma es UI framework layer que añade abstracciones.
|
|
|
|
|
- **Default widgets son snippets Svelte**, no Components. Los widgets actuales de kitchen-sink/color son snippets — seguimos con ese patrón para los defaults.
|
|
|
|
|
- **Imports circulares**: `lib/sium/svelte/widgets.ts` importa de `$uix/air` (ColorPicker) y `$soma/components/color-field` (DateField/TimeField). Verificar que no crea ciclo con soma/core.
|
|
|
|
|
|
|
|
|
|
## Impacto en demos tras aterrizar
|
|
|
|
|
|
|
|
|
|
**Antes** (color demo):
|
|
|
|
|
```svelte
|
|
|
|
|
<Form.AutoFields
|
|
|
|
|
schema={colorCustomizerFormSchema}
|
|
|
|
|
widgets={{
|
|
|
|
|
'kind:color': colorWidget,
|
|
|
|
|
'color-picker': colorWidget
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
```
|
|
|
|
|
+ 25 líneas de snippet inline con ColorPicker.Provider etc.
|
|
|
|
|
|
|
|
|
|
**Después**:
|
|
|
|
|
```svelte
|
|
|
|
|
<Form.AutoFields schema={colorCustomizerFormSchema} />
|
|
|
|
|
```
|
|
|
|
|
Cero widgets locales. El default de `Soma.services.sium.widgets['kind:color']` lo cubre.
|
|
|
|
|
|
|
|
|
|
Ganancia: color demo -35 líneas. kitchen-sink similar. booking ahora funciona correctamente sin cambios por su parte.
|
|
|
|
|
|
|
|
|
|
## Run
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
node src/lib/sium/status/audit.mjs CDX-212
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Delivery
|
|
|
|
|
|
|
|
|
|
Append `[Codex <ISO>] — Entrega CDX-212` con archivos, resumen ≤ 8 bullets, output audit + tests + `npm run check`. Bump state.json + Dia.
|
|
|
|
|
|
|
|
|
|
**Nota de release**: v2.3 = "extensibility refactor". Zero breaking pero abre la puerta a que librerías terceras extiendan Soma vía declaration merging. Primer caso de uso: sium widgets. Futuros casos: theming, telemetry, state machines, etc.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Contexto de conversación donde quedamos (2026-04-21, final del día)
|
|
|
|
|
|
|
|
|
|
**Decisiones tomadas** (no revisar, ir al código):
|
|
|
|
|
|
|
|
|
|
1. Services registry tipado via interface abierta (no `Record<string, unknown>`).
|
|
|
|
|
2. Declaration merging (`declare module '$soma/core/services' { interface SomaServices { ... } }`) como mecanismo canónico de extensión — NO `soma.registerService(key, svc)` imperativo.
|
|
|
|
|
3. `Soma.create({ services })` como único punto de registro. Por-instancia. Nada dinámico post-mount.
|
|
|
|
|
4. Backward-compat shortcuts (`Soma.langs` sigue funcionando como getter sobre `services.langs`).
|
|
|
|
|
5. `App` se queda flat — el registry es concepto Soma-level, no framework-level.
|
|
|
|
|
6. Widget registry es un service más (`SiumService.widgets`) dentro del mismo patrón.
|
|
|
|
|
7. 3-layer resolution para widgets: instance (`<AutoFields widgets>`) > subtree (`<Sium.Provider widgets>`) > global (`Soma.services.sium.widgets`).
|
|
|
|
|
8. Default widgets para `kind:color`/`kind:date`/`kind:time` en `lib/sium/svelte/widgets.ts`.
|
|
|
|
|
|
|
|
|
|
**Decisión diferida** (no en este task):
|
|
|
|
|
|
|
|
|
|
- `soma.registerService(key, impl)` imperativo — útil para plugins autoregistrados, pero añade riesgo de orden/race. No hay caso concreto hoy. Si emerge, es add-on no-breaking.
|
|
|
|
|
|
|
|
|
|
**Estado del código al cerrar**:
|
|
|
|
|
|
|
|
|
|
- Color demo tiene widget snippets inline (ver `src/routes/test/sium/color/+page.svelte`). Commiteado hoy. Cuando CDX-212 aterrice, estos desaparecen porque el default los cubre.
|
|
|
|
|
- Tree/settings/signup demos estaban "rotas" — fue slowness del dev server en cold start, no bug real. Todas funcionan.
|
|
|
|
|
- v2.2 cerrada en commit `8a4bbb4a` + `a722d443`.
|