Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
import type {
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
PrefsChangeHandler,
|
|
|
|
|
PrefsDimension,
|
|
|
|
|
PrefsEffectiveOf,
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
PrefsEnvironment,
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
PrefsIntentOf,
|
|
|
|
|
PrefsSchema,
|
|
|
|
|
PrefsSnapshot,
|
|
|
|
|
PrefsUnsubscribe
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
} from '$libs/prefs';
|
|
|
|
|
import { createEnginePrefs } from './engine-prefs.ts';
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
import { PREFS_KIND } from './consts.ts';
|
|
|
|
|
import { PrefsReservedKeyError, reservedKeyErrorMessage } from './errors.ts';
|
|
|
|
|
import type { EnginePrefsOptions } from './types.ts';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Per-dimension active surface. Every key in the schema becomes one of
|
|
|
|
|
* these on the parent `ActivePrefs<S>`, addressable as
|
|
|
|
|
* `App.prefs.<key>`. The verbs are uniform across every dimension: get
|
|
|
|
|
* the effective value, set / clear user intent, listen for changes.
|
|
|
|
|
*/
|
|
|
|
|
export interface ActivePrefsDimension<TIntent, TEffective = TIntent> {
|
|
|
|
|
/** Current effective value (intent → environment → default). */
|
|
|
|
|
get(): TEffective;
|
|
|
|
|
/**
|
|
|
|
|
* Set explicit user intent for this dimension. Validates first;
|
|
|
|
|
* throws `PrefsIntentInvalidError` on rejection.
|
|
|
|
|
*/
|
|
|
|
|
set(value: TIntent): void;
|
|
|
|
|
/** Drop user intent. Falls back to environment / default. */
|
|
|
|
|
clear(): void;
|
|
|
|
|
/**
|
|
|
|
|
* Subscribe to commits where this dimension's effective value
|
|
|
|
|
* changed. Returns an unsubscribe function. Best-effort: a handler
|
|
|
|
|
* that throws does not block its peers.
|
|
|
|
|
*/
|
|
|
|
|
onChange(handler: (value: TEffective) => void): PrefsUnsubscribe;
|
|
|
|
|
/** Optional capability catalog when the dimension exposes one. */
|
|
|
|
|
catalog(): readonly TIntent[] | undefined;
|
|
|
|
|
}
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reactive view of `EnginePrefs.state`. Backed by `$state` cells inside
|
|
|
|
|
* `createActivePrefs` — accessing the getters inside a Svelte template
|
|
|
|
|
* or `$derived` tracks dependencies the way you'd expect.
|
|
|
|
|
*
|
|
|
|
|
* `pending` and `lastError` are placeholders for async adapters (the
|
|
|
|
|
* storage bridge in particular). With a sync-only engine they stay
|
|
|
|
|
* `false` / `null`.
|
|
|
|
|
*/
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
export interface ActivePrefsState<S extends PrefsSchema> {
|
|
|
|
|
readonly snapshot: PrefsSnapshot<S>;
|
|
|
|
|
readonly effective: PrefsEffectiveOf<S>;
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
readonly environment: PrefsEnvironment;
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
readonly intent: PrefsIntentOf<S>;
|
|
|
|
|
readonly version: number;
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
readonly pending: boolean;
|
|
|
|
|
readonly lastError: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
* Reserved members of `ActivePrefs<S>`. A schema key that matches one
|
|
|
|
|
* of these would shadow the active surface — the constructor throws
|
|
|
|
|
* `PrefsReservedKeyError` so the misconfiguration fails fast.
|
|
|
|
|
*/
|
|
|
|
|
export const ACTIVE_PREFS_RESERVED_KEYS: readonly string[] = [
|
|
|
|
|
'kind',
|
|
|
|
|
'schema',
|
|
|
|
|
'state',
|
|
|
|
|
'snapshot',
|
|
|
|
|
'environment',
|
|
|
|
|
'intent',
|
|
|
|
|
'effective',
|
|
|
|
|
'resetIntent',
|
|
|
|
|
'refreshEnvironment',
|
|
|
|
|
'patchEnvironment',
|
|
|
|
|
'subscribe',
|
|
|
|
|
'dispose'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const RESERVED_SET = new Set(ACTIVE_PREFS_RESERVED_KEYS);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reactive Svelte adapter over `EnginePrefs<S>`. Adds the
|
|
|
|
|
* `ActivePrefsState` block and exposes one `ActivePrefsDimension` per
|
|
|
|
|
* schema key as a property — i.e. for `schema = { locale, theme }` the
|
|
|
|
|
* returned object has `.locale.get()` / `.locale.set(...)` /
|
|
|
|
|
* `.theme.get()` / etc., type-checked against each dimension's
|
|
|
|
|
* `TIntent` and `TEffective`.
|
|
|
|
|
*/
|
|
|
|
|
/**
|
|
|
|
|
* Top-level (non-dimension) surface. Always present regardless of the
|
|
|
|
|
* schema. Service factories see this shape via `core.prefs` and read
|
|
|
|
|
* specific dimensions defensively at runtime through their string key.
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
*/
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
export interface ActivePrefsBase<S extends PrefsSchema = PrefsSchema> {
|
|
|
|
|
readonly kind: typeof PREFS_KIND;
|
|
|
|
|
readonly schema: S;
|
|
|
|
|
readonly state: ActivePrefsState<S>;
|
|
|
|
|
|
|
|
|
|
snapshot(): PrefsSnapshot<S>;
|
|
|
|
|
environment(): PrefsEnvironment;
|
|
|
|
|
intent(): PrefsIntentOf<S>;
|
|
|
|
|
effective(): PrefsEffectiveOf<S>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Low-level mutator. The recommended public API is
|
|
|
|
|
* `App.prefs.<dim>.set(value)` — this stays exposed only for
|
|
|
|
|
* adapters (storage bridge, devtools) that operate generically over
|
|
|
|
|
* dimension keys.
|
|
|
|
|
*/
|
|
|
|
|
setIntent<K extends keyof S>(key: K, value: unknown): PrefsSnapshot<S>;
|
|
|
|
|
/** Low-level mutator; prefer `App.prefs.<dim>.clear()`. */
|
|
|
|
|
clearIntent<K extends keyof S>(key: K): PrefsSnapshot<S>;
|
|
|
|
|
resetIntent(next?: PrefsIntentOf<S>): PrefsSnapshot<S>;
|
|
|
|
|
refreshEnvironment(next: PrefsEnvironment): PrefsSnapshot<S>;
|
|
|
|
|
patchEnvironment(patch: Partial<PrefsEnvironment>): PrefsSnapshot<S>;
|
|
|
|
|
subscribe(handler: PrefsChangeHandler<S>): PrefsUnsubscribe;
|
|
|
|
|
dispose(): void;
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
}
|
|
|
|
|
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
/**
|
|
|
|
|
* Dimension surface — one slot per schema key. Only meaningful when
|
|
|
|
|
* `S` is a concrete schema literal. With the open `PrefsSchema`
|
|
|
|
|
* (`Record<string, PrefsDimension>`) the mapped type would collide
|
|
|
|
|
* with the index signature on the base, so we degrade to `unknown`
|
|
|
|
|
* for the open case (`X & unknown = X`).
|
|
|
|
|
*
|
|
|
|
|
* Service factories that need typed dimensions cast through their own
|
|
|
|
|
* schema generic; consumers that read `App.prefs.<key>` get the typed
|
|
|
|
|
* surface because `App` flows the concrete `TPrefsSchema`.
|
|
|
|
|
*/
|
|
|
|
|
export type ActivePrefsDimensions<S extends PrefsSchema> = string extends keyof S
|
|
|
|
|
? unknown
|
|
|
|
|
: {
|
|
|
|
|
readonly [K in keyof S]: S[K] extends PrefsDimension<infer TIntent, infer TEffective>
|
|
|
|
|
? ActivePrefsDimension<TIntent, TEffective>
|
|
|
|
|
: never;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ActivePrefs<S extends PrefsSchema = PrefsSchema> = ActivePrefsBase<S> &
|
|
|
|
|
ActivePrefsDimensions<S>;
|
|
|
|
|
|
|
|
|
|
export function createActivePrefs<S extends PrefsSchema>(
|
|
|
|
|
options: EnginePrefsOptions<S>
|
|
|
|
|
): ActivePrefs<S> {
|
|
|
|
|
for (const key of Object.keys(options.schema)) {
|
|
|
|
|
if (RESERVED_SET.has(key)) {
|
|
|
|
|
throw new PrefsReservedKeyError(key, reservedKeyErrorMessage(key));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
const engine = createEnginePrefs(options);
|
|
|
|
|
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
let snapshotCell = $state<PrefsSnapshot<S>>(engine.snapshot());
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
// `pending` / `lastError` are placeholders today (sync engine has
|
|
|
|
|
// nothing async to track). Declared as `let` so async adapters
|
|
|
|
|
// (storage bridge in particular) can flip them when wired in
|
|
|
|
|
// without restructuring the rune layout.
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
const pendingCell = $state(false);
|
|
|
|
|
const lastErrorCell = $state<unknown>(null);
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
|
|
|
|
|
const detachCommit = engine.subscribe((event) => {
|
|
|
|
|
snapshotCell = event.next;
|
|
|
|
|
});
|
|
|
|
|
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
const state: ActivePrefsState<S> = {
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
get snapshot() {
|
|
|
|
|
return snapshotCell;
|
|
|
|
|
},
|
|
|
|
|
get effective() {
|
|
|
|
|
return snapshotCell.effective;
|
|
|
|
|
},
|
|
|
|
|
get environment() {
|
|
|
|
|
return snapshotCell.environment;
|
|
|
|
|
},
|
|
|
|
|
get intent() {
|
|
|
|
|
return snapshotCell.intent;
|
|
|
|
|
},
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
get version() {
|
|
|
|
|
return snapshotCell.version;
|
|
|
|
|
},
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
get pending() {
|
|
|
|
|
return pendingCell;
|
|
|
|
|
},
|
|
|
|
|
get lastError() {
|
|
|
|
|
return lastErrorCell;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
const dimensionMembers: Record<string, ActivePrefsDimension<unknown, unknown>> = {};
|
|
|
|
|
for (const key of Object.keys(options.schema)) {
|
|
|
|
|
dimensionMembers[key] = makeDimension(engine, key);
|
|
|
|
|
}
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
const base = {
|
|
|
|
|
kind: PREFS_KIND as typeof PREFS_KIND,
|
|
|
|
|
schema: options.schema,
|
|
|
|
|
state,
|
|
|
|
|
snapshot: () => engine.snapshot(),
|
|
|
|
|
environment: () => engine.environment(),
|
|
|
|
|
intent: () => engine.intent(),
|
|
|
|
|
effective: () => engine.effective(),
|
|
|
|
|
setIntent: <K extends keyof S>(key: K, value: unknown) => engine.setIntent(key, value),
|
|
|
|
|
clearIntent: <K extends keyof S>(key: K) => engine.clearIntent(key),
|
|
|
|
|
resetIntent: (next?: PrefsIntentOf<S>) => engine.resetIntent(next),
|
|
|
|
|
refreshEnvironment: (next: PrefsEnvironment) => engine.refreshEnvironment(next),
|
|
|
|
|
patchEnvironment: (patch: Partial<PrefsEnvironment>) => engine.patchEnvironment(patch),
|
|
|
|
|
subscribe: (handler: PrefsChangeHandler<S>) => engine.subscribe(handler),
|
|
|
|
|
dispose: () => {
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
detachCommit();
|
|
|
|
|
engine.dispose();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
Prefs as schema-based core + lowercase App.* surface
Two structural changes that were overdue and got bundled because
they touched the same set of files.
## Prefs is now a schema, not a fixed shape
Previously every preference had to be declared in a closed
`PrefsCapabilities` interface (`languages`, `locales`, `currencies`,
`themes`, `densities`, `motions`, `timezones`, `unitSystems`). Adding
a new pref required forking `$libs/prefs` — bad framework design.
The redesign replaces the fixed shape with a schema:
PrefsSchema = Record<string, PrefsDimension<TIntent, TEffective>>
Each dimension owns its own validator (`validate`), environment-fed
resolver (`resolve`) and optional sibling-derived value (`derive`).
The engine is generic over the schema and iterates it; it knows
nothing about "locale" or "theme" specifically.
Built-in dimensions live in `arts/prefs/dimensions/*` (locale,
language, theme, density, motion, timezone, currency, unit-system,
direction, plus boolean / enum / string / number primitives). The
`standardPrefsDimensions(catalog)` preset composes the canonical
set; apps spread it and add their own:
const schema = {
...standardPrefsDimensions({ languages, locales, currencies }),
sidebarCollapsed: booleanDimension({ default: false }),
notificationLevel: enumDimension(
['all', 'mentions', 'none'] as const,
{ default: 'mentions' }
)
};
Active surface exposes one slot per schema key with uniform verbs:
App.prefs.locale.get()
App.prefs.locale.set('es-ES')
App.prefs.locale.clear()
App.prefs.locale.onChange((v) => …)
App.prefs.sidebarCollapsed.set(true)
`setIntent('locale', value)` stays available as a low-level pass-
through (storage bridge consumes it generically) but UI code uses
the dimension surface.
## Lowercase core surface
`App.Logger`, `App.Bus`, `App.Timers`, `App.Orca`, `App.Prefs` are
gone. The "PascalCase for core, lowercase for services" rule was
visual signalling against JS convention, no technical benefit, and
created an asymmetry on the same object. All core members are now
lowercase, matching services:
App.logger
App.bus
App.timers
App.orca
App.prefs
`createPrefsStorageBridge` keeps its old responsibilities; sources
helpers (`prefsLocaleSource`, …) are gone — the dimension API
replaces them.
## What changed
- `$libs/prefs`: fully generic schema-based types + resolver. Old
fixed `PrefsCapabilities` / `PrefsIntent` / `PrefsEffective`
removed; replaced by `PrefsDimension`, `PrefsSchema`,
`PrefsEffectiveOf<S>`, `PrefsIntentOf<S>`.
- `arts/prefs`: engine + active wrapper rewritten to schema. Per-
dimension active surface auto-built from schema keys. Sources file
deleted (replaced by dimension surface). New
`arts/prefs/dimensions/*` and `arts/prefs/standard.ts`. Storage
bridge made schema-generic.
- `arts/active-app`: lowercase `CoreServices` / `ActiveAppCore`,
`prefs?: ActiveAppPrefsOptions<S>` root option carrying the
schema. `defineActivePrefs` deleted (prefs is core, not service).
`lang` / `format` / `frontend` factories migrated to read
`core.prefs.<dim>` directly via defensive `readSlot()` helpers
(each dimension is optional from the factory's POV; if the app's
schema omits one, the integration degrades gracefully).
- Presets, demos, web routes, README docstrings, `check-aliases.mjs`
guards, marketing snippets all migrated.
- Tests: `engine-prefs`, `active-prefs`, `storage-bridge`,
`resolve-prefs`, `validate-intent`, `prefs-consumer-wiring`,
`service-factories` rewritten for the schema-based API.
`sources.test.ts` deleted (sources file is gone).
## Verification
- `npm run check`: 0 errors, 0 warnings (1527 files).
- `npm test`: 1645 tests across 139 files, all green.
- `node scripts/check-aliases.mjs`: clean (lowercase enforced for
every member of `App.*`, including `Logger`/`Bus`/`Timers`/`Orca`/
`Prefs` which now flag as forbidden capitals).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
return Object.assign(base, dimensionMembers) as unknown as ActivePrefs<S>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeDimension(
|
|
|
|
|
engine: ReturnType<typeof createEnginePrefs>,
|
|
|
|
|
key: string
|
|
|
|
|
): ActivePrefsDimension<unknown, unknown> {
|
|
|
|
|
return {
|
|
|
|
|
get() {
|
|
|
|
|
return (engine.snapshot().effective as Record<string, unknown>)[key];
|
|
|
|
|
},
|
|
|
|
|
set(value: unknown) {
|
|
|
|
|
engine.setIntent(key, value);
|
|
|
|
|
},
|
|
|
|
|
clear() {
|
|
|
|
|
engine.clearIntent(key);
|
|
|
|
|
},
|
|
|
|
|
onChange(handler: (value: unknown) => void) {
|
|
|
|
|
return engine.subscribe((event) => {
|
|
|
|
|
const diff = event.effectiveDiff as Record<string, unknown>;
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(diff, key)) {
|
|
|
|
|
handler(diff[key]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
catalog() {
|
|
|
|
|
const dim = engine.schema[key];
|
|
|
|
|
return dim?.catalog?.();
|
|
|
|
|
}
|
|
|
|
|
};
|
Bloque L2 — arts/prefs engine + rune adapter + capability sources
Runtime layer over `libs/prefs`. Pure data engine, runes-free so server-only
modules can import it; the Svelte adapter is opt-in.
- `engine-prefs.ts` — `createEnginePrefs({ capabilities, environment?, intent? })`
holds the four-layer state, recomputes `effective` per commit, and notifies
subscribers with `{previous, next, effectiveDiff, cause}`. No-op writes
short-circuit (no version bump, no listener walk). `setIntent` validates
synchronously and throws `PrefsIntentInvalidError` with the structured
failure code from `validateIntentValue`. `setCapabilities` validates the
new `defaults` against the new sets but preserves existing intent —
capability shrink does not corrupt the persisted user choice.
- `active-prefs.svelte.ts` — rune adapter exposing `state.snapshot`,
`state.effective`, `state.{capabilities,environment,intent}`. `pending` and
`lastError` are placeholders for the storage bridge to flip later.
- `sources.ts` — nine narrow `Source<T>` proxies (`prefsLanguageSource`,
`prefsLocaleSource`, …). Each forwards `onChange` only when its specific
field appears in `effectiveDiff`, so consumers wake up per-dimension.
- `errors.ts` — `PrefsDisposedError`, `PrefsIntentInvalidError`,
`PrefsCapabilitiesInvalidError`, all rooted at `PREFS_ERR` via `libs/errs`.
- README rewritten to match the locked-in design (parallel projection,
language/locale split, Source<T> port, persisted-intent-survives-shrink
rule).
Tests: 27 engine + 5 rune adapter (client project) + 8 capability sources.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
}
|