@vielzeug/codex 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/data/catalog.json +9 -5
  2. package/data/llms-full.txt +16 -44
  3. package/data/llms.txt +2 -2
  4. package/data/manifest.json +1 -1
  5. package/data/packages/arsenal.json +1 -1
  6. package/data/packages/assay.json +26 -26
  7. package/data/packages/clockwork.json +3 -3
  8. package/data/packages/codex.json +23 -23
  9. package/data/packages/coins.json +5 -5
  10. package/data/packages/conduit.json +8 -8
  11. package/data/packages/courier.json +8 -8
  12. package/data/packages/dnd.json +6 -6
  13. package/data/packages/familiar.json +9 -9
  14. package/data/packages/flux.json +9 -9
  15. package/data/packages/forge.json +2 -2
  16. package/data/packages/keymap.json +6 -6
  17. package/data/packages/lingua.json +22 -22
  18. package/data/packages/necromancer.json +2 -2
  19. package/data/packages/orbit.json +21 -21
  20. package/data/packages/ore.json +49 -49
  21. package/data/packages/prism.json +22 -22
  22. package/data/packages/pulse.json +17 -17
  23. package/data/packages/ripple.json +22 -16
  24. package/data/packages/rune.json +29 -29
  25. package/data/packages/scout.json +2 -2
  26. package/data/packages/scroll.json +15 -15
  27. package/data/packages/sourcerer.json +28 -28
  28. package/data/packages/spell.json +23 -23
  29. package/data/packages/tempo.json +14 -14
  30. package/data/packages/vault.json +4 -4
  31. package/data/packages/ward.json +25 -25
  32. package/data/packages/wayfinder.json +44 -44
  33. package/data/refine.json +4600 -4600
  34. package/data/search.json +33 -33
  35. package/dist/catalog.js.map +1 -1
  36. package/dist/cli.js +1 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/http.js +1 -1
  39. package/dist/http.js.map +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/snapshot.js.map +1 -1
  42. package/dist/tools/index.js.map +1 -1
  43. package/dist/tools/packages.js.map +1 -1
  44. package/dist/tools/refine.js.map +1 -1
  45. package/mcp-setup.json +5 -5
  46. package/package.json +20 -20
@@ -1,10 +1,10 @@
1
1
  {
2
- "apiSource": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';\n\nexport {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';\nexport { isReactive } from './runtime';\n\nexport { createRipple, type Ripple } from './_default';\n\nimport { defaultRipple } from './_default';\n\n// `resource`/`createStore`/`watch` are deliberately NOT re-exported here — they're reachable\n// only through their dedicated subpaths (`./async`, `./store`, `./watch`), so there's exactly\n// one canonical import path per primitive instead of two that resolve to the same binding.\nexport const signal = defaultRipple.signal;\nexport const computed = defaultRipple.computed;\nexport const effect = defaultRipple.effect;\nexport const batch = defaultRipple.batch;\nexport const createScope = defaultRipple.createScope;\nexport const untrack = defaultRipple.untrack;\n",
2
+ "apiSource": "export type { AsyncState, Resource, ResourceOptions } from './_async';\nexport { createRipple, type Ripple } from './_default';\nexport type { WatchOptions } from './_watch';\nexport {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';\nexport { isReactive } from './runtime';\nexport type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';\n\nimport { defaultRipple } from './_default';\n\nexport const signal = defaultRipple.signal;\nexport const computed = defaultRipple.computed;\nexport const effect = defaultRipple.effect;\nexport const batch = defaultRipple.batch;\nexport const createScope = defaultRipple.createScope;\nexport const untrack = defaultRipple.untrack;\nexport const watch = defaultRipple.watch;\nexport const resource = defaultRipple.resource;\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Ripple — Reactive graphs\ndescription: Framework-agnostic signals, derived values, effects, scopes, async resources, and immutable state.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, async]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, untrack, isReactive]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ripple\" />\n\n## Why Ripple?\n\nHand-rolled reactive state spreads subscription, cleanup, and derived-value rules across application code. Ripple gives you one graph boundary with explicit disposal and fine-grained dependencies while keeping rendering and routing outside the runtime.\n\n```ts\n// Before\nlet count = 0;\nconst listeners = new Set<() => void>();\n\nfunction setCount(next: number) {\n count = next;\n for (const listener of listeners) listener();\n}\n\n// After\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\ncount.value = 1;\nstop.dispose();\nripple.dispose();\n```\n\n| Feature | Ripple | Zustand | Jotai |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"ripple\" type=\"size\" /> | ~3.5 kB | ~7 kB |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework-agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | React-first |\n| Explicit graph lifetime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Fine-grained derived values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Selectors | Atoms |\n\n<div class=\"decision-callout\">\n\n**Use Ripple when** you need framework-independent state with explicit graph lifetime and small composable primitives.\n\n**Consider a framework store when** component bindings, server cache, or framework-specific tooling matter more than portable reactive state.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nCreate one graph, derive a value, observe it, then dispose resources when the graph lifetime ends.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\n\nstop.dispose();\nripple.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createRipple()` creates an isolated graph and lifetime boundary.\n- `signal()` stores writable values with configurable equality.\n- `computed()` derives lazy read-only values.\n- `effect()` reacts to dependency changes with cleanup support.\n- `batch()` coalesces synchronous writes and notifications.\n- `createScope()` groups owned reactive work.\n- `watch()` observes one selected source transition.\n- `resource()` loads async values with stale-work cancellation.\n- `createStore()` wraps explicit value replacement and updater functions.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — uses Ripple signals and effects for web-component reactivity.\n- [Clockwork](/clockwork/) — exposes machine state through reactive Ripple values.\n- [Ledger](/ledger/) — adds command-based undo and redo beside Ripple state.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
- "api": "---\ntitle: Ripple — API Reference\ndescription: Complete reference for reactive graphs, signals, effects, scopes, watchers, resources, and stores.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createRipple()` | Create isolated graph | Sync | Disposal is terminal; create a new graph instead of reusing it |\n| `signal()` | Create writable value | Sync | Default graph is process-wide |\n| `computed()` | Create lazy derived value | Sync | Keep derivation pure |\n| `effect()` | React to dependency reads | Sync | Dispose handle or return cleanup |\n| `batch()` | Coalesce synchronous writes | Sync | Does not roll back writes |\n| `createScope()` | Group owned reactive work | Sync | Call `run()` to activate it |\n| `untrack()` | Read without tracking | Sync | Read still happens immediately |\n| `watch()` | Observe selected output | Sync | Use `effect()` for broad reads |\n| `resource()` | Load async source | Async | Read dependencies in source callback |\n| `createStore()` | Hold replacement-based state | Sync | Return replacement objects from updates |\n| `isReactive()` | Test `Readable` identity | Sync | Does not test arbitrary objects |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | Default graph APIs, isolated graph factory, types, and errors |\n| `@vielzeug/ripple/watch` | `watch()` and `WatchOptions` |\n| `@vielzeug/ripple/async` | `resource()`, `Resource`, `AsyncState`, `ResourceOptions` |\n| `@vielzeug/ripple/store` | `createStore()`, `Store`, `StoreOptions` |\n\n## Graph Creation\n\n### `createRipple(options?)`\n\n```ts\nfunction createRipple(options?: RippleOptions): Ripple;\n```\n\nCreates one isolated reactive graph. Factories on the returned object share scheduling, ownership, observer, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `RippleDisposedRuntimeError`. Create a new graph for a new lifetime.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.onError` | `(error, context) => void` | Receives effect, cleanup, listener, or observer failures. |\n| `options.observer` | `ReactiveObserver` | Receives graph events. |\n\n**Returns:** `Ripple`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n---\n\n### `isReactive(value)`\n\n```ts\nfunction isReactive<T>(value: T | Readable<T>): value is Readable<T>;\n```\n\nTests whether a value is a Ripple readable node.\n\n**Returns:** `true` for a `Signal`, computed value, or other `Readable` node.\n\n**Example:**\n\n```ts\nimport { isReactive, signal } from '@vielzeug/ripple';\n\nconsole.log(isReactive(signal(0)));\n```\n\n## Default Graph Functions\n\n### `signal(initial, options?)`\n\n```ts\nfunction signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n```\n\nCreates writable state on the default graph.\n\n**Returns:** `Signal<T>`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\ncount.value += 1;\n```\n\n---\n\n### `computed(derive, options?)`\n\n```ts\nfunction computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n```\n\nCreates a lazy read-only value from reactive reads in `derive`.\n\n**Returns:** `Readable<T>`.\n\n**Example:**\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\n\nconst count = signal(2);\nconst doubled = computed(() => count.value * 2);\nconsole.log(doubled.value);\n```\n\n---\n\n### `effect(callback, options?)`\n\n```ts\nfunction effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;\n```\n\nRuns immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst connected = signal(false);\nconst stop = effect(() => {\n if (!connected.value) return;\n\n return () => console.log('disconnect');\n});\n\nstop.dispose();\n```\n\n---\n\n### `batch(fn)` and `untrack(fn)`\n\n```ts\nfunction batch<T>(fn: () => T): T;\nfunction untrack<T>(fn: () => T): T;\n```\n\n`batch()` defers effects and listeners until its callback returns. `untrack()` reads current state without adding dependencies to an enclosing effect.\n\n**Returns:** the callback result.\n\n**Example:**\n\n```ts\nimport { batch, signal, untrack } from '@vielzeug/ripple';\n\nconst first = signal('Ada');\nconst last = signal('Lovelace');\nconst locale = signal('en-US');\n\nbatch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n\nconsole.log(untrack(() => locale.value));\n```\n\n---\n\n### `createScope(name?)`\n\n```ts\nfunction createScope(name?: string): Scope;\n```\n\nCreates a disposable ownership boundary. Work created inside `scope.run()` belongs to that scope.\n\n**Returns:** `Scope`.\n\n**Example:**\n\n```ts\nimport { createScope, effect, signal } from '@vielzeug/ripple';\n\nconst scope = createScope('panel');\nconst count = signal(0);\n\nscope.run(() => effect(() => console.log(count.value)));\nscope.dispose();\n```\n\n## Watch, Resources, and Stores\n\n### `watch(source, callback, options?)`\n\n```ts\nfunction watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, previous: T | undefined) => void,\n options?: WatchOptions<T>,\n): EffectHandle;\n```\n\nObserves selected output changes using the default graph or a `Ripple.watch()` method.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { watch } from '@vielzeug/ripple/watch';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n---\n\n### `resource(source, loader, options?)`\n\n```ts\nfunction resource<Source, Value>(\n source: () => Source,\n loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,\n options?: ResourceOptions,\n): Resource<Value>;\n```\n\nTracks `source`, aborts stale loader work, and exposes `AsyncState<Value>`. Source and loader failures become `status: 'error'` state; handle them from `resource.value` rather than `RippleOptions.onError`, which is reserved for runtime callback, cleanup, listener, and observer failures.\n\n**Returns:** `Resource<Value>`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { resource } from '@vielzeug/ripple/async';\n\nconst userId = signal('42');\nconst user = resource(() => userId.value, async (id) => ({ id }));\n\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n---\n\n### `createStore(initial, options?)`\n\n```ts\nfunction createStore<T>(initial: T, options?: StoreOptions): Store<T>;\n```\n\nCreates one writable value wrapper with explicit `set()` and `update()` operations.\n\n**Returns:** `Store<T>`.\n\n**Example:**\n\n```ts\nimport { createStore } from '@vielzeug/ripple/store';\n\nconst user = createStore({ name: 'Ada', visits: 0 });\nuser.update((value) => ({ ...value, visits: value.visits + 1 }));\n```\n\n## Types\n\n```ts\ntype Cleanup = () => void;\ntype Equality<T> = (previous: T, next: T) => boolean;\ntype Unsubscribe = () => void;\n\ntype SignalOptions<T> = { equals?: Equality<T>; name?: string };\ntype ComputedOptions<T> = { equals?: Equality<T>; name?: string };\ntype EffectOptions = { name?: string; scheduler?: 'microtask' | 'sync' };\ntype WatchOptions<T> = { equals?: Equality<T>; immediate?: boolean; name?: string; once?: boolean };\ntype ResourceOptions = { name?: string };\ntype StoreOptions = { name?: string };\n\ntype ReactiveEvent =\n | { readonly kind: 'compute'; readonly name?: string }\n | { readonly kind: 'effect'; readonly name?: string }\n | { readonly kind: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly kind: 'dispose'; readonly name?: string; readonly node: 'effect' | 'scope' };\n\ntype ReactiveObserver = (event: ReactiveEvent) => void;\ntype ReactiveErrorContext = { readonly kind: 'cleanup' | 'effect' | 'listener' | 'observer'; readonly name?: string };\ntype RippleOptions = { observer?: ReactiveObserver; onError?: (error: unknown, context: ReactiveErrorContext) => void };\n\ntype AsyncState<T> =\n | { readonly previous?: T; readonly status: 'pending' }\n | { readonly status: 'success'; readonly value: T }\n | { readonly error: unknown; readonly previous?: T; readonly status: 'error' };\n\ninterface Readable<T> {\n readonly name?: string;\n peek(): T;\n subscribe(listener: () => void): Unsubscribe;\n readonly value: T;\n}\n\ninterface Signal<T> extends Readable<T> { value: T }\ninterface Disposable { dispose(): void; readonly disposed: boolean; readonly disposalSignal: AbortSignal; [Symbol.dispose](): void }\ntype EffectHandle = Disposable;\ninterface Scope extends Disposable { run<T>(fn: () => T): T }\n\ninterface Resource<T> extends Readable<AsyncState<T>>, Disposable { reload(): void }\ninterface Store<T> extends Readable<T> { set(value: T): void; update(updater: (value: T) => T): void }\n\ninterface Ripple {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n createStore<T>(initial: T, options?: StoreOptions): Store<T>;\n dispose(): void;\n readonly disposed: boolean;\n effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;\n resource<Source, Value>(source: () => Source, loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>, options?: ResourceOptions): Resource<Value>;\n signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n untrack<T>(fn: () => T): T;\n watch<T>(source: Readable<T> | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions<T>): EffectHandle;\n}\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RippleError` | Base Ripple error | `RippleError.is(error)` narrows unknown values. |\n| `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |\n| `RippleDisposedRuntimeError` | Factory or execution API used after `ripple.dispose()` | Extends `RippleError`. |\n| `RippleDisposedScopeError` | `scope.run()` after scope disposal | Extends `RippleError`. |\n| `RippleInfiniteLoopError` | Effect flush exceeds graph iteration limit | Extends `RippleError`. |\n",
6
- "usage": "---\ntitle: Ripple — Usage Guide\ndescription: Build reactive state with one explicit graph boundary.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse top-level functions when one application-lifetime graph is sufficient. Read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `Count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## Isolated Graphs\n\nUse `createRipple()` for tests, SSR requests, embedded applications, or independently disposable features. Never mix reactive values from separate graphs.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({\n onError(error, context) {\n console.log(context.kind, error);\n },\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## Derived Values and Batches\n\nUse `computed()` for pure derivation. Use `untrack()` when a current read must not become an effect dependency. Use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('Ada');\nconst last = ripple.signal('Lovelace');\nconst locale = ripple.signal('en-US');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n```\n\n## Ownership with Scopes\n\nCreate a scope when a group of effects or derived values shares one lifetime. Dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createScope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`Panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Use `effect()` when every reactive read in the callback should be a dependency.\n\n```ts\nconst stopWatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopWatch.dispose();\n```\n\n## Async Data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new Error(`Request failed: ${response.status}`);\n\n return response.json() as Promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## Object State\n\n`createStore()` holds one value and exposes `set()` and `update()`. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.createStore({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.set({ items: 3, label: 'ready' });\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects and resource work from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createRipple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createRipple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).toBe(4);\n ripple.dispose();\n});\n```\n\n## Framework Integration\n\nUse signals and effects with any renderer. Dispose component-owned effects when the component unmounts.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\n\nexport function Counter() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onClick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonUnmounted(() => stop.dispose());\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createRipple } from '@vielzeug/ripple';\n\n const ripple = createRipple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n onDestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nOre uses Ripple for component reactivity. Clockwork actors expose framework-neutral snapshots; bridge actor subscriptions into a Ripple signal. Ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst ripple = createRipple();\nconst actor = defineMachine<Record<string, never>, { type: 'START' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { START: { target: 'active' } } } },\n}).createActor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose request, test, and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions.\n- Read dependencies in a resource source, not its loader.\n- Use `onError` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\n",
7
- "examples": "---\ntitle: Ripple — Examples\ndescription: Practical Ripple recipes.\n---\n\n## Examples\n\n- [Reactive Counter](./examples/reactive-counter.md)\n- [Batch and Untrack](./examples/batch-and-untrack.md)\n- [Scope Ownership](./examples/scope-ownership.md)\n- [Watch Selected Value](./examples/watch-selected-value.md)\n- [Replacement-Based Store](./examples/immutable-store.md)\n- [Isolated Graph](./examples/isolated-runtime.md)\n- [Async Resource](./examples/async-resource.md)\n"
4
+ "index": "---\ntitle: Ripple — Reactive graphs\ndescription: Framework-agnostic signals, derived values, effects, scopes, watchers, and async resources.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, watch, resource, async]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, untrack, watch, resource, isReactive]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ripple\" />\n\n## Why Ripple?\n\nHand-rolled reactive state spreads subscription, cleanup, and derived-value rules across application code. Ripple gives you one graph boundary with explicit disposal and fine-grained dependencies while keeping rendering and routing outside the runtime.\n\n```ts\n// Before\nlet count = 0;\nconst listeners = new Set<() => void>();\n\nfunction setCount(next: number) {\n count = next;\n for (const listener of listeners) listener();\n}\n\n// After\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\ncount.value = 1;\nstop.dispose();\nripple.dispose();\n```\n\n| Feature | Ripple | Zustand | Jotai |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"ripple\" type=\"size\" /> | ~3.5 kB | ~7 kB |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework-agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | React-first |\n| Explicit graph lifetime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Fine-grained derived values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Selectors | Atoms |\n\n<div class=\"decision-callout\">\n\n**Use Ripple when** you need framework-independent state with explicit graph lifetime and small composable primitives.\n\n**Consider a framework store when** component bindings, server cache, or framework-specific tooling matter more than portable reactive state.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nCreate one graph, derive a value, observe it, then dispose resources when the graph lifetime ends.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\n\nstop.dispose();\nripple.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createRipple()` creates an isolated graph and lifetime boundary.\n- `signal()` stores writable values with configurable equality.\n- `computed()` derives lazy read-only values.\n- `effect()` reacts to dependency changes with cleanup support.\n- `batch()` coalesces synchronous writes and notifications.\n- `createScope()` groups owned reactive work.\n- `watch()` observes one selected source transition.\n- `resource()` loads async values with stale-work cancellation.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — uses Ripple signals and effects for web-component reactivity.\n- [Clockwork](/clockwork/) — exposes machine state through reactive Ripple values.\n- [Ledger](/ledger/) — adds command-based undo and redo beside Ripple state.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
+ "api": "---\ntitle: Ripple — API Reference\ndescription: Complete reference for reactive graphs, signals, effects, scopes, watchers, and resources.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createRipple()` | Create isolated graph | Sync | Disposal is terminal; create a new graph instead of reusing it |\n| `signal()` | Create writable value | Sync | Default graph is process-wide |\n| `computed()` | Create lazy derived value | Sync | Keep derivation pure |\n| `effect()` | React to dependency reads | Sync | Dispose handle or return cleanup |\n| `batch()` | Coalesce synchronous writes | Sync | Does not roll back writes |\n| `createScope()` | Group owned reactive work | Sync | Call `run()` to activate it |\n| `untrack()` | Read without tracking | Sync | Read still happens immediately |\n| `watch()` | Observe selected output | Sync | Use `effect()` for broad reads |\n| `resource()` | Load async source | Async | Read dependencies in source callback |\n| `isReactive()` | Test `Readable` identity | Sync | Does not test arbitrary objects |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | All primitives, types, and errors — signals, computed, effects, scopes, watch, resource, and the isolated graph factory |\n\n## Graph Creation\n\n### `createRipple(options?)`\n\n```ts\nfunction createRipple(options?: RippleOptions): Ripple;\n```\n\nCreates one isolated reactive graph. Factories on the returned object share scheduling, ownership, observer, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `RippleDisposedRuntimeError`. Create a new graph for a new lifetime.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.onError` | `(error, context) => void` | Receives effect, cleanup, listener, or observer failures. |\n| `options.observer` | `ReactiveObserver` | Receives graph events. |\n\n**Returns:** `Ripple`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n---\n\n### `isReactive(value)`\n\n```ts\nfunction isReactive<T>(value: T | Readable<T>): value is Readable<T>;\n```\n\nTests whether a value is a Ripple readable node.\n\n**Returns:** `true` for a `Signal`, computed value, or other `Readable` node.\n\n**Example:**\n\n```ts\nimport { isReactive, signal } from '@vielzeug/ripple';\n\nconsole.log(isReactive(signal(0)));\n```\n\n## Default Graph Functions\n\n### `signal(initial, options?)`\n\n```ts\nfunction signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n```\n\nCreates writable state on the default graph. Use `update()` for immutable replacement patterns.\n\n**Returns:** `Signal<T>`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\ncount.value += 1;\n\nconst cart = signal({ items: 0 });\ncart.update((state) => ({ ...state, items: state.items + 1 }));\n```\n\n---\n\n### `computed(derive, options?)`\n\n```ts\nfunction computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n```\n\nCreates a lazy read-only value from reactive reads in `derive`.\n\n**Returns:** `Readable<T>`.\n\n**Example:**\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\n\nconst count = signal(2);\nconst doubled = computed(() => count.value * 2);\nconsole.log(doubled.value);\n```\n\n---\n\n### `effect(callback, options?)`\n\n```ts\nfunction effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n```\n\nRuns immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst connected = signal(false);\nconst stop = effect(() => {\n if (!connected.value) return;\n\n return () => console.log('disconnect');\n});\n\nstop.dispose();\n```\n\n---\n\n### `batch(fn)` and `untrack(fn)`\n\n```ts\nfunction batch<T>(fn: () => T): T;\nfunction untrack<T>(fn: () => T): T;\n```\n\n`batch()` defers effects and listeners until its callback returns. `untrack()` reads current state without adding dependencies to an enclosing effect.\n\n**Returns:** the callback result.\n\n**Example:**\n\n```ts\nimport { batch, signal, untrack } from '@vielzeug/ripple';\n\nconst first = signal('Ada');\nconst last = signal('Lovelace');\nconst locale = signal('en-US');\n\nbatch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n\nconsole.log(untrack(() => locale.value));\n```\n\n---\n\n### `createScope(name?)`\n\n```ts\nfunction createScope(name?: string): Scope;\n```\n\nCreates a disposable ownership boundary. Work created inside `scope.run()` belongs to that scope.\n\n**Returns:** `Scope`.\n\n**Example:**\n\n```ts\nimport { createScope, effect, signal } from '@vielzeug/ripple';\n\nconst scope = createScope('panel');\nconst count = signal(0);\n\nscope.run(() => effect(() => console.log(count.value)));\nscope.dispose();\n```\n\n## Watch and Resources\n\n### `watch(source, callback, options?)`\n\n```ts\nfunction watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, previous: T | undefined) => void,\n options?: WatchOptions<T>,\n): EffectHandle;\n```\n\nObserves selected output changes using the default graph or a `Ripple.watch()` method.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n---\n\n### `resource(source, loader, options?)`\n\n```ts\nfunction resource<Source, Value>(\n source: () => Source,\n loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,\n options?: ResourceOptions,\n): Resource<Value>;\n```\n\nTracks `source`, aborts stale loader work, and exposes `AsyncState<Value>`. Source and loader failures become `status: 'error'` state; handle them from `resource.value` rather than `RippleOptions.onError`, which is reserved for runtime callback, cleanup, listener, and observer failures.\n\n**Returns:** `Resource<Value>`.\n\n**Example:**\n\n```ts\nimport { resource, signal } from '@vielzeug/ripple';\n\nconst userId = signal('42');\nconst user = resource(() => userId.value, async (id) => ({ id }));\n\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## Types\n\n```ts\ntype Cleanup = () => void;\ntype Equality<T> = (previous: T, next: T) => boolean;\ntype Unsubscribe = () => void;\n\ntype SignalOptions<T> = { equals?: Equality<T>; name?: string };\ntype ComputedOptions<T> = { equals?: Equality<T>; name?: string };\ntype EffectOptions = { name?: string; scheduler?: 'microtask' | 'sync' };\ntype WatchOptions<T> = { equals?: Equality<T>; immediate?: boolean; name?: string; once?: boolean };\ntype ResourceOptions = { name?: string };\n\ntype ReactiveEvent =\n | { readonly kind: 'compute'; readonly name?: string }\n | { readonly kind: 'effect'; readonly name?: string }\n | { readonly kind: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly kind: 'dispose'; readonly name?: string; readonly node: 'effect' | 'scope' };\n\ntype ReactiveObserver = (event: ReactiveEvent) => void;\ntype ReactiveErrorContext = { readonly kind: 'cleanup' | 'effect' | 'listener' | 'observer'; readonly name?: string };\ntype RippleOptions = { observer?: ReactiveObserver; onError?: (error: unknown, context: ReactiveErrorContext) => void };\n\ntype AsyncState<T> =\n | { readonly previous?: T; readonly status: 'pending' }\n | { readonly status: 'success'; readonly value: T }\n | { readonly error: unknown; readonly previous?: T; readonly status: 'error' };\n\ninterface Readable<T> {\n readonly name?: string;\n peek(): T;\n subscribe(listener: () => void): Unsubscribe;\n readonly value: T;\n}\n\ninterface Signal<T> extends Readable<T> { update(updater: (prev: T) => T): void; value: T }\ninterface Disposable { dispose(): void; readonly disposed: boolean; readonly disposalSignal: AbortSignal; [Symbol.dispose](): void }\ntype EffectHandle = Disposable;\ninterface Scope extends Disposable { run<T>(fn: () => T): T }\n\ninterface Resource<T> extends Readable<AsyncState<T>>, Disposable { reload(): void }\n\ninterface Ripple {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n dispose(): void;\n readonly disposed: boolean;\n effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;\n resource<Source, Value>(source: () => Source, loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>, options?: ResourceOptions): Resource<Value>;\n signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n untrack<T>(fn: () => T): T;\n watch<T>(source: Readable<T> | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions<T>): EffectHandle;\n}\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RippleError` | Base Ripple error | `RippleError.is(error)` narrows unknown values. |\n| `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |\n| `RippleDisposedRuntimeError` | Factory or execution API used after `ripple.dispose()` | Extends `RippleError`. |\n| `RippleDisposedScopeError` | `scope.run()` after scope disposal | Extends `RippleError`. |\n| `RippleInfiniteLoopError` | Effect flush exceeds graph iteration limit | Extends `RippleError`. |\n",
6
+ "usage": "---\ntitle: Ripple — Usage Guide\ndescription: Build reactive state with one explicit graph boundary.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse top-level functions when one application-lifetime graph is sufficient. Read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `Count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## Isolated Graphs\n\nUse `createRipple()` for tests, SSR requests, embedded applications, or independently disposable features. Never mix reactive values from separate graphs.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({\n onError(error, context) {\n console.log(context.kind, error);\n },\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## Derived Values and Batches\n\nUse `computed()` for pure derivation. Use `untrack()` when a current read must not become an effect dependency. Use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('Ada');\nconst last = ripple.signal('Lovelace');\nconst locale = ripple.signal('en-US');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n```\n\n## Ownership with Scopes\n\nCreate a scope when a group of effects or derived values shares one lifetime. Dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createScope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`Panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Use `effect()` when every reactive read in the callback should be a dependency.\n\n```ts\nconst stopWatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopWatch.dispose();\n```\n\n## Async Data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new Error(`Request failed: ${response.status}`);\n\n return response.json() as Promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## Object State\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects and resource work from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createRipple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createRipple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).toBe(4);\n ripple.dispose();\n});\n```\n\n## Framework Integration\n\nUse signals and effects with any renderer. Dispose component-owned effects when the component unmounts.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\n\nexport function Counter() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onClick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonUnmounted(() => stop.dispose());\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createRipple } from '@vielzeug/ripple';\n\n const ripple = createRipple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n onDestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nOre uses Ripple for component reactivity. Clockwork actors expose framework-neutral snapshots; bridge actor subscriptions into a Ripple signal. Ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst ripple = createRipple();\nconst actor = defineMachine<Record<string, never>, { type: 'START' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { START: { target: 'active' } } } },\n}).createActor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose request, test, and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions.\n- Read dependencies in a resource source, not its loader.\n- Use `onError` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\n",
7
+ "examples": "---\ntitle: Ripple — Examples\ndescription: Practical Ripple recipes.\n---\n\n## Examples\n\n- [Reactive Counter](./examples/reactive-counter.md)\n- [Batch and Untrack](./examples/batch-and-untrack.md)\n- [Scope Ownership](./examples/scope-ownership.md)\n- [Watch Selected Value](./examples/watch-selected-value.md)\n- [Immutable State](./examples/immutable-store.md)\n- [Isolated Graph](./examples/isolated-runtime.md)\n- [Async Resource](./examples/async-resource.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {
@@ -34,8 +34,8 @@
34
34
  },
35
35
  {
36
36
  "id": "store-basics",
37
- "code": "import { createRipple } from '@vielzeug/ripple'\n\n// Store keeps immutable object updates explicit.\nconst ripple = createRipple()\nconst user = ripple.createStore({ name: 'Ada', visits: 0 })\nconst greeting = ripple.computed(() => user.value.name + ': ' + user.value.visits)\n\nconst stop = ripple.effect(() => console.log(greeting.value))\n\nuser.update((state) => ({ ...state, visits: state.visits + 1 }))\nuser.set({ name: 'Grace', visits: 5 })\n\nstop.dispose()\nripple.dispose()",
38
- "name": "Immutable Store"
37
+ "code": "import { createRipple } from '@vielzeug/ripple'\n\n// signal.update keeps immutable object updates explicit.\nconst ripple = createRipple()\nconst user = ripple.signal({ name: 'Ada', visits: 0 })\nconst greeting = ripple.computed(() => user.value.name + ': ' + user.value.visits)\n\nconst stop = ripple.effect(() => console.log(greeting.value))\n\nuser.update((state) => ({ ...state, visits: state.visits + 1 }))\nuser.value = { name: 'Grace', visits: 5 }\n\nstop.dispose()\nripple.dispose()",
38
+ "name": "Immutable State"
39
39
  },
40
40
  {
41
41
  "id": "watch-selected-value",
@@ -44,6 +44,18 @@
44
44
  }
45
45
  ],
46
46
  "typeSignatures": {
47
+ "AsyncState": "export type { AsyncState, Resource, ResourceOptions } from './_async';",
48
+ "Resource": "export type { AsyncState, Resource, ResourceOptions } from './_async';",
49
+ "ResourceOptions": "export type { AsyncState, Resource, ResourceOptions } from './_async';",
50
+ "createRipple": "export { createRipple, type Ripple } from './_default';",
51
+ "Ripple": "export { createRipple, type Ripple } from './_default';",
52
+ "WatchOptions": "export type { WatchOptions } from './_watch';",
53
+ "RippleComputedCycleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
54
+ "RippleDisposedRuntimeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
55
+ "RippleDisposedScopeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
56
+ "RippleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
57
+ "RippleInfiniteLoopError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
58
+ "isReactive": "export { isReactive } from './runtime';",
47
59
  "Cleanup": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
48
60
  "ComputedOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
49
61
  "Disposable": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
@@ -59,19 +71,13 @@
59
71
  "Signal": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
60
72
  "SignalOptions": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
61
73
  "Unsubscribe": "export type {\n Cleanup,\n ComputedOptions,\n Disposable,\n EffectHandle,\n EffectOptions,\n Equality,\n ReactiveErrorContext,\n ReactiveEvent,\n ReactiveObserver,\n Readable,\n RippleOptions,\n Scope,\n Signal,\n SignalOptions,\n Unsubscribe,\n} from './types';",
62
- "RippleComputedCycleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
63
- "RippleDisposedRuntimeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
64
- "RippleDisposedScopeError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
65
- "RippleError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
66
- "RippleInfiniteLoopError": "export {\n RippleComputedCycleError,\n RippleDisposedRuntimeError,\n RippleDisposedScopeError,\n RippleError,\n RippleInfiniteLoopError,\n} from './errors';",
67
- "isReactive": "export { isReactive } from './runtime';",
68
- "createRipple": "export { createRipple, type Ripple } from './_default';",
69
- "Ripple": "export { createRipple, type Ripple } from './_default';",
70
74
  "signal": "export const signal = defaultRipple.signal;",
71
75
  "computed": "export const computed = defaultRipple.computed;",
72
76
  "effect": "export const effect = defaultRipple.effect;",
73
77
  "batch": "export const batch = defaultRipple.batch;",
74
78
  "createScope": "export const createScope = defaultRipple.createScope;",
75
- "untrack": "export const untrack = defaultRipple.untrack;"
79
+ "untrack": "export const untrack = defaultRipple.untrack;",
80
+ "watch": "export const watch = defaultRipple.watch;",
81
+ "resource": "export const resource = defaultRipple.resource;"
76
82
  }
77
83
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "apiSource": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';\n\nexport { RuneError } from './errors';\nexport { isLevelEnabled, PRIORITY } from './types';\nexport type { LazyBinding } from './lazy';\nexport { lazy } from './lazy';\nexport { defaultLogger, createLogger } from './logger';\nexport type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';\nexport { DEFAULT_THEME, consoleTransport, resolveTheme } from './console';\nexport { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';\n",
2
+ "apiSource": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';\nexport { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';\nexport { RuneError } from './errors';\nexport type { LazyBinding } from './lazy';\nexport { lazy } from './lazy';\nexport { createLogger, defaultLogger } from './logger';\nexport { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';\nexport type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';\nexport { isLevelEnabled, PRIORITY } from './types';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Rune — Structured logging for TypeScript\ndescription: Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.\npackage: rune\ncategory: logging\nkeywords: [logging, console, structured, scoped, transports, remote-logging, levels, namespaces, lazy-bindings]\nrelated: [courier, herald, familiar]\nexports:\n [\n createLogger,\n defaultLogger,\n consoleTransport,\n remoteTransport,\n jsonTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n pipe,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\n RuneError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"rune\" />\n\n## Why Rune?\n\nPlain `console.log` lacks structure: no log levels, no namespacing, no remote delivery, no way to silence logs in production.\n\n```ts\n// Before — manual approach\nconst path = '/users';\nconsole.log(`[api] GET ${path}`);\nfetch('/api/logs', { body: JSON.stringify({ level: 'error', path }), method: 'POST' });\n\n// After — Rune\nimport { consoleTransport, createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst api = createLogger({\n namespace: 'api',\n transports: [\n consoleTransport({ level: 'debug' }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ],\n});\n\napi.info({ method: 'GET', path }, 'request');\n```\n\n| Feature | Rune | Winston | Pino | console |\n| -------------------- | ------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"rune\" type=\"size\" /> | ~44 kB | ~4 kB | 0 kB |\n| Browser support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Scoped loggers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Child | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Pluggable transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Built-in factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> Transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Streams | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Structured log entry | <ore-icon name=\"check\" size=\"16\"></ore-icon> `LogEntry` type | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lazy bindings | <ore-icon name=\"check\" size=\"16\"></ore-icon> `lazy(fn)` | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Styled output | <ore-icon name=\"check\" size=\"16\"></ore-icon> CSS badges | Text only | Text only | Manual |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (15+) | <ore-icon name=\"x\" size=\"16\"></ore-icon> (5+) | N/A |\n\n<div class=\"decision-callout\">\n\n**Use Rune when** you need isomorphic logging (browser + Node.js), namespaced module loggers, or remote error delivery without a heavy dependency chain.\n\n**Consider alternatives when** you need high-throughput file-based logging (Pino), file rotation (Winston), or your team already uses a logging framework.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/rune\n```\n\n```sh [npm]\nnpm install @vielzeug/rune\n```\n\n```sh [yarn]\nyarn add @vielzeug/rune\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { batchTransport, consoleTransport, createLogger, lazy, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'server',\n transports: [\n consoleTransport({ timestamp: true }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ],\n});\n\nconst requestLog = log.withBindings({\n diagnostics: lazy(() => ({ queueDepth: 0 })),\n requestId: 'abc-123',\n});\n\nrequestLog.info({ method: 'GET', path: '/users' }, 'request');\nconst users = await requestLog.time('load users', () => Promise.resolve(['user-1']));\nconsole.log(users);\n\nconst batch = batchTransport({ onFlush: (entries) => console.debug('batch', entries) });\nconst bufferedLog = createLogger({ transports: [batch.transport] });\n\nbufferedLog.info('queued for delivery');\nawait batch.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Level filtering (`debug` to `off`) with `enabled()` checks, including `fatal` above `error`\n- Immutable config after construction — use `child()` or `withBindings()` to scope\n- Three call forms: `log.info('msg')`, `log.error(err, { id }, 'msg')` (Error-first), or `log.info({ key: 'val' }, 'msg')` — Error-first form auto-serializes to `data.err`\n- `Error` values in context fields are also auto-serialized to `{ message, name, stack }` — survives JSON.stringify\n- Pinned context bindings via `withBindings({ requestId })` — fields on every line\n- Lazy bindings via `lazy(fn)` — expensive computations gated behind the level check\n- Namespaced child loggers via `createLogger('name')` or `logger.child({ namespace })`\n- Middleware pipeline via `use(fn)` — transform or filter entries before transport dispatch\n- Pluggable transport pipeline: `consoleTransport`, `remoteTransport`, `jsonTransport`, `batchTransport`, `sampleTransport`, `redactTransport`\n- Fan-out via `pipe()` — dispatch to multiple transports independently, fault-tolerant\n- Structured `time()` wrapper: emits the label as message with `{ duration_ms }` in context\n- `group()` and `groupCollapsed()` wrappers that auto-close on throw/reject\n- `LogEntry.data` — single merged flat object for transports; no manual merging needed\n- Zero dependencies — <PackageInfo package=\"rune\" type=\"size\" /> gzipped\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Courier](/courier/) — HTTP client with built-in request/response interception; pipe Rune as a transport to log every API call with structured context\n- [Herald](/herald/) — typed event bus; emit log-level change or flush events across modules without coupling loggers directly\n- [Familiar](/familiar/) — Web Worker pool; use Rune inside task functions to surface structured worker-side logs back to the main thread\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Rune — API Reference\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `defaultLogger` | Pre-created default logger singleton | — | Shared instance — use `child()` or `withBindings()` to scope |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `pipe()` | Fan-out dispatcher to multiple transports | Sync | Errors in one transport don't propagate to others |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| — | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `RuneError` | Base class for all `rune`-originated errors | — | Use `RuneError.is(err)` as the type guard |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `remoteTransport()` | Async HTTP/webhook delivery | Async | Handler errors are swallowed to `console.warn` |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `batchTransport()` | Buffered batch delivery with flush interval | Async | Await `.dispose()` and handle rejected delivery |\n| `sampleTransport()` | Probabilistic entry forwarding | Sync | `rate: 1` forwards all entries; `rate: 0` forwards none |\n| `redactTransport()` | Sensitive field stripping before forwarding | Sync | Place this closest to the remote transport, not console |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports — logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance — no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note — disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs — only the group header is suppressed.\n\n> **Note — transport/middleware fault isolation:** if a transport or middleware function throws, the logger catches it, reports it via a dev-only warning, and continues — a single misbehaving transport can never crash the caller of `log.info()`/etc., and sibling transports still receive the entry. A throwing middleware drops just that one entry.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nimport { consoleTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n remoteTransport({\n handler: async (_type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\n## defaultLogger\n\n`defaultLogger` is the pre-created default logger (`createLogger()` called once at module load).\n\nUse it as a quick-start singleton or create a child for module-level use:\n\n```ts\nimport { defaultLogger } from '@vielzeug/rune';\n\nconst log = defaultLogger.child({ namespace: 'app.worker' });\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods share the same signature:\n\n```ts\nlog.debug / info / warn / error / fatal(message: string): void\nlog.debug / info / warn / error / fatal(error: Error, context?: Bindings, message?: string): void\nlog.debug / info / warn / error / fatal(context: Bindings, message?: string): void\n```\n\nArgument rules:\n\n- String-only calls accept a single message argument.\n- **Error-first form:** pass an `Error` as the first argument — it is auto-serialized to `{ message, name, stack }` under the `err` key. Optionally follow with a `Bindings` object and/or a message string.\n- Context object comes first when providing structured data without a top-level Error. `Error` values inside the context object are also auto-serialized to `{ message, name, stack }`.\n\n```ts\nlog.error(err, 'request failed'); // err auto-serialized to data.err\nlog.error(err, { requestId }, 'request failed'); // err + context + message\nlog.error({ err: new Error('boom') }, 'failed'); // Error nested in context object\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ----------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n| `use(middleware)` | `Logger` | Appends a middleware function to the pipeline, returns new logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Does **not** auto-dispose batch transports — hold a reference and call `batchTransport.dispose()` on shutdown. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\nForwards entries asynchronously to a remote handler. Fire-and-forget — handler errors are swallowed to `console.warn` and never propagate to the caller.\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws or rejects. Default: a dev-only `console.warn`. Silent in production — provide an explicit handler for production observability. |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n remoteTransport({\n handler: async (_type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info({ path: '/users', status: 200 }, 'request');\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\nBuffers entries and delivers them in order. Flushes when the buffer reaches `maxSize` or after `interval` elapses; `flush()` and `dispose()` wait for accepted batch delivery.\n\n| Option | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch; implement retry here when successful retry must fulfill drain |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Observes delivery failure; matching `flush()` or later `dispose()` rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Finite interval in milliseconds greater than zero |\n| `maxSize` | `number` | `50` | Finite positive integer batch size before early flush |\n| `maxBuffer` | `number` | unbounded | Finite non-negative integer hard cap; oldest entries drop when exceeded |\n\nReturns a `BatchHandle` with:\n\n- `.transport` — the `Transport` function to pass to `createLogger({ transports: [handle.transport] })`.\n- `.flush()` — immediately send buffered entries and resolve after delivery; rejects when delivery fails.\n- `.dispose()` — stop the interval, reject new entries, and settle after every accepted batch completes. Rejects if any automatic or final delivery fails. Idempotent.\n- `.disposed` — `true` when disposal starts.\n- `[Symbol.asyncDispose]()` — delegates to `.dispose()`. Enables `await using` declarations.\n\nAfter `dispose()`, the transport becomes inert: new entries are silently dropped.\n\n**Returns:** `BatchHandle`\n\n**Example:**\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n interval: 10_000,\n maxSize: 100,\n onFlush: (entries) => console.debug('batch', entries),\n});\n\nconst log = createLogger({ transports: [batch.transport] });\n\nasync function shutdown() {\n await batch.dispose();\n}\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\nProbabilistically forwards entries to a downstream transport.\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required finite fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n sampleTransport({\n rate: 0.1,\n transport: remoteTransport({ handler: (_type, data) => console.debug('sampled log', data) }),\n }),\n ],\n});\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\nStrips sensitive fields from `bindings` and `context` before forwarding. Redaction is applied recursively at any depth (up to 20 levels).\n\n::: warning Key matching\n`keys` matches **exact field names** at any nesting depth. Dot-path notation (e.g. `'user.password'`) is **not** supported — use `'password'` to redact every field named `password` regardless of nesting.\n:::\n\n| Option | Type | Default | Description |\n| ------------- | ----------- | -------------- | ------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n redactTransport({\n keys: ['password', 'token', 'ssn'],\n transport: remoteTransport({ handler: (_type, data) => console.debug('redacted log', data) }),\n }),\n ],\n});\n```\n\n### pipe(...transports) / pipe(options, ...transports)\n\n```ts\npipe(...transports: Transport[]): Transport\npipe(options: PipeOptions, ...transports: Transport[]): Transport\n```\n\nDispatches each `LogEntry` to every transport in the list independently. An error thrown by one transport does not stop the others. Use in place of separate array entries when you want fault isolation or a shared error observer.\n\n`pipe()` with no arguments creates a valid no-op transport — useful for conditional pipeline construction: `pipe(condition ? remoteTransport(opts) : undefined!)` pattern, or simply as a placeholder during development.\n\n| Option | Type | Description |\n| --------- | ------------------------------------------- | --------------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called with the error and entry when any transport throws |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, pipe, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n pipe(\n { onError: (error) => console.warn('transport error', error) },\n consoleTransport(),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ),\n ],\n});\n```\n\n````\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n````\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()` — call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`) — lower is more verbose. Exported for transport/middleware authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Errors\n\n### RuneError\n\nBase class for all `rune`-originated errors. Use `instanceof RuneError` or `RuneError.is(err)` to catch any error the package throws.\n\n```ts\nimport { RuneError } from '@vielzeug/rune';\n\ntry {\n // ...\n} catch (err) {\n if (RuneError.is(err)) {\n // handle a rune-originated error\n }\n}\n```\n\n**Static methods:**\n\n| Method | Returns | Description |\n| ------------ | ----------------------- | --------------------------------------------- |\n| `is(err)` | `err is RuneError` | Type guard — `true` for `RuneError` and subclasses |\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'` — threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>` — Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context — already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### RemoteLogData\n\nPayload shape delivered to `RemoteTransportOptions.handler`:\n\n| Field | Type | Description |\n| ----------- | ------------------------------- | ----------------------------------------- |\n| `data` | `Bindings?` | Merged structured data (omitted if empty) |\n| `env` | `'production' \\| 'development'` | Runtime env marker |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string?` | Effective namespace |\n| `timestamp` | `string` | Full ISO timestamp |\n\n### PipeOptions\n\n| Field | Type | Description |\n| --------- | ------------------------------------------- | ----------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called when a transport in the pipe throws or rejects |\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>` — fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | ------------------ | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n| `middleware` | `LogMiddleware[]?` | `[]` | Entry transform/filter chain |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string): void;\n (error: Error, context?: Bindings, message?: string): void;\n (context: Bindings, message?: string): void;\n};\n```\n\nEvery log-level method uses this signature. Three call forms are supported:\n\n- **String-only:** `log.info('message')`\n- **Error-first:** `log.error(err, { requestId }, 'failed')` — `Error` is auto-serialized to `{ message, name, stack }` under `data.err`. Optionally follow with a `Bindings` object and/or a message string.\n- **Context-first:** `log.info({ key: 'value' }, 'message')` — structured context object, optional message. `Error` values nested inside the context are also auto-serialized.\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nMiddleware functions intercept entries before they reach transports. Return the (optionally mutated) entry to continue, or return `null` to drop the entry. Added via `use(fn)` or `RuneOptions.middleware`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### BatchHandle\n\n```ts\ntype BatchHandle = {\n [Symbol.asyncDispose]: () => Promise<void>;\n dispose: () => Promise<void>;\n readonly disposed: boolean;\n flush: () => Promise<void>;\n transport: Transport;\n};\n```\n\nReturned by `batchTransport()`. Pass `handle.transport` to `createLogger({ transports })`; await `handle.dispose()` during graceful shutdown. `disposed` is `true` when disposal starts.\n\n### Logger\n\nThe full interface returned by `createLogger()` and `defaultLogger`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n /** Returns a new child logger with additional pinned bindings. The returned logger is fully independent — disposing it does not affect the parent, and vice versa. */\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n### RemoteTransportOptions\n\n| Field | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### BatchTransportOptions\n\n| Field | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | --------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch (may be async) |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Observes delivery failure; matching `flush()` or later `dispose()` rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Finite interval in milliseconds greater than zero |\n| `maxSize` | `number` | `50` | Finite positive integer batch size before early flush |\n| `maxBuffer` | `number` | unbounded | Finite non-negative integer cap; drops oldest entries when exceeded |\n\n### SampleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required finite fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n### RedactTransportOptions\n\n| Field | Type | Default | Description |\n| ------------- | ----------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact at any depth |\n| `maxDepth` | `number` | `20` | Finite non-negative integer nesting depth. Fields deeper than this are not redacted — a dev-only warning is emitted when hit. **Security:** the warning is suppressed in production; ensure sensitive fields are not nested beyond this limit. |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\n",
@@ -39,42 +39,42 @@
39
39
  }
40
40
  ],
41
41
  "typeSignatures": {
42
- "BatchHandle": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
43
- "BatchTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
44
- "Bindings": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
45
- "JsonTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
46
- "LogEntry": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
47
- "LogLevel": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
48
- "LogMethod": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
49
- "LogMiddleware": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
50
- "Logger": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
51
- "LogType": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
52
- "PipeOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
53
- "RedactTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
54
- "RemoteLogData": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
55
- "RemoteTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
56
- "RuneOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
57
- "SampleTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
58
- "Transport": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
59
- "RuneError": "export { RuneError } from './errors';",
60
- "isLevelEnabled": "export { isLevelEnabled, PRIORITY } from './types';",
61
- "PRIORITY": "export { isLevelEnabled, PRIORITY } from './types';",
62
- "LazyBinding": "export type { LazyBinding } from './lazy';",
63
- "lazy": "export { lazy } from './lazy';",
64
- "defaultLogger": "export { defaultLogger, createLogger } from './logger';",
65
- "createLogger": "export { defaultLogger, createLogger } from './logger';",
66
42
  "ConsoleTheme": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
67
43
  "ConsoleThemeEntry": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
68
44
  "ConsoleTransportOptions": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
69
45
  "ResolvedTheme": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';",
70
- "DEFAULT_THEME": "export { DEFAULT_THEME, consoleTransport, resolveTheme } from './console';",
71
- "consoleTransport": "export { DEFAULT_THEME, consoleTransport, resolveTheme } from './console';",
72
- "resolveTheme": "export { DEFAULT_THEME, consoleTransport, resolveTheme } from './console';",
46
+ "consoleTransport": "export { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';",
47
+ "DEFAULT_THEME": "export { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';",
48
+ "resolveTheme": "export { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';",
49
+ "RuneError": "export { RuneError } from './errors';",
50
+ "LazyBinding": "export type { LazyBinding } from './lazy';",
51
+ "lazy": "export { lazy } from './lazy';",
52
+ "createLogger": "export { createLogger, defaultLogger } from './logger';",
53
+ "defaultLogger": "export { createLogger, defaultLogger } from './logger';",
73
54
  "batchTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
74
55
  "jsonTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
75
56
  "pipe": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
76
57
  "redactTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
77
58
  "remoteTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
78
- "sampleTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';"
59
+ "sampleTransport": "export { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';",
60
+ "BatchHandle": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
61
+ "BatchTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
62
+ "Bindings": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
63
+ "JsonTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
64
+ "LogEntry": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
65
+ "Logger": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
66
+ "LogLevel": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
67
+ "LogMethod": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
68
+ "LogMiddleware": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
69
+ "LogType": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
70
+ "PipeOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
71
+ "RedactTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
72
+ "RemoteLogData": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
73
+ "RemoteTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
74
+ "RuneOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
75
+ "SampleTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
76
+ "Transport": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
77
+ "isLevelEnabled": "export { isLevelEnabled, PRIORITY } from './types';",
78
+ "PRIORITY": "export { isLevelEnabled, PRIORITY } from './types';"
79
79
  }
80
80
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "apiSource": "export { toFilterPredicate, toSearchMatcher } from './adapters';\nexport { ScoutConfigurationError, ScoutDisposedError, ScoutError } from './errors';\nexport { findMatchRanges, highlight, highlightField } from './highlight';\nexport { createReactiveSearch, createSearch } from './reactive';\nexport type { ReactiveSearch } from './reactive';\nexport type { ScoutIndex } from './scout-index';\nexport { createIndex } from './scout-index';\nexport { segmentWords } from './segment';\nexport type {\n CreateSearchOptions,\n FieldDef,\n FieldMatch,\n HighlightPart,\n ScoutIndexOptions,\n SearchConstraints,\n SearchResult,\n SearchState,\n} from './types';\n",
2
+ "apiSource": "export { toFilterPredicate, toSearchMatcher } from './adapters';\nexport { ScoutConfigurationError, ScoutDisposedError, ScoutError } from './errors';\nexport { findMatchRanges, highlight, highlightField } from './highlight';\nexport type { ReactiveSearch } from './reactive';\nexport { createReactiveSearch, createSearch } from './reactive';\nexport type { ScoutIndex } from './scout-index';\nexport { createIndex } from './scout-index';\nexport { segmentWords } from './segment';\nexport type {\n CreateSearchOptions,\n FieldDef,\n FieldMatch,\n HighlightPart,\n ScoutIndexOptions,\n SearchConstraints,\n SearchResult,\n SearchState,\n} from './types';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Scout — Fast fuzzy search for TypeScript\ndescription: Trigram-indexed fuzzy search with per-field weights, match highlighting, and an optional reactive layer.\npackage: scout\ncategory: utilities\nkeywords: [fuzzy-search, search, trigram, full-text, filter, highlight, reactive, ripple]\nexports:\n [\n createIndex,\n createReactiveSearch,\n createSearch,\n ScoutConfigurationError,\n ScoutDisposedError,\n ScoutError,\n debugSearch,\n findMatchRanges,\n highlight,\n highlightField,\n segmentWords,\n toFilterPredicate,\n toSearchMatcher,\n ]\nrelated: [arsenal, sourcerer, vault, ripple]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"scout\" />\n\n## Why Scout?\n\nArsenal's `fuzzy` / `fuzzyFilter` helpers perform pairwise Levenshtein distance — O(n·m) per item per query. For ≤200 items they are fine. For 500–100k items with real-time keystrokes, you need an index.\n\nScout builds a **trigram inverted index** at construction time. Query time scores only items sharing a trigram with the query; broad queries can still approach O(n), while selective queries avoid scoring the whole corpus.\n\n```ts\n// Before\nconst matches = users.filter((user) => user.name.toLowerCase().includes(query.toLowerCase()));\n\n// After\nimport { createIndex } from '@vielzeug/scout';\n\nconst index = createIndex(users, { fields: ['name', 'email'] });\nconst matches = index.search(query);\n```\n\n| Feature | Arsenal `fuzzy*` | Scout `createIndex` | Fuse.js |\n| ------------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- |\n| Bundle size | ~3 KB | <PackageInfo package=\"scout\" type=\"size\" /> | ~23 KB |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> `@vielzeug/ripple` runtime dependency | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Algorithm | Levenshtein | Trigram + overlap coefficient | Bitap |\n| Query time | O(n·m) | O(candidates) | O(n·m) |\n| Stateful index | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Match highlighting | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Reactive layer | <ore-icon name=\"x\" size=\"16\"></ore-icon> | ripple signals + debounce | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Incremental updates | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n\n<div class=\"decision-callout\">\n\n**Use Scout when** you need search over 500+ items, real-time UI search boxes (combobox, command palette), or reactive query state with ripple signals.\n\n**Consider `arsenal.fuzzyFilter` when** you have fewer than 200 items and don't need a persistent index.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/scout\n```\n\n```sh [npm]\nnpm install @vielzeug/scout\n```\n\n```sh [yarn]\nyarn add @vielzeug/scout\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createIndex } from '@vielzeug/scout';\n\nconst users = [\n { email: 'ada@example.com', name: 'Ada Lovelace' },\n { email: 'grace@example.com', name: 'Grace Hopper' },\n];\n\nconst index = createIndex(users, {\n fields: [\n { field: 'name', weight: 2 },\n { field: 'email' },\n ],\n});\n\nconst results = index.search('ada');\nconsole.log(results[0]?.item.name); // Ada Lovelace\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createIndex()` — Trigram inverted index; construction O(corpus × field_length), query O(candidates)\n- Per-field weights — Promote `name` matches over secondary fields; finite positive weights and custom `stringify` functions supported\n- `createReactiveSearch()` — Index + reactive `SearchState` in one call; `.index` for incremental mutations\n- `createSearch()` — Reactive search state backed by an existing `ScoutIndex`; share one index across many states\n- `highlight()` / `highlightField()` — Split field text into `HighlightPart[]` fragments for styled rendering\n- `findMatchRanges()` — Compute match ranges for custom display strings (truncated previews, formatted values)\n- `toSearchMatcher()` — Matcher adapter for sourcerer's `LocalSource`\n- `toFilterPredicate()` — Snapshot `(item: T) => boolean` predicate for `Array.filter` or vault queries\n- `setItems()` — Reconcile a refreshed corpus by reference, preserve incoming order, and notify once\n- Incremental updates — `add()` / `remove()` / `reindex()` patch individual items in O(field_length)\n- `onMutate()` — Subscribe to index mutations; powers `createSearch()`'s reactivity and bulk reconciliation\n- `segmentWords()` — Split unsegmented-script text (CJK, Thai, ...) into words via native `Intl.Segmenter`\n- Debug logging via `debugSearch()` (`@vielzeug/scout/devtools`) — logs query/results transitions, tree-shaken from production bundles\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Arsenal](/arsenal/) — Use `fuzzyFilter` for ad-hoc filtering of small lists (< 200 items) without building an index\n- [Ripple](/ripple/) — `createReactiveSearch()` and `createSearch()` use Ripple signals for reactive query state and debounce\n- [Sourcerer](/sourcerer/) — use a `ScoutIndex` inside `createLocalSource`'s explicit `match` callback\n- [Vault](/vault/) — `toFilterPredicate()` wraps a one-time Scout query as a vault-compatible `filter()` predicate\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: Scout — API Reference\ndescription: Complete API reference for @vielzeug/scout — createIndex, createReactiveSearch, createSearch, highlight, highlightField, toSearchMatcher, toFilterPredicate.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ------------------------- | ----------------------------------------------------- | -------------- | ------------------------------------------------------------- |\n| `createIndex()` | Build trigram index from an item array | Sync | Index is built at call time — pass all initial items |\n| `ScoutIndex.search()` | Query the index, returns scored + highlighted results | Sync | Empty query returns all items with `score = 1` |\n| `ScoutIndex.add()` | Add one item to the index | Sync | No-op if same reference already indexed |\n| `ScoutIndex.remove()` | Remove one item by reference | Sync | No-op for unknown references |\n| `ScoutIndex.reindex()` | Re-index a mutated item in-place; preserves order | Sync | Call after mutating item properties; no-op if not in index |\n| `ScoutIndex.setItems()` | Reconcile a refreshed corpus in one mutation | Sync | Uses reference identity; duplicate references collapse |\n| `ScoutIndex.items` | All indexed items in insertion order | Sync | Returns a new array snapshot each call |\n| `ScoutIndex.onMutate()` | Subscribe to changed index mutations | Sync | A changed `setItems()` reconciliation emits once; no-ops emit nothing |\n| `createSearch()` | Reactive search state backed by a `ScoutIndex` | Sync | Requires `@vielzeug/ripple` — dispose when done |\n| `createReactiveSearch()` | One-call index + reactive search state | Sync | Exposes `.index` for incremental mutations |\n| `findMatchRanges()` | Compute match ranges for a text + query pair | Sync | Returns sorted, non-overlapping `[start, end]` ranges |\n| `highlight()` | Split text into highlighted/unhighlighted fragments | Sync | Ranges must be sorted and non-overlapping |\n| `highlightField()` | Highlight a named field from a `SearchResult` | Sync | Shorthand for the `matches.find(…).ranges → highlight()` pattern |\n| `toSearchMatcher()` | Adapt `ScoutIndex` to Sourcerer's `match` callback | Sync | Recomputes cached query matches after index mutation |\n| `toFilterPredicate()` | Snapshot predicate from a one-time query | Sync | Re-call when query or corpus changes |\n| `segmentWords()` | Split unsegmented-script text (CJK, Thai, ...) into words | Sync | Uses native `Intl.Segmenter` — not applied inside `tokenize()` itself (see Pitfalls) |\n| `debugSearch()` | Log a `SearchState`'s query/results transitions | Sync | Import from `@vielzeug/scout/devtools`, not the main entry point |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/scout` | All exports — index/search/highlighting/adapters, `ScoutConfigurationError`, `ScoutDisposedError`, `ScoutError`, and all types |\n| `@vielzeug/scout/devtools` | `debugSearch` — reactive search state logger (dev only) |\n\n---\n\n## `createIndex(items, options)`\n\nBuilds a trigram inverted index from `items`. Construction is O(corpus × field_length); subsequent `search()` calls are O(candidates).\n\n```ts\nfunction createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T>\n```\n\n**Parameters**\n\n| Param | Type | Description |\n| --- | --- | --- |\n| `items` | `T[]` | Initial corpus to index. |\n| `options.fields` | `ReadonlyArray<FieldDef<T>>` | Fields to index. Required; at least one entry. |\n| `options.threshold` | `number` | Finite overlap score in `0..1` (default `0.2`). |\n| `options.limit` | `number` | Finite non-negative integer max results (default `50`). |\n| `options.minQueryLength` | `number` | Finite positive integer min chars before trigram scoring; shorter queries use O(n) containment scan (default `3`). |\n\n**Example**\n\n```ts\nimport { createIndex } from '@vielzeug/scout';\n\nconst products = [\n { sku: 'WGT-001', title: 'Widget Pro' },\n { sku: 'GAD-002', title: 'Gadget Plus' },\n];\n\nconst index = createIndex(products, {\n fields: [\n { field: 'title', weight: 2 },\n { field: 'sku' },\n ],\n threshold: 0.25,\n limit: 20,\n});\n```\n\n---\n\n## `ScoutIndex<T>`\n\nReturned by `createIndex()`.\n\n### `.search(query, options?)`\n\n```ts\nsearch(query: string, options?: SearchConstraints): SearchResult<T>[]\n```\n\nReturns results sorted by score descending. Empty query returns all items with `score = 1`. Results below `threshold` are excluded; at most `limit` results are returned.\n\n```ts\nconst results = index.search('alice');\n// [{ item, score, matches }]\n```\n\n### `.add(item)`\n\nAdds `item` to the index. No-op if the same reference is already indexed. O(field_length).\n\n### `.remove(item)`\n\nRemoves `item` by reference equality. No-op if not found. O(field_length).\n\n### `.reindex(item)`\n\nRe-reads the item's current field values and rebuilds its index entry in-place, updating only fields whose values changed. Preserves insertion order. No-op if the item is not in the index.\n\n```ts\nitem.name = 'new name';\nindex.reindex(item);\n```\n\n### `.setItems(items)`\n\n```ts\nsetItems(items: readonly T[]): void\n```\n\nReconciles the index to a refreshed corpus in one mutation. Existing references are reindexed, missing references are removed, added references are indexed, and incoming first-occurrence order becomes index order. Duplicate references collapse to one item. Calls `onMutate()` once when indexed values, membership, or order changes.\n\n```ts\nindex.setItems(latestUsers);\n```\n\n### `.size`\n\n`number` — current number of indexed items.\n\n### `.items`\n\n`readonly T[]` — all indexed items in insertion order. Returns a new array snapshot each call.\n\n```ts\nconst all = index.items;\n```\n\n### `.onMutate(listener)`\n\n```ts\nonMutate(listener: () => void): () => void\n```\n\nSubscribes `listener` to run after every changed `add()` / `remove()` / `reindex()` / `setItems()` operation. No-ops, including unchanged bulk reconciliation, do not fire it. A changed `setItems()` reconciliation fires once. `createSearch()` uses this internally to keep `results` in sync with index mutations; most callers building on `createIndex()` directly will not need it.\n\n```ts\nconst unsubscribe = index.onMutate(() => {\n console.log(`Index changed — now ${index.size} items`);\n});\n\nindex.add(newUser); // logs \"Index changed — now 6 items\"\nunsubscribe();\n```\n\n---\n\n## `createSearch(index, options?)`\n\nWraps a `ScoutIndex` in a reactive search state powered by `@vielzeug/ripple` signals.\n\n```ts\nfunction createSearch<T>(index: ScoutIndex<T>, options?: CreateSearchOptions): SearchState<T>\n```\n\n**Parameters**\n\n| Param | Type | Description |\n| --- | --- | --- |\n| `options.debounce` | `number` | Finite non-negative integer milliseconds before query commit (default `200`). Pass `0` for immediate updates. |\n| `options.limit` | `number` | Finite non-negative integer override of index-level limit. |\n| `options.threshold` | `number` | Finite `0..1` override of index-level threshold. |\n| `options.minQueryLength` | `number` | Finite positive integer override of index-level minimum query length. |\n\n**Returns `SearchState<T>`**\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `query` | `Signal<string>` | Writable search query. Set `.value` to trigger search. |\n| `results` | `Computed<SearchResult<T>[]>` | Reactive results, updated after debounce. |\n| `isSearching` | `Computed<boolean>` | `true` during the debounce window. |\n| `clear()` | `() => void` | Resets query, cancels debounce, clears results synchronously. |\n| `dispose()` | `() => void` | Releases all reactive subscriptions. |\n| `[Symbol.dispose]()` | `() => void` | `using`-compatible disposal. |\n\n**Example**\n\n```ts\nimport { createIndex, createSearch } from '@vielzeug/scout';\nimport { effect } from '@vielzeug/ripple';\n\nconst users = [{ name: 'Ada Lovelace' }, { name: 'Grace Hopper' }];\nconst index = createIndex(users, { fields: ['name'] });\nconst search = createSearch(index, { debounce: 150 });\n\neffect(() => {\n console.log(search.results.value.map((result) => result.item.name));\n});\n\nsearch.query.value = 'ada';\n```\n\n---\n\n## `createReactiveSearch(items, options)`\n\nCreates a `ScoutIndex` and a reactive `SearchState` in one call — the shorthand for `createIndex` + `createSearch`. Returns a `ReactiveSearch<T>` which extends `SearchState<T>` with a `.index` property for incremental mutations.\n\n```ts\nfunction createReactiveSearch<T>(\n items: T[],\n options: ScoutIndexOptions<T> & { debounce?: number },\n): ReactiveSearch<T>\n```\n\n**Parameters**\n\n| Param | Type | Description |\n| --- | --- | --- |\n| `items` | `T[]` | Initial corpus to index. |\n| `options.fields` | `ReadonlyArray<FieldDef<T>>` | Fields to index. Required. |\n| `options.debounce` | `number` | Finite non-negative integer debounce milliseconds (default `200`). |\n| `options.threshold` | `number` | Finite overlap score in `0..1` (default `0.2`). |\n| `options.limit` | `number` | Finite non-negative integer max results (default `50`). |\n| `options.minQueryLength` | `number` | Finite positive integer min chars before trigram scoring (default `3`). |\n\n**Returns `ReactiveSearch<T>`** — all `SearchState<T>` members plus:\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `index` | `ScoutIndex<T>` | The underlying index for `add`, `remove`, `reindex`. |\n\n**Example**\n\n```ts\nimport { createReactiveSearch } from '@vielzeug/scout';\nimport { effect } from '@vielzeug/ripple';\n\nconst users = [{ email: 'ada@example.com', name: 'Ada Lovelace' }];\nconst search = createReactiveSearch(users, {\n fields: [{ field: 'name', weight: 2 }, 'email'],\n debounce: 150,\n});\n\neffect(() => console.log(search.results.value.map((result) => result.item.name)));\n\nsearch.index.add({ email: 'grace@example.com', name: 'Grace Hopper' });\nsearch.dispose();\n```\n\n---\n\n## `findMatchRanges(text, query)`\n\nNormalizes raw `query` with Scout's tokenizer, then computes sorted, non-overlapping literal ranges for each normalized token within `text`. Useful when you need to apply highlighting to a different string than the indexed field value (e.g. a truncated preview or a differently formatted display string).\n\n```ts\nfunction findMatchRanges(text: string, query: string): [number, number][]\n```\n\n**Example**\n\n```ts\nimport { findMatchRanges, highlight } from '@vielzeug/scout';\n\nconst ranges = findMatchRanges('Alice Johnson', 'alice!');\n// [[0, 5]]\n\nconst parts = highlight('Alice Johnson', ranges);\n// [{ text: 'Alice', highlighted: true }, { text: ' Johnson', highlighted: false }]\n```\n\nReturns an empty array if either `text` or `query` is empty.\n\n---\n\n## `highlight(text, ranges)`\n\nSplits `text` into `HighlightPart[]` fragments based on `ranges` from `FieldMatch.ranges`.\n\n```ts\nfunction highlight(text: string, ranges: [number, number][]): HighlightPart[]\n```\n\n**Example**\n\n```ts\nimport { highlight } from '@vielzeug/scout';\n\nhighlight('Hello World', [[0, 5]]);\n// [{ text: 'Hello', highlighted: true }, { text: ' World', highlighted: false }]\n```\n\nReturns an empty array when `text` is empty. Returns a single unhighlighted part when `ranges` is empty.\n\n---\n\n## `highlightField(result, field, text)`\n\nConvenience shorthand that finds the match ranges for `field` in `result.matches` and calls `highlight()` in one step. Eliminates the manual `result.matches.find(m => m.field === …).ranges` lookup.\n\n```ts\nfunction highlightField<T>(result: SearchResult<T>, field: keyof T & string, text: string): HighlightPart[]\n```\n\n**Example**\n\n```ts\nimport { createIndex, highlightField } from '@vielzeug/scout';\n\nconst users = [{ name: 'Alice Johnson' }];\nconst index = createIndex(users, { fields: ['name'] });\n\nfor (const result of index.search('alice')) {\n const parts = highlightField(result, 'name', result.item.name);\n console.log(parts.map((part) => part.highlighted ? `[${part.text}]` : part.text).join(''));\n}\n```\n\nWhen the field has no match (e.g. the query matched via a different field), returns a single unhighlighted part.\n\n---\n\n## `toSearchMatcher(index, options?)`\n\nReturns an `(item, query) => boolean` matcher compatible with `sourcerer`'s `match` option.\n\n```ts\nfunction toSearchMatcher<T>(index: ScoutIndex<T>, options?: SearchConstraints): (item: T, query: string) => boolean\n```\n\nOne matching-item set is cached per query and index revision, so filtering does not repeat index work per item and stays current after index mutation.\n\n```ts\nimport { createIndex, toSearchMatcher } from '@vielzeug/scout';\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst users = [{ email: 'ada@example.com', name: 'Ada Lovelace' }];\nconst index = createIndex(users, { fields: ['name', 'email'] });\nconst source = createLocalSource(users, { match: toSearchMatcher(index) });\n```\n\n---\n\n## `toFilterPredicate(index, query, options?)`\n\nReturns a `(item: T) => boolean` predicate computed from a one-time query. Use with `Array.filter` or vault's `query.filter()`.\n\n```ts\nfunction toFilterPredicate<T>(\n index: ScoutIndex<T>,\n query: string,\n options?: SearchConstraints,\n): (item: T) => boolean\n```\n\nThe predicate is a snapshot — re-call `toFilterPredicate` if the query or corpus changes.\n\n```ts\nimport { createIndex, toFilterPredicate } from '@vielzeug/scout';\n\nconst products = [{ title: 'Widget Pro' }, { title: 'Gadget Plus' }];\nconst index = createIndex(products, { fields: ['title'] });\nconst results = products.filter(toFilterPredicate(index, 'widget'));\n\nconst top5 = products.filter(toFilterPredicate(index, 'widget', { limit: 5 }));\n```\n\n---\n\n## `segmentWords(text)`\n\nSplits `text` into whitespace-joined word segments using the runtime's native `Intl.Segmenter` — no dependency beyond the platform API. Falls back to returning `text` unchanged where `Intl.Segmenter` isn't available.\n\n```ts\nfunction segmentWords(text: string): string\n```\n\n`tokenize()`'s trigram-based scoring already works on unsegmented scripts (Chinese, Japanese, Thai, ...) without this — trigrams are generated per-character, not per-word. `segmentWords()` is for `findMatchRanges()` / highlighting and the multi-word query semantics on `SearchConstraints`, which assume space-separated words. **Not applied inside `tokenize()` itself** — benchmarked at ~15x slower than the plain regex path for the common whitespace-delimited case, which would regress `createIndex()`'s construction cost for every caller, not just those indexing unsegmented scripts.\n\n**Example**\n\n```ts\nimport { createIndex, segmentWords } from '@vielzeug/scout';\n\nconst documents = [{ title: '日本語を勉強しています' }];\nconst index = createIndex(documents, {\n fields: [{ field: 'title', stringify: (value) => segmentWords(String(value)) }],\n});\n```\n\n---\n\n## `debugSearch(search)` <Badge type=\"tip\" text=\"@vielzeug/scout/devtools\" />\n\n```ts\ndebugSearch<T>(search: SearchState<T>): () => void\n```\n\nLogs `query` → `isSearching` → `results` transitions of a `SearchState` to `console.debug`. Returns a function that unsubscribes all listeners installed by this call. Import from the dedicated sub-path so it's tree-shaken from production bundles.\n\n::: warning Development only\nLogs the full, literal search query string — if your queries may carry PII (names, emails, medical/financial terms typed by end users), don't enable this in production.\n:::\n\n**Example**\n\n```ts\nimport { createIndex, createSearch } from '@vielzeug/scout';\nimport { debugSearch } from '@vielzeug/scout/devtools';\n\nconst index = createIndex([{ name: 'Ada Lovelace' }], { fields: ['name'] });\nconst search = createSearch(index);\nconst stopDebugging = debugSearch(search);\n\nsearch.query.value = 'alice';\n// [scout:search] query -> \"alice\"\n// [scout:search] isSearching -> true\n// [scout:search] isSearching -> false\n// [scout:search] results -> 1 item(s)\n\nstopDebugging();\n```\n\n---\n\n## Types\n\n### `SearchConstraints`\n\nShared search-tuning knobs used by `ScoutIndexOptions`, `CreateSearchOptions`, and all search functions.\n\n```ts\ntype SearchConstraints = {\n limit?: number; // finite non-negative integer; default 50\n minQueryLength?: number; // finite positive integer; default 3\n threshold?: number; // finite 0..1 value; default 0.2\n};\n```\n\n### `FieldDef<T>`\n\n```ts\ntype FieldDef<T> =\n | (keyof T & string)\n | {\n field: keyof T & string;\n weight?: number; // default 1\n stringify?: (value: unknown) => string;\n };\n```\n\n### `ScoutIndexOptions<T>`\n\n```ts\ntype ScoutIndexOptions<T> = SearchConstraints & {\n fields: ReadonlyArray<FieldDef<T>>;\n};\n```\n\n### `CreateSearchOptions`\n\n```ts\ntype CreateSearchOptions = SearchConstraints & {\n debounce?: number; // finite non-negative integer; default 200\n};\n```\n\n### `SearchResult<T>`\n\n```ts\ntype SearchResult<T> = {\n item: T;\n matches: FieldMatch<keyof T & string>[]; // literal normalized-token ranges; may be empty for fuzzy-only results\n score: number; // [0, 1]; 1 when query is empty\n};\n```\n\n### `FieldMatch<F>`\n\nGeneric over the union of field names — `match.field` is typed to the actual fields of `T`.\n\n```ts\ntype FieldMatch<F extends string = string> = {\n field: F;\n ranges: [number, number][]; // literal normalized-token [start, end] ranges in original field value\n};\n```\n\n### `HighlightPart`\n\n```ts\ntype HighlightPart = {\n highlighted: boolean;\n text: string;\n};\n```\n\n### `SearchState<T>`\n\nSee `createSearch()` above.\n\n### `ReactiveSearch<T>`\n\n```ts\ntype ReactiveSearch<T> = SearchState<T> & {\n readonly index: ScoutIndex<T>;\n};\n```\n\nSee `createReactiveSearch()` above.\n\n---\n\n## Errors\n\n### `ScoutError`\n\nBase class for all scout errors. Use `instanceof ScoutError` or `ScoutError.is()` to catch any scout-originated error.\n\n```ts\nclass ScoutError extends Error {\n static is(err: unknown): err is ScoutError;\n}\n```\n\n**Named subclasses**\n\n| Class | Thrown when |\n| ------------------- | ---------------------------------------------------------------------- |\n| `ScoutConfigurationError` | An index, search, or reactive search receives invalid fields or numeric options |\n| `ScoutDisposedError` | A method is called on a disposed `SearchState` instance |\n",
@@ -42,9 +42,9 @@
42
42
  "findMatchRanges": "export { findMatchRanges, highlight, highlightField } from './highlight';",
43
43
  "highlight": "export { findMatchRanges, highlight, highlightField } from './highlight';",
44
44
  "highlightField": "export { findMatchRanges, highlight, highlightField } from './highlight';",
45
+ "ReactiveSearch": "export type { ReactiveSearch } from './reactive';",
45
46
  "createReactiveSearch": "export { createReactiveSearch, createSearch } from './reactive';",
46
47
  "createSearch": "export { createReactiveSearch, createSearch } from './reactive';",
47
- "ReactiveSearch": "export type { ReactiveSearch } from './reactive';",
48
48
  "ScoutIndex": "export type { ScoutIndex } from './scout-index';",
49
49
  "createIndex": "export { createIndex } from './scout-index';",
50
50
  "segmentWords": "export { segmentWords } from './segment';",