master
${ noResults }
162 Commits (94ef90fdcfbbda565b89ed886aaff787dac05c01)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
94ef90fdcf |
G4 — auth DB adapter contract tests + revokeFlows where-clause fix
Closes the codex P2/P3 audit item deferred on 2026-05-05.
`createDbAuthAdapter` is the auth artifact's pluggable persistence
facade. Until now it had no contract test — only the in-memory STORE
adapter was covered, and the DB facade is the path real production
deployments take. This commit adds:
- `test/db-adapter-fake.ts` — reference `AuthDbRepositories` fake, in
memory, encoding the same where-clause conventions a real SQL repo
must satisfy: `tenantId` aliases to `actorRef.tenantId` for
credentials and linked accounts, `flowId` aliases to `id` for flows,
`null` matches absent fields (SQL `IS NULL` semantics), `actorRef`
compares deep instead of reference. The README documents the
conventions; the fake makes them executable.
- `test/db-adapter-contract.test.ts` — 19 contract tests covering all
22 `AuthStoreAdapter` methods through the adapter:
credentials (create/find/update/markVerified, tenant isolation),
flows (create/find/consume + the multi-row `revokeFlows` cascade),
linked accounts (link/find with cross-tenant invisibility),
devices (upsert insert/update split, list scoping, revoke cascade
into bound sessions),
session bindings (bind/find/revoke + the `revokeActorSessions`
cascade that preserves already-revoked rows),
refresh tokens (rotate inside a transaction, family revoke
cascade across tokens, missing-token error path).
Real bug fixed during the contract suite: `revokeFlows` was
forwarding the full input (`{ tenantId, actorRef?, kind?, nowMs }`)
to `repos.flows.findMany(whereOf(input))`. A real SQL translator
turns `nowMs` into `WHERE now_ms = ?` — a column that doesn't exist,
producing a silent no-op. The adapter now constructs the where
clause explicitly, including only the predicate fields. The contract
test that revealed it (`revokeFlows scoped by tenant + actor + kind
cascades to all matching rows`) failed pre-fix and passes post-fix.
Suite: 1695 / 1695 passing (+19 tests, +1 file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
331cc72674 |
Bloque L5 — prefs closure: frontend deep-integration + storage helper + browser auto-detect
Closes the remaining wiring gaps so an app can adopt prefs end-to-end without hand-rolling subscriptions. - `defineActiveFrontend` extended: when prefs is in the schema, subscribes to `prefs.theme/density/motion/direction` and drives Frontend's `setMode/setDensity/setReducedMotion/setDir` per-dimension. Initial values are applied before subscribing so the first paint reflects prefs without an extra commit. `prefs.theme` (light|dark) maps to Frontend.MODE — Frontend's "theme" is a deeper UI variant name; "mode" is the light/dark scheme, which is exactly the prefs effective theme. - `defineActivePrefsWithStorage(options)` — bundles `createActivePrefs` + `createPrefsStorageBridge` into a single service factory. Bridge teardown runs before engine dispose. Use this when a `PrefsIntentStorage` port is ready; manual wiring via `defineActivePrefs(...)` + `createPrefsStorageBridge(...)` still works for apps that need finer control. - `applyBrowserEnvironment(engine, overrides?)` — convenience wrapper that calls `detectBrowserEnvironment` + `watchBrowserEnvironment` and pipes both into the engine. Returns the watcher detach function. Drop it inside a SvelteKit `onMount` and the rest is automatic. Tests: 2 new integration scenarios in prefs-consumer-wiring covering the frontend per-dimension wiring (theme/density/motion/direction) and the storage-bundled factory (synchronous-storage hydrate + persist). Full suite: 1676 / 1676 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
1f8015f2fe |
Bloque L4 — active-app: defineActivePrefs + consumer wiring
Surfaces `App.prefs` and routes lang/format/frontend through the prefs engine when it's declared in the schema. Apps that don't opt into prefs keep the existing lang-driven locale chain untouched. - `defineActivePrefs(options)` — service factory with `initMode: 'immediate'` (consumer factories ask for `prefs` synchronously at construction time; deferring would make the dependency graph order-sensitive). No core dependencies — the engine is pure data. - `defineActiveLang` — when `prefs` is in the schema, subscribes to `prefs.language` and drives `lang.setLocale()` for both the initial value and changes; the subscription is detached on `dispose()`. - `defineActiveFormat` — locale-source resolution is now `options.localeSource` → `prefs.locale` → `lang.locale` → Format default. `prefs.locale` (regional formatting) wins over `prefs.language` (i18n) when both are present. - `defineActiveFrontend` — same precedence chain but uses `prefs.language` (NOT `prefs.locale`) because Frontend's `dir = auto` follows the writing system, which is a property of the language. - `$prefs` alias added to `svelte.config.js`. Sium needs no factory change — it consumes `lang` for translation strings, so the prefs-driven language flows through transitively. Tests: 1 service-factory integration + 4 consumer-wiring integration scenarios covering lang.t() flip, format.getLocale flip, frontend.getLocale flip, and the prefs-less fallback. Full suite: 1674 / 1674 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
620a471850 |
Bloque L3 — arts/prefs adapters: browser-env + server-env + storage bridge
External IO behind ports. None of these are imported by the engine — the
application's bootstrap code calls them and feeds the result into
`engine.refreshEnvironment(...)` / `engine.resetIntent(...)`.
- `adapters/browser-environment.ts` — reads `navigator.languages`,
`Intl.DateTimeFormat().resolvedOptions().timeZone`,
`matchMedia('prefers-color-scheme' | 'prefers-reduced-motion')`. Every
IO touchpoint is overridable through `overrides` so tests and SSR
shims can substitute deterministic doubles. `watchBrowserEnvironment`
wires the `matchMedia` change listeners and returns a detach.
- `adapters/server-environment.ts` — pure parsing of an SSR request
snapshot. `parseAcceptLanguage` is exported standalone so callers can
reuse the quality-ordered locale parser without going through
`detectServerEnvironment`.
- `adapters/storage-bridge.ts` — `createPrefsStorageBridge({ engine,
storage, onError? })`. Subscribes to engine commits and persists ONLY
`intent` (never environment, never effective). On hydrate, applies
`storage.load()` via `resetIntent` and skips the echo; user writes
during the hydrate window win, race-protected. Storage failures route
through `onError` and never corrupt the in-memory engine. Empty intent
calls `clear()` instead of `save({})` so backends can drop the entry.
The storage backing is an injected `PrefsIntentStorage` port — `arts/prefs`
deliberately does not import `arts/storage`; callers adapt their preferred
backend (LocalStorage, sessionStorage, in-memory, Redis) to the port.
Tests: 7 browser-env + 9 server-env + 12 storage-bridge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
80d7b9ad45 |
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 |
|
|
29dcd6b8ce |
Bloque L1 — prefs foundation: capability libs + Source<T> port
Introduce the pure preference layer and the framework-wide value port that the
runtime layers (`arts/prefs`, format, frontend) consume.
- `libs/reactive` gains `Source<T>` — a small `{ get, onChange? }` port any
artifact uses to observe an external value (locale, currency, theme, …).
- `libs/locale` extracts the lookup-style `matchLocale` helper plus aliases
`LocaleSource` to `Source<string>`. Tests added for the four-step
exact / lang+script / lang+region / lang priority.
- New per-domain libs (`currency`, `density`, `direction`, `motion`, `theme`,
`timezone`, `units`) own their own primitives, capability sources and pure
helpers (`*FromLocale(s)`, `directionFromLanguage`, `resolveTheme`,
`resolveMotion`). The currency catalogue + region table moved here from
`arts/format/currency`.
- `libs/prefs` is the pure preference layer: `PrefsCapabilities`,
`PrefsEnvironment`, `PrefsIntent`, `PrefsEffective`, intent validation
(with stable `PrefsValidationFailure` codes) and the parallel-projection
resolver. Each `effective` field is now an INDEPENDENT projection of
`environment.locales[]` against its own capability catalog — `language`
and `locale` are split (i18n catalog vs regional formatting); `currency`,
`unitSystem`, `direction` derive per-dimension instead of from a single
`effective.locale` anchor.
- `arts/format` and `arts/frontend` migrated to the new `Source<T>` shape:
`localeSource?.getLocale()` → `localeSource?.get()` and
`onLocaleChange?` → `onChange?`. `arts/format/currency/locale-currencies`
and `arts/format/units/locale-defaults` collapse to thin re-exports of
their `libs/*` counterparts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
178e76bfd3 |
Bloque J2 revert — drop frontend runtime validators
Removes the `VALID_FRONTEND_DENSITIES`/`VALID_FRONTEND_MODES`/ `VALID_FRONTEND_DIRS` const triplet introduced by Bloque J2. The matching `assertValidFrontendValue` helper and its three call sites in `active-frontend.svelte.ts` were already cleaned up earlier in the working tree. Validation moves to `libs/prefs/validate-intent.ts` once the prefs artifact lands — the design at `src/arts/prefs/README.md` puts every `setIntent(...)` write through one shared validator instead of spreading per-art runtime guards. J1 (lang `SvelteSet → Set`) and J3 (sium `CodeError` migration) stay; only the J2 portion of the combined commit is reverted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
191a834a67 |
Revert "Bloque I1+I2 — frontend snapshot() + persistFrontendPreferences preset"
This reverts commit
|
5 months ago |
|
|
40da4def72 |
Bloque K3 — perm decision audit sink
`EnginePermsOptions.onDecision?: PermDecisionAuditSink` lets hosts
forward every `check()` decision to an audit pipeline (a database, an
event bus, S3, etc.). The reference SQL schema's
`permission_decision_audit` table is one such consumer — the engine
gives the host the data, the host writes wherever its compliance
needs.
The sink is awaited so DB writes that need to commit before the
request continues block correctly. Errors thrown by the sink are
caught and emitted as the new `perm.server.audit_failed` diagnostic
(LogLevel.ERROR) — an audit failure cannot turn a granted permission
into a denial or vice versa. Hosts wire alerts on that event.
`EnginePermsOptions.clock?: { now }` controls the `settledAt`
timestamp on audit entries — defaults to `Date.now`, hosts wire
`core.timers.clock` for deterministic audit timestamps in tests.
Test covers (a) the sink receives every decision with `settledAt` from
the injected clock, (b) sink errors are swallowed and the decision
still returns the expected effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
5d6777bfa4 |
Bloque J1+J2+J3 — P3 polish: lang Set, frontend validators, sium CodeError
J1 — `arts/lang/active-lang`: `localeListeners` migrates from `SvelteSet` to plain `Set`. Listeners are notified manually via `forEach`, never rendered as derived state — `SvelteSet` would re- render every downstream component on add/remove with zero upside. J2 — `arts/frontend`: `setMode`, `setDir`, `setDensity` validate their input against the closed string-union in DEV. Typed callers still get the compile-time error first; the runtime guard catches formless inputs (HTML form selects, untyped IPC, untyped JS imports) with a domain-specific `TypeError` instead of silently writing an unrecognized value to a `data-*` attribute. PROD is a no-op. J3 — `arts/sium`: complete the `CodeError` migration. Replaces every `throw new TypeError(SIUM_ERRORS.X)` site with a typed class: - `SiumEncodeExpectsObjectError` (object/discriminated encode) - `SiumEncodeExpectsArrayError` (array encode) - `SiumEncodeNoMatchError` (discriminated encode without match) - `SiumLazyResolvingError` (lazy() accessed mid-resolution) Each carries its `ErrCode` and is exported from the public surface with matching `is*Error` type guards. Existing `SiumValidationError`, `SiumAsyncSchemaError` and `SiumDiscriminatedUnionError` were already typed and stay as-is. The `SIUM_ERRORS` legacy catalogue keeps the diagnostic message strings (`VALIDATION_FAILED`, `RESOLVE_FALLBACK`) used by the diagnostics layer — those are not thrown errors, they are catalog entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
e9eb69dbd4 |
Bloque I5 — session broadcastChannel: false explicit opt-out
`EngineSessionOptions.broadcastChannel` now accepts `string | false` in addition to `undefined`. Passing `false` skips the `BroadcastChannel` setup entirely — useful for: - privacy-strict modes that don't want any cross-tab signal - tests that want deterministic identity (no cross-tab race) without having to rely on `BroadcastChannel` being undefined - SSR / Worker environments Storage-driven sync via `localStorage`'s `storage` event still runs when the storage adapter exposes `onChange` — only the explicit channel post is skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
b1e73f4586 |
Bloque I3+I4 — format LRU cap + unified locale source
I3 — `Intl.NumberFormat` cache in currency now caps at 256 entries with LRU eviction. The cache was module-global and unbounded; long-running multi-locale / multi-currency apps (financial dashboards, i18n test matrices) accumulated formatter instances forever. Map iteration order is insertion-order so the LRU is implemented as "delete on hit, set on hit, evict the first key when full" — no extra structure. I4 — `createActiveFormatLocaleSource` now maintains its own listener set, so `setLocale()` fires every subscribing submodule through one notification. Previously the parent's `setLocale` only mutated the internal `currentLocale` and the parent then re-called `setLocale` on every submodule manually — two notifications per change. The unified source delivers exactly one. `createActiveFormat.setLocale()` no longer needs to fan out to numbers/currency/units/dates by hand. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
7fb071d22e |
Bloque I1+I2 — frontend snapshot() + persistFrontendPreferences preset
I1 — `ActiveFrontend.snapshot(): FrontendSnapshot` returns every
observable preference resolved at call time
(`{ locale, dir, theme, mode, reducedMotion, reducedSound, density }`).
Computed fresh on each call from the live state — useful for logger
context, persistence, devtools, snapshot diffing.
I2 — `applyPersistFrontendPreferences(App, options?)` preset round-
trips the frontend preferences through `App.storage`. Replays a
persisted snapshot at attach time, writes back on every preference
change, optionally filters which keys to persist. The detacher cleanly
stops persisting and disposes the storage entry — idempotent.
Cross-tab sync rides on the storage adapter's `onChange` (the
`storage` event for `localAdapter`, `BroadcastChannel` for
`broadcastAdapter`); the preset guards against re-entrant writes when
its own change triggers an external echo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
3ec92acdc3 |
Bloque H6 — http per-attempt observability
New diagnostic event `http.attempt_completed` carries `attempt`, `durationMs` (wall-clock from request start to response settle), `ok`, `status?` and `error?`. Lets dashboards compute p50/p95 latency without inferring it from the request + retrying events. `http.retrying` is now emitted AFTER the wait so it can include `actualDelayMs` — the observed time between attempts may differ from the computed `retryDelay` when an abort cuts the wait short or a `beforeRetry` hook takes noticeable time. `HttpDiagnosticMeta` gains `durationMs`, `ok`, `actualDelayMs` and `abortReason` slots. The pre-existing test that asserted exactly 1 DEBUG log per request now asserts 2 (REQUEST + ATTEMPT_COMPLETED) — that is the change in shape this block introduces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
e40fc7fa1c |
Bloque H4 — cache memory adapter routes prod warning through logger
`MemoryCacheAdapterOptions.logger?: Logger` lets the adapter's
production warning flow through the framework's logger (and from
there, every transport the host has wired) instead of always landing
on `console.warn`.
Resolution order:
1. `onProductionWarning?` callback (caller has full control)
2. `logger?.warn(CACHE_MODULE, message)` (framework path, picks up
Sentry/Datadog/Loki/whatever the host uses)
3. `console.warn` (legacy fallback, kept for callers that do not
wire either of the above)
No behavior change for existing apps — the new option is opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
0c15e0b94c |
Bloque H3 — official createFakeTimerClock() exported from \$timer
The `engine-timers.test.ts` file already had a `createFakeClock` for internal use, but every artifact under test that accepts a `clock` injection (storage, format, logger, http, session auto-refresh, …) was rolling its own. Promoting the helper to the public surface gives the ecosystem a single source of deterministic time for tests. `createFakeTimerClock(start = 0): FakeTimerClock` exposes `advanceBy(ms)`, `advanceTo(ms)` and `pendingCount()` on top of the shared `TimerClock` shape. Microtask flushes between fired entries keep awaited promises inside scheduled callbacks resolved before time moves on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
5152b23c6b |
Bloque H1+H2 — logger onInternalError + clock/idFactory injection
H1 — `LoggerOptions.onInternalError?: (info) => void` lets enterprise
hosts capture transport failures somewhere other than `console.error`
(Sentry's captureException, an audit pipeline, etc.). When defined, the
engine routes the failure-path notification to the hook instead of
`console.error`. The synthetic failure entry that flows to remaining
transports is independent of the hook — it always dispatches.
H2 — `LoggerOptions.clock?: { now }` and `LoggerOptions.idFactory?:
() => string` make timestamps and entry ids deterministic for tests and
runtimes with strict time discipline. The Logger is created BEFORE
`App.Timers`, so this is opt-in injection (not App-wired). Default
`Date.now` is late-bound through a closure so existing
`vi.spyOn(Date, 'now')` test patterns keep working.
Tests cover both injections plus the fallback path (no
`onInternalError` → `console.error` is still called).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
21ca220fef |
Bloque G5+G6 — auth anti-enumeration + redaction contracts
G5 — `requestPasswordReset` and `requestEmailVerification` now return a
public response shape that is indistinguishable for known and unknown
identifiers. A real bug surfaced while writing the test: the
known-identifier branch returned `{ ok: true, expiresAt }` while the
unknown branch returned `{ ok: true }`, which let any caller enumerate
accounts by checking the field's presence. Both flows now drop
`expiresAt` from the public response — internal flow records keep it,
the wire never exposes it. `AuthFlowPublicResult.expiresAt` stays in
the type as a forward-compat slot for authenticated trigger flows.
Tests pin: (a) shape parity between known/unknown, (b) no flow
created for unknown identifier, (c) no mail sent for unknown
identifier, (d) re-requesting verification on an already-verified
credential short-circuits silently.
G6 — Tests pin the redaction-by-design contract: every emitted
`AuthLogEntry` is searched for `password`, `identifier`, and `token`
substrings (in `data`/`meta`/`message`/etc.), and they must never
appear. Covers sign-up, sign-in, password-reset request and
failed sign-in. The framework's design enforces this through
`identifierHash`, `AuthRequestMeta` (hashes only) and `challengeHash`
— the tests guard against future code adding raw fields by accident.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
a4c3378d1a |
Bloque G3 — defineActivePerm wires App.http when declared
The perm client accepts `http?: EngineHttp` for transport, but the
service factory previously did not forward `App.http` automatically. An
app that declared both `http` and `perm` had to wire them together by
hand or pass `endpoint` + a custom `fetcher`.
`defineActivePerm` now declares `serviceDependencies: ['http']` and
forwards `App.http` to `createActivePerms({ http })` when:
- the caller did NOT pass `options.http` (explicit wins)
- AND did NOT pass `options.fetcher` (caller signaled their own
transport — leave `http` undefined to avoid double-wiring)
Apps without `http` declared keep working — `services.http` is
`undefined` and we leave the `http` slot empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
ddfc4dc6ea |
Bloque G1+G2 — perm preordered policies + per-decision provider memo
G1 — `createPermRuntime` sorts policies by priority once at construction instead of every decision. Policies are immutable for the runtime's lifetime; per-decision sorting was wasted work that scaled poorly with policy count. `combineEvaluatedPolicies` already assumed entries arrive in priority order, so the change is behavior-preserving. G2 — `DefaultPermEvaluator.evaluate(expr, context, memo?)` accepts an optional `PermEvaluatorMemo` (Map<string, unknown>) and threads it through every internal recursion. The runtime allocates one fresh memo per `evaluatePolicies` call, so concurrent matching policies asking for the same `actor.role` attribute or the same `member_of(team)` relation hit the providers exactly once per decision. Adjacent decisions get fresh memos — stale data never leaks across requests. Test covers (a) attribute provider called once across N policies in one decision, (b) relation provider called once across N policies in one decision, (c) two adjacent decisions allocate two memos. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
5c92a5cc7a |
Bloque F5 — sium follows lang's active locale lazily
`createSiumResolver` now reads `lang.getLocale()` on every `resolve()` when the wired lang exposes that getter (i.e. `ActiveLang`). Pure `EngineLang` consumers fall back to the captured construction-time default — same behavior as before, no breakage. Closes the audit's P2: "Sium captures `defaultLocale` at construction; a later `Lang.setLocale(...)` was ignored unless the caller passed an explicit `locale` to every resolve()/resolveIssue() call". Test covers the lazy follow-through with a duck-typed `getLocale` shim over an `EngineLang`, so the test does not need to spin up the full `ActiveLang` Svelte runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
529a93b813 |
Bloque F3+F4 — format Rates clock injection + storage dynamicEntry guard
F3 — `ActiveCurrencyOptions.ratesOptions` shorthand builds the rates
provider on the caller's behalf and threads the injected `clock` into
`createRates({ now })`. `ActiveFormatOptions.clock` propagates to the
currency submodule. The active-app `format` service factory now declares
`coreDependencies: ['timers']` and wires `core.timers.clock` so rate
expiration math runs through the same time source as the rest of the
ecosystem. Tests cover (a) clock-driven expiration of cached rates and
(b) `rates` (explicit provider) winning over `ratesOptions`.
F4 — `Storage.dynamicEntry()` now throws a domain-specific
`StorageDynamicEntryOutOfScopeError` when invoked outside a Svelte
component or `$effect.root` scope, instead of leaking Svelte's internal
`effect_orphan` error. The new error code, class and type guard are
exported from the `$storage` barrel. Storage's clock injection was
already wired through `defineActiveStorage` from a previous block —
no change needed there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
15737e0f37 |
Bloque F1+F2 — connection backoff random + reauth singleflight
F1 — `ConnectionReconnectOptions.random?: () => number` lets callers inject a deterministic source for backoff jitter, threaded through `computeBackoffDelay`. Default remains `Math.random` so existing apps are unaffected. Test covers maxAttempts, the new random injection (jitter +max and -max clamped to minDelay), the disabled case and the disposed/intentional-close gate. F2 — `runAuth()` singleflight in the connection request runtime: when an auth round is in flight, every concurrent caller awaits the same promise, so only one auth frame goes on the wire. Closes the gap where `session.changed` + `session.external_changed` could land back-to-back and produce two auth frames. Tested at the request-runtime level with fake ack registry + sender (integration-level testing of this through the mock transport is timing-flaky and adds no extra coverage). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
22aab00b7d |
Bloque E — compound ecosystem cross-actor isolation test
Wires real `createActiveCache` + `createEngineSession` + a stubbed `perm`
+ stubbed `connections` + `applyStandardOrca`, then drives the canonical
A→logout→B flow to confirm cache/perm/connection reactions fire on
session lifecycle transitions.
Findings while writing the test, documented in the file header:
- `SESSION_EVENT_IDENTITY_CHANGED` only fires on identity-state
transitions (`none` ↔ `anonymous` ↔ `identified`), not on in-place
`adopt(A) → adopt(B)`. The realistic cross-actor flow is therefore
`adopt → revoke → adopt`, which the suite exercises end-to-end.
- The orca dispatches reactions through `void (async () => { ... })()`
microtask runs; flushing fixed rounds is flaky. The test polls
`Orca.running` until idle, capped to avoid hangs.
Coverage: B sees no cache/perm of A after re-login; revoke clears the
cache + closes connections; reauth fires only on identity-state
transitions; detaching the preset stops the reactions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c4b843ceca |
Bloque C5 (revert) — logger back to threshold-only filtering
Reverts the per-level enable map (`LevelsConfig` / `LevelConfig`) added in
|
5 months ago |
|
|
3567206fa3 |
Bloque C5 — logger gates by per-level enablement at the engine level
Audit P1/P2: transports already accepted `LevelsConfig`
(`{ [LogLevel.WARN]: { enabled: true } }`), but the engine itself
gated entries with a threshold (`if (lvl < state.level) return`).
Two filtering vocabularies for the same vocabulary; a 1.0 contract
should pick one.
Decision: align the engine on per-level enablement (the same
shape transports use). Threshold semantics stay as a shorthand —
`level: LogLevel.WARN` is equivalent to
`levels: levelsAtLeast(LogLevel.WARN)`. When both are set, `levels`
wins. Backward-compatible: existing apps that only pass `level`
get the exact same enabled set as before because the engine
projects the threshold into `enabledLevels` at boot.
Implementation:
- `LoggerOptions` gains `levels?: LevelsConfig`. Two helpers,
`buildEnabledLevelsFromThreshold(level)` and
`buildEnabledLevelsFromLevelsConfig(levels)`, project either form
into the runtime `Set<LogLevel>` the engine consults at the log
site.
- Engine state grows `enabledLevels: Set<LogLevel>`. The hot path
becomes `if (!state.enabledLevels.has(lvl)) return`.
- `setLevel(level)` keeps working — it rebuilds `enabledLevels`
from the new threshold.
Two regression tests pin the new behaviour: arbitrary subset via
`levelsAtLeast(LogLevel.WARN)`, and `levels` overriding `level`
when both are set.
Suite: 1515 / 1515 (+3 tests across logger and storage clock).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
6b6a96fb11 |
Bloque C4 — service factories wire `clock` from `App.Timers`
Audit P2: cache, perm and storage already accepted `clock` in
their engine options, but their `defineActive*` factories only
threaded `logger` from the core. App-composed apps therefore fell
back to `Date.now`-backed clocks for TTL math, decision-cache
expiration and envelope expiration — out of band with the rest of
the ecosystem.
- `defineActiveCache` now declares `'timers'` as a core dependency
and passes `clock: { now: () => core.timers.clock.now() }` (only
when the user didn't override it themselves).
- `defineActivePerm` does the same for the perm client's decision
cache TTL.
- `defineActiveStorage` does the same for envelope TTL. The
underlying engine gains a real `EngineStorageOptions.clock`
field (resolved to `Date.now` when omitted) and threads it
through `entry-runtime.ts`'s `encodeEnvelope` /
`decodeEnvelope` calls. New regression test pins the behaviour:
two engines on the same adapter with different clocks see TTL
through their own clock.
Format / rates: `createRates({ now })` was already injectable;
the format engine itself doesn't read `Date.now` anywhere. The
audit's note about format/rates clock injection was about user
documentation, not factory wiring.
Suite: 1512 / 1512 (+1 storage clock test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
f82e174708 |
Bloque C3 — `applySessionAutoRefresh` preset wires App.Timers
Audit P2: `withAutoRefresh` already accepted `timers`, `now` and
`random` injectors, but `defineActiveSession` could not wire them
because the auto-refresh wrapper is opt-in (the caller decides
when to start the ticker). Result: apps that built sessions
through `App` still fell back to `setInterval` + `Date.now` when
they enabled auto-refresh by hand.
New preset `$active-app/presets/session-auto-refresh.ts` closes
the loop:
- `applySessionAutoRefresh(App, opts?)` calls
`withAutoRefresh(App.session, { ...opts, timers: App.Timers,
now: () => App.Timers.clock.now() })`.
- Caller-provided `timers` / `now` / `random` still win.
- Returns the same idempotent cleanup `withAutoRefresh` returns.
Re-exports through `$active-app/presets`. Two regression tests
verify the preset routes through the App's clock and lets the
caller override `random` when jitter is enabled.
Other determinism in `arts/session` (engine clock for
`expiresAt`, broadcast channel) was already injectable; the
preset is the missing wiring piece for the App composition path.
Suite: 1512 / 1512.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
1a92d280dc |
Bloque C1+C2 — http retry/timeout determinism via `HttpTimerPort`
Audit P2: `http/retry.ts` and `http/timeout.ts` reached for
`Date.now`, `Math.random`, host `setTimeout` and `clearTimeout`
directly, breaking determinism in tests / replay and routing
around the ecosystem's "all time via timr" rule when used inside
the App composition.
New `HttpTimerPort` interface bundles `now`, `random`, `setTimeout`
and `clearTimeout`. Defaults route to host primitives via
`createDefaultHttpTimerPort()`. `EngineHttpOptions` exposes the
four functions individually so callers can replace any subset; the
engine bundles them into an internal `port` field on
`ResolvedHttpDefaults` and threads it through:
- `computeRetryDelay(policy, attempt, response, port)` — `now()`
drives `Retry-After` math, `random()` drives jitter.
- `delayWithSignal(ms, signal, port)` — schedules + cancels via the
port's `setTimeout` / `clearTimeout`.
- `attemptTimeoutSignal(ms, port)` and `totalTimeoutSignal(ms, port)`
now return `{ signal, cancel }` instead of a bare `AbortSignal`.
The engine calls `cancel()` when each attempt settles and when
the request finishes, closing the audit's "leaked timeouts in
long-volume runtimes" finding.
- `defineEngineHttp` wires `now: () => core.timers.clock.now()`
from `App.Timers`, so the App path uses a single clock; `random`
and `setTimeout` keep host defaults (deterministic injection
remains an opt-in per call).
`mergeHttpOptions` propagates the port fields too, so
`engine.with({...})` keeps test injectors intact.
Tests: timeout suite gains 4 cases (cancel suppresses fire,
injected port is honored, both attempt and total scopes); retry
suite migrates to a `PORT = { now, random }` constant. Original
behavior unchanged.
Suite: 1510 / 1510.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
a52f918be2 |
Bloque B3 — connection WebSocket test coverage
Audit P2: `websocket.test.ts` only validated the "WebSocket missing" error path. The transport's actual surface is substantial — URL/protocols factory resolution, binaryType forcing, browser open/message/close/error mapping, state projection, send/bufferedAmount, close forwarding, and listener cleanup between sockets — and all of it shipped untested. Adds a full coverage suite using a hand-rolled mock WebSocket constructor (captures URL/protocols, lets the test drive open/message/close/error transitions deterministically). 11 new cases: - URL + protocols factories invoked at open() time - `binaryType` forced to `arraybuffer` on every fresh socket - open() resolves on browser open + state flips to OPEN - string and ArrayBuffer messages forwarded to onMessage - open() rejects on close-before-open (with the close meta) - open() rejects on error-before-open - post-open errors hit onError without re-settling open() - send() forwards both string and ArrayBuffer; bufferedAmount reads through to the socket - close() forwards code+reason and transitions to CLOSED - listener cleanup verified across reconnect (no leaks from the previous socket fire on the next one) - transport.kind === 'websocket' The original "WebSocket unavailable" test stays. Suite: 1507 / 1507. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
45011ea4a7 |
Bloque B2 — auth handlers route DEVICES + DEVICE_REVOKE
Audit P1: `ActiveAuth.listDevices()` and `revokeDevice()` POSTed
to `/api/auth/devices` and `/api/auth/devices/revoke`, but the
generic handler (the same one used by the SvelteKit integration)
only routed current/csrf/password/recovery/sign-out — devices and
OAuth fell through to 404.
Closes the device gap end-to-end:
- `AuthHandlerEngine` (handler-runtime contract) now declares
`listDevices` and `revokeDevice`. The engine already implemented
them; the gap was purely in the handler surface.
- `createAuthRouteHandlers` adds two new handlers and registers
them in `handle()`:
- `GET /api/auth/devices` → `engine.listDevices({ actorRef })`
- `POST /api/auth/devices/revoke` → CSRF-verified, body
`{ deviceId, meta? }` → `engine.revokeDevice({ actorRef, ... })`
- Both derive `actorRef` via a new internal `requireAuthCurrent`
helper that calls `engine.current()` and returns 401 when the
session is anonymous, mirroring how the rest of the auth API
treats unauthenticated requests.
OAuth / MFA / WebAuthn endpoints (which the audit also flagged in
the same finding) stay deferred — those are bigger surface
additions that need server-side flow work, not just routing. The
client cooperates: those methods are not yet declared on
`ActiveAuth`. Devices / device revoke are the only pair the client
already exposed and the handler ignored.
Test: `src/svrs/auth/test/handlers-devices.test.ts` exercises
routing + auth gating with stub engines (4 cases). Engine-level
device semantics (revocation invalidates bound sessions etc.)
remain covered by `engine-password.test.ts`.
Suite: 1496 / 1496.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
2a6706370e |
Bloque B1 — perm SQL compiler resolves nested paths via getPath
Audit P1: the SQL compiler used a literal property lookup (`(input.actor as Record<string, unknown>)[expr.path]`) for actor and context references, while the in-memory runtime evaluator goes through `getPath()` which respects the `.` separator. A policy condition like `actor.risk.mfa === true` therefore resolved correctly in memory but produced `undefined` in the SQL parameter — silently misaligning DB-side filters with allow/deny decisions. Fix: route both `PERM_ROOT_ACTOR` and `PERM_ROOT_CONTEXT` through `getPath()` in `src/libs/perm/compilers/sql.ts` so both code paths agree on segmentation. Resource references stay on `columnName` (they map to a real DB column, not to a JS object). Adds `src/libs/perm/test/sql-nested-paths.test.ts` with three regression cases: 1. Nested actor path (`actor.risk.mfa`) emits the resolved value. 2. Nested context path (`context.request.region`) likewise. 3. Missing nested path emits `undefined`, matching the runtime evaluator (so the SQL/runtime alignment doesn't accidentally diverge in the "missing" case either). Caveat documented in the new comment: the fix assumes DB column names don't contain `.`. Apps that need columns with dotted identifiers must override `columnName` and the actor/context paths must avoid `.` for those references. Suite: 1492 / 1492. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
5efec94367 |
Bloque D — uniform lifecycle for declarable services
Audit P2: every service the app declares in `createActiveApp({
services })` should respect the same dispose contract: idempotent,
post-dispose mutators are inert, no late side effects on torn-down
subscriptions.
storage:
- New `STORAGE_ERR_DISPOSED` + `StorageDisposedError` (with
`isStorageDisposedError` guard).
- `entry()`, `clear()` and `entries()` throw `StorageDisposedError`
after `dispose()` instead of silently mutating refcounted
registries with the bus already torn down.
- Re-exports added to the index barrel.
frontend:
- `ActiveFrontend.disposed` getter on the public type.
- Every mutating setter (`setLocale`, `setDir`, `clearDir`,
`setTheme`, `setMode`, `clearMode`, `setReducedMotion`,
`clearReducedMotion`, `setReducedSound`, `setDensity`) now
short-circuits when disposed, so a late media-query event or a
locale-source emit during teardown can't rewrite the DOM through
a torn-down `applyDom()`. Read-only getters keep returning the
last applied value.
- `onPreferenceChange` returns a no-op detacher post-dispose.
- `dispose()` is idempotent (was already, now also guarded against
resurrected mutations).
format:
- `ActiveFormat.disposed` getter on the public type.
- `dispose()` is now idempotent at the root and walks each
sub-engine in stable order.
- `setLocale()` is a no-op post-dispose.
Tests: +3 regression tests (one per art) covering the new dispose
semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
f1055842dd |
Bloque A — sweep stale aliases and `App.<Capitalized>` references
Audit P1: documentation must stop teaching APIs the runtime no longer exposes. The svelte.config.js aliases are full words now (`$cache`, `$session`, `$connection`, `$timer`, `$logger`, `$format`, `$storage`, `$active-app`, `$bus`); the legacy 4-letter forms (`$cach`, `$sess`, `$conn`, `$timr`, `$logr`, `$fmts`, `$stor`, `$aapp`, `$buss`) were retired earlier but still lived in READMEs, demo pages, comments and a few code docstrings. Likewise, the `App.<service>` surface is lowercase for declarable services. The capitalized form is reserved for the four-piece core (`Logger`, `Bus`, `Timers`, `Orca`). References like `App.Cache`, `App.Sess`, `App.Storage`, `App.Format`, `App.Frontend`, `App.Lang`, `App.Auth`, `App.Perms`, `App.Http`, `App.Dom`, `App.Sium` were either ported to the new lowercase or migrated where it made sense. Mechanical sweep across `src/`, then a guard script: - `scripts/check-aliases.mjs` walks `src/`, fails the run if any forbidden alias or `App.<forbidden capitalized>` appears in any `.ts` / `.svelte` / `.md` / `.txt` / `.js` / `.mjs` file. `arts/active-app/types.ts` is allowlisted because its block comment explicitly documents the legacy uppercase surface as "removed". - `npm run test:aliases` exposes the script. - `npm run test:all` now includes the alias check. No runtime change; tests still 1486 / 1486. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
e6b6074c94 |
Address Codex audit P2.11 — split orca pure helpers out of `engine-orca.ts`
The engine file shrank from ~1900 LOC to ~1430 by lifting the state-free helpers into three focused modules: - `ids.ts` — `createDefaultIdFactory(timers)`. Already conceptually factored after P1.4; this commit moves the implementation out so the engine no longer references `TimerScheduler` from a one-off factory. - `validation.ts` — the entire `Orca.validate()` analysis: `validateConfiguration` orchestrator, `sortActionsCanonical`, `validateGatesForEvent`, `validateCyclesForEvent`, `validateTransactionsForEvent`, `canonicalCycleFingerprint`, and the public `RegisteredActionEntry` shape they share. Pure functions over the registry map; safe to unit-test in isolation. - `runner-helpers.ts` — `buildWaves`, `evaluateGates`, `interruptedActionRun`, `mapResultToActionStatus`, `computeRunStatus`. `computeRunStatus` now takes plain primitives (`traceAborted`, `traceAbortedReason`) instead of the engine's internal `TraceState` map, so the helper module has zero knowledge of engine state. `engine-orca.ts` keeps only the genuinely stateful orchestration core: registry, queue, drain / spawn / executeRun, runAction / runActionWithTimeout / runCompensation, ALS bus interception, and the public API surface. Constants and types that became private to the extracted modules left the engine's import block too — the file is now scannable in a single pass without having to swap mental contexts between "validation rules" and "run loop". No behavioural change. Suite: 1486 / 1486. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
c1d1ae9534 |
Address Codex audit P3.15 — ecosystem orca integration test
New `src/arts/active-app/test/ecosystem-orca.test.ts` validates the
canonical motivating scenario for `arts/orca`:
- user A → user B switch fires cache.clear, perm.invalidate and
connections.reauthenticateAll in a single orca trace, with one
runId and the union of every preset's `provides` tokens.
- session revoke fires cache.clear-on-revoke and
connections.closeAll('session-revoked'); identity-change actions
do not run on revoke.
- when one art's reaction throws, the others still run because
every preset declares `onError: continue`; the failing action is
recorded in the run trace with status `error`, run status
`partial`.
- the detacher returned by `applyStandardOrca` unregisters every
preset — subsequent events are inert.
Closes the audit's "missing compound test of the user A → user B
scenario" finding and replaces the README's pending-note with a
pointer to the live test file.
Suite: 1486 / 1486 (+4 from this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
2473ade4ad |
Address Codex audit P1.6 — connection presets + dead `autoReauthOn` removal
Two new orca presets in `arts/active-app/presets/`:
- `applyConnectionsReauthOnIdentityChange` — listens to
`SESSION_EVENT_IDENTITY_CHANGED` and calls
`App.connections.reauthenticateAll()`. Closes the canonical motivating
scenario for orca: "chat connected with the previous user's
credentials" can no longer happen with this preset wired.
- `applyConnectionsCloseOnRevoke` — listens to `SESSION_EVENT_REVOKED`
and calls `App.connections.closeAll('session-revoked')`, leaving no
socket alive carrying revoked credentials.
`applyStandardOrca` now picks both up automatically when `App.connections`
is declared, and the index barrel re-exports the new shapes.
Removes the dead `autoReauthOn` config — declared on
`EngineConnectionsOptions` but never read by any runtime code:
- field removed from `connection/types.ts`
- `CONNECTION_AUTO_REAUTH_*` constants removed from `connection/consts.ts`
- `ConnectionAutoReauthOn` / `ConnectionAutoReauthTarget` types removed
- unused test import removed from `connection.test.ts`
- connection README rewritten: orca preset is now the canonical bridge,
per-connection `session: { ... }` documented as the manual / standalone
alternative
- demo route artifact-docs.ts and aapp page updated to use
`applyStandardOrca(App)` instead of `autoReauthOn: 'standard'`
The per-connection `session-wiring.ts` mechanism stays as-is — it's
useful for connections that live outside an App composition or that
need a custom `ConnectionSessionSource`. README now spells out the
two paths: orca preset for App-composed apps, per-connection `session`
for manual control.
Suite: 1482 / 1482 (+4 from this commit: 3 preset behaviour tests
+ 1 `applyStandardOrca` connection wiring test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c0ed619372 |
Address Codex audit P2.7 / P2.8 / P2.9 / P2.10
P2.7 — `ctx.throwIfAborted()` helper. The action context now exposes `throwIfAborted()`. It throws an `AbortError`-style exception when `signal.aborted` is true, idiomatic for breaking out between awaits and avoiding side effects after a timeout / run-abort / dispose. Compensation contexts get the same helper. Documents cooperative cancellation as a hard contract: actions that touch external state must check `signal.aborted` (or call this helper) before mutating, especially after long awaits. Three regression tests (no-op when live, throws inside FINALLY when upstream aborted, prevents post-timeout side effects). P2.8 — global serial lane decision documented. Reformulates `canStartRun` with a doc block making the design explicit: non-parallel events share a single global lane (at most one fifo / replace-queued / drop-latest run in flight at any time); parallel-policy events bypass the lane. README's queue-policies section now leads with the lane invariant and the v2 roadmap lists "per-event concurrency lane for non-parallel policies" as a deferred upgrade. P2.9 — `commit()` documented as production seal, not a mandatory step. Adds a "When to commit" paragraph to the JSDoc: apps with an eager bootstrap should commit; apps that register actions from lazy-loaded routes must not. Removes the implicit assumption that every app should call commit during init. P2.10 — bus interception documented as bonus diagnostic, not a contract. README's bus-interception bullet now warns that the ALS attribution is an opt-in-by-environment feature: it improves the trace where AsyncLocalStorage exists (Node/Bun, modern browsers with AsyncContext) and silently degrades to root-event semantics elsewhere. Hard rule: actions must use `ctx.emit()` for attribution-critical fan-out; reserve `bus.publish` for genuinely root events. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
7e2907a75b |
Address Codex audit P1.1 / P1.2 / P1.3 / P1.4 + leftover cleanup
P1.1 — trace cleanup safe under parallel runs. `TraceState` gains `inFlightRuns`. `spawnRun` increments it synchronously before the IIFE awaits `executeRun`, decrements it in its `finally` and only then attempts `maybeReleaseTrace`. The release helper waits for both the queue snapshot and `inFlightRuns` to be empty before dropping the entry. Previously a parallel sibling finishing first could erase counters another run still relied on (reentry guards, dedupeKeys, abort flags). P1.2 — `provides` becomes the actual contract. When an action declares a non-empty `provides`, `runAction` checks each emitted token against it and emits `orca.configuration.invalid` for every undeclared token. Soft enforcement: the token is *not* dropped, keeping runtime back-compat; the diagnostic flags drift between the declaration and the runtime so authors notice. A v2 strict-drop mode can opt in later. P1.3 — `replace` queue policy renamed. The constant is now `ORCA_QUEUE_REPLACE_QUEUED` (literal `'replace-queued'`). The old name implied `takeLatest`-style "abort in-flight + queue new", which the engine never did. The hard variant lives in Roadmap v2 as `'replace-current'`. Tests updated; v1 has no external consumers yet so no back-compat alias. P1.4 — `idFactory` becomes injectable. New `OrcaIdFactory` type + `EngineOrcaOptions.idFactory`. Default factory uses the injected `timers.clock.now()` (no more direct `Date.now()` violating the "all time via timr" rule); replay/snapshot tests pass a deterministic counter. Eliminated `generateRunId` / `generateEventId` / `generateTraceId` standalone helpers. Cleanup leftovers from the audit: - `engine-orca.ts` header rewritten — was still claiming `after`/`unless`/`abortOn`/`actionTimeoutMs`/`compensate` are "accepted, ignored". Now describes the real surface. - `README.md` "Estado Del Documento" already updated; this commit also drops the legacy `## Roadmap` block, removes the `setupOrca` recommendation (moved to roadmap), rewrites "Tokens Flag" to cover the with-payload form, refreshes the Diagnostics list to match `consts.ts`, and replaces the "Tests Requeridos" wishlist with a snapshot of actual coverage + the pending ecosystem test. Tests: +5 (149 in engine-orca.test.ts, 16 in active-orca, 0 in result.test.ts → 165 in orca; 1475 / 1475 across the repo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
38462f3282 |
Cleanup: remove stale orca v0/v0.1+ markers across docs and types
After v1 closed, the public surface still carried `@v0.0 Accepted, ignored`, `@v0.1+ never produced`, `@v1+ never invoked` notes that no longer match the engine. They lied to readers about what the runtime does. Code: - `types.ts` — rewrite docstrings for `OrcaTimeout`, `OrcaFatal`, `ctx.tokens`, `after`, `unless`, `abortOn`, `provides`, `actionTimeoutMs`, `onError`, `compensate` to describe current behaviour. - `consts.ts` — `ORCA_RESULT_TIMEOUT` / `_FATAL` and the error policy block lose their "never produced" / "v0.0" hedges. - `result.ts` — `orcaTimeout` / `orcaFatal` get real docstrings instead of `@v0.1+` markers. Docs: - `orca/README.md` "Estado Del Documento" — summarise v1 surface (engine + active wrapper, queue policies, transactions, fan-in, bus interception, tokens with payload). Drop the legacy `## Roadmap` section that listed v0/v0.1/v1 line items already delivered or already covered by the lower "Roadmap v1" / "v2" sections. - `active-app/README.md` — drop the dangling "orca v0.0" link. - `docs/orca_minds.txt` — prepend an ARCHIVED banner. - `docs/active-app-refactorizacion.md` — replace the "Working document, abierto a evaluación" header with an ARCHIVED notice (refactor completed 2026-05-04). No behavioural change. Tests still 1471/1471. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
6217c86b0d |
createActiveOrca — Svelte 5 reactive wrapper, closes orca v1
`createActiveOrca(options)` returns the engine's full interface plus five `$state`-backed snapshot properties: `runningSnapshot`, `recentRunsSnapshot`, `latestRun`, `committedSnapshot`, `disposedSnapshot`. The cells are refreshed inside an `engine.onChange` listener — no `$effect` chains, so no risk of `effect_update_depth_exceeded`. To wire that, `EngineOrca` gains `onChange(listener): () => void` and notifies on register / detach / run-start / run-complete / commit / dispose. Run controllers now live on `spawnRun` (created synchronously and tracked in `inFlightControllers`) instead of inside `executeRun`, so the `running` getter flips before the first await — reactive consumers see the start tick. Also stabilises an existing parallel-waves test that flaked when the suite ran under heavier concurrent load by raising the await margin. With this change, every line of the v1 roadmap is honoured by the engine. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
23351e99c2 |
Bus interception — attribute `bus.publish` from inside an action
When a module calls `bus.publish('e', payload)` from the body of a
running orca action, the engine now treats the resulting bus event
as a child of that action: same `traceId`, `parentEventId` pointing
at the active run's envelope, depth+1, `emittedByAction` set to the
action's id. Previously the event entered as a fresh root and broke
causal traceability.
Implementation: a new `als.ts` module loads `AsyncLocalStorage`
cross-env — sync detect on `globalThis` first, fallback to dynamic
`node:async_hooks` import for Node/Bun. The first
`runActionInWave` awaits the loader and caches the resolved value
synchronously; the bus listener reads `getStore()` sync. In
browsers without AsyncContext the cached value stays `null` and
the semantics fall back to root-event (authors there should use
`ctx.emit()` for attribution).
Reentry guards count intercepted events identically to
`ctx.emit()`-derived ones, so depth/event/dedupe limits still
apply. Parallel siblings receive independent ALS contexts via
the standard ALS isolation, so attribution doesn't cross between
in-flight peers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
ef73979899 |
README: document v2 roadmap, move bus-interception to v2
Splits pending work into v1 (only `createActiveOrca()` left), v2 deferred-from-v1 items (bus interception via AsyncLocalStorage, `replace` with abort-in-flight, cross-event and nested transactions), the original v0/v1 roadmap not yet attacked (retry policies, concurrency limits, typed token payloads, graph visualisation, inspector, app presets, integration tests with `sess` / `perm` / `cach` / etc.), and the further-out `active-server` direction. Documenting the deferred set keeps audit context honest about what v1 leaves unfinished without renegotiating each item. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
c9ff825d13 |
Per-event queue policies — fifo / replace / drop-latest / parallel
`orca.configureEvent(event, { queuePolicy })` selects how concurrent
or queued runs of an event are handled:
- `fifo` (default): runs serialise globally with all other
non-parallel runs, preserving the v0 single-queue invariant.
- `replace`: a new event of the same name displaces any queued
envelope (in-flight is not aborted) and emits
`orca.queue.dropped` with `reason: 'replaced'`.
- `drop-latest`: an incoming event is dropped when one of the same
name is already in flight or queued (`reason: 'drop-latest'`).
- `parallel`: runs of this event launch concurrently via spawned
IIFEs and bypass the global non-parallel lock — they can race
with each other and with non-parallel events.
Engine refactor: drainQueue is now sync, walks the queue and
delegates to `spawnRun` (fire-and-forget IIFE). Each run tracks
its `AbortController` in `inFlightControllers` so `dispose()`
aborts them all in one pass. `running` getter reads
`inFlightControllers.size > 0`.
`configureEvent` is idempotent with the same policy, throws on a
conflicting reconfiguration, and throws `OrcaFrozenError` after
`commit()`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
6eb97abe2b |
Honor `fanIn` — quorum gate over multiple tokens
A `fanIn: { tokens, min }` declaration runs the action only when at
least `min` of the listed tokens are present in the wave snapshot.
Default `min = tokens.length` (AND); `min: 1` yields
"first-to-finish wins"; intermediate values give k-of-n quorum
patterns useful for voting / multi-source aggregation.
Evaluation order: `unless` → `abortOn` → `fanIn` → `after`.
`fanIn` and `after` may co-exist; both must pass. Skipped runs
carry `reason: fan-in-not-met:<count>/<min>:<tokens-present>`.
`validate()` reports `unsatisfiable-fan-in` (error) when fewer than
`min` upstream actions on the same event provide any of the listed
tokens — the gate could never fire at runtime. Stabilises an
existing parallel-wave timing test that flaked on slow CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
7341b9c62a |
Tokens with payload — `emits` accepts `{ token, payload }`
Token emissions can now carry arbitrary payload data. The `emits`
array accepts either a bare token name (existing form) or
`{ token, payload }`; both forms mix freely. Downstream actions
read payloads via `ctx.tokenPayloads.get(name)`, with
`ctx.tokens.has()` still answering name-presence. The two views
can diverge: a string-form emission has presence but no payload.
Gates (`after` / `unless` / `abortOn` / `provides`) keep comparing
names only — payload semantics are opt-in metadata. Wave snapshots
extend to payloads, so parallel siblings never read each other's
payloads mid-flight. Last-write-wins on duplicate names.
Surface: `OrcaTokenWithPayload`, `OrcaTokenEmit`,
`OrcaActionContext.tokenPayloads`, `OrcaActionRun.emittedPayloads?`,
`OrcaRunResult.tokenPayloads`. Compensation contexts also expose
`tokenPayloads` so rollback paths can read the run-level state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
6f9b558238 |
Honor `transaction` — atomic groups with immediate LIFO rollback
Actions on the same event sharing a `transaction` tag form an atomic group. When any member completes with `ERROR` or `FATAL`, the engine immediately compensates that group's already-succeeded members in LIFO order of completion, marks the run aborted, and emits `RUN_ABORTED`. Transaction semantics override the failing member's own `onError` — `abort-run` is implicit. Compensators run at most once per action: tx-driven rollback marks its entries as compensated, and the standard pre-`FINALLY` rollback skips them. Non-tx compensable actions still compensate at the global phase. `OrcaActionRun.transactionId` mirrors the tag for trace navigation. `validate()` warns `transaction-without-compensate` when no member of a transaction declares a compensator (rollback would be a no-op). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
a518dcac6b |
Honor `parallel: true` — wave-based execution within a stage
Consecutive `parallel: true` actions in the same stage form a wave that runs concurrently via `Promise.all`. Sequential actions break the wave (each is a wave of one). Tokens emitted inside a wave merge into the run-level set only **after** the wave settles, so parallel siblings never see each other's tokens — gate evaluation runs against a wave-start snapshot. Errors and `OrcaFatal` decide aborts after the surrounding wave settles; in-flight siblings are not cancelled. Compensable parallel successes enter the LIFO stack in registration order. `validate()` retains `provides` until wave end and now flags `unsatisfiable-after` when two parallel siblings cross-depend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
f62020e687 |
Add Orca.commit() — freeze the action graph
`commit()` flips a one-way `committed` flag; subsequent `onEvent()` calls throw `OrcaFrozenError` (code `ORCA_ERR_FROZEN`). It is idempotent, does not run `validate()` implicitly, and does not disturb already-registered actions, in-flight runs, or detach functions returned before the freeze. `dispose()` keeps precedence over the frozen guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
c996c90dc8 |
Honor compensate: invoke compensators LIFO when a run aborts
Fourth batch of v1 features. With this, OrcaAction.compensate moves
from accepted-and-ignored to a real Saga-style rollback hook: when a
run aborts (OrcaFatal, ORCA_ON_ERROR_ABORT_RUN, or trace-aborted), the
engine walks back through every action that previously completed in
success and invokes its compensator before the FINALLY stage runs.
Engine semantics:
- During the action loop, when an action returns success and has
declared `compensate`, push { action, payload } onto a LIFO stack
(compensable[]). Stage FINALLY actions are not compensable —
FINALLY is the cleanup pass itself.
- On entry to the FINALLY stage with aborted=true and
compensable.length > 0, walk the stack in reverse. Each
compensator gets a fresh AbortController (the run controller is
already aborted) and an OrcaActionContext whose emit() returns
null — compensations do not fan out new events, they roll back.
- Compensations are best-effort: a thrown compensator is recorded as
ORCA_ACTION_STATUS_ERROR but the next compensation in the chain
still runs. v1 chooses best-effort over fail-fast because rolling
back N-1 entries when one of them failed is more useful than
rolling back zero.
- FINALLY actions run AFTER compensations, in normal stage order.
Conceptual order on abort:
action loop → compensation phase → FINALLY → run trace recorded.
- Compensations appear in OrcaRunResult.compensations[], a separate
field from actions[]. The original action's record stays in
actions[] with its original success status; the compensator's
record lives only in compensations[]. This keeps the run trace
accurate (the action did succeed; it was just compensated later).
- Diagnostics: orca.compensation.started (DEBUG),
orca.compensation.completed (DEBUG), orca.compensation.failed
(ERROR). All carry runId, actionId, eventId, traceId, depth.
- Compensation does NOT trigger for PARTIAL runs (CONTINUE-onError
actions that errored without aborting) or TIMEOUT-only runs that
didn't abort. The semantic is "all-or-nothing rollback for
aborted runs", not "partial cleanup".
OrcaRunResult gains a `compensations: readonly OrcaActionRun[]` field
(empty array when no compensation ran).
Tests (+10 in a new "v1 — compensate" describe block):
- does not invoke compensate when the run completes successfully
- invokes compensate of a previously successful action when the run
aborts (the simplest happy path)
- does not invoke compensate of an action that errored itself
- runs compensations in LIFO order (with three compensators)
- runs compensations even when OrcaFatal aborts the run
- continues with remaining compensations even if one throws
(best-effort, the failing compensator's status is ERROR but
earlier and later ones still ran)
- runs FINALLY actions after compensations (order: ['compensate',
'finally'])
- does not invoke compensate when a CONTINUE-onError action errors
(PARTIAL run, no abort)
- emits orca.compensation.{started,completed,failed} diagnostics
- passes the original event payload to compensate
- emits inside ctx during compensation are dropped (return null)
The legacy "accepts compensate without invoking it" test from the v0
forward-compat block was deleted; v0 promise is now v1 reality. The
case it asserted (compensate of a failing action is not invoked) is
now covered explicitly by the new "does not invoke compensate of an
action that errored itself" test.
Verification: 1402/1402 vitest tests pass (92 in orca, +10 from this
commit on top of 82 from previous v1 commits).
README updated: compensate moved from "Roadmap v1" to "Ya en el motor
(de v1)". Remaining v1 items: commit() configuration freeze, queue
policies (commit/replace), transaction groups, parallel,
payload-bearing tokens, fan-in, bus interception (Option B), and
createActiveOrca() reactive wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |