# Prefs `prefs` is the Active preference resolution module. It is part of the core (`App.prefs`) and is generic over a user-defined `PrefsSchema = Record>`. ## Handoff 2026-05-13 `prefs` es la pieza que debe resolver las preferencias compartidas entre `ActiveApp` y las capas consumidoras, no una bolsa generica duplicada por cada capa. Queda decidido: - `prefs.language` alimenta `langs`; - `prefs.locale` alimenta `format`; - `prefs.direction` resuelve la direccion efectiva; - `prefs.motion`, `prefs.sound` y `prefs.haptic` son preferencias transversales de percepcion/interaccion; - `createActivePrefsDomProjection(...)` proyecta esas preferencias al DOM cuando el composition root le pasa un `ActiveDom`; - `theme`, `mode` y `density` son visuales y pertenecen a Eidos, no al preset core de `prefs` ni a `ActiveUix`. > **Heads-up:** the sections below describe the original four-layer > design (capabilities + environment + intent → effective). The current > implementation is **schema-based**: each preference is a > `PrefsDimension` that owns its own validator, environment-fed > resolver and (optionally) sibling-derived value. Built-in dimensions > (`localeDimension`, `motionDimension`, …) live in > `arts/prefs/dimensions/*` and the `standardPrefsDimensions(catalog)` > preset composes the canonical set. The active surface exposes one > slot per schema key with `.get()` / `.set()` / `.clear()` / > `.onChange()` verbs: > > ```ts > createActiveApp({ > prefs: { > schema: { > ...standardPrefsDimensions({ languages, locales, currencies }), > sidebarCollapsed: booleanDimension({ default: false }) > } > } > }); > > App.prefs.locale.get(); > App.prefs.locale.set('es-ES'); > App.prefs.sidebarCollapsed.set(true); > ``` > > The historical text below is kept for archival reference until this > README is rewritten in full. ## Core Rule `prefs` is not a Svelte store and must not depend on `svelte/store`. The core is a pure preference resolver: ```txt capabilities + environment + intent -> effective ``` The Svelte layer is only an adapter around that core: ```txt libs/prefs = pure contracts, validation and resolution arts/prefs = engine + active rune adapter active-app later = composition and wiring ``` No resolver may read `window`, `navigator`, cookies, headers, `localStorage`, IndexedDB, Svelte stores, DOM APIs or framework services directly. Those are adapters/ports. ## Purpose `prefs` owns application preferences that influence translation, formatting and UI presentation. It answers: - Which locale does the user explicitly want? - Which locale did the server or browser detect? - Which values does this application support? - Which values are effective after validation and fallback? - Which subset should be persisted as user intent? It does not translate, format, persist by itself, apply DOM changes, own security policy, or become a generic settings bag. Boundary: ```txt Prefs = preference resolution Langs = translation/catalog lookup Format = number/date/currency/unit formatting Prefs DOM projector = optional cross-modal DOM attrs Eidos = visual theme/mode/density projection Storage = persistence adapter Bus = optional event publication bridge Orca = optional orchestration bridge ``` ## Conceptual Model There are four state layers plus defaults. ```txt capabilities = what the application can offer environment = what the current request/browser/system suggests intent = what the user explicitly selected effective = what the system actually uses defaults = final fallback inside capabilities ``` Rules: - `capabilities` is declared by application composition. - `environment` can contain unsupported values. - `intent` is sparse and stores only explicit user choices. - `effective` is total, derived and always valid against capabilities. - `defaults` are part of capabilities and must be valid. - Only `intent` is persisted. - `environment` is recalculated. - `effective` is never persisted. Resolution: ```txt effective[key] = valid(intent[key], capabilities) ?? resolveFrom(environment, capabilities, key) ?? capabilities.defaults[key] ``` If an old persisted intent becomes unsupported after an app update, it is not deleted automatically. It is ignored for `effective` until capabilities allow it again or the app explicitly migrates/clears it. ## Naming Use: ```txt capabilities environment intent effective defaults ``` Avoid: ```txt allowed // sounds like permissions/security settings // too broad; becomes a junk drawer current // ambiguous active // already overloaded in Active chosen // ambiguous between intent and effective formatLocale // duplicate source of truth ``` ## Domain Types ```ts export type PrefsLocale = string; export type PrefsCurrency = string; export type PrefsTimezone = string; export type PrefsUnitSystem = 'metric' | 'imperial'; export type PrefsThemeIntent = 'light' | 'dark' | 'system'; export type PrefsThemeEffective = 'light' | 'dark'; export type PrefsDensity = 'compact' | 'comfortable' | 'spacious'; export type PrefsMotion = 'allow' | 'reduce' | 'system'; export type PrefsMotionEffective = 'allow' | 'reduce'; export type PrefsSoundEffective = 'allow' | 'reduce'; export type PrefsDirection = 'ltr' | 'rtl'; ``` `system` can be a real intent for theme/motion. It means "the user explicitly wants to follow the environment". It must not appear in `effective`. ## Capabilities `capabilities` is the legal universe of user-selectable values. ```ts export interface PrefsCapabilities { readonly languages: readonly PrefsLocale[]; readonly locales: readonly PrefsLocale[]; readonly currencies: readonly PrefsCurrency[]; readonly unitSystems: readonly PrefsUnitSystem[]; readonly themes: readonly PrefsThemeIntent[]; readonly densities: readonly PrefsDensity[]; readonly motions: readonly PrefsMotion[]; readonly sounds: readonly PrefsSoundEffective[]; readonly timezones?: readonly PrefsTimezone[]; readonly defaults: PrefsEffective; } ``` The app composes capabilities. `prefs` does not discover them by importing other modules. Example future composition: ```ts const capabilities: PrefsCapabilities = { languages: intersect(appConfig.languages, Langs.availableLocales), locales: intersect(appConfig.locales, Langs.availableLocales), currencies: appConfig.currencies, unitSystems: appConfig.unitSystems, themes: ['light', 'dark', 'system'], densities: ['compact', 'comfortable', 'spacious'], motions: ['allow', 'reduce', 'system'], sounds: ['allow', 'reduce'], defaults: { locale: 'es-ES', language: 'es-ES', currency: 'EUR', timezone: 'Europe/Madrid', unitSystem: 'metric', theme: 'light', density: 'comfortable', motion: 'allow', sound: 'allow', direction: 'ltr' } }; ``` If `Langs` does not provide `es-MX` and the application requires user-selectable locales to exist in `Langs`, then `es-MX` must not be in `capabilities.locales`. ## Environment `environment` is detected context. It is not user intent. ```ts export interface PrefsEnvironment { readonly locales?: readonly PrefsLocale[]; readonly timezone?: PrefsTimezone; readonly currency?: PrefsCurrency; readonly region?: string; readonly unitSystem?: PrefsUnitSystem; readonly colorScheme?: 'light' | 'dark'; readonly reducedMotion?: boolean; readonly source?: 'server' | 'browser' | 'mixed' | 'test'; } ``` Examples: ```txt Accept-Language: es-MX,es;q=0.9,en;q=0.8 navigator.languages: ['es-MX', 'es', 'en-US'] Intl timezone: America/Mexico_City matchMedia prefers-color-scheme: dark ``` Environment may contain values outside capabilities. That is useful diagnostic information and should not be confused with selectable values. ## Intent `intent` is what the user explicitly selected. It is sparse. Missing means "derive it". ```ts export interface PrefsIntent { readonly language?: PrefsLocale; readonly locale?: PrefsLocale; readonly currency?: PrefsCurrency; readonly timezone?: PrefsTimezone; readonly unitSystem?: PrefsUnitSystem; readonly theme?: PrefsThemeIntent; readonly density?: PrefsDensity; readonly motion?: PrefsMotion; readonly sound?: PrefsSoundEffective; readonly direction?: PrefsDirection; } ``` Intent writes must validate against capabilities. ```txt setIntent('locale', 'es-ES') -> ok if capabilities.locales includes es-ES setIntent('locale', 'es-MX') -> rejected if capabilities.locales excludes es-MX setIntent('currency', 'MXN') -> rejected if capabilities.currencies excludes MXN ``` Do not use `undefined` as a write command. Clearing intent must be explicit: ```ts Prefs.setIntent('locale', 'es-ES'); Prefs.clearIntent('locale'); Prefs.resetIntent(); ``` ## Effective `effective` is the only layer consumers should read. ```ts export interface PrefsEffective { readonly language: PrefsLocale; readonly locale: PrefsLocale; readonly currency: PrefsCurrency; readonly timezone: PrefsTimezone; readonly unitSystem: PrefsUnitSystem; readonly theme: PrefsThemeEffective; readonly density: PrefsDensity; readonly motion: PrefsMotionEffective; readonly sound: PrefsSoundEffective; readonly direction: PrefsDirection; } ``` `effective` is total, readonly and always valid. ```txt Langs reads effective.language Format reads effective.locale, currency, timezone, unitSystem PrefsDomProjection reads effective.direction, motion, sound, haptic Eidos owns visual theme/mode/density ``` `language` and `locale` are intentionally distinct. `language` drives translations and writing direction; `locale` drives regional formatting. There is no `formatLocale` alias: `Format` uses the effective locale that the app allowed. ## Snapshot ```ts export interface PrefsSnapshot { readonly capabilities: PrefsCapabilities; readonly environment: PrefsEnvironment; readonly intent: PrefsIntent; readonly effective: PrefsEffective; readonly version: number; } ``` Snapshots are serializable and immutable. Consumers must not mutate returned objects. ## Resolver The resolver is pure. ```ts export interface PrefsResolveInput { readonly capabilities: PrefsCapabilities; readonly environment: PrefsEnvironment; readonly intent: PrefsIntent; } export function resolvePrefs(input: PrefsResolveInput): PrefsEffective; ``` The resolver does not persist, emit events, read browser APIs or touch Svelte state. It only returns data. ## Locale Matching Locale matching should use canonical locale data, not string splitting. Recommended order: ```txt 1. exact match 2. language + script match 3. language + region match 4. language match 5. default locale ``` Example: ```txt environment.locales = ['es-MX', 'en-US'] capabilities.locales = ['es-ES', 'en-US'] effective.locale = 'es-ES' ``` `environment.locales` keeps `es-MX`; `effective.locale` uses `es-ES`. Pseudo-helper: ```ts function safeLocale(tag: string): Intl.Locale | undefined { try { return new Intl.Locale(Intl.getCanonicalLocales(tag)[0]); } catch { return undefined; } } ``` The exact implementation belongs in `libs/prefs/match-locale.ts`. ## Currency Currency is strict against capabilities. ```txt intent.currency -> accepted only if in capabilities.currencies environment.currency -> used only if in capabilities.currencies default.currency -> must be in capabilities.currencies ``` Locale does not imply currency. A user can use `en-US` with `EUR` if the app allows that currency. ## Timezone Timezone may be configured as either: ```txt open IANA validation closed capabilities.timezones allowlist ``` Validation should canonicalize when possible: ```ts function normalizeTimezone(timezone: string): string | undefined { try { return new Intl.DateTimeFormat('en-US', { timeZone: timezone }) .resolvedOptions() .timeZone; } catch { return undefined; } } ``` If `capabilities.timezones` exists, the normalized timezone must be present in that list. ## Engine Contract `EnginePrefs` is the non-Svelte runtime. ```ts export interface EnginePrefs { readonly kind: 'prefs'; snapshot(): PrefsSnapshot; capabilities(): PrefsCapabilities; environment(): PrefsEnvironment; intent(): Readonly; effective(): PrefsEffective; setIntent( key: K, value: NonNullable ): PrefsSnapshot; clearIntent(key: K): PrefsSnapshot; resetIntent(next?: PrefsIntent): PrefsSnapshot; refreshEnvironment(next: PrefsEnvironment): PrefsSnapshot; patchEnvironment(patch: Partial): PrefsSnapshot; setCapabilities(next: PrefsCapabilities): PrefsSnapshot; subscribe(handler: PrefsChangeHandler): PrefsUnsubscribe; dispose(): void; } ``` Writes recompute `effective` and notify subscribers only when the effective view or relevant snapshot layer changes. `dispose()` must be idempotent. ## Active Contract `ActivePrefs` is a Svelte rune adapter over `EnginePrefs`. It may use `$state`/`$derived` inside `.svelte.ts`, but the public contract is still the Active interface. It must not expose `svelte/store` as the module contract. ```ts export interface ActivePrefs extends EnginePrefs { readonly state: { readonly snapshot: PrefsSnapshot; readonly effective: PrefsEffective; readonly pending: boolean; readonly lastError: unknown; }; } ``` The active layer must not invent different rules. All validation and resolution comes from the core. ## Adapters And Ports External IO lives behind ports. ```ts export interface PrefsEnvironmentDetector { detect(): PrefsEnvironment; } export interface PrefsIntentStorage { load(): PrefsIntent | null | Promise; save(intent: PrefsIntent): void | Promise; clear(): void | Promise; } ``` `prefs` should not import `storage`; a bridge connects them later: ```ts createPrefsStorageBridge({ prefs, storage, key: 'active:prefs' }); ``` Storage bridge rules: - Persist only `intent`. - Do not persist `environment`. - Do not persist `effective`. - Do not persist secrets. - Do not write during initial hydrate unless explicitly configured. - Storage failures must not corrupt in-memory preferences. ## Consumer Ports Consumers should define ports in their own modules. `prefs` can satisfy those ports, but it should not know them. Example future ports: ```ts // langs export interface LangsPrefsPort { readonly locale: PrefsLocale; subscribe(handler: (locale: PrefsLocale) => void): PrefsUnsubscribe; } // format export interface FormatPrefsPort { readonly locale: PrefsLocale; readonly currency: PrefsCurrency; readonly timezone: PrefsTimezone; readonly unitSystem: PrefsUnitSystem; subscribe(handler: (view: FormatPrefsView) => void): PrefsUnsubscribe; } // active-uix / eidos export interface UixPrefsPort { readonly theme: PrefsThemeEffective; readonly density: PrefsDensity; readonly motion: PrefsMotionEffective; readonly sound: PrefsSoundEffective; readonly direction: PrefsDirection; subscribe(handler: (view: UixPrefsView) => void): PrefsUnsubscribe; } ``` Consumers build filtered views/proxies from `App.prefs` or `uix.prefs`. `prefs` itself remains unaware of `langs`, `format`, `uix`, `eidos` or DOM. ## Events The engine exposes local subscriptions. It does not import `buss`. ```ts export interface PrefsChangeEvent { readonly previous: PrefsSnapshot; readonly next: PrefsSnapshot; readonly effectiveDiff: Partial; readonly cause: | 'intent:set' | 'intent:clear' | 'intent:reset' | 'environment:refresh' | 'capabilities:set' | 'hydrate'; } ``` Later, `active-app` can bridge this to `Bus`: ```ts Prefs.subscribe((event) => { App.bus.publish(PREFS_EVENT_CHANGED, event); }); ``` All event names must be constants. ## Server And Browser Initialization Server flow: ```txt 1. app builds capabilities 2. server detector reads request/session/profile/cookies/Accept-Language 3. optional server storage loads persisted intent 4. engine resolves snapshot synchronously 5. snapshot is serialized into SSR payload ``` Browser flow: ```txt 1. hydrate from server snapshot 2. browser detector reads navigator/matchMedia/Intl APIs 3. optional client storage loads persisted intent 4. engine re-resolves 5. emit change only if effective values changed ``` Hydration rule: ```txt server snapshot wins first paint browser detection may refine after hydration browser detection must not override explicit intent ``` Avoid first-paint theme flashes at the UI shell boundary, not inside `prefs`: an inline head script may apply early `data-theme`/`data-mode` before Svelte mounts, then `ActiveEidos` adopts the same visual state during hydration. ## Validation Validation is field-specific: ```txt locale -> capability membership, with environment matching fallback currency -> strict capability membership timezone -> IANA validation, optionally capability membership unitSystem -> strict capability membership motion -> strict capability membership; system allowed as intent sound -> strict capability membership haptic -> strict capability membership ``` `theme` and `density` factories still exist for apps that declare their own custom preference schema, but they are no longer part of the standard core preset. Eidos owns visual `theme` / `mode` / `density`. ## DOM projection `ActivePrefs` is pure state and never writes the DOM by itself. When an app wants global preference attrs it wires the explicit projector: ```ts import { createActivePrefsDomProjection } from '$prefs'; const prefsProjection = createActivePrefsDomProjection({ prefs: App.prefs, dom: App.dom }); ``` `ActiveUix` follows the same rule: it exposes `uix.prefs` and `uix.dom`, but does not create this projector automatically. A UI shell that wants global attrs must wire it explicitly: ```ts const uix = createActiveUix({ langs, prefs: { schema } }); const prefsProjection = createActivePrefsDomProjection({ prefs: uix.prefs, dom: uix.dom }); ``` The projector only owns cross-modal attrs: ```txt prefs.direction -> dir prefs.motion -> data-motion prefs.sound -> data-sound prefs.haptic -> data-haptic ``` Visual attrs belong to `ActiveEidos`: ```txt eidos.theme -> data-theme eidos.mode -> data-mode eidos.density -> data-density ``` Do not declare or write `prefs.theme` for UIX. A docs shell or app theme toggle should pass visual mode/theme/density to `ActiveEidos` (`modeSource`, `densitySource`, `theme`) and leave `prefs` for cross-modal preferences. Invalid user writes should return/throw structured `prefs` errors when the `errs` module is available. Until then, tests should assert stable error names. ## Stability The engine should avoid unnecessary reactions: - Emit only when snapshot/effective data actually changes. - Provide field-level diffs. - Allow consumer ports to subscribe to focused views. - Return readonly snapshots. The resolver may create new objects internally, but subscribers should not be notified if values are shallow-equal. ## Future File Layout ```txt src/ libs/ prefs/ index.ts consts.ts types.ts ports.ts guards.ts match-locale.ts validate-intent.ts resolve-prefs.ts arts/ prefs/ index.ts README.md engine-prefs.ts active-prefs.svelte.ts adapters/ browser-environment.ts server-environment.ts storage-bridge.ts test/ ``` ## Implementation Agenda 1. Implement `libs/prefs` domain types and constants. 2. Implement `match-locale.ts` with exact/script/region/language/default order. 3. Implement field validation and intent sanitization. 4. Implement pure `resolvePrefs()`. 5. Implement `createEnginePrefs()` with local subscriptions and diffs. 6. Implement `active-prefs.svelte.ts` as a rune adapter over the engine. 7. Implement environment detector adapters. 8. Implement optional storage bridge that persists only `intent`. 9. Add tests for locale fallback, currency strictness, timezone normalization, explicit clear, theme `system`, hydration and dispose. 10. Only after that, integrate through `active-app` as `App.prefs`. ## Test Agenda Required tests before integration: - Default snapshot is valid against capabilities. - Intent writes reject unsupported values. - `clearIntent()` restores derived behavior without storing `undefined`. - Only `intent` is persisted. - `environment` may contain unsupported values. - `effective` never contains unsupported values. - Locale matching handles exact, language/script, language/region and language. - Currency never falls back by locale unless the app explicitly implements that policy in the resolver. - Timezone is canonicalized or rejected according to policy. - `theme: 'system'` persists as intent but resolves to `light` or `dark`. - Browser detection cannot override explicit intent. - `setCapabilities()` revalidates without deleting old intent. - Subscribers receive diffs and can unsubscribe. - `dispose()` is idempotent. ## IA Agents When working on `prefs`: - Do not wire it into `active-app` unless explicitly requested. - Do not import `svelte/store` in the core. - Do not import `langs`, `format`, `frontend`, `storage`, `buss`, `orca` or `active-app` from `libs/prefs`. - Do not persist `environment` or `effective`. - Do not create `formatLocale`. - Do not let `Langs` own preferences. - Do not let `Format` keep parallel preference state. - Do not add arbitrary settings. - Keep IO behind ports/adapters. - Add tests before another artifact consumes `prefs`.