# Introduction > What mycl is, what it is not, and who reaches for it. import introTax from '../../../examples/intro-tax.ts?raw'; mycl lets you modify a TypeScript function's behavior without touching its declaration or its call sites. A **capability** is a wrapped function whose behavior a caller can re-bind per **scope**: swapping, wrapping or layering functionality transparently to the call site. Once declared, a capability becomes its own registry key, type contract and default implementation. When you call a capability, it dispatches through its active scope and resolves its own binding there. The scope's **registries** compose when you activate them through a `mycl()` factory call, bound to a private **channel** that encapsulates your capabilities. ## What mycl is not **Not a DI container.** DI containers wire a graph of objects at startup and hand you instances by type or token. mycl binds behavior to a scope on the call stack; there is no container, no lifetime, and no construction graph. You re-bind functions, not assemble services. **Not an effect system.** It does not track effects in the type system, thread a runtime, or ask you to restructure programs around a monad. A capability is a plain function; calling it looks like calling any function. **Not monkey-patching.** Monkey-patching mutates a shared module export for the whole process. A registry binding is immutable, and it only applies inside the scope you activate. The same capability keeps its default in every other scope. ## Who it's for **Library authors** who want extension points without designing a plugin API. Wrap the functions you want consumers to be able to change in `capable()`, and the capability surface *is* the extension surface: consumers write bindings instead of asking you for new config knobs. **Application developers** who need behavior that varies by context: tests that swap I/O without mocks, per-request flags on a server, or platform-specific implementations bound once at the entry point instead of branching throughout the code. ## Where next - [Installation](/getting-started/installation/): add mycl and set up the `./capabilities` convention. - [Quick Start](/getting-started/quick-start/): 60 seconds to your first override. - [Capabilities](/guide/capabilities/): the full story on defining and calling them. - [Prior Art](/advanced/concepts/): the deeper design rationale behind the mechanism. --- # Installation > Install mycl in an app or a library, mint your channel, and set up the ./capabilities convention. ## Initial configuration Install the package: ```bash pnpm add @mycl/core ``` Then mint your channel once, in one module, and export the bound surface: ```ts // src/kernel.ts: the capability kernel import { createFnChannel } from '@mycl/core'; export const { capable, snapshot, channel, mycl, scope } = createFnChannel('my-app-or-library'); ``` Everything else in your codebase imports from `./kernel`. The channel is yours alone: mycl ships no ambient default, so nothing you mint can collide with another library's capabilities, and theirs cannot see yours. Building an application? You're done: head to the [Quick Start](/getting-started/quick-start/). The rest of this page is for library authors. ## Library exports The same wiring, embedded: take `@mycl/core` as a dependency. Regular keeps it fully invisible: it rides along inside your install, and consumers never see it. Peer shares one copy across the libraries in an app, at a cost: `@mycl/core` surfaces in the consumer's own install, a version constraint they now satisfy and manage for a package they never import. Channels stay disjoint either way, so the trade is packaging, not privacy. In both cases consumers never need to import from `@mycl/core` directly: your public entry point should re-export whatever they need under your own name. ```ts // my-library/src/index.ts: the library export export { registry } from '@mycl/core'; // plugin-author surface, re-exported as yours ``` Your channel is private by construction, so other mycl copies in a consumer's realm are harmless: their channels are disjoint from yours, and duplicate copies of your own library each work independently too. The one hazard is a consumer whose tree resolves *your library* twice and mixes tokens across the copies. No channel name can bridge them: the name is a label, never a key, so two copies minting `'my-library'` still hold two disjoint channels (the same fact that keeps strangers from reaching your channel by guessing its name). That is a packaging problem; for a loud startup signal, see the self-report recipe in [Duplicate detection](/advanced/duplicate-detection/#recipe-self-report-your-own-copy). To accept extensions from the outside, take registries through your entry point (an option, a method, whatever fits your API) and pass them to your `mycl`/`scope`, which compose them for you. What that seam looks like is your design; mycl only supplies the mechanism. The mechanical half is small: ```ts // my-library/src/index.ts: one possible seam import type { Registry } from '@mycl/core'; import { mycl } from './kernel'; import { buildApi } from './api'; export const createMyLibrary = (...extensions: Registry[]) => mycl(buildApi, ...extensions)(); ``` For the end-to-end version, capabilities export and all, see the [Extend a function from outside](/recipes/hero/) recipe. ### The `./capabilities` convention A package that exposes capabilities should export them from a `./capabilities` subpath. Add the export, with explicit `types` conditions so the typed contract resolves under every consumer's module-resolution mode; on this subpath the contract is the product: ```json { "name": "my-library", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./capabilities": { "types": "./dist/capabilities.d.ts", "default": "./dist/capabilities.js" } } } ``` The file re-exports the `capable()`-wrapped functions, which are used as registry keys when binding external functionality. Only the exported functions can be rebound: unexported capabilities stay internal to the library. ```ts // src/capabilities.ts export { fetchUser, sendEmail, renderTemplate } from './impl'; ``` Consumers import from `my-library/capabilities` to get the capability objects, then write bindings against them. Keeping capabilities on one predictable subpath makes the extension surface easy to find and lets separate contributors bind the same capabilities without coordinating. ## Where next - [Quick Start](/getting-started/quick-start/): 60 seconds to your first override. - [Duplicate detection](/advanced/duplicate-detection/): the two guards, and when to self-report. --- # Quick Start > Mint a channel, define a capability, override it per scope, and hand out instances from a factory, all in about a minute. import quickStartDefine from '../../../examples/quick-start-define.ts?raw'; import quickStartOverride from '../../../examples/quick-start-override.ts?raw'; import quickStartFactory from '../../../examples/quick-start-factory.ts?raw'; Three steps to your first override. Each block below runs. Edit the code and it re-runs live. Everything in mycl happens on a [**channel**](/guide/channels/): a private namespace you mint once with `createFnChannel(name)`. It returns your kernel: `capable`, `snapshot`, `mycl`, `scope`, and the `channel` token, all bound to your channel. Your channel is yours alone; other mycl users in the same app dispatch through their own, without coordination. ## 1. Define a capability `capable(fn, idPath)` wraps a function and gives it a stable **identifier**: your channel's name prefixed onto the **identifier path** you pass. The identifier is a label, for types, errors, and introspection; dispatch keys on the capability object itself, so two `capable()` calls mint two distinct capabilities even with the same path (the duplicate label just trips a dev warning). A capability always resolves through an active **scope** when you call it. App logic runs through a **factory** built with `mycl()`; with no registries its scope is empty, so the default implementation fires. :::caution[Scope must be active] A capability called outside any active scope throws rather than silently misdispatching, and a scope does not survive an `await`. Step 3's `snapshot()` and [Snapshots & Async](/guide/snapshots-and-async/) cover the async story. ::: ## 2. Override it in a scope Collect bindings in a **registry**. `.layer(cap, replacement)` binds a new behavior for step 1's `price` capability (each block on this page is self-contained, so it re-declares what it uses). The call site is identical. The active scope decides which behavior runs: hand the registry to `mycl()` and everything inside the factory dispatches through it. ## 3. Keep the factory, hand out instances `mycl(make, ...registries)`, the function the library is named after, resolves the registries once and returns the factory as a frozen callable you can keep and reuse. Each call runs your `make` **builder** inside that resolved scope and its return value comes back verbatim. Anything `make` returns that is called *later* (after `make` finishes) must be wrapped in `snapshot()` so it replays the scope that was active while `make` ran. ## Where next - [Capabilities](/guide/capabilities/): defaults, identifiers, and calling capabilities. - [Registries and layers](/guide/registries-and-layers/): binding, augmenting, and composing. - [Factories](/guide/factories/): building instances with `mycl()`, and why `snapshot()` is needed. --- # Channels > A channel is a private capability namespace with its own scope context: mint one kernel per app or library, and nothing outside it can see in. import channelsMint from '../../../examples/channels-mint.ts?raw'; import channelsDisjoint from '../../../examples/channels-disjoint.ts?raw'; Everything in mycl happens on a **channel**: a private namespace you mint once with `createFnChannel(name)`. The call returns your **kernel**, bound to that channel: `capable`, `snapshot`, `mycl`, `scope`, and the `channel` token. The rest of this guide is about what those members do; this page is about the namespace they share. ## Mint once per project A channel is meant to be minted exactly once, in a module of its own, with the kernel exported for the rest of the codebase: ```ts // src/kernel.ts: the app's capability kernel import { createFnChannel } from '@mycl/core'; export const { capable, snapshot, channel, mycl, scope } = createFnChannel('my-app-or-library'); ``` Everything else imports from `./kernel`. A library does exactly the same behind its own entry point; see [Installation](/getting-started/installation/#library-exports). ## A name never connects channels Calling `createFnChannel('my-app-or-library')` twice gives two unrelated channels that merely share a label: there is no channel registry and no name-based rendezvous, so nothing can reach or join your channel by knowing its name. Sharing a channel happens the ordinary JavaScript way, by importing the kernel from the module that minted it, which is why the mint-once pattern above is the whole coordination story. What the name does do: it becomes the `name:` prefix of every identifier minted through the kernel's `capable` (`my-app-or-library:greet`), the label you see in error messages and introspection. Reusing a name elsewhere cannot break isolation, but it muddies those labels, and two same-named channels minting the same identifier path trip the dev duplicate-identifier warning ([error 13](/errors/13/)): the labels collide even though the channels never do. The name must not contain `:` or `/` (either is a type error, and in dev throws [error 12](/errors/12/)): `:` because the first `:` of an assembled identifier must split it unambiguously back into channel and path, and `/` because it is reserved as the segment separator inside identifier paths. Dots are fine (`my.kernel` is a valid name). ## Channels are disjoint A channel owns its own scope [context](/reference/glossary/#context). A scope made by one kernel installs bindings into that channel only, and capabilities minted on another channel keep reading their own. That is the isolation the embedded model rests on: nothing you mint can collide with another library's capabilities, and theirs cannot see yours, even at the same identifier path. The first factory is the proof: `overrides` carries a `widgetGreet` binding, and that registry is active while the factory body runs, yet the widgets channel never sees it, because `app.mycl` installed it into the `app` channel only. If channels shared a context, `widgetGreet()` would have printed the widget override; instead it fails loud, the way a missing scope always does. The second factory shows the registry was never the obstacle, and the general rule for crossing channels: factories compose. The widgets channel gets its own `mycl` factory, the app factory calls it, and each one installs the same registry into its own channel. ## The channel token `channel` is an opaque token proving which channel you hold. Its everyday job is context swapping: pass it to `setChannelContext` to replace how the channel carries its scope, for example installing `alsContext()` on Node so scopes survive `await` (see [Snapshots & Async](/guide/snapshots-and-async/)). Export the token from your kernel module only if you want that power available outside it. :::caution[One context, one channel] A context instance must back exactly one channel. Sharing one across channels merges their scope universes: bindings bleed between them, and the loud out-of-scope error stops firing, silently voiding the isolation shown above. Always mint a fresh context per channel (`alsContext()`, not a saved instance); installing a context that already backs another channel throws [error 16](/errors/16/) in dev. ::: ## Where next - [Capabilities](/guide/capabilities/): minting extension points through your kernel's `capable`. - [Scopes](/guide/scopes/): how a channel's active registry is chosen at call time. - [Building a Kernel](/advanced/building-a-kernel/): the connector level beneath `createFnChannel`, for kernels with a different shape. --- # Capabilities > A capability is one function that is three things at once, a callable default, a typed contract, and a registry key. import capabilitiesDefine from '../../../examples/capabilities-define.ts?raw'; import capabilitiesCompose from '../../../examples/capabilities-compose.ts?raw'; A **capability** is an ordinary function you wrap with `capable()`. That one function is three things at once: a callable default you can run today, a typed contract others bind against, and its own registry key. Reach for a capability wherever you want a function's behavior to be swappable per scope without editing its source or its call sites. ## Three things at once The wrapped `greet` above is, simultaneously: - **A callable default.** Call it inside a scope and, with nothing bound, the function you passed to `capable()` runs. Its call sites never change. - **A typed contract.** TypeScript infers the type of every binding and augment from the capability's own signature: a replacement with the wrong shape is a compile error, not a runtime surprise. - **Its own registry key.** The capability object itself is the key: `registry().layer(greet, replacement)` keys on `greet`. There is no separate token and no string lookup. ## Identifier: the path you pass, the id you get The second argument to `capable()` is the **identifier path**, a non-empty string. A flat name is enough for a private, single-owner channel; use `'project/capability'` when several packages share one channel, so their capabilities never collide. mycl prepends the [channel](/reference/glossary/#channel)'s name to form the capability's **identifier**, its canonical id. A capability belongs to the channel whose `capable` minted it: with `createFnChannel('my-app-or-library')`, the identifier path `'greet'` becomes the identifier `my-app-or-library:greet`, the name you see in error messages and introspection. ```ts capable(fn, idPath, config?) ``` Identifier rules: - **Mandatory and non-empty.** There is no anonymous capability; an empty path is a type error, and in dev throws when the capability is created. - **Any non-empty name works.** Channels are disjoint, so uniqueness only matters within your own channel. When several packages or feature areas share one channel, prefix a project segment (`users/fetch`, `billing/fetch`); further `/` sub-namespacing is fine. - **Stable once published.** The identifier is public: it is the error label, the introspection label, and the type-level layer-record key. Renaming a shipped capability's identifier is a breaking change, like renaming an exported symbol. ## Capabilities are plain functions Because a capability is just a function, you pass it, store it, and compose it like any other. One capability can call another, and they all dispatch through the same active scope. ## Configuring how bindings combine The optional third argument, `config`, carries a **layer strategy**: it controls how multiple bindings for the same capability combine (accumulate into a list, concatenate, and so on) instead of the default last-one-wins. Most capabilities never need it. See [Layer strategies](/guide/layer-strategies/). ## Where next - [Scopes](/guide/scopes/): how a capability finds its implementation at call time. - [Registries and layers](/guide/registries-and-layers/): collect, inspect, and compose bindings. - [Layer strategies](/guide/layer-strategies/): combine multiple bindings for one capability. --- # Scopes > A capability dispatches through the active scope; scope() binds a function to one, and calling a capability with no scope throws loud. import scopesBind from '../../../examples/scopes-bind.ts?raw'; import scopesOutside from '../../../examples/scopes-outside.ts?raw'; import scopesShadow from '../../../examples/scopes-shadow.ts?raw'; A **scope** is the resolved registry active on the call stack when a capability is called. That is the whole dispatch mechanism: there is no global registry and no ambient default. `scope(fn, registry?)` binds a function to a scope; when you call the returned function, that scope is active for the duration of the call. ## `scope(fn)` and `scope(fn, reg)` - **`scope(fn)`** with no registry binds an **empty scope**: every capability called inside `fn` falls back to its default. Use it when you want the real call path with no overrides. - **`scope(fn, reg)`** binds `reg`: capabilities inside `fn` dispatch through it. `reg` may be a registry, or a precomputed resolved registry. `scope` returns a new function and resolves the registry once. The scope is not active until you call that function, and the function is reusable, dispatching through the same resolved bindings on every call. ## Calling with no scope throws A capability with no scope on the stack has no table to consult and no safe default to assume, so it throws instead of guessing. This is deliberate: a silent wrong-scope dispatch (running a default you meant to override, or an override from an unrelated part of the call tree) is far harder to find than a loud failure at the call site. The error names the capability by its full identifier (`my-app-or-library:greet`), so a failure points to the exact capability that ran without a scope. ## Functions that outlive the scope The scope is synchronous: it is active while the scoped function runs and gone once it returns. A callback that is stored or returned and then called _later_ (after an `await`, in a `setTimeout`, or from a closure that escaped) runs with no scope and throws. Capture the scope with `snapshot()` so it replays when the callback later runs. See [Snapshots and async](/guide/snapshots-and-async/) for the full pattern. ## Scopes don't inherit: each function carries its own Scopes do not nest dynamically. A function wrapped by `scope()` carries the scope it captured and replays it, shadowing whatever scope is active where it is called. So the scope a capability sees is the one captured by the nearest `scope()` around the code that calls it, not the caller's scope. ## Where next - [Registries and layers](/guide/registries-and-layers/): build and compose the registries a scope dispatches through. - [Snapshots and async](/guide/snapshots-and-async/): carry a scope across async boundaries and escaping callbacks. - [Capabilities](/guide/capabilities/): what a scope is dispatching for. --- # Registries & Layers > A registry is an immutable collection of bindings: layer to add them, compose by passing several, and swap I/O in tests without a mock library. import registriesImmutable from '../../../examples/registries-immutable.ts?raw'; import registriesInspect from '../../../examples/registries-inspect.ts?raw'; import registriesMerge from '../../../examples/registries-merge.ts?raw'; import registriesTesting from '../../../examples/registries-testing.ts?raw'; A **registry** is an immutable collection of capability bindings. `registry()` makes an empty one; `.layer()` and `.augment()` each return a **new** registry with the binding added: the original is never touched. You build registries up, then hand them to `scope()` or `mycl()`. ## A layer is one binding `.layer(capability, value)` binds a single capability: one **layer**. Because every call returns a fresh registry, chaining `.layer(...).layer(...)` simply threads each new registry into the next call. Nothing mutates, so you can branch off a shared base and the branches stay independent. ## Inspecting a registry A registry has two read-only methods for introspection, useful in tests, tooling, and debugging: - **`.has(capability)`** returns whether the registry binds that capability. - **`.bindings()`** returns a `ReadonlyMap` of every binding, keyed by capability. ## Composing registries Registries compose: pass several to `mycl()` or `scope()` and they fold into one **resolved registry**, the form a scope dispatches against. When two registries bind the same capability, the later one wins; augments accumulate across all of them. Don't confuse composition with `.layer()`: `.layer()` adds one binding to one registry, while composition combines whole registries. (The primitive underneath, `merge`, lives on `@mycl/core/factory` for [kernel authors](/advanced/building-a-kernel/); the everyday surface composes for you.) ## Testing without mocks Immutability makes a registry the natural test seam. Keep the code under test exactly as it ships and bind a test registry that swaps its I/O capabilities for in-memory stand-ins. No mock library, no module patching: the capability surface is the seam. The example passes only because both swaps took: if the test registry missed `sendEmail`, the real default would throw. That is the seam doing its job. ## Where next - [Factories](/guide/factories/): bundle registries once and build instances with `mycl()`. - [Layer strategies](/guide/layer-strategies/): make multiple bindings for one capability combine instead of overwrite. - [Scopes](/guide/scopes/): how a resolved registry becomes active at call time. --- # Factories > mycl(make, ...registries) returns a frozen factory: registries resolve once at creation, the builder runs per call and its result comes back verbatim. import factoriesBuilder from '../../../examples/factories-builder.ts?raw'; import factoriesExtend from '../../../examples/factories-extend.ts?raw'; import factoriesPlatforms from '../../../examples/factories-platforms.ts?raw'; `mycl(make, ...registries)` bundles registries into a **factory**, a frozen callable. The registries resolve once, when `mycl()` is called. Each time you call the factory, your **builder** (`make`) runs inside that one resolved scope and whatever it returns (the **instance**) comes back verbatim. Reach for `mycl()` when you want to fix a scope up front and then hand out ready-to-use instances that dispatch through it. Two build lines print: the builder ran once per call. Both instances read `'09:00'` from the same resolved `clock`, and each carries its own `user`. The returned object is exactly what the builder produced. ## The instance comes back verbatim `mycl()` does not inspect, wrap, freeze, or scope-bind the builder's return. It runs `make`, and returns the result unchanged. That keeps the factory predictable: the shape you build is the shape callers get. The catch lives in the scope. The resolved scope is active only *while* `make` runs. A method on the instance that is called *later*, after the builder has returned, is outside that scope and would throw. Wrap any such method in `snapshot()` (as `stamp` above does) so it replays the build-time scope when it runs. See [Snapshots and async](/guide/snapshots-and-async/) for the full model. The factory itself is frozen and not extensible: you cannot bolt bindings onto it after creation. To add behavior, pass the factory back into `mycl()`. ## Extending a factory Pass an existing factory as the first argument to fold more registries onto it. The factory's own registries fold first; the ones you add fold on top and win on conflict. The original factory is untouched. `makeLoggingApp` reuses everything `makeApp` was built over and adds the augment on top. This is how features, plugins, or test overrides compose onto a base factory without editing its definition. See [Augmentation](/guide/augmentation/) for what `.augment()` does. ## Declaring requirements with `requires` Some capabilities have no usable default: their base throws or stubs (a `db` handle, the current user) and they only work once a registry binds them. `requires(...caps)(make)` declares those up front. It tags `make`'s **type** with the identifiers of the capabilities it needs, and returns `make` unchanged. `requires` is **compile-time only**: zero runtime effect, no wrapper, no allocation. The enforcement point is `mycl()`: passing registries that don't provide every required capability is a type error that names the missing one. Because this check cannot run (it fails at compile time, not at runtime), the example below is a static block rather than a live Playground. ```ts import { createFnChannel, registry, requires } from '@mycl/core'; const { capable, mycl } = createFnChannel('my-app-or-library'); type Db = { query(id: string): string }; // A required-effect seam: no usable base, so a registry MUST bind it. const db = capable((): Db => { throw new Error('no db bound'); }, 'db'); // requires(db)(make) tags make's TYPE with db's identifier and returns make // unchanged (zero runtime effect). The check fires later, at mycl(). const makeApp = requires(db)((id: string) => db().query(id)); // ✓ the registry provides db → compiles. mycl(makeApp, registry().layer(db, () => ({ query: (id) => `row ${id}` }))); // ✗ the registry does not provide db → mycl() is where it fails: mycl(makeApp, registry()); // ~~~~~~~~~~~~ Argument of type 'Registry' is not assignable to // parameter of type '"mycl: registry is missing required capability: my-app-or-library:db"'. // (That literal is produced by mycl's internal MissingCapabilities guard type.) ``` `requires` is opt-in: a `make` passed to `mycl()` *without* `requires` has an empty required set and is never checked, so the bare `mycl(make, ...registries)` form keeps working. Reserve it for required-effect seams (capabilities whose base throws), not for enrichment capabilities that already run fine unbound. ## One builder, many platforms Because the platform decision lives in a registry, the same builder can run anywhere. Keep the builder platform-agnostic (no `if (typeof window)` guards) and let the entry point pick the registry that binds the platform capability. `makeStore` never learns which platform it runs on. Swapping `serverBacking` for `localStorage` or an in-memory map is a registry change at the entry point. Everything the builder calls stays the same. ## Where next - [Augmentation](/guide/augmentation/): wrap what a capability resolves to, on top of a factory's registries. - [Snapshots and async](/guide/snapshots-and-async/): why builder methods called later must be wrapped in `snapshot()`. - [Type guarantees](/advanced/type-guarantees/): how `requires` and the type-level layers enforce completeness at compile time. --- # Augmentation > .augment(cap, wrapper) composes around whatever a capability resolves to, so you can observe, transform, or recover from it without replacing the implementation. import augmentationLogging from '../../../examples/augmentation-logging.ts?raw'; import augmentationStacking from '../../../examples/augmentation-stacking.ts?raw'; import augmentationAsync from '../../../examples/augmentation-async.ts?raw'; import augmentationCanary from '../../../examples/augmentation-canary.ts?raw'; `.augment(cap, wrapper)` wraps whatever a capability resolves to: the layered value if a `.layer()` bound one, otherwise the base. Unlike `.layer()`, an augment does not replace anything: it composes *around* the resolved implementation, which still runs inside it unless the wrapper deliberately skips it. Reach for augmentation to log, time, transform a result, or recover from a failure without touching the implementation or its call sites. ## The wrapper, and the helpers A `wrapper` receives `next` (the callable it wraps) and returns a new callable with the same signature. Call `next(...args)` to run the wrapped implementation; do anything you like before, after, or instead. ```ts const wrapper = (next) => (...args) => { // before... const result = next(...args); // after... return result; }; ``` You rarely write that by hand. Four helpers cover the common shapes. Import them from `@mycl/core/helpers`: - **`before(fn)`** runs `fn(...args)` before each call; its return value is ignored. Good for logging inputs, metrics, assertions. - **`after(fn)`** runs `fn(result, ...args)` after each call; the result is passed through unchanged. - **`pipe(...transforms)`** chains transforms left-to-right over the return value; each transform receives the previous one's output and returns the next. - **`handleError(handler, shouldHandle?, { rethrow }?)`** catches a throw (and an async rejection); `handler(error, ...args)` returns the fallback value. Optional `shouldHandle(error)` filters which errors are caught: the rest re-throw. `{ rethrow: true }` runs the handler for side effects only and re-throws the original error. ## Stacking order Augments nest like an onion. The **first** augment added sits closest to the implementation (innermost); each later augment wraps the previous ones. So the **last** augment added is outermost: its logic runs first on the way in and last on the way out. `outer` was added last, so it wraps `inner` and runs first. If a `.layer()` binding is present too, the augments wrap the layered value, not the base default. ## Async: `after` and `pipe` see the Promise, not the value :::caution[after and pipe are synchronous] `after` and `pipe` operate on the **raw return value**. For an async capability that value is the **Promise itself**, not the settled result. If you transform an async result with `pipe`, you are transforming the Promise object, almost never what you want. To act on the settled value, `await` it yourself inside a wrapper (or chain `.then`). `handleError` is the exception: it detects a returned Promise and catches its rejection. ::: ## Canary rollout with automatic fallback Augmentation is the tool for a cautious rollout. Layer a new implementation and augment it with `handleError` to fall back to the stable one on failure. If the new code misbehaves, the old path runs automatically: no redeploy to roll back. The fallback lives in the registry, not the implementation: drop the `.layer()` binding and the safety net is gone. `pay(50)` runs the canary. `pay(200)` throws inside the canary; `handleError` catches it (it is an `Error`, so `shouldHandle` passes) and returns `stable(200)`. The fallback calls `stable` directly rather than re-dispatching `charge`, which would loop straight back into the canary. ## Where next - [Layer strategies](/guide/layer-strategies/): the other way multiple bindings combine, folding a value rather than wrapping a call. - [Registries and layers](/guide/registries-and-layers/): where `.augment()` lives alongside `.layer()`. - [Snapshots and async](/guide/snapshots-and-async/): the scope model behind the async caveat above. --- # Layer Strategies > Attach a LayerStrategy to a capability: a left fold over its layers (accumulate a list, concatenate strings) instead of the default last-binding-wins. import strategiesLastwins from '../../../examples/strategies-lastwins.ts?raw'; import strategiesClasses from '../../../examples/strategies-classes.ts?raw'; By default, the **last** `.layer()` binding for a capability wins. Two layers for the same capability, and the later one replaces the earlier one whole. That is the right behavior for almost every capability: a replacement is a replacement. Sometimes you want the layers to *combine* rather than compete: a list that grows across layers, a set of CSS classes that concatenate, a config object that merges field by field. A **layer strategy** is how you say so. It travels with the capability, not with any registry, so every registry that layers the capability folds the same way. ## The `LayerStrategy` shape A layer strategy is a **left fold** over the capability's layers, the shape of `Array.prototype.reduce`: `step` is the reducer, folding each layer's **contributed value** (the value one `.layer()` call feeds in) into the accumulated value; `seed` is the optional initial accumulator; `extract` projects the final accumulated value into a callable. Two functions, and one optional seed: ```ts interface LayerStrategy { // Fold one layer's value into the value so far. step: (acc: V | undefined) => (...args: Args) => V; // Project the folded value into the capability's callable. extract: (value: V, base: T) => T; // Optional fold seed; omit it and the first `acc` is `undefined`. seed?: V; } ``` - **`step`** runs once per layer, in registration order. `acc` is the value folded so far and arrives as `V | undefined`: on the first layer there is nothing yet, so it is `undefined`. **The strategy owns that case.** You decide what "no prior value" means (start fresh, seed an empty array, and so on); there is no branching hidden in the resolver, and `seed` is optional. The returned value becomes the next `acc`. - **`extract`** runs once, after the last layer. It receives the final folded `value` and the capability's `base` function, and returns the callable dispatch will use. `extract` **must** return a function. If it returns a non-function, dispatch throws with a message naming the offending capability. - **`seed`** seeds the fold when you would rather not handle `undefined` in `step`. It is a seed, not a trigger: with no layers, the base still runs even when `seed` is set. `V` is the accumulated value type; `Args` is the tuple each `.layer()` call accepts. For an ordinary capability both collapse to the function itself, which is why the default last-wins needs no strategy at all: `step` keeps the newest value, `extract` returns it unchanged. ## Accumulating CSS classes Here a capability's value is a class string, and each layer contributes more classes. `step` space-joins them in layer order; `extract` turns the final string into the function `buttonClass()` calls. `theme` contributes `'bg-blue-600 text-white'`; `compact` contributes `'px-2 py-1'`. Neither is a replacement: the strategy folds both. With no layers at all, `buttonClass()` falls back to its base, `'btn'`. This is what a left fold buys: **composition is free**. Layers from composed registries fold as one sequence, exactly as if a single registry held them all, in registration order within a registry and composition order across registries. Two packages can contribute to the same capability without knowing about each other, and the result is still one deterministic fold. ## You hand-roll strategies, on purpose mycl ships **no** strategy library and no `defineStrategy` helper. A layer strategy is a plain object with two functions; you write the one your capability needs, inline, in a few lines (exactly the CSS example above). That is a deliberate line: the substrate ships the fold *mechanism*, and the policy (how *your* values combine) stays in your code, where you can read it. There is nothing to import and no catalogue to learn. ## Layer strategy vs. augment A layer strategy and an augment both let several bindings act on one capability, on different axes: - A **layer strategy** folds the *values* that layers contribute into one value. Reach for it when a capability accumulates data: a list, a string, a merged object. - An **augment** wraps the *callable* that resolution produces (logging, timing, error recovery) without changing the value. See [Augmentation](/guide/augmentation/). A layer strategy is also unrelated to registry composition: passing several registries to `mycl()`/`scope()` composes whole registries, while a strategy folds one capability's layers within whatever registry it lands in. ## Where next - [Augmentation](/guide/augmentation/): combine bindings by wrapping the call instead of folding the value. - [Registries and layers](/guide/registries-and-layers/): where `.layer()` collects the contributions a strategy folds. - [Capabilities](/guide/capabilities/): the `config` argument that carries a strategy. --- # Snapshots & Async > A scope lives on the synchronous stack, so async callbacks escape it, and snapshot(fn) pins the active scope into a function you can call later while, on Node, alsContext does it automatically. import snapshotsBoundary from '../../../examples/snapshots-boundary.ts?raw'; A [scope](/guide/scopes/) lives on the synchronous call stack. It is active while the scoped function runs and gone the instant control returns. So the code *after* an `await`, inside a `.then()`, or in any callback that outlives the scope body runs with **no scope on the stack**, and a capability called there throws the loud "called outside any registry scope" error. `snapshot(fn)` is the fix. Called while a scope is active, it captures that scope and returns a function that replays it on every later call, no matter when or where the call happens. Wrap it where the boundary is: the callback you hand to `setTimeout`, the function inside a `.then()`, the event handler you register, the method your `mycl()` builder returns for later use. Capture at the boundary; call whenever. ## `snapshot` captures whatever is active, including no scope `snapshot` pins the scope active *at call time*, and "no scope" is a valid thing to pin. Call `snapshot` with no scope on the stack and the returned function replays scopelessness: a capability inside it throws, rather than silently adopting whatever scope happens to be active when the function later runs. That is deliberate: a missed boundary fails loud at the call site instead of dispatching through a stranger's scope. To pin the defaults on purpose, take the snapshot inside a `scope(fn)` body with no registry. This is the counterpart to the [escaping-function rule](/guide/scopes/#functions-that-outlive-the-scope): `scope()` establishes a scope up front, `snapshot()` carries an already-active one across a boundary. ## Node.js: `alsContext` for automatic propagation On Node you can retire the manual snapshots entirely. Swap your channel's context for `alsContext()`, an `AsyncLocalStorage`-backed [`ScopeContext`](/reference/core/#scopecontextr) shipped from `@mycl/core/context`, and the scope propagates across `await`, `setTimeout`, and microtask boundaries on its own. This snippet needs Node built-ins the Playground can't run, so it is shown as static configuration. Wire it once at startup, wherever you minted the channel: ```ts import { setChannelContext } from '@mycl/core'; import { alsContext } from '@mycl/core/context'; import { channel } from './kernel'; // your createFnChannel('my-app-or-library') module setChannelContext(channel, alsContext()); ``` After this, every `scope()` and `mycl()` call for that channel uses `AsyncLocalStorage` as its backing store, and capabilities called after an `await` dispatch through the right scope automatically. `snapshot` still works. It just becomes optional. `alsContext` resolves `node:async_hooks` lazily, through `process.getBuiltinModule`, the first time it is *called*, not through a static import: the entry stays browser-safe to import even for consumers that never call it. That lazy lookup needs Node 20.16 or newer (or another runtime that provides `process.getBuiltinModule`); calling `alsContext()` where it is missing throws [error 14](/errors/14/). A `ScopeContext` is just `{ get, run }`, so if you need something other than the stack or `AsyncLocalStorage`, for example a test harness or a different async-tracking primitive, writing your own stays a few lines: see [Building a Kernel](/advanced/building-a-kernel/) for the `{ get, run }` contract and a hand-rolled example. ## Browser caveat Browsers have no `AsyncLocalStorage` (and the `zone.js`-style shims that emulate it are not part of mycl). In the browser, `snapshot` is the mechanism: capture at every async boundary. The `mycl()` habit of wrapping each builder method in `snapshot` covers the common cases: the method carries its scope wherever it is later called. ## Where next - [Scopes](/guide/scopes/): the synchronous scope model these snapshots carry. - [Factories](/guide/factories/): why `mycl()` builder methods are wrapped in `snapshot`. - [Augmentation](/guide/augmentation/): the async caveat behind `after` and `pipe`. --- # The Core Substrate > Why @mycl/core splits into a main entry (the everyday kernel) and substrate subpaths (/helpers, /context, /factory, /introspect), and how to read a registry at runtime. mycl is one package, `@mycl/core`, with five entry points. The main entry is the **everyday surface**: `createFnChannel` hands you a kernel with the factory ergonomics (`mycl`, `scope`, `requires`) already wired, plus `registry` and the guards, all in one import. The subpaths are the **substrate**: the mechanism layer underneath, the part that knows how a capability dispatches, how the layer fold runs, and how the type contracts hold, and nothing about *when* code runs or how a scope crosses an `await`. Most people import the main entry and never touch a subpath directly. Reach for one when you are building a build plugin, tooling that inspects registries, or a kernel with an execution model of its own: anything that needs the mechanism alone. ## Why the split exists The line is mechanism versus policy, drawn at the entry boundary rather than a package boundary. The main entry ships the everyday policy already decided (the stack context, wired through `createFnChannel`). The substrate subpaths ship only mechanism: which context is active, and how a scope propagates across an async boundary, is policy, and policy lives in *your* kernel: the module where you mint your channel and pick its context. The stack context is the simplest policy; a different kernel can pick a different one (an `AsyncLocalStorage`-backed context via `alsContext`, a React context, a test harness) on the same substrate. See [Building a Kernel](/advanced/building-a-kernel/). The unit a kernel wires is a **channel**, and the thing that wires it is a **connector**: `createChannel(name, connector)` hands the name to the connector, mints the channel over the connector's fresh context, and returns the kernel the connector builds from the channel's **surface**: ```ts import { stackContext } from '@mycl/core/context'; import { createChannel } from '@mycl/core/factory'; import type { ChannelSurface } from '@mycl/core/factory'; // The minimal (identity) connector: fresh stack context, surface passthrough. const bare = (_name: G) => ({ context: stackContext(), build: (surface: ChannelSurface) => surface, }); const { capable, snapshot, channel, context } = createChannel('app', bare); ``` | surface field | what it is | |-------|------------| | `capable` | the **capability constructor**: mints capabilities that dispatch through this channel | | `snapshot` | captures this channel's active scope for replay across an async boundary | | `channel` | the channel token: pass it to `setChannelContext` to swap the backing context later | | `context` | the channel's live `ScopeContext` (`{ get, run }`): use when you need `run` directly | The connector behind [`createFnChannel`](/reference/core/#createfnchannel) is the everyday one: it builds `{ channel, capable, snapshot, mycl, scope }` from this surface (keeping the raw context private). That is all a kernel is: a connector's build applied to a channel's surface. ## Subpaths `@mycl/core` ships five entry points. Import from the narrowest one you need: the introspection and factory surfaces stay out of the dispatch critical path. | entry | what it carries | |---------|-----------------| | `@mycl/core` (main) | the everyday kernel: `createFnChannel`, `registry`, `requires`, `setChannelContext`, `isCapability`, `isResolvedRegistry`, and the full capability/registry type vocabulary | | `@mycl/core/helpers` | augmentation helpers: `before`, `after`, `pipe`, `handleError` | | `@mycl/core/context` | `ScopeContext` implementations: `stackContext` (synchronous) and `alsContext` (`AsyncLocalStorage`-backed, Node 20.16+) | | `@mycl/core/factory` | the build-plugin and connector-author contract: `createChannel`, `merge`, `Connector`, `ChannelSurface`, `foldBindings`, `resolveContext`, `defineInternal` | | `@mycl/core/introspect` | read-only `describe` / `explain` / `diff` over a registry | `merge` lives on `/factory`, with the kernel authors who call it directly: channel users compose by passing registries to `mycl`/`scope`, which run the composition themselves. `createChannel` lives on `/factory` too, one level above `createFnChannel`, which is exactly one application of it. ## Reading a registry at runtime A registry is immutable and its bindings are inspectable. The `@mycl/core/introspect` subpath answers "what does this composed registry bind, and what would a capability resolve to?" without invoking anything. Import the trio as a namespace. `describe` collides with the Vitest/Jest global, and these functions are used most inside tests, so the namespace form keeps them unambiguous: ```ts import * as introspect from '@mycl/core/introspect'; introspect.describe(reg); // [{ identity: 'my-app-or-library:db', layers: 1, augments: 0, strategy: 'last-wins' }, …] introspect.explain(reg, db); // { identity: 'my-app-or-library:db', bound: true, resolvesToBase: false, layers: 1, augments: 0, strategy: 'last-wins' } introspect.diff(before, after); // { added: ['my-app-or-library:log'], removed: [], changed: [] } ``` `explain(reg, cap)` reports what `cap` resolves to (base vs. how many layers/augments) without calling it, so a test can assert an override took: `expect(introspect.explain(reg, cap).resolvesToBase).toBe(false)`. `diff` keys `changed` on a structural signature (layer count + augment count), so it catches added or removed contributions but not a changed *value* at the same count. ## Where next - [Building a Kernel](/advanced/building-a-kernel/): wire your own channel to a context. - [Type Guarantees](/advanced/type-guarantees/): how the substrate's type contracts hold. - [`@mycl/core` reference](/reference/core/): every export, signature by signature. --- # Building a Kernel > Write a connector (a fresh context plus a kernel builder), stand a channel up with createChannel(name, connector), and swap the context later with setChannelContext (AsyncLocalStorage). A **kernel** is the module that binds a channel to an execution context and exports an everyday API. The one-call wiring in the [quick start](/getting-started/quick-start/) already is one, on the simplest context. This page is the deeper story: what a `ScopeContext` is, how to write your own (async propagation, a React tree, an isolated test harness), and how to swap a channel's context after the fact. The examples on this page import `@mycl/core/factory`, `@mycl/core/context`, and Node built-ins, so they are shown as static code rather than runnable Playgrounds (the Playground only ships `@mycl/core`'s main entry and `/helpers`). ## A ScopeContext holds the active scope A channel needs one thing from you: a `ScopeContext`, a `{ get, run }` pair that stores and retrieves the active resolved registry. It never inspects the value; it only holds it. ```ts interface ScopeContext { get: () => R | undefined; run: (value: R | undefined, fn: () => T) => T; } ``` `get` is called on every capability dispatch and returns the current registry, or `undefined` when no scope is active. `run` installs `value` for the duration of `fn` and returns whatever `fn` returns. Passing `undefined` to `run` pins scopelessness: inside `fn`, `get()` returns `undefined` even if an outer scope was active. For synchronous code you do not have to write one. `stackContext()` returns a ready `ScopeContext` backed by a push/pop stack, the default choice for browsers and synchronous code. ## Write a connector A **connector** is what [`createChannel`](/reference/core/#createchannel) invokes to stand a channel up: a function of the channel name returning the channel's fresh context and `build`, which shapes the public kernel from the channel's surface (`capable`, `snapshot`, `channel`, `context`). The connector behind [`createFnChannel`](/reference/core/#createfnchannel) is the everyday one, and it is internal; you write your own when you want a different execution model, curating your kernel from the surface alone: ```ts import type { Registry } from '@mycl/core'; import { createChannel, merge } from '@mycl/core/factory'; import type { ChannelSurface } from '@mycl/core/factory'; import { alsContext } from '@mycl/core/context'; // A connector with AsyncLocalStorage from the start (no swap needed later). // alsContext() mints a fresh store per call, so each channel this connector // stands up gets its own: exactly the "mint inside the connector" rule below. const alsConnector = (_name: G) => ({ context: alsContext(), build: (surface: ChannelSurface) => ({ channel: surface.channel, capable: surface.capable, snapshot: surface.snapshot, // This kernel's own boundary: run fn inside a scope over the registries. run: (fn: () => T, ...regs: Registry[]): T => surface.context.run(merge(...regs), fn), }), }); // The connector-author deliverable: a bound factory, your createFnChannel. export const createAlsChannel = (name: G) => createChannel(name, alsConnector); export const { capable, snapshot, channel, run } = createAlsChannel('myproject'); ``` The kernel's boundary members are yours to design: this one exposes a bare `run`; `createFnChannel`'s exposes `mycl` and `scope`. What every kernel shares is the shape underneath: a connector's `build` applied to a channel's surface. ### Want createFnChannel's kernel on a custom context? Don't write a connector at all: mint the kernel with `createFnChannel` and swap the context, as shown in [the section below](#swap-the-context-later). The kernel members read the channel's context cell live, so a swap installed at module load is indistinguishable from a connector that started with it. Two rules. **Mint the context inside the connector**: a context closed over from outside would be shared by every channel the connector mints, collapsing their scopes into one. This is dev-enforced: a context that already backs another channel throws [error 16](/errors/16/) at `createChannel` or `setChannelContext`. **Keep the connector generic over the name** so the kernel's types carry each channel's literal. And the convention: channel users should meet your *bound factory* (`createAlsChannel` above, `createFnChannel` for the everyday kernel), not `createChannel` or the connector; those are this level's tools. The channel name becomes the leading `channelName:` segment of every capability's identifier, so it must not contain a `:` (the delimiter that splits an identifier back into channel and path) or a `/` (reserved for path segments); `createChannel` rejects a bad name at the type level and, outside production, throws [error 12](/errors/12/) at runtime. Channels are disjoint by construction: capabilities carry their channel's identifier prefix and dispatch through their own context, so two channels never mix. A library embedding mycl does exactly the same, importing the same entries. See [Installation → Library exports](/getting-started/installation/#library-exports). ## Swap the context later The channel token you exported lets you replace the backing context at any time with `setChannelContext(channel, ctx)`. Already-built capabilities pick up the swap on their next dispatch: they read the channel's context cell live, so nothing needs rebuilding. The common reason to swap is async propagation. `stackContext` is correct for synchronous code but a scope does not survive an `await`. On Node, back the channel with [`alsContext`](/reference/core/#alscontext) instead, the shipped `AsyncLocalStorage`-backed `ScopeContext` from `@mycl/core/context`, and the scope propagates across `await`, `setTimeout`, and microtask boundaries on its own: ```ts import { setChannelContext } from '@mycl/core'; import { alsContext } from '@mycl/core/context'; setChannelContext(channel, alsContext()); ``` A `ScopeContext` is just `{ get, run }` (as above), so if `alsContext` is not the shape you need, hand-rolling one stays a few lines. After the swap, `snapshot` still works: it just becomes optional, because the context now carries the scope across boundaries for you. In the browser there is no `AsyncLocalStorage`, so a stack context plus manual `snapshot` stays the model. ## Where next - [The Core Substrate](/advanced/core-substrate/): what `createChannel` returns and the subpaths a kernel imports. - [Snapshots & Async](/guide/snapshots-and-async/): the async-boundary problem `alsContext` retires. - [Duplicate Detection](/advanced/duplicate-detection/): why duplicate copies coexist, and the one hazard that remains. --- # Type Guarantees > How mycl's types enforce contracts at the layer boundary: the Capability discriminant, AnyCapability, requires provider-completeness, and reading a registry's type-level layers. mycl enforces its contracts at compile time. The value you `layer` and the wrapper you `augment` must match the capability's declared signature, a capability's identifier is part of its type, and `requires` can make a registry type-error before it is ever run. This page shows each guarantee and the actual compiler error it produces. The blocks below are static because they are meant *not* to compile. Each shows the error TypeScript prints. TypeScript reports resolved signatures, not the alias names, so the messages name concrete function types. ## `layer` rejects a mismatched value `layer(cap, value)` requires `value` to satisfy the capability's full signature. An incompatible implementation does not compile: ```ts import { createFnChannel, registry } from '@mycl/core'; const { capable } = createFnChannel('my-app-or-library'); const greet = capable((name: string) => `hello ${name}`, 'greet'); registry().layer(greet, (name: number) => `hello ${name}`); // error TS2345: Argument of type '(name: number) => string' is not assignable // to parameter of type '(name: string) => string'. // Types of parameters 'name' and 'name' are incompatible. // Type 'string' is not assignable to type 'number'. ``` ## `augment` rejects a signature change `augment(cap, wrapper)` requires the wrapper to preserve the signature end-to-end. Change the return type (or a parameter type) and it does not compile: ```ts registry().augment(greet, next => (name: string) => next(name).length); // error TS2322: Type '(name: string) => number' is not assignable to // type '(name: string) => string'. // Type 'number' is not assignable to type 'string'. ``` A wrapper that keeps the signature is accepted: ```ts registry().augment(greet, next => (name: string) => next(name).toUpperCase()); // ✓ still (name: string) => string ``` ## `Capability` and identifier as discriminant A capability's type carries four parameters: - `T`: the callable signature users invoke. - `V`: the value a `layer` contribution stores (equal to `T` for ordinary function capabilities; different when a [layer strategy](/guide/layer-strategies/) accumulates something else). - `Args`: the tuple each `layer` call accepts (`[V]` by default). - `Id`: the assembled identifier literal, `channelName:idPath`. `Id` is not just a runtime label. It is a string **literal** in the type, preserved by `const` inference from the identifier path you pass to `capable`. Two capabilities that share a signature and value contract are still **distinct types**, because their identifier literals differ: ```ts const a = capable((n: string) => n, 'a'); // Capability<…, 'my-app-or-library:a'> const b = capable((n: string) => n, 'b'); // Capability<…, 'my-app-or-library:b'> // a and b have identical call signatures but are not interchangeable types. ``` Because every capability carries a mandatory identifier, a registry's type-level **layers** are *total*: they record every layered capability, and same-shaped ones never conflate. The value contract `V` is an invariant brand on top of that: a capability typed for `string` values is not assignable to one typed for `number` values even when their call signatures overlap, which blocks silent substitution of capabilities with different semantics. ## `AnyCapability` for heterogeneous collections Code that operates on capabilities *generically* (a guard, a middleware helper, a registry inspector) cannot name a specific `V` or `Id`. `AnyCapability` (`Capability`) is the erased view: enough to invoke or key on a capability, without the value-contract brand or a concrete identifier. It is what lets a heterogeneous list of capabilities share one type: ```ts import type { AnyCapability, Registry } from '@mycl/core'; // Works over any mix of capabilities, whatever each one's value contract is. const boundCount = (reg: Registry, caps: AnyCapability[]): number => caps.filter(cap => reg.has(cap)).length; ``` `registry().has(cap)` and the map returned by `registry().bindings()` are both typed in terms of `AnyCapability` for exactly this reason. Code that *contributes* a concrete implementation keeps the full `Capability` type so the value can be checked; code that *ranges over* capabilities takes `AnyCapability`. ## Provider-completeness with `requires` The registry's total layers let `mycl` check that the registries you pass actually provide what an entry point needs. Wrap a `make` function with `requires(...)` to declare its required capabilities; `mycl(make, ...regs)` then type-errors unless the registries provide every one, naming any that are missing: ```ts import { createFnChannel, registry, requires } from '@mycl/core'; const { capable, mycl } = createFnChannel('my-app-or-library'); type Db = { query: (id: string) => unknown }; const db = capable((): Db => { throw new Error('no db bound'); }, 'db'); const handler = requires(db)((req: { id: string }) => db().query(req.id)); mycl(handler, registry().layer(db, () => realDb)); // ✓ provides db mycl(handler, registry()); // error TS2345: Argument of type 'Registry' is not assignable to // parameter of type '"mycl: registry is missing required capability: my-app-or-library:db"'. ``` `requires` is **compile-time only, zero runtime**: it returns `make` unchanged; the required set is read purely at the type level. It is honest about its bounds: - The list is **hand-maintained**: it declares intent and cannot read the call graph. Use it for required-effect seams (capabilities whose base throws or stubs); an enrichment capability with a working base need not be listed. - It is enforced at **`mycl` only**, not `scope`. Requirements split across several registries union their provided sets. - It is **permissive when the layers aren't visible**: a pre-resolved `ResolvedRegistry` (from `merge(...)`) or a build-collapsed registry carries no type-level layers, so the provided set is unknown and the check passes rather than failing falsely. It is sound when it fires, silent when it can't see the layers. ## Reading the layers The same total layers are readable at the type level. Four readers cover it. Note that `CapabilityLayers` takes **two** type parameters (the layers and the capability): ```ts import type { RegistryLayers, CapabilityLayers, ProvidedIds, CapabilityId } from '@mycl/core'; const reg = registry().layer(greet, (n: string) => `hi ${n}`); type Layers = RegistryLayers; // the registry's type-level layers type Greets = CapabilityLayers; // layers on `greet` type Ids = ProvidedIds; // union of provided identifiers type GreetId = CapabilityId; // 'my-app-or-library:greet' ``` `RegistryLayers` extracts a registry's layers tuple; `CapabilityLayers` projects the layers on one capability; `ProvidedIds` is the union of identifiers the registry provides (what `requires` checks against); `CapabilityId` reads a single capability's identifier literal. All four live on `@mycl/core`'s main entry, alongside `requires` itself. ## Where next - [Layer Strategies](/guide/layer-strategies/): when `V` and `Args` diverge from `T`. - [The Core Substrate](/advanced/core-substrate/): reading the same bindings at runtime with `describe`/`explain`. - [`requires` reference](/reference/core/#requires): the full signature and semantics. --- # Duplicate Detection > Why duplicate copies coexist harmlessly by construction, the one real hazard (cross-copy token mixing), the duplicate-identifier guard (error 13), and a self-report recipe for libraries that want startup detection. Code being loaded twice is normally where plugin systems break. mycl's embedded model is built so it mostly cannot: **duplicate copies coexist by construction**. This page explains the guarantee, the one genuine hazard that remains, the guard mycl ships for it, and a recipe for libraries that want more. ## Duplicates coexist by design A channel lives in the module instance that minted it. If two copies of a package that mints a channel load in one realm (a nested install, or a dual ESM+CJS load), each copy runs its own channel: capabilities minted by one copy dispatch through their own context, scopes activated through one copy bind that copy's capabilities, and each copy is fully self-consistent. Nothing crashes, nothing corrupts. Two copies of a form library each render their own forms correctly. mycl itself is immune twice over: `@mycl/core` mints no channel of its own and touches no realm-global state (every entry is side-effect-free), so duplicate copies of *mycl* are simply independent mechanism. ## The one real hazard: cross-copy token mixing The failure that remains needs a *split import graph* in the consumer: a registry built against capability tokens from copy A handed to the runtime of copy B. Bindings key on the capability object itself, so copy B's dispatch finds nothing and silently falls back to base implementations. Extensions appear to register but never apply. This is the classic npm dual-package hazard (the same class as two copies of React disagreeing about hooks), and it is a **packaging problem**: it can only happen when the consumer's dependency tree resolves your package to two places. To confirm a single copy resolves: ```sh pnpm why @myorg/widgets ``` It should resolve to one version at one path. ## Duplicate-identifier (error 13) Every capability's identifier is `channelName:idPath`. If two capabilities resolve to the *same* identifier, one silently shadows the other in tooling and error labels. When `capable` mints a capability whose identifier was already seen this realm, it reports via `console.error`, in dev and test only: - It **reports, never throws.** A duplicate identifier does not break dispatch (the dispatch cache keys on the capability object, not its identifier string), and a report avoids false positives under HMR and module re-evaluation. - In **production** the check is dropped entirely (the whole dev-only block is dead-code-eliminated). One cause is a second copy of the package that mints the capability, which makes this guard a useful *symptom* of a split graph. The other is two unrelated packages colliding on the same `project/capability` path, which is why identifier paths are namespaced by project. [Error 13](/errors/13/) names the offending identifier. ## Recipe: self-report your own copy mycl ships no package-level guard (duplicates coexist, so it has nothing to detect on its own behalf). If your library wants a loud startup signal when a consumer's tree carries two copies of *it*, hand-roll the self-report: a realm-global ledger keyed by your package name. The subtle parts are the `version (moduleId)` identity and the query-string strip, which keep test isolation and Vite HMR (same file, timestamped URL) silent while catching a genuine second copy even at the same version: ```ts // duplicateGuard.ts: call selfReport() once at your package entry's init. const seen: Record> = ((globalThis as any)[Symbol.for('myorg.instances')] ??= {}); export const selfReport = (name: string, version: string, moduleId = 'unknown'): void => { const id = moduleId.split('?')[0]; const copies = (seen[name] ??= new Set()); copies.add(`${version} (${id})`); if (copies.size > 1 && process.env.NODE_ENV !== 'production') { const msg = `[${name}] duplicate copies loaded: ${[...copies].join(' + ')}. ` + 'Registries built against one copy cannot bind the other; deduplicate the install.'; if (process.env.NODE_ENV === 'development') throw new Error(msg); console.warn(msg); } }; ``` ```ts // your package entry import { selfReport } from './duplicateGuard'; selfReport('@myorg/widgets', '1.2.0', typeof __filename === 'string' ? __filename : import.meta.url); ``` Throw in development (a duplicate is a real bug, fail loud), warn in test (runners legitimately load multiple copies; crashing a suite is wrong), stay quiet in production or warn there too, your call. Use your own `Symbol.for` key; the ledger is your package's, not mycl's. ## Where next - [Installation](/getting-started/installation/#library-exports): the embedded setup for libraries. - [Building a Kernel](/advanced/building-a-kernel/): how channels and the module boundary relate. - [Error 13](/errors/13/): the duplicate-identifier message text. --- # Prior Art > The established ideas mycl draws on (effects and handlers, the expression problem, microkernels, information hiding, and aspects), each mapped to the part of mycl it informs. mycl is not a new idea so much as a small, hopefully practical assembly of old ones. If any of the names below are familiar, this page is the shortest path to "oh, it's *that*." Each section states the idea in plain terms, then maps it to mycl. None of this is required reading: the [guide](/guide/capabilities/) stands on its own. ## Effects and handlers A computation signals an operation; a separate handler decides what that operation actually does and returns a result. Because the handler is chosen independently of the operation, one operation can be answered many ways: real, in-memory, mocked. **In mycl terms:** a capability is the operation, a registry is the handler, and the active scope is which handler is installed. That is the whole trick behind changing *how* without touching *what*. See [Scopes](/guide/scopes/) and [Registries and Layers](/guide/registries-and-layers/). ## The expression problem The classic tension: how do you add new behaviours to existing types, *and* new types under existing behaviours, without modifying either party or losing type safety? Most designs let you extend along one axis cheaply and pay dearly on the other. **In mycl terms:** a capability's behaviour lives outside the capability itself, so any registry (including a third party's) can contribute to it without editing the original, and the type system still checks each contribution. Extension is by layering into a scope, not by patching source. See [Registries and Layers](/guide/registries-and-layers/) and [Augmentation](/guide/augmentation/). ## Microkernel and plugin architecture Second-generation microkernels (L4, seL4) draw a hard line between mechanism and policy: the kernel provides only dispatch and validation; everything else is user-space. The result is a tiny, stable core with all the variety pushed outward. **In mycl terms:** `@mycl/core`'s substrate subpaths are the microkernel: they ship only mechanism (dispatch, the layer fold, the type contracts). Policy (which context is active, how a scope crosses an async boundary) lives in the kernel above it, like the one `createFnChannel` builds. Each capability is itself a tiny core: its base is the mechanism, its layers are the policy. See [The Core Substrate](/advanced/core-substrate/) and [Building a Kernel](/advanced/building-a-kernel/). ## Information hiding Parnas (1972): split a system so each module hides one design decision: above all, the decision most likely to change. The test of a good seam is that the things it separates can be built independently against the seam alone. **In mycl terms:** a capability is a Parnas module whose secret is the *how*. The consumer holds the capability and its contract; whoever supplies the implementation is free to change it without the consumer noticing. If two sides of a seam can be built against the capability's signature alone, the seam is in the right place. See [Capabilities](/guide/capabilities/). ## Cross-cutting concerns Some concerns (logging, timing, validation, caching) scatter across many functions and tangle with the real logic. Aspect-oriented programming pulls such a concern into one place and attaches it at named points in the program. **In mycl terms:** `augment` is that attachment, a capability is the named, first- class point it attaches to, and layering is the explicit wiring. Unlike classic AOP there are no hidden pointcuts: every join point is a capability you can see and name, so there is nothing spooky to trace. See [Augmentation](/guide/augmentation/). ## Where next - [Capabilities](/guide/capabilities/): the mechanism these ideas converge on. - [The Core Substrate](/advanced/core-substrate/): the mechanism/policy split in code. - [Glossary](/reference/glossary/): one definition per term, used identically everywhere. --- # @mycl/core > Complete API reference for @mycl/core, covering every exported function and type across its five entry points, with signatures, error codes, and minimal examples. `@mycl/core` is mycl: one package, five entry points. The main entry is the everyday surface, one ceremony: mint your channel with [`createFnChannel`](#createfnchannel), one import, one call, and work with the kernel it returns plus the registry machinery (`registry`, the augmentation helpers, [`requires`](#requires)). The subpaths below it are the substrate: `/helpers` (augmentation helpers, deliberately not on main), `/context` (`ScopeContext` implementations for kernel authors), `/factory` (the connector and build-plugin contract), and `/introspect` (read-only registry inspection). Connector authors, the level above the everyday surface, work against `/factory` directly (`createChannel` and the [`Connector`](#connector) contract); the connector behind `createFnChannel` is itself internal. Every entry is side-effect-free. ```bash pnpm add @mycl/core ``` Every example below shares one running kernel and a `fetchUser` capability: ```ts import { createFnChannel } from '@mycl/core'; const { capable, snapshot, channel, mycl, scope } = createFnChannel('my-app-or-library'); const fetchUser = capable( async (id: string) => ({ id, name: 'base' }), 'fetchUser', ); // full identifier: 'my-app-or-library:fetchUser' ``` --- ## `@mycl/core` (main entry) ```ts import { createFnChannel, registry, requires, setChannelContext, isCapability, isResolvedRegistry, } from '@mycl/core'; ``` ### `createFnChannel` ```ts createFnChannel(name: G): FnKernel ``` Mint a channel with the plain-function kernel bound to it: `FnKernel` is `{ channel, capable, snapshot, mycl, scope }`. This is the whole ceremony; each call mints a fresh, isolated channel, and `name` becomes the identifier prefix of every capability minted through the kernel's `capable`. The raw context is deliberately absent from the kernel (the substrate stays private; swap contexts through [`setChannelContext`](#setchannelcontext) with the `channel` token). ```ts import { createFnChannel } from '@mycl/core'; const { capable, snapshot, channel, mycl, scope } = createFnChannel('my-app-or-library'); ``` Under the hood it is one application of the mechanism: [`createChannel`](#createchannel) (on `/factory`) applied to an internal connector. For a custom context (say `AsyncLocalStorage` on Node), mint the channel and swap its context with [`setChannelContext`](#setchannelcontext); connector authors building a different kernel shape work against `/factory`'s [`Connector`](#connector) contract and export their own bound factory. See [Building a Kernel](/advanced/building-a-kernel/). **Errors:** a name containing `:` or `/` is a type error and, outside production, throws [error 12](/errors/12/) at runtime. ### `capable` ```ts // on the kernel createFnChannel returns, for channel name G: capable( baseFn: T | Capability, idPath: IdPath, config?: CapableConfig, ): Capability ``` Create a capability from a default implementation. `capable` is not a package export: it comes on the kernel [`createFnChannel`](#createfnchannel) returns (and on the [`ChannelSurface`](/reference/core/#channelsurfaceg) every connector receives), pre-bound to your channel. The returned [`Capability`](#capabilityt-v-args-id) is a plain callable that dispatches through the active scope, falling back to `baseFn` when nothing is bound. It is simultaneously its own registry key, its type contract, its default implementation, and its stable identifier. The mandatory `idPath` is a non-empty **identifier path**. A flat name is enough for a private, single-owner channel; use `'project/capability'` when several packages share one channel, so their capabilities never collide. A capability belongs to the channel whose `capable` minted it, so with `createFnChannel('my-app-or-library')` and idPath `'fetchUser'` the full identifier becomes `my-app-or-library:fetchUser`. The optional `config.strategy` is a [`LayerStrategy`](#layerstrategyt-v-args) that changes how `.layer()` contributions fold. See [Layer strategies](/guide/layer-strategies/). See [Capability identifier](#capability-identifier) for the full model. ```ts const fetchUser = capable( async (id: string) => ({ id, name: 'base' }), 'fetchUser', ); ``` **Errors:** the identifier path is validated in dev: an empty path throws [error 11](/errors/11/). A second capability minted with the same full identifier is, in dev, reported to the console (`console.error`), [error 13](/errors/13/); it does not throw. Calling the returned capability with no active scope throws [error 1](/errors/1/); a binding that resolves to a non-function throws [error 2](/errors/2/). ### Capability identifier Every capability carries a mandatory **identifier** string. `capable(baseFn, idPath, config?)` takes the **identifier path** as its second argument, and the channel prepends its own name to form the full identifier `name:idPath`. Any non-empty path works (channels are disjoint, so uniqueness only matters within one channel); a flat path is enough for a private, single-owner channel, and reaching for the `project/capability` shape when several packages share one channel gives three segments: | Segment | Example | Owned by | Supplied at | |---------|---------|----------|-------------| | channel | `app` | whoever defines the capability *channel* | `createFnChannel('my-app-or-library')` (or `createChannel('my-app-or-library', connector)`) | | project | `demo` | the package *minting* capabilities into that channel | the `idPath` argument to `capable` | | capability | `greet` | the individual capability | the `idPath` argument to `capable` | The identifier-path type is enforced by the [`IdPath`](#idpathid) validator: any non-empty literal passes; an empty one is replaced by a self-describing message type. Structure beyond non-emptiness is convention, not contract, and uniqueness within a channel is the author's responsibility. - An empty identifier path from an untyped (JS) caller throws [error 11](/errors/11/) (**dev only**). - A duplicate assembled identifier is reported via `console.error` ([error 13](/errors/13/)). This is **dev only, not a throw**. A duplicate corrupts the type-level layers and introspection labels but does not break dispatch, which keys on the capability object itself, not its identifier string; a `console.error` (rather than a throw) also avoids false positives under HMR / module re-evaluation. The identifier does triple duty: the **type-level layer-record key** (making the registry's layers total and every capability type-distinguishable; see [`Capability`](#capabilityt-v-args-id)), the **runtime error/debug label** ([error 1](/errors/1/) and [error 2](/errors/2/) name it), and the **introspection label**. Because it is a stable public identifier, changing a published capability's identifier is a breaking change. ### `registry` ```ts registry(): Registry ``` Create a fresh, frozen, empty [`Registry`](#registryl). Every `.layer()` and `.augment()` call returns a **new** registry (clone-on-write); nothing mutates in place. It starts with an empty type-level layer record (`readonly []`) so a `.layer` chain builds a precise per-registry layers tuple. ```ts import { registry } from '@mycl/core'; import { after } from '@mycl/core/helpers'; const testReg = registry() .layer(fetchUser, async (id) => ({ id, name: 'mock' })) .augment(fetchUser, after((u) => console.log('fetched', u))); ``` The registry never accepts a caller-built bindings map: bindings flow in only through its two derivation methods, which keeps the binding value shape an internal invariant the runtime relies on. The registry carries four methods: #### `.layer(cap, ...args)` ```ts layer( cap: Capability, ...args: A ): Registry; args: A }]> ``` Bind a capability to an implementation (or, for a capability with a custom strategy, a contribution). The args are spread into the capability's `step` fold. Under the default last-wins strategy the final `.layer()` call for a capability is the one dispatched; custom strategies accumulate across calls. Each layer is also recorded in the registry's type-level layers. **Errors:** throws [error 5](/errors/5/) if `cap` is not a capability, and [error 6](/errors/6/) if the contribution is rejected by the strategy (default: an `undefined` value). #### `.augment(cap, wrapper)` ```ts augment(cap: Capability, wrapper: AugmentWrapper): Registry ``` Wrap whatever the capability resolves to, without replacing it. `wrapper` is an [`AugmentWrapper`](#augmentwrapperf): a raw `next => (...args) => …` function, or the result of a helper (`before`, `after`, `pipe`, `handleError`). Augments always apply, even when nothing is layered (they wrap the base). Multiple augments compose outer-wraps-inner. See [Augmentation](/guide/augmentation/). **Errors:** throws [error 7](/errors/7/) if `cap` is not a capability and [error 8](/errors/8/) if `wrapper` is not a function. In **dev only**, a canary probe additionally rejects a builder-style argument (`h => h.after(fn)`) up front rather than at first dispatch: [error 9](/errors/9/) for a builder-shaped value, [error 10](/errors/10/) if the wrapper throws during the probe. This block is dead-code-eliminated in production. #### `.has(cap)` ```ts has(cap: AnyCapability): boolean ``` Report whether the registry holds any binding (a layer or an augment) for `cap`. #### `.bindings()` ```ts bindings(): RegistryBindings ``` Return the registry's internal capability-to-binding-value map as a [`RegistryBindings`](#registrybindings). Read-only; primarily for introspection (see [`/introspect`](#myclcoreintrospect)) and build tooling. ### `scope` ```ts scope(fn: T): Snapshotted scope(fn: T, reg: Registry | ResolvedRegistry): Snapshotted ``` Bind a function to a registry scope and return a call-signature-only wrapper (own properties of `fn` are dropped; the result is a [`Snapshotted`](#snapshottedt)). Every call to the wrapper replays that scope. Pass a [`Registry`](#registryl) or a pre-composed [`ResolvedRegistry`](#resolvedregistry); with no second argument, `scope(fn)` pins the **base** behavior: capabilities inside fall back to their default implementations regardless of any ambient scope. ```ts const base = scope(() => fetchUser('42')); // pins base implementations const bound = scope(() => fetchUser('42'), testReg); // pins to testReg bound(); ``` `scope` is not a package export: it comes on the kernel [`createFnChannel`](#createfnchannel) returns, as in the running kernel above. Its function type is exported as `Scope` (and `mycl`'s as `Mycl`), so a kernel module can annotate what it re-exports: `export const scope: Scope = kernel.scope`. **Errors:** `scope` always establishes a scope (empty when none is given), so capabilities called inside never throw [error 1](/errors/1/): they resolve to their base. A binding whose strategy assembles a non-function still surfaces [error 2](/errors/2/) at call time. See [Scopes](/guide/scopes/). ### `mycl` ```ts mycl( make: F, ...registries: [Exclude, ProvidedIds>] extends [never] ? Regs : readonly [MissingCapabilities, ProvidedIds>>] ): MyclFactory, Parameters, readonly [...StoredRegs, ...Regs]> ``` Build a factory. `mycl(make, ...regs)` returns a frozen [`MyclFactory`](#myclfactory): the registries resolve **once** at creation, and each call runs `make(...args)` inside that resolved scope, returning its result verbatim: `mycl` does not inspect, walk, scope-bind, or freeze the result. There is a single signature: because a `MyclFactory` is structurally a `(...args) => T`, passing one back as `make` matches the same call. When you do, its stored registries are merged **ahead** of the newly-passed ones and a fresh factory is built (detected at runtime via an internal brand), on the factory's **own channel**: a factory stays on its channel when extended, even by another channel's `mycl`. The stored registry tuple is threaded onto the factory type: nothing the call site knew is erased. Read it back with [`SuppliedRegistries`](#suppliedregistries) and compose with `RegistryLayers` / `CapabilityLayers` / `ProvidedIds` to introspect a factory at the type level. When `make` is wrapped with [`requires(...)`](#requires), `mycl` also performs a **provider-completeness check**: it type-errors unless the passed registries provide every required capability, naming any that are missing. An unwrapped `make` has no requirements, so the check passes vacuously. This check is compile-time only: it raises no runtime error, and it is enforced at `mycl`, not at `scope`. ```ts const createApp = mycl( () => ({ getUser: snapshot((id: string) => fetchUser(id)) }), testReg, ); const app = createApp(); const createExtended = mycl(createApp, loggingReg); // extends: testReg + loggingReg ``` `mycl` is not a package export: it comes on the kernel [`createFnChannel`](#createfnchannel) returns, as in the running kernel above. **Errors:** provider-completeness is a compile-time type error, not a throw. At runtime, a capability invoked **after** `make` returns (inside a callback that was not wrapped in [`snapshot`](#snapshot)) has lost the resolved scope and throws [error 1](/errors/1/). See [Factories](/guide/factories/). ### `requires` ```ts requires(...caps: Caps): (make: F) => Requiring> ``` **Compile-time only, with zero runtime effect.** Declare the capabilities an entry function requires its registry to provide. Curried: `requires(db, currentUser)(make)` returns `make` **unchanged at runtime** and tags its *type* with the union of those capabilities' identifiers (e.g. `'my-app-or-library:db' | 'my-app-or-library:currentUser'`). The enforcement point is [`mycl(make, ...regs)`](#mycl), which then type-errors unless the registries provide every tagged identifier, naming any that are missing. The currency is capability **values**, not identifier strings: refactor-safe and autocompleting. ```ts import { registry, requires } from '@mycl/core'; const db = capable((): Db => { throw new Error('no db bound'); }, 'db'); const handler = requires(db)((req: Req) => db().query(req.id)); mycl(handler, registry().layer(db, () => realDb)); // ✓ provides db mycl(handler, registry()); // ✗ type error: mycl: registry is missing required capability: my-app-or-library:db ``` Opt-in: a `make` passed to `mycl` without `requires` has an empty required set and is unchecked. Intended for **required-effect seams**: capabilities whose base throws or stubs (like `db` above); enrichment capabilities with a usable base need not be listed. Worth knowing: the list is hand-maintained (it cannot read the call body); it is enforced at `mycl` only; and it is **permissive when the registry's layers are not visible**: passing a pre-resolved `ResolvedRegistry` (from `merge(...)`) or a build-collapsed registry makes the provided set unknown, so the check passes rather than failing falsely. **Errors:** none; `requires` has no runtime behavior. ### `snapshot` ```ts snapshot(fn: T): Snapshotted ``` Pin the currently-active scope (including "no scope") onto `fn` and replay it on every later call: the manual carrier across async boundaries. Returns a call-signature-only wrapper (a [`Snapshotted`](#snapshottedt)): `this`, params, and return type are preserved, own properties are dropped, and a capability passed in comes back de-branded. Call `snapshot` **while a scope is active** (inside a `mycl` factory, or a synchronous call chain it initiates); the returned wrapper then carries that scope into deferred callbacks. ```ts const createApp = mycl(() => ({ load: snapshot(async (id: string) => { const finish = snapshot((u: User) => fetchUser(u.id)); // scope preserved in .then() return fetchUser(id).then(finish); }), }), testReg); ``` `snapshot` comes on the kernel [`createFnChannel`](#createfnchannel) returns, pre-bound to your channel, as in the running kernel above. **Errors:** if `snapshot` captures a moment with no active scope, the replayed wrapper dispatches capabilities without one and throws [error 1](/errors/1/), the loud signal of a missed async boundary. On Node, [`alsContext`](#alscontext) retires manual snapshotting for automatic propagation instead. See [Snapshots and async](/guide/snapshots-and-async/). ### `channel` (the token on your kernel) ```ts channel: Channel // on the kernel createFnChannel returns ``` Your channel's token (a [`Channel`](#channelg)), on the kernel alongside `capable`/`snapshot`/`mycl`/`scope`. Pass it to [`setChannelContext`](#setchannelcontext) to swap the channel's backing context, for example to install [`alsContext`](#alscontext) for automatic async scope propagation instead of manual `snapshot`. ```ts import { setChannelContext } from '@mycl/core'; import { alsContext } from '@mycl/core/context'; import { channel } from './kernel'; // your createFnChannel('my-app-or-library') module setChannelContext(channel, alsContext()); ``` ### `setChannelContext` ```ts setChannelContext(channel: Channel, ctx: ScopeContext): void ``` Swap the [`ScopeContext`](#scopecontextr) backing an existing channel, in place. The swap is visible immediately to **every** capability and snapshot already built from that channel: dispatch reads the channel's mutable context cell live, so no per-call lookup and no rebuild are needed. This is how you install [`alsContext`](#alscontext) (or any custom context) once at startup. Throws [error 3](/errors/3/) if `channel` is not a valid channel. A context instance belongs to **one** channel: installing one that already backs another channel throws [error 16](/errors/16/) in dev, because a shared context would merge the channels' scopes (see [Channels are disjoint](/guide/channels/#channels-are-disjoint)). Mint a fresh context per channel. ```ts import { setChannelContext } from '@mycl/core'; import { alsContext } from '@mycl/core/context'; import { channel } from './kernel'; // your createFnChannel('my-app-or-library') module setChannelContext(channel, alsContext()); ``` ### `isCapability` ```ts isCapability(value: unknown): value is AnyCapability ``` Type guard: `true` if `value` is a capability created by any `capable()` call in the same realm (it carries the package's global brand symbol). Narrows to [`AnyCapability`](#anycapability). ### `isResolvedRegistry` ```ts isResolvedRegistry(reg: Registry | ResolvedRegistry): reg is ResolvedRegistry ``` Type guard: `true` if `reg` is a [`ResolvedRegistry`](#resolvedregistry) (it exposes `resolve`) rather than a buildable [`Registry`](#registryl). --- ### Types (root) Every type below is exported from `@mycl/core`'s main entry. Import with `import type`. #### `FnKernel` ```ts interface FnKernel { channel: Channel; capable: ChannelSurface['capable']; snapshot: ChannelSurface['snapshot']; mycl: Mycl; scope: Scope; } ``` The kernel shape [`createFnChannel`](#createfnchannel) returns: the channel-bound everyday surface, without the raw context. You rarely write this by hand; it is what `createFnChannel(name)` produces. #### `MyclFactory` ```ts type MyclFactory< T = unknown, Args extends any[] = any[], Regs extends readonly Registry[] = readonly Registry[], > = ((...args: Args) => T) & { readonly [MYCL_META]: MyclFactoryMeta } ``` The frozen callable returned by [`mycl`](#mycl). `T` is `make`'s return type, `Args` its parameters, `Regs` the stored registry tuple (extension appends to it). You rarely write this by hand: it is what a `mycl(...)` call produces; annotate a variable with it when passing a factory across a module boundary. #### `SuppliedRegistries` ```ts type SuppliedRegistries = F extends { readonly [MYCL_META]: MyclFactoryMeta } ? Regs : never ``` The registry tuple a factory was built over (`never` for a non-factory). The single factory-level reader. Everything else composes from the core readers: ```ts type Regs = SuppliedRegistries; // the stored registry tuple type Layers = RegistryLayers; // combined type-level layers type Caps = CapabilityLayers; // layers on fetchUser type Ids = ProvidedIds; // provided capability identifiers ``` #### `Requiring` ```ts type Requiring = F & { readonly [REQUIRES]: R } ``` A `make` function tagged with the union `R` of capability identifiers it requires: the return type of [`requires(...)(make)`](#requires). You seldom name it; it flows automatically from `requires` into `mycl`'s provider-completeness check. #### `RequiredIds` ```ts type RequiredIds = F extends { readonly [REQUIRES]: infer R extends string } ? R : never ``` Extract the required-identifier union tagged on a make (`never` when unwrapped). Used internally by `mycl`; occasionally handy when writing a wrapper over `mycl` that needs to read a make's requirements. #### `Mycl` and `Scope` ```ts interface Mycl { ( make: F, ...registries: Regs /* or a MissingCapabilities message, see mycl() */ ): MyclFactory, Parameters, readonly [...StoredRegs, ...Regs]>; } type Scope = { (fn: T): Snapshotted; (fn: T, reg: Registry | ResolvedRegistry): Snapshotted; }; ``` The call-signature types of [`mycl`](#mycl) and [`scope`](#scope) as they appear on a kernel. Annotate a re-export with them: `export const scope: Scope = kernel.scope`. #### `ChannelName` ```ts type ChannelName = G extends `${string}${':' | '/'}${string}` ? `mycl: channel name must not contain ':' or '/', got '${G}'` : G ``` Rejects a channel name containing the `:` or `/` identifier separators at the type level: a clean name passes through unchanged (literal inference of `G` intact), an offending one is replaced by a self-describing message type, so the compiler error names the culprit instead of collapsing to `never`. [`createChannel`](#createchannel)'s `name` parameter uses this; a bound factory like `createFnChannel` validates identically. #### `Channel` ```ts interface Channel { readonly [CHANNEL_KEY]: symbol; readonly name: G; } ``` The opaque channel token returned as `channel` from [`createFnChannel`](#createfnchannel) (or [`createChannel`](#createchannel)). Its hidden symbol proves provenance. Pass it to [`setChannelContext`](#setchannelcontext). #### `ScopeContext` ```ts interface ScopeContext { get: () => R | undefined; run: (value: R | undefined, fn: () => T) => T; } ``` Scoped access to a value: stores and retrieves, never inspects. `get` returns the active value or `undefined` when no scope is active; `run` executes `fn` within the scope of `value` and must be synchronous. Passing `undefined` to `run` **pins scopelessness**: inside `fn`, `get()` returns `undefined` even when an outer scope is active. A channel requires `ScopeContext`. [`stackContext`](#stackcontext) and [`alsContext`](#alscontext), both on `/context`, are the built-in implementations. #### `LayerStrategy` ```ts interface LayerStrategy { extract: (value: V, base: T) => T; step: (acc: V | undefined) => (...args: Args) => V; seed?: V; } ``` The **layer strategy** on a capability: a left fold (the shape of `reduce`) over its `.layer()` values: - `step(acc)(...args)`: the fold function; `acc` is the value folded so far, `undefined` for the first layer (the strategy owns the no-prior-value case, no resolver branching). - `extract(value, base)`: projects the final folded value into a callable `T`; `base` is the prior stage's result, or the capability's base function for the first stage. Must return a function: a non-function makes dispatch throw [error 2](/errors/2/). - `seed`: optional initial accumulator. It seeds `step`; it does **not** trigger the strategy. With zero layers, dispatch falls through to the base regardless. Because it is a left fold, composition is free: layers from composed registries fold as one sequence, in registration order within a registry and composition order across registries, so contributors never coordinate. With no strategy a capability uses the implicit default: last-wins `step`, an identity-function `extract`. This is a **layer strategy** (it folds `.layer()` values), unrelated to [`merge()`](#merge). See [Layer strategies](/guide/layer-strategies/). #### `AnyFn` ```ts type AnyFn = (...args: any[]) => unknown ``` The unconstrained function bound used throughout the capability generics. Params are `any[]` so every function satisfies it; the return is `unknown` to stop leakage. #### `AugmentWrapper` ```ts type AugmentWrapper = (next: F) => F ``` The unit `.augment()` composes: takes the next callable in the chain and returns a new one of the same signature (outer wraps inner wraps base). Write it directly, or produce one with a [helper](#myclcorehelpers). #### `Transform` ```ts type Transform = (value: ReturnType) => ReturnType ``` A single result transformer in a [`pipe`](#pipe) chain: maps the capability's return value to a new value of the same type. #### `Capability` ```ts type Capability ``` The full nominal type produced by `capable()`. `T` is the callable signature (what users call). `V` is the value stored in a registry (what `.layer()` contributions accumulate into). `Args` is the tuple each `.layer()` call accepts. For ordinary function capabilities `V = T` and `Args = [V]`; ignore both. `Id` is the assembled identifier literal (`name:project/capability`), an invariant brand that makes two otherwise identically-shaped capabilities distinct types (the discriminant behind a registry's total layers). See [Type guarantees](/advanced/type-guarantees/#capabilityt-v-args-id-and-identifier-as-discriminant). #### `AnyCapability` ```ts type AnyCapability = Capability ``` The runtime-erasure view: any capability, with its value/identifier contracts intentionally discarded. Used wherever code only needs to *invoke* or *key on* a capability, not honor its value contract: `.augment`, `.has`, `explain`, the guards. #### `IdPath` ```ts type IdPath ``` The type-level validator for the string `capable()` takes as `idPath`: any non-empty literal passes through unchanged; an empty one is replaced by a self-describing message type, so the compiler error says why instead of collapsing to a generic mismatch. #### `CapableConfig` ```ts interface CapableConfig { strategy?: LayerStrategy; } ``` The optional third argument to `capable(baseFn, idPath, config)`. Its only field is `strategy`, which selects custom layer-fold semantics. The identifier path is a mandatory positional argument, **not** a config field. #### `Snapshotted` ```ts type Snapshotted = (this: ThisParameterType, ...args: Parameters) => ReturnType ``` The return type of `snapshot(fn)` (and a kernel's `scope(fn, reg?)`). A call-signature-only wrapper: `this`, params, and return type match `T`; own properties are dropped (a capability passed in comes back de-branded). #### `CapabilityId` ```ts type CapabilityId = /* C's identifier literal */ ``` A capability's assembled identifier literal (`name:project/capability`), read straight off its identifier slot. #### `AccumulatedValue` ```ts type AccumulatedValue = /* C's V */ ``` The value type `V` a capability accumulates in the registry (`T` for a plain capability): the type `.layer()` contributions fold into, read straight off `C`'s invariant contract slot. #### `LayerArgs` ```ts type LayerArgs = /* C's Args */ ``` The tuple each `.layer()` call accepts for a capability (`[T]` for a plain capability), read off the same contract slot as [`AccumulatedValue`](#accumulatedvaluec). #### `Registry` The type of an immutable, clone-on-write registry, parameterized by its type-level layers `L` (a `readonly RegistryLayer[]`). Exposes `.layer`, `.augment`, `.has`, `.bindings` (see [`registry`](#registry)). #### `RegistryBindings` ```ts type RegistryBindings = ReadonlyMap ``` The capability→binding-value map a registry stores, returned by [`.bindings()`](#bindings). The `RegistryBindingValue` shape is an internal binding record (layer args and boxed augments) and is intentionally **not** root-exported: read it through [introspection](#myclcoreintrospect), not by hand. #### `ResolvedRegistry` ```ts interface ResolvedRegistry { resolve: (capability: AnyCapability) => AnyFn | null; } ``` The composed, dispatch-ready output of [`merge`](#merge) (or built internally by a kernel's `scope`), and the only form a scope dispatches against. Its single `resolve` method maps a capability to the function to call (or `null` for the base), and must return the same result for the same capability for the registry's lifetime: the dispatch cache assumes this; build a new `ResolvedRegistry` when bindings change. #### `RegistryLayer` ```ts type RegistryLayer = { cap: AnyCapability; args: readonly unknown[] } ``` One layer recorded in a registry's **type-level layers**: the type-level record of a `.layer(cap, ...args)` call. Because every capability has an identifier, the registry's layers are **total**: they record every layered capability, not an opt-in subset. #### `RegistryLayers` ```ts type RegistryLayers = R extends Registry ? L : never ``` Extracts a registry type's type-level layers tuple `L`. #### `CapabilityLayers` ```ts type CapabilityLayers ``` The layers in `L` layered onto a specific capability `C`. Takes **two** type parameters (the layers tuple and the capability) and filters by the capability's identifier literal (its 4th type param), so it selects `C`'s layers even among same-shaped capabilities. #### `ProvidedIds` ```ts type ProvidedIds = R extends Registry ? CapabilityId : never ``` The union of capability identifiers a registry provides, read off its type-level layers. Distributes over a union of registries, so `ProvidedIds` unions both. This is the type a kernel's provider-completeness check measures a `requires` set against. See [Provider-completeness](/advanced/type-guarantees/#provider-completeness-with-requires). --- ## `@mycl/core/helpers` Standalone augmentation helpers: pure generics over a capability's function type `T`, inferred at the `.augment(cap, …)` call site. Each returns an [`AugmentWrapper`](#augmentwrapperf). Deliberately not on the main entry; import from here whenever you augment. ```ts import { before, after, pipe, handleError } from '@mycl/core/helpers'; ``` ### `before` ```ts before(fn: (...args: Parameters) => void): AugmentWrapper ``` Build an augment that runs `fn` with the call's arguments **before** the capability executes. The return value of `fn` is ignored; the capability's result passes through untouched. ```ts import { registry } from '@mycl/core'; import { before } from '@mycl/core/helpers'; registry().augment(fetchUser, before((id) => console.log('fetching', id))); ``` ### `after` ```ts after(fn: (result: ReturnType, ...args: Parameters) => void): AugmentWrapper ``` Build an augment that runs `fn` with the result and the original arguments **after** the capability executes; the original result passes through. **Async caveat:** the result is observed as-is, so for an async capability that is the *Promise*, not the settled value; `await` inside `fn` if you need it. See [Augmentation](/guide/augmentation/). ### `pipe` ```ts pipe(...fns: Transform[]): AugmentWrapper ``` Build an augment that threads the capability's return value through one or more [`Transform`](#transformt) functions left-to-right. **Async caveat:** sync-only, so for an async capability the value piped is the *Promise*. See [Augmentation](/guide/augmentation/). ### `handleError` ```ts handleError( handler: (error: unknown, ...args: Parameters) => ReturnType | Awaited>, shouldHandle?: (error: unknown) => boolean, options?: { rethrow?: boolean }, ): AugmentWrapper ``` Build an augment that catches sync throws **and** async rejections (any thenable) from the capability, replacing the result with `handler(error, ...args)`. Pass `shouldHandle` to filter which errors are caught: errors it rejects re-throw. With `{ rethrow: true }` the handler runs for side effects only: its return value is discarded and the original error rethrows. This is the one augmentation helper that catches across the async boundary. ```ts import { registry } from '@mycl/core'; import { handleError } from '@mycl/core/helpers'; const canaryReg = registry() .layer(processPayment, newPaymentImpl) .augment(processPayment, handleError( (err, ...args) => { reportError(err); return oldPaymentImpl(...args); }, (err) => err instanceof PaymentError, )); ``` --- ## `@mycl/core/context` `ScopeContext` implementations, for kernel authors wiring a channel's backing store. ```ts import { stackContext, alsContext } from '@mycl/core/context'; import type { ScopeContext } from '@mycl/core'; ``` ### `stackContext` ```ts stackContext(): ScopeContext ``` Create a fresh synchronous, stack-based [`ScopeContext`](#scopecontextr), the built-in default. `run` pushes the value onto a stack for the duration of `fn` and always pops after; `get` reads the top. `R` defaults to `ResolvedRegistry` (what `createChannel` expects), so `stackContext()` needs no annotation in the common case. Correct for synchronous code and tests. It does **not** propagate scope across `await` boundaries. For that, install [`alsContext`](#alscontext) via [`setChannelContext`](#setchannelcontext), or carry scope manually with `snapshot`. ### `alsContext` ```ts alsContext(): ScopeContext ``` Create an `AsyncLocalStorage`-backed [`ScopeContext`](#scopecontextr): one store minted per call, carrying the scoped registry across `await`, `setTimeout`, and microtask boundaries inside `run`. Install it with [`setChannelContext`](#setchannelcontext) to retire manual `snapshot` calls on Node: ```ts import { setChannelContext } from '@mycl/core'; import { alsContext } from '@mycl/core/context'; import { channel } from './kernel'; // your createFnChannel('my-app-or-library') module setChannelContext(channel, alsContext()); ``` The `node:async_hooks` builtin resolves **lazily**, through `process.getBuiltinModule`, the first time `alsContext()` is *called*, never through a static import: the module stays browser-safe to import even for consumers that never call it, so importing `@mycl/core/context` cannot break a browser build on its own. That lazy resolution needs Node 20.16 or newer (or another runtime that provides `process.getBuiltinModule`); this is `alsContext`'s own floor, not the package's, everything else in mycl runs anywhere. **Errors:** throws [error 14](/errors/14/) where `process.getBuiltinModule` is unavailable. Reach for [`stackContext`](#stackcontext) there instead, or install your own `ScopeContext` (it is just `{ get, run }`). See [Snapshots and async](/guide/snapshots-and-async/). --- ## `@mycl/core/factory` :::caution[Connector and build-plugin authors only] This is the contract one level above the everyday surface: connector authors writing a kernel like `createFnChannel`'s, and build tooling that folds registries ahead of time. It is **not** a general-purpose user API; symbols here may change as the contract evolves. Ordinary application code never imports this subpath. ::: ```ts import { createChannel, merge, foldBindings, resolveContext, defineInternal } from '@mycl/core/factory'; import type { Connector, ChannelSurface } from '@mycl/core/factory'; ``` One more export, `resolveSnapshot`, is intentionally undocumented: `createFnChannel`'s internal scope binder composes it, and it is not part of the documented surface. ### `createChannel` ```ts createChannel( name: G, // must not contain ':' or '/' (enforced at the type level) connector: Connector, ): K ``` Create a named **channel** for a **connector**, the once-per-kernel call that stands up a runtime. `createChannel` hands `name` to the connector, mints the channel over the connector's fresh context, and returns the kernel the connector's `build` curates from the channel's [`ChannelSurface`](#channelsurfaceg). There is no connectorless form: a channel always belongs to the connector that operates it, and its context never escapes, so two channels cannot share a scope stack by accident. The channel `name` (e.g. `'app'`) becomes the leading `name:` segment of every capability identifier minted through this channel's `capable` (see [Capability identifier](#capability-identifier)). The name **must not contain `:` or `/`**: banning `:` keeps the first `:` an unambiguous delimiter, so distinct name/path pairs can never assemble the same identifier, and `/` is reserved as the path segment separator. A name with either is a type error, and in dev throws [error 12](/errors/12/). Dots are fine (`my.kernel` is valid). ```ts import { stackContext } from '@mycl/core/context'; import { createChannel } from '@mycl/core/factory'; import type { ChannelSurface } from '@mycl/core/factory'; const myConnector = (_name: G) => ({ context: stackContext(), build: (surface: ChannelSurface) => surface, // curate your kernel here }); const { capable, snapshot, channel, context } = createChannel('my.kernel', myConnector); const greet = capable((name: string) => `hi ${name}`, 'greet'); // greet identifier: 'my.kernel:greet' ``` [`createFnChannel`](#createfnchannel) is exactly one such call bound to an internal connector. See [Building a Kernel](/advanced/building-a-kernel/) for the full walkthrough, including writing a connector of your own. ### `merge` ```ts merge(...registries: Registry[]): ResolvedRegistry ``` Compose whole registries into a single [`ResolvedRegistry`](#resolvedregistry), the dispatch-ready form a scope runs against. Later registries win per capability; augments accumulate across all of them. `merge()` with no arguments is the empty resolved registry. The same registry sequence (by object identity, in order) always returns the **same** instance, cached on a composite-symbol trie. This is the composition primitive a kernel's boundary members are built on: `mycl` and `scope` call it internally on their variadic registries, which is why channel users never import it. Reach for it directly when *you* are the kernel: a connector's boundary member runs a scope by hand, and a build plugin precomputes the resolved form. ```ts import { merge } from '@mycl/core/factory'; // A connector's boundary member: run fn inside a scope over the registries. run: (fn: () => T, ...regs: Registry[]): T => surface.context.run(merge(...regs), fn), ``` A precomputed `ResolvedRegistry` is still accepted by any kernel's `scope`/`mycl` as the **sole** scope source (`scope(fn, resolved)`), so tooling can hand its output back to the everyday surface. **Errors:** `merge` composes lazily and does not throw for valid registries. If a capability's `LayerStrategy.extract` returns a non-function, that surfaces as [error 2](/errors/2/) when the resolved scope dispatches the capability. A `ResolvedRegistry` cannot be re-composed: passing one to `merge` throws [error 15](/errors/15/) in dev. ### `Connector` ```ts type Connector = (name: G) => { context: ScopeContext; build(surface: ChannelSurface): K; }; ``` The per-environment bundle [`createChannel`](#createchannel) invokes: a function of the channel name returning the channel's **fresh context** and `build`, which shapes the public kernel `K` from the channel's [`ChannelSurface`](#channelsurfaceg). Core defines and invokes this contract but never implements one (the same pattern as `LayerStrategy`): the everyday implementation is the internal connector behind [`createFnChannel`](#createfnchannel), and a custom connector is how you pick a different context (e.g. [`alsContext`](#alscontext) from the start) or curate a different kernel surface. Two rules for connector authors: - **Mint the context inside the connector.** A context closed over from outside is shared by every channel the connector mints, collapsing their scopes into one. - **Write your connector generic over `G`** (`(name: G) => …`) so the kernel type carries each channel's name literal. The name parameter is the type-level carrier; it is also legitimately useful for diagnostics. The minimal **identity connector**, for tooling and infrastructure that want the raw channel surface: ```ts const bare = (_name: G) => ({ context: stackContext(), build: (surface: ChannelSurface) => surface, }); const { capable, snapshot, channel, context } = createChannel('infra', bare); ``` ### `ChannelSurface` ```ts interface ChannelSurface { capable: ( baseFn: T | Capability, idPath: IdPath, config?: CapableConfig, ) => Capability; snapshot: (fn: T) => Snapshotted; channel: Channel; context: ScopeContext; } ``` Everything core exposes per channel (capable, snapshot, token, context facade), handed to a [`Connector`](#connector)'s `build`, which curates it into the kernel (and what the identity connector returns verbatim). `capable` prefixes the channel name `G` onto the identifier path to form the `${G}:${Id}` identifier carried in the capability's type; `context` is the live facade that follows [`setChannelContext`](#setchannelcontext) swaps. What of this reaches users is each connector's choice: `createFnChannel`'s connector passes on everything but `context`. ### `foldBindings` ```ts foldBindings( cap: AnyCapability, // one binding per contributing registry; augments arrive boxed and are unboxed here entries: Iterable<{ argsList?: ReadonlyArray>; augments: ReadonlyArray }>, ): AnyFn | null ``` The single fold both the runtime path (`merge`, after it dedups a registry sequence into canonical binding values) and the build-plugin path route through, so the two can never compute a capability differently. Each binding's `augments` arrive as boxed singleton tuples, the shape `RegistryBindingValue` also carries at rest; `foldBindings` unboxes them as it collects. A build plugin folds a capability's collapsed binding stream through this eagerly, at module init, and stores the result (`AnyFn` = dispatch to it, `null` = use the base) in its own keying structure: typically a `WeakMap` behind a plain frozen [`ResolvedRegistry`](#resolvedregistry), which `scope`/`mycl` accept through the existing passthrough with no separate build-plugin code path. ### `resolveContext` ```ts resolveContext(channel: Channel): ScopeContext ``` The channel-keyed context accessor, for kernel authors who hold only the channel token. Returns a forwarding facade over the channel's context: it captures the channel's context cell once (validating provenance eagerly, [error 3](/errors/3/)) and reads the live context per call, so the facade follows [`setChannelContext`](#setchannelcontext) swaps made after it was created. `createChannel` builds its `.context` field through this; `createFnChannel`'s internal `mycl` and `scope` binders are its main consumers. ### `defineInternal` ```ts defineInternal(obj: T, key: PropertyKey, value: unknown): void ``` Define a non-writable, non-enumerable property: the substrate's helper for stamping capabilities and channel tokens with their hidden symbol slots. Exported for kernels that stamp their own internal metadata the same way. The capability symbols (tag, base, config, id, cache) stay private to the substrate: [`isCapability`](#iscapability) is the public detection surface. --- ## `@mycl/core/introspect` Three **pure, read-only** functions over a [`Registry`](#registryl). They never mutate, do I/O, or **invoke a capability**: they describe a registry's bindings and what a capability *would* resolve to, computed from the same entry shape dispatch reads. They live on their own subpath to stay out of the core critical-path bundle: you pay for them only if you import them. Being deterministic over the immutable registry makes them both an audit/test primitive and the runtime counterpart to the compile-time [provider-completeness check](/advanced/type-guarantees/#provider-completeness-with-requires). `describe` collides with the Vitest/Jest test global exactly where introspection is most used. Import the namespace to keep both: ```ts import * as introspect from '@mycl/core/introspect'; introspect.describe(reg); // [{ identity: 'my-app-or-library:db', layers: 1, augments: 0, strategy: 'last-wins' }, …] introspect.explain(reg, db); // { identity: 'my-app-or-library:db', bound: true, resolvesToBase: false, layers: 1, augments: 0, strategy: 'last-wins' } introspect.diff(regA, regB); // { added: ['my-app-or-library:log'], removed: [], changed: [] } ``` ### `describe` ```ts describe(reg: Registry): CapabilityDescription[] ``` List every capability `reg` binds. Each row is `{ identity, layers, augments, strategy }`. ### `explain` ```ts explain(reg: Registry, cap: AnyCapability): Explanation ``` What `cap` resolves to under `reg`, **without invoking it**: the debugging answer to "is my override taking?". Returns the `CapabilityDescription` fields plus `bound` (the registry carries any contribution for `cap`) and `resolvesToBase` (nothing is bound, so dispatch falls through to the capability's base). For example, `expect(explain(reg, cap).resolvesToBase).toBe(false)` asserts an override is in place. ### `diff` ```ts diff(regA: Registry, regB: Registry): RegistryDiff ``` Capability identities `{ added, removed, changed }` between two registries. `changed` is detected by a structural signature (layer count + augment count), so it catches added or removed contributions, but **not** a changed *value* at the same count (that would need a deep argument comparison). ### Introspection types #### `StrategyKind` ```ts type StrategyKind = 'last-wins' | 'custom' ``` Whether a capability merges via the implicit last-wins default or a custom [`LayerStrategy`](#layerstrategyt-v-args). #### `CapabilityDescription` ```ts interface CapabilityDescription { identity: string; layers: number; // count of .layer(cap, …) contributions augments: number; // count of .augment(cap, …) wrappers strategy: StrategyKind; } ``` One row of [`describe`](#describe). #### `Explanation` ```ts interface Explanation extends CapabilityDescription { bound: boolean; // registry carries any contribution for this capability resolvesToBase: boolean; // nothing bound → dispatch falls through to the base } ``` The result of [`explain`](#explain). #### `RegistryDiff` ```ts interface RegistryDiff { added: string[]; removed: string[]; changed: string[]; } ``` The result of [`diff`](#diff): identities in each bucket. --- ## Where next - [Capabilities guide](/guide/capabilities/): the mental model behind `capable`. - [Factories guide](/guide/factories/): `mycl`, `scope`, and `snapshot` in practice. - [Building a Kernel](/advanced/building-a-kernel/): the `/factory` and `/context` subpaths, for connector authors. - [The Core Substrate](/advanced/core-substrate/): how the entry split fits together. --- # Glossary > Every mycl term, defined once. One entry per term, alphabetized. **Bold** words inside a definition name another entry on this page. *Appears in* links go to the reference and guide pages where the term is defined in full. ## augment A wrapper applied via `.augment(cap, wrapper)` that composes around whatever the capability resolves to, without replacing it. *Not to be confused with:* a **layer**; a decorator/AOP pointcut. *Appears in:* [`.augment()`](/reference/core/#augmentcap-wrapper); [helpers](/reference/core/#myclcorehelpers); [Augmentation guide](/guide/augmentation/). ## binding One entry of a registry: a capability and everything bound to it, its layers and augments. The map returned by `.bindings()` holds one binding per capability; the value half is `RegistryBindingValue`. *Not to be confused with:* a **layer** (one `.layer()` call); an **augment** (one `.augment()` call); the ECMAScript sense of binding (a name bound to a value in a scope). *Appears in:* [`.bindings()`](/reference/core/#bindings); [`RegistryBindings`](/reference/core/#registrybindings). ## builder (`make`) The function passed as `mycl()`'s first argument; constructs and returns the instance, run inside the resolved scope. *Not to be confused with:* a JS class constructor; the returned **factory**. *Appears in:* [`mycl()`](/reference/core/#mycl). ## capability A function wrapped by `capable()` that is simultaneously its own registry key, type contract, default implementation, and stable identifier, dispatching through the active scope when called. *Not to be confused with:* a plain imported function; a class/service. *Appears in:* [`capable`](/reference/core/#capable), [Capability identifier](/reference/core/#capability-identifier); [Capabilities guide](/guide/capabilities/). ## capability constructor The bound `capable`: the function that mints capabilities for a channel, returned by `createChannel`/`createFnChannel`. *Not to be confused with:* a JS constructor; a **factory**. *Appears in:* [`capable`](/reference/core/#capable). ## channel A named capability namespace (`createChannel(name, connector)`) owning the `channel:` identifier prefix; the unit of context ownership and duplicate detection. Channels are disjoint by construction; each owns exactly one context. *Not to be confused with:* a **registry**; a package. *Appears in:* [`@mycl/core`](/reference/core/#createchannel); [kernel guide](/advanced/building-a-kernel/). ## connector What `createChannel` invokes to stand a channel up: a function of the channel name returning the channel's fresh context and `build`, which shapes the public kernel from the channel's surface. The everyday one is internal, behind `createFnChannel`; custom connectors pick other contexts or curate other surfaces. *Not to be confused with:* the **context** it mints; the **kernel** its build returns. *Appears in:* [`Connector`](/reference/core/#connector); [`createFnChannel`](/reference/core/#createfnchannel); [kernel guide](/advanced/building-a-kernel/). ## context A `ScopeContext` (`{ get, run }`) telling a channel how to read and set the active resolved registry; swappable via `setChannelContext`. *Not to be confused with:* a **scope** (the state the context carries). *Appears in:* [`@mycl/core`](/reference/core/#scopecontextr); [kernel guide](/advanced/building-a-kernel/). ## dispatch The call-time resolution a capability performs: consult the active scope, run the bound/folded/augmented implementation, else the base. *Not to be confused with:* binding (`.layer`); composition (`merge()`). *Appears in:* [Scopes guide](/guide/scopes/). ## duplicate detection The **duplicate-identifier** guard: `capable` reports (dev/test `console.error`, never throws) when two capabilities assemble one identifier, error 13. Duplicate *packages* need no guard: copies coexist by construction, and a library wanting a startup self-report hand-rolls the documented recipe. *Not to be confused with:* a package-copy guard (mycl ships none). *Appears in:* [error 13](/errors/13/); [Duplicate detection page](/advanced/duplicate-detection/). ## factory The frozen callable returned by `mycl(make, ...regs)`; registries resolve once at creation, `make` runs per call, its result returned verbatim. *Not to be confused with:* the **substrate**; the **capability constructor**; the **builder**. *Appears in:* [`mycl()`](/reference/core/#mycl); [Factories guide](/guide/factories/). ## identifier The assembled `channel:idPath` string (a flat `channel:idPath` for a private channel, or `channel:project/capability` when several packages share one, e.g. `my-app-or-library:fetchUser`): a capability's canonical public identifier, error label, and type-level layer-record key. *Not to be confused with:* the **identifier path** argument; the capability object itself, which is what dispatch keys on, not this string. *Appears in:* [`capable()`](/reference/core/#capable); [error 1](/errors/1/), [error 2](/errors/2/); [Capability identifier](/reference/core/#capability-identifier). ## identifier path The non-empty string you pass to `capable()` as `idPath` (conventionally `project/capability`); the channel name is prepended to form the full identifier. *Not to be confused with:* the full **identifier**; a file path. *Appears in:* [`capable()` signature](/reference/core/#capable); [error 11](/errors/11/). ## kernel The module that stands a channel up and exports an everyday API; the quick-start's one-call `createFnChannel(name)` module is one. What a connector's `build` returns. *Not to be confused with:* the **substrate** it builds on; a build plugin. *Appears in:* [core-substrate page](/advanced/core-substrate/). ## layer A single capability binding contributed by one `.layer(cap, value)` call: the folded unit within a registry. *Not to be confused with:* a whole registry in a stack; `LayerStrategy`; `merge()`. *Appears in:* [`.layer()`](/reference/core/#layercap-args); [Registries and layers guide](/guide/registries-and-layers/#a-layer-is-one-binding). ## layer strategy The `LayerStrategy` on a capability: a left fold over its `.layer()` values. **step** is the fold function (`acc: V | undefined`, so the strategy owns the no-prior-value case), **extract** projects the accumulated value into the callable, optional **seed** is the initial accumulator; default last-wins. *Not to be confused with:* `merge()`; "merge strategy" (misnomer); an **augment**. *Appears in:* [`capable()` config](/reference/core/#layerstrategyt-v-args); [Layer strategies guide](/guide/layer-strategies/). ## layers (type-level) The `Registry` type-level tuple recording every layered entry; read with `RegistryLayers`/`CapabilityLayers`/`ProvidedIds`. *Not to be confused with:* any runtime value. *Appears in:* [Type guarantees page](/advanced/type-guarantees/#reading-the-layers). ## merge The free function `merge(...registries)` composing whole registries into one resolved registry: later wins per capability, augments accumulate. Lives on `@mycl/core/factory`; `mycl()`/`scope()` call it internally on their variadic registries, so channel users never import it. *Not to be confused with:* `.layer()`; a **layer strategy**. *Appears in:* [`merge`](/reference/core/#merge). ## provider-completeness The compile-time check where `requires(...caps)(make)` makes `mycl(make, ...regs)` type-error unless the registries provide every required capability. Type-only; zero runtime effect. *Not to be confused with:* a runtime check; introspection. *Appears in:* [`requires`](/reference/core/#requires); [Type guarantees page](/advanced/type-guarantees/#provider-completeness-with-requires). ## registry An immutable, clone-on-write, keyed collection of per-capability **bindings** (`.layer()`/`.augment()`) created by `registry()`; every operation returns a new one. *Not to be confused with:* a **resolved registry**; a **layer**; a **binding**. *Appears in:* [`@mycl/core`](/reference/core/#registryl). ## resolved registry The composed, dispatch-ready output of `merge()` (or built internally by `scope()`/`mycl()`); the only form a scope dispatches against. *Not to be confused with:* a buildable `Registry`; a **scope**. *Appears in:* [`merge`](/reference/core/#merge) / [`scope`](/reference/core/#scope) / [`mycl`](/reference/core/#mycl) signatures; [`ResolvedRegistry`](/reference/core/#resolvedregistry). ## scope The active resolved registry on the call stack at invocation time; capabilities dispatch through it; calling with no scope throws loud. *Not to be confused with:* a **context** (the mechanism carrying it); a registry. *Appears in:* [`scope()`](/reference/core/#scope); [Scopes guide](/guide/scopes/). ## snapshot `snapshot(fn)` captures the scope active at call time (including "no scope") and replays it on every later invocation: the manual async-boundary carrier. *Not to be confused with:* `scope(fn)` (declares a scope up front); a data snapshot. *Appears in:* [`@mycl/core`](/reference/core/#snapshot); [Snapshots and async guide](/guide/snapshots-and-async/). ## substrate The mechanism layer under the everyday surface (capability dispatch, the strategy fold, type contracts), shipped on `@mycl/core`'s subpaths; what kernels build on. *Not to be confused with:* a **kernel**; mycl-the-whole-library. *Appears in:* [`@mycl/core`](/reference/core/). --- # Changelog > Release history for @mycl/core, one section per published version. ## 0.1.1 (2026-07-21) fix: export the Capable type from the main entry ## 0.1.0 (2026-07-20) Initial release: the capability/registry core. `createFnChannel` mints a private channel with the everyday kernel (`capable`, `snapshot`, `mycl`, `scope`, the `channel` token); `registry` and `requires` build and check registries, and `mycl`/`scope` compose their variadic registries themselves. Augmentation helpers ship on `/helpers`, `ScopeContext` implementations on `/context`, the connector contract (including `merge`) on `/factory`, and read-only registry introspection on `/introspect`. ESM-only, TypeScript 5.4 or newer, main entry under 2 KB min+gz. --- # Using mycl (for agents) > Terse operating manual for AI agents reading, writing, and extending mycl code, covering invariants, conventions, recipes, and a drop-in AGENTS.md block. Machine-optimized reference. Rules first, prose last. If you write mycl code, obey §2 exactly. ## 1. What mycl is mycl is a **capability/registry system** for TypeScript, not a DI framework. A **capability** is a function you call normally; a **registry** re-binds what that call runs, per **scope**, at the composition root. Layer defaults and overrides into an immutable registry, activate it for a call tree, and every capability inside dispatches through it. Call sites never change. > Mental model: **a capability is a function whose behavior the caller can re-bind per scope without touching the call site.** One package, `@mycl/core`, five entry points. The main entry is the everyday surface: `createFnChannel` (whose kernel carries `capable`, `snapshot`, `mycl`, `scope`, and the `channel` token; these are kernel members, **not** package exports), plus `registry`, `requires`, `setChannelContext`, and the guards; `/helpers` holds the augment helpers (`before`/`after`/`pipe`/`handleError`, deliberately not on main); `/context`, `/factory` (including `merge`, the composition primitive `mycl`/`scope` call internally), and `/introspect` are the substrate subpaths kernel and connector authors build on. Default to the main entry. ## 2. Invariants (never violate) - **Registries are immutable.** Every `.layer()` / `.augment()` returns a **new** registry (clone-on-write); nothing mutates in place. Keep the returned value. - **Capability identifier path is mandatory and non-empty** (convention: `project/capability` when several areas share a channel), and **stable once published**: it is the public id, error label, and type-level layer-record key. Changing it is a breaking change. - **Error codes are stable and never renumbered.** Match on the code, never the message. - **Scope is synchronous.** A capability called after an `await`, in a `.then()`, a timer, or any deferred callback has lost the scope and throws error 1. Carry it across the boundary with `snapshot`. - **Layer strategies own the `undefined`-acc case.** `step` receives `acc: V | undefined`; the strategy decides what "no prior value" means. No branching hides in the resolver; `seed` is optional. - **Augments compose first-added innermost, last-added outermost.** So at call time the last-added augment runs first and delegates inward to the base. - **Core ships mechanism, not policy.** There is **no** strategy library and no `defineStrategy`: you hand-roll strategies (§5.4). This is deliberate; do not look for one. - **`requires(...)` is compile-time only.** It tags a make's type and is enforced at `mycl(...)`; it has zero runtime effect and raises no throw. ## 3. Conventions (follow in any mycl-using project) - **Expose capabilities from a `./capabilities` package export** so consumers import the capability *value* (refactor-safe) rather than an identifier string: ```json "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs" }, "./capabilities": { "types": "./dist/capabilities/index.d.ts", "import": "./dist/capabilities/index.mjs" } } ``` - **Libraries take `@mycl/core` as a regular or peer dependency** (regular for the simplest install, peer to share one copy across libraries) and mint a private channel behind their own entry point: duplicate copies of mycl coexist harmlessly (channels are disjoint by construction, and every entry is side-effect-free). The real hazard is a consumer's tree resolving *your* package twice: registries built against one copy's capability tokens silently fall back to base in the other. That is a packaging problem, not a mycl setting. See [Installation → Library exports](/getting-started/installation/#library-exports) and [Duplicate detection](/advanced/duplicate-detection/) for the optional self-report recipe. - **Prefer `project/capability` identifier paths when several areas share a channel.** The `project` segment namespaces them so they never collide (duplicate-identifier, error 13); a private single-owner channel can use flat names. ## 4. Do / Don't | Do | Don't | |----|-------| | Build one `registry()` per test, holding only the layers under test | Call mycl "DI" or a dependency-injection framework (it is a capability/registry system) | | Compose whole registries by passing several to `mycl`/`scope` (later wins per capability; augments accumulate) | Mutate or monkey-patch a resolved function (rebind through a registry instead) | | `import * as introspect from '@mycl/core/introspect'`; use `introspect.explain(reg, cap)` (and `introspect.describe(reg)`) to debug "why isn't my override taking" | Catch mycl errors by message text (match the numeric code). Prod messages are only `mycl: error , visit https://mycl.dev/errors/ for more information.` | | Reach for the main entry (`createFnChannel` and friends) first | Reach for `/factory`, `/context`, or `/introspect` when the main entry suffices (those subpaths are for kernel/connector authors) | ## 5. Extending a mycl project Real API only. Every capability below is imported as a *value*, not referenced by identifier string. ### 5.1 Add a capability Define it, export it from `./capabilities`, layer a default where you compose. ```ts // capabilities.ts: surfaced via the "./capabilities" package export import { createFnChannel } from '@mycl/core'; const { capable } = createFnChannel('my-app-or-library'); export const fetchUser = capable( async (id: string) => ({ id, name: 'base' }), // base impl = the default 'fetchUser', // identifier path (mandatory) ); ``` ### 5.2 Override behavior at the composition root Bind a replacement in a registry, then activate it for a call tree with `scope`. ```ts import { createFnChannel, registry } from '@mycl/core'; const { scope } = createFnChannel('my-app-or-library'); import { fetchUser } from './capabilities'; const reg = registry().layer(fetchUser, async (id) => ({ id, name: 'override' })); scope(() => fetchUser('42'), reg)(); // dispatches the override; base untouched elsewhere ``` ### 5.3 Cross-cutting concerns via `.augment` + helpers Augments wrap the resolved callable without replacing it: logging, timing, error recovery. Compose several; they stack outer-wraps-inner. ```ts import { registry } from '@mycl/core'; import { after, handleError } from '@mycl/core/helpers'; import { fetchUser } from './capabilities'; const observed = registry() .augment(fetchUser, after((u) => console.log('fetched', u))) .augment(fetchUser, handleError((err) => { report(err); return { id: 'unknown', name: 'anonymous' }; })); ``` > `after` / `pipe` observe the capability's **raw** result. For an async capability that is the Promise, not the settled value. `await` inside your callback if you need the resolved value. ### 5.4 Hand-rolled layer strategy (~5 lines) No strategy library exists. A strategy is a left fold over the capability's layers: `step` is the fold function, `seed` the optional initial accumulator, `extract` the final projection into a callable. Write the fold your capability needs, inline. Here every layer *accumulates* instead of the default last-wins: ```ts import { createFnChannel } from '@mycl/core'; const { capable } = createFnChannel('my-app-or-library'); const middleware = capable(() => [] as Mw[], 'middleware', { strategy: { // acc is the array folded so far (undefined on the first layer, §2) step: (acc: Mw[] | undefined) => (m: Mw) => [...(acc ?? []), m], extract: (all: Mw[]) => () => all, // extract must return a function }, }); ``` ## 6. Drop this into your project's AGENTS.md Copy the block below into a consumer project (e.g. a library built on mycl) and fill the placeholder line. It is self-contained and useful verbatim. ```md ## This project uses mycl mycl is a capability/registry system (NOT dependency injection). A capability is a function whose behavior the caller re-binds per scope without touching the call site. Package `@mycl/core`, main entry = everyday API; `/factory`, `/context`, `/introspect` subpaths = substrate for kernel/connector authors. Invariants (never violate): - Registries are immutable: `.layer()` / `.augment()` return a NEW registry. Keep it. - Capability identifier path is mandatory and non-empty (convention: project/capability when several packages share a channel, flat otherwise), stable once published. - Error codes are stable and never renumbered: match the code, never the message (prod message is only `mycl: error , visit https://mycl.dev/errors/`). - Scope is synchronous: anything past an await / .then / timer needs `snapshot`. - Layer strategies own the `undefined`-parent (no-prior-value) case. - Augments compose first-added innermost, last-added outermost. - No strategy library exists on purpose: hand-roll strategies (a plain object, ~5 lines). - `requires(...)` is compile-time only, enforced at `mycl(...)`; zero runtime effect. Conventions: - Import capabilities as VALUES from the `./capabilities` export, never identifier strings. - Take `@mycl/core` as a regular or peer dependency (regular for the simplest install, peer to share one copy across libraries) and mint a private channel per package. - One `registry()` per test, holding only the layers under test. - Debug overrides with `import * as introspect from '@mycl/core/introspect'` → `introspect.explain(reg, cap)`. Full machine-readable docs: https://mycl.dev/llms-full.txt ``` ## Where next - [llms.txt](https://mycl.dev/llms.txt): the doc index for agents. - [llms-full.txt](https://mycl.dev/llms-full.txt): every doc page as one text file. - [`@mycl/core` reference](/reference/core/): every function and type, with signatures and error codes. - [Glossary](/reference/glossary/): each term defined once. ---