docs(arts): ethereal README + A1 drift sweep (orca/cache/adom) + honest guard

Ethereal (was undocumented — the miss that prompted this pass):
- new src/arts/ethereal/README.md, API drawn from the code (computePosition,
  the 7 middleware, autoUpdate, selectPositioningStrategy, dual JS/CSS-anchor
  engine, +1-frame read-phase model)
- wired into src/arts/README.md Map + aliases + cross-deps; $ethereal added to
  the CLAUDE.md alias table; the also-missing bus/orca/prefs aliases + Map rows
  completed against vite.config.ts

A1 drift (every fix verified against the cited code symbol, not the plan):
- orca: ~13 phantom identifiers across 7 sections (priority, execution /
  ORCA_EXEC_*, tokenTimeoutMs, onFatal / ORCA_ON_FATAL_*, onTimeout, the whole
  multi-level timeout model, the transaction-port model + ORCA_TX_*,
  OrcaMaybePromise, OrcaCompensationContext) reconciled to the real API
  (parallel waves, actionTimeoutMs-only, transaction:string tag + compensate,
  fatal via returning OrcaFatal); added the real missing fields fanIn / parallel
  / compensate; glossary OrcaRun -> OrcaRunResult
- cache: residual createActiveApp({cache}) -> services:{cache:defineActiveCache()};
  documented the ActiveEngine contract surface (loading / lastError / disposed /
  snapshot / clearError / onChange / dispose); dangling demo URL ->
  /active/get-started/ecosystem
- adom: added prefersReducedMotion / writeProperty / removeProperty; listen
  1 -> 4 typed overloads; documented 12 undocumented standalone rune helpers;
  /test/adom -> /active/docs/adom

Guard (scripts/arts-check.ts): removed the ethereal README exemption (set empty)
and promoted A-index from warn to error — the net no longer passes green with a
real gap. arts:check: 0 errors, 0 warnings, 22 arts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 0ca2495468
commit 9e48b80b10

@ -93,7 +93,7 @@ resolve and the server-test project).
| Alias | Target |
| --- | --- |
| `@` | `src/` |
| `$active-app`, `$adom`, `$auth`, `$bus`, `$cache`, `$clipboard`, `$color`, `$connection`, `$format`, `$http`, `$langs`, `$logger`, `$motion`, `$orca`, `$perf`, `$perm`, `$prefs`, `$session`, `$sium`, `$storage`, `$timer` | `src/arts/{name}` |
| `$active-app`, `$adom`, `$auth`, `$bus`, `$cache`, `$clipboard`, `$color`, `$connection`, `$ethereal`, `$format`, `$http`, `$langs`, `$logger`, `$motion`, `$orca`, `$perf`, `$perm`, `$prefs`, `$session`, `$sium`, `$storage`, `$timer` | `src/arts/{name}` |
| `$libs`, `$locale`, `$reactive` | `src/libs`, `src/libs/locale`, `src/libs/reactive` |
| `$svrs` | `src/svrs/` |
| `$uix`, `$active-uix`, `$soma` | `src/uix`, `src/uix/active-uix`, `src/uix/soma` |

@ -8,10 +8,10 @@
* conventions" + the "Map" index:
*
* A-readme (error) — every `src/arts/{name}/` has a README.md. A missing
* README is a hole in the corpus. `ethereal` is the one known gap (it only
* ships PERF.md); it is exempt until batch A3 authors its README. The
* exemption is self-cleaning: once the file lands, the guard demands the
* art be removed from README_PENDING.
* README is a hole in the corpus. README_PENDING is a self-cleaning
* exemption set (empty now — every art is documented, ethereal included):
* if an exempted art later gains a README the guard demands it be removed
* from the set, so the exemption can never outlive the gap it covers.
* A-naming (error) — the `Engine*` / `Active*` split holds:
* · No `Engine*` root is DECLARED in a `.svelte.ts` file. Engines are
* pure factories over private/no state (README §7-9) — they never own
@ -20,9 +20,9 @@
* · Every reactive `class Active*` root lives in a `.svelte.ts` file
* (README §12) — a plain `.ts` cannot own module `$state`. Error
* classes (`Active…Error`) are not reactive roots and are exempt.
* A-index (warn) — the `src/arts/README.md` Map lists every art. WARN (not
* error) because closing the four current gaps (bus, orca, prefs, ethereal)
* is batch B3's job; the warnings are B3's worklist and drive to zero there.
* A-index (error) — the `src/arts/README.md` Map lists every art. An art
* that never reaches the index is invisible to readers (ethereal was: a
* full positioning engine absent from the Map, the aliases and the deps).
*
* Mirrors scripts/docs-check.ts (textual invariants, no Svelte-graph imports).
* Exit code 1 when any error-level finding exists.
@ -87,8 +87,9 @@ const tsFiles = walkTs(ARTS);
// ── Invariant A-readme — every art has a README ─────────────────────────────
/** ethereal only ships PERF.md today; batch A3 authors its README. */
const README_PENDING = new Set(['ethereal']);
/** Self-cleaning exemption for arts whose README is not authored yet. Empty:
* every art is documented (ethereal's README landed with its Map/alias wiring). */
const README_PENDING = new Set<string>();
for (const art of arts) {
const readme = join(ARTS, art, 'README.md');
@ -156,11 +157,11 @@ const indexed = new Set([...indexSrc.matchAll(/\.\/([a-z][\w-]*)\/README\.md/g)]
for (const art of arts) {
if (!indexed.has(art)) {
report(
'warn',
'error',
'A-index',
join(ARTS, 'README.md'),
1,
`art '${art}' is not listed in the arts index Map table — batch B3 adds it`
`art '${art}' is not listed in the arts index Map table (src/arts/README.md)`
);
}
}

@ -108,26 +108,30 @@ errors are typed.
## Map
| Artifact | Layer(s) | Purpose | Depends on |
| -------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [`langs`](./langs/README.md) | `EngineLangs`, `ActiveLangs`, `ActiveMonoLangs` | i18n: type-safe translations, BCP 47 resolution, plurals, refs, JSON round-trip | — |
| [`logger`](./logger/README.md) | `EngineLogger` | Structured logger: levels, transports, filters, vitals, dispose | — |
| [`timer`](./timer/README.md) | `EngineTimers`, `ActiveTimers` | Deterministic timer scheduler: clock injection, one-shots, intervals, cancellation, snapshots, backoff | `$libs/timers`, `$logger` (optional) |
| [`format`](./format/README.md) | `EngineFormat`, `ActiveFormat` | Localized formatting: numbers, currency, units, dates | `$logger` (currency) |
| [`adom`](./adom/README.md) | `ActiveDom` | Reactive DOM service: viewport, breakpoints, attribute writes, scroll lock, post-layout read scheduling (`measure`) | `$libs/dom`, `$reactive` |
| [`motion`](./motion/README.md) | `EngineMotion` | Animation runtime: registers + runs `--state` presets (CSS settle / JS drivers — spring / waapi / rect FLIP); the bridge BOTH UIX layers consume via `uix.motion` | `MotionDom` port (injected; `adom` satisfies it) |
| [`clipboard`](./clipboard/README.md) | `ActiveClipboard` | Clipboard write capability with injectable writer and explicit unavailable errors | browser `navigator.clipboard` or injected writer |
| [`sium`](./sium/README.md) | `EngineSium` | Validation contracts: schemas, issues, introspection, Standard Schema interop | `$langs` (optional), `$logger` (optional), `$libs/days`, `$libs/color` |
| [`storage`](./storage/README.md) | `EngineStorage`, `ActiveStorage` | Reactive sync key/value: pluggable adapters, version+migrate, TTL, validation, intra-tab + cross-tab sync, reactive keys | `$sium` (Standard Schema interop, optional) |
| [`http`](./http/README.md) | `EngineHttp` | HTTP client: tagged `HttpResult`, Standard Schema validation, retry, timeouts, hooks, SvelteKit `event.fetch` integration | `$libs/http`, `$libs/standard-schema` (type-only), `$logger` |
| [`session`](./session/README.md) | `EngineSession`, `ActiveSession` | Session lifecycle: adopt/revoke/refresh, auto-refresh, 401-rescue hook, SvelteKit SSR via `adoptServer` + cookie reader | `$storage`, `$timer`, `$http`, `$logger` (optional) |
| [`connection`](./connection/README.md) | `EngineConnections`, `ActiveConnections` | Realtime connection registry: transports, reconnect, heartbeat, request/reply, channels, session bridge | `$timer`, `$logger` (optional), `$session` bridge (optional) |
| [`auth`](./auth/README.md) | `ActiveAuth` (`EngineAuth` in `$svrs/auth`) | Authentication: password flows, CSRF, current session reflector, devices, logout, server-authoritative auth handlers | `$libs/auth`, `$http`, `$cache`, `$svrs/auth` |
| [`perm`](./perm/README.md) | `ActivePerms` (`EnginePerms` in `$svrs/perm`) | Authorization: policy runtime adapter, HTTP client/handlers, cache snapshot, `<Can />` guard | `$libs/perm`, `$libs/svrs`, `$http`, `$logger` (optional) |
| [`cache`](./cache/README.md) | `ActiveCache` (`EngineCache` in `$svrs/cache`) | Data cache: deterministic keys, policies, scopes, stale/revalidate, tags, memory/storage adapters | `$libs/cache`, `$storage` (adapter), `$logger` (optional) |
| [`color`](./color/README.md) | `$color` namespace (`uix.color`) | Isomorphic colour math: OKLCH↔sRGB, APCA, scale/scheme generation, alpha. Pure + stateless — `Engine`-grade, no class | — (zero-dep; consumed by eidos at build + runtime) |
| [`perf`](./perf/README.md) | `ActivePerf` (`uix.perf`) | Dev forced-reflow detector: Long Animation Frames → attributed `forcedStyleAndLayoutDuration` reports; opt-in, inert in prod | platform LoAF API (Chromium) — zero-dep |
| [`active-app`](./active-app/README.md) | `ActiveApp` | App composition: core Logger + Bus + Timers + Orca + Prefs, plus declared services via factories | every artifact above |
| Artifact | Layer(s) | Purpose | Depends on |
| -------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| [`langs`](./langs/README.md) | `EngineLangs`, `ActiveLangs`, `ActiveMonoLangs` | i18n: type-safe translations, BCP 47 resolution, plurals, refs, JSON round-trip | — |
| [`logger`](./logger/README.md) | `EngineLogger` | Structured logger: levels, transports, filters, vitals, dispose | — |
| [`timer`](./timer/README.md) | `EngineTimers`, `ActiveTimers` | Deterministic timer scheduler: clock injection, one-shots, intervals, cancellation, snapshots, backoff | `$libs/timers`, `$logger` (optional) |
| [`format`](./format/README.md) | `EngineFormat`, `ActiveFormat` | Localized formatting: numbers, currency, units, dates | `$logger` (currency) |
| [`adom`](./adom/README.md) | `ActiveDom` | Reactive DOM service: viewport, breakpoints, attribute writes, scroll lock, post-layout read scheduling (`measure`) | `$libs/dom`, `$reactive` |
| [`motion`](./motion/README.md) | `EngineMotion` | Animation runtime: registers + runs `--state` presets (CSS settle / JS drivers — spring / waapi / rect FLIP); the bridge BOTH UIX layers consume via `uix.motion` | `MotionDom` port (injected; `adom` satisfies it) |
| [`ethereal`](./ethereal/README.md) | `$ethereal` (`computePosition` + middleware) | In-house positioning engine: collision-aware placement (offset/shift/flip/arrow/size/hide), `autoUpdate`, native CSS-anchor strategy — our parity-verified subset of `@floating-ui`; the JS + CSS paths BOTH UIX layers consume | `$adom` (DOM reads); no other art (`@floating-ui` = devDep parity baseline) |
| [`clipboard`](./clipboard/README.md) | `ActiveClipboard` | Clipboard write capability with injectable writer and explicit unavailable errors | browser `navigator.clipboard` or injected writer |
| [`sium`](./sium/README.md) | `EngineSium` | Validation contracts: schemas, issues, introspection, Standard Schema interop | `$langs` (optional), `$logger` (optional), `$libs/days`, `$libs/color` |
| [`storage`](./storage/README.md) | `EngineStorage`, `ActiveStorage` | Reactive sync key/value: pluggable adapters, version+migrate, TTL, validation, intra-tab + cross-tab sync, reactive keys | `$sium` (Standard Schema interop, optional) |
| [`http`](./http/README.md) | `EngineHttp` | HTTP client: tagged `HttpResult`, Standard Schema validation, retry, timeouts, hooks, SvelteKit `event.fetch` integration | `$libs/http`, `$libs/standard-schema` (type-only), `$logger` |
| [`session`](./session/README.md) | `EngineSession`, `ActiveSession` | Session lifecycle: adopt/revoke/refresh, auto-refresh, 401-rescue hook, SvelteKit SSR via `adoptServer` + cookie reader | `$storage`, `$timer`, `$http`, `$logger` (optional) |
| [`connection`](./connection/README.md) | `EngineConnections`, `ActiveConnections` | Realtime connection registry: transports, reconnect, heartbeat, request/reply, channels, session bridge | `$timer`, `$logger` (optional), `$session` bridge (optional) |
| [`auth`](./auth/README.md) | `ActiveAuth` (`EngineAuth` in `$svrs/auth`) | Authentication: password flows, CSRF, current session reflector, devices, logout, server-authoritative auth handlers | `$libs/auth`, `$http`, `$cache`, `$svrs/auth` |
| [`perm`](./perm/README.md) | `ActivePerms` (`EnginePerms` in `$svrs/perm`) | Authorization: policy runtime adapter, HTTP client/handlers, cache snapshot, `<Can />` guard | `$libs/perm`, `$libs/svrs`, `$http`, `$logger` (optional) |
| [`cache`](./cache/README.md) | `ActiveCache` (`EngineCache` in `$svrs/cache`) | Data cache: deterministic keys, policies, scopes, stale/revalidate, tags, memory/storage adapters | `$libs/cache`, `$storage` (adapter), `$logger` (optional) |
| [`color`](./color/README.md) | `$color` namespace (`uix.color`) | Isomorphic colour math: OKLCH↔sRGB, APCA, scale/scheme generation, alpha. Pure + stateless — `Engine`-grade, no class | — (zero-dep; consumed by eidos at build + runtime) |
| [`perf`](./perf/README.md) | `ActivePerf` (`uix.perf`) | Dev forced-reflow detector: Long Animation Frames → attributed `forcedStyleAndLayoutDuration` reports; opt-in, inert in prod | platform LoAF API (Chromium) — zero-dep |
| [`bus`](./bus/README.md) | `EngineBus`, `ActiveBus` | Mechanical typed event bus: typed envelopes, deterministic order, explicit error policy, re-entrancy guard, observability hooks, Svelte adapter; catalog-agnostic, injected by `active-app` | `$libs/bus` (pure contracts) |
| [`orca`](./orca/README.md) | `EngineOrca`, `ActiveOrca` | Active orchestration kernel: runs declarative actions on bus events with order, dependencies, `Result` + tokens, failure policies, timers, optional transactions, queue policies + static `validate()` | `$bus`, `$timer`, `$logger` (optional) |
| [`prefs`](./prefs/README.md) | `EnginePrefs`, `ActivePrefs` | Active preferences: resolves user intent × detected environment × effective value for a declared schema; feeds `langs` / `format` / direction; generic dimensions; optional DOM projection | — (optional DOM projection) |
| [`active-app`](./active-app/README.md) | `ActiveApp` | App composition: core Logger + Bus + Timers + Orca + Prefs, plus declared services via factories | every artifact above |
## Composition
@ -222,6 +226,11 @@ adom ──────────────────\ | / | connect
(soma's `Presence` + eidos wrappers), which dissolves the would-be soma→eidos
coupling. It imports no other art — the DOM dependency arrives injected via the
structural `MotionDom` port, which `adom` satisfies.
- `ethereal` is the positioning engine consumed by BOTH UIX layers — soma's
`layers/floating` (the JS path) and eidos's `render-css` (the native
CSS-anchor path) — the same both-layers shape as `motion`. It imports no other
art; its DOM reads arrive injected via `$adom`. `@floating-ui` is a
devDep-only parity baseline, never a runtime import.
- `active-app` composes always-present roots and exposes factories for scoped
artifacts (`sium`, `session`, `connection`, `auth`, `perm`).
@ -240,17 +249,21 @@ alias: {
$active-app: 'src/arts/active-app',
$adom: 'src/arts/adom',
$auth: 'src/arts/auth',
$bus: 'src/arts/bus',
$cache: 'src/arts/cache',
$clipboard: 'src/arts/clipboard',
$color: 'src/arts/color',
$connection: 'src/arts/connection',
$ethereal: 'src/arts/ethereal',
$format: 'src/arts/format',
$http: 'src/arts/http',
$langs: 'src/arts/langs',
$logger: 'src/arts/logger',
$motion: 'src/arts/motion',
$orca: 'src/arts/orca',
$perf: 'src/arts/perf',
$perm: 'src/arts/perm',
$prefs: 'src/arts/prefs',
$session: 'src/arts/session',
$sium: 'src/arts/sium',
$storage: 'src/arts/storage',

@ -104,11 +104,18 @@ export interface ActiveDom {
breakpoints: Active<Breakpoints>;
viewport: { readonly width: number };
currentBreakpoint: Active<Breakpoint>;
/** Reactive `matchMedia('(prefers-reduced-motion: reduce)')`; `false` in SSR. */
prefersReducedMotion: { readonly matches: boolean };
resolve<T>(value: ResponsiveProp<T> | undefined): T | undefined;
isAtLeast(breakpoint: Breakpoint): boolean;
matches(breakpoint: Breakpoint): boolean;
apply(change: StructuralChange): void;
remove(target: HTMLElement, names: readonly string[]): void;
/** Write a single style property (e.g. a CSS custom property) on an element —
* the per-element counterpart of `apply` (attrs) and `writeStyle` (global). */
writeProperty(target: HTMLElement, property: string, value: string): void;
/** Remove a style property written by `writeProperty`. */
removeProperty(target: HTMLElement, property: string): void;
writeStyle(
id: string,
css: string,
@ -118,11 +125,32 @@ export interface ActiveDom {
writeNode(id: string, options?: ActiveDomWriteNodeOptions): HTMLElement | undefined;
writeText(target: HTMLElement, text: string): void;
removeNode(id: string, options?: ActiveDomRemoveNodeOptions): void;
// `listen` is overloaded so the handler's event is typed per target
// (Window / Document / HTMLElement); the last overload is the generic fallback.
listen<K extends keyof WindowEventMap>(
target: Window,
event: K | readonly K[],
handler: (e: WindowEventMap[K]) => void,
options?: boolean | AddEventListenerOptions
): ActiveDomListenerCleanup;
listen<K extends keyof DocumentEventMap>(
target: Document,
event: K | readonly K[],
handler: (e: DocumentEventMap[K]) => void,
options?: boolean | AddEventListenerOptions
): ActiveDomListenerCleanup;
listen<K extends keyof HTMLElementEventMap>(
target: HTMLElement,
event: K | readonly K[],
handler: (e: HTMLElementEventMap[K]) => void,
options?: boolean | AddEventListenerOptions
): ActiveDomListenerCleanup;
listen(
target: EventTarget,
event: string | readonly string[],
handler: EventListener
): () => void;
handler: EventListener,
options?: boolean | AddEventListenerOptions
): ActiveDomListenerCleanup;
observeResize(
target: Element,
callback: ResizeObserverCallback,
@ -272,6 +300,30 @@ const popupDom = createActiveDom({ targetWindow: popup });
Ignorado cuando `shareViewport: true` (el singleton siempre rastrea el
top-level `window`).
## Helpers rune standalone
Además del runtime `createActiveDom`, `$adom` exporta primitivas reactivas
autónomas (ports 0-dep de `runed`, corregidas para respetar el `targetWindow` vía
`getWindow(node)`). Se importan directamente del barrel `$adom`:
| Helper | Qué expone |
| --------------------------------- | ----------------------------------------------------------------- |
| `ActiveElement` / `activeElement` | valor reactivo = `document.activeElement` |
| `IsDocumentVisible` | ¿la pestaña está visible? (`visibilitychange`) |
| `IsFocusWithin` | ¿el foco está dentro de un elemento? |
| `IsInViewport` | ¿un nodo intersecta el viewport? (`IntersectionObserver`) |
| `IsIdle` | inactividad del usuario tras N ms sin interacción |
| `PressedKeys` | conjunto reactivo de teclas pulsadas ahora mismo |
| `ElementRect` | `DOMRect` reactivo de un elemento |
| `ElementSize` | tamaño reactivo de un elemento (`ResizeObserver`) |
| `ScrollState` | posición, dirección y bordes de scroll |
| `TextareaAutosize` | crece un `<textarea>` para ajustarse a su contenido |
| `AnimationFrames` | bucle `requestAnimationFrame` gestionado (fps / delta) |
| `onClickOutside` | callback al hacer click / foco fuera de un elemento |
| `BodyScrollLock` | bloquea el scroll del `body` |
| `DOMContext` | resuelve `window` / `document` del contexto (iframe / popup safe) |
| `RovingFocusGroup` | grupo de foco con roving tabindex |
## Qué no es
`ActiveDom` no es:
@ -295,7 +347,7 @@ solo utilidades puras.
## Pagina De Prueba
La pagina manual esta en `/test/adom` y cubre:
La pagina manual esta en `/active/docs/adom` y cubre:
- viewport y breakpoint actual
- `resolve()` responsive
@ -374,9 +426,9 @@ una lectura nunca fuerza un reflow EN MEDIO de un turno de escritura — el orig
idempotente que `raf`; `dispose()` cancela los frames pendientes.
**Por qué.** El framework gobierna las ESCRITURAS (`apply`) y el TIEMPO
(`uix.timers`) pero no tenía superficie para el *timing* de las LECTURAS de layout,
que solo son seguras post-layout. `measure` es el hogar sancionado: posee *cuándo*
corre la lectura (post-turno, coalescida), no *qué* elemento. **Solo lecturas** — las
(`uix.timers`) pero no tenía superficie para el _timing_ de las LECTURAS de layout,
que solo son seguras post-layout. `measure` es el hogar sancionado: posee _cuándo_
corre la lectura (post-turno, coalescida), no _qué_ elemento. **Solo lecturas** — las
escrituras ya secuencian por `apply` + el runtime; un segundo eje `mutate`/dos-fases
duplicaría lo que `apply` + Svelte ya hacen (sobre-ingeniería descartada).

@ -90,10 +90,10 @@ intencional, configúralo de forma explícita:
```ts
const App = createActiveApp({
cache: {
defaultMemoryAdapter: {
suppressProductionWarning: true
}
services: {
cache: defineActiveCache({
defaultMemoryAdapter: { suppressProductionWarning: true }
})
}
});
```
@ -129,10 +129,7 @@ applyStandardOrca(App);
Cherry-pick if the standard set is too aggressive:
```ts
import {
applyCacheClearOnIdentityChange,
applyCacheClearOnRevoke
} from '$active-app';
import { applyCacheClearOnIdentityChange, applyCacheClearOnRevoke } from '$active-app';
applyCacheClearOnIdentityChange(App);
// SESSION_EVENT_REVOKED is not handled — caches survive sign-out.
@ -285,6 +282,21 @@ entry.loading;
`entry.set(value)` reutiliza por defecto la `policy`, `tags`, `schemaVersion`,
`persist` y `scope` definidos en la entry, y permite sobrescribirlos por llamada.
### Superficie del contrato `ActiveEngine`
Además de reflejar el engine (`get` / `set` / `invalidate` / `mutate` / `explain` /
`clear` / `entry`), `ActiveCache` implementa el contrato reactivo compartido:
- `Cache.loading` — `true` mientras haya cargas en vuelo (getter reactivo).
- `Cache.lastError` — último error normalizado, o `null`.
- `Cache.disposed` — `true` tras `dispose()`.
- `Cache.clearError()` — limpia `lastError`.
- `Cache.snapshot()` — instantánea `{ lastEvent, eventCount, loading, lastError, disposed }`.
- `Cache.onChange(listener)` — invoca `listener(snapshot())` en cada cambio de estado
(carga, error, evento de cache, dispose); devuelve el desuscriptor. Es la base para
envolver `ActiveCache` en una vista reactiva.
- `Cache.dispose()` — idempotente; libera las entries propias y sus timers.
## Mutate
La mutación v1 es conservadora:
@ -391,10 +403,8 @@ Defaults y recomendaciones:
## Página De Prueba
La demo interactiva está en:
La documentacion actual de runtime vive en `/active`.
`cache` se ejercita dentro de la demo integrada del ecosistema, en
`/active/get-started/ecosystem` (la documentación de runtime vive en `/active`).
Muestra `App.cache`, entry reactiva, invalidación por tag, scope actor/tenant,
eventos y `explain()`.

@ -0,0 +1,115 @@
# ethereal (`$ethereal`)
The in-house **positioning engine** — collision-aware placement geometry for
floating overlays (popover, tooltip, menu, select…). Pure, deterministic,
DOM-read-only: `computePosition` reads the layout **once** through `$adom` and
then runs a middleware chain **purely** over that snapshot. It is an art
(parallel to [`$motion`](../motion/README.md) and [`$color`](../color/README.md))
so **both** UIX layers consume it with no cross-layer dependency:
- **soma** — `soma/layers/floating` (`use-floating`, `FloatingContent`) calls
`computePosition` + `autoUpdate` for the JS positioning path.
- **eidos** — `render-css` emits the native **CSS Anchor Positioning** rules for
the overlays `selectPositioningStrategy` clears for the native path.
Nothing is vendored: `@floating-ui` was read as the **spec** (MIT); these are our
own idioms and types, kept **pixel-identical** to it by the parity suites.
> **Status — consumed.** `@floating-ui` is a **devDep-only** parity baseline; the
> runtime is this engine (`layers/floating` + `$ethereal`). Correctness &
> performance record: [`PERF.md`](./PERF.md) — **487 parity cases** (350 synthetic,
> 137 real-DOM), cross-browser on Chromium + WebKit + Firefox (`engine.test.ts` +
> `engine-dom.svelte.test.ts`). Interactive demo:
> [`web/routes/demos/ethereal`](../../../web/routes/demos/ethereal/+page.svelte).
## Why an art
The positioning math has two consumers — soma's JS path and eidos's CSS-anchor
generator — exactly like `$motion` (soma `Presence` + eidos CSS) and `$color`
(eidos build + runtime). Living in `arts/` (below the UIX layers) lets both
consume it with no soma→eidos coupling. Per the arts convention it imports **no
other art**, owns **no reactive `$state`**, and touches the DOM **only** through
the injected `$adom` runtime (iframe / popup safe — every read goes through
`dom.getWindow(node)` / `dom.measure`, never a bare `window`).
## The read-phase model
The whole point of the `sec-dom` design: `computePosition` positions inside a
single coalesced `dom.measure` rAF and never forces a synchronous reflow.
```
dom.measure(floating) ──▶ read EVERYTHING once
reference rect · floating dims · offsetParent · clipping ancestor-walk
· arrow dims · offsetParent→viewport delta · scale · direction
│
▼
runMiddleware(...) ──▶ pure chain over the snapshot (NO DOM access)
offset → shift → flip → arrow → size → hide … (reset restarts, cap 50)
│
▼
{ x, y, placement, strategy, middlewareData }
```
`runMiddleware` is **synchronous**; `@floating-ui`'s `computePosition` is async
(a Promise per read + per middleware). The honest trade-off is **+1 frame of
latency** (the deliberate rAF), documented in [`PERF.md`](./PERF.md).
## Dual engine — JS + native CSS Anchor Positioning
`selectPositioningStrategy(features)` decides, per overlay, between the JS engine
and the browser's native anchor positioning (`anchor-name` / `position-anchor` /
`position-area` / `@position-try`). Native wins **only** when the browser
supports it (`supportsCssAnchor`) **and** none of the JS-forcing features are
present — a continuous `shift`, an `arrow`, a virtual anchor, or an explicit
boundary. This gate is the behavioural twin of eidos's `@supports (anchor-name)
and (position-area)` block; the two must agree. See
[`strategy.ts`](./strategy.ts) + [`PERF.md`](./PERF.md) §"Dual engine".
## API
| Export | Purpose |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `computePosition(reference, floating, config)` | orchestrator → `Promise<ComputePositionReturn>`; one `dom.measure` read, then the pure chain |
| `runMiddleware(placement, strategy, middleware, snap)` | the **pure** middleware loop over a `ReadSnapshot` — no DOM; exported so the math is parity-testable |
| `autoUpdate(reference, floating, update, options)` | re-runs `update` on ancestor scroll/resize, element resize, and (opt-in) a per-frame loop → cleanup fn |
| `selectPositioningStrategy(features)` | `'native' \| 'js'` — the native-vs-JS discriminator |
| `supportsCssAnchor(window)` | runtime capability probe for CSS Anchor Positioning |
**Middleware** (chain order is caller-controlled): `offset`, `shift`, `flip`,
`arrow`, `size`, `hide`, `limitShift` (a limiter _on_ `shift`, the `sticky`
behaviour). Each is a `Middleware` — `{ name, options?, fn(state) }` — pure over
the `MiddlewareState`.
**Placement vocabulary** ([`placement.ts`](./placement.ts)): `SIDE_OPTIONS`
(`top`/`right`/`bottom`/`left`), `ALIGN_OPTIONS` (`start`/`center`/`end`),
`OPPOSITE_SIDE`; `Placement = Side | ${Side}-start|end` (center carries **no**
suffix — it is the bare side); `Strategy = 'absolute' | 'fixed'`.
**Types** ([`types.ts`](./types.ts)): `Measurable` (a real element **or** a
virtual anchor exposing only `getBoundingClientRect` — the context-menu /
`customAnchor` pattern), `Middleware`, `MiddlewareData`, `MiddlewareState`,
`ComputePositionConfig` / `ComputePositionReturn`, `Coords` / `Dimensions` /
`Rect` / `SideObject` / `Padding`, `ClippingContext`, `Boundary` (`Element |
null`). Geometry helpers (`getSide`, `getAlignment`, `getOppositePlacement`,
`getExpandedPlacements`, `computeCoordsFromPlacement`, …) back the middleware
math and are exported for reuse.
`config` = `{ placement = 'bottom', strategy = 'absolute', middleware = [], dom }`
— `dom` is the required `ActiveDom`. `autoUpdate` options default
`ancestorScroll` / `ancestorResize` / `elementResize` to `true` and
`animationFrame` to `false`; the `observeMove` IntersectionObserver layout-shift
path is intentionally **not** implemented (scope: scroll + resize + rAF).
## Depends on
- **`$adom`** — every DOM read/observe (`getWindow`, `measure`, `listen`,
`observeResize`, `requestFrame`), so the engine is iframe / popup / test safe.
- **no other art**; `@floating-ui` is a **devDep-only** parity baseline, never a
runtime import.
## Consumers
- `src/uix/soma/layers/floating/` — `use-floating.svelte.ts` (calls
`computePosition`), `floating.svelte.ts` (the middleware wiring + `autoUpdate`).
- `src/uix/eidos/lib/render-css.ts` — the native CSS-anchor block gated on the
same capability set `selectPositioningStrategy` uses.

@ -48,7 +48,7 @@ orca entre engine y wrapper reactivo). El motor expone:
- `OrcaActionContext`: `eventId`, `traceId`, `depth`, `signal`,
`emit()`, `tokens`, `tokenPayloads`, `logger`
- `OrcaResult`: `success / skipped / error / interrupted / timeout /
fatal`
fatal`
- `OrcaRunResult` con `tokens`, `tokenPayloads`, `actions[]` y
`compensations[]`
- Reentry guards: `maxDepth`, `maxEventsPerTrace`,
@ -57,7 +57,7 @@ orca entre engine y wrapper reactivo). El motor expone:
- `actionTimeoutMs` por acción — race contra timer + abort
per-acción, aislado de hermanas
- `OrcaFatal` con precedencia `FATAL > TIMEOUT > ABORTED >
INTERRUPTED > PARTIAL > SUCCESS`
INTERRUPTED > PARTIAL > SUCCESS`
- Gates `unless` → `abortOn` → `fanIn` → `after` (orden de
evaluación); `fanIn: { tokens, min }` para quórum k-of-n
- `parallel: true` por acción — waves concurrentes con `Promise.all`
@ -92,15 +92,15 @@ Las decisiones arquitectónicas que sostienen el diseño:
## Naming
| Concepto | Nombre |
| -------- | ------ |
| Artefacto | `orca` |
| Raiz imperativa | `createEngineOrca()` / `EngineOrca` |
| Raiz reactiva | `createActiveOrca()` / `ActiveOrca` |
| Accion | `OrcaAction` |
| Resultado | `OrcaResult` |
| Ejecucion de evento | `OrcaRun` |
| Token semantico | `OrcaToken` |
| Concepto | Nombre |
| ------------------- | ----------------------------------- |
| Artefacto | `orca` |
| Raiz imperativa | `createEngineOrca()` / `EngineOrca` |
| Raiz reactiva | `createActiveOrca()` / `ActiveOrca` |
| Accion | `OrcaAction` |
| Resultado | `OrcaResult` |
| Ejecucion de evento | `OrcaRunResult` |
| Token semantico | `OrcaToken` |
El alias publico previsto seria:
@ -170,15 +170,15 @@ coordinar varios artefactos cuando ocurre algo, registra acciones en `orca`.
`orca` toma ideas de varios ecosistemas, pero no copia ninguno:
| Referente | Idea aprovechable | Diferencia de `orca` |
| --------- | ----------------- | -------------------- |
| Redux Toolkit listener middleware | listeners, async workflows, cancelacion, `take`, `condition`, `fork` | `orca` no esta ligado a Redux ni reducers |
| Redux-Saga | concurrencia, `fork`, `join`, `race`, `takeLatest` | `orca` evita generators y usa Result explicito |
| NgRx Effects | aislar side-effects de componentes | `orca` no depende de RxJS ni Angular |
| redux-observable | actions in, actions out | `orca` no fuerza stream Rx |
| Effector | eventos, efectos, scopes, `allSettled` | `orca` define stages, tokens y policies |
| Effect-TS | Result, timeout, retry, schedules | `orca` debe ser mucho mas pequeno y adapter-driven |
| Temporal / Durable Functions | workflows con pasos, retries y timers | `orca` v0 no es durable ni server-authoritative |
| Referente | Idea aprovechable | Diferencia de `orca` |
| --------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------- |
| Redux Toolkit listener middleware | listeners, async workflows, cancelacion, `take`, `condition`, `fork` | `orca` no esta ligado a Redux ni reducers |
| Redux-Saga | concurrencia, `fork`, `join`, `race`, `takeLatest` | `orca` evita generators y usa Result explicito |
| NgRx Effects | aislar side-effects de componentes | `orca` no depende de RxJS ni Angular |
| redux-observable | actions in, actions out | `orca` no fuerza stream Rx |
| Effector | eventos, efectos, scopes, `allSettled` | `orca` define stages, tokens y policies |
| Effect-TS | Result, timeout, retry, schedules | `orca` debe ser mucho mas pequeno y adapter-driven |
| Temporal / Durable Functions | workflows con pasos, retries y timers | `orca` v0 no es durable ni server-authoritative |
Ideas concretas que conviene robar:
@ -233,7 +233,7 @@ Orca.onEvent(APP_EVENT_USER_IDENTITY_CHANGED, {
La accion es la que decide llamar a `App.cache`, `App.perm`,
`App.connections` o cualquier otro servicio.
## Setup Tipado *(roadmap v2)*
## Setup Tipado _(roadmap v2)_
> No implementado. Ver "Roadmap v2" más abajo.
@ -296,25 +296,24 @@ Una accion es una unidad de trabajo asociada a un evento.
interface OrcaAction<TPayload = unknown, TValue = unknown> {
readonly id: OrcaActionId;
readonly stage: OrcaStage;
readonly priority?: number;
readonly execution?: OrcaExecutionMode;
readonly after?: readonly OrcaToken[];
readonly unless?: readonly OrcaToken[];
readonly abortOn?: readonly OrcaToken[];
readonly provides?: readonly OrcaToken[];
readonly transaction?: OrcaTransactionMode;
readonly actionTimeoutMs?: number;
readonly tokenTimeoutMs?: number;
readonly onError?: OrcaErrorPolicy;
readonly onFatal?: OrcaFatalPolicy;
readonly onTimeout?: OrcaTimeoutPolicy;
action(
payload: TPayload,
context: OrcaActionContext
): OrcaMaybePromise<OrcaResult<TValue>>;
readonly after?: readonly OrcaToken[]; // espera estos tokens antes de correr
readonly unless?: readonly OrcaToken[]; // idempotencia: se salta si alguno está presente
readonly abortOn?: readonly OrcaToken[]; // se BLOQUEA si alguno está presente
readonly fanIn?: OrcaFanInSpec; // quórum: corre con `min` de `tokens` presentes
readonly provides?: readonly OrcaToken[]; // tokens que declara emitir (guía a validate())
readonly actionTimeoutMs?: number; // timeout por acción → OrcaTimeout + abort del signal
readonly transaction?: string; // tag de grupo atómico (compensación LIFO al fallar)
readonly parallel?: boolean; // wave concurrente con acciones consecutivas del mismo stage
readonly onError?: OrcaErrorPolicy; // reacción a OrcaError (CONTINUE por defecto / ABORT_RUN)
readonly compensate?: OrcaActionFn<TPayload, void>; // rollback si el run aborta tras su éxito
readonly action: OrcaActionFn<TPayload, TValue>;
}
```
No hay `priority`, `tokenTimeoutMs`, `onFatal` ni `onTimeout`: el orden intra-stage
es el de registro, el único timeout es `actionTimeoutMs`, y un fallo irrecuperable se
señala **devolviendo** `OrcaFatal` desde la acción (no con una política aparte).
Una accion debe ser:
- nombrada con constante
@ -327,8 +326,9 @@ Una accion debe ser:
## Stages
Los stages dan estructura al pipeline. La prioridad solo ordena dentro del
stage; no debe sustituir a los stages.
Los stages dan estructura al pipeline. Dentro de un stage el orden es el de
**registro** (no hay prioridad numérica); para ordenar por dependencias usa
tokens (`after` / `provides`), y para concurrencia intra-stage, `parallel`.
```txt
guard valida precondiciones
@ -353,32 +353,25 @@ export const ORCA_STAGE_FINALLY = 'finally' as const;
`finally` debe poder ejecutarse aunque el pipeline haya abortado, salvo que el
orquestador haya sido disposed.
## Execution Modes
Una accion puede declarar como se integra en el stage:
```ts
export const ORCA_EXEC_SYNC = 'sync' as const;
export const ORCA_EXEC_ASYNC = 'async' as const;
export const ORCA_EXEC_PARALLEL = 'parallel' as const;
```
## Concurrencia dentro del stage (`parallel`)
Semantica propuesta:
Dentro de un stage el orden de ejecución es el **orden de registro**. Una acción
puede declarar `parallel: true`: las acciones **consecutivas** con `parallel: true`
y el mismo stage forman una **wave** que corre en paralelo vía `Promise.all`. Una
acción secuencial (el default, `parallel: false`) rompe la wave y corre hasta
completarse antes de que arranque la siguiente.
| Modo | Semantica |
| ---- | --------- |
| `sync` | Ejecuta y resuelve antes de continuar. |
| `async` | Puede esperar promesas, pero bloquea su grupo/stage. |
| `parallel` | Puede ejecutarse junto a otras acciones listas del mismo stage. |
Los tokens emitidos dentro de una wave se fusionan en el set del run **después** de
que la wave se asiente; las puertas (`after` / `unless` / `abortOn` / `fanIn`) se
evalúan al inicio de la wave contra los tokens acumulados antes, así que los
hermanos de una misma wave nunca ven los tokens de los otros. Si una acción paralela
devuelve `OrcaFatal` o falla con `onError: 'abort-run'`, el run aborta solo tras
asentarse la wave — los hermanos no se cancelan a media ejecución.
El motor debe decidir grupos de ejecucion usando:
- stage
- priority
- tokens disponibles
- dependencias pendientes
- modo de ejecucion
- limites de concurrencia futuros
No existen modos `sync` / `async` ni constantes `ORCA_EXEC_*`: la única palanca de
concurrencia **intra-stage** es `parallel`. La concurrencia **entre runs** del mismo
evento se configura aparte con `configureEvent(event, { queuePolicy })` (siguiente
sección).
## Concurrencia De Runs
@ -391,20 +384,20 @@ decisión es parte del contrato del evento y se configura con
Constantes vivas (`consts.ts`):
```ts
ORCA_QUEUE_FIFO // 'fifo'
ORCA_QUEUE_REPLACE_QUEUED // 'replace-queued'
ORCA_QUEUE_DROP_LATEST // 'drop-latest'
ORCA_QUEUE_PARALLEL // 'parallel'
ORCA_QUEUE_FIFO; // 'fifo'
ORCA_QUEUE_REPLACE_QUEUED; // 'replace-queued'
ORCA_QUEUE_DROP_LATEST; // 'drop-latest'
ORCA_QUEUE_PARALLEL; // 'parallel'
```
Semántica:
| Modo | Uso |
| ---- | --- |
| `fifo` (default) | Cada evento encola un run; los runs no-paralelos se serializan globalmente. Garantía simple. |
| Modo | Uso |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fifo` (default) | Cada evento encola un run; los runs no-paralelos se serializan globalmente. Garantía simple. |
| `replace-queued` | Si llega un evento mientras hay otro encolado, el queued se descarta y se sustituye por el nuevo. **No aborta in-flight.** Última intención pendiente gana. |
| `drop-latest` | Si hay un run del mismo evento in-flight o encolado, el incoming se descarta. Ignora retriggers durante trabajo. |
| `parallel` | Lanza runs concurrentes; eventos paralelos no toman el lock global. Footgun: solapa side-effects. |
| `drop-latest` | Si hay un run del mismo evento in-flight o encolado, el incoming se descarta. Ignora retriggers durante trabajo. |
| `parallel` | Lanza runs concurrentes; eventos paralelos no toman el lock global. Footgun: solapa side-effects. |
`replace-queued` deja el run activo terminar (FINALLY incluido) — la
variante "fuerte" que aborta in-flight (`replace-current`, takeLatest)
@ -544,21 +537,21 @@ interno antes de llegar a la cola de runs:
```ts
interface OrcaEnvelope<TPayload> {
readonly event: string;
readonly payload: TPayload;
readonly meta: OrcaEventMeta;
readonly event: string;
readonly payload: TPayload;
readonly meta: OrcaEventMeta;
}
interface OrcaEventMeta {
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly parentRunId?: OrcaRunId;
readonly emittedByAction?: OrcaActionId;
readonly depth: number;
readonly stack: readonly string[];
readonly publishedAt: number;
readonly dedupeKey?: string;
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly parentRunId?: OrcaRunId;
readonly emittedByAction?: OrcaActionId;
readonly depth: number;
readonly stack: readonly string[];
readonly publishedAt: number;
readonly dedupeKey?: string;
}
```
@ -570,21 +563,17 @@ para esa ejecución:
```ts
interface OrcaActionContext {
readonly runId: OrcaRunId;
readonly event: string;
readonly stage: OrcaStage;
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly depth: number;
readonly tokens: ReadonlySet<OrcaToken>;
readonly signal: AbortSignal;
readonly logger: Logger;
emit<TPayload>(
event: string,
payload: TPayload,
options?: OrcaEmitOptions
): OrcaEventId | null;
readonly runId: OrcaRunId;
readonly event: string;
readonly stage: OrcaStage;
readonly eventId: OrcaEventId;
readonly traceId: OrcaTraceId;
readonly parentEventId?: OrcaEventId;
readonly depth: number;
readonly tokens: ReadonlySet<OrcaToken>;
readonly signal: AbortSignal;
readonly logger: Logger;
emit<TPayload>(event: string, payload: TPayload, options?: OrcaEmitOptions): OrcaEventId | null;
}
```
@ -628,13 +617,13 @@ main(action A)
```ts
interface OrcaReentryOptions {
readonly maxDepth?: number; // default 16
readonly maxEventsPerTrace?: number; // default 128
readonly repeatedEventLimit?: number; // default 2
readonly repeatedEventPolicy?:
| typeof ORCA_REENTRY_SKIP // default
| typeof ORCA_REENTRY_ABORT_TRACE
| typeof ORCA_REENTRY_ERROR;
readonly maxDepth?: number; // default 16
readonly maxEventsPerTrace?: number; // default 128
readonly repeatedEventLimit?: number; // default 2
readonly repeatedEventPolicy?:
| typeof ORCA_REENTRY_SKIP // default
| typeof ORCA_REENTRY_ABORT_TRACE
| typeof ORCA_REENTRY_ERROR;
}
```
@ -759,13 +748,15 @@ export const ORCA_ON_ERROR_CONTINUE = 'continue' as const;
export const ORCA_ON_ERROR_ABORT_ACTION = 'abort-action' as const;
export const ORCA_ON_ERROR_ABORT_STAGE = 'abort-stage' as const;
export const ORCA_ON_ERROR_ABORT_RUN = 'abort-run' as const;
export const ORCA_ON_FATAL_ABORT_RUN = 'abort-run' as const;
export const ORCA_ON_FATAL_DISPOSE = 'dispose' as const;
```
`fatal` debe tener semantica fuerte: por defecto aborta el run. Seguir tras un
fatal solo debe permitirse con politica explicita y muy justificada.
`ABORT_ACTION` y `ABORT_STAGE` se aceptan por compat pero hoy se comportan como
`CONTINUE`; solo `CONTINUE` (default) y `ABORT_RUN` son distintos.
Un fallo **irrecuperable** no tiene constante de política: la acción **devuelve**
`OrcaFatal` (`orcaFatal(error)`) y el motor **siempre** aborta el run — precede a
cualquier otro estado (timeout, aborted, partial) e ignora el `onError` de la
acción. No existen `ORCA_ON_FATAL_*`.
### Rollback Y Compensacion
@ -780,50 +771,41 @@ La reparacion de estado se hace con:
- acciones idempotentes
- acciones de compensacion explicitas
Contrato futuro para compensaciones:
Compensaciones (implementadas):
```ts
interface OrcaAction<TPayload = unknown, TValue = unknown> {
action(
payload: TPayload,
context: OrcaActionContext
): OrcaMaybePromise<OrcaResult<TValue>>;
compensate?(
payload: TPayload,
context: OrcaCompensationContext
): OrcaMaybePromise<OrcaResult>;
readonly action: OrcaActionFn<TPayload, TValue>;
readonly compensate?: OrcaActionFn<TPayload, void>;
}
type OrcaActionFn<TPayload, TValue> = (
payload: TPayload,
context: OrcaActionContext
) => OrcaResult<TValue> | Promise<OrcaResult<TValue>>;
```
El compensador recibe el mismo `OrcaActionContext` que la acción (su `ctx.emit()`
devuelve `null` — el rollback no es lugar para fan-out).
La compensacion no es rollback magico. Es una accion inversa o saneadora
declarada por la aplicacion. `orca` puede invocarla en orden inverso cuando un
run aborta, pero solo para acciones que la hayan declarado.
## Politicas De Timeout
## Timeouts
Los timeouts se declaran en `orca`, pero los ejecuta `timer`.
El único timeout implementado es **`actionTimeoutMs`** por acción. El motor corre
la promesa de la acción contra un timer del `TimerScheduler` inyectado; si el timer
vence primero, produce `OrcaTimeout` (`{ ok: false, status: ORCA_RESULT_TIMEOUT,
timeoutMs }`) y aborta el `signal` de esa acción. Las acciones hermanas no se ven
afectadas — cada una tiene su propio controller. `actionTimeoutMs <= 0` (u omitido)
corre sin mediación.
Niveles previstos:
Un timeout es un **resultado** (`OrcaTimeout`), no una política: no hay campo
`onTimeout` ni constantes `ORCA_ON_TIMEOUT_*`. Los timeouts de nivel superior
(`runTimeoutMs` / `stageTimeoutMs` / `idleTimeoutMs`) **no están implementados** —
son roadmap.
| Timeout | Descripcion |
| ------- | ----------- |
| `runTimeoutMs` | Tiempo maximo para todo el pipeline del evento. |
| `stageTimeoutMs` | Tiempo maximo para un stage. |
| `actionTimeoutMs` | Tiempo maximo de una accion. |
| `tokenTimeoutMs` | Tiempo maximo esperando tokens. |
| `idleTimeoutMs` | Tiempo maximo sin progreso del pipeline. |
Politicas:
```ts
export const ORCA_ON_TIMEOUT_CONTINUE = 'continue' as const;
export const ORCA_ON_TIMEOUT_ABORT_ACTION = 'abort-action' as const;
export const ORCA_ON_TIMEOUT_ABORT_STAGE = 'abort-stage' as const;
export const ORCA_ON_TIMEOUT_ABORT_RUN = 'abort-run' as const;
export const ORCA_ON_TIMEOUT_FATAL = 'fatal' as const;
```
`orca` nunca debe usar `Date.now()` ni `setTimeout()` directamente. Debe usar
`orca` nunca usa `Date.now()` ni `setTimeout()` directamente: siempre el
`TimerScheduler` de `timer`.
## Timers
@ -839,12 +821,10 @@ const Orca = createEngineOrca({
});
```
Las keys de timer deben generarse con helpers constantes, no con strings
dispersos:
```ts
orcaTimerKey(runId, stage, actionId, ORCA_TIMER_ACTION_TIMEOUT);
```
Internamente el motor mintea un timer por acción con `actionTimeoutMs` (atado a
`runId` / `stage` / `actionId`) y lo cancela al asentarse la acción, al terminar el
run o en `dispose()`. No hay un helper público de claves de timer: la gestión es
interna al motor.
Si `orca` crea timers sobre un scheduler inyectado, no es propietario del
scheduler. `Orca.dispose()` cancela los timers registrados por `orca`, pero no
@ -852,36 +832,33 @@ destruye `App.timers`.
## Transacciones
`orca` no implementa una transaccion concreta. Acepta un puerto:
```ts
interface OrcaTransactionPort {
run<T>(work: () => Promise<T>): Promise<T>;
}
```
Modos:
Una transacción es un **grupo atómico** de acciones que comparten el mismo tag
`transaction: string` sobre el mismo evento (pueden abarcar varios stages). Si
cualquier miembro termina en `ERROR` o `FATAL`, el motor **compensa** de inmediato
a los miembros que ya habían tenido éxito — en orden **LIFO** de finalización — y
aborta el run. El `onError` de un miembro se ignora dentro de una transacción: la
semántica transaccional siempre aborta.
```ts
export const ORCA_TX_NONE = 'none' as const;
export const ORCA_TX_OPTIONAL = 'optional' as const;
export const ORCA_TX_REQUIRED = 'required' as const;
export const ORCA_TX_REQUIRES_NEW = 'requires-new' as const;
Orca.onEvent('checkout.submit', {
id: ORCA_ACTION_RESERVE_STOCK,
stage: ORCA_STAGE_MAIN,
transaction: 'checkout',
action: reserveStock,
compensate: releaseStock // se invoca en rollback si otro miembro falla
});
```
En cliente, una transaccion puede ser un batch de UI/cache/overlays. En futuro
`active-server`, puede mapear a una transaccion real de DB/outbox.
Regla:
```txt
orca coordina la frontera transaccional; el adapter ejecuta la transaccion.
```
Etiquetar un miembro con `transaction` no obliga a declarar `compensate` — un
miembro sin compensador simplemente no tiene nada que deshacer, y `validate()` emite
un **warning** cuando ningún miembro de la transacción lo declara (no tendría efecto
de rollback). Las compensaciones de transacción se anexan a
`OrcaRunResult.compensations[]` igual que las estándar, y cada compensador corre
**como mucho una vez** por run.
En v0, `transaction` puede existir como contrato conceptual, pero no debe
prometer rollback universal. Si no hay `OrcaTransactionPort`, una
accion con `ORCA_TX_REQUIRED` debe fallar al registrarse o al validar la
configuracion, no degradar silenciosamente a `none`.
No existen `OrcaTransactionPort` ni constantes `ORCA_TX_*`: la transacción es el tag
de agrupación + `compensate`, no un puerto externo ni modos `required` /
`requires-new`.
## Run Result
@ -976,7 +953,7 @@ registre acciones.
Nombre recomendado en `App`:
```ts
App.Orchestration
App.Orchestration;
```
`orca` queda como nombre del artefacto, alias/import y prefijo de constantes
@ -1153,7 +1130,7 @@ on bus event:
find actions whose after tokens are satisfied
skip actions whose unless tokens are present
block/abort actions whose abortOn tokens are present
run ready actions according to priority and execution mode
run ready actions in registration order (parallel waves via Promise.all)
collect Result
add emitted tokens
apply error/fatal/timeout policies
@ -1188,25 +1165,25 @@ diagnostics catalogada. Las claves vivas (`ORCA_DIAGNOSTIC_EVENTS` en
`consts.ts`):
```ts
'orca.run.started'
'orca.run.completed'
'orca.run.aborted'
'orca.action.started'
'orca.action.completed'
'orca.action.failed'
'orca.action.fatal'
'orca.action.timeout'
'orca.action.skipped'
'orca.action.blocked'
'orca.action.interrupted'
'orca.compensation.started'
'orca.compensation.completed'
'orca.compensation.failed'
'orca.event.emitted'
'orca.reentry.blocked'
'orca.trace.aborted'
'orca.queue.dropped'
'orca.configuration.invalid'
'orca.run.started';
'orca.run.completed';
'orca.run.aborted';
'orca.action.started';
'orca.action.completed';
'orca.action.failed';
'orca.action.fatal';
'orca.action.timeout';
'orca.action.skipped';
'orca.action.blocked';
'orca.action.interrupted';
'orca.compensation.started';
'orca.compensation.completed';
'orca.compensation.failed';
'orca.event.emitted';
'orca.reentry.blocked';
'orca.trace.aborted';
'orca.queue.dropped';
'orca.configuration.invalid';
```
Cada evento carga meta con `runId` / `eventId` / `traceId` / `depth`

Loading…
Cancel
Save

Powered by TurnKey Linux.