From bc9a112cba186b178c65fe645d130ca8f17d18c1 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:08:48 -0400 Subject: [PATCH 01/68] feat(kernel-utils): add sheaf programming module Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.ts | 11 + packages/kernel-utils/src/sheaf/README.md | 103 +++ packages/kernel-utils/src/sheaf/guard.test.ts | 193 ++++++ packages/kernel-utils/src/sheaf/guard.ts | 137 ++++ .../src/sheaf/sheafify.e2e.test.ts | 386 ++++++++++++ .../kernel-utils/src/sheaf/sheafify.test.ts | 593 ++++++++++++++++++ packages/kernel-utils/src/sheaf/sheafify.ts | 298 +++++++++ packages/kernel-utils/src/sheaf/stalk.test.ts | 168 +++++ packages/kernel-utils/src/sheaf/stalk.ts | 80 +++ packages/kernel-utils/src/sheaf/types.ts | 79 +++ 10 files changed, 2048 insertions(+) create mode 100644 packages/kernel-utils/src/sheaf/README.md create mode 100644 packages/kernel-utils/src/sheaf/guard.test.ts create mode 100644 packages/kernel-utils/src/sheaf/guard.ts create mode 100644 packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts create mode 100644 packages/kernel-utils/src/sheaf/sheafify.test.ts create mode 100644 packages/kernel-utils/src/sheaf/sheafify.ts create mode 100644 packages/kernel-utils/src/sheaf/stalk.test.ts create mode 100644 packages/kernel-utils/src/sheaf/stalk.ts create mode 100644 packages/kernel-utils/src/sheaf/types.ts diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index bc895d4a1b..94fa2d7a76 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -44,3 +44,14 @@ export { DEFAULT_MAX_DELAY_MS, } from './retry.ts'; export type { RetryBackoffOptions, RetryOnRetryInfo } from './retry.ts'; +export type { + Section, + PresheafSection, + Lift, + LiftContext, + Presheaf, + Sheaf, +} from './sheaf/types.ts'; +export { sheafify } from './sheaf/sheafify.ts'; +export { collectSheafGuard } from './sheaf/guard.ts'; +export { getStalk, guardCoversPoint } from './sheaf/stalk.ts'; diff --git a/packages/kernel-utils/src/sheaf/README.md b/packages/kernel-utils/src/sheaf/README.md new file mode 100644 index 0000000000..19b5f7d2a0 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/README.md @@ -0,0 +1,103 @@ +# Sheaf + +Runtime capability routing adapted from sheaf theory in algebraic topology. + +`sheafify({ name, sections })` produces a **sheaf** — an authority manager +over a presheaf of capabilities. The sheaf grants revocable dispatch sections +via `getSection`, tracks all delegated authority, and supports point-wise +revocation. + +## Concepts + +**Presheaf section** (`PresheafSection`) — The input data: a capability (exo) +paired with operational metadata, assigned over the open set defined by the +exo's guard. This is an element of the presheaf F = F_sem x F_op. + +> A `getBalance(string)` provider with `{ cost: 100 }` is one presheaf +> section. A `getBalance("alice")` provider with `{ cost: 1 }` is another, +> covering a narrower open set. + +**Germ** — An equivalence class of presheaf sections at an invocation point, +identified by metadata. At dispatch time, sections in the stalk with identical +metadata are collapsed into a single germ; the system picks an arbitrary +representative for dispatch. If two capabilities are indistinguishable by +metadata, the sheaf has no data to prefer one over the other. + +> Two `getBalance(string)` providers both with `{ cost: 1 }` collapse into +> one germ. The lift never sees both — it receives one representative. + +**Stalk** — The set of germs matching a specific `(method, args)` invocation, +computed at dispatch time by guard filtering and then collapsing equivalent +entries. + +> Stalk at `("getBalance", "alice")` might contain two germs (cost 1 vs 100); +> stalk at `("transfer", ...)` might contain one. + +**Lift** — An async function that selects one germ from a multi-germ stalk. +At dispatch time, metadata is decomposed into **constraints** (keys with the +same value across every germ — topologically determined, not a choice) and +**options** (the remaining keys — the lift's actual decision space). The lift +receives only options on each germ; constraints arrive separately in the +context. + +> `argmin` by cost, `argmin` by latency, or any custom selection logic. The +> lift is never invoked for single-germ stalks. + +**Sheaf** — The authority manager returned by `sheafify`. Holds the presheaf +data (captured at construction time) and a registry of all granted sections. + +``` +const sheaf = sheafify({ name: 'Wallet', sections }); +``` + +- `sheaf.getSection({ guard?, lift })` — produce a revocable dispatch exo +- `sheaf.revokePoint(method, ...args)` — revoke every granted section whose + guard covers the point +- `sheaf.getExported()` — union guard of all active (non-revoked) sections +- `sheaf.revokeAll()` — revoke every granted section + +## Dispatch pipeline + +At each invocation point `(method, args)` within a granted section: + +``` +getStalk(sections, method, args) presheaf → stalk (filter by guard) +collapseEquivalent(stalk) locality condition (quotient by metadata) +decomposeMetadata(collapsed) restriction map (constraints / options) +lift(stripped, { method, args, operational selection (extra-theoretic) + constraints }) +dispatch to collapsed[index].exo evaluation +``` + +## Design choices + +**Germ identity is metadata identity.** The collapse step quotients by +metadata: if two sections should be distinguishable, the caller must give them +distinguishable metadata. Sections with identical metadata are treated as +interchangeable. Under the sheaf condition (effect-equivalence), this recovers +the classical equivalence relation on germs. + +**Pseudosheafification.** The sheafification functor would precompute the full +etale space. This system defers to invocation time: compute the stalk, +collapse, decompose, lift. The trade-off is that global coherence (a lift +choosing consistently across points) is not guaranteed. + +**Restriction and gluing are implicit.** Guard restriction induces a +restriction map on metadata: restricting to a point filters the presheaf to +covering sections (`getStalk`), then `decomposeMetadata` strips the metadata +to distinguishing keys — the restricted metadata over that point. The join +works dually: the union of two sections has the join of their metadata, and +restriction at any point recovers the local distinguishing keys in O(n). +Gluing follows: compatible sections (equal metadata on their overlap) produce a +well-defined join. The dispatch pipeline computes all of this implicitly. The +remaining gap is `revokeSite` (revoking over an open set rather than a point), +which requires an `intersects` operator on guards not yet available. + +## Relationship to stacks + +This construction is more properly a **stack** in algebraic geometry. We call +it a sheaf because engineers already know "stack" as a LIFO data structure, and +the algebraic geometry term is unrelated. Within a germ, any representative +will do — authority-equivalence is asserted by constructor contract, not +verified at runtime. Between germs, metadata distinguishes them and the lift +resolves the choice. diff --git a/packages/kernel-utils/src/sheaf/guard.test.ts b/packages/kernel-utils/src/sheaf/guard.test.ts new file mode 100644 index 0000000000..35e5b75dc4 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/guard.test.ts @@ -0,0 +1,193 @@ +import { makeExo } from '@endo/exo'; +import { + M, + matches, + getInterfaceGuardPayload, + getMethodGuardPayload, +} from '@endo/patterns'; +import type { MethodGuard, Pattern } from '@endo/patterns'; +import { describe, it, expect } from 'vitest'; + +import { collectSheafGuard } from './guard.ts'; +import type { Section } from './types.ts'; + +const makeSection = ( + tag: string, + guards: Record, + methods: Record unknown>, +): Section => { + const interfaceGuard = M.interface(tag, guards); + return makeExo(tag, interfaceGuard, methods) as unknown as Section; +}; + +describe('collectSheafGuard', () => { + it('variable arity: add with 1, 2, and 3 args', () => { + const sections = [ + makeSection( + 'Calc:0', + { add: M.call(M.number()).returns(M.number()) }, + { add: (a: number) => a }, + ), + makeSection( + 'Calc:1', + { add: M.call(M.number(), M.number()).returns(M.number()) }, + { add: (a: number, b: number) => a + b }, + ), + makeSection( + 'Calc:2', + { + add: M.call(M.number(), M.number(), M.number()).returns(M.number()), + }, + { add: (a: number, b: number, cc: number) => a + b + cc }, + ), + ]; + + const guard = collectSheafGuard('Calc', sections); + const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + }; + const payload = getMethodGuardPayload(methodGuards.add) as unknown as { + argGuards: Pattern[]; + optionalArgGuards?: Pattern[]; + }; + + // 1 required arg (present in all), 2 optional (variable arity) + expect(payload.argGuards).toHaveLength(1); + expect(payload.optionalArgGuards).toHaveLength(2); + }); + + it('return guard union', () => { + const sections = [ + makeSection( + 'S:0', + { f: M.call(M.eq(0)).returns(M.eq(0)) }, + { f: (_: number) => 0 }, + ), + makeSection( + 'S:1', + { f: M.call(M.eq(1)).returns(M.eq(1)) }, + { f: (_: number) => 1 }, + ), + ]; + + const guard = collectSheafGuard('S', sections); + const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + }; + const { returnGuard } = getMethodGuardPayload( + methodGuards.f, + ) as unknown as { returnGuard: Pattern }; + + // Return guard is union of eq(0) and eq(1) + expect(matches(0, returnGuard)).toBe(true); + expect(matches(1, returnGuard)).toBe(true); + }); + + it('section with its own optional args: optional preserved in union', () => { + const sections = [ + makeSection( + 'Greeter', + { + greet: M.callWhen(M.string()) + .optional(M.string()) + .returns(M.string()), + }, + { greet: (name: string, _greeting?: string) => `hello ${name}` }, + ), + ]; + + const guard = collectSheafGuard('Greeter', sections); + const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + }; + const payload = getMethodGuardPayload(methodGuards.greet) as unknown as { + argGuards: Pattern[]; + optionalArgGuards?: Pattern[]; + }; + + expect(payload.argGuards).toHaveLength(1); + expect(payload.optionalArgGuards).toHaveLength(1); + }); + + it('rest arg guard preserved in collected union', () => { + const sections = [ + makeSection( + 'Logger', + { log: M.call(M.string()).rest(M.string()).returns(M.any()) }, + { log: (..._args: string[]) => undefined }, + ), + ]; + + const guard = collectSheafGuard('Logger', sections); + const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + }; + const payload = getMethodGuardPayload(methodGuards.log) as unknown as { + argGuards: Pattern[]; + optionalArgGuards?: Pattern[]; + restArgGuard?: Pattern; + }; + + expect(payload.argGuards).toHaveLength(1); + expect(payload.optionalArgGuards ?? []).toHaveLength(0); + expect(payload.restArgGuard).toBeDefined(); + }); + + it('rest arg guards unioned across sections', () => { + const sections = [ + makeSection( + 'A', + { log: M.call(M.string()).rest(M.string()).returns(M.any()) }, + { log: (..._args: string[]) => undefined }, + ), + makeSection( + 'B', + { log: M.call(M.string()).rest(M.number()).returns(M.any()) }, + { log: (..._args: unknown[]) => undefined }, + ), + ]; + + const guard = collectSheafGuard('AB', sections); + const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + }; + const { restArgGuard } = getMethodGuardPayload( + methodGuards.log, + ) as unknown as { restArgGuard?: Pattern }; + + expect(matches('hello', restArgGuard)).toBe(true); + expect(matches(42, restArgGuard)).toBe(true); + }); + + it('multi-method guard collection', () => { + const sections = [ + makeSection( + 'Multi:0', + { + translate: M.call(M.string(), M.string()).returns(M.string()), + }, + { + translate: (from: string, to: string) => `${from}->${to}`, + }, + ), + makeSection( + 'Multi:1', + { + translate: M.call(M.string(), M.string()).returns(M.string()), + summarize: M.call(M.string()).returns(M.string()), + }, + { + translate: (from: string, to: string) => `${from}->${to}`, + summarize: (text: string) => `summary: ${text}`, + }, + ), + ]; + + const guard = collectSheafGuard('Multi', sections); + const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + }; + expect('translate' in methodGuards).toBe(true); + expect('summarize' in methodGuards).toBe(true); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/guard.ts b/packages/kernel-utils/src/sheaf/guard.ts new file mode 100644 index 0000000000..6666e9d52c --- /dev/null +++ b/packages/kernel-utils/src/sheaf/guard.ts @@ -0,0 +1,137 @@ +import { GET_INTERFACE_GUARD } from '@endo/exo'; +import type { Methods } from '@endo/exo'; +import { + M, + getInterfaceGuardPayload, + getMethodGuardPayload, +} from '@endo/patterns'; +import type { InterfaceGuard, MethodGuard, Pattern } from '@endo/patterns'; + +import type { Section } from './types.ts'; + +export type MethodGuardPayload = { + argGuards: Pattern[]; + optionalArgGuards?: Pattern[]; + restArgGuard?: Pattern; + returnGuard: Pattern; +}; + +/** + * Naive union of guards via M.or — no pattern canonicalization. + * + * @param guards - Guards to union. + * @returns A single guard representing the union. + */ +const unionGuard = (guards: Pattern[]): Pattern => { + if (guards.length === 1) { + const [first] = guards; + return first; + } + return M.or(...guards); +}; + +/** + * Compute the union of all section guards — the open set covered by the sheafified facade. + * + * For each method name across all sections, collects the arg guards at each + * position and produces a union via M.or. Sections with fewer args than + * the maximum contribute to required args; the remainder become optional. + * + * @param name - The name for the collected interface guard. + * @param sections - The sections whose guards are collected. + * @returns An interface guard covering all sections. + */ +export const collectSheafGuard = ( + name: string, + sections: Section[], +): InterfaceGuard => { + const payloadsByMethod = new Map(); + + for (const section of sections) { + const interfaceGuard = section[GET_INTERFACE_GUARD]?.(); + if (!interfaceGuard) { + continue; + } + const { methodGuards } = getInterfaceGuardPayload( + interfaceGuard, + ) as unknown as { methodGuards: Record }; + for (const [methodName, methodGuard] of Object.entries(methodGuards)) { + const payload = getMethodGuardPayload( + methodGuard, + ) as unknown as MethodGuardPayload; + if (!payloadsByMethod.has(methodName)) { + payloadsByMethod.set(methodName, []); + } + const existing = payloadsByMethod.get(methodName); + existing?.push(payload); + } + } + + const getGuardAt = ( + payload: MethodGuardPayload, + idx: number, + ): Pattern | undefined => { + if (idx < payload.argGuards.length) { + return payload.argGuards[idx]; + } + return payload.optionalArgGuards?.[idx - payload.argGuards.length]; + }; + + const unionMethodGuards: Record = {}; + for (const [methodName, payloads] of payloadsByMethod) { + const minArity = Math.min( + ...payloads.map((payload) => payload.argGuards.length), + ); + const maxArity = Math.max( + ...payloads.map( + (payload) => + payload.argGuards.length + (payload.optionalArgGuards?.length ?? 0), + ), + ); + + const requiredArgGuards = []; + for (let idx = 0; idx < minArity; idx++) { + requiredArgGuards.push( + unionGuard(payloads.map((payload) => payload.argGuards[idx])), + ); + } + + const optionalArgGuards = []; + for (let idx = minArity; idx < maxArity; idx++) { + const guards = payloads + .map((payload) => getGuardAt(payload, idx)) + .filter((guard): guard is Pattern => guard !== undefined); + optionalArgGuards.push(unionGuard(guards)); + } + + const restArgGuards = payloads + .map((payload) => payload.restArgGuard) + .filter((restGuard): restGuard is Pattern => restGuard !== undefined); + const unionRestArgGuard = + restArgGuards.length > 0 ? unionGuard(restArgGuards) : undefined; + + const returnGuard = unionGuard( + payloads.map((payload) => payload.returnGuard), + ); + + const base = M.callWhen(...requiredArgGuards); + if (optionalArgGuards.length > 0 && unionRestArgGuard !== undefined) { + unionMethodGuards[methodName] = base + .optional(...optionalArgGuards) + .rest(unionRestArgGuard) + .returns(returnGuard); + } else if (optionalArgGuards.length > 0) { + unionMethodGuards[methodName] = base + .optional(...optionalArgGuards) + .returns(returnGuard); + } else if (unionRestArgGuard === undefined) { + unionMethodGuards[methodName] = base.returns(returnGuard); + } else { + unionMethodGuards[methodName] = base + .rest(unionRestArgGuard) + .returns(returnGuard); + } + } + + return M.interface(name, unionMethodGuards); +}; diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts new file mode 100644 index 0000000000..afee771b41 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -0,0 +1,386 @@ +import { makeExo } from '@endo/exo'; +import { M } from '@endo/patterns'; +import { describe, it, expect } from 'vitest'; + +import { sheafify } from './sheafify.ts'; +import type { Lift, PresheafSection, Section } from './types.ts'; + +// Thin cast for calling exo methods directly in tests without going through +// HandledPromise (which is not available in the test environment). +// eslint-disable-next-line id-length +const E = (obj: unknown) => + obj as Record Promise>; + +// --------------------------------------------------------------------------- +// E2E: cost-optimal routing +// --------------------------------------------------------------------------- + +describe('e2e: cost-optimal routing', () => { + it('argmin picks cheapest section, re-sheafification expands landscape', async () => { + const argmin: Lift<{ cost: number }> = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.cost ?? Infinity) < + (germs[bestIdx]!.metadata?.cost ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + const sections: PresheafSection<{ cost: number }>[] = [ + { + // Remote: covers all accounts, expensive + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (acct: string) => (acct === 'alice' ? 1000 : 500) }, + ) as unknown as Section, + metadata: { cost: 100 }, + }, + { + // Local cache: covers only 'alice', cheap + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.eq('alice')).returns(M.number()), + }), + { getBalance: (_acct: string) => 1000 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + + // alice: both sections match, argmin picks local (cost=1) + expect(await E(wallet).getBalance('alice')).toBe(1000); + + // bob: only remote matches (stalk=1, lift not invoked) + expect(await E(wallet).getBalance('bob')).toBe(500); + + // Expand with a broader local cache (cost=2), re-sheafify. + sections.push({ + exo: makeExo( + 'Wallet:2', + M.interface('Wallet:2', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (acct: string) => (acct === 'alice' ? 1000 : 500) }, + ) as unknown as Section, + metadata: { cost: 2 }, + }); + wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + + // bob: now remote (cost=100) and new local (cost=2) both match, argmin picks cost=2 + expect(await E(wallet).getBalance('bob')).toBe(500); + + // alice: three sections match, argmin still picks cost=1 + expect(await E(wallet).getBalance('alice')).toBe(1000); + }); +}); + +// --------------------------------------------------------------------------- +// E2E: multi-tier capability routing +// --------------------------------------------------------------------------- + +describe('e2e: multi-tier capability routing', () => { + // A wallet integrates multiple data sources. Each declares its coverage + // via guards and carries latency metadata. The sheaf routes every call + // to the fastest matching source — no manual if/else, no strategy + // registration, just: + // guards (what can handle it) + metadata (how fast) + lift (pick best) + + type Tier = { latencyMs: number; label: string }; + + const fastest: Lift = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.latencyMs ?? Infinity) < + (germs[bestIdx]!.metadata?.latencyMs ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + it('routes reads to the fastest matching tier and writes to the only capable section', async () => { + // Dispatch log — sections push their label on every call so we can + // observe which tier actually handled each request. + const log: string[] = []; + + // Shared ledger — all sections read from this, so the sheaf condition + // (effect-equivalence) holds by construction. + const ledger: Record = { + alice: 1000, + bob: 500, + carol: 250, + }; + + const sections: PresheafSection[] = []; + + // ── Tier 1: Network RPC ────────────────────────────────── + // Covers ALL accounts (M.string()), but slow (500ms). + sections.push({ + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { + getBalance: (acct: string) => { + log.push('network'); + return ledger[acct] ?? 0; + }, + }, + ) as unknown as Section, + metadata: { latencyMs: 500, label: 'network' }, + }); + + let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: fastest, + }); + + // Phase 1 — single backend: stalk is always 1, lift never fires. + expect(await E(wallet).getBalance('alice')).toBe(1000); + expect(await E(wallet).getBalance('bob')).toBe(500); + expect(await E(wallet).getBalance('dave')).toBe(0); + expect(log).toStrictEqual(['network', 'network', 'network']); + log.length = 0; + + // ── Tier 2: Local state for owned account ──────────────── + // Only covers 'alice' (M.eq), 1ms. + sections.push({ + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.eq('alice')).returns(M.number()), + }), + { + getBalance: (_acct: string) => { + log.push('local'); + return ledger.alice ?? 0; + }, + }, + ) as unknown as Section, + metadata: { latencyMs: 1, label: 'local' }, + }); + wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: fastest, + }); + + // Phase 2 — alice routes to local (1ms < 500ms), bob still hits network. + expect(await E(wallet).getBalance('alice')).toBe(1000); + expect(await E(wallet).getBalance('bob')).toBe(500); + expect(log).toStrictEqual(['local', 'network']); + log.length = 0; + + // ── Tier 3: In-memory cache for specific accounts ──────── + // Covers bob and carol via M.or, instant (0ms). + sections.push({ + exo: makeExo( + 'Wallet:2', + M.interface('Wallet:2', { + getBalance: M.call(M.or(M.eq('bob'), M.eq('carol'))).returns( + M.number(), + ), + }), + { + getBalance: (acct: string) => { + log.push('cache'); + return ledger[acct] ?? 0; + }, + }, + ) as unknown as Section, + metadata: { latencyMs: 0, label: 'cache' }, + }); + wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: fastest, + }); + + // Phase 3 — every known account hits its optimal tier. + expect(await E(wallet).getBalance('alice')).toBe(1000); // local (1ms) + expect(await E(wallet).getBalance('bob')).toBe(500); // cache (0ms) + expect(await E(wallet).getBalance('carol')).toBe(250); // cache (0ms) + expect(await E(wallet).getBalance('dave')).toBe(0); // network (only match) + expect(log).toStrictEqual(['local', 'cache', 'cache', 'network']); + log.length = 0; + + // ── Tier 4: Heterogeneous methods ──────────────────────── + // A write-capable section that declares `transfer`. None of the + // read-only tiers above declared it, so writes route here + // automatically — the guard algebra handles it, no config needed. + sections.push({ + exo: makeExo( + 'Wallet:3', + M.interface('Wallet:3', { + getBalance: M.call(M.string()).returns(M.number()), + transfer: M.call(M.string(), M.string(), M.number()).returns( + M.boolean(), + ), + }), + { + getBalance: (acct: string) => { + log.push('write-backend'); + return ledger[acct] ?? 0; + }, + transfer: (from: string, to: string, amt: number) => { + log.push('write-backend'); + const fromBal = ledger[from] ?? 0; + if (fromBal < amt) { + return false; + } + ledger[from] = fromBal - amt; + ledger[to] = (ledger[to] ?? 0) + amt; + return true; + }, + }, + ) as unknown as Section, + metadata: { latencyMs: 200, label: 'write-backend' }, + }); + wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: fastest, + }); + + // transfer: only write-backend declares it → stalk=1, lift bypassed. + const facade = wallet as unknown as Record< + string, + (...args: unknown[]) => unknown + >; + expect(await E(facade).transfer('alice', 'dave', 100)).toBe(true); + expect(log).toStrictEqual(['write-backend']); + log.length = 0; + + // The shared ledger is mutated. All tiers see the new state because + // they all close over the same ledger (sheaf condition by construction). + expect(await E(wallet).getBalance('alice')).toBe(900); // local (1ms), was 1000 + expect(await E(wallet).getBalance('dave')).toBe(100); // write-backend (200ms < 500ms) + expect(await E(wallet).getBalance('bob')).toBe(500); // cache, unchanged + expect(log).toStrictEqual(['local', 'write-backend', 'cache']); + }); + + it('same germ structure, different lifts, different routing', async () => { + // The lift is the operational policy — swap it and the same + // set of sections produces different routing behavior. + const ledger: Record = { alice: 1000, bob: 500 }; + + const build = (lift: Lift) => { + const log: string[] = []; + const sections: PresheafSection[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { + getBalance: (acct: string) => { + log.push('network'); + return ledger[acct] ?? 0; + }, + }, + ) as unknown as Section, + metadata: { latencyMs: 500, label: 'network' }, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { + getBalance: (acct: string) => { + log.push('mirror'); + return ledger[acct] ?? 0; + }, + }, + ) as unknown as Section, + metadata: { latencyMs: 50, label: 'mirror' }, + }, + ]; + + return { + wallet: sheafify({ name: 'Wallet', sections }).getSection({ lift }), + log, + }; + }; + + // Policy A: fastest wins (mirror at 50ms < network at 500ms). + const { wallet: walletA, log: logA } = build(fastest); + expect(await E(walletA).getBalance('alice')).toBe(1000); + expect(logA).toStrictEqual(['mirror']); + + // Policy B: highest latency wins (simulate "prefer-canonical-source"). + const slowest: Lift = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.latencyMs ?? 0) > + (germs[bestIdx]!.metadata?.latencyMs ?? 0) + ? idx + : bestIdx, + 0, + ), + ); + const { wallet: walletB, log: logB } = build(slowest); + expect(await E(walletB).getBalance('alice')).toBe(1000); + expect(logB).toStrictEqual(['network']); + }); +}); + +// --------------------------------------------------------------------------- +// E2E: preferAutonomous recovered as degenerate case +// --------------------------------------------------------------------------- + +describe('e2e: preferAutonomous recovered as degenerate case', () => { + it('binary push metadata recovers push-pull lift rule', async () => { + // Binary metadata: { push: true } = push section, { push: false } = pull + const preferPush: Lift<{ push: boolean }> = async (germs) => { + const pushIdx = germs.findIndex((entry) => entry.metadata?.push); + return Promise.resolve(pushIdx >= 0 ? pushIdx : 0); + }; + + const sections: PresheafSection<{ push: boolean }>[] = [ + { + // Pull section: M.any() guards, push=false + exo: makeExo( + 'PushPull:0', + M.interface('PushPull:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 999 }, + ) as unknown as Section, + metadata: { push: false }, + }, + { + // Push section: narrow guard, push=true + exo: makeExo( + 'PushPull:1', + M.interface('PushPull:1', { + getBalance: M.call(M.eq('alice')).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { push: true }, + }, + ]; + + const wallet = sheafify({ name: 'PushPull', sections }).getSection({ + lift: preferPush, + }); + + // alice: both match, preferPush picks push section + expect(await E(wallet).getBalance('alice')).toBe(42); + + // bob: only pull matches (stalk=1, lift bypassed) + expect(await E(wallet).getBalance('bob')).toBe(999); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts new file mode 100644 index 0000000000..8a0d268e4a --- /dev/null +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -0,0 +1,593 @@ +import { makeExo, GET_INTERFACE_GUARD } from '@endo/exo'; +import { M, getInterfaceGuardPayload } from '@endo/patterns'; +import { describe, it, expect } from 'vitest'; + +import { sheafify } from './sheafify.ts'; +import type { Lift, LiftContext, PresheafSection, Section } from './types.ts'; + +// Thin cast for calling exo methods directly in tests without going through +// HandledPromise (which is not available in the test environment). +// eslint-disable-next-line id-length +const E = (obj: unknown) => + obj as Record Promise>; + +// --------------------------------------------------------------------------- +// Unit: sheafify +// --------------------------------------------------------------------------- + +describe('sheafify', () => { + it('single-section bypass: lift not invoked', async () => { + let liftCalled = false; + const lift: Lift<{ cost: number }> = async (_germs) => { + liftCalled = true; + return Promise.resolve(0); + }; + + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ lift }); + expect(await E(wallet).getBalance('alice')).toBe(42); + expect(liftCalled).toBe(false); + }); + + it('zero-coverage throws', async () => { + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.eq('alice')).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: async (_germs) => Promise.resolve(0), + }); + await expect(E(wallet).getBalance('bob')).rejects.toThrow( + 'No section covers', + ); + }); + + it('lift receives metadata and picks winner', async () => { + const argmin: Lift<{ cost: number }> = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.cost ?? Infinity) < + (germs[bestIdx]!.metadata?.cost ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { cost: 100 }, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + // argmin picks cost=1 section which returns 42 + expect(await E(wallet).getBalance('alice')).toBe(42); + }); + + // eslint-disable-next-line vitest/prefer-lowercase-title + it('GET_INTERFACE_GUARD returns collected guard', () => { + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.eq('alice')).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { cost: 100 }, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.eq('bob')).returns(M.number()), + }), + { getBalance: (_acct: string) => 50 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: async (_germs) => Promise.resolve(0), + }); + const guard = wallet[GET_INTERFACE_GUARD](); + expect(guard).toBeDefined(); + + const { methodGuards } = getInterfaceGuardPayload(guard); + expect(methodGuards).toHaveProperty('getBalance'); + }); + + it('re-sheafification picks up new sections and methods', async () => { + const argmin: Lift<{ cost: number }> = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.cost ?? Infinity) < + (germs[bestIdx]!.metadata?.cost ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { cost: 100 }, + }, + ]; + + let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + expect(await E(wallet).getBalance('alice')).toBe(100); + + // Add a cheaper section with a new method to the sections array, re-sheafify. + sections.push({ + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + transfer: M.call(M.string(), M.string(), M.number()).returns( + M.boolean(), + ), + }), + { + getBalance: (_acct: string) => 42, + transfer: (_from: string, _to: string, _amt: number) => true, + }, + ) as unknown as Section, + metadata: { cost: 1 }, + }); + wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + + // argmin picks the cheaper section + expect(await E(wallet).getBalance('alice')).toBe(42); + // New method is available on the re-sheafified facade + const facade = wallet as unknown as Record< + string, + (...args: unknown[]) => unknown + >; + expect(await E(facade).transfer('alice', 'bob', 10)).toBe(true); + }); + + it('pre-built exo dispatches correctly', async () => { + const exo = makeExo( + 'bal', + M.interface('bal', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ); + const sections: PresheafSection<{ cost: number }>[] = [ + { exo: exo as unknown as Section, metadata: { cost: 1 } }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: async (_germs) => Promise.resolve(0), + }); + expect(await E(wallet).getBalance('alice')).toBe(42); + }); + + it('re-sheafification with pre-built exo picks up new methods', async () => { + const argmin: Lift<{ cost: number }> = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.cost ?? Infinity) < + (germs[bestIdx]!.metadata?.cost ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { cost: 100 }, + }, + ]; + + let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + expect(await E(wallet).getBalance('alice')).toBe(100); + + // Add a pre-built exo with a cheaper getBalance + new transfer method + const exo = makeExo( + 'cheap', + M.interface('cheap', { + getBalance: M.call(M.string()).returns(M.number()), + transfer: M.call(M.string(), M.string(), M.number()).returns( + M.boolean(), + ), + }), + { + getBalance: (_acct: string) => 42, + transfer: (_from: string, _to: string, _amt: number) => true, + }, + ); + sections.push({ exo: exo as unknown as Section, metadata: { cost: 1 } }); + wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + + // argmin picks the cheaper section + expect(await E(wallet).getBalance('alice')).toBe(42); + // New method is available on the re-sheafified facade + const facade = wallet as unknown as Record< + string, + (...args: unknown[]) => unknown + >; + expect(await E(facade).transfer('alice', 'bob', 10)).toBe(true); + }); + + it('guard reflected in GET_INTERFACE_GUARD for pre-built exo', () => { + const exo = makeExo( + 'bal', + M.interface('bal', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ); + const sections: PresheafSection<{ cost: number }>[] = [ + { exo: exo as unknown as Section, metadata: { cost: 1 } }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: async (_germs) => Promise.resolve(0), + }); + const guard = wallet[GET_INTERFACE_GUARD](); + expect(guard).toBeDefined(); + + const { methodGuards } = getInterfaceGuardPayload(guard); + expect(methodGuards).toHaveProperty('getBalance'); + }); + + it('lift receives constraints in context and only distinguishing metadata', async () => { + type Meta = { region: string; cost: number }; + let capturedGerms: PresheafSection>[] = []; + let capturedContext: LiftContext | undefined; + + const spy: Lift = async (germs, context) => { + capturedGerms = germs; + capturedContext = context; + return Promise.resolve(0); + }; + + const sections: PresheafSection[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { region: 'us', cost: 100 }, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { region: 'us', cost: 1 }, + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: spy, + }); + await E(wallet).getBalance('alice'); + + expect(capturedContext).toStrictEqual({ + method: 'getBalance', + args: ['alice'], + constraints: { region: 'us' }, + }); + expect(capturedGerms.map((germ) => germ.metadata)).toStrictEqual([ + { cost: 100 }, + { cost: 1 }, + ]); + }); + + it('all-shared metadata yields empty distinguishing metadata', async () => { + type Meta = { region: string }; + let capturedGerms: PresheafSection>[] = []; + let capturedContext: LiftContext | undefined; + + const spy: Lift = async (germs, context) => { + capturedGerms = germs; + capturedContext = context; + return Promise.resolve(0); + }; + + const sections: PresheafSection[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { region: 'us' }, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { region: 'us' }, + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: spy, + }); + await E(wallet).getBalance('alice'); + + // Both sections collapsed to one germ → lift not invoked + expect(capturedContext).toBeUndefined(); + expect(capturedGerms).toHaveLength(0); + }); + + it('collapses equivalent presheaf sections by metadata', async () => { + type Meta = { cost: number }; + let liftCalled = false; + + const sections: PresheafSection[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: async (_germs) => { + liftCalled = true; + return Promise.resolve(0); + }, + }); + await E(wallet).getBalance('alice'); + + // Both sections have identical metadata → collapsed to one germ → lift bypassed + expect(liftCalled).toBe(false); + }); + + it('mixed sections participate in lift', async () => { + const argmin: Lift<{ cost: number }> = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.cost ?? Infinity) < + (germs[bestIdx]!.metadata?.cost ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + const exo = makeExo( + 'cheap', + M.interface('cheap', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ); + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + metadata: { cost: 100 }, + }, + { exo: exo as unknown as Section, metadata: { cost: 1 } }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + lift: argmin, + }); + // argmin picks the exo section (cost=1) + expect(await E(wallet).getBalance('alice')).toBe(42); + }); + + // --------------------------------------------------------------------------- + // Revocation + // --------------------------------------------------------------------------- + + it('revokePoint revokes sections covering the point', async () => { + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const sheaf = sheafify({ name: 'Wallet', sections }); + const wallet = sheaf.getSection({ + lift: async () => Promise.resolve(0), + }); + + expect(await E(wallet).getBalance('alice')).toBe(42); + + sheaf.revokePoint('getBalance', 'alice'); + + // Entire section is revoked, not just the specific point + await expect(E(wallet).getBalance('alice')).rejects.toThrow( + 'Section revoked', + ); + await expect(E(wallet).getBalance('bob')).rejects.toThrow( + 'Section revoked', + ); + }); + + it('revokeAll revokes all sections', async () => { + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const sheaf = sheafify({ name: 'Wallet', sections }); + const wallet = sheaf.getSection({ + lift: async () => Promise.resolve(0), + }); + + expect(await E(wallet).getBalance('alice')).toBe(42); + + sheaf.revokeAll(); + + await expect(E(wallet).getBalance('alice')).rejects.toThrow( + 'Section revoked', + ); + }); + + it('getExported returns union of active section guards', () => { + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const sheaf = sheafify({ name: 'Wallet', sections }); + + // No sections granted yet + expect(sheaf.getExported()).toBeUndefined(); + + sheaf.getSection({ lift: async () => Promise.resolve(0) }); + + const exported = sheaf.getExported(); + expect(exported).toBeDefined(); + const { methodGuards } = getInterfaceGuardPayload(exported!); + expect(methodGuards).toHaveProperty('getBalance'); + }); + + it('getExported excludes revoked sections', () => { + const sections: PresheafSection<{ cost: number }>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: { cost: 1 }, + }, + ]; + + const sheaf = sheafify({ name: 'Wallet', sections }); + sheaf.getSection({ lift: async () => Promise.resolve(0) }); + + expect(sheaf.getExported()).toBeDefined(); + + sheaf.revokeAll(); + expect(sheaf.getExported()).toBeUndefined(); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts new file mode 100644 index 0000000000..ab55845ac4 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -0,0 +1,298 @@ +/** + * Sheafify a presheaf into an authority manager. + * + * `sheafify({ name, sections })` returns a `Sheaf` — an immutable object + * that tracks granted authority and produces revocable dispatch sections. + * + * Each dispatch through a granted section: + * 1. Computes the stalk (getStalk — presheaf sections matching the point) + * 2. Collapses equivalent germs (same metadata → one representative) + * 3. Decomposes metadata into constraints + options + * 4. Invokes the lift on the distinguished options + * 5. Dispatches to some element of the opted germ + */ + +import { makeExo } from '@endo/exo'; +import { + M, + getInterfaceGuardPayload, + getMethodGuardPayload, +} from '@endo/patterns'; +import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; + +import { stringify } from '../stringify.ts'; +import { collectSheafGuard } from './guard.ts'; +import type { MethodGuardPayload } from './guard.ts'; +import { getStalk, guardCoversPoint } from './stalk.ts'; +import type { Lift, PresheafSection, Section, Sheaf } from './types.ts'; + +/** + * Serialize metadata for equivalence-class keying (collapse step). + * + * @param metadata - The metadata value to serialize. + * @returns A string key for equivalence comparison. + */ +const metadataKey = (metadata: unknown): string => { + if (metadata === undefined || metadata === null) { + return 'null'; + } + if (typeof metadata !== 'object') { + return JSON.stringify(metadata); + } + const entries = Object.entries(metadata as Record).sort( + ([a], [b]) => a.localeCompare(b), + ); + return JSON.stringify(entries); +}; + +/** + * Collapse stalk entries into equivalence classes (germs) by metadata identity. + * Returns one representative per class; the choice within a class is arbitrary. + * + * @param stalk - The stalk entries to collapse. + * @returns One representative per equivalence class. + */ +const collapseEquivalent = ( + stalk: PresheafSection[], +): PresheafSection[] => { + const seen = new Set(); + const representatives: PresheafSection[] = []; + for (const entry of stalk) { + const key = metadataKey(entry.metadata); + if (!seen.has(key)) { + seen.add(key); + representatives.push(entry); + } + } + return representatives; +}; + +/** + * Decompose stalk metadata into constraints (shared by all germs) and + * stripped germs (carrying only distinguishing keys). + * + * @param stalk - The collapsed stalk entries. + * @returns Constraints and stripped germs. + */ +const decomposeMetadata = ( + stalk: PresheafSection[], +): { + constraints: Partial; + stripped: PresheafSection>[]; +} => { + const constraints: Record = {}; + + const first = stalk[0]?.metadata; + if (first !== undefined && first !== null && typeof first === 'object') { + for (const key of Object.keys(first as Record)) { + const val = (first as Record)[key]; + const shared = stalk.every((entry) => { + if ( + entry.metadata === undefined || + entry.metadata === null || + typeof entry.metadata !== 'object' + ) { + return false; + } + const meta = entry.metadata as Record; + return key in meta && meta[key] === val; + }); + if (shared) { + constraints[key] = val; + } + } + } + + const stripped = stalk.map((entry) => { + if ( + entry.metadata === undefined || + entry.metadata === null || + typeof entry.metadata !== 'object' + ) { + return { exo: entry.exo }; + } + const remaining: Record = {}; + for (const [key, val] of Object.entries( + entry.metadata as Record, + )) { + if (!(key in constraints)) { + remaining[key] = val; + } + } + return { exo: entry.exo, metadata: remaining as Partial }; + }); + + return { constraints: constraints as Partial, stripped }; +}; + +/** + * Upgrade all method guards to M.callWhen for async dispatch. + * + * @param resolvedGuard - The interface guard to upgrade. + * @returns A record of async method guards. + */ +const asyncifyMethodGuards = ( + resolvedGuard: InterfaceGuard, +): Record => { + const { methodGuards: resolvedMethodGuards } = getInterfaceGuardPayload( + resolvedGuard, + ) as unknown as { methodGuards: Record }; + + const asyncMethodGuards: Record = {}; + for (const [methodName, methodGuard] of Object.entries( + resolvedMethodGuards, + )) { + const { argGuards, optionalArgGuards, restArgGuard, returnGuard } = + getMethodGuardPayload(methodGuard) as unknown as MethodGuardPayload; + const optionals = optionalArgGuards ?? []; + const base = M.callWhen(...argGuards); + if (optionals.length > 0 && restArgGuard !== undefined) { + asyncMethodGuards[methodName] = base + .optional(...optionals) + .rest(restArgGuard) + .returns(returnGuard); + } else if (optionals.length > 0) { + asyncMethodGuards[methodName] = base + .optional(...optionals) + .returns(returnGuard); + } else if (restArgGuard === undefined) { + asyncMethodGuards[methodName] = base.returns(returnGuard); + } else { + asyncMethodGuards[methodName] = base + .rest(restArgGuard) + .returns(returnGuard); + } + } + return asyncMethodGuards; +}; + +type Grant = { + exo: Section; + guard: InterfaceGuard; + revoke: () => void; + isRevoked: () => boolean; +}; + +export const sheafify = ({ + name, + sections, +}: { + name: string; + sections: PresheafSection[]; +}): Sheaf => { + const frozenSections = [...sections]; + const grants: Grant[] = []; + + const getSection = ({ + guard, + lift, + }: { + guard?: InterfaceGuard; + lift: Lift; + }): object => { + const resolvedGuard = + guard ?? + collectSheafGuard( + name, + frozenSections.map(({ exo }) => exo), + ); + + const asyncMethodGuards = asyncifyMethodGuards(resolvedGuard); + const asyncGuard = M.interface(`${name}:section`, asyncMethodGuards); + + let revoked = false; + + const dispatch = async ( + method: string, + args: unknown[], + ): Promise => { + if (revoked) { + throw new Error(`Section revoked: ${name}`); + } + + const stalk = getStalk(frozenSections, method, args); + let winner: PresheafSection; + switch (stalk.length) { + case 0: + throw new Error(`No section covers ${method}(${stringify(args, 0)})`); + case 1: + winner = stalk[0] as PresheafSection; + break; + default: { + const collapsed = collapseEquivalent(stalk); + if (collapsed.length === 1) { + winner = collapsed[0] as PresheafSection; + break; + } + const { constraints, stripped } = decomposeMetadata(collapsed); + const index = await lift(stripped, { method, args, constraints }); + winner = collapsed[index] as PresheafSection; + break; + } + } + + const obj = winner.exo as Record unknown>; + const fn = obj[method]; + if (fn === undefined) { + throw new Error(`Section has guard for '${method}' but no handler`); + } + return fn.call(obj, ...args); + }; + + const handlers: Record Promise> = + {}; + for (const method of Object.keys(asyncMethodGuards)) { + handlers[method] = async (...args: unknown[]) => dispatch(method, args); + } + + const exo = makeExo( + `${name}:section`, + asyncGuard, + handlers, + ) as unknown as Section; + + grants.push({ + exo, + guard: resolvedGuard, + revoke: () => { + revoked = true; + }, + isRevoked: () => revoked, + }); + + return exo; + }; + + const revokePoint = (method: string, ...args: unknown[]): void => { + for (const grant of grants) { + if (!grant.isRevoked() && guardCoversPoint(grant.guard, method, args)) { + grant.revoke(); + } + } + }; + + const getExported = (): InterfaceGuard | undefined => { + const activeExos = grants + .filter((grant) => !grant.isRevoked()) + .map((grant) => grant.exo); + if (activeExos.length === 0) { + return undefined; + } + return collectSheafGuard(`${name}:exported`, activeExos); + }; + + const revokeAll = (): void => { + for (const grant of grants) { + if (!grant.isRevoked()) { + grant.revoke(); + } + } + }; + + return { + getSection, + revokePoint, + getExported, + revokeAll, + }; +}; diff --git a/packages/kernel-utils/src/sheaf/stalk.test.ts b/packages/kernel-utils/src/sheaf/stalk.test.ts new file mode 100644 index 0000000000..c0e909adea --- /dev/null +++ b/packages/kernel-utils/src/sheaf/stalk.test.ts @@ -0,0 +1,168 @@ +import { makeExo } from '@endo/exo'; +import { M } from '@endo/patterns'; +import type { MethodGuard } from '@endo/patterns'; +import { describe, it, expect } from 'vitest'; + +import { getStalk } from './stalk.ts'; +import type { PresheafSection, Section } from './types.ts'; + +const makePresheafSection = ( + tag: string, + guards: Record, + methods: Record unknown>, + metadata: { cost: number }, +): PresheafSection<{ cost: number }> => { + const interfaceGuard = M.interface(tag, guards); + const exo = makeExo(tag, interfaceGuard, methods); + return { exo: exo as unknown as Section, metadata }; +}; + +describe('getStalk', () => { + it('returns matching sections for a method and args', () => { + const sections = [ + makePresheafSection( + 'A', + { add: M.call(M.number(), M.number()).returns(M.number()) }, + { add: (a: number, b: number) => a + b }, + { cost: 1 }, + ), + makePresheafSection( + 'B', + { add: M.call(M.number(), M.number()).returns(M.number()) }, + { add: (a: number, b: number) => a + b }, + { cost: 2 }, + ), + ]; + + const stalk = getStalk(sections, 'add', [1, 2]); + expect(stalk).toHaveLength(2); + }); + + it('filters out sections without matching method', () => { + const sections = [ + makePresheafSection( + 'A', + { add: M.call(M.number()).returns(M.number()) }, + { add: (a: number) => a }, + { cost: 1 }, + ), + makePresheafSection( + 'B', + { sub: M.call(M.number()).returns(M.number()) }, + { sub: (a: number) => -a }, + { cost: 2 }, + ), + ]; + + const stalk = getStalk(sections, 'add', [1]); + expect(stalk).toHaveLength(1); + expect(stalk[0]!.metadata?.cost).toBe(1); + }); + + it('filters out sections with arg count mismatch', () => { + const sections = [ + makePresheafSection( + 'A', + { add: M.call(M.number(), M.number()).returns(M.number()) }, + { add: (a: number, b: number) => a + b }, + { cost: 1 }, + ), + ]; + + const stalk = getStalk(sections, 'add', [1]); + expect(stalk).toHaveLength(0); + }); + + it('filters out sections with arg type mismatch', () => { + const sections = [ + makePresheafSection( + 'A', + { add: M.call(M.number()).returns(M.number()) }, + { add: (a: number) => a }, + { cost: 1 }, + ), + ]; + + const stalk = getStalk(sections, 'add', ['not-a-number']); + expect(stalk).toHaveLength(0); + }); + + it('returns empty array when no sections match', () => { + const sections = [ + makePresheafSection( + 'A', + { add: M.call(M.eq('alice')).returns(M.number()) }, + { add: (_a: string) => 42 }, + { cost: 1 }, + ), + ]; + + const stalk = getStalk(sections, 'add', ['bob']); + expect(stalk).toHaveLength(0); + }); + + it('matches sections with optional args when optional arg is provided', () => { + const sections = [ + makePresheafSection( + 'A', + { + greet: M.callWhen(M.string()) + .optional(M.string()) + .returns(M.string()), + }, + { greet: (name: string, _greeting?: string) => `hello ${name}` }, + { cost: 1 }, + ), + ]; + + expect(getStalk(sections, 'greet', ['alice'])).toHaveLength(1); + expect(getStalk(sections, 'greet', ['alice', 'hi'])).toHaveLength(1); + expect(getStalk(sections, 'greet', [])).toHaveLength(0); + expect(getStalk(sections, 'greet', ['alice', 'hi', 'extra'])).toHaveLength( + 0, + ); + }); + + it('matches sections with rest args', () => { + const sections = [ + makePresheafSection( + 'A', + { log: M.call(M.string()).rest(M.string()).returns(M.any()) }, + { log: (..._args: string[]) => undefined }, + { cost: 1 }, + ), + ]; + + expect(getStalk(sections, 'log', ['info'])).toHaveLength(1); + expect(getStalk(sections, 'log', ['info', 'msg'])).toHaveLength(1); + expect(getStalk(sections, 'log', ['info', 'msg', 'extra'])).toHaveLength(1); + expect(getStalk(sections, 'log', [])).toHaveLength(0); + expect(getStalk(sections, 'log', [42])).toHaveLength(0); + }); + + it('returns all sections when all match', () => { + const sections = [ + makePresheafSection( + 'A', + { f: M.call(M.string()).returns(M.number()) }, + { f: () => 1 }, + { cost: 1 }, + ), + makePresheafSection( + 'B', + { f: M.call(M.string()).returns(M.number()) }, + { f: () => 2 }, + { cost: 2 }, + ), + makePresheafSection( + 'C', + { f: M.call(M.string()).returns(M.number()) }, + { f: () => 3 }, + { cost: 3 }, + ), + ]; + + const stalk = getStalk(sections, 'f', ['hello']); + expect(stalk).toHaveLength(3); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/stalk.ts b/packages/kernel-utils/src/sheaf/stalk.ts new file mode 100644 index 0000000000..2c17a5ecce --- /dev/null +++ b/packages/kernel-utils/src/sheaf/stalk.ts @@ -0,0 +1,80 @@ +/** + * Stalk computation: filter presheaf sections by guard matching. + */ + +import { GET_INTERFACE_GUARD } from '@endo/exo'; +import { + matches, + getInterfaceGuardPayload, + getMethodGuardPayload, +} from '@endo/patterns'; +import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; + +import type { MethodGuardPayload } from './guard.ts'; +import type { PresheafSection } from './types.ts'; + +/** + * Check whether an interface guard covers the invocation point (method, args). + * + * @param guard - The interface guard to test. + * @param method - The method name being invoked. + * @param args - The arguments to the method invocation. + * @returns True if the guard accepts the invocation. + */ +export const guardCoversPoint = ( + guard: InterfaceGuard, + method: string, + args: unknown[], +): boolean => { + const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + }; + if (!(method in methodGuards)) { + return false; + } + const methodGuard = methodGuards[method]; + if (!methodGuard) { + return false; + } + const { argGuards, optionalArgGuards, restArgGuard } = getMethodGuardPayload( + methodGuard, + ) as unknown as MethodGuardPayload; + const optionals = optionalArgGuards ?? []; + const maxFixedArgs = argGuards.length + optionals.length; + return ( + args.length >= argGuards.length && + (restArgGuard !== undefined || args.length <= maxFixedArgs) && + args + .slice(0, argGuards.length) + .every((arg, i) => matches(arg, argGuards[i])) && + args + .slice(argGuards.length, maxFixedArgs) + .every((arg, i) => matches(arg, optionals[i])) && + (restArgGuard === undefined || + args.slice(maxFixedArgs).every((arg) => matches(arg, restArgGuard))) + ); +}; + +/** + * Get the stalk at an invocation point. + * + * Returns the presheaf sections whose guards accept the given method + args. + * + * @param sections - The presheaf sections to filter. + * @param method - The method name being invoked. + * @param args - The arguments to the method invocation. + * @returns The presheaf sections whose guards accept the invocation. + */ +export const getStalk = ( + sections: PresheafSection[], + method: string, + args: unknown[], +): PresheafSection[] => { + return sections.filter(({ exo }) => { + const interfaceGuard = exo[GET_INTERFACE_GUARD]?.(); + if (!interfaceGuard) { + return false; + } + return guardCoversPoint(interfaceGuard, method, args); + }); +}; diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts new file mode 100644 index 0000000000..295155f7f5 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -0,0 +1,79 @@ +/** + * Sheaf types: the product decomposition F_sem x F_op. + * + * The section (guard + behavior) is the semantic component F_sem. + * The metadata is the operational component F_op. + * Effect-equivalence (the sheaf condition) is asserted by the interface: + * sections covering the same open set produce the same observable result. + */ + +import type { GET_INTERFACE_GUARD, Methods } from '@endo/exo'; +import type { InterfaceGuard } from '@endo/patterns'; + +/** A section: a capability covering a region of the interface topology. */ +export type Section = Partial & { + [K in typeof GET_INTERFACE_GUARD]?: (() => InterfaceGuard) | undefined; +}; + +/** + * A presheaf section: a section (F_sem) paired with optional metadata (F_op). + * + * This is the input data to sheafify — an (exo, metadata) pair assigned over + * the open set defined by the exo's guard. + */ +export type PresheafSection = { + exo: Section; + metadata?: MetaData; +}; + +/** + * Context passed to the lift alongside the stalk. + * + * `constraints` holds metadata keys whose values are identical across every + * germ in the stalk — these are topologically determined and not a choice. + * Typed as `Partial` because the actual partition is runtime-dependent. + */ +export type LiftContext = { + method: string; + args: unknown[]; + constraints: Partial; +}; + +/** + * Lift: selects one germ from the stalk when multiple germs remain after + * collapsing equivalent presheaf sections. + * + * Each germ carries only distinguishing metadata (options); shared metadata + * (constraints) is delivered separately in the context. + * + * Returns a Promise — the index into the germs array. + */ +export type Lift = ( + germs: PresheafSection>[], + context: LiftContext, +) => Promise; + +/** + * A presheaf: a plain array of presheaf sections. + */ +export type Presheaf = PresheafSection[]; + +/** + * A sheaf: an authority manager over a presheaf. + * + * Produces revocable dispatch sections via `getSection` and tracks all + * granted authority for auditing and revocation. + */ +export type Sheaf = { + /** Produce a revocable dispatch exo over the given guard (or the full union). */ + getSection: (opts: { + guard?: InterfaceGuard; + lift: Lift; + }) => object; + /** Revoke every granted section whose guard covers the point (method, ...args). */ + revokePoint: (method: string, ...args: unknown[]) => void; + /** Union guard of all active (non-revoked) granted sections, or undefined. */ + getExported: () => InterfaceGuard | undefined; + /** Revoke all granted sections. */ + revokeAll: () => void; +}; From 6168f0d59db2a5a288965bc052605abe53581692 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:09:39 -0400 Subject: [PATCH 02/68] test(kernel-utils): update index exports snapshot for sheaf and GET_DESCRIPTION Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index cc1985bc46..05ccbc4f3c 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -13,9 +13,12 @@ describe('index', () => { 'GET_DESCRIPTION', 'abortableDelay', 'calculateReconnectionBackoff', + 'collectSheafGuard', 'delay', 'fetchValidatedJson', 'fromHex', + 'getStalk', + 'guardCoversPoint', 'ifDefined', 'installWakeDetector', 'isCapData', @@ -35,6 +38,7 @@ describe('index', () => { 'prettifySmallcaps', 'retry', 'retryWithBackoff', + 'sheafify', 'stringify', 'toHex', 'waitUntilQuiescent', From 8be73e06d06e262442e4e1ac022d62395741b26e Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:22:33 -0400 Subject: [PATCH 03/68] refactor(kernel-utils): require guard in getSection, add getGlobalSection `getSection({ guard, lift })` now requires an explicit interface guard, mirroring how `makeExo` always requires one. `getGlobalSection({ lift })` is the new convenience wrapper that computes the full union guard from all presheaf sections, analogous to `makeDefaultExo`. Co-Authored-By: Claude Sonnet 4.6 --- .../src/sheaf/sheafify.e2e.test.ts | 18 +++++---- .../kernel-utils/src/sheaf/sheafify.test.ts | 38 ++++++++++--------- packages/kernel-utils/src/sheaf/sheafify.ts | 20 ++++++---- packages/kernel-utils/src/sheaf/types.ts | 9 ++--- 4 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts index afee771b41..b9717214be 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -54,7 +54,7 @@ describe('e2e: cost-optimal routing', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); @@ -75,7 +75,7 @@ describe('e2e: cost-optimal routing', () => { ) as unknown as Section, metadata: { cost: 2 }, }); - wallet = sheafify({ name: 'Wallet', sections }).getSection({ + wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); @@ -145,7 +145,7 @@ describe('e2e: multi-tier capability routing', () => { metadata: { latencyMs: 500, label: 'network' }, }); - let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: fastest, }); @@ -173,7 +173,7 @@ describe('e2e: multi-tier capability routing', () => { ) as unknown as Section, metadata: { latencyMs: 1, label: 'local' }, }); - wallet = sheafify({ name: 'Wallet', sections }).getSection({ + wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: fastest, }); @@ -202,7 +202,7 @@ describe('e2e: multi-tier capability routing', () => { ) as unknown as Section, metadata: { latencyMs: 0, label: 'cache' }, }); - wallet = sheafify({ name: 'Wallet', sections }).getSection({ + wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: fastest, }); @@ -246,7 +246,7 @@ describe('e2e: multi-tier capability routing', () => { ) as unknown as Section, metadata: { latencyMs: 200, label: 'write-backend' }, }); - wallet = sheafify({ name: 'Wallet', sections }).getSection({ + wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: fastest, }); @@ -308,7 +308,9 @@ describe('e2e: multi-tier capability routing', () => { ]; return { - wallet: sheafify({ name: 'Wallet', sections }).getSection({ lift }), + wallet: sheafify({ name: 'Wallet', sections }).getGlobalSection({ + lift, + }), log, }; }; @@ -373,7 +375,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }, ]; - const wallet = sheafify({ name: 'PushPull', sections }).getSection({ + const wallet = sheafify({ name: 'PushPull', sections }).getGlobalSection({ lift: preferPush, }); diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 8a0d268e4a..d350dd7170 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -36,7 +36,9 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ lift }); + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + lift, + }); expect(await E(wallet).getBalance('alice')).toBe(42); expect(liftCalled).toBe(false); }); @@ -55,7 +57,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: async (_germs) => Promise.resolve(0), }); await expect(E(wallet).getBalance('bob')).rejects.toThrow( @@ -99,7 +101,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); // argmin picks cost=1 section which returns 42 @@ -131,7 +133,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: async (_germs) => Promise.resolve(0), }); const guard = wallet[GET_INTERFACE_GUARD](); @@ -167,7 +169,7 @@ describe('sheafify', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); expect(await E(wallet).getBalance('alice')).toBe(100); @@ -189,7 +191,7 @@ describe('sheafify', () => { ) as unknown as Section, metadata: { cost: 1 }, }); - wallet = sheafify({ name: 'Wallet', sections }).getSection({ + wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); @@ -215,7 +217,7 @@ describe('sheafify', () => { { exo: exo as unknown as Section, metadata: { cost: 1 } }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: async (_germs) => Promise.resolve(0), }); expect(await E(wallet).getBalance('alice')).toBe(42); @@ -247,7 +249,7 @@ describe('sheafify', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', sections }).getSection({ + let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); expect(await E(wallet).getBalance('alice')).toBe(100); @@ -267,7 +269,7 @@ describe('sheafify', () => { }, ); sections.push({ exo: exo as unknown as Section, metadata: { cost: 1 } }); - wallet = sheafify({ name: 'Wallet', sections }).getSection({ + wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); @@ -293,7 +295,7 @@ describe('sheafify', () => { { exo: exo as unknown as Section, metadata: { cost: 1 } }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: async (_germs) => Promise.resolve(0), }); const guard = wallet[GET_INTERFACE_GUARD](); @@ -337,7 +339,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: spy, }); await E(wallet).getBalance('alice'); @@ -387,7 +389,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: spy, }); await E(wallet).getBalance('alice'); @@ -424,7 +426,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: async (_germs) => { liftCalled = true; return Promise.resolve(0); @@ -470,7 +472,7 @@ describe('sheafify', () => { { exo: exo as unknown as Section, metadata: { cost: 1 } }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getSection({ + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); // argmin picks the exo section (cost=1) @@ -496,7 +498,7 @@ describe('sheafify', () => { ]; const sheaf = sheafify({ name: 'Wallet', sections }); - const wallet = sheaf.getSection({ + const wallet = sheaf.getGlobalSection({ lift: async () => Promise.resolve(0), }); @@ -528,7 +530,7 @@ describe('sheafify', () => { ]; const sheaf = sheafify({ name: 'Wallet', sections }); - const wallet = sheaf.getSection({ + const wallet = sheaf.getGlobalSection({ lift: async () => Promise.resolve(0), }); @@ -560,7 +562,7 @@ describe('sheafify', () => { // No sections granted yet expect(sheaf.getExported()).toBeUndefined(); - sheaf.getSection({ lift: async () => Promise.resolve(0) }); + sheaf.getGlobalSection({ lift: async () => Promise.resolve(0) }); const exported = sheaf.getExported(); expect(exported).toBeDefined(); @@ -583,7 +585,7 @@ describe('sheafify', () => { ]; const sheaf = sheafify({ name: 'Wallet', sections }); - sheaf.getSection({ lift: async () => Promise.resolve(0) }); + sheaf.getGlobalSection({ lift: async () => Promise.resolve(0) }); expect(sheaf.getExported()).toBeDefined(); diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index ab55845ac4..faa9c5cfc9 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -187,15 +187,10 @@ export const sheafify = ({ guard, lift, }: { - guard?: InterfaceGuard; + guard: InterfaceGuard; lift: Lift; }): object => { - const resolvedGuard = - guard ?? - collectSheafGuard( - name, - frozenSections.map(({ exo }) => exo), - ); + const resolvedGuard = guard; const asyncMethodGuards = asyncifyMethodGuards(resolvedGuard); const asyncGuard = M.interface(`${name}:section`, asyncMethodGuards); @@ -263,6 +258,16 @@ export const sheafify = ({ return exo; }; + const getGlobalSection = ({ lift }: { lift: Lift }): object => { + return getSection({ + guard: collectSheafGuard( + name, + frozenSections.map(({ exo }) => exo), + ), + lift, + }); + }; + const revokePoint = (method: string, ...args: unknown[]): void => { for (const grant of grants) { if (!grant.isRevoked() && guardCoversPoint(grant.guard, method, args)) { @@ -291,6 +296,7 @@ export const sheafify = ({ return { getSection, + getGlobalSection, revokePoint, getExported, revokeAll, diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index 295155f7f5..abb78ab890 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -65,11 +65,10 @@ export type Presheaf = PresheafSection[]; * granted authority for auditing and revocation. */ export type Sheaf = { - /** Produce a revocable dispatch exo over the given guard (or the full union). */ - getSection: (opts: { - guard?: InterfaceGuard; - lift: Lift; - }) => object; + /** Produce a revocable dispatch exo over the given guard. */ + getSection: (opts: { guard: InterfaceGuard; lift: Lift }) => object; + /** Produce a revocable dispatch exo over the full union guard of all presheaf sections. */ + getGlobalSection: (opts: { lift: Lift }) => object; /** Revoke every granted section whose guard covers the point (method, ...args). */ revokePoint: (method: string, ...args: unknown[]) => void; /** Union guard of all active (non-revoked) granted sections, or undefined. */ From aaae4b5cc902d8dd965705e16b4d160a0894ae95 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:44:14 -0400 Subject: [PATCH 04/68] test(kernel-utils): use vi.fn() spies in sheafify e2e tests to verify dispatch Replace string-log side-channels and return-value inference with explicit vi.fn() spy assertions. Each section's handler is a named mock; tests call expect(spy).toHaveBeenCalledWith(...) and .not.toHaveBeenCalled() to verify routing directly rather than inferring it from coincident return values. Co-Authored-By: Claude Sonnet 4.6 --- .../src/sheaf/sheafify.e2e.test.ts | 263 ++++++++++-------- 1 file changed, 143 insertions(+), 120 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts index b9717214be..e355d59c98 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -1,6 +1,6 @@ import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { sheafify } from './sheafify.ts'; import type { Lift, PresheafSection, Section } from './types.ts'; @@ -29,6 +29,9 @@ describe('e2e: cost-optimal routing', () => { ), ); + const remote0GetBalance = vi.fn((_acct: string): number => 0); + const local1GetBalance = vi.fn((_acct: string): number => 0); + const sections: PresheafSection<{ cost: number }>[] = [ { // Remote: covers all accounts, expensive @@ -37,7 +40,7 @@ describe('e2e: cost-optimal routing', () => { M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), - { getBalance: (acct: string) => (acct === 'alice' ? 1000 : 500) }, + { getBalance: remote0GetBalance }, ) as unknown as Section, metadata: { cost: 100 }, }, @@ -48,7 +51,7 @@ describe('e2e: cost-optimal routing', () => { M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), - { getBalance: (_acct: string) => 1000 }, + { getBalance: local1GetBalance }, ) as unknown as Section, metadata: { cost: 1 }, }, @@ -59,19 +62,26 @@ describe('e2e: cost-optimal routing', () => { }); // alice: both sections match, argmin picks local (cost=1) - expect(await E(wallet).getBalance('alice')).toBe(1000); + await E(wallet).getBalance('alice'); + expect(local1GetBalance).toHaveBeenCalledWith('alice'); + expect(remote0GetBalance).not.toHaveBeenCalled(); + local1GetBalance.mockClear(); // bob: only remote matches (stalk=1, lift not invoked) - expect(await E(wallet).getBalance('bob')).toBe(500); + await E(wallet).getBalance('bob'); + expect(remote0GetBalance).toHaveBeenCalledWith('bob'); + expect(local1GetBalance).not.toHaveBeenCalled(); + remote0GetBalance.mockClear(); // Expand with a broader local cache (cost=2), re-sheafify. + const local2GetBalance = vi.fn((_acct: string): number => 0); sections.push({ exo: makeExo( 'Wallet:2', M.interface('Wallet:2', { getBalance: M.call(M.string()).returns(M.number()), }), - { getBalance: (acct: string) => (acct === 'alice' ? 1000 : 500) }, + { getBalance: local2GetBalance }, ) as unknown as Section, metadata: { cost: 2 }, }); @@ -80,10 +90,16 @@ describe('e2e: cost-optimal routing', () => { }); // bob: now remote (cost=100) and new local (cost=2) both match, argmin picks cost=2 - expect(await E(wallet).getBalance('bob')).toBe(500); + await E(wallet).getBalance('bob'); + expect(local2GetBalance).toHaveBeenCalledWith('bob'); + expect(remote0GetBalance).not.toHaveBeenCalled(); + local2GetBalance.mockClear(); // alice: three sections match, argmin still picks cost=1 - expect(await E(wallet).getBalance('alice')).toBe(1000); + await E(wallet).getBalance('alice'); + expect(local1GetBalance).toHaveBeenCalledWith('alice'); + expect(remote0GetBalance).not.toHaveBeenCalled(); + expect(local2GetBalance).not.toHaveBeenCalled(); }); }); @@ -113,10 +129,6 @@ describe('e2e: multi-tier capability routing', () => { ); it('routes reads to the fastest matching tier and writes to the only capable section', async () => { - // Dispatch log — sections push their label on every call so we can - // observe which tier actually handled each request. - const log: string[] = []; - // Shared ledger — all sections read from this, so the sheaf condition // (effect-equivalence) holds by construction. const ledger: Record = { @@ -125,6 +137,26 @@ describe('e2e: multi-tier capability routing', () => { carol: 250, }; + const networkGetBalance = vi.fn( + (acct: string): number => ledger[acct] ?? 0, + ); + const localGetBalance = vi.fn((_acct: string): number => ledger.alice ?? 0); + const cacheGetBalance = vi.fn((acct: string): number => ledger[acct] ?? 0); + const writeBackendGetBalance = vi.fn( + (acct: string): number => ledger[acct] ?? 0, + ); + const writeBackendTransfer = vi.fn( + (from: string, to: string, amt: number): boolean => { + const fromBal = ledger[from] ?? 0; + if (fromBal < amt) { + return false; + } + ledger[from] = fromBal - amt; + ledger[to] = (ledger[to] ?? 0) + amt; + return true; + }, + ); + const sections: PresheafSection[] = []; // ── Tier 1: Network RPC ────────────────────────────────── @@ -135,12 +167,7 @@ describe('e2e: multi-tier capability routing', () => { M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), - { - getBalance: (acct: string) => { - log.push('network'); - return ledger[acct] ?? 0; - }, - }, + { getBalance: networkGetBalance }, ) as unknown as Section, metadata: { latencyMs: 500, label: 'network' }, }); @@ -150,11 +177,14 @@ describe('e2e: multi-tier capability routing', () => { }); // Phase 1 — single backend: stalk is always 1, lift never fires. - expect(await E(wallet).getBalance('alice')).toBe(1000); - expect(await E(wallet).getBalance('bob')).toBe(500); - expect(await E(wallet).getBalance('dave')).toBe(0); - expect(log).toStrictEqual(['network', 'network', 'network']); - log.length = 0; + await E(wallet).getBalance('alice'); + await E(wallet).getBalance('bob'); + await E(wallet).getBalance('dave'); + expect(networkGetBalance).toHaveBeenCalledTimes(3); + expect(networkGetBalance).toHaveBeenCalledWith('alice'); + expect(networkGetBalance).toHaveBeenCalledWith('bob'); + expect(networkGetBalance).toHaveBeenCalledWith('dave'); + networkGetBalance.mockClear(); // ── Tier 2: Local state for owned account ──────────────── // Only covers 'alice' (M.eq), 1ms. @@ -164,12 +194,7 @@ describe('e2e: multi-tier capability routing', () => { M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), - { - getBalance: (_acct: string) => { - log.push('local'); - return ledger.alice ?? 0; - }, - }, + { getBalance: localGetBalance }, ) as unknown as Section, metadata: { latencyMs: 1, label: 'local' }, }); @@ -178,10 +203,14 @@ describe('e2e: multi-tier capability routing', () => { }); // Phase 2 — alice routes to local (1ms < 500ms), bob still hits network. - expect(await E(wallet).getBalance('alice')).toBe(1000); - expect(await E(wallet).getBalance('bob')).toBe(500); - expect(log).toStrictEqual(['local', 'network']); - log.length = 0; + await E(wallet).getBalance('alice'); + await E(wallet).getBalance('bob'); + expect(localGetBalance).toHaveBeenCalledWith('alice'); + expect(networkGetBalance).toHaveBeenCalledWith('bob'); + expect(networkGetBalance).not.toHaveBeenCalledWith('alice'); + expect(localGetBalance).not.toHaveBeenCalledWith('bob'); + localGetBalance.mockClear(); + networkGetBalance.mockClear(); // ── Tier 3: In-memory cache for specific accounts ──────── // Covers bob and carol via M.or, instant (0ms). @@ -193,12 +222,7 @@ describe('e2e: multi-tier capability routing', () => { M.number(), ), }), - { - getBalance: (acct: string) => { - log.push('cache'); - return ledger[acct] ?? 0; - }, - }, + { getBalance: cacheGetBalance }, ) as unknown as Section, metadata: { latencyMs: 0, label: 'cache' }, }); @@ -207,12 +231,20 @@ describe('e2e: multi-tier capability routing', () => { }); // Phase 3 — every known account hits its optimal tier. - expect(await E(wallet).getBalance('alice')).toBe(1000); // local (1ms) - expect(await E(wallet).getBalance('bob')).toBe(500); // cache (0ms) - expect(await E(wallet).getBalance('carol')).toBe(250); // cache (0ms) - expect(await E(wallet).getBalance('dave')).toBe(0); // network (only match) - expect(log).toStrictEqual(['local', 'cache', 'cache', 'network']); - log.length = 0; + await E(wallet).getBalance('alice'); // local (1ms) + await E(wallet).getBalance('bob'); // cache (0ms) + await E(wallet).getBalance('carol'); // cache (0ms) + await E(wallet).getBalance('dave'); // network (only match) + expect(localGetBalance).toHaveBeenCalledWith('alice'); + expect(cacheGetBalance).toHaveBeenCalledWith('bob'); + expect(cacheGetBalance).toHaveBeenCalledWith('carol'); + expect(networkGetBalance).toHaveBeenCalledWith('dave'); + expect(networkGetBalance).toHaveBeenCalledTimes(1); + expect(localGetBalance).toHaveBeenCalledTimes(1); + expect(cacheGetBalance).toHaveBeenCalledTimes(2); + localGetBalance.mockClear(); + cacheGetBalance.mockClear(); + networkGetBalance.mockClear(); // ── Tier 4: Heterogeneous methods ──────────────────────── // A write-capable section that declares `transfer`. None of the @@ -228,20 +260,8 @@ describe('e2e: multi-tier capability routing', () => { ), }), { - getBalance: (acct: string) => { - log.push('write-backend'); - return ledger[acct] ?? 0; - }, - transfer: (from: string, to: string, amt: number) => { - log.push('write-backend'); - const fromBal = ledger[from] ?? 0; - if (fromBal < amt) { - return false; - } - ledger[from] = fromBal - amt; - ledger[to] = (ledger[to] ?? 0) + amt; - return true; - }, + getBalance: writeBackendGetBalance, + transfer: writeBackendTransfer, }, ) as unknown as Section, metadata: { latencyMs: 200, label: 'write-backend' }, @@ -255,70 +275,61 @@ describe('e2e: multi-tier capability routing', () => { string, (...args: unknown[]) => unknown >; - expect(await E(facade).transfer('alice', 'dave', 100)).toBe(true); - expect(log).toStrictEqual(['write-backend']); - log.length = 0; + await E(facade).transfer('alice', 'dave', 100); + expect(writeBackendTransfer).toHaveBeenCalledWith('alice', 'dave', 100); + writeBackendTransfer.mockClear(); // The shared ledger is mutated. All tiers see the new state because // they all close over the same ledger (sheaf condition by construction). - expect(await E(wallet).getBalance('alice')).toBe(900); // local (1ms), was 1000 - expect(await E(wallet).getBalance('dave')).toBe(100); // write-backend (200ms < 500ms) - expect(await E(wallet).getBalance('bob')).toBe(500); // cache, unchanged - expect(log).toStrictEqual(['local', 'write-backend', 'cache']); + await E(wallet).getBalance('alice'); // local (1ms), was 1000 + await E(wallet).getBalance('dave'); // write-backend (200ms < 500ms for dave) + await E(wallet).getBalance('bob'); // cache, unchanged + expect(localGetBalance).toHaveBeenCalledWith('alice'); + expect(writeBackendGetBalance).toHaveBeenCalledWith('dave'); + expect(cacheGetBalance).toHaveBeenCalledWith('bob'); + expect(ledger.alice).toBe(900); + expect(ledger.dave).toBe(100); + expect(ledger.bob).toBe(500); }); it('same germ structure, different lifts, different routing', async () => { // The lift is the operational policy — swap it and the same // set of sections produces different routing behavior. - const ledger: Record = { alice: 1000, bob: 500 }; + const networkGetBalance = vi.fn((_acct: string): number => 0); + const mirrorGetBalance = vi.fn((_acct: string): number => 0); - const build = (lift: Lift) => { - const log: string[] = []; - const sections: PresheafSection[] = [ - { - exo: makeExo( - 'Wallet:0', - M.interface('Wallet:0', { - getBalance: M.call(M.string()).returns(M.number()), - }), - { - getBalance: (acct: string) => { - log.push('network'); - return ledger[acct] ?? 0; - }, - }, - ) as unknown as Section, - metadata: { latencyMs: 500, label: 'network' }, - }, - { - exo: makeExo( - 'Wallet:1', - M.interface('Wallet:1', { - getBalance: M.call(M.string()).returns(M.number()), - }), - { - getBalance: (acct: string) => { - log.push('mirror'); - return ledger[acct] ?? 0; - }, - }, - ) as unknown as Section, - metadata: { latencyMs: 50, label: 'mirror' }, - }, - ]; - - return { - wallet: sheafify({ name: 'Wallet', sections }).getGlobalSection({ - lift, - }), - log, - }; - }; + const makeSections = (): PresheafSection[] => [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: networkGetBalance }, + ) as unknown as Section, + metadata: { latencyMs: 500, label: 'network' }, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: mirrorGetBalance }, + ) as unknown as Section, + metadata: { latencyMs: 50, label: 'mirror' }, + }, + ]; // Policy A: fastest wins (mirror at 50ms < network at 500ms). - const { wallet: walletA, log: logA } = build(fastest); - expect(await E(walletA).getBalance('alice')).toBe(1000); - expect(logA).toStrictEqual(['mirror']); + const walletA = sheafify({ + name: 'Wallet', + sections: makeSections(), + }).getGlobalSection({ lift: fastest }); + await E(walletA).getBalance('alice'); + expect(mirrorGetBalance).toHaveBeenCalledWith('alice'); + expect(networkGetBalance).not.toHaveBeenCalled(); + mirrorGetBalance.mockClear(); // Policy B: highest latency wins (simulate "prefer-canonical-source"). const slowest: Lift = async (germs) => @@ -332,9 +343,13 @@ describe('e2e: multi-tier capability routing', () => { 0, ), ); - const { wallet: walletB, log: logB } = build(slowest); - expect(await E(walletB).getBalance('alice')).toBe(1000); - expect(logB).toStrictEqual(['network']); + const walletB = sheafify({ + name: 'Wallet', + sections: makeSections(), + }).getGlobalSection({ lift: slowest }); + await E(walletB).getBalance('alice'); + expect(networkGetBalance).toHaveBeenCalledWith('alice'); + expect(mirrorGetBalance).not.toHaveBeenCalled(); }); }); @@ -350,15 +365,18 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { return Promise.resolve(pushIdx >= 0 ? pushIdx : 0); }; + const pullGetBalance = vi.fn((_acct: string): number => 0); + const pushGetBalance = vi.fn((_acct: string): number => 0); + const sections: PresheafSection<{ push: boolean }>[] = [ { - // Pull section: M.any() guards, push=false + // Pull section: M.string() guards, push=false exo: makeExo( 'PushPull:0', M.interface('PushPull:0', { getBalance: M.call(M.string()).returns(M.number()), }), - { getBalance: (_acct: string) => 999 }, + { getBalance: pullGetBalance }, ) as unknown as Section, metadata: { push: false }, }, @@ -369,7 +387,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { M.interface('PushPull:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), - { getBalance: (_acct: string) => 42 }, + { getBalance: pushGetBalance }, ) as unknown as Section, metadata: { push: true }, }, @@ -380,9 +398,14 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }); // alice: both match, preferPush picks push section - expect(await E(wallet).getBalance('alice')).toBe(42); + await E(wallet).getBalance('alice'); + expect(pushGetBalance).toHaveBeenCalledWith('alice'); + expect(pullGetBalance).not.toHaveBeenCalled(); + pushGetBalance.mockClear(); // bob: only pull matches (stalk=1, lift bypassed) - expect(await E(wallet).getBalance('bob')).toBe(999); + await E(wallet).getBalance('bob'); + expect(pullGetBalance).toHaveBeenCalledWith('bob'); + expect(pushGetBalance).not.toHaveBeenCalled(); }); }); From 8b260ebb1d27f528d3ffc9138e1e7abcac42b7e8 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:41:55 -0400 Subject: [PATCH 05/68] fix(kernel-utils): fix false negative in collectSheafGuard for rest-arg sections `getGuardAt` was returning `undefined` for positions beyond a section's fixed argument range, even when a `restArgGuard` was present. This caused rest-arg sections to be absent from optional-position unions, producing a false negative: e.g. `M.call().rest(M.string())` would not cover position 0 in the union, so a call `['hello']` would fail the collected guard even though the section accepts it. Fix: fall through to `payload.restArgGuard` after exhausting the optional array, so rest-arg sections contribute to every optional position in the union. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/guard.test.ts | 25 +++++++++++++++++++ packages/kernel-utils/src/sheaf/guard.ts | 9 ++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/kernel-utils/src/sheaf/guard.test.ts b/packages/kernel-utils/src/sheaf/guard.test.ts index 35e5b75dc4..ffac24fd86 100644 --- a/packages/kernel-utils/src/sheaf/guard.test.ts +++ b/packages/kernel-utils/src/sheaf/guard.test.ts @@ -9,6 +9,7 @@ import type { MethodGuard, Pattern } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; import { collectSheafGuard } from './guard.ts'; +import { guardCoversPoint } from './stalk.ts'; import type { Section } from './types.ts'; const makeSection = ( @@ -159,6 +160,30 @@ describe('collectSheafGuard', () => { expect(matches(42, restArgGuard)).toBe(true); }); + it('rest-arg section covers optional positions (no false negative)', () => { + // Section A requires 1 number; Section B requires 0 args but accepts any + // number of strings via rest. A call ['hello'] is covered by B — the + // collected guard must pass it too. + const sections = [ + makeSection( + 'AB:0', + { f: M.call(M.number()).returns(M.any()) }, + { f: (_: number) => undefined }, + ), + makeSection( + 'AB:1', + { f: M.call().rest(M.string()).returns(M.any()) }, + { f: (..._args: string[]) => undefined }, + ), + ]; + + const guard = collectSheafGuard('AB', sections); + + expect(guardCoversPoint(guard, 'f', ['hello'])).toBe(true); // covered by B + expect(guardCoversPoint(guard, 'f', [42])).toBe(true); // covered by A + expect(guardCoversPoint(guard, 'f', [])).toBe(true); // covered by B (0 required) + }); + it('multi-method guard collection', () => { const sections = [ makeSection( diff --git a/packages/kernel-utils/src/sheaf/guard.ts b/packages/kernel-utils/src/sheaf/guard.ts index 6666e9d52c..36b29df97a 100644 --- a/packages/kernel-utils/src/sheaf/guard.ts +++ b/packages/kernel-utils/src/sheaf/guard.ts @@ -74,7 +74,14 @@ export const collectSheafGuard = ( if (idx < payload.argGuards.length) { return payload.argGuards[idx]; } - return payload.optionalArgGuards?.[idx - payload.argGuards.length]; + const optIdx = idx - payload.argGuards.length; + if ( + payload.optionalArgGuards && + optIdx < payload.optionalArgGuards.length + ) { + return payload.optionalArgGuards[optIdx]; + } + return payload.restArgGuard; }; const unionMethodGuards: Record = {}; From bbb24850a8f8f8ec5e78511ea056040b227785ea Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:19:53 -0400 Subject: [PATCH 06/68] feat(kernel-utils): metadata as polynomials of invocation data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MetaDataSpec discriminated union (constant | source | callable) so that sheaf metadata can vary with call arguments rather than being static. - constant(v) — static value, evaluated once - source(s) — JS source string compiled via Compartment at sheafify construction time, called at dispatch time - callable(fn) — live function called at dispatch time PresheafSection.metadata changes from M to MetaDataSpec (breaking). A new EvaluatedSection type carries post-evaluation metadata and is what Lift receives as its germs array. EvaluatedSection is distinct from PresheafSection because the "germ" in the sheaf-theoretic sense only exists after quotienting by the metadata-equivalence relation (the collapseEquivalent step); EvaluatedSection describes the pre-collapse stage where the spec has been applied to the invocation args. getStalk is generalised to so it works over ResolvedSection (the internal post-resolution type) without a cast. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.test.ts | 3 + packages/kernel-utils/src/index.ts | 3 + .../kernel-utils/src/sheaf/metadata.test.ts | 80 +++++++++++++ packages/kernel-utils/src/sheaf/metadata.ts | 85 ++++++++++++++ .../src/sheaf/sheafify.e2e.test.ts | 106 +++++++++++++++-- .../sheaf/sheafify.string-metadata.test.ts | 110 ++++++++++++++++++ .../kernel-utils/src/sheaf/sheafify.test.ts | 64 +++++----- packages/kernel-utils/src/sheaf/sheafify.ts | 55 ++++++--- packages/kernel-utils/src/sheaf/stalk.test.ts | 5 +- packages/kernel-utils/src/sheaf/stalk.ts | 8 +- packages/kernel-utils/src/sheaf/types.ts | 24 +++- 11 files changed, 483 insertions(+), 60 deletions(-) create mode 100644 packages/kernel-utils/src/sheaf/metadata.test.ts create mode 100644 packages/kernel-utils/src/sheaf/metadata.ts create mode 100644 packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index 05ccbc4f3c..e897f50762 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -13,7 +13,9 @@ describe('index', () => { 'GET_DESCRIPTION', 'abortableDelay', 'calculateReconnectionBackoff', + 'callable', 'collectSheafGuard', + 'constant', 'delay', 'fetchValidatedJson', 'fromHex', @@ -39,6 +41,7 @@ describe('index', () => { 'retry', 'retryWithBackoff', 'sheafify', + 'source', 'stringify', 'toHex', 'waitUntilQuiescent', diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index 94fa2d7a76..e40797c3f2 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -47,11 +47,14 @@ export type { RetryBackoffOptions, RetryOnRetryInfo } from './retry.ts'; export type { Section, PresheafSection, + EvaluatedSection, + MetaDataSpec, Lift, LiftContext, Presheaf, Sheaf, } from './sheaf/types.ts'; +export { constant, source, callable } from './sheaf/metadata.ts'; export { sheafify } from './sheaf/sheafify.ts'; export { collectSheafGuard } from './sheaf/guard.ts'; export { getStalk, guardCoversPoint } from './sheaf/stalk.ts'; diff --git a/packages/kernel-utils/src/sheaf/metadata.test.ts b/packages/kernel-utils/src/sheaf/metadata.test.ts new file mode 100644 index 0000000000..5421094a18 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/metadata.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, vi } from 'vitest'; + +import { + callable, + constant, + evaluateMetadata, + resolveMetaDataSpec, + source, +} from './metadata.ts'; + +describe('constant', () => { + it('returns a constant spec with the given value', () => { + expect(constant(42)).toStrictEqual({ kind: 'constant', value: 42 }); + }); + + it('evaluateMetadata returns the value regardless of args', () => { + const spec = resolveMetaDataSpec(constant({ cost: 7 })); + expect(evaluateMetadata(spec, [])).toStrictEqual({ cost: 7 }); + expect(evaluateMetadata(spec, [1, 2, 3])).toStrictEqual({ cost: 7 }); + }); +}); + +describe('callable', () => { + it('returns a callable spec wrapping the function', () => { + const fn = (args: unknown[]) => args[0] as number; + const spec = callable(fn); + expect(spec).toStrictEqual({ kind: 'callable', fn }); + }); + + it('evaluateMetadata calls fn with args', () => { + const fn = vi.fn((args: unknown[]) => (args[0] as number) * 2); + const spec = resolveMetaDataSpec(callable(fn)); + expect(evaluateMetadata(spec, [5])).toBe(10); + expect(fn).toHaveBeenCalledWith([5]); + }); +}); + +describe('source', () => { + it('returns a source spec with the src string', () => { + expect(source('(args) => args[0]')).toStrictEqual({ + kind: 'source', + src: '(args) => args[0]', + }); + }); + + it('resolveMetaDataSpec compiles source to callable via compartment', () => { + const mockFn = (args: unknown[]) => args[0] as number; + const compartment = { evaluate: vi.fn(() => mockFn) }; + const spec = resolveMetaDataSpec(source('(args) => args[0]'), compartment); + expect(spec.kind).toBe('callable'); + expect(compartment.evaluate).toHaveBeenCalledWith('(args) => args[0]'); + expect(evaluateMetadata(spec, [99])).toBe(99); + }); +}); + +describe('resolveMetaDataSpec', () => { + it('passes constant spec through unchanged', () => { + const spec = constant(42); + expect(resolveMetaDataSpec(spec)).toStrictEqual(spec); + }); + + it('passes callable spec through unchanged', () => { + const fn = (_args: unknown[]) => 0; + const spec = callable(fn); + expect(resolveMetaDataSpec(spec)).toStrictEqual(spec); + }); + + it("throws if kind is 'source' and no compartment supplied", () => { + expect(() => resolveMetaDataSpec(source('() => 0'))).toThrow( + "compartment required to evaluate 'source' metadata", + ); + }); +}); + +describe('evaluateMetadata', () => { + it('returns undefined when spec is undefined', () => { + expect(evaluateMetadata(undefined, [])).toBeUndefined(); + expect(evaluateMetadata(undefined, [1, 2])).toBeUndefined(); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/metadata.ts b/packages/kernel-utils/src/sheaf/metadata.ts new file mode 100644 index 0000000000..33846c247a --- /dev/null +++ b/packages/kernel-utils/src/sheaf/metadata.ts @@ -0,0 +1,85 @@ +/** + * MetaDataSpec constructors and evaluation helpers. + */ + +import type { MetaDataSpec } from './types.ts'; + +/** Resolved spec: 'source' has been compiled away; only constant or callable remain. */ +export type ResolvedMetaDataSpec = + | { kind: 'constant'; value: M } + | { kind: 'callable'; fn: (args: unknown[]) => M }; + +/** + * Wrap a static value as a constant metadata spec. + * + * @param value - The static metadata value. + * @returns A constant MetaDataSpec wrapping the value. + */ +export const constant = (value: M): MetaDataSpec => + harden({ kind: 'constant', value }); + +/** + * Wrap JS function source. Evaluated in a Compartment at sheafify construction time. + * + * @param src - JS source string of the form `(args) => M`. + * @returns A source MetaDataSpec wrapping the source string. + */ +export const source = (src: string): MetaDataSpec => + harden({ kind: 'source', src }); + +/** + * Wrap a live function as a callable metadata spec. + * + * @param fn - Function from invocation args to metadata value. + * @returns A callable MetaDataSpec wrapping the function. + */ +export const callable = (fn: (args: unknown[]) => M): MetaDataSpec => + harden({ kind: 'callable', fn }); + +/** + * Compile a 'source' spec to 'callable' using the supplied compartment. + * 'constant' and 'callable' pass through unchanged. + * + * @param spec - The MetaDataSpec to resolve. + * @param compartment - Compartment used to evaluate 'source' specs. Required when spec is 'source'. + * @param compartment.evaluate - Evaluate a JS source string and return the result. + * @returns A ResolvedMetaDataSpec with no 'source' variant. + */ +export const resolveMetaDataSpec = ( + spec: MetaDataSpec, + compartment?: { evaluate: (src: string) => unknown }, +): ResolvedMetaDataSpec => { + if (spec.kind === 'source') { + if (!compartment) { + throw new Error( + `sheafify: compartment required to evaluate 'source' metadata`, + ); + } + return { + kind: 'callable', + fn: compartment.evaluate(spec.src) as (args: unknown[]) => M, + }; + } + return spec; +}; + +/** + * Evaluate a resolved metadata spec against the invocation args. + * Returns undefined if spec is undefined (no metadata on the section). + * + * @param spec - The resolved spec to evaluate, or undefined. + * @param args - The invocation arguments. + * @returns The evaluated metadata value, or undefined. + */ +export const evaluateMetadata = ( + spec: ResolvedMetaDataSpec | undefined, + args: unknown[], +): M | undefined => { + if (spec === undefined) { + return undefined; + } + if (spec.kind === 'constant') { + return spec.value; + } + return spec.fn(args); +}; diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts index e355d59c98..976b819fb9 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -2,6 +2,7 @@ import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; import { describe, expect, it, vi } from 'vitest'; +import { callable, constant } from './metadata.ts'; import { sheafify } from './sheafify.ts'; import type { Lift, PresheafSection, Section } from './types.ts'; @@ -42,7 +43,7 @@ describe('e2e: cost-optimal routing', () => { }), { getBalance: remote0GetBalance }, ) as unknown as Section, - metadata: { cost: 100 }, + metadata: constant({ cost: 100 }), }, { // Local cache: covers only 'alice', cheap @@ -53,7 +54,7 @@ describe('e2e: cost-optimal routing', () => { }), { getBalance: local1GetBalance }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -83,7 +84,7 @@ describe('e2e: cost-optimal routing', () => { }), { getBalance: local2GetBalance }, ) as unknown as Section, - metadata: { cost: 2 }, + metadata: constant({ cost: 2 }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, @@ -169,7 +170,7 @@ describe('e2e: multi-tier capability routing', () => { }), { getBalance: networkGetBalance }, ) as unknown as Section, - metadata: { latencyMs: 500, label: 'network' }, + metadata: constant({ latencyMs: 500, label: 'network' }), }); let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -196,7 +197,7 @@ describe('e2e: multi-tier capability routing', () => { }), { getBalance: localGetBalance }, ) as unknown as Section, - metadata: { latencyMs: 1, label: 'local' }, + metadata: constant({ latencyMs: 1, label: 'local' }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: fastest, @@ -224,7 +225,7 @@ describe('e2e: multi-tier capability routing', () => { }), { getBalance: cacheGetBalance }, ) as unknown as Section, - metadata: { latencyMs: 0, label: 'cache' }, + metadata: constant({ latencyMs: 0, label: 'cache' }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: fastest, @@ -264,7 +265,7 @@ describe('e2e: multi-tier capability routing', () => { transfer: writeBackendTransfer, }, ) as unknown as Section, - metadata: { latencyMs: 200, label: 'write-backend' }, + metadata: constant({ latencyMs: 200, label: 'write-backend' }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: fastest, @@ -307,7 +308,7 @@ describe('e2e: multi-tier capability routing', () => { }), { getBalance: networkGetBalance }, ) as unknown as Section, - metadata: { latencyMs: 500, label: 'network' }, + metadata: constant({ latencyMs: 500, label: 'network' }), }, { exo: makeExo( @@ -317,7 +318,7 @@ describe('e2e: multi-tier capability routing', () => { }), { getBalance: mirrorGetBalance }, ) as unknown as Section, - metadata: { latencyMs: 50, label: 'mirror' }, + metadata: constant({ latencyMs: 50, label: 'mirror' }), }, ]; @@ -359,7 +360,7 @@ describe('e2e: multi-tier capability routing', () => { describe('e2e: preferAutonomous recovered as degenerate case', () => { it('binary push metadata recovers push-pull lift rule', async () => { - // Binary metadata: { push: true } = push section, { push: false } = pull + // Binary metadata: constant({ push: true }) = push section, { push: false } = pull const preferPush: Lift<{ push: boolean }> = async (germs) => { const pushIdx = germs.findIndex((entry) => entry.metadata?.push); return Promise.resolve(pushIdx >= 0 ? pushIdx : 0); @@ -378,7 +379,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }), { getBalance: pullGetBalance }, ) as unknown as Section, - metadata: { push: false }, + metadata: constant({ push: false }), }, { // Push section: narrow guard, push=true @@ -389,7 +390,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }), { getBalance: pushGetBalance }, ) as unknown as Section, - metadata: { push: true }, + metadata: constant({ push: true }), }, ]; @@ -409,3 +410,84 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { expect(pushGetBalance).not.toHaveBeenCalled(); }); }); + +// --------------------------------------------------------------------------- +// E2E: callable metadata — cost varies with invocation args +// --------------------------------------------------------------------------- + +describe('e2e: callable metadata — cost varies with invocation args', () => { + // Two swap sections whose cost is a function of the swap amount. + // Swap A is cheaper for small amounts; Swap B is cheaper for large amounts. + // Breakeven ≈ 90.9 (1 + 0.1x = 10 + 0.001x → 0.099x = 9 → x ≈ 90.9) + + type SwapCost = { cost: number }; + + const cheapest: Lift = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.cost ?? Infinity) < + (germs[bestIdx]!.metadata?.cost ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + it('routes swap(50) to A and swap(100) to B based on callable cost metadata', async () => { + const swapAFn = vi.fn( + (_amount: number, _from: string, _to: string): boolean => true, + ); + const swapBFn = vi.fn( + (_amount: number, _from: string, _to: string): boolean => true, + ); + + const sections: PresheafSection[] = [ + { + exo: makeExo( + 'SwapA', + M.interface('SwapA', { + swap: M.call(M.number(), M.string(), M.string()).returns( + M.boolean(), + ), + }), + { swap: swapAFn }, + ) as unknown as Section, + // cost(amount) = 1 + 0.1 * amount + metadata: callable((args) => ({ + cost: 1 + 0.1 * (args[0] as number), + })), + }, + { + exo: makeExo( + 'SwapB', + M.interface('SwapB', { + swap: M.call(M.number(), M.string(), M.string()).returns( + M.boolean(), + ), + }), + { swap: swapBFn }, + ) as unknown as Section, + // cost(amount) = 10 + 0.001 * amount + metadata: callable((args) => ({ + cost: 10 + 0.001 * (args[0] as number), + })), + }, + ]; + + const facade = sheafify({ name: 'Swap', sections }).getGlobalSection({ + lift: cheapest, + }) as unknown as Record Promise>; + + // swap(50): A costs 6, B costs 10.05 → A wins + await facade.swap(50, 'FUZ', 'BIZ'); + expect(swapAFn).toHaveBeenCalledWith(50, 'FUZ', 'BIZ'); + expect(swapBFn).not.toHaveBeenCalled(); + swapAFn.mockClear(); + + // swap(100): A costs 11, B costs 10.1 → B wins + await facade.swap(100, 'FUZ', 'BIZ'); + expect(swapBFn).toHaveBeenCalledWith(100, 'FUZ', 'BIZ'); + expect(swapAFn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts b/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts new file mode 100644 index 0000000000..302d70300f --- /dev/null +++ b/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts @@ -0,0 +1,110 @@ +// This test verifies that source-kind metadata specs are compiled via a +// compartment at sheafify construction time and evaluated at dispatch time. +// +// We use a new Function()-based compartment rather than a real SES Compartment +// because importing 'ses' alongside '@endo/exo' triggers a module-evaluation +// ordering conflict in the test environment: @endo/patterns module initialization +// calls assertPattern() under SES lockdown before its internal objects are frozen. +// That conflict is an environment limitation, not a feature limitation. +// +// The functional properties under test are identical regardless of which +// Compartment implementation compiles the source string. + +import { makeExo } from '@endo/exo'; +import { M } from '@endo/patterns'; +import { describe, it, expect, vi } from 'vitest'; + +import { source } from './metadata.ts'; +import { sheafify } from './sheafify.ts'; +import type { Lift, PresheafSection, Section } from './types.ts'; + +// Thin cast for calling exo methods directly in tests without going through +// HandledPromise (which is not available in the test environment). +// eslint-disable-next-line id-length +const E = (obj: unknown) => + obj as Record Promise>; + +// A Compartment-shaped object that actually evaluates JS source strings. +/* eslint-disable @typescript-eslint/no-implied-eval, no-new-func */ +const makeTestCompartment = () => ({ + evaluate: (src: string) => new Function(`return (${src})`)(), +}); +/* eslint-enable @typescript-eslint/no-implied-eval, no-new-func */ + +describe('e2e: source metadata — compartment evaluates cost function', () => { + // Same two-swap scenario as the callable e2e test, but cost functions are + // provided as JS source strings and compiled via the test compartment. + // Breakeven ≈ 90.9 (same arithmetic as callable variant). + + type SwapCost = { cost: number }; + + const cheapest: Lift = async (germs) => + Promise.resolve( + germs.reduce( + (bestIdx, entry, idx) => + (entry.metadata?.cost ?? Infinity) < + (germs[bestIdx]!.metadata?.cost ?? Infinity) + ? idx + : bestIdx, + 0, + ), + ); + + it('routes swap(50) to A and swap(100) to B using source-kind metadata', async () => { + const swapAFn = vi.fn( + (_amount: number, _from: string, _to: string): boolean => true, + ); + const swapBFn = vi.fn( + (_amount: number, _from: string, _to: string): boolean => true, + ); + + const sections: PresheafSection[] = [ + { + exo: makeExo( + 'SwapA', + M.interface('SwapA', { + swap: M.call(M.number(), M.string(), M.string()).returns( + M.boolean(), + ), + }), + { swap: swapAFn }, + ) as unknown as Section, + // cost(amount) = 1 + 0.1 * amount + metadata: source(`(args) => ({ cost: 1 + 0.1 * args[0] })`), + }, + { + exo: makeExo( + 'SwapB', + M.interface('SwapB', { + swap: M.call(M.number(), M.string(), M.string()).returns( + M.boolean(), + ), + }), + { swap: swapBFn }, + ) as unknown as Section, + // cost(amount) = 10 + 0.001 * amount + metadata: source(`(args) => ({ cost: 10 + 0.001 * args[0] })`), + }, + ]; + + const facade = sheafify({ + name: 'Swap', + sections, + compartment: makeTestCompartment(), + }).getGlobalSection({ lift: cheapest }) as unknown as Record< + string, + (...args: unknown[]) => Promise + >; + + // swap(50): A costs 6, B costs 10.05 → A wins + await E(facade).swap(50, 'FUZ', 'BIZ'); + expect(swapAFn).toHaveBeenCalledWith(50, 'FUZ', 'BIZ'); + expect(swapBFn).not.toHaveBeenCalled(); + swapAFn.mockClear(); + + // swap(100): A costs 11, B costs 10.1 → B wins + await E(facade).swap(100, 'FUZ', 'BIZ'); + expect(swapBFn).toHaveBeenCalledWith(100, 'FUZ', 'BIZ'); + expect(swapAFn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index d350dd7170..82fb42444c 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -2,8 +2,15 @@ import { makeExo, GET_INTERFACE_GUARD } from '@endo/exo'; import { M, getInterfaceGuardPayload } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; +import { constant } from './metadata.ts'; import { sheafify } from './sheafify.ts'; -import type { Lift, LiftContext, PresheafSection, Section } from './types.ts'; +import type { + EvaluatedSection, + Lift, + LiftContext, + PresheafSection, + Section, +} from './types.ts'; // Thin cast for calling exo methods directly in tests without going through // HandledPromise (which is not available in the test environment). @@ -32,7 +39,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -53,7 +60,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -87,7 +94,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { cost: 100 }, + metadata: constant({ cost: 100 }), }, { exo: makeExo( @@ -97,7 +104,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -119,7 +126,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { cost: 100 }, + metadata: constant({ cost: 100 }), }, { exo: makeExo( @@ -129,7 +136,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 50 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -165,7 +172,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { cost: 100 }, + metadata: constant({ cost: 100 }), }, ]; @@ -189,7 +196,7 @@ describe('sheafify', () => { transfer: (_from: string, _to: string, _amt: number) => true, }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, @@ -214,7 +221,7 @@ describe('sheafify', () => { { getBalance: (_acct: string) => 42 }, ); const sections: PresheafSection<{ cost: number }>[] = [ - { exo: exo as unknown as Section, metadata: { cost: 1 } }, + { exo: exo as unknown as Section, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -245,7 +252,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { cost: 100 }, + metadata: constant({ cost: 100 }), }, ]; @@ -268,7 +275,10 @@ describe('sheafify', () => { transfer: (_from: string, _to: string, _amt: number) => true, }, ); - sections.push({ exo: exo as unknown as Section, metadata: { cost: 1 } }); + sections.push({ + exo: exo as unknown as Section, + metadata: constant({ cost: 1 }), + }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ lift: argmin, }); @@ -292,7 +302,7 @@ describe('sheafify', () => { { getBalance: (_acct: string) => 42 }, ); const sections: PresheafSection<{ cost: number }>[] = [ - { exo: exo as unknown as Section, metadata: { cost: 1 } }, + { exo: exo as unknown as Section, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -307,7 +317,7 @@ describe('sheafify', () => { it('lift receives constraints in context and only distinguishing metadata', async () => { type Meta = { region: string; cost: number }; - let capturedGerms: PresheafSection>[] = []; + let capturedGerms: EvaluatedSection>[] = []; let capturedContext: LiftContext | undefined; const spy: Lift = async (germs, context) => { @@ -325,7 +335,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { region: 'us', cost: 100 }, + metadata: constant({ region: 'us', cost: 100 }), }, { exo: makeExo( @@ -335,7 +345,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { region: 'us', cost: 1 }, + metadata: constant({ region: 'us', cost: 1 }), }, ]; @@ -357,7 +367,7 @@ describe('sheafify', () => { it('all-shared metadata yields empty distinguishing metadata', async () => { type Meta = { region: string }; - let capturedGerms: PresheafSection>[] = []; + let capturedGerms: EvaluatedSection>[] = []; let capturedContext: LiftContext | undefined; const spy: Lift = async (germs, context) => { @@ -375,7 +385,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { region: 'us' }, + metadata: constant({ region: 'us' }), }, { exo: makeExo( @@ -385,7 +395,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { region: 'us' }, + metadata: constant({ region: 'us' }), }, ]; @@ -412,7 +422,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, { exo: makeExo( @@ -422,7 +432,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -467,9 +477,9 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 100 }, ) as unknown as Section, - metadata: { cost: 100 }, + metadata: constant({ cost: 100 }), }, - { exo: exo as unknown as Section, metadata: { cost: 1 } }, + { exo: exo as unknown as Section, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -493,7 +503,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -525,7 +535,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -553,7 +563,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; @@ -580,7 +590,7 @@ describe('sheafify', () => { }), { getBalance: (_acct: string) => 42 }, ) as unknown as Section, - metadata: { cost: 1 }, + metadata: constant({ cost: 1 }), }, ]; diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index faa9c5cfc9..07ac36df74 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -23,8 +23,16 @@ import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; import { stringify } from '../stringify.ts'; import { collectSheafGuard } from './guard.ts'; import type { MethodGuardPayload } from './guard.ts'; +import { evaluateMetadata, resolveMetaDataSpec } from './metadata.ts'; +import type { ResolvedMetaDataSpec } from './metadata.ts'; import { getStalk, guardCoversPoint } from './stalk.ts'; -import type { Lift, PresheafSection, Section, Sheaf } from './types.ts'; +import type { + EvaluatedSection, + Lift, + PresheafSection, + Section, + Sheaf, +} from './types.ts'; /** * Serialize metadata for equivalence-class keying (collapse step). @@ -53,10 +61,10 @@ const metadataKey = (metadata: unknown): string => { * @returns One representative per equivalence class. */ const collapseEquivalent = ( - stalk: PresheafSection[], -): PresheafSection[] => { + stalk: EvaluatedSection[], +): EvaluatedSection[] => { const seen = new Set(); - const representatives: PresheafSection[] = []; + const representatives: EvaluatedSection[] = []; for (const entry of stalk) { const key = metadataKey(entry.metadata); if (!seen.has(key)) { @@ -75,10 +83,10 @@ const collapseEquivalent = ( * @returns Constraints and stripped germs. */ const decomposeMetadata = ( - stalk: PresheafSection[], + stalk: EvaluatedSection[], ): { constraints: Partial; - stripped: PresheafSection>[]; + stripped: EvaluatedSection>[]; } => { const constraints: Record = {}; @@ -173,14 +181,29 @@ type Grant = { isRevoked: () => boolean; }; +type ResolvedSection = { + exo: Section; + spec: ResolvedMetaDataSpec | undefined; +}; + export const sheafify = ({ name, sections, + compartment, }: { name: string; sections: PresheafSection[]; + compartment?: { evaluate: (src: string) => unknown }; }): Sheaf => { - const frozenSections = [...sections]; + const frozenSections: readonly ResolvedSection[] = Object.freeze( + sections.map((section) => ({ + exo: section.exo, + spec: + section.metadata === undefined + ? undefined + : resolveMetaDataSpec(section.metadata, compartment), + })), + ); const grants: Grant[] = []; const getSection = ({ @@ -206,22 +229,28 @@ export const sheafify = ({ } const stalk = getStalk(frozenSections, method, args); - let winner: PresheafSection; - switch (stalk.length) { + const evaluatedStalk: EvaluatedSection[] = stalk.map( + (section) => ({ + exo: section.exo, + metadata: evaluateMetadata(section.spec, args), + }), + ); + let winner: EvaluatedSection; + switch (evaluatedStalk.length) { case 0: throw new Error(`No section covers ${method}(${stringify(args, 0)})`); case 1: - winner = stalk[0] as PresheafSection; + winner = evaluatedStalk[0] as EvaluatedSection; break; default: { - const collapsed = collapseEquivalent(stalk); + const collapsed = collapseEquivalent(evaluatedStalk); if (collapsed.length === 1) { - winner = collapsed[0] as PresheafSection; + winner = collapsed[0] as EvaluatedSection; break; } const { constraints, stripped } = decomposeMetadata(collapsed); const index = await lift(stripped, { method, args, constraints }); - winner = collapsed[index] as PresheafSection; + winner = collapsed[index] as EvaluatedSection; break; } } diff --git a/packages/kernel-utils/src/sheaf/stalk.test.ts b/packages/kernel-utils/src/sheaf/stalk.test.ts index c0e909adea..534f576b59 100644 --- a/packages/kernel-utils/src/sheaf/stalk.test.ts +++ b/packages/kernel-utils/src/sheaf/stalk.test.ts @@ -3,6 +3,7 @@ import { M } from '@endo/patterns'; import type { MethodGuard } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; +import { constant } from './metadata.ts'; import { getStalk } from './stalk.ts'; import type { PresheafSection, Section } from './types.ts'; @@ -14,7 +15,7 @@ const makePresheafSection = ( ): PresheafSection<{ cost: number }> => { const interfaceGuard = M.interface(tag, guards); const exo = makeExo(tag, interfaceGuard, methods); - return { exo: exo as unknown as Section, metadata }; + return { exo: exo as unknown as Section, metadata: constant(metadata) }; }; describe('getStalk', () => { @@ -56,7 +57,7 @@ describe('getStalk', () => { const stalk = getStalk(sections, 'add', [1]); expect(stalk).toHaveLength(1); - expect(stalk[0]!.metadata?.cost).toBe(1); + expect(stalk[0]!.metadata).toStrictEqual(constant({ cost: 1 })); }); it('filters out sections with arg count mismatch', () => { diff --git a/packages/kernel-utils/src/sheaf/stalk.ts b/packages/kernel-utils/src/sheaf/stalk.ts index 2c17a5ecce..ad06eee0af 100644 --- a/packages/kernel-utils/src/sheaf/stalk.ts +++ b/packages/kernel-utils/src/sheaf/stalk.ts @@ -11,7 +11,7 @@ import { import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; import type { MethodGuardPayload } from './guard.ts'; -import type { PresheafSection } from './types.ts'; +import type { Section } from './types.ts'; /** * Check whether an interface guard covers the invocation point (method, args). @@ -65,11 +65,11 @@ export const guardCoversPoint = ( * @param args - The arguments to the method invocation. * @returns The presheaf sections whose guards accept the invocation. */ -export const getStalk = ( - sections: PresheafSection[], +export const getStalk = ( + sections: T[], method: string, args: unknown[], -): PresheafSection[] => { +): T[] => { return sections.filter(({ exo }) => { const interfaceGuard = exo[GET_INTERFACE_GUARD]?.(); if (!interfaceGuard) { diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index abb78ab890..ad31e5e1de 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -16,12 +16,32 @@ export type Section = Partial & { }; /** - * A presheaf section: a section (F_sem) paired with optional metadata (F_op). + * A metadata specification: either a static value, a JS source string, or a + * live function. Source strings are compiled once at sheafify construction time. + */ +export type MetaDataSpec = + | { kind: 'constant'; value: M } + | { kind: 'source'; src: string } + | { kind: 'callable'; fn: (args: unknown[]) => M }; + +/** + * A presheaf section: a section (F_sem) paired with an optional metadata spec (F_op). * * This is the input data to sheafify — an (exo, metadata) pair assigned over * the open set defined by the exo's guard. */ export type PresheafSection = { + exo: Section; + metadata?: MetaDataSpec; +}; + +/** + * A section with evaluated metadata: the metadata spec has been computed against + * the invocation args, yielding a concrete value. Used internally during dispatch + * and as the element type of the `germs` array received by Lift (where each entry + * is already a representative of an equivalence class after collapsing). + */ +export type EvaluatedSection = { exo: Section; metadata?: MetaData; }; @@ -49,7 +69,7 @@ export type LiftContext = { * Returns a Promise — the index into the germs array. */ export type Lift = ( - germs: PresheafSection>[], + germs: EvaluatedSection>[], context: LiftContext, ) => Promise; From 8f3b9e21f031d6944b65a14e65a806f7c271954f Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:39:05 -0400 Subject: [PATCH 07/68] fix(kernel-utils): fix build errors in sheafify metadata eval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getStalk: accept readonly T[] to allow frozen section arrays - evaluatedStalk map: omit metadata when undefined via ifDefined for exactOptionalPropertyTypes (metadata?: M ≠ metadata: M | undefined) Co-Authored-By: Claude Sonnet 4.6 Made-with: Cursor --- packages/kernel-utils/src/sheaf/sheafify.ts | 5 ++++- packages/kernel-utils/src/sheaf/stalk.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 07ac36df74..61cac8f095 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -20,6 +20,7 @@ import { } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; +import { ifDefined } from '../misc.ts'; import { stringify } from '../stringify.ts'; import { collectSheafGuard } from './guard.ts'; import type { MethodGuardPayload } from './guard.ts'; @@ -232,7 +233,9 @@ export const sheafify = ({ const evaluatedStalk: EvaluatedSection[] = stalk.map( (section) => ({ exo: section.exo, - metadata: evaluateMetadata(section.spec, args), + ...ifDefined({ + metadata: evaluateMetadata(section.spec, args), + }), }), ); let winner: EvaluatedSection; diff --git a/packages/kernel-utils/src/sheaf/stalk.ts b/packages/kernel-utils/src/sheaf/stalk.ts index ad06eee0af..f7988ba15d 100644 --- a/packages/kernel-utils/src/sheaf/stalk.ts +++ b/packages/kernel-utils/src/sheaf/stalk.ts @@ -66,7 +66,7 @@ export const guardCoversPoint = ( * @returns The presheaf sections whose guards accept the invocation. */ export const getStalk = ( - sections: T[], + sections: readonly T[], method: string, args: unknown[], ): T[] => { From 71df8e50af583ce9be7f7a6c691a621ddfb82db3 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:23:52 -0400 Subject: [PATCH 08/68] feat(kernel-utils): treat {} as empty sheaf metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - evaluateMetadata returns a plain object; missing spec and nullish raw → {} - reject primitives, arrays, and non-plain objects; hint { value: myValue } - require EvaluatedSection.metadata; MetaData extends Record - simplify metadataKey and decomposeMetadata; drop ifDefined in dispatch Made-with: Cursor --- .../kernel-utils/src/sheaf/metadata.test.ts | 85 +++++++++++++++---- packages/kernel-utils/src/sheaf/metadata.ts | 80 ++++++++++++----- .../kernel-utils/src/sheaf/sheafify.test.ts | 37 ++++++++ packages/kernel-utils/src/sheaf/sheafify.ts | 69 ++++++--------- packages/kernel-utils/src/sheaf/types.ts | 22 +++-- 5 files changed, 206 insertions(+), 87 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/metadata.test.ts b/packages/kernel-utils/src/sheaf/metadata.test.ts index 5421094a18..8b77f80ff6 100644 --- a/packages/kernel-utils/src/sheaf/metadata.test.ts +++ b/packages/kernel-utils/src/sheaf/metadata.test.ts @@ -10,7 +10,10 @@ import { describe('constant', () => { it('returns a constant spec with the given value', () => { - expect(constant(42)).toStrictEqual({ kind: 'constant', value: 42 }); + expect(constant({ n: 42 })).toStrictEqual({ + kind: 'constant', + value: { n: 42 }, + }); }); it('evaluateMetadata returns the value regardless of args', () => { @@ -22,59 +25,109 @@ describe('constant', () => { describe('callable', () => { it('returns a callable spec wrapping the function', () => { - const fn = (args: unknown[]) => args[0] as number; + const fn = (args: unknown[]) => ({ out: args[0] as number }); const spec = callable(fn); expect(spec).toStrictEqual({ kind: 'callable', fn }); }); it('evaluateMetadata calls fn with args', () => { - const fn = vi.fn((args: unknown[]) => (args[0] as number) * 2); + const fn = vi.fn((args: unknown[]) => ({ + value: (args[0] as number) * 2, + })); const spec = resolveMetaDataSpec(callable(fn)); - expect(evaluateMetadata(spec, [5])).toBe(10); + expect(evaluateMetadata(spec, [5])).toStrictEqual({ value: 10 }); expect(fn).toHaveBeenCalledWith([5]); }); }); describe('source', () => { it('returns a source spec with the src string', () => { - expect(source('(args) => args[0]')).toStrictEqual({ + expect(source('(args) => ({ x: args[0] })')).toStrictEqual({ kind: 'source', - src: '(args) => args[0]', + src: '(args) => ({ x: args[0] })', }); }); it('resolveMetaDataSpec compiles source to callable via compartment', () => { - const mockFn = (args: unknown[]) => args[0] as number; + const mockFn = (args: unknown[]) => ({ value: args[0] as number }); const compartment = { evaluate: vi.fn(() => mockFn) }; - const spec = resolveMetaDataSpec(source('(args) => args[0]'), compartment); + const spec = resolveMetaDataSpec( + source<{ value: number }>('(args) => ({ value: args[0] })'), + compartment, + ); expect(spec.kind).toBe('callable'); - expect(compartment.evaluate).toHaveBeenCalledWith('(args) => args[0]'); - expect(evaluateMetadata(spec, [99])).toBe(99); + expect(compartment.evaluate).toHaveBeenCalledWith( + '(args) => ({ value: args[0] })', + ); + expect(evaluateMetadata(spec, [99])).toStrictEqual({ value: 99 }); }); }); describe('resolveMetaDataSpec', () => { it('passes constant spec through unchanged', () => { - const spec = constant(42); + const spec = constant({ answer: 42 }); expect(resolveMetaDataSpec(spec)).toStrictEqual(spec); }); it('passes callable spec through unchanged', () => { - const fn = (_args: unknown[]) => 0; + const fn = (_args: unknown[]) => ({ count: 0 }); const spec = callable(fn); expect(resolveMetaDataSpec(spec)).toStrictEqual(spec); }); it("throws if kind is 'source' and no compartment supplied", () => { - expect(() => resolveMetaDataSpec(source('() => 0'))).toThrow( + expect(() => resolveMetaDataSpec(source('() => ({})'))).toThrow( "compartment required to evaluate 'source' metadata", ); }); }); describe('evaluateMetadata', () => { - it('returns undefined when spec is undefined', () => { - expect(evaluateMetadata(undefined, [])).toBeUndefined(); - expect(evaluateMetadata(undefined, [1, 2])).toBeUndefined(); + it('returns empty object when spec is undefined', () => { + expect(evaluateMetadata(undefined, [])).toStrictEqual({}); + expect(evaluateMetadata(undefined, [1, 2])).toStrictEqual({}); + }); + + it('normalizes null from callable to empty object', () => { + const spec = resolveMetaDataSpec( + callable( + ((_args: unknown[]) => null) as unknown as ( + args: unknown[], + ) => Record, + ), + ); + expect(evaluateMetadata(spec, [])).toStrictEqual({}); + }); + + it('throws when callable returns a primitive', () => { + const spec = resolveMetaDataSpec( + callable( + ((_args: unknown[]) => 7) as unknown as ( + args: unknown[], + ) => Record, + ), + ); + expect(() => evaluateMetadata(spec, [])).toThrow(/cannot be a primitive/u); + expect(() => evaluateMetadata(spec, [])).toThrow(/value: myValue/u); + }); + + it('throws when callable returns an array', () => { + const spec = resolveMetaDataSpec( + callable(((_args: unknown[]) => [1, 2]) as unknown as ( + args: unknown[], + ) => Record), + ); + expect(() => evaluateMetadata(spec, [])).toThrow(/cannot be an array/u); + }); + + it('throws when callable returns a Date', () => { + const spec = resolveMetaDataSpec( + callable( + ((_args: unknown[]) => new Date()) as unknown as ( + args: unknown[], + ) => Record, + ), + ); + expect(() => evaluateMetadata(spec, [])).toThrow(/must be a plain object/u); }); }); diff --git a/packages/kernel-utils/src/sheaf/metadata.ts b/packages/kernel-utils/src/sheaf/metadata.ts index 33846c247a..6ee84ceee8 100644 --- a/packages/kernel-utils/src/sheaf/metadata.ts +++ b/packages/kernel-utils/src/sheaf/metadata.ts @@ -5,18 +5,57 @@ import type { MetaDataSpec } from './types.ts'; /** Resolved spec: 'source' has been compiled away; only constant or callable remain. */ -export type ResolvedMetaDataSpec = +export type ResolvedMetaDataSpec> = | { kind: 'constant'; value: M } | { kind: 'callable'; fn: (args: unknown[]) => M }; +const metadataPlainObjectHint = + 'Sheaf metadata must be a plain object; use e.g. { value: myValue } if you need to attach a primitive.'; + +const isPlainObjectRecord = (value: object): boolean => { + const proto = Object.getPrototypeOf(value); + return proto === null || proto === Object.prototype; +}; + +/** + * Normalize evaluated metadata: empty sentinel is `{}`; invalid shapes throw. + * + * @param raw - Result from constant value or callable, before validation. + * @returns A plain object suitable for stalk metadata. + */ +const normalizeEvaluatedSheafMetadata = ( + raw: unknown, +): Record => { + if (raw === undefined || raw === null) { + return {}; + } + if (typeof raw !== 'object') { + throw new Error( + `sheafify: metadata cannot be a primitive (${typeof raw}). ${metadataPlainObjectHint}`, + ); + } + if (Array.isArray(raw)) { + throw new Error( + `sheafify: metadata cannot be an array. ${metadataPlainObjectHint}`, + ); + } + if (!isPlainObjectRecord(raw)) { + throw new Error( + `sheafify: metadata must be a plain object. ${metadataPlainObjectHint}`, + ); + } + return raw as Record; +}; + /** * Wrap a static value as a constant metadata spec. * * @param value - The static metadata value. * @returns A constant MetaDataSpec wrapping the value. */ -export const constant = (value: M): MetaDataSpec => - harden({ kind: 'constant', value }); +export const constant = >( + value: M, +): MetaDataSpec => harden({ kind: 'constant', value }); /** * Wrap JS function source. Evaluated in a Compartment at sheafify construction time. @@ -24,17 +63,19 @@ export const constant = (value: M): MetaDataSpec => * @param src - JS source string of the form `(args) => M`. * @returns A source MetaDataSpec wrapping the source string. */ -export const source = (src: string): MetaDataSpec => - harden({ kind: 'source', src }); +export const source = >( + src: string, +): MetaDataSpec => harden({ kind: 'source', src }); /** * Wrap a live function as a callable metadata spec. * * @param fn - Function from invocation args to metadata value. - * @returns A callable MetaDataSpec wrapping the function. + * @returns A callable metadata spec. */ -export const callable = (fn: (args: unknown[]) => M): MetaDataSpec => - harden({ kind: 'callable', fn }); +export const callable = >( + fn: (args: unknown[]) => M, +): MetaDataSpec => harden({ kind: 'callable', fn }); /** * Compile a 'source' spec to 'callable' using the supplied compartment. @@ -45,7 +86,7 @@ export const callable = (fn: (args: unknown[]) => M): MetaDataSpec => * @param compartment.evaluate - Evaluate a JS source string and return the result. * @returns A ResolvedMetaDataSpec with no 'source' variant. */ -export const resolveMetaDataSpec = ( +export const resolveMetaDataSpec = >( spec: MetaDataSpec, compartment?: { evaluate: (src: string) => unknown }, ): ResolvedMetaDataSpec => { @@ -65,21 +106,22 @@ export const resolveMetaDataSpec = ( /** * Evaluate a resolved metadata spec against the invocation args. - * Returns undefined if spec is undefined (no metadata on the section). + * + * Missing spec yields `{}` (no metadata). Callable/constant results must be plain objects; + * `undefined`/`null` from the producer normalize to `{}`. Primitives, arrays, and non-plain + * objects throw with guidance to use an explicit record such as `{ value: myValue }`. * * @param spec - The resolved spec to evaluate, or undefined. * @param args - The invocation arguments. - * @returns The evaluated metadata value, or undefined. + * @returns The evaluated metadata object (possibly empty). */ -export const evaluateMetadata = ( - spec: ResolvedMetaDataSpec | undefined, +export const evaluateMetadata = >( + spec: ResolvedMetaDataSpec | undefined, args: unknown[], -): M | undefined => { +): MetaData => { if (spec === undefined) { - return undefined; - } - if (spec.kind === 'constant') { - return spec.value; + return {} as MetaData; } - return spec.fn(args); + const raw = spec.kind === 'constant' ? spec.value : spec.fn(args); + return normalizeEvaluatedSheafMetadata(raw) as MetaData; }; diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 82fb42444c..868dd2b255 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -448,6 +448,43 @@ describe('sheafify', () => { expect(liftCalled).toBe(false); }); + it('collapses no-metadata and empty-object metadata as equivalent', async () => { + type Meta = Record; + let liftCalled = false; + + const sections: PresheafSection[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ) as unknown as Section, + }, + { + exo: makeExo( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + metadata: constant({}), + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + lift: async (_germs) => { + liftCalled = true; + return Promise.resolve(0); + }, + }); + await E(wallet).getBalance('alice'); + + expect(liftCalled).toBe(false); + }); + it('mixed sections participate in lift', async () => { const argmin: Lift<{ cost: number }> = async (germs) => Promise.resolve( diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 61cac8f095..33c0f09f36 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -20,7 +20,6 @@ import { } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; -import { ifDefined } from '../misc.ts'; import { stringify } from '../stringify.ts'; import { collectSheafGuard } from './guard.ts'; import type { MethodGuardPayload } from './guard.ts'; @@ -41,15 +40,13 @@ import type { * @param metadata - The metadata value to serialize. * @returns A string key for equivalence comparison. */ -const metadataKey = (metadata: unknown): string => { - if (metadata === undefined || metadata === null) { +const metadataKey = (metadata: Record): string => { + const keys = Object.keys(metadata); + if (keys.length === 0) { return 'null'; } - if (typeof metadata !== 'object') { - return JSON.stringify(metadata); - } - const entries = Object.entries(metadata as Record).sort( - ([a], [b]) => a.localeCompare(b), + const entries = Object.entries(metadata).sort(([a], [b]) => + a.localeCompare(b), ); return JSON.stringify(entries); }; @@ -61,7 +58,7 @@ const metadataKey = (metadata: unknown): string => { * @param stalk - The stalk entries to collapse. * @returns One representative per equivalence class. */ -const collapseEquivalent = ( +const collapseEquivalent = >( stalk: EvaluatedSection[], ): EvaluatedSection[] => { const seen = new Set(); @@ -83,7 +80,7 @@ const collapseEquivalent = ( * @param stalk - The collapsed stalk entries. * @returns Constraints and stripped germs. */ -const decomposeMetadata = ( +const decomposeMetadata = >( stalk: EvaluatedSection[], ): { constraints: Partial; @@ -91,39 +88,25 @@ const decomposeMetadata = ( } => { const constraints: Record = {}; - const first = stalk[0]?.metadata; - if (first !== undefined && first !== null && typeof first === 'object') { - for (const key of Object.keys(first as Record)) { - const val = (first as Record)[key]; - const shared = stalk.every((entry) => { - if ( - entry.metadata === undefined || - entry.metadata === null || - typeof entry.metadata !== 'object' - ) { - return false; - } - const meta = entry.metadata as Record; - return key in meta && meta[key] === val; - }); - if (shared) { - constraints[key] = val; - } + const head = stalk[0]; + if (head === undefined) { + return { constraints: {} as Partial, stripped: [] }; + } + const first = head.metadata; + for (const key of Object.keys(first)) { + const val = first[key]; + const shared = stalk.every((entry) => { + const meta = entry.metadata; + return key in meta && meta[key] === val; + }); + if (shared) { + constraints[key] = val; } } const stripped = stalk.map((entry) => { - if ( - entry.metadata === undefined || - entry.metadata === null || - typeof entry.metadata !== 'object' - ) { - return { exo: entry.exo }; - } const remaining: Record = {}; - for (const [key, val] of Object.entries( - entry.metadata as Record, - )) { + for (const [key, val] of Object.entries(entry.metadata)) { if (!(key in constraints)) { remaining[key] = val; } @@ -182,12 +165,14 @@ type Grant = { isRevoked: () => boolean; }; -type ResolvedSection = { +type ResolvedSection> = { exo: Section; spec: ResolvedMetaDataSpec | undefined; }; -export const sheafify = ({ +export const sheafify = < + MetaData extends Record = Record, +>({ name, sections, compartment, @@ -233,9 +218,7 @@ export const sheafify = ({ const evaluatedStalk: EvaluatedSection[] = stalk.map( (section) => ({ exo: section.exo, - ...ifDefined({ - metadata: evaluateMetadata(section.spec, args), - }), + metadata: evaluateMetadata(section.spec, args), }), ); let winner: EvaluatedSection; diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index ad31e5e1de..b42f3e89ee 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -18,8 +18,10 @@ export type Section = Partial & { /** * A metadata specification: either a static value, a JS source string, or a * live function. Source strings are compiled once at sheafify construction time. + * Evaluated metadata must be a plain object (`{}` means no metadata; primitives + * must be wrapped, e.g. `{ value: n }`). */ -export type MetaDataSpec = +export type MetaDataSpec> = | { kind: 'constant'; value: M } | { kind: 'source'; src: string } | { kind: 'callable'; fn: (args: unknown[]) => M }; @@ -30,20 +32,21 @@ export type MetaDataSpec = * This is the input data to sheafify — an (exo, metadata) pair assigned over * the open set defined by the exo's guard. */ -export type PresheafSection = { +export type PresheafSection> = { exo: Section; metadata?: MetaDataSpec; }; /** * A section with evaluated metadata: the metadata spec has been computed against - * the invocation args, yielding a concrete value. Used internally during dispatch + * the invocation args, yielding a concrete plain object. Used internally during dispatch * and as the element type of the `germs` array received by Lift (where each entry * is already a representative of an equivalence class after collapsing). + * Empty `{}` means no metadata. */ -export type EvaluatedSection = { +export type EvaluatedSection> = { exo: Section; - metadata?: MetaData; + metadata: MetaData; }; /** @@ -53,7 +56,7 @@ export type EvaluatedSection = { * germ in the stalk — these are topologically determined and not a choice. * Typed as `Partial` because the actual partition is runtime-dependent. */ -export type LiftContext = { +export type LiftContext> = { method: string; args: unknown[]; constraints: Partial; @@ -68,7 +71,7 @@ export type LiftContext = { * * Returns a Promise — the index into the germs array. */ -export type Lift = ( +export type Lift> = ( germs: EvaluatedSection>[], context: LiftContext, ) => Promise; @@ -76,7 +79,8 @@ export type Lift = ( /** * A presheaf: a plain array of presheaf sections. */ -export type Presheaf = PresheafSection[]; +export type Presheaf> = + PresheafSection[]; /** * A sheaf: an authority manager over a presheaf. @@ -84,7 +88,7 @@ export type Presheaf = PresheafSection[]; * Produces revocable dispatch sections via `getSection` and tracks all * granted authority for auditing and revocation. */ -export type Sheaf = { +export type Sheaf> = { /** Produce a revocable dispatch exo over the given guard. */ getSection: (opts: { guard: InterfaceGuard; lift: Lift }) => object; /** Produce a revocable dispatch exo over the full union guard of all presheaf sections. */ From 0c865b02ad679e74f3d3b7fd0cb131ac3beca8c6 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:10:32 -0400 Subject: [PATCH 09/68] refactor(kernel-utils): redesign Lift as AsyncGenerator coroutine Replace the one-shot `Lift = (...) => Promise` with an AsyncGenerator coroutine protocol. The lift receives a snapshot of the accumulated error array on each `gen.next(errors)` call, yields candidates one at a time, and can stop early or fall through based on the error history. Add `drive.ts` with `driveLift` to encapsulate the retry loop used by `sheafify.ts`. Add `compose.ts` with `proxyLift`, `withFilter`, `withRanking`, and `fallthrough` as composition primitives. Export all four from `index.ts`. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.test.ts | 4 + packages/kernel-utils/src/index.ts | 6 + packages/kernel-utils/src/sheaf/compose.ts | 106 ++++++++++++++++ packages/kernel-utils/src/sheaf/drive.ts | 43 +++++++ .../src/sheaf/sheafify.e2e.test.ts | 66 ++++------ .../sheaf/sheafify.string-metadata.test.ts | 14 +-- .../kernel-utils/src/sheaf/sheafify.test.ts | 117 +++++++++--------- packages/kernel-utils/src/sheaf/sheafify.ts | 61 ++++++--- packages/kernel-utils/src/sheaf/types.ts | 15 ++- 9 files changed, 301 insertions(+), 131 deletions(-) create mode 100644 packages/kernel-utils/src/sheaf/compose.ts create mode 100644 packages/kernel-utils/src/sheaf/drive.ts diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index e897f50762..aa7694ed12 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -17,6 +17,7 @@ describe('index', () => { 'collectSheafGuard', 'constant', 'delay', + 'fallthrough', 'fetchValidatedJson', 'fromHex', 'getStalk', @@ -38,6 +39,7 @@ describe('index', () => { 'mergeDisjointRecords', 'methodArgsToStruct', 'prettifySmallcaps', + 'proxyLift', 'retry', 'retryWithBackoff', 'sheafify', @@ -45,6 +47,8 @@ describe('index', () => { 'stringify', 'toHex', 'waitUntilQuiescent', + 'withFilter', + 'withRanking', ]); }); }); diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index e40797c3f2..df7a6b05fa 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -56,5 +56,11 @@ export type { } from './sheaf/types.ts'; export { constant, source, callable } from './sheaf/metadata.ts'; export { sheafify } from './sheaf/sheafify.ts'; +export { + proxyLift, + withFilter, + withRanking, + fallthrough, +} from './sheaf/compose.ts'; export { collectSheafGuard } from './sheaf/guard.ts'; export { getStalk, guardCoversPoint } from './sheaf/stalk.ts'; diff --git a/packages/kernel-utils/src/sheaf/compose.ts b/packages/kernel-utils/src/sheaf/compose.ts new file mode 100644 index 0000000000..66e323bdb6 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/compose.ts @@ -0,0 +1,106 @@ +import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; + +/** + * Proxy a lift coroutine, forwarding yielded candidates up and received + * error arrays down to the inner generator. + * + * Note: async generator `yield*` DOES forward `.next(value)` to the + * delegated async iterator, so for simple sequential composition (e.g. + * `fallthrough`) you can use `yield*` directly. `proxyLift` is the right + * primitive when you need to add logic between yields — for example, + * logging, counting attempts, or conditionally stopping early based on the + * error history. + * + * @param gen - The inner async generator to proxy. + * @yields Candidates from the inner generator. + * @returns void when the inner generator is exhausted. + * @example + * // Lift that logs each retry + * const withLogging = (inner: Lift): Lift => + * async function*(germs, context) { + * const gen = inner(germs, context); + * let next = await gen.next([]); + * while (!next.done) { + * const errors: unknown[] = yield next.value; + * if (errors.length > 0) console.log(`retry #${errors.length}`); + * next = await gen.next(errors); + * } + * }; + * // The above pattern is exactly proxyLift with a side-effect added. + */ +export async function* proxyLift>( + gen: AsyncGenerator>, void, unknown[]>, +): AsyncGenerator>, void, unknown[]> { + let next = await gen.next([]); + while (!next.done) { + const errors: unknown[] = yield next.value; + next = await gen.next(errors); + } +} + +/** + * Filter germs before passing to a lift. + * + * Returns the inner lift's generator directly — no proxying needed since + * this is a pure input transform that delegates entirely to the inner lift. + * + * @param predicate - Returns true for germs that should be passed to the inner lift. + * @returns A lift combinator that filters its germs before delegating. + */ +export const withFilter = + >( + predicate: ( + germ: EvaluatedSection>, + ctx: LiftContext, + ) => boolean, + ) => + (inner: Lift): Lift => + (germs, context) => + inner( + germs.filter((germ) => predicate(germ, context)), + context, + ); + +/** + * Sort germs by a comparator before passing to a lift. + * + * Returns the inner lift's generator directly — no proxying needed since + * this is a pure input transform that delegates entirely to the inner lift. + * The original germs array is not mutated. + * + * @param comparator - Comparator function for sorting (same signature as Array.sort). + * @returns A lift combinator that sorts its germs before delegating. + */ +export const withRanking = + >( + comparator: ( + a: EvaluatedSection>, + b: EvaluatedSection>, + ) => number, + ) => + (inner: Lift): Lift => + (germs, context) => + inner([...germs].sort(comparator), context); + +/** + * Try all candidates from liftA, then all candidates from liftB. + * + * Uses `yield*` directly since async generator delegation forwards + * `.next(value)` to the inner iterator, so error arrays are correctly + * threaded through each inner lift. + * + * liftB starts fresh and only sees errors from its own failed attempts, + * not from liftA's attempts. + * + * @param liftA - First lift; its candidates are tried before liftB's. + * @param liftB - Fallback lift; only invoked after liftA is exhausted. + * @returns A combined lift that sequences liftA then liftB. + */ +export const fallthrough = >( + liftA: Lift, + liftB: Lift, +): Lift => + async function* (germs, context) { + yield* liftA(germs, context); + yield* liftB(germs, context); + }; diff --git a/packages/kernel-utils/src/sheaf/drive.ts b/packages/kernel-utils/src/sheaf/drive.ts new file mode 100644 index 0000000000..5b4f199ebd --- /dev/null +++ b/packages/kernel-utils/src/sheaf/drive.ts @@ -0,0 +1,43 @@ +import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; + +/** + * Drive a lift coroutine, retrying on failure and accumulating errors. + * + * Primes the generator with gen.next([]), then calls gen.next(errors) after + * each failed attempt where errors is the full ordered history. Returns the + * first successful result, or rethrows the last error when exhausted. + * + * @param lift - The lift coroutine to drive. + * @param germs - The evaluated sections to pass to the lift. + * @param context - The dispatch context (method, args, constraints). + * @param invoke - Calls the section exo; throws on failure. + * @returns The result of the first successful invocation. + * @internal + */ +export const driveLift = async >( + lift: Lift, + germs: EvaluatedSection>[], + context: LiftContext, + invoke: (germ: EvaluatedSection>) => Promise, +): Promise => { + const errors: unknown[] = []; + const gen = lift(germs, context); + let next = await gen.next(errors); + while (!next.done) { + try { + const result = await invoke(next.value); + await gen.return(undefined); + return result; + } catch (error) { + errors.push(error); + next = await gen.next(errors); + } + } + const lastError = errors.at(-1); + if (lastError instanceof Error) { + throw lastError; + } + throw new Error(`No viable section for ${context.method}`, { + cause: lastError, + }); +}; diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts index 976b819fb9..5e669acc19 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -18,17 +18,12 @@ const E = (obj: unknown) => describe('e2e: cost-optimal routing', () => { it('argmin picks cheapest section, re-sheafification expands landscape', async () => { - const argmin: Lift<{ cost: number }> = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.cost ?? Infinity) < - (germs[bestIdx]!.metadata?.cost ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const argmin: Lift<{ cost: number }> = async function* (germs) { + yield* [...germs].sort( + (a, b) => + (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); + }; const remote0GetBalance = vi.fn((_acct: string): number => 0); const local1GetBalance = vi.fn((_acct: string): number => 0); @@ -117,17 +112,13 @@ describe('e2e: multi-tier capability routing', () => { type Tier = { latencyMs: number; label: string }; - const fastest: Lift = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.latencyMs ?? Infinity) < - (germs[bestIdx]!.metadata?.latencyMs ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const fastest: Lift = async function* (germs) { + yield* [...germs].sort( + (a, b) => + (a.metadata?.latencyMs ?? Infinity) - + (b.metadata?.latencyMs ?? Infinity), ); + }; it('routes reads to the fastest matching tier and writes to the only capable section', async () => { // Shared ledger — all sections read from this, so the sheaf condition @@ -333,17 +324,11 @@ describe('e2e: multi-tier capability routing', () => { mirrorGetBalance.mockClear(); // Policy B: highest latency wins (simulate "prefer-canonical-source"). - const slowest: Lift = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.latencyMs ?? 0) > - (germs[bestIdx]!.metadata?.latencyMs ?? 0) - ? idx - : bestIdx, - 0, - ), + const slowest: Lift = async function* (germs) { + yield* [...germs].sort( + (a, b) => (b.metadata?.latencyMs ?? 0) - (a.metadata?.latencyMs ?? 0), ); + }; const walletB = sheafify({ name: 'Wallet', sections: makeSections(), @@ -360,10 +345,9 @@ describe('e2e: multi-tier capability routing', () => { describe('e2e: preferAutonomous recovered as degenerate case', () => { it('binary push metadata recovers push-pull lift rule', async () => { - // Binary metadata: constant({ push: true }) = push section, { push: false } = pull - const preferPush: Lift<{ push: boolean }> = async (germs) => { - const pushIdx = germs.findIndex((entry) => entry.metadata?.push); - return Promise.resolve(pushIdx >= 0 ? pushIdx : 0); + const preferPush: Lift<{ push: boolean }> = async function* (germs) { + yield* germs.filter((germ) => germ.metadata?.push); + yield* germs.filter((germ) => !germ.metadata?.push); }; const pullGetBalance = vi.fn((_acct: string): number => 0); @@ -422,17 +406,11 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { type SwapCost = { cost: number }; - const cheapest: Lift = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.cost ?? Infinity) < - (germs[bestIdx]!.metadata?.cost ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const cheapest: Lift = async function* (germs) { + yield* [...germs].sort( + (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); + }; it('routes swap(50) to A and swap(100) to B based on callable cost metadata', async () => { const swapAFn = vi.fn( diff --git a/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts b/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts index 302d70300f..e0037042f5 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts @@ -38,17 +38,11 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { type SwapCost = { cost: number }; - const cheapest: Lift = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.cost ?? Infinity) < - (germs[bestIdx]!.metadata?.cost ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const cheapest: Lift = async function* (germs) { + yield* [...germs].sort( + (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); + }; it('routes swap(50) to A and swap(100) to B using source-kind metadata', async () => { const swapAFn = vi.fn( diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 868dd2b255..34decee2a7 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -25,9 +25,10 @@ const E = (obj: unknown) => describe('sheafify', () => { it('single-section bypass: lift not invoked', async () => { let liftCalled = false; - const lift: Lift<{ cost: number }> = async (_germs) => { + // eslint-disable-next-line require-yield + const lift: Lift<{ cost: number }> = async function* (_germs) { liftCalled = true; - return Promise.resolve(0); + // unreachable — fast path bypasses lift for single section }; const sections: PresheafSection<{ cost: number }>[] = [ @@ -65,7 +66,9 @@ describe('sheafify', () => { ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - lift: async (_germs) => Promise.resolve(0), + async *lift(_germs) { + // unreachable — zero-coverage path throws before reaching lift + }, }); await expect(E(wallet).getBalance('bob')).rejects.toThrow( 'No section covers', @@ -73,17 +76,12 @@ describe('sheafify', () => { }); it('lift receives metadata and picks winner', async () => { - const argmin: Lift<{ cost: number }> = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.cost ?? Infinity) < - (germs[bestIdx]!.metadata?.cost ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const argmin: Lift<{ cost: number }> = async function* (germs) { + yield* [...germs].sort( + (a, b) => + (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); + }; const sections: PresheafSection<{ cost: number }>[] = [ { @@ -141,7 +139,9 @@ describe('sheafify', () => { ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - lift: async (_germs) => Promise.resolve(0), + async *lift(germs) { + yield germs[0]!; + }, }); const guard = wallet[GET_INTERFACE_GUARD](); expect(guard).toBeDefined(); @@ -151,17 +151,12 @@ describe('sheafify', () => { }); it('re-sheafification picks up new sections and methods', async () => { - const argmin: Lift<{ cost: number }> = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.cost ?? Infinity) < - (germs[bestIdx]!.metadata?.cost ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const argmin: Lift<{ cost: number }> = async function* (germs) { + yield* [...germs].sort( + (a, b) => + (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); + }; const sections: PresheafSection<{ cost: number }>[] = [ { @@ -225,23 +220,20 @@ describe('sheafify', () => { ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - lift: async (_germs) => Promise.resolve(0), + async *lift(germs) { + yield germs[0]!; + }, }); expect(await E(wallet).getBalance('alice')).toBe(42); }); it('re-sheafification with pre-built exo picks up new methods', async () => { - const argmin: Lift<{ cost: number }> = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.cost ?? Infinity) < - (germs[bestIdx]!.metadata?.cost ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const argmin: Lift<{ cost: number }> = async function* (germs) { + yield* [...germs].sort( + (a, b) => + (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); + }; const sections: PresheafSection<{ cost: number }>[] = [ { @@ -306,7 +298,9 @@ describe('sheafify', () => { ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - lift: async (_germs) => Promise.resolve(0), + async *lift(germs) { + yield germs[0]!; + }, }); const guard = wallet[GET_INTERFACE_GUARD](); expect(guard).toBeDefined(); @@ -320,10 +314,10 @@ describe('sheafify', () => { let capturedGerms: EvaluatedSection>[] = []; let capturedContext: LiftContext | undefined; - const spy: Lift = async (germs, context) => { + const spy: Lift = async function* (germs, context) { capturedGerms = germs; capturedContext = context; - return Promise.resolve(0); + yield germs[0]!; }; const sections: PresheafSection[] = [ @@ -370,10 +364,10 @@ describe('sheafify', () => { let capturedGerms: EvaluatedSection>[] = []; let capturedContext: LiftContext | undefined; - const spy: Lift = async (germs, context) => { + const spy: Lift = async function* (germs, context) { capturedGerms = germs; capturedContext = context; - return Promise.resolve(0); + yield germs[0]!; }; const sections: PresheafSection[] = [ @@ -437,9 +431,9 @@ describe('sheafify', () => { ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - lift: async (_germs) => { + // eslint-disable-next-line require-yield + async *lift(_germs) { liftCalled = true; - return Promise.resolve(0); }, }); await E(wallet).getBalance('alice'); @@ -475,9 +469,9 @@ describe('sheafify', () => { ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - lift: async (_germs) => { + // eslint-disable-next-line require-yield + async *lift(_germs) { liftCalled = true; - return Promise.resolve(0); }, }); await E(wallet).getBalance('alice'); @@ -486,17 +480,12 @@ describe('sheafify', () => { }); it('mixed sections participate in lift', async () => { - const argmin: Lift<{ cost: number }> = async (germs) => - Promise.resolve( - germs.reduce( - (bestIdx, entry, idx) => - (entry.metadata?.cost ?? Infinity) < - (germs[bestIdx]!.metadata?.cost ?? Infinity) - ? idx - : bestIdx, - 0, - ), + const argmin: Lift<{ cost: number }> = async function* (germs) { + yield* [...germs].sort( + (a, b) => + (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); + }; const exo = makeExo( 'cheap', @@ -546,7 +535,9 @@ describe('sheafify', () => { const sheaf = sheafify({ name: 'Wallet', sections }); const wallet = sheaf.getGlobalSection({ - lift: async () => Promise.resolve(0), + async *lift(germs) { + yield germs[0]!; + }, }); expect(await E(wallet).getBalance('alice')).toBe(42); @@ -578,7 +569,9 @@ describe('sheafify', () => { const sheaf = sheafify({ name: 'Wallet', sections }); const wallet = sheaf.getGlobalSection({ - lift: async () => Promise.resolve(0), + async *lift(germs) { + yield germs[0]!; + }, }); expect(await E(wallet).getBalance('alice')).toBe(42); @@ -609,7 +602,11 @@ describe('sheafify', () => { // No sections granted yet expect(sheaf.getExported()).toBeUndefined(); - sheaf.getGlobalSection({ lift: async () => Promise.resolve(0) }); + sheaf.getGlobalSection({ + async *lift(germs) { + yield germs[0]!; + }, + }); const exported = sheaf.getExported(); expect(exported).toBeDefined(); @@ -632,7 +629,11 @@ describe('sheafify', () => { ]; const sheaf = sheafify({ name: 'Wallet', sections }); - sheaf.getGlobalSection({ lift: async () => Promise.resolve(0) }); + sheaf.getGlobalSection({ + async *lift(germs) { + yield germs[0]!; + }, + }); expect(sheaf.getExported()).toBeDefined(); diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 33c0f09f36..be576a5147 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -21,6 +21,7 @@ import { import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; import { stringify } from '../stringify.ts'; +import { driveLift } from './drive.ts'; import { collectSheafGuard } from './guard.ts'; import type { MethodGuardPayload } from './guard.ts'; import { evaluateMetadata, resolveMetaDataSpec } from './metadata.ts'; @@ -158,6 +159,23 @@ const asyncifyMethodGuards = ( return asyncMethodGuards; }; +/** + * Invoke a method on a section exo, throwing if the handler is missing. + * + * @param exo - The section exo to invoke. + * @param method - The method name to call. + * @param args - The positional arguments. + * @returns The synchronous return value of the method (typically a Promise). + */ +const invokeExo = (exo: Section, method: string, args: unknown[]): unknown => { + const obj = exo as Record unknown>; + const fn = obj[method]; + if (fn === undefined) { + throw new Error(`Section has guard for '${method}' but no handler`); + } + return fn.call(obj, ...args); +}; + type Grant = { exo: Section; guard: InterfaceGuard; @@ -221,32 +239,45 @@ export const sheafify = < metadata: evaluateMetadata(section.spec, args), }), ); - let winner: EvaluatedSection; switch (evaluatedStalk.length) { case 0: throw new Error(`No section covers ${method}(${stringify(args, 0)})`); case 1: - winner = evaluatedStalk[0] as EvaluatedSection; - break; + return invokeExo( + (evaluatedStalk[0] as EvaluatedSection).exo, + method, + args, + ); default: { const collapsed = collapseEquivalent(evaluatedStalk); if (collapsed.length === 1) { - winner = collapsed[0] as EvaluatedSection; - break; + return invokeExo( + (collapsed[0] as EvaluatedSection).exo, + method, + args, + ); } const { constraints, stripped } = decomposeMetadata(collapsed); - const index = await lift(stripped, { method, args, constraints }); - winner = collapsed[index] as EvaluatedSection; - break; + const strippedToCollapsed = new Map( + stripped.map((strippedGerm, i) => [ + strippedGerm, + collapsed[i] as EvaluatedSection, + ]), + ); + return driveLift( + lift, + stripped, + { method, args, constraints }, + async (germ) => { + const section = strippedToCollapsed.get(germ); + if (section === undefined) { + throw new Error('lift yielded an unknown germ'); + } + return invokeExo(section.exo, method, args); + }, + ); } } - - const obj = winner.exo as Record unknown>; - const fn = obj[method]; - if (fn === undefined) { - throw new Error(`Section has guard for '${method}' but no handler`); - } - return fn.call(obj, ...args); }; const handlers: Record Promise> = diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index b42f3e89ee..613d484d91 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -63,18 +63,25 @@ export type LiftContext> = { }; /** - * Lift: selects one germ from the stalk when multiple germs remain after - * collapsing equivalent presheaf sections. + * Lift: a coroutine that yields candidates in preference order and receives + * the accumulated error list after each failed attempt. * * Each germ carries only distinguishing metadata (options); shared metadata * (constraints) is delivered separately in the context. * - * Returns a Promise — the index into the germs array. + * The sheaf calls gen.next([]) to prime the coroutine, then gen.next(errors) + * after each failure, where errors is the ordered list of every error + * encountered so far. The generator can inspect the history to decide whether + * to yield another candidate or return (signal exhaustion). The sheaf + * rethrows the last error when the generator is done. + * + * Simple lifts that do not need retry logic can ignore the error input: + * async function*(germs) { yield* [...germs].sort(comparator); } */ export type Lift> = ( germs: EvaluatedSection>[], context: LiftContext, -) => Promise; +) => AsyncGenerator>, void, unknown[]>; /** * A presheaf: a plain array of presheaf sections. From 24e7962205fd1963b935e4ca25e28d8d209cc432 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:12:35 -0400 Subject: [PATCH 10/68] test(kernel-utils): add unit tests for lift composition helpers Cover proxyLift, withFilter, withRanking, fallthrough, and composed combinations in compose.test.ts. Includes driveToExhaustion and driveWithSuccessOn test helpers that pass error snapshots (not mutable references) to gen.next, so inner generators can safely store the received arrays. Co-Authored-By: Claude Sonnet 4.6 --- .../kernel-utils/src/sheaf/compose.test.ts | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 packages/kernel-utils/src/sheaf/compose.test.ts diff --git a/packages/kernel-utils/src/sheaf/compose.test.ts b/packages/kernel-utils/src/sheaf/compose.test.ts new file mode 100644 index 0000000000..446221c807 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/compose.test.ts @@ -0,0 +1,388 @@ +import { describe, it, expect, vi } from 'vitest'; + +import { fallthrough, proxyLift, withFilter, withRanking } from './compose.ts'; +import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +type Meta = { id: string; cost: number }; +type G = EvaluatedSection>; + +const makeGerm = (id: string, cost = 0): G => ({ + exo: {} as G['exo'], + metadata: { id, cost }, +}); + +const ctx: LiftContext = { + method: 'transfer', + args: ['alice', 100n], + constraints: {}, +}; + +/** + * Drive a lift to exhaustion, simulating a failure after each yielded + * candidate. Returns all yielded germs in order and the error arrays + * the generator received. + * + * @param lift - The lift to drive. + * @param germs - The germs to pass to the lift. + * @param context - The lift context. + * @returns Yielded germs and error snapshots received by the generator. + */ +const driveToExhaustion = async ( + lift: Lift, + germs: G[], + context: LiftContext = ctx, +): Promise<{ yielded: G[]; receivedErrors: unknown[][] }> => { + const yielded: G[] = []; + const receivedErrors: unknown[][] = []; + const errors: unknown[] = []; + const gen = lift(germs, context); + let next = await gen.next([...errors]); + while (!next.done) { + yielded.push(next.value); + errors.push(new Error(`attempt ${errors.length + 1} failed`)); + receivedErrors.push([...errors]); + next = await gen.next([...errors]); + } + return { yielded, receivedErrors }; +}; + +/** + * Drive a lift, succeeding on the nth candidate (1-based). + * Returns the winning germ. + * + * @param lift - The lift to drive. + * @param germs - The germs to pass to the lift. + * @param successOn - Which attempt number (1-based) should succeed. + * @param context - The lift context. + * @returns The germ that won on attempt `successOn`. + */ +const driveWithSuccessOn = async ( + lift: Lift, + germs: G[], + successOn: number, + context: LiftContext = ctx, +): Promise => { + const errors: unknown[] = []; + const gen = lift(germs, context); + let attempt = 0; + let next = await gen.next([...errors]); + while (!next.done) { + attempt += 1; + if (attempt === successOn) { + await gen.return(undefined); + return next.value; + } + errors.push(new Error(`attempt ${attempt} failed`)); + next = await gen.next([...errors]); + } + throw new Error('generator exhausted before success'); +}; + +// --------------------------------------------------------------------------- +// proxyLift +// --------------------------------------------------------------------------- + +describe('proxyLift', () => { + it('forwards all yielded values from inner generator', async () => { + const [germA, germB, germC] = [makeGerm('a'), makeGerm('b'), makeGerm('c')]; + const inner = async function* (): AsyncGenerator { + yield germA; + yield germB; + yield germC; + }; + + const { yielded } = await driveToExhaustion(() => proxyLift(inner()), []); + expect(yielded).toStrictEqual([germA, germB, germC]); + }); + + it('forwards error arrays down to inner generator', async () => { + const [germA, germB] = [makeGerm('a'), makeGerm('b')]; + const receivedByInner: unknown[][] = []; + + const inner = async function* (): AsyncGenerator { + const errors1: unknown[] = yield germA; + receivedByInner.push(errors1); + const errors2: unknown[] = yield germB; + receivedByInner.push(errors2); + }; + + await driveToExhaustion(() => proxyLift(inner()), []); + + expect(receivedByInner).toHaveLength(2); + expect(receivedByInner[0]).toHaveLength(1); // one error after first attempt + expect(receivedByInner[1]).toHaveLength(2); // two errors after second attempt + }); + + it('stops when inner generator is done', async () => { + const inner = async function* (): AsyncGenerator { + // immediately done + }; + + const { yielded } = await driveToExhaustion(() => proxyLift(inner()), []); + expect(yielded).toHaveLength(0); + }); + + it('allows inner generator to stop early based on errors', async () => { + const [germA, germB, germC] = [makeGerm('a'), makeGerm('b'), makeGerm('c')]; + + const inner = async function* (): AsyncGenerator { + let errors: unknown[] = yield germA; + // stop after first failure + if (errors.length > 0) { + return; + } + errors = yield germB; + if (errors.length > 0) { + return; + } + yield germC; + }; + + const { yielded } = await driveToExhaustion(() => proxyLift(inner()), []); + // Only 'a' yielded — inner stops after receiving the first error + expect(yielded).toStrictEqual([germA]); + }); +}); + +// --------------------------------------------------------------------------- +// withFilter +// --------------------------------------------------------------------------- + +describe('withFilter', () => { + it('passes only matching germs to the inner lift', async () => { + const germs = [makeGerm('a', 1), makeGerm('b', 2), makeGerm('c', 3)]; + const received = vi.fn(); + + const inner: Lift = async function* (allGerms) { + received(allGerms.map((item) => item.metadata.id)); + yield* allGerms; + }; + + const lift = withFilter((germ) => (germ.metadata.cost ?? 0) >= 2)( + inner, + ); + await driveToExhaustion(lift, germs); + + expect(received).toHaveBeenCalledWith(['b', 'c']); + }); + + it('passes context to the predicate', async () => { + const germs = [makeGerm('alice'), makeGerm('bob')]; + const contextUsed: LiftContext[] = []; + + const lift = withFilter((_germ, liftContext) => { + contextUsed.push(liftContext); + return true; + })(async function* (allGerms) { + yield* allGerms; + }); + + await driveToExhaustion(lift, germs); + + expect(contextUsed.length).toBeGreaterThan(0); + expect(contextUsed[0]).toStrictEqual(ctx); + }); + + it('yields nothing when no germs match', async () => { + const germs = [makeGerm('a', 1)]; + const lift = withFilter(() => false)(async function* (allGerms) { + yield* allGerms; + }); + + const { yielded } = await driveToExhaustion(lift, germs); + expect(yielded).toHaveLength(0); + }); + + it('returns the inner lift generator directly (no extra wrapping)', () => { + // withFilter is a pure input transform — it returns the inner lift's + // generator, not a new proxy generator. + const innerGen = {} as AsyncGenerator; + const inner: Lift = vi.fn(() => innerGen); + const lift = withFilter(() => true)(inner); + + const result = lift([], ctx); + expect(result).toBe(innerGen); + }); +}); + +// --------------------------------------------------------------------------- +// withRanking +// --------------------------------------------------------------------------- + +describe('withRanking', () => { + it('sorts germs before passing to inner lift', async () => { + const germs = [makeGerm('a', 3), makeGerm('b', 1), makeGerm('c', 2)]; + const received = vi.fn(); + + const inner: Lift = async function* (allGerms) { + received(allGerms.map((item) => item.metadata.id)); + yield* allGerms; + }; + + const lift = withRanking( + (a, b) => (a.metadata.cost ?? 0) - (b.metadata.cost ?? 0), + )(inner); + await driveToExhaustion(lift, germs); + + expect(received).toHaveBeenCalledWith(['b', 'c', 'a']); + }); + + it('does not mutate the original germs array', async () => { + const germs = [makeGerm('a', 3), makeGerm('b', 1)]; + const original = [...germs]; + + const lift = withRanking( + (a, b) => (a.metadata.cost ?? 0) - (b.metadata.cost ?? 0), + )(async function* (allGerms) { + yield* allGerms; + }); + + await driveToExhaustion(lift, germs); + expect(germs).toStrictEqual(original); + }); + + it('returns the inner lift generator directly (no extra wrapping)', () => { + const innerGen = {} as AsyncGenerator; + const inner: Lift = vi.fn(() => innerGen); + const lift = withRanking(() => 0)(inner); + + const result = lift([], ctx); + expect(result).toBe(innerGen); + }); +}); + +// --------------------------------------------------------------------------- +// fallthrough +// --------------------------------------------------------------------------- + +describe('fallthrough', () => { + it('yields all candidates from liftA then liftB', async () => { + const [a1, a2, b1, b2] = [ + makeGerm('a1'), + makeGerm('a2'), + makeGerm('b1'), + makeGerm('b2'), + ]; + + const liftA: Lift = async function* () { + yield a1; + yield a2; + }; + const liftB: Lift = async function* () { + yield b1; + yield b2; + }; + + const { yielded } = await driveToExhaustion(fallthrough(liftA, liftB), []); + expect(yielded).toStrictEqual([a1, a2, b1, b2]); + }); + + it('stops at liftA winner and does not invoke liftB', async () => { + const [a1, a2] = [makeGerm('a1'), makeGerm('a2')]; + const liftBInvoked = vi.fn(); + + const liftA: Lift = async function* () { + yield a1; + yield a2; + }; + const liftB: Lift = async function* () { + liftBInvoked(); + yield makeGerm('b1'); + }; + + // Succeed on first candidate + const winner = await driveWithSuccessOn(fallthrough(liftA, liftB), [], 1); + expect(winner).toBe(a1); + expect(liftBInvoked).not.toHaveBeenCalled(); + }); + + it('falls through to liftB when liftA is exhausted', async () => { + const [a1, b1] = [makeGerm('a1'), makeGerm('b1')]; + + const liftA: Lift = async function* () { + yield a1; + }; + const liftB: Lift = async function* () { + yield b1; + }; + + // liftA has one candidate (a1), fail it, then liftB kicks in + const winner = await driveWithSuccessOn(fallthrough(liftA, liftB), [], 2); + expect(winner).toBe(b1); + }); + + it('forwards error arrays through yield* to each inner lift', async () => { + const [a1, b1] = [makeGerm('a1'), makeGerm('b1')]; + const errorsReceivedByA: unknown[][] = []; + const errorsReceivedByB: unknown[][] = []; + + const liftA: Lift = async function* () { + const errors: unknown[] = yield a1; + errorsReceivedByA.push(errors); + }; + const liftB: Lift = async function* () { + const errors: unknown[] = yield b1; + errorsReceivedByB.push(errors); + }; + + await driveToExhaustion(fallthrough(liftA, liftB), []); + + // liftA's first yield received one error (a1 failed) + expect(errorsReceivedByA[0]).toHaveLength(1); + // liftB's first yield received two errors (a1 + b1 both failed) + expect(errorsReceivedByB[0]).toHaveLength(2); + }); +}); + +// --------------------------------------------------------------------------- +// Composition: withFilter + withRanking + fallthrough +// --------------------------------------------------------------------------- + +describe('composition', () => { + it('withFilter composed with withRanking applies both transforms', async () => { + const germs = [ + makeGerm('a', 3), + makeGerm('b', 1), + makeGerm('c', 2), + makeGerm('d', 4), // filtered out (cost > 3) + ]; + const received = vi.fn(); + + const base: Lift = async function* (allGerms) { + received(allGerms.map((item) => item.metadata.id)); + yield* allGerms; + }; + + const lift = withFilter((germ) => (germ.metadata.cost ?? 0) <= 3)( + withRanking( + (a, b) => (a.metadata.cost ?? 0) - (b.metadata.cost ?? 0), + )(base), + ); + + await driveToExhaustion(lift, germs); + // filtered to a/b/c, sorted by cost ascending + expect(received).toHaveBeenCalledWith(['b', 'c', 'a']); + }); + + it('proxyLift wrapping fallthrough threads errors through both layers', async () => { + const [a1, b1] = [makeGerm('a1'), makeGerm('b1')]; + const inner: Lift = fallthrough( + async function* () { + yield a1; + }, + async function* () { + yield b1; + }, + ); + + // proxyLift wrapping the whole fallthrough + const lift: Lift = () => proxyLift(inner([], ctx)); + + const { yielded } = await driveToExhaustion(lift, []); + expect(yielded).toStrictEqual([a1, b1]); + }); +}); From d8f2a88271e37d646f4f32232c8c94031e44aabd Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:36:32 -0400 Subject: [PATCH 11/68] fix(kernel-utils): driveLift throws with accumulated errors as cause On exhaustion, throw a new Error with the full errors array as `cause` rather than re-throwing the last error. This preserves the complete failure history for diagnostics. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/drive.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/drive.ts b/packages/kernel-utils/src/sheaf/drive.ts index 5b4f199ebd..2bba341509 100644 --- a/packages/kernel-utils/src/sheaf/drive.ts +++ b/packages/kernel-utils/src/sheaf/drive.ts @@ -5,7 +5,8 @@ import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; * * Primes the generator with gen.next([]), then calls gen.next(errors) after * each failed attempt where errors is the full ordered history. Returns the - * first successful result, or rethrows the last error when exhausted. + * first successful result, or throws a new error with all accumulated errors + * as the cause when exhausted. * * @param lift - The lift coroutine to drive. * @param germs - The evaluated sections to pass to the lift. @@ -33,11 +34,7 @@ export const driveLift = async >( next = await gen.next(errors); } } - const lastError = errors.at(-1); - if (lastError instanceof Error) { - throw lastError; - } throw new Error(`No viable section for ${context.method}`, { - cause: lastError, + cause: errors, }); }; From 939fc73cb558de4e802d679337d3fa5ebdccdb91 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:43:14 -0400 Subject: [PATCH 12/68] docs(kernel-utils): document sheaf module in README Add a Sheaf Module section covering sheafify, metadata kinds, lift authoring, composition helpers, and error handling on exhaustion. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/README.md | 104 ++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/packages/kernel-utils/README.md b/packages/kernel-utils/README.md index 7bc7e8cf29..53abcb1b0a 100644 --- a/packages/kernel-utils/README.md +++ b/packages/kernel-utils/README.md @@ -26,6 +26,110 @@ or npm install --save-dev patch-package ``` +## Sheaf Module + +The sheaf module provides a dispatch abstraction for routing method calls across multiple capability objects (_sections_) that each cover a region of a shared interface. + +### Overview + +``` +sheafify({ name, sections, compartment? }) → Sheaf +sheaf.getGlobalSection({ lift }) → section proxy +sheaf.getSection({ guard, lift }) → section proxy +``` + +Each call on the proxy is dispatched to whichever section covers that method. When multiple sections are eligible, a **lift** selects among them. A lift is an `AsyncGenerator` coroutine that yields candidates one at a time and receives the accumulated error history on each resume — enabling retry, fallback, and cost-aware routing without callers needing to know the selection strategy. + +### Defining sections + +```ts +import { sheafify, constant, callable } from '@metamask/kernel-utils'; + +const sheaf = sheafify({ + name: 'Wallet', + sections: [ + { + exo: walletA, + metadata: constant({ cost: 10, push: false }), + }, + { + exo: walletB, + // callable metadata is evaluated per-call with the actual arguments + metadata: callable((args) => ({ cost: 1 + 0.1 * (args[0] as number) })), + }, + { + exo: walletC, + // source metadata is compiled once at sheafify time via the compartment + metadata: source(`(args) => ({ cost: 5 + 0.01 * args[0] })`), + }, + ], + compartment, // required only when using source-kind metadata +}); +``` + +**Metadata kinds:** +| Kind | When evaluated | Use case | +|------|---------------|----------| +| `constant(v)` | Never (static) | Fixed priority or capability flags | +| `callable(fn)` | Each call | Arg-dependent cost, remaining spend | +| `source(str)` | Each call (compiled at construction) | Sandboxed cost functions | + +### Writing a lift + +A lift receives `EvaluatedSection>[]` (germs) and a context, and yields candidates in preference order. It receives a snapshot of all accumulated errors on each `gen.next(errors)` call. + +```ts +import type { Lift } from '@metamask/kernel-utils'; + +// Yield cheapest section first; fall back in cost order on failure +const cheapest: Lift<{ cost: number }> = async function* (germs) { + yield* [...germs].sort( + (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), + ); +}; + +const section = sheaf.getGlobalSection({ lift: cheapest }); +``` + +### Composing lifts + +```ts +import { + withFilter, + withRanking, + fallthrough, + proxyLift, +} from '@metamask/kernel-utils'; + +// Filter out sections with insufficient remaining spend +const spendable = withFilter( + (germ, { args }) => + (germ.metadata?.remainingSpend ?? Infinity) >= (args[0] as number), +); + +// Sort by cost before passing to the inner lift +const byCost = withRanking( + (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), +); + +// Try local sections first, fall through to remote on exhaustion +const withFallback = fallthrough(localLift, remoteLift); + +// Compose: filter → rank → select +const lift = spendable(byCost(cheapest)); +``` + +`withFilter` and `withRanking` are pure input transforms that return the inner lift's generator directly. `fallthrough` sequences two lifts via `yield*`, which forwards the error array to each inner lift. `proxyLift` is the primitive for adding logic (logging, circuit-breaking) between yields. + +### Error handling + +When all candidates are exhausted, `driveLift` throws: + +``` +Error: No viable section for + cause: [Error: ..., Error: ..., ...] // all accumulated attempt errors +``` + ## Contributing This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme). From 1ba14d95432bf6d0f2fe371ae7c57a0ef0d12ed3 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:47:56 -0400 Subject: [PATCH 13/68] feat(kernel-utils): add makeRemoteSection for CapTP remote refs as sheaf sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `makeRemoteSection(name, remoteRef, metadata?)` which asynchronously fetches the interface guard from a CapTP remote ref via E()[GET_INTERFACE_GUARD]() and returns a PresheafSection with a local forwarding exo — eliminating the boilerplate of building per-method handlers by hand when wrapping remote caps. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/package.json | 1 + packages/kernel-utils/src/index.test.ts | 1 + packages/kernel-utils/src/index.ts | 1 + .../kernel-utils/src/sheaf/remote.test.ts | 111 ++++++++++++++++++ packages/kernel-utils/src/sheaf/remote.ts | 50 ++++++++ 5 files changed, 164 insertions(+) create mode 100644 packages/kernel-utils/src/sheaf/remote.test.ts create mode 100644 packages/kernel-utils/src/sheaf/remote.ts diff --git a/packages/kernel-utils/package.json b/packages/kernel-utils/package.json index b57ed11fbc..8bf7d03b66 100644 --- a/packages/kernel-utils/package.json +++ b/packages/kernel-utils/package.json @@ -121,6 +121,7 @@ "@chainsafe/libp2p-yamux": "8.0.1", "@endo/captp": "^4.4.8", "@endo/errors": "^1.2.13", + "@endo/eventual-send": "^1.3.0", "@endo/exo": "^1.5.12", "@endo/patterns": "^1.7.0", "@endo/promise-kit": "^1.1.13", diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index aa7694ed12..5d7737a767 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -36,6 +36,7 @@ describe('index', () => { 'makeDefaultExo', 'makeDefaultInterface', 'makeDiscoverableExo', + 'makeRemoteSection', 'mergeDisjointRecords', 'methodArgsToStruct', 'prettifySmallcaps', diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index df7a6b05fa..03ecc6a7c5 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -63,4 +63,5 @@ export { fallthrough, } from './sheaf/compose.ts'; export { collectSheafGuard } from './sheaf/guard.ts'; +export { makeRemoteSection } from './sheaf/remote.ts'; export { getStalk, guardCoversPoint } from './sheaf/stalk.ts'; diff --git a/packages/kernel-utils/src/sheaf/remote.test.ts b/packages/kernel-utils/src/sheaf/remote.test.ts new file mode 100644 index 0000000000..55c731494d --- /dev/null +++ b/packages/kernel-utils/src/sheaf/remote.test.ts @@ -0,0 +1,111 @@ +import { GET_INTERFACE_GUARD, makeExo } from '@endo/exo'; +import { M } from '@endo/patterns'; +import { describe, it, expect, vi } from 'vitest'; + +import { constant } from './metadata.ts'; +import { makeRemoteSection } from './remote.ts'; +import type { Section } from './types.ts'; + +// Mirrors the local-E pattern used throughout sheaf tests: the test +// environment has no HandledPromise, so we mock E as a transparent cast. +// With this mock, E(exo) === exo, so [GET_INTERFACE_GUARD] and method calls +// resolve locally against the exo — equivalent to a local CapTP loopback. +vi.mock('@endo/eventual-send', () => ({ + E: (ref: unknown) => ref, +})); + +const makeRemoteExo = (tag: string) => + makeExo( + tag, + M.interface( + tag, + { + greet: M.callWhen(M.string()).returns(M.string()), + add: M.callWhen(M.number(), M.number()).returns(M.number()), + }, + { defaultGuards: 'passable' }, + ), + { + greet: async (name: string) => `Hello, ${name}!`, + add: async (a: number, b: number) => a + b, + }, + ) as unknown as Section; + +describe('makeRemoteSection', () => { + it('fetches the interface guard from the remote ref', async () => { + const remoteExo = makeRemoteExo('Remote'); + const { exo } = await makeRemoteSection('Wrapper', remoteExo); + expect(exo[GET_INTERFACE_GUARD]?.()).toStrictEqual( + remoteExo[GET_INTERFACE_GUARD]?.(), + ); + }); + + it('forwards method calls to the remote ref', async () => { + const greet = vi.fn(async (name: string) => `Hello, ${name}!`); + const remoteExo = makeExo( + 'Remote', + M.interface( + 'Remote', + { greet: M.callWhen(M.string()).returns(M.string()) }, + { defaultGuards: 'passable' }, + ), + { greet }, + ) as unknown as Section; + + const { exo } = await makeRemoteSection('Wrapper', remoteExo); + const wrapper = exo as Record< + string, + (...a: unknown[]) => Promise + >; + const result = await wrapper.greet('Alice'); + + expect(greet).toHaveBeenCalledWith('Alice'); + expect(result).toBe('Hello, Alice!'); + }); + + it('forwards all methods declared in the guard', async () => { + const greet = vi.fn(async (_: string) => ''); + const add = vi.fn(async (a: number, b: number) => a + b); + const remoteExo = makeExo( + 'Remote', + M.interface( + 'Remote', + { + greet: M.callWhen(M.string()).returns(M.string()), + add: M.callWhen(M.number(), M.number()).returns(M.number()), + }, + { defaultGuards: 'passable' }, + ), + { greet, add }, + ) as unknown as Section; + + const { exo } = await makeRemoteSection('Wrapper', remoteExo); + const wrapper = exo as Record< + string, + (...a: unknown[]) => Promise + >; + await wrapper.greet('x'); + await wrapper.add(2, 3); + + expect(greet).toHaveBeenCalledTimes(1); + expect(add).toHaveBeenCalledWith(2, 3); + }); + + it('passes metadata through to the section', async () => { + const metadata = constant({ mode: 'remote' as const }); + const { metadata: actual } = await makeRemoteSection( + 'Wrapper', + makeRemoteExo('Remote'), + metadata, + ); + expect(actual).toBe(metadata); + }); + + it('metadata is undefined when not provided', async () => { + const { metadata } = await makeRemoteSection( + 'Wrapper', + makeRemoteExo('Remote'), + ); + expect(metadata).toBeUndefined(); + }); +}); diff --git a/packages/kernel-utils/src/sheaf/remote.ts b/packages/kernel-utils/src/sheaf/remote.ts new file mode 100644 index 0000000000..d905bd2996 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/remote.ts @@ -0,0 +1,50 @@ +import { E } from '@endo/eventual-send'; +import { GET_INTERFACE_GUARD, makeExo } from '@endo/exo'; +import { getInterfaceGuardPayload } from '@endo/patterns'; +import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; + +import type { MetaDataSpec, PresheafSection } from './types.ts'; + +/** + * Wrap a remote (CapTP) reference as a PresheafSection. + * + * The sheaf requires synchronous [GET_INTERFACE_GUARD] access on every section, + * but remote references are opaque CapTP handles that cannot provide this + * synchronously. This function fetches the guard from the remote via E() once + * at construction time, then creates a local wrapper exo that carries it and + * forwards every method call back to the remote via E(). + * + * @param name - Name for the wrapper exo. + * @param remoteRef - The remote reference to forward calls to. + * @param metadata - Optional metadata spec for the presheaf section. + * @returns A PresheafSection whose exo forwards method calls to the remote. + */ +export const makeRemoteSection = async >( + name: string, + remoteRef: object, + metadata?: MetaDataSpec, +): Promise> => { + const eProxy = E(remoteRef); + + const interfaceGuard: InterfaceGuard = await ( + eProxy as unknown as { [GET_INTERFACE_GUARD](): Promise } + )[GET_INTERFACE_GUARD](); + + const { methodGuards } = getInterfaceGuardPayload( + interfaceGuard, + ) as unknown as { + methodGuards: Record; + }; + + const remote = eProxy as unknown as Record< + string, + (...args: unknown[]) => Promise + >; + const handlers: Record Promise> = {}; + for (const method of Object.keys(methodGuards)) { + handlers[method] = async (...args: unknown[]) => remote[method](...args); + } + + const exo = makeExo(name, interfaceGuard, handlers); + return { exo, metadata }; +}; From f0666da4149bb97079774a160e4f8162f6cb8d84 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:48:27 -0400 Subject: [PATCH 14/68] feat(kernel-utils): add getDiscoverableSection, deprecate global section methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds getDiscoverableSection and getDiscoverableGlobalSection to the Sheaf API so callers can attach a MethodSchema (for __getDescription__) to the caller-facing dispatch section rather than inside individual capability wrappers. Marks getGlobalSection and getDiscoverableGlobalSection as @deprecated — callers should supply an explicit InterfaceGuard via getSection/getDiscoverableSection instead of relying on the auto-computed union. Co-Authored-By: Claude Sonnet 4.6 --- .../kernel-utils/src/sheaf/sheafify.test.ts | 58 +++++++++++++++++ packages/kernel-utils/src/sheaf/sheafify.ts | 64 ++++++++++++++----- packages/kernel-utils/src/sheaf/types.ts | 23 ++++++- 3 files changed, 129 insertions(+), 16 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 34decee2a7..2c98994a1e 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -2,6 +2,7 @@ import { makeExo, GET_INTERFACE_GUARD } from '@endo/exo'; import { M, getInterfaceGuardPayload } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; +import { GET_DESCRIPTION } from '../discoverable.ts'; import { constant } from './metadata.ts'; import { sheafify } from './sheafify.ts'; import type { @@ -614,6 +615,63 @@ describe('sheafify', () => { expect(methodGuards).toHaveProperty('getBalance'); }); + it('getDiscoverableGlobalSection exposes __getDescription__', async () => { + const schema = { + getBalance: { + description: 'Get account balance.', + args: { acct: { type: 'string' as const, description: 'Account id.' } }, + returns: { type: 'number' as const, description: 'Balance.' }, + }, + }; + const sections: PresheafSection>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + }, + ]; + + const section = sheafify({ + name: 'Wallet', + sections, + }).getDiscoverableGlobalSection({ + async *lift(germs) { + yield germs[0]!; + }, + schema, + }); + + expect(E(section)[GET_DESCRIPTION]()).toStrictEqual(schema); + }); + + it('getSection does not expose __getDescription__', () => { + const sections: PresheafSection>[] = [ + { + exo: makeExo( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ) as unknown as Section, + }, + ]; + + const section = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + async *lift(germs) { + yield germs[0]!; + }, + }); + + expect( + (section as Record)[GET_DESCRIPTION], + ).toBeUndefined(); + }); + it('getExported excludes revoked sections', () => { const sections: PresheafSection<{ cost: number }>[] = [ { diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index be576a5147..ca74773081 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -20,6 +20,8 @@ import { } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; +import { makeDiscoverableExo } from '../discoverable.ts'; +import type { MethodSchema } from '../schema.ts'; import { stringify } from '../stringify.ts'; import { driveLift } from './drive.ts'; import { collectSheafGuard } from './guard.ts'; @@ -210,12 +212,14 @@ export const sheafify = < ); const grants: Grant[] = []; - const getSection = ({ + const buildSection = ({ guard, lift, + schema, }: { guard: InterfaceGuard; lift: Lift; + schema?: Record; }): object => { const resolvedGuard = guard; @@ -286,11 +290,14 @@ export const sheafify = < handlers[method] = async (...args: unknown[]) => dispatch(method, args); } - const exo = makeExo( - `${name}:section`, - asyncGuard, - handlers, - ) as unknown as Section; + const exo = (schema === undefined + ? makeExo(`${name}:section`, asyncGuard, handlers) + : makeDiscoverableExo( + `${name}:section`, + handlers, + schema, + asyncGuard, + )) as unknown as Section; grants.push({ exo, @@ -304,15 +311,40 @@ export const sheafify = < return exo; }; - const getGlobalSection = ({ lift }: { lift: Lift }): object => { - return getSection({ - guard: collectSheafGuard( - name, - frozenSections.map(({ exo }) => exo), - ), - lift, - }); - }; + const unionGuard = (): InterfaceGuard => + collectSheafGuard( + name, + frozenSections.map(({ exo }) => exo), + ); + + const getSection = ({ + guard, + lift, + }: { + guard: InterfaceGuard; + lift: Lift; + }): object => buildSection({ guard, lift }); + + const getDiscoverableSection = ({ + guard, + lift, + schema, + }: { + guard: InterfaceGuard; + lift: Lift; + schema: Record; + }): object => buildSection({ guard, lift, schema }); + + const getGlobalSection = ({ lift }: { lift: Lift }): object => + buildSection({ guard: unionGuard(), lift }); + + const getDiscoverableGlobalSection = ({ + lift, + schema, + }: { + lift: Lift; + schema: Record; + }): object => buildSection({ guard: unionGuard(), lift, schema }); const revokePoint = (method: string, ...args: unknown[]): void => { for (const grant of grants) { @@ -342,7 +374,9 @@ export const sheafify = < return { getSection, + getDiscoverableSection, getGlobalSection, + getDiscoverableGlobalSection, revokePoint, getExported, revokeAll, diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index 613d484d91..86e319f497 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -10,6 +10,8 @@ import type { GET_INTERFACE_GUARD, Methods } from '@endo/exo'; import type { InterfaceGuard } from '@endo/patterns'; +import type { MethodSchema } from '../schema.ts'; + /** A section: a capability covering a region of the interface topology. */ export type Section = Partial & { [K in typeof GET_INTERFACE_GUARD]?: (() => InterfaceGuard) | undefined; @@ -98,8 +100,27 @@ export type Presheaf> = export type Sheaf> = { /** Produce a revocable dispatch exo over the given guard. */ getSection: (opts: { guard: InterfaceGuard; lift: Lift }) => object; - /** Produce a revocable dispatch exo over the full union guard of all presheaf sections. */ + /** Produce a revocable discoverable dispatch exo over the given guard. */ + getDiscoverableSection: (opts: { + guard: InterfaceGuard; + lift: Lift; + schema: Record; + }) => object; + /** + * Produce a revocable dispatch exo over the full union guard of all presheaf sections. + * + * @deprecated Provide an explicit guard via getSection instead. + */ getGlobalSection: (opts: { lift: Lift }) => object; + /** + * Produce a revocable discoverable dispatch exo over the full union guard of all presheaf sections. + * + * @deprecated Provide an explicit guard via getDiscoverableSection instead. + */ + getDiscoverableGlobalSection: (opts: { + lift: Lift; + schema: Record; + }) => object; /** Revoke every granted section whose guard covers the point (method, ...args). */ revokePoint: (method: string, ...args: unknown[]) => void; /** Union guard of all active (non-revoked) granted sections, or undefined. */ From d75c0f4fad18339ccfd83c70b06fe3b89dfd8d14 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:23:29 -0400 Subject: [PATCH 15/68] fix(kernel-utils): allow passable default guards for async section interfaces The async interface guard synthesized for a sheaf section must admit implicit exo methods like __getDescription__ that @endo/exo adds to every discoverable exo. Without passable default guards, those methods are rejected at dispatch time, preventing sheafs from being sent across a CapTP connection. Co-Authored-By: Claude Opus 4.7 --- packages/kernel-utils/src/sheaf/sheafify.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index ca74773081..29f924dc07 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -224,7 +224,12 @@ export const sheafify = < const resolvedGuard = guard; const asyncMethodGuards = asyncifyMethodGuards(resolvedGuard); - const asyncGuard = M.interface(`${name}:section`, asyncMethodGuards); + const asyncGuard = + schema === undefined + ? M.interface(`${name}:section`, asyncMethodGuards) + : M.interface(`${name}:section`, asyncMethodGuards, { + defaultGuards: 'passable', + }); let revoked = false; From d5a67fe42b9d40c37eddbb7a5e669f2d1b17f1d3 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:23:58 -0400 Subject: [PATCH 16/68] fix(kernel-utils): bind method calls in makeRemoteSection via E proxy Detaching a method via destructuring or assignment strips the CapTP receiver binding and the remote rejects the call as an "Unexpected receiver". Invoke each method through a fresh E(remote)[method] access so the receiver is preserved on every dispatch. Co-Authored-By: Claude Opus 4.7 --- packages/kernel-utils/package.json | 2 +- packages/kernel-utils/src/sheaf/remote.ts | 20 ++++++++++++-------- yarn.lock | 1 + 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/kernel-utils/package.json b/packages/kernel-utils/package.json index 8bf7d03b66..45f0e1fb3e 100644 --- a/packages/kernel-utils/package.json +++ b/packages/kernel-utils/package.json @@ -121,7 +121,7 @@ "@chainsafe/libp2p-yamux": "8.0.1", "@endo/captp": "^4.4.8", "@endo/errors": "^1.2.13", - "@endo/eventual-send": "^1.3.0", + "@endo/eventual-send": "^1.3.4", "@endo/exo": "^1.5.12", "@endo/patterns": "^1.7.0", "@endo/promise-kit": "^1.1.13", diff --git a/packages/kernel-utils/src/sheaf/remote.ts b/packages/kernel-utils/src/sheaf/remote.ts index d905bd2996..e06592db2f 100644 --- a/packages/kernel-utils/src/sheaf/remote.ts +++ b/packages/kernel-utils/src/sheaf/remote.ts @@ -3,7 +3,8 @@ import { GET_INTERFACE_GUARD, makeExo } from '@endo/exo'; import { getInterfaceGuardPayload } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; -import type { MetaDataSpec, PresheafSection } from './types.ts'; +import { ifDefined } from '../misc.ts'; +import type { MetaDataSpec, PresheafSection, Section } from './types.ts'; /** * Wrap a remote (CapTP) reference as a PresheafSection. @@ -24,10 +25,10 @@ export const makeRemoteSection = async >( remoteRef: object, metadata?: MetaDataSpec, ): Promise> => { - const eProxy = E(remoteRef); - const interfaceGuard: InterfaceGuard = await ( - eProxy as unknown as { [GET_INTERFACE_GUARD](): Promise } + E(remoteRef) as unknown as { + [GET_INTERFACE_GUARD](): Promise; + } )[GET_INTERFACE_GUARD](); const { methodGuards } = getInterfaceGuardPayload( @@ -36,15 +37,18 @@ export const makeRemoteSection = async >( methodGuards: Record; }; - const remote = eProxy as unknown as Record< + const remote = remoteRef as unknown as Record< string, (...args: unknown[]) => Promise >; const handlers: Record Promise> = {}; for (const method of Object.keys(methodGuards)) { - handlers[method] = async (...args: unknown[]) => remote[method](...args); + handlers[method] = async (...args: unknown[]) => + (E(remote) as Record Promise>)[ + method + ](...args); } - const exo = makeExo(name, interfaceGuard, handlers); - return { exo, metadata }; + const exo = makeExo(name, interfaceGuard, handlers) as unknown as Section; + return ifDefined({ exo, metadata }) as PresheafSection; }; diff --git a/yarn.lock b/yarn.lock index aae0c5f932..50c4244ac0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2721,6 +2721,7 @@ __metadata: "@chainsafe/libp2p-yamux": "npm:8.0.1" "@endo/captp": "npm:^4.4.8" "@endo/errors": "npm:^1.2.13" + "@endo/eventual-send": "npm:^1.3.4" "@endo/exo": "npm:^1.5.12" "@endo/patterns": "npm:^1.7.0" "@endo/promise-kit": "npm:^1.1.13" From 6319d09b87cca27c233bb2aa4410da1591be899d Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:31:11 -0400 Subject: [PATCH 17/68] fix(kernel-utils): suppress noUncheckedIndexedAccess false positive in makeRemoteSection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The E() proxy index lookup returns T|undefined under noUncheckedIndexedAccess, but method is always present — it comes from Object.keys(methodGuards). Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/remote.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kernel-utils/src/sheaf/remote.ts b/packages/kernel-utils/src/sheaf/remote.ts index e06592db2f..592dc70526 100644 --- a/packages/kernel-utils/src/sheaf/remote.ts +++ b/packages/kernel-utils/src/sheaf/remote.ts @@ -44,9 +44,11 @@ export const makeRemoteSection = async >( const handlers: Record Promise> = {}; for (const method of Object.keys(methodGuards)) { handlers[method] = async (...args: unknown[]) => + // method is always present: it comes from Object.keys(methodGuards) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (E(remote) as Record Promise>)[ method - ](...args); + ]!(...args); } const exo = makeExo(name, interfaceGuard, handlers) as unknown as Section; From 9d40a7909c989802300d282247510dcd14e3b49f Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:51:29 -0400 Subject: [PATCH 18/68] docs: Update changelogs --- packages/kernel-utils/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/kernel-utils/CHANGELOG.md b/packages/kernel-utils/CHANGELOG.md index e03168b0f3..56f164019b 100644 --- a/packages/kernel-utils/CHANGELOG.md +++ b/packages/kernel-utils/CHANGELOG.md @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `getLibp2pRelayHome()` to the `./nodejs` exports, returning the libp2p relay's bookkeeping directory (default `~/.libp2p-relay`, overridable via `$LIBP2P_RELAY_HOME`) — kept separate from `$OCAP_HOME` so one relay can serve daemons with different OCAP_HOMEs ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) - `startRelay()` accepts an optional `publicIp` that is fed to libp2p's `appendAnnounce`, so a relay running on a NAT-backed host can announce its publicly-reachable IPv4 alongside its bound NIC addresses ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) +- Add sheaf programming module ([#870](https://github.com/MetaMask/ocap-kernel/pull/870)) + - `sheafify()` for building a `Sheaf` capability authority from a collection of `PresheafSection`s, each an exo with optional invocation-dependent metadata + - `constant()`, `source()`, `callable()` for constructing metadata specs (static value, compartment-evaluated code string, and per-call function respectively) + - `proxyLift()`, `withFilter()`, `withRanking()`, `fallthrough()` for composing lifts to route and rank sections at dispatch time + - `collectSheafGuard()` for deriving a combined `InterfaceGuard` from all sections in a sheaf + - `getStalk()`, `guardCoversPoint()` for section lookup and guard checks + - `makeRemoteSection()` for wrapping a remote CapTP reference as a `PresheafSection`, fetching its interface guard once at construction and forwarding method calls via `E()` + - Types: `Sheaf`, `Section`, `PresheafSection`, `EvaluatedSection`, `MetaDataSpec`, `Lift`, `LiftContext`, `Presheaf` ## [0.5.0] From b595236194d191c5b7b99cb0af03b9ba42618dd8 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:50:34 -0400 Subject: [PATCH 19/68] docs(kernel-utils): Improve sheaf documentation --- packages/kernel-utils/src/sheaf/LIFT.md | 139 +++++++++++++++++ packages/kernel-utils/src/sheaf/README.md | 30 +++- packages/kernel-utils/src/sheaf/USAGE.md | 177 ++++++++++++++++++++++ 3 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 packages/kernel-utils/src/sheaf/LIFT.md create mode 100644 packages/kernel-utils/src/sheaf/USAGE.md diff --git a/packages/kernel-utils/src/sheaf/LIFT.md b/packages/kernel-utils/src/sheaf/LIFT.md new file mode 100644 index 0000000000..95445c69fe --- /dev/null +++ b/packages/kernel-utils/src/sheaf/LIFT.md @@ -0,0 +1,139 @@ +# Lift + +The lift is the caller-supplied selection policy in the sheaf dispatch +pipeline. It runs when the stalk at an invocation point contains more than one +germ and the sheaf has no data to resolve the ambiguity on its own. The caller +is responsible for writing a lift that is correct for the sections it will +receive. + +## Coroutine protocol + +The lift is an `async function*` generator, not a plain async function: + +```ts +type Lift = ( + germs: EvaluatedSection>[], + context: LiftContext, +) => AsyncGenerator>, void, unknown[]>; +``` + +The sheaf drives it with the following protocol: + +1. **Prime** — `gen.next([])` starts the coroutine. The empty array is + discarded; it exists only to satisfy the generator type. +2. **Yield** — the coroutine yields a candidate germ to try next. +3. **Attempt** — the sheaf calls the candidate's exo method. +4. **Success** — the result is returned; the generator is abandoned. +5. **Failure** — the sheaf calls `gen.next(errors)`, passing the ordered list + of every error thrown so far (cumulative, not just the last). The coroutine + receives this as the resolved value of its `yield` expression. +6. **Exhausted** — if the generator returns without yielding, the sheaf + rethrows the last error. + +Most lifts express a fixed priority order and can ignore the error input: + +```ts +const awayLift: Lift = async function* (germs) { + yield* germs.filter((g) => g.metadata?.mode === 'delegation'); + yield* germs.filter((g) => g.metadata?.mode === 'call-home'); +}; +``` + +A lift that inspects failure history can read the errors from yield: + +```ts +const cautious: Lift = async function* (germs) { + for (const germ of germs) { + const errors: unknown[] = yield germ; + // errors is the cumulative list of all failures so far, including the one + // just returned for this germ. Inspect to decide whether to continue. + if (errors.some(isUnrecoverable)) return; + } +}; +``` + +## LiftContext + +The second argument to the lift is a `LiftContext`: + +```ts +type LiftContext = { + method: string; // the method being dispatched + args: unknown[]; // the invocation arguments + constraints: Partial; // metadata keys identical across every germ +}; +``` + +**`constraints`** are metadata keys whose values are the same on every germ in +the stalk. Because all candidates agree on these keys, they carry no +information useful for choosing between them — the sheaf strips them from each +germ and delivers them separately. A lift that needs to know, say, the agreed +`protocol` version reads it from `context.constraints.protocol` rather than +from any individual germ. + +**`args`** is available for cases where the lift itself must inspect the call. +Most of the time, however, arg-dependent selection is better expressed as +`callable` metadata on the sections than as conditional logic in the lift. + +Consider a swap where each provider has a different cost curve over volume. +Encode each provider's cost as `callable` metadata evaluated at dispatch time: + +```ts +const sections: PresheafSection[] = [ + { + exo: providerAExo, + metadata: callable((args) => ({ cost: providerACost(Number(args[0])) })), + }, + { + exo: providerBExo, + metadata: callable((args) => ({ cost: providerBCost(Number(args[0])) })), + }, +]; +``` + +By the time the lift runs, `germ.metadata.cost` already holds the concrete +cost for this specific invocation — the swap amount has been applied. A lift +that sorts by cost needs no knowledge of `args` at all: + +```ts +const cheapestFirst: Lift = async function* (germs) { + yield* [...germs].sort( + (a, b) => (a.metadata?.cost ?? 0) - (b.metadata?.cost ?? 0), + ); +}; +``` + +This is why evaluable metadata exists: the arg-dependent logic lives with the +sections that own it, and the lift stays a pure selection policy. + +## Semantic equivalence assumption + +Two sections may differ in real ways — one might use TCP and the other UDP; one +might be a Rust implementation and the other JavaScript. The semantic +equivalence contract does not require that two sections be identical. It +requires only that **if two sections are indistinguishable by metadata, their +differences are immaterial to the authority invoker**. + +The sheaf relies on the following separation of responsibilities: + +- **Section constructors** are responsible for advertising every feature that + matters to callers. If transport protocol, latency tier, cost curve, or + freshness guarantee could affect the invoker's decision, it belongs in the + section's metadata. Omitting a distinguishing feature is a declaration that + callers need not care about it. + +- **Lift constructors** are responsible for selecting among the features that + section constructors have chosen to expose. The lift cannot see what was not + advertised. + +This is a semantic contract, not a runtime enforcement — the sheaf cannot +verify it. When a section constructor omits a feature from metadata, they are +asserting: for any authority invoker using this sheaf, that feature is +irrelevant. If the assertion is wrong, the collapse step may silently discard a +candidate that the lift would have ranked differently. + +> One `getBalance` provider uses a fully-synced node; another uses a lagging +> replica. If both are tagged `{ cost: 1 }` with no freshness field, the +> section constructors are asserting that freshness is immaterial to callers of +> this sheaf. If that is not true, `{ cost: 1, freshness: 'lagging' }` vs +> `{ cost: 1, freshness: 'live' }` would let the lift choose. diff --git a/packages/kernel-utils/src/sheaf/README.md b/packages/kernel-utils/src/sheaf/README.md index 19b5f7d2a0..c988136ac4 100644 --- a/packages/kernel-utils/src/sheaf/README.md +++ b/packages/kernel-utils/src/sheaf/README.md @@ -7,6 +7,9 @@ over a presheaf of capabilities. The sheaf grants revocable dispatch sections via `getSection`, tracks all delegated authority, and supports point-wise revocation. +See [USAGE.md](./USAGE.md) for annotated examples and [LIFT.md](./LIFT.md) for +the lift coroutine protocol and semantic equivalence assumption. + ## Concepts **Presheaf section** (`PresheafSection`) — The input data: a capability (exo) @@ -33,7 +36,11 @@ entries. > Stalk at `("getBalance", "alice")` might contain two germs (cost 1 vs 100); > stalk at `("transfer", ...)` might contain one. -**Lift** — An async function that selects one germ from a multi-germ stalk. +**Lift** — An `async function*` coroutine that yields candidates from a +multi-germ stalk in preference order. See [LIFT.md](./LIFT.md) for the +coroutine protocol, `LiftContext`, and the semantic equivalence assumption +required of all lifts. + At dispatch time, metadata is decomposed into **constraints** (keys with the same value across every germ — topologically determined, not a choice) and **options** (the remaining keys — the lift's actual decision space). The lift @@ -41,7 +48,9 @@ receives only options on each germ; constraints arrive separately in the context. > `argmin` by cost, `argmin` by latency, or any custom selection logic. The -> lift is never invoked for single-germ stalks. +> lift is never invoked when the stalk resolves to a single germ — either +> because only one section matched, or because all matching sections had +> identical metadata and collapsed to one representative. **Sheaf** — The authority manager returned by `sheafify`. Holds the presheaf data (captured at construction time) and a registry of all granted sections. @@ -50,7 +59,8 @@ data (captured at construction time) and a registry of all granted sections. const sheaf = sheafify({ name: 'Wallet', sections }); ``` -- `sheaf.getSection({ guard?, lift })` — produce a revocable dispatch exo +- `sheaf.getSection({ guard, lift })` — produce a revocable dispatch exo +- `sheaf.getDiscoverableSection({ guard, lift, schema })` — same, but the exo exposes its guard - `sheaf.revokePoint(method, ...args)` — revoke every granted section whose guard covers the point - `sheaf.getExported()` — union guard of all active (non-revoked) sections @@ -62,13 +72,25 @@ At each invocation point `(method, args)` within a granted section: ``` getStalk(sections, method, args) presheaf → stalk (filter by guard) +evaluateMetadata(stalk, args) metadata specs → concrete values collapseEquivalent(stalk) locality condition (quotient by metadata) decomposeMetadata(collapsed) restriction map (constraints / options) lift(stripped, { method, args, operational selection (extra-theoretic) constraints }) -dispatch to collapsed[index].exo evaluation +dispatch to chosen.exo evaluation ``` +The pipeline short-circuits at two points: if only one section matches the +guard, it is invoked directly without evaluate/collapse/lift; if all matching +sections collapse to an identical germ, the single representative is invoked +without calling the lift. + +`callable` and `source` metadata specs make the stalk shape depend on the +invocation arguments. A `swap(amount)` section can produce `{ cost: 'low' }` +for small amounts and `{ cost: 'high' }` for large ones, yielding a different +set of germs — and potentially a different lift outcome — for the same method +called with different arguments. + ## Design choices **Germ identity is metadata identity.** The collapse step quotients by diff --git a/packages/kernel-utils/src/sheaf/USAGE.md b/packages/kernel-utils/src/sheaf/USAGE.md new file mode 100644 index 0000000000..ac71282e6d --- /dev/null +++ b/packages/kernel-utils/src/sheaf/USAGE.md @@ -0,0 +1,177 @@ +# Usage + +## Single provider + +When there is only one section per invocation point, no lift is needed — the +dispatch short-circuits before the lift is ever called. Provide a no-op lift +as a placeholder: + +```ts +import { M } from '@endo/patterns'; +import { makeDefaultExo } from ''; +import { sheafify } from '@metamask/kernel-utils'; + +const noop = async function* (germs) { + yield* germs; +}; + +const priceGuard = M.interface('PriceService', { + getPrice: M.callWhen(M.await(M.string())).returns(M.await(M.number())), +}); + +const priceExo = makeDefaultExo('PriceService', priceGuard, { + async getPrice(token) { + return fetchPrice(token); + }, +}); + +const sheaf = sheafify({ + name: 'PriceService', + sections: [{ exo: priceExo }], +}); + +const section = sheaf.getSection({ guard: priceGuard, lift: noop }); +// section is a revocable dispatch exo; call it like any capability +const price = await E(section).getPrice('ETH'); +``` + +## Multiple providers with a lift + +When the stalk at a given invocation point contains more than one germ, the +sheaf calls the lift to choose. The lift is an `async function*` coroutine that +yields candidates in preference order; it receives accumulated errors as the +argument to each subsequent `.next()` so it can adapt its ranking. + +The idiomatic pattern is a generator that `yield*`s candidates filtered by +metadata, expressing priority tiers in source order: + +```ts +import { sheafify, constant } from '@metamask/kernel-utils'; +import type { Lift } from '@metamask/kernel-utils'; + +type WalletMeta = { mode: 'fast' | 'reliable' }; + +const preferFast: Lift = async function* (germs) { + yield* germs.filter((g) => g.metadata?.mode === 'fast'); + yield* germs.filter((g) => g.metadata?.mode === 'reliable'); +}; + +const sheaf = sheafify({ + name: 'Wallet', + sections: [ + { exo: fastExo, metadata: constant({ mode: 'fast' }) }, + { exo: reliableExo, metadata: constant({ mode: 'reliable' }) }, + ], +}); + +// guard restricts which methods callers may invoke +const section = sheaf.getSection({ guard: clientGuard, lift: preferFast }); +``` + +The sheaf drives the generator: it primes it with `gen.next([])`, calls the +chosen candidate, then passes any thrown errors back as `gen.next(errors)` so +the lift can adapt before yielding the next candidate. + +Use the `constant`, `source`, or `callable` helpers to build metadata specs: + +```ts +import { constant, source, callable } from '@metamask/kernel-utils'; + +// static value known at construction time +constant({ mode: 'fast' }); + +// JS source string compiled once in the sheaf's compartment at construction time +source(`(args) => ({ cost: args[0] > 9000 ? 'high' : 'low' })`); + +// live function evaluated at each dispatch — useful when cost varies by argument, +// e.g. a swap whose metadata encodes volume-based cost tiers +callable((args) => ({ cost: Number(args[0]) > 9000 ? 'high' : 'low' })); +``` + +## Discoverable sections + +`getDiscoverableSection` works like `getSection` but the returned exo exposes +its guard — it can be introspected by the caller to discover what methods and +argument shapes it accepts. Use this when the recipient needs to advertise +capability to a third party. It requires a `schema` map describing each method: + +```ts +import type { MethodSchema } from '@metamask/kernel-utils'; + +const schema: Record = { + getPrice: { description: 'Get the current price of a token.' }, +}; + +const section = sheaf.getDiscoverableSection({ + guard: clientGuard, + lift, + schema, +}); +``` + +`getSection` is the non-discoverable variant (no `schema` required). + +`getGlobalSection` and `getDiscoverableGlobalSection` derive the guard +automatically from the union of all presheaf sections. They are `@deprecated` +as a nudge toward explicit guards once the caller knows the section set — +explicit guards make the capability's scope visible at the call site. When +sections are assembled dynamically (e.g., rebuilt at runtime from a set of +grants that changes) and the union guard isn't known until after `sheafify` +runs, the global variants are the right choice. + +## Revocation + +```ts +// revoke every granted section whose guard covers this invocation point +sheaf.revokePoint('getPrice', 'ETH'); + +// revoke all granted sections at once +sheaf.revokeAll(); + +// union guard of all currently active (non-revoked) sections +const exported = sheaf.getExported(); +``` + +## Remote sections + +`makeRemoteSection` wraps a CapTP remote reference as a `PresheafSection`, +fetching the remote's guard once at construction and forwarding all calls via +`E()`. This lets you mix local exos and remote capabilities in the same sheaf: + +```ts +import { makeRemoteSection, constant } from '@metamask/kernel-utils'; + +const remoteSection = await makeRemoteSection( + 'RemoteWallet', // name for the wrapper exo + remoteCapRef, // CapTP reference + constant({ mode: 'remote' }), // optional metadata +); + +const sheaf = sheafify({ + name: 'Mixed', + sections: [localSection, remoteSection], +}); +``` + +## Lift composition + +`@metamask/kernel-utils` exports helpers for building lifts from composable +parts, useful when lift logic would otherwise be duplicated across callers: + +```ts +import { + proxyLift, + withFilter, + withRanking, + fallthrough, +} from '@metamask/kernel-utils'; +``` + +- **`withRanking(comparator, inner)`** — sort germs by comparator before + passing to `inner` +- **`withFilter(predicate, inner)`** — remove germs that fail `predicate` + before passing to `inner` +- **`fallthrough(liftA, liftB)`** — try all candidates from `liftA` first; + if all fail, try `liftB` +- **`proxyLift(inner)`** — forward yielded candidates up and error arrays down; + useful when wrapping a lift in middleware From 6b14ec50cea8f986af513b103244eedf4b1b4d16 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:54:29 -0400 Subject: [PATCH 20/68] refactor(kernel-utils): remove unused sheaf revocation API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revokePoint, revokeAll, and getExported are unused in application code — the evm-wallet prototype rebuilds the sheaf wholesale when the grant set changes rather than revoking individual sections. Remove the implementation, the Grant type, the revoked flag in buildSection, and all associated tests. Co-Authored-By: Claude Sonnet 4.6 --- .../kernel-utils/src/sheaf/sheafify.test.ts | 126 ------------------ packages/kernel-utils/src/sheaf/sheafify.ts | 57 +------- packages/kernel-utils/src/sheaf/types.ts | 6 - 3 files changed, 2 insertions(+), 187 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 2c98994a1e..a44e5502c9 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -516,105 +516,6 @@ describe('sheafify', () => { expect(await E(wallet).getBalance('alice')).toBe(42); }); - // --------------------------------------------------------------------------- - // Revocation - // --------------------------------------------------------------------------- - - it('revokePoint revokes sections covering the point', async () => { - const sections: PresheafSection<{ cost: number }>[] = [ - { - exo: makeExo( - 'Wallet:0', - M.interface('Wallet:0', { - getBalance: M.call(M.string()).returns(M.number()), - }), - { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, - metadata: constant({ cost: 1 }), - }, - ]; - - const sheaf = sheafify({ name: 'Wallet', sections }); - const wallet = sheaf.getGlobalSection({ - async *lift(germs) { - yield germs[0]!; - }, - }); - - expect(await E(wallet).getBalance('alice')).toBe(42); - - sheaf.revokePoint('getBalance', 'alice'); - - // Entire section is revoked, not just the specific point - await expect(E(wallet).getBalance('alice')).rejects.toThrow( - 'Section revoked', - ); - await expect(E(wallet).getBalance('bob')).rejects.toThrow( - 'Section revoked', - ); - }); - - it('revokeAll revokes all sections', async () => { - const sections: PresheafSection<{ cost: number }>[] = [ - { - exo: makeExo( - 'Wallet:0', - M.interface('Wallet:0', { - getBalance: M.call(M.string()).returns(M.number()), - }), - { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, - metadata: constant({ cost: 1 }), - }, - ]; - - const sheaf = sheafify({ name: 'Wallet', sections }); - const wallet = sheaf.getGlobalSection({ - async *lift(germs) { - yield germs[0]!; - }, - }); - - expect(await E(wallet).getBalance('alice')).toBe(42); - - sheaf.revokeAll(); - - await expect(E(wallet).getBalance('alice')).rejects.toThrow( - 'Section revoked', - ); - }); - - it('getExported returns union of active section guards', () => { - const sections: PresheafSection<{ cost: number }>[] = [ - { - exo: makeExo( - 'Wallet:0', - M.interface('Wallet:0', { - getBalance: M.call(M.string()).returns(M.number()), - }), - { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, - metadata: constant({ cost: 1 }), - }, - ]; - - const sheaf = sheafify({ name: 'Wallet', sections }); - - // No sections granted yet - expect(sheaf.getExported()).toBeUndefined(); - - sheaf.getGlobalSection({ - async *lift(germs) { - yield germs[0]!; - }, - }); - - const exported = sheaf.getExported(); - expect(exported).toBeDefined(); - const { methodGuards } = getInterfaceGuardPayload(exported!); - expect(methodGuards).toHaveProperty('getBalance'); - }); - it('getDiscoverableGlobalSection exposes __getDescription__', async () => { const schema = { getBalance: { @@ -671,31 +572,4 @@ describe('sheafify', () => { (section as Record)[GET_DESCRIPTION], ).toBeUndefined(); }); - - it('getExported excludes revoked sections', () => { - const sections: PresheafSection<{ cost: number }>[] = [ - { - exo: makeExo( - 'Wallet:0', - M.interface('Wallet:0', { - getBalance: M.call(M.string()).returns(M.number()), - }), - { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, - metadata: constant({ cost: 1 }), - }, - ]; - - const sheaf = sheafify({ name: 'Wallet', sections }); - sheaf.getGlobalSection({ - async *lift(germs) { - yield germs[0]!; - }, - }); - - expect(sheaf.getExported()).toBeDefined(); - - sheaf.revokeAll(); - expect(sheaf.getExported()).toBeUndefined(); - }); }); diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 29f924dc07..905f826486 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -2,7 +2,7 @@ * Sheafify a presheaf into an authority manager. * * `sheafify({ name, sections })` returns a `Sheaf` — an immutable object - * that tracks granted authority and produces revocable dispatch sections. + * that produces dispatch sections over a fixed presheaf. * * Each dispatch through a granted section: * 1. Computes the stalk (getStalk — presheaf sections matching the point) @@ -28,7 +28,7 @@ import { collectSheafGuard } from './guard.ts'; import type { MethodGuardPayload } from './guard.ts'; import { evaluateMetadata, resolveMetaDataSpec } from './metadata.ts'; import type { ResolvedMetaDataSpec } from './metadata.ts'; -import { getStalk, guardCoversPoint } from './stalk.ts'; +import { getStalk } from './stalk.ts'; import type { EvaluatedSection, Lift, @@ -178,13 +178,6 @@ const invokeExo = (exo: Section, method: string, args: unknown[]): unknown => { return fn.call(obj, ...args); }; -type Grant = { - exo: Section; - guard: InterfaceGuard; - revoke: () => void; - isRevoked: () => boolean; -}; - type ResolvedSection> = { exo: Section; spec: ResolvedMetaDataSpec | undefined; @@ -210,8 +203,6 @@ export const sheafify = < : resolveMetaDataSpec(section.metadata, compartment), })), ); - const grants: Grant[] = []; - const buildSection = ({ guard, lift, @@ -231,16 +222,10 @@ export const sheafify = < defaultGuards: 'passable', }); - let revoked = false; - const dispatch = async ( method: string, args: unknown[], ): Promise => { - if (revoked) { - throw new Error(`Section revoked: ${name}`); - } - const stalk = getStalk(frozenSections, method, args); const evaluatedStalk: EvaluatedSection[] = stalk.map( (section) => ({ @@ -304,15 +289,6 @@ export const sheafify = < asyncGuard, )) as unknown as Section; - grants.push({ - exo, - guard: resolvedGuard, - revoke: () => { - revoked = true; - }, - isRevoked: () => revoked, - }); - return exo; }; @@ -351,39 +327,10 @@ export const sheafify = < schema: Record; }): object => buildSection({ guard: unionGuard(), lift, schema }); - const revokePoint = (method: string, ...args: unknown[]): void => { - for (const grant of grants) { - if (!grant.isRevoked() && guardCoversPoint(grant.guard, method, args)) { - grant.revoke(); - } - } - }; - - const getExported = (): InterfaceGuard | undefined => { - const activeExos = grants - .filter((grant) => !grant.isRevoked()) - .map((grant) => grant.exo); - if (activeExos.length === 0) { - return undefined; - } - return collectSheafGuard(`${name}:exported`, activeExos); - }; - - const revokeAll = (): void => { - for (const grant of grants) { - if (!grant.isRevoked()) { - grant.revoke(); - } - } - }; - return { getSection, getDiscoverableSection, getGlobalSection, getDiscoverableGlobalSection, - revokePoint, - getExported, - revokeAll, }; }; diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index 86e319f497..d63985fd25 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -121,10 +121,4 @@ export type Sheaf> = { lift: Lift; schema: Record; }) => object; - /** Revoke every granted section whose guard covers the point (method, ...args). */ - revokePoint: (method: string, ...args: unknown[]) => void; - /** Union guard of all active (non-revoked) granted sections, or undefined. */ - getExported: () => InterfaceGuard | undefined; - /** Revoke all granted sections. */ - revokeAll: () => void; }; From abd7675bc4afbaf5424d89082590a031580e90f2 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:19:15 -0400 Subject: [PATCH 21/68] docs(kernel-utils): fix sheaf doc errors from revocation removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove stale revokePoint/revokeAll/getExported references from README.md and USAGE.md (the revocation API was removed in the preceding commit but the docs were not updated) - Fix withRanking/withFilter signatures in USAGE.md: the combinators are curried — withRanking(comparator)(inner), not withRanking(comparator, inner) - Correct the fallthrough doc comment: liftB does receive accumulated errors from liftA's attempts via yield* after its own failures (the test at compose.test.ts:337 confirms this); liftB is only unaware of them at its prime call - Document the germ identity invariant in LIFT.md: the lift must yield elements from its input germs array, not reconstructed objects, because the sheaf resolves the dispatch target by object identity Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/LIFT.md | 5 ++++- packages/kernel-utils/src/sheaf/README.md | 11 +++-------- packages/kernel-utils/src/sheaf/USAGE.md | 19 +++---------------- packages/kernel-utils/src/sheaf/compose.ts | 5 +++-- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/LIFT.md b/packages/kernel-utils/src/sheaf/LIFT.md index 95445c69fe..1c44ba14d0 100644 --- a/packages/kernel-utils/src/sheaf/LIFT.md +++ b/packages/kernel-utils/src/sheaf/LIFT.md @@ -21,7 +21,10 @@ The sheaf drives it with the following protocol: 1. **Prime** — `gen.next([])` starts the coroutine. The empty array is discarded; it exists only to satisfy the generator type. -2. **Yield** — the coroutine yields a candidate germ to try next. +2. **Yield** — the coroutine yields a candidate germ to try next. The yielded + value must be an element of the `germs` array received on entry — the sheaf + uses object identity to map it back to the original section, so constructing + a new object with the same shape will produce "lift yielded an unknown germ". 3. **Attempt** — the sheaf calls the candidate's exo method. 4. **Success** — the result is returned; the generator is abandoned. 5. **Failure** — the sheaf calls `gen.next(errors)`, passing the ordered list diff --git a/packages/kernel-utils/src/sheaf/README.md b/packages/kernel-utils/src/sheaf/README.md index c988136ac4..cbc51d3c76 100644 --- a/packages/kernel-utils/src/sheaf/README.md +++ b/packages/kernel-utils/src/sheaf/README.md @@ -3,9 +3,8 @@ Runtime capability routing adapted from sheaf theory in algebraic topology. `sheafify({ name, sections })` produces a **sheaf** — an authority manager -over a presheaf of capabilities. The sheaf grants revocable dispatch sections -via `getSection`, tracks all delegated authority, and supports point-wise -revocation. +over a presheaf of capabilities. The sheaf grants dispatch sections via +`getSection` and tracks all delegated authority. See [USAGE.md](./USAGE.md) for annotated examples and [LIFT.md](./LIFT.md) for the lift coroutine protocol and semantic equivalence assumption. @@ -59,12 +58,8 @@ data (captured at construction time) and a registry of all granted sections. const sheaf = sheafify({ name: 'Wallet', sections }); ``` -- `sheaf.getSection({ guard, lift })` — produce a revocable dispatch exo +- `sheaf.getSection({ guard, lift })` — produce a dispatch exo - `sheaf.getDiscoverableSection({ guard, lift, schema })` — same, but the exo exposes its guard -- `sheaf.revokePoint(method, ...args)` — revoke every granted section whose - guard covers the point -- `sheaf.getExported()` — union guard of all active (non-revoked) sections -- `sheaf.revokeAll()` — revoke every granted section ## Dispatch pipeline diff --git a/packages/kernel-utils/src/sheaf/USAGE.md b/packages/kernel-utils/src/sheaf/USAGE.md index ac71282e6d..bf645db02d 100644 --- a/packages/kernel-utils/src/sheaf/USAGE.md +++ b/packages/kernel-utils/src/sheaf/USAGE.md @@ -31,7 +31,7 @@ const sheaf = sheafify({ }); const section = sheaf.getSection({ guard: priceGuard, lift: noop }); -// section is a revocable dispatch exo; call it like any capability +// section is a dispatch exo; call it like any capability const price = await E(section).getPrice('ETH'); ``` @@ -119,19 +119,6 @@ sections are assembled dynamically (e.g., rebuilt at runtime from a set of grants that changes) and the union guard isn't known until after `sheafify` runs, the global variants are the right choice. -## Revocation - -```ts -// revoke every granted section whose guard covers this invocation point -sheaf.revokePoint('getPrice', 'ETH'); - -// revoke all granted sections at once -sheaf.revokeAll(); - -// union guard of all currently active (non-revoked) sections -const exported = sheaf.getExported(); -``` - ## Remote sections `makeRemoteSection` wraps a CapTP remote reference as a `PresheafSection`, @@ -167,9 +154,9 @@ import { } from '@metamask/kernel-utils'; ``` -- **`withRanking(comparator, inner)`** — sort germs by comparator before +- **`withRanking(comparator)(inner)`** — sort germs by comparator before passing to `inner` -- **`withFilter(predicate, inner)`** — remove germs that fail `predicate` +- **`withFilter(predicate)(inner)`** — remove germs that fail `predicate` before passing to `inner` - **`fallthrough(liftA, liftB)`** — try all candidates from `liftA` first; if all fail, try `liftB` diff --git a/packages/kernel-utils/src/sheaf/compose.ts b/packages/kernel-utils/src/sheaf/compose.ts index 66e323bdb6..26dc375d5f 100644 --- a/packages/kernel-utils/src/sheaf/compose.ts +++ b/packages/kernel-utils/src/sheaf/compose.ts @@ -89,8 +89,9 @@ export const withRanking = * `.next(value)` to the inner iterator, so error arrays are correctly * threaded through each inner lift. * - * liftB starts fresh and only sees errors from its own failed attempts, - * not from liftA's attempts. + * liftB is not informed of liftA's failures at its prime call, but via + * `yield*` it receives all accumulated errors (including liftA's) as the + * argument to each subsequent `next(errors)` after its own failed attempts. * * @param liftA - First lift; its candidates are tried before liftB's. * @param liftB - Fallback lift; only invoked after liftA is exhausted. From afa531897bd09f0d1d60a9b4385330eefd0c16df Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:26:56 -0400 Subject: [PATCH 22/68] refactor(kernel-utils): extract buildMethodGuard to eliminate duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit asyncifyMethodGuards (sheafify.ts) and collectSheafGuard (guard.ts) both contained an identical 4-way if/else for assembling a MethodGuard from its components. The chain order required by @endo/patterns (callWhen → optional → rest → returns) makes each branch non-obvious — a bad candidate for duplication. Extract buildMethodGuard into guard.ts and use it in both sites. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/guard.ts | 55 ++++++++++++++------- packages/kernel-utils/src/sheaf/sheafify.ts | 25 +++------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/guard.ts b/packages/kernel-utils/src/sheaf/guard.ts index 36b29df97a..e5347f509a 100644 --- a/packages/kernel-utils/src/sheaf/guard.ts +++ b/packages/kernel-utils/src/sheaf/guard.ts @@ -16,6 +16,38 @@ export type MethodGuardPayload = { returnGuard: Pattern; }; +/** + * Assemble a MethodGuard from its components. + * + * The @endo/patterns builder API requires a strict chain order: + * callWhen → optional → rest → returns. All four combinations of + * optional/rest presence are handled here so callers don't repeat this logic. + * + * @param base - Result of M.callWhen(...requiredArgs). + * @param optionals - Optional positional arg guards (may be empty). + * @param restGuard - Rest arg guard, or undefined if none. + * @param returnGuard - Return value guard. + * @returns The assembled MethodGuard. + */ +export const buildMethodGuard = ( + base: ReturnType, + optionals: Pattern[], + restGuard: Pattern | undefined, + returnGuard: Pattern, +): MethodGuard => { + if (optionals.length > 0 && restGuard !== undefined) { + return base + .optional(...optionals) + .rest(restGuard) + .returns(returnGuard); + } else if (optionals.length > 0) { + return base.optional(...optionals).returns(returnGuard); + } else if (restGuard === undefined) { + return base.returns(returnGuard); + } + return base.rest(restGuard).returns(returnGuard); +}; + /** * Naive union of guards via M.or — no pattern canonicalization. * @@ -121,23 +153,12 @@ export const collectSheafGuard = ( payloads.map((payload) => payload.returnGuard), ); - const base = M.callWhen(...requiredArgGuards); - if (optionalArgGuards.length > 0 && unionRestArgGuard !== undefined) { - unionMethodGuards[methodName] = base - .optional(...optionalArgGuards) - .rest(unionRestArgGuard) - .returns(returnGuard); - } else if (optionalArgGuards.length > 0) { - unionMethodGuards[methodName] = base - .optional(...optionalArgGuards) - .returns(returnGuard); - } else if (unionRestArgGuard === undefined) { - unionMethodGuards[methodName] = base.returns(returnGuard); - } else { - unionMethodGuards[methodName] = base - .rest(unionRestArgGuard) - .returns(returnGuard); - } + unionMethodGuards[methodName] = buildMethodGuard( + M.callWhen(...requiredArgGuards), + optionalArgGuards, + unionRestArgGuard, + returnGuard, + ); } return M.interface(name, unionMethodGuards); diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 905f826486..ec5a7b5894 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -24,7 +24,7 @@ import { makeDiscoverableExo } from '../discoverable.ts'; import type { MethodSchema } from '../schema.ts'; import { stringify } from '../stringify.ts'; import { driveLift } from './drive.ts'; -import { collectSheafGuard } from './guard.ts'; +import { buildMethodGuard, collectSheafGuard } from './guard.ts'; import type { MethodGuardPayload } from './guard.ts'; import { evaluateMetadata, resolveMetaDataSpec } from './metadata.ts'; import type { ResolvedMetaDataSpec } from './metadata.ts'; @@ -140,23 +140,12 @@ const asyncifyMethodGuards = ( const { argGuards, optionalArgGuards, restArgGuard, returnGuard } = getMethodGuardPayload(methodGuard) as unknown as MethodGuardPayload; const optionals = optionalArgGuards ?? []; - const base = M.callWhen(...argGuards); - if (optionals.length > 0 && restArgGuard !== undefined) { - asyncMethodGuards[methodName] = base - .optional(...optionals) - .rest(restArgGuard) - .returns(returnGuard); - } else if (optionals.length > 0) { - asyncMethodGuards[methodName] = base - .optional(...optionals) - .returns(returnGuard); - } else if (restArgGuard === undefined) { - asyncMethodGuards[methodName] = base.returns(returnGuard); - } else { - asyncMethodGuards[methodName] = base - .rest(restArgGuard) - .returns(returnGuard); - } + asyncMethodGuards[methodName] = buildMethodGuard( + M.callWhen(...argGuards), + optionals, + restArgGuard, + returnGuard, + ); } return asyncMethodGuards; }; From 6159028bc7667037568cfb900b10f5980993e73b Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:27:24 -0400 Subject: [PATCH 23/68] fix(kernel-utils): harden sheafify return value and frozenSections sheafify returned a plain mutable object and used Object.freeze (shallow) for frozenSections. Replace both with harden() for deep transitive immutability under SES lockdown, consistent with the convention applied to constant/source/callable in metadata.ts. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/sheafify.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index ec5a7b5894..7d195f4a6c 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -183,7 +183,7 @@ export const sheafify = < sections: PresheafSection[]; compartment?: { evaluate: (src: string) => unknown }; }): Sheaf => { - const frozenSections: readonly ResolvedSection[] = Object.freeze( + const frozenSections: readonly ResolvedSection[] = harden( sections.map((section) => ({ exo: section.exo, spec: @@ -316,10 +316,10 @@ export const sheafify = < schema: Record; }): object => buildSection({ guard: unionGuard(), lift, schema }); - return { + return harden({ getSection, getDiscoverableSection, getGlobalSection, getDiscoverableGlobalSection, - }; + }); }; From e26f256d700f0b6b549026f6e68e327788b335e9 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:21:00 -0400 Subject: [PATCH 24/68] feat(kernel-utils): export noopLift as canonical single-section placeholder lift Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.test.ts | 1 + packages/kernel-utils/src/index.ts | 1 + packages/kernel-utils/src/sheaf/USAGE.md | 8 ++------ packages/kernel-utils/src/sheaf/compose.ts | 16 ++++++++++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index 5d7737a767..15430e43e8 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -39,6 +39,7 @@ describe('index', () => { 'makeRemoteSection', 'mergeDisjointRecords', 'methodArgsToStruct', + 'noopLift', 'prettifySmallcaps', 'proxyLift', 'retry', diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index 03ecc6a7c5..dee872dbb7 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -57,6 +57,7 @@ export type { export { constant, source, callable } from './sheaf/metadata.ts'; export { sheafify } from './sheaf/sheafify.ts'; export { + noopLift, proxyLift, withFilter, withRanking, diff --git a/packages/kernel-utils/src/sheaf/USAGE.md b/packages/kernel-utils/src/sheaf/USAGE.md index bf645db02d..f81f1be63a 100644 --- a/packages/kernel-utils/src/sheaf/USAGE.md +++ b/packages/kernel-utils/src/sheaf/USAGE.md @@ -9,11 +9,7 @@ as a placeholder: ```ts import { M } from '@endo/patterns'; import { makeDefaultExo } from ''; -import { sheafify } from '@metamask/kernel-utils'; - -const noop = async function* (germs) { - yield* germs; -}; +import { sheafify, noopLift } from '@metamask/kernel-utils'; const priceGuard = M.interface('PriceService', { getPrice: M.callWhen(M.await(M.string())).returns(M.await(M.number())), @@ -30,7 +26,7 @@ const sheaf = sheafify({ sections: [{ exo: priceExo }], }); -const section = sheaf.getSection({ guard: priceGuard, lift: noop }); +const section = sheaf.getSection({ guard: priceGuard, lift: noopLift }); // section is a dispatch exo; call it like any capability const price = await E(section).getPrice('ETH'); ``` diff --git a/packages/kernel-utils/src/sheaf/compose.ts b/packages/kernel-utils/src/sheaf/compose.ts index 26dc375d5f..754d8cbed6 100644 --- a/packages/kernel-utils/src/sheaf/compose.ts +++ b/packages/kernel-utils/src/sheaf/compose.ts @@ -1,5 +1,21 @@ import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; +/** + * A lift that yields all germs in their original order without filtering. + * + * Use as a placeholder when the sheaf always has a single-section stalk + * (the lift is never actually called) or to express "try everything in + * declaration order" as an explicit policy. + * + * @param germs - Evaluated sections to yield in order. + * @yields Each germ in the original array order. + */ +export async function* noopLift>( + germs: EvaluatedSection>[], +): AsyncGenerator>, void, unknown[]> { + yield* germs; +} + /** * Proxy a lift coroutine, forwarding yielded candidates up and received * error arrays down to the inner generator. From b8a9fe2a040316e379dc7a2b8956c1fafeb671c1 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:31:28 -0400 Subject: [PATCH 25/68] feat(kernel-utils): add makeSection to eliminate as-unknown-as-Section cast Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.test.ts | 1 + packages/kernel-utils/src/index.ts | 1 + packages/kernel-utils/src/sheaf/USAGE.md | 6 +- packages/kernel-utils/src/sheaf/guard.test.ts | 54 +++++----- .../kernel-utils/src/sheaf/remote.test.ts | 16 +-- packages/kernel-utils/src/sheaf/remote.ts | 7 +- packages/kernel-utils/src/sheaf/section.ts | 22 ++++ .../src/sheaf/sheafify.e2e.test.ts | 56 +++++----- .../sheaf/sheafify.string-metadata.test.ts | 12 +-- .../kernel-utils/src/sheaf/sheafify.test.ts | 100 +++++++++--------- packages/kernel-utils/src/sheaf/stalk.test.ts | 13 ++- 11 files changed, 157 insertions(+), 131 deletions(-) create mode 100644 packages/kernel-utils/src/sheaf/section.ts diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index 15430e43e8..a67abd3b64 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -37,6 +37,7 @@ describe('index', () => { 'makeDefaultInterface', 'makeDiscoverableExo', 'makeRemoteSection', + 'makeSection', 'mergeDisjointRecords', 'methodArgsToStruct', 'noopLift', diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index dee872dbb7..d3d9bb0f8b 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -65,4 +65,5 @@ export { } from './sheaf/compose.ts'; export { collectSheafGuard } from './sheaf/guard.ts'; export { makeRemoteSection } from './sheaf/remote.ts'; +export { makeSection } from './sheaf/section.ts'; export { getStalk, guardCoversPoint } from './sheaf/stalk.ts'; diff --git a/packages/kernel-utils/src/sheaf/USAGE.md b/packages/kernel-utils/src/sheaf/USAGE.md index f81f1be63a..afdff6e416 100644 --- a/packages/kernel-utils/src/sheaf/USAGE.md +++ b/packages/kernel-utils/src/sheaf/USAGE.md @@ -122,7 +122,11 @@ fetching the remote's guard once at construction and forwarding all calls via `E()`. This lets you mix local exos and remote capabilities in the same sheaf: ```ts -import { makeRemoteSection, constant } from '@metamask/kernel-utils'; +import { + makeSection, + makeRemoteSection, + constant, +} from '@metamask/kernel-utils'; const remoteSection = await makeRemoteSection( 'RemoteWallet', // name for the wrapper exo diff --git a/packages/kernel-utils/src/sheaf/guard.test.ts b/packages/kernel-utils/src/sheaf/guard.test.ts index ffac24fd86..b01054ff47 100644 --- a/packages/kernel-utils/src/sheaf/guard.test.ts +++ b/packages/kernel-utils/src/sheaf/guard.test.ts @@ -1,4 +1,3 @@ -import { makeExo } from '@endo/exo'; import { M, matches, @@ -9,36 +8,29 @@ import type { MethodGuard, Pattern } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; import { collectSheafGuard } from './guard.ts'; +import { makeSection } from './section.ts'; import { guardCoversPoint } from './stalk.ts'; -import type { Section } from './types.ts'; - -const makeSection = ( - tag: string, - guards: Record, - methods: Record unknown>, -): Section => { - const interfaceGuard = M.interface(tag, guards); - return makeExo(tag, interfaceGuard, methods) as unknown as Section; -}; describe('collectSheafGuard', () => { it('variable arity: add with 1, 2, and 3 args', () => { const sections = [ makeSection( 'Calc:0', - { add: M.call(M.number()).returns(M.number()) }, + M.interface('Calc:0', { add: M.call(M.number()).returns(M.number()) }), { add: (a: number) => a }, ), makeSection( 'Calc:1', - { add: M.call(M.number(), M.number()).returns(M.number()) }, + M.interface('Calc:1', { + add: M.call(M.number(), M.number()).returns(M.number()), + }), { add: (a: number, b: number) => a + b }, ), makeSection( 'Calc:2', - { + M.interface('Calc:2', { add: M.call(M.number(), M.number(), M.number()).returns(M.number()), - }, + }), { add: (a: number, b: number, cc: number) => a + b + cc }, ), ]; @@ -61,12 +53,12 @@ describe('collectSheafGuard', () => { const sections = [ makeSection( 'S:0', - { f: M.call(M.eq(0)).returns(M.eq(0)) }, + M.interface('S:0', { f: M.call(M.eq(0)).returns(M.eq(0)) }), { f: (_: number) => 0 }, ), makeSection( 'S:1', - { f: M.call(M.eq(1)).returns(M.eq(1)) }, + M.interface('S:1', { f: M.call(M.eq(1)).returns(M.eq(1)) }), { f: (_: number) => 1 }, ), ]; @@ -88,11 +80,11 @@ describe('collectSheafGuard', () => { const sections = [ makeSection( 'Greeter', - { + M.interface('Greeter', { greet: M.callWhen(M.string()) .optional(M.string()) .returns(M.string()), - }, + }), { greet: (name: string, _greeting?: string) => `hello ${name}` }, ), ]; @@ -114,7 +106,9 @@ describe('collectSheafGuard', () => { const sections = [ makeSection( 'Logger', - { log: M.call(M.string()).rest(M.string()).returns(M.any()) }, + M.interface('Logger', { + log: M.call(M.string()).rest(M.string()).returns(M.any()), + }), { log: (..._args: string[]) => undefined }, ), ]; @@ -138,12 +132,16 @@ describe('collectSheafGuard', () => { const sections = [ makeSection( 'A', - { log: M.call(M.string()).rest(M.string()).returns(M.any()) }, + M.interface('A', { + log: M.call(M.string()).rest(M.string()).returns(M.any()), + }), { log: (..._args: string[]) => undefined }, ), makeSection( 'B', - { log: M.call(M.string()).rest(M.number()).returns(M.any()) }, + M.interface('B', { + log: M.call(M.string()).rest(M.number()).returns(M.any()), + }), { log: (..._args: unknown[]) => undefined }, ), ]; @@ -167,12 +165,12 @@ describe('collectSheafGuard', () => { const sections = [ makeSection( 'AB:0', - { f: M.call(M.number()).returns(M.any()) }, + M.interface('AB:0', { f: M.call(M.number()).returns(M.any()) }), { f: (_: number) => undefined }, ), makeSection( 'AB:1', - { f: M.call().rest(M.string()).returns(M.any()) }, + M.interface('AB:1', { f: M.call().rest(M.string()).returns(M.any()) }), { f: (..._args: string[]) => undefined }, ), ]; @@ -188,19 +186,19 @@ describe('collectSheafGuard', () => { const sections = [ makeSection( 'Multi:0', - { + M.interface('Multi:0', { translate: M.call(M.string(), M.string()).returns(M.string()), - }, + }), { translate: (from: string, to: string) => `${from}->${to}`, }, ), makeSection( 'Multi:1', - { + M.interface('Multi:1', { translate: M.call(M.string(), M.string()).returns(M.string()), summarize: M.call(M.string()).returns(M.string()), - }, + }), { translate: (from: string, to: string) => `${from}->${to}`, summarize: (text: string) => `summary: ${text}`, diff --git a/packages/kernel-utils/src/sheaf/remote.test.ts b/packages/kernel-utils/src/sheaf/remote.test.ts index 55c731494d..e4439c8f15 100644 --- a/packages/kernel-utils/src/sheaf/remote.test.ts +++ b/packages/kernel-utils/src/sheaf/remote.test.ts @@ -1,10 +1,10 @@ -import { GET_INTERFACE_GUARD, makeExo } from '@endo/exo'; +import { GET_INTERFACE_GUARD } from '@endo/exo'; import { M } from '@endo/patterns'; import { describe, it, expect, vi } from 'vitest'; import { constant } from './metadata.ts'; import { makeRemoteSection } from './remote.ts'; -import type { Section } from './types.ts'; +import { makeSection } from './section.ts'; // Mirrors the local-E pattern used throughout sheaf tests: the test // environment has no HandledPromise, so we mock E as a transparent cast. @@ -15,7 +15,7 @@ vi.mock('@endo/eventual-send', () => ({ })); const makeRemoteExo = (tag: string) => - makeExo( + makeSection( tag, M.interface( tag, @@ -29,7 +29,7 @@ const makeRemoteExo = (tag: string) => greet: async (name: string) => `Hello, ${name}!`, add: async (a: number, b: number) => a + b, }, - ) as unknown as Section; + ); describe('makeRemoteSection', () => { it('fetches the interface guard from the remote ref', async () => { @@ -42,7 +42,7 @@ describe('makeRemoteSection', () => { it('forwards method calls to the remote ref', async () => { const greet = vi.fn(async (name: string) => `Hello, ${name}!`); - const remoteExo = makeExo( + const remoteExo = makeSection( 'Remote', M.interface( 'Remote', @@ -50,7 +50,7 @@ describe('makeRemoteSection', () => { { defaultGuards: 'passable' }, ), { greet }, - ) as unknown as Section; + ); const { exo } = await makeRemoteSection('Wrapper', remoteExo); const wrapper = exo as Record< @@ -66,7 +66,7 @@ describe('makeRemoteSection', () => { it('forwards all methods declared in the guard', async () => { const greet = vi.fn(async (_: string) => ''); const add = vi.fn(async (a: number, b: number) => a + b); - const remoteExo = makeExo( + const remoteExo = makeSection( 'Remote', M.interface( 'Remote', @@ -77,7 +77,7 @@ describe('makeRemoteSection', () => { { defaultGuards: 'passable' }, ), { greet, add }, - ) as unknown as Section; + ); const { exo } = await makeRemoteSection('Wrapper', remoteExo); const wrapper = exo as Record< diff --git a/packages/kernel-utils/src/sheaf/remote.ts b/packages/kernel-utils/src/sheaf/remote.ts index 592dc70526..7689a79b4c 100644 --- a/packages/kernel-utils/src/sheaf/remote.ts +++ b/packages/kernel-utils/src/sheaf/remote.ts @@ -1,10 +1,11 @@ import { E } from '@endo/eventual-send'; -import { GET_INTERFACE_GUARD, makeExo } from '@endo/exo'; +import { GET_INTERFACE_GUARD } from '@endo/exo'; import { getInterfaceGuardPayload } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; import { ifDefined } from '../misc.ts'; -import type { MetaDataSpec, PresheafSection, Section } from './types.ts'; +import { makeSection } from './section.ts'; +import type { MetaDataSpec, PresheafSection } from './types.ts'; /** * Wrap a remote (CapTP) reference as a PresheafSection. @@ -51,6 +52,6 @@ export const makeRemoteSection = async >( ]!(...args); } - const exo = makeExo(name, interfaceGuard, handlers) as unknown as Section; + const exo = makeSection(name, interfaceGuard, handlers); return ifDefined({ exo, metadata }) as PresheafSection; }; diff --git a/packages/kernel-utils/src/sheaf/section.ts b/packages/kernel-utils/src/sheaf/section.ts new file mode 100644 index 0000000000..9b9581b7ac --- /dev/null +++ b/packages/kernel-utils/src/sheaf/section.ts @@ -0,0 +1,22 @@ +import { makeExo } from '@endo/exo'; +import type { InterfaceGuard } from '@endo/patterns'; + +import type { Section } from './types.ts'; + +/** + * Create a local presheaf section from a name, guard, and handler map. + * + * Encapsulates the cast from makeExo's opaque return type to Section. + * Use this when constructing sections for a presheaf; do not use it for + * the dispatch exo produced by sheafify itself. + * + * @param name - Exo tag name. + * @param guard - Interface guard describing the section's methods. + * @param handlers - Method handler map. + * @returns A Section suitable for inclusion in a presheaf. + */ +export const makeSection = ( + name: string, + guard: InterfaceGuard, + handlers: Record unknown>, +): Section => makeExo(name, guard, handlers) as unknown as Section; diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts index 5e669acc19..6e035cee8f 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -1,10 +1,10 @@ -import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; import { describe, expect, it, vi } from 'vitest'; import { callable, constant } from './metadata.ts'; +import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; -import type { Lift, PresheafSection, Section } from './types.ts'; +import type { Lift, PresheafSection } from './types.ts'; // Thin cast for calling exo methods directly in tests without going through // HandledPromise (which is not available in the test environment). @@ -31,24 +31,24 @@ describe('e2e: cost-optimal routing', () => { const sections: PresheafSection<{ cost: number }>[] = [ { // Remote: covers all accounts, expensive - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: remote0GetBalance }, - ) as unknown as Section, + ), metadata: constant({ cost: 100 }), }, { // Local cache: covers only 'alice', cheap - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), { getBalance: local1GetBalance }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }, ]; @@ -72,13 +72,13 @@ describe('e2e: cost-optimal routing', () => { // Expand with a broader local cache (cost=2), re-sheafify. const local2GetBalance = vi.fn((_acct: string): number => 0); sections.push({ - exo: makeExo( + exo: makeSection( 'Wallet:2', M.interface('Wallet:2', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: local2GetBalance }, - ) as unknown as Section, + ), metadata: constant({ cost: 2 }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -154,13 +154,13 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 1: Network RPC ────────────────────────────────── // Covers ALL accounts (M.string()), but slow (500ms). sections.push({ - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: networkGetBalance }, - ) as unknown as Section, + ), metadata: constant({ latencyMs: 500, label: 'network' }), }); @@ -181,13 +181,13 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 2: Local state for owned account ──────────────── // Only covers 'alice' (M.eq), 1ms. sections.push({ - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), { getBalance: localGetBalance }, - ) as unknown as Section, + ), metadata: constant({ latencyMs: 1, label: 'local' }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -207,7 +207,7 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 3: In-memory cache for specific accounts ──────── // Covers bob and carol via M.or, instant (0ms). sections.push({ - exo: makeExo( + exo: makeSection( 'Wallet:2', M.interface('Wallet:2', { getBalance: M.call(M.or(M.eq('bob'), M.eq('carol'))).returns( @@ -215,7 +215,7 @@ describe('e2e: multi-tier capability routing', () => { ), }), { getBalance: cacheGetBalance }, - ) as unknown as Section, + ), metadata: constant({ latencyMs: 0, label: 'cache' }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -243,7 +243,7 @@ describe('e2e: multi-tier capability routing', () => { // read-only tiers above declared it, so writes route here // automatically — the guard algebra handles it, no config needed. sections.push({ - exo: makeExo( + exo: makeSection( 'Wallet:3', M.interface('Wallet:3', { getBalance: M.call(M.string()).returns(M.number()), @@ -255,7 +255,7 @@ describe('e2e: multi-tier capability routing', () => { getBalance: writeBackendGetBalance, transfer: writeBackendTransfer, }, - ) as unknown as Section, + ), metadata: constant({ latencyMs: 200, label: 'write-backend' }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -292,23 +292,23 @@ describe('e2e: multi-tier capability routing', () => { const makeSections = (): PresheafSection[] => [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: networkGetBalance }, - ) as unknown as Section, + ), metadata: constant({ latencyMs: 500, label: 'network' }), }, { - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: mirrorGetBalance }, - ) as unknown as Section, + ), metadata: constant({ latencyMs: 50, label: 'mirror' }), }, ]; @@ -356,24 +356,24 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { const sections: PresheafSection<{ push: boolean }>[] = [ { // Pull section: M.string() guards, push=false - exo: makeExo( + exo: makeSection( 'PushPull:0', M.interface('PushPull:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: pullGetBalance }, - ) as unknown as Section, + ), metadata: constant({ push: false }), }, { // Push section: narrow guard, push=true - exo: makeExo( + exo: makeSection( 'PushPull:1', M.interface('PushPull:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), { getBalance: pushGetBalance }, - ) as unknown as Section, + ), metadata: constant({ push: true }), }, ]; @@ -422,7 +422,7 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { const sections: PresheafSection[] = [ { - exo: makeExo( + exo: makeSection( 'SwapA', M.interface('SwapA', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -430,14 +430,14 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { ), }), { swap: swapAFn }, - ) as unknown as Section, + ), // cost(amount) = 1 + 0.1 * amount metadata: callable((args) => ({ cost: 1 + 0.1 * (args[0] as number), })), }, { - exo: makeExo( + exo: makeSection( 'SwapB', M.interface('SwapB', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -445,7 +445,7 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { ), }), { swap: swapBFn }, - ) as unknown as Section, + ), // cost(amount) = 10 + 0.001 * amount metadata: callable((args) => ({ cost: 10 + 0.001 * (args[0] as number), diff --git a/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts b/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts index e0037042f5..501adb3968 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts @@ -10,13 +10,13 @@ // The functional properties under test are identical regardless of which // Compartment implementation compiles the source string. -import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; import { describe, it, expect, vi } from 'vitest'; import { source } from './metadata.ts'; +import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; -import type { Lift, PresheafSection, Section } from './types.ts'; +import type { Lift, PresheafSection } from './types.ts'; // Thin cast for calling exo methods directly in tests without going through // HandledPromise (which is not available in the test environment). @@ -54,7 +54,7 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { const sections: PresheafSection[] = [ { - exo: makeExo( + exo: makeSection( 'SwapA', M.interface('SwapA', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -62,12 +62,12 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { ), }), { swap: swapAFn }, - ) as unknown as Section, + ), // cost(amount) = 1 + 0.1 * amount metadata: source(`(args) => ({ cost: 1 + 0.1 * args[0] })`), }, { - exo: makeExo( + exo: makeSection( 'SwapB', M.interface('SwapB', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -75,7 +75,7 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { ), }), { swap: swapBFn }, - ) as unknown as Section, + ), // cost(amount) = 10 + 0.001 * amount metadata: source(`(args) => ({ cost: 10 + 0.001 * args[0] })`), }, diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index a44e5502c9..020b83fa4c 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -1,16 +1,16 @@ -import { makeExo, GET_INTERFACE_GUARD } from '@endo/exo'; +import { GET_INTERFACE_GUARD } from '@endo/exo'; import { M, getInterfaceGuardPayload } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; import { GET_DESCRIPTION } from '../discoverable.ts'; import { constant } from './metadata.ts'; +import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; import type { EvaluatedSection, Lift, LiftContext, PresheafSection, - Section, } from './types.ts'; // Thin cast for calling exo methods directly in tests without going through @@ -34,13 +34,13 @@ describe('sheafify', () => { const sections: PresheafSection<{ cost: number }>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }, ]; @@ -55,13 +55,13 @@ describe('sheafify', () => { it('zero-coverage throws', async () => { const sections: PresheafSection<{ cost: number }>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }, ]; @@ -86,23 +86,23 @@ describe('sheafify', () => { const sections: PresheafSection<{ cost: number }>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ cost: 100 }), }, { - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }, ]; @@ -118,23 +118,23 @@ describe('sheafify', () => { it('GET_INTERFACE_GUARD returns collected guard', () => { const sections: PresheafSection<{ cost: number }>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.eq('alice')).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ cost: 100 }), }, { - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('bob')).returns(M.number()), }), { getBalance: (_acct: string) => 50 }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }, ]; @@ -161,13 +161,13 @@ describe('sheafify', () => { const sections: PresheafSection<{ cost: number }>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ cost: 100 }), }, ]; @@ -179,7 +179,7 @@ describe('sheafify', () => { // Add a cheaper section with a new method to the sections array, re-sheafify. sections.push({ - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -191,7 +191,7 @@ describe('sheafify', () => { getBalance: (_acct: string) => 42, transfer: (_from: string, _to: string, _amt: number) => true, }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -209,7 +209,7 @@ describe('sheafify', () => { }); it('pre-built exo dispatches correctly', async () => { - const exo = makeExo( + const exo = makeSection( 'bal', M.interface('bal', { getBalance: M.call(M.string()).returns(M.number()), @@ -217,7 +217,7 @@ describe('sheafify', () => { { getBalance: (_acct: string) => 42 }, ); const sections: PresheafSection<{ cost: number }>[] = [ - { exo: exo as unknown as Section, metadata: constant({ cost: 1 }) }, + { exo, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -238,13 +238,13 @@ describe('sheafify', () => { const sections: PresheafSection<{ cost: number }>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ cost: 100 }), }, ]; @@ -255,7 +255,7 @@ describe('sheafify', () => { expect(await E(wallet).getBalance('alice')).toBe(100); // Add a pre-built exo with a cheaper getBalance + new transfer method - const exo = makeExo( + const exo = makeSection( 'cheap', M.interface('cheap', { getBalance: M.call(M.string()).returns(M.number()), @@ -269,7 +269,7 @@ describe('sheafify', () => { }, ); sections.push({ - exo: exo as unknown as Section, + exo, metadata: constant({ cost: 1 }), }); wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -287,7 +287,7 @@ describe('sheafify', () => { }); it('guard reflected in GET_INTERFACE_GUARD for pre-built exo', () => { - const exo = makeExo( + const exo = makeSection( 'bal', M.interface('bal', { getBalance: M.call(M.string()).returns(M.number()), @@ -295,7 +295,7 @@ describe('sheafify', () => { { getBalance: (_acct: string) => 42 }, ); const sections: PresheafSection<{ cost: number }>[] = [ - { exo: exo as unknown as Section, metadata: constant({ cost: 1 }) }, + { exo, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -323,23 +323,23 @@ describe('sheafify', () => { const sections: PresheafSection[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ region: 'us', cost: 100 }), }, { - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), metadata: constant({ region: 'us', cost: 1 }), }, ]; @@ -373,23 +373,23 @@ describe('sheafify', () => { const sections: PresheafSection[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ region: 'us' }), }, { - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), metadata: constant({ region: 'us' }), }, ]; @@ -410,23 +410,23 @@ describe('sheafify', () => { const sections: PresheafSection[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }, { - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), metadata: constant({ cost: 1 }), }, ]; @@ -449,22 +449,22 @@ describe('sheafify', () => { const sections: PresheafSection[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), }, { - exo: makeExo( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), metadata: constant({}), }, ]; @@ -488,7 +488,7 @@ describe('sheafify', () => { ); }; - const exo = makeExo( + const exo = makeSection( 'cheap', M.interface('cheap', { getBalance: M.call(M.string()).returns(M.number()), @@ -497,16 +497,16 @@ describe('sheafify', () => { ); const sections: PresheafSection<{ cost: number }>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 100 }, - ) as unknown as Section, + ), metadata: constant({ cost: 100 }), }, - { exo: exo as unknown as Section, metadata: constant({ cost: 1 }) }, + { exo, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ @@ -526,13 +526,13 @@ describe('sheafify', () => { }; const sections: PresheafSection>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), }, ]; @@ -552,13 +552,13 @@ describe('sheafify', () => { it('getSection does not expose __getDescription__', () => { const sections: PresheafSection>[] = [ { - exo: makeExo( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, - ) as unknown as Section, + ), }, ]; diff --git a/packages/kernel-utils/src/sheaf/stalk.test.ts b/packages/kernel-utils/src/sheaf/stalk.test.ts index 534f576b59..3a7c9abb2f 100644 --- a/packages/kernel-utils/src/sheaf/stalk.test.ts +++ b/packages/kernel-utils/src/sheaf/stalk.test.ts @@ -1,22 +1,21 @@ -import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; import type { MethodGuard } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; import { constant } from './metadata.ts'; +import { makeSection } from './section.ts'; import { getStalk } from './stalk.ts'; -import type { PresheafSection, Section } from './types.ts'; +import type { PresheafSection } from './types.ts'; const makePresheafSection = ( tag: string, guards: Record, methods: Record unknown>, metadata: { cost: number }, -): PresheafSection<{ cost: number }> => { - const interfaceGuard = M.interface(tag, guards); - const exo = makeExo(tag, interfaceGuard, methods); - return { exo: exo as unknown as Section, metadata: constant(metadata) }; -}; +): PresheafSection<{ cost: number }> => ({ + exo: makeSection(tag, M.interface(tag, guards), methods), + metadata: constant(metadata), +}); describe('getStalk', () => { it('returns matching sections for a method and args', () => { From 88f5510e59cec5ea950927307aacd112fc81c592 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:35:35 -0400 Subject: [PATCH 26/68] refactor(kernel-utils): centralize guard payload helpers and move asyncifyMethodGuards to guard.ts Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/guard.test.ts | 62 +++++-------------- packages/kernel-utils/src/sheaf/guard.ts | 51 +++++++++++++++ packages/kernel-utils/src/sheaf/sheafify.ts | 41 +----------- packages/kernel-utils/src/sheaf/stalk.ts | 19 ++---- 4 files changed, 77 insertions(+), 96 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/guard.test.ts b/packages/kernel-utils/src/sheaf/guard.test.ts index b01054ff47..b2cbbea38c 100644 --- a/packages/kernel-utils/src/sheaf/guard.test.ts +++ b/packages/kernel-utils/src/sheaf/guard.test.ts @@ -1,13 +1,11 @@ -import { - M, - matches, - getInterfaceGuardPayload, - getMethodGuardPayload, -} from '@endo/patterns'; -import type { MethodGuard, Pattern } from '@endo/patterns'; +import { M, matches } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; -import { collectSheafGuard } from './guard.ts'; +import { + collectSheafGuard, + getInterfaceMethodGuards, + getMethodPayload, +} from './guard.ts'; import { makeSection } from './section.ts'; import { guardCoversPoint } from './stalk.ts'; @@ -36,13 +34,8 @@ describe('collectSheafGuard', () => { ]; const guard = collectSheafGuard('Calc', sections); - const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { - methodGuards: Record; - }; - const payload = getMethodGuardPayload(methodGuards.add) as unknown as { - argGuards: Pattern[]; - optionalArgGuards?: Pattern[]; - }; + const methodGuards = getInterfaceMethodGuards(guard); + const payload = getMethodPayload(methodGuards.add!); // 1 required arg (present in all), 2 optional (variable arity) expect(payload.argGuards).toHaveLength(1); @@ -64,12 +57,8 @@ describe('collectSheafGuard', () => { ]; const guard = collectSheafGuard('S', sections); - const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { - methodGuards: Record; - }; - const { returnGuard } = getMethodGuardPayload( - methodGuards.f, - ) as unknown as { returnGuard: Pattern }; + const methodGuards = getInterfaceMethodGuards(guard); + const { returnGuard } = getMethodPayload(methodGuards.f!); // Return guard is union of eq(0) and eq(1) expect(matches(0, returnGuard)).toBe(true); @@ -90,13 +79,8 @@ describe('collectSheafGuard', () => { ]; const guard = collectSheafGuard('Greeter', sections); - const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { - methodGuards: Record; - }; - const payload = getMethodGuardPayload(methodGuards.greet) as unknown as { - argGuards: Pattern[]; - optionalArgGuards?: Pattern[]; - }; + const methodGuards = getInterfaceMethodGuards(guard); + const payload = getMethodPayload(methodGuards.greet!); expect(payload.argGuards).toHaveLength(1); expect(payload.optionalArgGuards).toHaveLength(1); @@ -114,14 +98,8 @@ describe('collectSheafGuard', () => { ]; const guard = collectSheafGuard('Logger', sections); - const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { - methodGuards: Record; - }; - const payload = getMethodGuardPayload(methodGuards.log) as unknown as { - argGuards: Pattern[]; - optionalArgGuards?: Pattern[]; - restArgGuard?: Pattern; - }; + const methodGuards = getInterfaceMethodGuards(guard); + const payload = getMethodPayload(methodGuards.log!); expect(payload.argGuards).toHaveLength(1); expect(payload.optionalArgGuards ?? []).toHaveLength(0); @@ -147,12 +125,8 @@ describe('collectSheafGuard', () => { ]; const guard = collectSheafGuard('AB', sections); - const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { - methodGuards: Record; - }; - const { restArgGuard } = getMethodGuardPayload( - methodGuards.log, - ) as unknown as { restArgGuard?: Pattern }; + const methodGuards = getInterfaceMethodGuards(guard); + const { restArgGuard } = getMethodPayload(methodGuards.log!); expect(matches('hello', restArgGuard)).toBe(true); expect(matches(42, restArgGuard)).toBe(true); @@ -207,9 +181,7 @@ describe('collectSheafGuard', () => { ]; const guard = collectSheafGuard('Multi', sections); - const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { - methodGuards: Record; - }; + const methodGuards = getInterfaceMethodGuards(guard); expect('translate' in methodGuards).toBe(true); expect('summarize' in methodGuards).toBe(true); }); diff --git a/packages/kernel-utils/src/sheaf/guard.ts b/packages/kernel-utils/src/sheaf/guard.ts index e5347f509a..cabe70dd94 100644 --- a/packages/kernel-utils/src/sheaf/guard.ts +++ b/packages/kernel-utils/src/sheaf/guard.ts @@ -16,6 +16,30 @@ export type MethodGuardPayload = { returnGuard: Pattern; }; +/** + * Extract the typed method guard map from an interface guard. + * + * @param guard - The interface guard to inspect. + * @returns A record mapping method names to their guards. + */ +export const getInterfaceMethodGuards = ( + guard: InterfaceGuard, +): Record => + ( + getInterfaceGuardPayload(guard) as unknown as { + methodGuards: Record; + } + ).methodGuards; + +/** + * Extract the typed payload from a method guard. + * + * @param guard - The method guard to inspect. + * @returns The guard's argument and return guard components. + */ +export const getMethodPayload = (guard: MethodGuard): MethodGuardPayload => + getMethodGuardPayload(guard) as unknown as MethodGuardPayload; + /** * Assemble a MethodGuard from its components. * @@ -163,3 +187,30 @@ export const collectSheafGuard = ( return M.interface(name, unionMethodGuards); }; + +/** + * Upgrade all method guards in an interface guard to M.callWhen for async dispatch. + * + * @param resolvedGuard - The interface guard whose methods should be upgraded. + * @returns A record of async method guards keyed by method name. + */ +export const asyncifyMethodGuards = ( + resolvedGuard: InterfaceGuard, +): Record => { + const resolvedMethodGuards = getInterfaceMethodGuards(resolvedGuard); + const asyncMethodGuards: Record = {}; + for (const [methodName, methodGuard] of Object.entries( + resolvedMethodGuards, + )) { + const { argGuards, optionalArgGuards, restArgGuard, returnGuard } = + getMethodPayload(methodGuard); + const optionals = optionalArgGuards ?? []; + asyncMethodGuards[methodName] = buildMethodGuard( + M.callWhen(...argGuards), + optionals, + restArgGuard, + returnGuard, + ); + } + return asyncMethodGuards; +}; diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 7d195f4a6c..348386e72e 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -13,19 +13,14 @@ */ import { makeExo } from '@endo/exo'; -import { - M, - getInterfaceGuardPayload, - getMethodGuardPayload, -} from '@endo/patterns'; -import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; +import { M } from '@endo/patterns'; +import type { InterfaceGuard } from '@endo/patterns'; import { makeDiscoverableExo } from '../discoverable.ts'; import type { MethodSchema } from '../schema.ts'; import { stringify } from '../stringify.ts'; import { driveLift } from './drive.ts'; -import { buildMethodGuard, collectSheafGuard } from './guard.ts'; -import type { MethodGuardPayload } from './guard.ts'; +import { asyncifyMethodGuards, collectSheafGuard } from './guard.ts'; import { evaluateMetadata, resolveMetaDataSpec } from './metadata.ts'; import type { ResolvedMetaDataSpec } from './metadata.ts'; import { getStalk } from './stalk.ts'; @@ -120,36 +115,6 @@ const decomposeMetadata = >( return { constraints: constraints as Partial, stripped }; }; -/** - * Upgrade all method guards to M.callWhen for async dispatch. - * - * @param resolvedGuard - The interface guard to upgrade. - * @returns A record of async method guards. - */ -const asyncifyMethodGuards = ( - resolvedGuard: InterfaceGuard, -): Record => { - const { methodGuards: resolvedMethodGuards } = getInterfaceGuardPayload( - resolvedGuard, - ) as unknown as { methodGuards: Record }; - - const asyncMethodGuards: Record = {}; - for (const [methodName, methodGuard] of Object.entries( - resolvedMethodGuards, - )) { - const { argGuards, optionalArgGuards, restArgGuard, returnGuard } = - getMethodGuardPayload(methodGuard) as unknown as MethodGuardPayload; - const optionals = optionalArgGuards ?? []; - asyncMethodGuards[methodName] = buildMethodGuard( - M.callWhen(...argGuards), - optionals, - restArgGuard, - returnGuard, - ); - } - return asyncMethodGuards; -}; - /** * Invoke a method on a section exo, throwing if the handler is missing. * diff --git a/packages/kernel-utils/src/sheaf/stalk.ts b/packages/kernel-utils/src/sheaf/stalk.ts index f7988ba15d..01d50c2208 100644 --- a/packages/kernel-utils/src/sheaf/stalk.ts +++ b/packages/kernel-utils/src/sheaf/stalk.ts @@ -3,14 +3,10 @@ */ import { GET_INTERFACE_GUARD } from '@endo/exo'; -import { - matches, - getInterfaceGuardPayload, - getMethodGuardPayload, -} from '@endo/patterns'; -import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; +import { matches } from '@endo/patterns'; +import type { InterfaceGuard } from '@endo/patterns'; -import type { MethodGuardPayload } from './guard.ts'; +import { getInterfaceMethodGuards, getMethodPayload } from './guard.ts'; import type { Section } from './types.ts'; /** @@ -26,9 +22,7 @@ export const guardCoversPoint = ( method: string, args: unknown[], ): boolean => { - const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as { - methodGuards: Record; - }; + const methodGuards = getInterfaceMethodGuards(guard); if (!(method in methodGuards)) { return false; } @@ -36,9 +30,8 @@ export const guardCoversPoint = ( if (!methodGuard) { return false; } - const { argGuards, optionalArgGuards, restArgGuard } = getMethodGuardPayload( - methodGuard, - ) as unknown as MethodGuardPayload; + const { argGuards, optionalArgGuards, restArgGuard } = + getMethodPayload(methodGuard); const optionals = optionalArgGuards ?? []; const maxFixedArgs = argGuards.length + optionals.length; return ( From aa76581b783710f454bfe7391a293c085cfdbf72 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:36:00 -0400 Subject: [PATCH 27/68] fix(kernel-utils): improve error message for lift object-identity protocol violation Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/LIFT.md | 6 ++++-- packages/kernel-utils/src/sheaf/sheafify.ts | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/LIFT.md b/packages/kernel-utils/src/sheaf/LIFT.md index 1c44ba14d0..4359a58ab0 100644 --- a/packages/kernel-utils/src/sheaf/LIFT.md +++ b/packages/kernel-utils/src/sheaf/LIFT.md @@ -23,8 +23,10 @@ The sheaf drives it with the following protocol: discarded; it exists only to satisfy the generator type. 2. **Yield** — the coroutine yields a candidate germ to try next. The yielded value must be an element of the `germs` array received on entry — the sheaf - uses object identity to map it back to the original section, so constructing - a new object with the same shape will produce "lift yielded an unknown germ". + uses object identity to map it back to the original section. Constructing a + new object with the same shape will throw with a message like "Lift yielded + an unrecognized germ". Sorting with `[...germs].sort(...)` is safe because + sort preserves references; mapping to new objects is not. 3. **Attempt** — the sheaf calls the candidate's exo method. 4. **Success** — the result is returned; the generator is abandoned. 5. **Failure** — the sheaf calls `gen.next(errors)`, passing the ordered list diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 348386e72e..e49ace8fa2 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -219,7 +219,12 @@ export const sheafify = < async (germ) => { const section = strippedToCollapsed.get(germ); if (section === undefined) { - throw new Error('lift yielded an unknown germ'); + throw new Error( + `Lift yielded an unrecognized germ for '${method}'. ` + + `The yielded value must be one of the EvaluatedSection objects ` + + `passed into the lift (object identity, not structural equality). ` + + `Did the lift construct a new object instead of yielding from the germs array?`, + ); } return invokeExo(section.exo, method, args); }, From 5fdd40f9291d9ed93c142b64ee6e2c82fe18aef0 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:36:58 -0400 Subject: [PATCH 28/68] docs(kernel-utils): note that source metadata is for trust-boundary use only Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/USAGE.md | 3 ++- packages/kernel-utils/src/sheaf/metadata.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kernel-utils/src/sheaf/USAGE.md b/packages/kernel-utils/src/sheaf/USAGE.md index afdff6e416..41d19863d4 100644 --- a/packages/kernel-utils/src/sheaf/USAGE.md +++ b/packages/kernel-utils/src/sheaf/USAGE.md @@ -76,7 +76,8 @@ import { constant, source, callable } from '@metamask/kernel-utils'; // static value known at construction time constant({ mode: 'fast' }); -// JS source string compiled once in the sheaf's compartment at construction time +// @experimental — prefer callable unless the function must cross a trust boundary +// or be serialized. Compiled once in the sheaf's compartment at construction time. source(`(args) => ({ cost: args[0] > 9000 ? 'high' : 'low' })`); // live function evaluated at each dispatch — useful when cost varies by argument, diff --git a/packages/kernel-utils/src/sheaf/metadata.ts b/packages/kernel-utils/src/sheaf/metadata.ts index 6ee84ceee8..e69e5c1052 100644 --- a/packages/kernel-utils/src/sheaf/metadata.ts +++ b/packages/kernel-utils/src/sheaf/metadata.ts @@ -60,6 +60,10 @@ export const constant = >( /** * Wrap JS function source. Evaluated in a Compartment at sheafify construction time. * + * Prefer `callable` unless the metadata function must be supplied as a + * serializable source string — for example, when crossing a trust boundary or + * deserializing from storage. Requires a `compartment` passed to `sheafify`. + * * @param src - JS source string of the form `(args) => M`. * @returns A source MetaDataSpec wrapping the source string. */ From 5a8d7e5f6a4597fee1f51774af44e8d4779aeffb Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:37:14 -0400 Subject: [PATCH 29/68] refactor(kernel-utils): remove getStalk and guardCoversPoint from public exports Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.test.ts | 2 -- packages/kernel-utils/src/index.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index a67abd3b64..891074cfff 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -20,8 +20,6 @@ describe('index', () => { 'fallthrough', 'fetchValidatedJson', 'fromHex', - 'getStalk', - 'guardCoversPoint', 'ifDefined', 'installWakeDetector', 'isCapData', diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index d3d9bb0f8b..d2dd149183 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -66,4 +66,3 @@ export { export { collectSheafGuard } from './sheaf/guard.ts'; export { makeRemoteSection } from './sheaf/remote.ts'; export { makeSection } from './sheaf/section.ts'; -export { getStalk, guardCoversPoint } from './sheaf/stalk.ts'; From 8e99886f29fbabd0c8bbe8e5d9a89ac3c222aaf5 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:37:57 -0400 Subject: [PATCH 30/68] refactor(kernel-utils): inline driveLift into sheafify and remove drive.ts Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/drive.ts | 40 --------------------- packages/kernel-utils/src/sheaf/sheafify.ts | 26 +++++++++++++- 2 files changed, 25 insertions(+), 41 deletions(-) delete mode 100644 packages/kernel-utils/src/sheaf/drive.ts diff --git a/packages/kernel-utils/src/sheaf/drive.ts b/packages/kernel-utils/src/sheaf/drive.ts deleted file mode 100644 index 2bba341509..0000000000 --- a/packages/kernel-utils/src/sheaf/drive.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; - -/** - * Drive a lift coroutine, retrying on failure and accumulating errors. - * - * Primes the generator with gen.next([]), then calls gen.next(errors) after - * each failed attempt where errors is the full ordered history. Returns the - * first successful result, or throws a new error with all accumulated errors - * as the cause when exhausted. - * - * @param lift - The lift coroutine to drive. - * @param germs - The evaluated sections to pass to the lift. - * @param context - The dispatch context (method, args, constraints). - * @param invoke - Calls the section exo; throws on failure. - * @returns The result of the first successful invocation. - * @internal - */ -export const driveLift = async >( - lift: Lift, - germs: EvaluatedSection>[], - context: LiftContext, - invoke: (germ: EvaluatedSection>) => Promise, -): Promise => { - const errors: unknown[] = []; - const gen = lift(germs, context); - let next = await gen.next(errors); - while (!next.done) { - try { - const result = await invoke(next.value); - await gen.return(undefined); - return result; - } catch (error) { - errors.push(error); - next = await gen.next(errors); - } - } - throw new Error(`No viable section for ${context.method}`, { - cause: errors, - }); -}; diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index e49ace8fa2..d52733f286 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -19,7 +19,6 @@ import type { InterfaceGuard } from '@endo/patterns'; import { makeDiscoverableExo } from '../discoverable.ts'; import type { MethodSchema } from '../schema.ts'; import { stringify } from '../stringify.ts'; -import { driveLift } from './drive.ts'; import { asyncifyMethodGuards, collectSheafGuard } from './guard.ts'; import { evaluateMetadata, resolveMetaDataSpec } from './metadata.ts'; import type { ResolvedMetaDataSpec } from './metadata.ts'; @@ -27,6 +26,7 @@ import { getStalk } from './stalk.ts'; import type { EvaluatedSection, Lift, + LiftContext, PresheafSection, Section, Sheaf, @@ -137,6 +137,30 @@ type ResolvedSection> = { spec: ResolvedMetaDataSpec | undefined; }; +const driveLift = async >( + lift: Lift, + germs: EvaluatedSection>[], + context: LiftContext, + invoke: (germ: EvaluatedSection>) => Promise, +): Promise => { + const errors: unknown[] = []; + const gen = lift(germs, context); + let next = await gen.next(errors); + while (!next.done) { + try { + const result = await invoke(next.value); + await gen.return(undefined); + return result; + } catch (error) { + errors.push(error); + next = await gen.next(errors); + } + } + throw new Error(`No viable section for ${context.method}`, { + cause: errors, + }); +}; + export const sheafify = < MetaData extends Record = Record, >({ From 6c7055277c36a5cda2c9e15af13e1b5962a30cfb Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:48:20 -0400 Subject: [PATCH 31/68] =?UTF-8?q?refactor(kernel-utils):=20rename=20MetaDa?= =?UTF-8?q?taSpec=20=E2=86=92=20MetadataSpec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Metadata" is one compound word; the mid-word capital was inconsistent with the surrounding identifiers and prose docs. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.ts | 2 +- .../kernel-utils/src/sheaf/metadata.test.ts | 26 ++++++++--------- packages/kernel-utils/src/sheaf/metadata.ts | 28 +++++++++---------- packages/kernel-utils/src/sheaf/remote.ts | 4 +-- packages/kernel-utils/src/sheaf/sheafify.ts | 8 +++--- packages/kernel-utils/src/sheaf/types.ts | 4 +-- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index d2dd149183..040325ecc4 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -48,7 +48,7 @@ export type { Section, PresheafSection, EvaluatedSection, - MetaDataSpec, + MetadataSpec, Lift, LiftContext, Presheaf, diff --git a/packages/kernel-utils/src/sheaf/metadata.test.ts b/packages/kernel-utils/src/sheaf/metadata.test.ts index 8b77f80ff6..8257f03cad 100644 --- a/packages/kernel-utils/src/sheaf/metadata.test.ts +++ b/packages/kernel-utils/src/sheaf/metadata.test.ts @@ -4,7 +4,7 @@ import { callable, constant, evaluateMetadata, - resolveMetaDataSpec, + resolveMetadataSpec, source, } from './metadata.ts'; @@ -17,7 +17,7 @@ describe('constant', () => { }); it('evaluateMetadata returns the value regardless of args', () => { - const spec = resolveMetaDataSpec(constant({ cost: 7 })); + const spec = resolveMetadataSpec(constant({ cost: 7 })); expect(evaluateMetadata(spec, [])).toStrictEqual({ cost: 7 }); expect(evaluateMetadata(spec, [1, 2, 3])).toStrictEqual({ cost: 7 }); }); @@ -34,7 +34,7 @@ describe('callable', () => { const fn = vi.fn((args: unknown[]) => ({ value: (args[0] as number) * 2, })); - const spec = resolveMetaDataSpec(callable(fn)); + const spec = resolveMetadataSpec(callable(fn)); expect(evaluateMetadata(spec, [5])).toStrictEqual({ value: 10 }); expect(fn).toHaveBeenCalledWith([5]); }); @@ -48,10 +48,10 @@ describe('source', () => { }); }); - it('resolveMetaDataSpec compiles source to callable via compartment', () => { + it('resolveMetadataSpec compiles source to callable via compartment', () => { const mockFn = (args: unknown[]) => ({ value: args[0] as number }); const compartment = { evaluate: vi.fn(() => mockFn) }; - const spec = resolveMetaDataSpec( + const spec = resolveMetadataSpec( source<{ value: number }>('(args) => ({ value: args[0] })'), compartment, ); @@ -63,20 +63,20 @@ describe('source', () => { }); }); -describe('resolveMetaDataSpec', () => { +describe('resolveMetadataSpec', () => { it('passes constant spec through unchanged', () => { const spec = constant({ answer: 42 }); - expect(resolveMetaDataSpec(spec)).toStrictEqual(spec); + expect(resolveMetadataSpec(spec)).toStrictEqual(spec); }); it('passes callable spec through unchanged', () => { const fn = (_args: unknown[]) => ({ count: 0 }); const spec = callable(fn); - expect(resolveMetaDataSpec(spec)).toStrictEqual(spec); + expect(resolveMetadataSpec(spec)).toStrictEqual(spec); }); it("throws if kind is 'source' and no compartment supplied", () => { - expect(() => resolveMetaDataSpec(source('() => ({})'))).toThrow( + expect(() => resolveMetadataSpec(source('() => ({})'))).toThrow( "compartment required to evaluate 'source' metadata", ); }); @@ -89,7 +89,7 @@ describe('evaluateMetadata', () => { }); it('normalizes null from callable to empty object', () => { - const spec = resolveMetaDataSpec( + const spec = resolveMetadataSpec( callable( ((_args: unknown[]) => null) as unknown as ( args: unknown[], @@ -100,7 +100,7 @@ describe('evaluateMetadata', () => { }); it('throws when callable returns a primitive', () => { - const spec = resolveMetaDataSpec( + const spec = resolveMetadataSpec( callable( ((_args: unknown[]) => 7) as unknown as ( args: unknown[], @@ -112,7 +112,7 @@ describe('evaluateMetadata', () => { }); it('throws when callable returns an array', () => { - const spec = resolveMetaDataSpec( + const spec = resolveMetadataSpec( callable(((_args: unknown[]) => [1, 2]) as unknown as ( args: unknown[], ) => Record), @@ -121,7 +121,7 @@ describe('evaluateMetadata', () => { }); it('throws when callable returns a Date', () => { - const spec = resolveMetaDataSpec( + const spec = resolveMetadataSpec( callable( ((_args: unknown[]) => new Date()) as unknown as ( args: unknown[], diff --git a/packages/kernel-utils/src/sheaf/metadata.ts b/packages/kernel-utils/src/sheaf/metadata.ts index e69e5c1052..3a7aa03f79 100644 --- a/packages/kernel-utils/src/sheaf/metadata.ts +++ b/packages/kernel-utils/src/sheaf/metadata.ts @@ -1,11 +1,11 @@ /** - * MetaDataSpec constructors and evaluation helpers. + * MetadataSpec constructors and evaluation helpers. */ -import type { MetaDataSpec } from './types.ts'; +import type { MetadataSpec } from './types.ts'; /** Resolved spec: 'source' has been compiled away; only constant or callable remain. */ -export type ResolvedMetaDataSpec> = +export type ResolvedMetadataSpec> = | { kind: 'constant'; value: M } | { kind: 'callable'; fn: (args: unknown[]) => M }; @@ -51,11 +51,11 @@ const normalizeEvaluatedSheafMetadata = ( * Wrap a static value as a constant metadata spec. * * @param value - The static metadata value. - * @returns A constant MetaDataSpec wrapping the value. + * @returns A constant MetadataSpec wrapping the value. */ export const constant = >( value: M, -): MetaDataSpec => harden({ kind: 'constant', value }); +): MetadataSpec => harden({ kind: 'constant', value }); /** * Wrap JS function source. Evaluated in a Compartment at sheafify construction time. @@ -65,11 +65,11 @@ export const constant = >( * deserializing from storage. Requires a `compartment` passed to `sheafify`. * * @param src - JS source string of the form `(args) => M`. - * @returns A source MetaDataSpec wrapping the source string. + * @returns A source MetadataSpec wrapping the source string. */ export const source = >( src: string, -): MetaDataSpec => harden({ kind: 'source', src }); +): MetadataSpec => harden({ kind: 'source', src }); /** * Wrap a live function as a callable metadata spec. @@ -79,21 +79,21 @@ export const source = >( */ export const callable = >( fn: (args: unknown[]) => M, -): MetaDataSpec => harden({ kind: 'callable', fn }); +): MetadataSpec => harden({ kind: 'callable', fn }); /** * Compile a 'source' spec to 'callable' using the supplied compartment. * 'constant' and 'callable' pass through unchanged. * - * @param spec - The MetaDataSpec to resolve. + * @param spec - The MetadataSpec to resolve. * @param compartment - Compartment used to evaluate 'source' specs. Required when spec is 'source'. * @param compartment.evaluate - Evaluate a JS source string and return the result. - * @returns A ResolvedMetaDataSpec with no 'source' variant. + * @returns A ResolvedMetadataSpec with no 'source' variant. */ -export const resolveMetaDataSpec = >( - spec: MetaDataSpec, +export const resolveMetadataSpec = >( + spec: MetadataSpec, compartment?: { evaluate: (src: string) => unknown }, -): ResolvedMetaDataSpec => { +): ResolvedMetadataSpec => { if (spec.kind === 'source') { if (!compartment) { throw new Error( @@ -120,7 +120,7 @@ export const resolveMetaDataSpec = >( * @returns The evaluated metadata object (possibly empty). */ export const evaluateMetadata = >( - spec: ResolvedMetaDataSpec | undefined, + spec: ResolvedMetadataSpec | undefined, args: unknown[], ): MetaData => { if (spec === undefined) { diff --git a/packages/kernel-utils/src/sheaf/remote.ts b/packages/kernel-utils/src/sheaf/remote.ts index 7689a79b4c..1a3f388f3b 100644 --- a/packages/kernel-utils/src/sheaf/remote.ts +++ b/packages/kernel-utils/src/sheaf/remote.ts @@ -5,7 +5,7 @@ import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; import { ifDefined } from '../misc.ts'; import { makeSection } from './section.ts'; -import type { MetaDataSpec, PresheafSection } from './types.ts'; +import type { MetadataSpec, PresheafSection } from './types.ts'; /** * Wrap a remote (CapTP) reference as a PresheafSection. @@ -24,7 +24,7 @@ import type { MetaDataSpec, PresheafSection } from './types.ts'; export const makeRemoteSection = async >( name: string, remoteRef: object, - metadata?: MetaDataSpec, + metadata?: MetadataSpec, ): Promise> => { const interfaceGuard: InterfaceGuard = await ( E(remoteRef) as unknown as { diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index d52733f286..8da2cc4993 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -20,8 +20,8 @@ import { makeDiscoverableExo } from '../discoverable.ts'; import type { MethodSchema } from '../schema.ts'; import { stringify } from '../stringify.ts'; import { asyncifyMethodGuards, collectSheafGuard } from './guard.ts'; -import { evaluateMetadata, resolveMetaDataSpec } from './metadata.ts'; -import type { ResolvedMetaDataSpec } from './metadata.ts'; +import { evaluateMetadata, resolveMetadataSpec } from './metadata.ts'; +import type { ResolvedMetadataSpec } from './metadata.ts'; import { getStalk } from './stalk.ts'; import type { EvaluatedSection, @@ -134,7 +134,7 @@ const invokeExo = (exo: Section, method: string, args: unknown[]): unknown => { type ResolvedSection> = { exo: Section; - spec: ResolvedMetaDataSpec | undefined; + spec: ResolvedMetadataSpec | undefined; }; const driveLift = async >( @@ -178,7 +178,7 @@ export const sheafify = < spec: section.metadata === undefined ? undefined - : resolveMetaDataSpec(section.metadata, compartment), + : resolveMetadataSpec(section.metadata, compartment), })), ); const buildSection = ({ diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index d63985fd25..ff6da8c03c 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -23,7 +23,7 @@ export type Section = Partial & { * Evaluated metadata must be a plain object (`{}` means no metadata; primitives * must be wrapped, e.g. `{ value: n }`). */ -export type MetaDataSpec> = +export type MetadataSpec> = | { kind: 'constant'; value: M } | { kind: 'source'; src: string } | { kind: 'callable'; fn: (args: unknown[]) => M }; @@ -36,7 +36,7 @@ export type MetaDataSpec> = */ export type PresheafSection> = { exo: Section; - metadata?: MetaDataSpec; + metadata?: MetadataSpec; }; /** From e4ae6a01c60472a050117378e896485238357caa Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:48:37 -0400 Subject: [PATCH 32/68] refactor(kernel-utils): remove Presheaf thin alias The alias added a second public name for PresheafSection[] with no external consumers. Callers write the array type directly. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.ts | 1 - packages/kernel-utils/src/sheaf/types.ts | 6 ------ 2 files changed, 7 deletions(-) diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index 040325ecc4..2ba078b777 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -51,7 +51,6 @@ export type { MetadataSpec, Lift, LiftContext, - Presheaf, Sheaf, } from './sheaf/types.ts'; export { constant, source, callable } from './sheaf/metadata.ts'; diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index ff6da8c03c..083c3391a5 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -85,12 +85,6 @@ export type Lift> = ( context: LiftContext, ) => AsyncGenerator>, void, unknown[]>; -/** - * A presheaf: a plain array of presheaf sections. - */ -export type Presheaf> = - PresheafSection[]; - /** * A sheaf: an authority manager over a presheaf. * From f2737d577f6c2b131b924279db8b4d3439157840 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:48:57 -0400 Subject: [PATCH 33/68] docs(kernel-utils): document why Sheaf.getSection returns object The guard is passed dynamically at call time so TypeScript cannot propagate the method signatures through Sheaf. The comment prevents future contributors from chasing a phantom improvement. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/types.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index 083c3391a5..87231b4fee 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -92,9 +92,20 @@ export type Lift> = ( * granted authority for auditing and revocation. */ export type Sheaf> = { - /** Produce a revocable dispatch exo over the given guard. */ + /** + * Produce a revocable dispatch exo over the given guard. + * + * Returns `object` rather than a typed exo because the guard is passed + * dynamically at call time — TypeScript cannot propagate the method + * signatures through `Sheaf` without knowing the specific guard. + * Cast to the interface type at the call site once you know the guard. + */ getSection: (opts: { guard: InterfaceGuard; lift: Lift }) => object; - /** Produce a revocable discoverable dispatch exo over the given guard. */ + /** + * Produce a revocable discoverable dispatch exo over the given guard. + * + * Returns `object` for the same reason as `getSection`. + */ getDiscoverableSection: (opts: { guard: InterfaceGuard; lift: Lift; From 93ecd74f02c8fd5381cd3bd6fc4ca3cdc40eb717 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:16:52 -0400 Subject: [PATCH 34/68] refactor(kernel-utils): remove dead resolvedGuard alias in sheafify Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/sheafify.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 8da2cc4993..2b8ec854d3 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -190,9 +190,7 @@ export const sheafify = < lift: Lift; schema?: Record; }): object => { - const resolvedGuard = guard; - - const asyncMethodGuards = asyncifyMethodGuards(resolvedGuard); + const asyncMethodGuards = asyncifyMethodGuards(guard); const asyncGuard = schema === undefined ? M.interface(`${name}:section`, asyncMethodGuards) From 76ddb87ff6f8a4703a53f6d70f22729ac7a8e255 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:17:58 -0400 Subject: [PATCH 35/68] test(kernel-utils): add getSection explicit-guard test coverage Co-Authored-By: Claude Sonnet 4.6 --- .../kernel-utils/src/sheaf/sheafify.test.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 020b83fa4c..7ece864a03 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -573,3 +573,110 @@ describe('sheafify', () => { ).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Unit: getSection with explicit guard +// --------------------------------------------------------------------------- + +describe('getSection with explicit guard', () => { + it('dispatches calls that fall within the explicit guard', async () => { + const sections: PresheafSection>[] = [ + { + exo: makeSection( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + transfer: M.call(M.string(), M.number()).returns(M.boolean()), + }), + { + getBalance: (_acct: string) => 42, + transfer: (_to: string, _amt: number) => true, + }, + ), + }, + ]; + + const readGuard = M.interface('ReadOnly', { + getBalance: M.call(M.string()).returns(M.number()), + }); + + const section = sheafify({ name: 'Wallet', sections }).getSection({ + guard: readGuard, + async *lift(germs) { + yield germs[0]!; + }, + }); + + expect(await E(section).getBalance('alice')).toBe(42); + }); + + it('rejects method calls outside the explicit guard', async () => { + const sections: PresheafSection>[] = [ + { + exo: makeSection( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + transfer: M.call(M.string(), M.number()).returns(M.boolean()), + }), + { + getBalance: (_acct: string) => 42, + transfer: (_to: string, _amt: number) => true, + }, + ), + }, + ]; + + const readGuard = M.interface('ReadOnly', { + getBalance: M.call(M.string()).returns(M.number()), + }); + + const section = sheafify({ name: 'Wallet', sections }).getSection({ + guard: readGuard, + async *lift(germs) { + yield germs[0]!; + }, + }); + + // makeExo only places methods from the guard on the object — transfer is absent + expect((section as Record).transfer).toBeUndefined(); + }); + + it('getDiscoverableSection exposes __getDescription__ and obeys explicit guard', async () => { + const sections: PresheafSection>[] = [ + { + exo: makeSection( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + transfer: M.call(M.string(), M.number()).returns(M.boolean()), + }), + { + getBalance: (_acct: string) => 42, + transfer: (_to: string, _amt: number) => true, + }, + ), + }, + ]; + + const readGuard = M.interface('ReadOnly', { + getBalance: M.call(M.string()).returns(M.number()), + }); + + const schema = { getBalance: { description: 'Get account balance.' } }; + + const section = sheafify({ + name: 'Wallet', + sections, + }).getDiscoverableSection({ + guard: readGuard, + async *lift(germs) { + yield germs[0]!; + }, + schema, + }); + + expect(E(section)[GET_DESCRIPTION]()).toStrictEqual(schema); + expect(await E(section).getBalance('alice')).toBe(42); + }); +}); From 11e723484148b709576a3b81a217e8252b3cdd57 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:22:25 -0400 Subject: [PATCH 36/68] test(kernel-utils): add e2e test for multi-candidate lift retry on handler failure Co-Authored-By: Claude Sonnet 4.6 --- .../src/sheaf/sheafify.e2e.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts index 6e035cee8f..d22f6ab769 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -469,3 +469,115 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { expect(swapAFn).not.toHaveBeenCalled(); }); }); + +// --------------------------------------------------------------------------- +// E2E: lift retry — first candidate throws, sheaf recovers to fallback +// --------------------------------------------------------------------------- + +describe('e2e: lift retry on handler failure', () => { + it('recovers to next candidate when first throws, lift receives non-empty errors', async () => { + type RouteMeta = { priority: number }; + + const primaryFn = vi.fn((_acct: string): number => { + throw new Error('primary unavailable'); + }); + const fallbackFn = vi.fn((_acct: string): number => 99); + + const sections: PresheafSection[] = [ + { + exo: makeSection( + 'Primary', + M.interface('Primary', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: primaryFn }, + ), + metadata: constant({ priority: 0 }), + }, + { + exo: makeSection( + 'Fallback', + M.interface('Fallback', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: fallbackFn }, + ), + metadata: constant({ priority: 1 }), + }, + ]; + + // Track the error-array length the lift receives after each failed attempt. + const errorCountsSeenByLift: number[] = []; + const priorityFirst: Lift = async function* (germs) { + const ordered = [...germs].sort( + (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), + ); + for (const germ of ordered) { + const errors: unknown[] = yield germ; + errorCountsSeenByLift.push(errors.length); + } + }; + + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + lift: priorityFirst, + }); + + const result = await E(wallet).getBalance('alice'); + + // fallback succeeded and both handlers were invoked + expect(result).toBe(99); + expect(primaryFn).toHaveBeenCalledWith('alice'); + expect(fallbackFn).toHaveBeenCalledWith('alice'); + + // after the primary failed the lift received an errors array with one entry + expect(errorCountsSeenByLift).toHaveLength(1); + expect(errorCountsSeenByLift[0]).toBe(1); + }); + + it('throws accumulated errors when all candidates fail', async () => { + type RouteMeta = { priority: number }; + + const sections: PresheafSection[] = [ + { + exo: makeSection( + 'A', + M.interface('A', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { + getBalance: (_acct: string): number => { + throw new Error('A failed'); + }, + }, + ), + metadata: constant({ priority: 0 }), + }, + { + exo: makeSection( + 'B', + M.interface('B', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { + getBalance: (_acct: string): number => { + throw new Error('B failed'); + }, + }, + ), + metadata: constant({ priority: 1 }), + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + async *lift(germs) { + yield* [...germs].sort( + (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), + ); + }, + }); + + await expect(E(wallet).getBalance('alice')).rejects.toThrow( + 'No viable section', + ); + }); +}); From 742f725d4a5e1a2f5737702eee62bd9c4ef4de63 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:22:53 -0400 Subject: [PATCH 37/68] refactor(kernel-utils): unexport collectSheafGuard from public index Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/index.test.ts | 1 - packages/kernel-utils/src/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index 891074cfff..255fba8ed1 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -14,7 +14,6 @@ describe('index', () => { 'abortableDelay', 'calculateReconnectionBackoff', 'callable', - 'collectSheafGuard', 'constant', 'delay', 'fallthrough', diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index 2ba078b777..0382ef716e 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -62,6 +62,5 @@ export { withRanking, fallthrough, } from './sheaf/compose.ts'; -export { collectSheafGuard } from './sheaf/guard.ts'; export { makeRemoteSection } from './sheaf/remote.ts'; export { makeSection } from './sheaf/section.ts'; From b39d573b5651a002f696437e0966f32404f74ae9 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:24:55 -0400 Subject: [PATCH 38/68] docs(kernel-utils): documentation pass for sheaf module - LIFT.md: fix exhaustion description to match actual error shape - README.md: remove stale "registry" and "tracks" claims post-revocation-removal - types.ts: remove "revocable" from Sheaf method docs; clarify when to use global section variants vs explicit-guard variants - USAGE.md: use makeSection (public API) in single-provider example; clarify proxyLift vs yield* for lift composition Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/LIFT.md | 3 ++- packages/kernel-utils/src/sheaf/README.md | 7 ++++--- packages/kernel-utils/src/sheaf/USAGE.md | 12 +++++++----- packages/kernel-utils/src/sheaf/types.ts | 21 +++++++++++++++------ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/LIFT.md b/packages/kernel-utils/src/sheaf/LIFT.md index 4359a58ab0..8f0abd9526 100644 --- a/packages/kernel-utils/src/sheaf/LIFT.md +++ b/packages/kernel-utils/src/sheaf/LIFT.md @@ -33,7 +33,8 @@ The sheaf drives it with the following protocol: of every error thrown so far (cumulative, not just the last). The coroutine receives this as the resolved value of its `yield` expression. 6. **Exhausted** — if the generator returns without yielding, the sheaf - rethrows the last error. + throws `new Error('No viable section for ', { cause: errors })` + where `errors` is the full accumulated list of every failure so far. Most lifts express a fixed priority order and can ignore the error input: diff --git a/packages/kernel-utils/src/sheaf/README.md b/packages/kernel-utils/src/sheaf/README.md index cbc51d3c76..45e10449ce 100644 --- a/packages/kernel-utils/src/sheaf/README.md +++ b/packages/kernel-utils/src/sheaf/README.md @@ -3,8 +3,8 @@ Runtime capability routing adapted from sheaf theory in algebraic topology. `sheafify({ name, sections })` produces a **sheaf** — an authority manager -over a presheaf of capabilities. The sheaf grants dispatch sections via -`getSection` and tracks all delegated authority. +over a presheaf of capabilities. The sheaf produces dispatch sections via +`getSection`, each of which routes invocations through the presheaf. See [USAGE.md](./USAGE.md) for annotated examples and [LIFT.md](./LIFT.md) for the lift coroutine protocol and semantic equivalence assumption. @@ -52,7 +52,8 @@ context. > identical metadata and collapsed to one representative. **Sheaf** — The authority manager returned by `sheafify`. Holds the presheaf -data (captured at construction time) and a registry of all granted sections. +data (sections frozen at construction time) and exposes factory methods that +produce dispatch exos on demand. ``` const sheaf = sheafify({ name: 'Wallet', sections }); diff --git a/packages/kernel-utils/src/sheaf/USAGE.md b/packages/kernel-utils/src/sheaf/USAGE.md index 41d19863d4..307972faee 100644 --- a/packages/kernel-utils/src/sheaf/USAGE.md +++ b/packages/kernel-utils/src/sheaf/USAGE.md @@ -8,14 +8,13 @@ as a placeholder: ```ts import { M } from '@endo/patterns'; -import { makeDefaultExo } from ''; -import { sheafify, noopLift } from '@metamask/kernel-utils'; +import { sheafify, makeSection, noopLift } from '@metamask/kernel-utils'; const priceGuard = M.interface('PriceService', { getPrice: M.callWhen(M.await(M.string())).returns(M.await(M.number())), }); -const priceExo = makeDefaultExo('PriceService', priceGuard, { +const priceExo = makeSection('PriceService', priceGuard, { async getPrice(token) { return fetchPrice(token); }, @@ -161,5 +160,8 @@ import { before passing to `inner` - **`fallthrough(liftA, liftB)`** — try all candidates from `liftA` first; if all fail, try `liftB` -- **`proxyLift(inner)`** — forward yielded candidates up and error arrays down; - useful when wrapping a lift in middleware +- **`proxyLift(gen)`** — forward yielded candidates up and error arrays down + to an already-started generator; useful when you need to add logic between + yields (logging, counting, conditional abort). For simple sequential + composition (`fallthrough`, `withFilter`) you do not need `proxyLift` — + `yield*` forwards `.next(value)` to the delegated iterator automatically. diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/kernel-utils/src/sheaf/types.ts index 87231b4fee..afc9d98746 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/kernel-utils/src/sheaf/types.ts @@ -88,12 +88,12 @@ export type Lift> = ( /** * A sheaf: an authority manager over a presheaf. * - * Produces revocable dispatch sections via `getSection` and tracks all - * granted authority for auditing and revocation. + * Produces dispatch sections via `getSection`, each routing invocations + * through the presheaf sections supplied at construction time. */ export type Sheaf> = { /** - * Produce a revocable dispatch exo over the given guard. + * Produce a dispatch exo over the given guard. * * Returns `object` rather than a typed exo because the guard is passed * dynamically at call time — TypeScript cannot propagate the method @@ -102,7 +102,7 @@ export type Sheaf> = { */ getSection: (opts: { guard: InterfaceGuard; lift: Lift }) => object; /** - * Produce a revocable discoverable dispatch exo over the given guard. + * Produce a discoverable dispatch exo over the given guard. * * Returns `object` for the same reason as `getSection`. */ @@ -112,13 +112,22 @@ export type Sheaf> = { schema: Record; }) => object; /** - * Produce a revocable dispatch exo over the full union guard of all presheaf sections. + * Produce a dispatch exo over the full union guard of all presheaf sections. + * + * Prefer `getSection` with an explicit guard when the guard is statically + * known — it makes the capability's scope visible at the call site. Use the + * global variant when sections are assembled dynamically at runtime and the + * union guard is not known until after `sheafify` runs. * * @deprecated Provide an explicit guard via getSection instead. */ getGlobalSection: (opts: { lift: Lift }) => object; /** - * Produce a revocable discoverable dispatch exo over the full union guard of all presheaf sections. + * Produce a discoverable dispatch exo over the full union guard of all presheaf sections. + * + * Prefer `getDiscoverableSection` with an explicit guard when the guard is + * statically known. Use the global variant when sections are assembled + * dynamically and the union guard is not known until after `sheafify` runs. * * @deprecated Provide an explicit guard via getDiscoverableSection instead. */ From db760930aaae76f38d2f8eed61007454b4cb394e Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:06:52 -0400 Subject: [PATCH 39/68] test(kernel-utils): add failing tests for driveLift snapshot and metadataKey conflation bugs Co-Authored-By: Claude Sonnet 4.6 --- .../src/sheaf/sheafify.e2e.test.ts | 47 +++++++++++++++++++ .../kernel-utils/src/sheaf/sheafify.test.ts | 38 +++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts index d22f6ab769..05115d40dc 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts @@ -534,6 +534,53 @@ describe('e2e: lift retry on handler failure', () => { expect(errorCountsSeenByLift[0]).toBe(1); }); + it('lift receives error snapshots not live references', async () => { + type RouteMeta = { priority: number }; + + const handlers = [ + vi.fn((_acct: string): number => { + throw new Error('handler 0 failed'); + }), + vi.fn((_acct: string): number => { + throw new Error('handler 1 failed'); + }), + vi.fn((_acct: string): number => 99), + ]; + + const sections: PresheafSection[] = handlers.map((fn, i) => ({ + exo: makeSection( + `Section:${i}`, + M.interface(`Section:${i}`, { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: fn }, + ), + metadata: constant({ priority: i }), + })); + + let errorsAfterFirst: unknown[] | undefined; + const priorityFirst: Lift = async function* (germs) { + const ordered = [...germs].sort( + (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), + ); + for (const germ of ordered) { + const errors: unknown[] = yield germ; + errorsAfterFirst ??= errors; + } + }; + + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + lift: priorityFirst, + }); + + await E(wallet).getBalance('alice'); + + // After the first handler fails and the second handler is attempted, + // the errors array grows. errorsAfterFirst should be a snapshot with + // exactly one entry — not a live reference that was later mutated to two. + expect(errorsAfterFirst).toHaveLength(1); + }); + it('throws accumulated errors when all candidates fail', async () => { type RouteMeta = { priority: number }; diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 7ece864a03..515caf9650 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -443,6 +443,44 @@ describe('sheafify', () => { expect(liftCalled).toBe(false); }); + it('does not collapse Infinity and null metadata as equivalent', async () => { + type Meta = { cost: number | null }; + let germCount = 0; + + const sections: PresheafSection[] = [ + { + exo: makeSection( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 0 }, + ), + metadata: constant({ cost: Infinity }), + }, + { + exo: makeSection( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 0 }, + ), + metadata: constant({ cost: null }), + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + async *lift(germs) { + germCount = germs.length; + yield germs[0]!; + }, + }); + + await E(wallet).getBalance('alice'); + expect(germCount).toBe(2); + }); + it('collapses no-metadata and empty-object metadata as equivalent', async () => { type Meta = Record; let liftCalled = false; From 1beebbf96f84f7be19fd642d55ee5e6cdff28462 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:07:18 -0400 Subject: [PATCH 40/68] fix(kernel-utils): pass error snapshots to lift generator in driveLift gen.next(errors) was passing the same live mutable array reference on every resume. A lift that stores the received value from one yield and inspects it after a later yield would see mutations from subsequent failures. Pass [...errors] snapshots so each yield receives an independent copy. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/sheafify.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 2b8ec854d3..35bdca929c 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -145,7 +145,7 @@ const driveLift = async >( ): Promise => { const errors: unknown[] = []; const gen = lift(germs, context); - let next = await gen.next(errors); + let next = await gen.next([...errors]); while (!next.done) { try { const result = await invoke(next.value); @@ -153,7 +153,7 @@ const driveLift = async >( return result; } catch (error) { errors.push(error); - next = await gen.next(errors); + next = await gen.next([...errors]); } } throw new Error(`No viable section for ${context.method}`, { From 9d6497217576b4949b1f44fc753d8eaf25ade646 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:07:52 -0400 Subject: [PATCH 41/68] fix(kernel-utils): use type-tagged encoding in metadataKey to prevent conflation JSON.stringify maps undefined, NaN, Infinity, and -Infinity all to null, so sections with e.g. { cost: Infinity } and { cost: null } produced identical keys and were incorrectly collapsed into one germ. Replace the plain JSON.stringify(entries) with encodeMetadataEntry, which includes a typeof tag in each tuple so all of these distinct values produce distinct keys. BigInt metadata values no longer throw at serialization time either. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/sheafify.ts | 32 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 35bdca929c..21aa12bc7a 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -32,9 +32,35 @@ import type { Sheaf, } from './types.ts'; +type EncodedEntry = [key: string, type: string, value: unknown]; + +const encodeMetadataEntry = (key: string, value: unknown): EncodedEntry => { + if (value === undefined) { + return [key, 'undefined', null]; + } + if (typeof value === 'bigint') { + return [key, 'bigint', String(value)]; + } + if (typeof value === 'number') { + if (Number.isNaN(value)) { + return [key, 'NaN', null]; + } + if (value === Infinity) { + return [key, '+Infinity', null]; + } + if (value === -Infinity) { + return [key, '-Infinity', null]; + } + } + return [key, typeof value, value]; +}; + /** * Serialize metadata for equivalence-class keying (collapse step). * + * Uses type-tagged encoding so that values JSON.stringify conflates + * (undefined, null, NaN, Infinity, -Infinity) produce distinct keys. + * * @param metadata - The metadata value to serialize. * @returns A string key for equivalence comparison. */ @@ -43,9 +69,9 @@ const metadataKey = (metadata: Record): string => { if (keys.length === 0) { return 'null'; } - const entries = Object.entries(metadata).sort(([a], [b]) => - a.localeCompare(b), - ); + const entries = Object.entries(metadata) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, val]) => encodeMetadataEntry(key, val)); return JSON.stringify(entries); }; From 73c767faa2eec98b184afeb1ce7b2c2332f6ac36 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:21:10 -0400 Subject: [PATCH 42/68] refactor(kernel-utils): move sheaf exports to ./sheaf subpath Sheaf is a large, self-contained subsystem. Keeping it under its own subpath import reduces coupling on consumers who don't need it, and keeps the main index focused on general utilities. - Add @metamask/kernel-utils/sheaf entry point (src/sheaf/index.ts) - Remove sheaf re-exports from the main index - Add ./sheaf export to package.json alongside the other subpaths - Remove sheaf overview from README (belongs in sheaf/README.md) - Update CHANGELOG: use subpath import, drop internal exports (collectSheafGuard, getStalk, guardCoversPoint), add makeSection and noopLift, fix MetadataSpec capitalisation Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/CHANGELOG.md | 9 +- packages/kernel-utils/README.md | 104 ----------------------- packages/kernel-utils/package.json | 10 +++ packages/kernel-utils/src/index.test.ts | 11 --- packages/kernel-utils/src/index.ts | 20 ----- packages/kernel-utils/src/sheaf/index.ts | 20 +++++ 6 files changed, 34 insertions(+), 140 deletions(-) create mode 100644 packages/kernel-utils/src/sheaf/index.ts diff --git a/packages/kernel-utils/CHANGELOG.md b/packages/kernel-utils/CHANGELOG.md index 56f164019b..3bb536593a 100644 --- a/packages/kernel-utils/CHANGELOG.md +++ b/packages/kernel-utils/CHANGELOG.md @@ -11,14 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `getLibp2pRelayHome()` to the `./nodejs` exports, returning the libp2p relay's bookkeeping directory (default `~/.libp2p-relay`, overridable via `$LIBP2P_RELAY_HOME`) — kept separate from `$OCAP_HOME` so one relay can serve daemons with different OCAP_HOMEs ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) - `startRelay()` accepts an optional `publicIp` that is fed to libp2p's `appendAnnounce`, so a relay running on a NAT-backed host can announce its publicly-reachable IPv4 alongside its bound NIC addresses ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) -- Add sheaf programming module ([#870](https://github.com/MetaMask/ocap-kernel/pull/870)) +- Add `@metamask/kernel-utils/sheaf` subpath export ([#870](https://github.com/MetaMask/ocap-kernel/pull/870)) - `sheafify()` for building a `Sheaf` capability authority from a collection of `PresheafSection`s, each an exo with optional invocation-dependent metadata - `constant()`, `source()`, `callable()` for constructing metadata specs (static value, compartment-evaluated code string, and per-call function respectively) - - `proxyLift()`, `withFilter()`, `withRanking()`, `fallthrough()` for composing lifts to route and rank sections at dispatch time - - `collectSheafGuard()` for deriving a combined `InterfaceGuard` from all sections in a sheaf - - `getStalk()`, `guardCoversPoint()` for section lookup and guard checks + - `noopLift()`, `proxyLift()`, `withFilter()`, `withRanking()`, `fallthrough()` for composing lifts to route and rank sections at dispatch time + - `makeSection()` for constructing a typed exo section from a guard and handler map - `makeRemoteSection()` for wrapping a remote CapTP reference as a `PresheafSection`, fetching its interface guard once at construction and forwarding method calls via `E()` - - Types: `Sheaf`, `Section`, `PresheafSection`, `EvaluatedSection`, `MetaDataSpec`, `Lift`, `LiftContext`, `Presheaf` + - Types: `Sheaf`, `Section`, `PresheafSection`, `EvaluatedSection`, `MetadataSpec`, `Lift`, `LiftContext` ## [0.5.0] diff --git a/packages/kernel-utils/README.md b/packages/kernel-utils/README.md index 53abcb1b0a..7bc7e8cf29 100644 --- a/packages/kernel-utils/README.md +++ b/packages/kernel-utils/README.md @@ -26,110 +26,6 @@ or npm install --save-dev patch-package ``` -## Sheaf Module - -The sheaf module provides a dispatch abstraction for routing method calls across multiple capability objects (_sections_) that each cover a region of a shared interface. - -### Overview - -``` -sheafify({ name, sections, compartment? }) → Sheaf -sheaf.getGlobalSection({ lift }) → section proxy -sheaf.getSection({ guard, lift }) → section proxy -``` - -Each call on the proxy is dispatched to whichever section covers that method. When multiple sections are eligible, a **lift** selects among them. A lift is an `AsyncGenerator` coroutine that yields candidates one at a time and receives the accumulated error history on each resume — enabling retry, fallback, and cost-aware routing without callers needing to know the selection strategy. - -### Defining sections - -```ts -import { sheafify, constant, callable } from '@metamask/kernel-utils'; - -const sheaf = sheafify({ - name: 'Wallet', - sections: [ - { - exo: walletA, - metadata: constant({ cost: 10, push: false }), - }, - { - exo: walletB, - // callable metadata is evaluated per-call with the actual arguments - metadata: callable((args) => ({ cost: 1 + 0.1 * (args[0] as number) })), - }, - { - exo: walletC, - // source metadata is compiled once at sheafify time via the compartment - metadata: source(`(args) => ({ cost: 5 + 0.01 * args[0] })`), - }, - ], - compartment, // required only when using source-kind metadata -}); -``` - -**Metadata kinds:** -| Kind | When evaluated | Use case | -|------|---------------|----------| -| `constant(v)` | Never (static) | Fixed priority or capability flags | -| `callable(fn)` | Each call | Arg-dependent cost, remaining spend | -| `source(str)` | Each call (compiled at construction) | Sandboxed cost functions | - -### Writing a lift - -A lift receives `EvaluatedSection>[]` (germs) and a context, and yields candidates in preference order. It receives a snapshot of all accumulated errors on each `gen.next(errors)` call. - -```ts -import type { Lift } from '@metamask/kernel-utils'; - -// Yield cheapest section first; fall back in cost order on failure -const cheapest: Lift<{ cost: number }> = async function* (germs) { - yield* [...germs].sort( - (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), - ); -}; - -const section = sheaf.getGlobalSection({ lift: cheapest }); -``` - -### Composing lifts - -```ts -import { - withFilter, - withRanking, - fallthrough, - proxyLift, -} from '@metamask/kernel-utils'; - -// Filter out sections with insufficient remaining spend -const spendable = withFilter( - (germ, { args }) => - (germ.metadata?.remainingSpend ?? Infinity) >= (args[0] as number), -); - -// Sort by cost before passing to the inner lift -const byCost = withRanking( - (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), -); - -// Try local sections first, fall through to remote on exhaustion -const withFallback = fallthrough(localLift, remoteLift); - -// Compose: filter → rank → select -const lift = spendable(byCost(cheapest)); -``` - -`withFilter` and `withRanking` are pure input transforms that return the inner lift's generator directly. `fallthrough` sequences two lifts via `yield*`, which forwards the error array to each inner lift. `proxyLift` is the primitive for adding logic (logging, circuit-breaking) between yields. - -### Error handling - -When all candidates are exhausted, `driveLift` throws: - -``` -Error: No viable section for - cause: [Error: ..., Error: ..., ...] // all accumulated attempt errors -``` - ## Contributing This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme). diff --git a/packages/kernel-utils/package.json b/packages/kernel-utils/package.json index 45f0e1fb3e..d32b2df934 100644 --- a/packages/kernel-utils/package.json +++ b/packages/kernel-utils/package.json @@ -79,6 +79,16 @@ "default": "./dist/nodejs/index.cjs" } }, + "./sheaf": { + "import": { + "types": "./dist/sheaf/index.d.mts", + "default": "./dist/sheaf/index.mjs" + }, + "require": { + "types": "./dist/sheaf/index.d.cts", + "default": "./dist/sheaf/index.cjs" + } + }, "./vite-plugins": { "import": { "types": "./dist/vite-plugins/index.d.mts", diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index 255fba8ed1..cc1985bc46 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -13,10 +13,7 @@ describe('index', () => { 'GET_DESCRIPTION', 'abortableDelay', 'calculateReconnectionBackoff', - 'callable', - 'constant', 'delay', - 'fallthrough', 'fetchValidatedJson', 'fromHex', 'ifDefined', @@ -33,22 +30,14 @@ describe('index', () => { 'makeDefaultExo', 'makeDefaultInterface', 'makeDiscoverableExo', - 'makeRemoteSection', - 'makeSection', 'mergeDisjointRecords', 'methodArgsToStruct', - 'noopLift', 'prettifySmallcaps', - 'proxyLift', 'retry', 'retryWithBackoff', - 'sheafify', - 'source', 'stringify', 'toHex', 'waitUntilQuiescent', - 'withFilter', - 'withRanking', ]); }); }); diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index 0382ef716e..bc895d4a1b 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -44,23 +44,3 @@ export { DEFAULT_MAX_DELAY_MS, } from './retry.ts'; export type { RetryBackoffOptions, RetryOnRetryInfo } from './retry.ts'; -export type { - Section, - PresheafSection, - EvaluatedSection, - MetadataSpec, - Lift, - LiftContext, - Sheaf, -} from './sheaf/types.ts'; -export { constant, source, callable } from './sheaf/metadata.ts'; -export { sheafify } from './sheaf/sheafify.ts'; -export { - noopLift, - proxyLift, - withFilter, - withRanking, - fallthrough, -} from './sheaf/compose.ts'; -export { makeRemoteSection } from './sheaf/remote.ts'; -export { makeSection } from './sheaf/section.ts'; diff --git a/packages/kernel-utils/src/sheaf/index.ts b/packages/kernel-utils/src/sheaf/index.ts new file mode 100644 index 0000000000..1f735d3083 --- /dev/null +++ b/packages/kernel-utils/src/sheaf/index.ts @@ -0,0 +1,20 @@ +export type { + Section, + PresheafSection, + EvaluatedSection, + MetadataSpec, + Lift, + LiftContext, + Sheaf, +} from './types.ts'; +export { constant, source, callable } from './metadata.ts'; +export { sheafify } from './sheafify.ts'; +export { + noopLift, + proxyLift, + withFilter, + withRanking, + fallthrough, +} from './compose.ts'; +export { makeRemoteSection } from './remote.ts'; +export { makeSection } from './section.ts'; From 47fae16f1d4d239639e4cecefae6133824da0b34 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:24:11 -0400 Subject: [PATCH 43/68] test(kernel-utils): add failing test for NaN constraint detection in decomposeMetadata Co-Authored-By: Claude Sonnet 4.6 --- .../kernel-utils/src/sheaf/sheafify.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 515caf9650..8fbaf72075 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -443,6 +443,53 @@ describe('sheafify', () => { expect(liftCalled).toBe(false); }); + it('extracts shared NaN metadata values into constraints', async () => { + type Meta = { cost: number; priority: number }; + let capturedGerms: EvaluatedSection>[] = []; + let capturedContext: LiftContext | undefined; + + const sections: PresheafSection[] = [ + { + exo: makeSection( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 0 }, + ), + metadata: constant({ cost: NaN, priority: 0 }), + }, + { + exo: makeSection( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 0 }, + ), + metadata: constant({ cost: NaN, priority: 1 }), + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + async *lift(germs, context) { + capturedGerms = germs; + capturedContext = context; + yield germs[0]!; + }, + }); + + await E(wallet).getBalance('alice'); + + // NaN is shared across all germs, so it should be extracted as a constraint + // — not left as distinguishing metadata in each germ's options. + expect(Number.isNaN(capturedContext?.constraints.cost)).toBe(true); + expect(capturedGerms.map((germ) => germ.metadata)).toStrictEqual([ + { priority: 0 }, + { priority: 1 }, + ]); + }); + it('does not collapse Infinity and null metadata as equivalent', async () => { type Meta = { cost: number | null }; let germCount = 0; From 2db4dca2d951c05cd74db04a521d274b552c2579 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:24:31 -0400 Subject: [PATCH 44/68] fix(kernel-utils): use Object.is for value equality in decomposeMetadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit === fails for NaN (NaN !== NaN), so a NaN value shared by all germs was never promoted to a constraint — it remained in each germ's distinguishing metadata instead. Object.is correctly treats NaN === NaN and is consistent with the type-tagged encoding already used in collapseEquivalent. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/sheafify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 21aa12bc7a..2d971506bd 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -121,7 +121,7 @@ const decomposeMetadata = >( const val = first[key]; const shared = stalk.every((entry) => { const meta = entry.metadata; - return key in meta && meta[key] === val; + return key in meta && Object.is(meta[key], val); }); if (shared) { constraints[key] = val; From 84a33735306dc2b441ee81a75c0a3abc524f4cbb Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:40:42 -0400 Subject: [PATCH 45/68] test(kernel-utils): add failing test for -0 vs +0 collapse in metadataKey Co-Authored-By: Claude Sonnet 4.6 --- .../kernel-utils/src/sheaf/sheafify.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/kernel-utils/src/sheaf/sheafify.test.ts index 8fbaf72075..b66c71cccd 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.test.ts @@ -490,6 +490,44 @@ describe('sheafify', () => { ]); }); + it('does not collapse +0 and -0 metadata as equivalent', async () => { + type Meta = { cost: number }; + let germCount = 0; + + const sections: PresheafSection[] = [ + { + exo: makeSection( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 0 }, + ), + metadata: constant({ cost: +0 }), + }, + { + exo: makeSection( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 0 }, + ), + metadata: constant({ cost: -0 }), + }, + ]; + + const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + async *lift(germs) { + germCount = germs.length; + yield germs[0]!; + }, + }); + + await E(wallet).getBalance('alice'); + expect(germCount).toBe(2); + }); + it('does not collapse Infinity and null metadata as equivalent', async () => { type Meta = { cost: number | null }; let germCount = 0; From 953d26abb4901db09a219ba2d271a5b7432c2ac7 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:41:01 -0400 Subject: [PATCH 46/68] fix(kernel-utils): handle -0 in encodeMetadataEntry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON.stringify(-0) produces "0", so -0 and +0 were serialised to the same metadataKey and incorrectly collapsed into one germ by collapseEquivalent. Object.is(0, -0) is false, so decomposeMetadata already treated them as distinct — making the two functions inconsistent. Add -0 as an explicit special case alongside NaN, +Infinity, -Infinity. Co-Authored-By: Claude Sonnet 4.6 --- packages/kernel-utils/src/sheaf/sheafify.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/kernel-utils/src/sheaf/sheafify.ts index 2d971506bd..1a3331b300 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/kernel-utils/src/sheaf/sheafify.ts @@ -51,6 +51,9 @@ const encodeMetadataEntry = (key: string, value: unknown): EncodedEntry => { if (value === -Infinity) { return [key, '-Infinity', null]; } + if (Object.is(value, -0)) { + return [key, '-0', null]; + } } return [key, typeof value, value]; }; From 88629de46921a9bfe755c42660846c5645592ce1 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 7 May 2026 14:53:47 -0400 Subject: [PATCH 47/68] feat(sheaves): add sheaves package --- packages/sheaves/CHANGELOG.md | 14 + packages/sheaves/LICENSE.APACHE2 | 201 +++ packages/sheaves/LICENSE.MIT | 20 + .../src/sheaf => sheaves}/README.md | 0 .../src/sheaf => sheaves/documents}/LIFT.md | 0 .../src/sheaf => sheaves/documents}/USAGE.md | 0 packages/sheaves/package.json | 105 ++ .../src/sheaf => sheaves/src}/compose.test.ts | 0 .../src/sheaf => sheaves/src}/compose.ts | 0 .../src/sheaf => sheaves/src}/guard.test.ts | 0 .../src/sheaf => sheaves/src}/guard.ts | 0 .../src/sheaf => sheaves/src}/index.ts | 0 .../sheaf => sheaves/src}/metadata.test.ts | 0 .../src/sheaf => sheaves/src}/metadata.ts | 0 .../src/sheaf => sheaves/src}/remote.test.ts | 0 .../src/sheaf => sheaves/src}/remote.ts | 2 +- .../src/sheaf => sheaves/src}/section.ts | 0 .../src}/sheafify.e2e.test.ts | 0 .../src}/sheafify.string-metadata.test.ts | 0 .../sheaf => sheaves/src}/sheafify.test.ts | 2 +- .../src/sheaf => sheaves/src}/sheafify.ts | 6 +- .../src/sheaf => sheaves/src}/stalk.test.ts | 0 .../src/sheaf => sheaves/src}/stalk.ts | 0 .../src/sheaf => sheaves/src}/types.ts | 3 +- packages/sheaves/tsconfig.build.json | 13 + packages/sheaves/tsconfig.json | 14 + packages/sheaves/typedoc.json | 8 + packages/sheaves/vitest.config.ts | 22 + tsconfig.build.json | 1 + tsconfig.json | 1 + yarn.lock | 1183 ++++++++--------- 31 files changed, 949 insertions(+), 646 deletions(-) create mode 100644 packages/sheaves/CHANGELOG.md create mode 100644 packages/sheaves/LICENSE.APACHE2 create mode 100644 packages/sheaves/LICENSE.MIT rename packages/{kernel-utils/src/sheaf => sheaves}/README.md (100%) rename packages/{kernel-utils/src/sheaf => sheaves/documents}/LIFT.md (100%) rename packages/{kernel-utils/src/sheaf => sheaves/documents}/USAGE.md (100%) create mode 100644 packages/sheaves/package.json rename packages/{kernel-utils/src/sheaf => sheaves/src}/compose.test.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/compose.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/guard.test.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/guard.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/index.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/metadata.test.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/metadata.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/remote.test.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/remote.ts (97%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/section.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/sheafify.e2e.test.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/sheafify.string-metadata.test.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/sheafify.test.ts (99%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/sheafify.ts (98%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/stalk.test.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/stalk.ts (100%) rename packages/{kernel-utils/src/sheaf => sheaves/src}/types.ts (98%) create mode 100644 packages/sheaves/tsconfig.build.json create mode 100644 packages/sheaves/tsconfig.json create mode 100644 packages/sheaves/typedoc.json create mode 100644 packages/sheaves/vitest.config.ts diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md new file mode 100644 index 0000000000..927ff7d580 --- /dev/null +++ b/packages/sheaves/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] + +### Added + +- Initial release, extracted from `@metamask/kernel-utils` diff --git a/packages/sheaves/LICENSE.APACHE2 b/packages/sheaves/LICENSE.APACHE2 new file mode 100644 index 0000000000..8194a06aee --- /dev/null +++ b/packages/sheaves/LICENSE.APACHE2 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Consensys Software Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/sheaves/LICENSE.MIT b/packages/sheaves/LICENSE.MIT new file mode 100644 index 0000000000..658c855eb8 --- /dev/null +++ b/packages/sheaves/LICENSE.MIT @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 Consensys Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/kernel-utils/src/sheaf/README.md b/packages/sheaves/README.md similarity index 100% rename from packages/kernel-utils/src/sheaf/README.md rename to packages/sheaves/README.md diff --git a/packages/kernel-utils/src/sheaf/LIFT.md b/packages/sheaves/documents/LIFT.md similarity index 100% rename from packages/kernel-utils/src/sheaf/LIFT.md rename to packages/sheaves/documents/LIFT.md diff --git a/packages/kernel-utils/src/sheaf/USAGE.md b/packages/sheaves/documents/USAGE.md similarity index 100% rename from packages/kernel-utils/src/sheaf/USAGE.md rename to packages/sheaves/documents/USAGE.md diff --git a/packages/sheaves/package.json b/packages/sheaves/package.json new file mode 100644 index 0000000000..7c678ec516 --- /dev/null +++ b/packages/sheaves/package.json @@ -0,0 +1,105 @@ +{ + "name": "@metamask/sheaves", + "version": "0.1.0", + "description": "Capability routing via sheaf theory", + "keywords": [ + "MetaMask", + "object capabilities", + "ocap" + ], + "homepage": "https://github.com/MetaMask/ocap-kernel/tree/main/packages/sheaves#readme", + "bugs": { + "url": "https://github.com/MetaMask/ocap-kernel/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/ocap-kernel.git" + }, + "license": "(MIT OR Apache-2.0)", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --no-references --clean", + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/sheaves", + "changelog:update": "../../scripts/update-changelog.sh @metamask/sheaves", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", + "lint:dependencies": "depcheck --quiet", + "lint:eslint": "eslint . --cache", + "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies", + "lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore --log-level error", + "publish:preview": "yarn npm publish --tag preview", + "test": "vitest run --config vitest.config.ts", + "test:clean": "yarn test --no-cache --coverage.clean", + "test:dev": "yarn test --mode development", + "test:verbose": "yarn test --reporter verbose", + "test:watch": "vitest --config vitest.config.ts", + "test:dev:quiet": "yarn test:dev --reporter @ocap/repo-tools/vitest-reporters/silent" + }, + "dependencies": { + "@endo/eventual-send": "^1.3.4", + "@endo/exo": "^1.5.12", + "@endo/patterns": "^1.7.0", + "@metamask/kernel-utils": "workspace:^" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.17.4", + "@metamask/auto-changelog": "^5.3.0", + "@metamask/eslint-config": "^15.0.0", + "@metamask/eslint-config-nodejs": "^15.0.0", + "@metamask/eslint-config-typescript": "^15.0.0", + "@metamask/kernel-shims": "workspace:^", + "@ocap/repo-tools": "workspace:^", + "@ts-bridge/cli": "^0.6.3", + "@ts-bridge/shims": "^0.1.1", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", + "@typescript-eslint/utils": "^8.29.0", + "@vitest/eslint-plugin": "^1.6.14", + "depcheck": "^1.4.7", + "eslint": "^9.23.0", + "eslint-config-prettier": "^10.1.1", + "eslint-import-resolver-typescript": "^4.3.1", + "eslint-plugin-import-x": "^4.10.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-n": "^17.17.0", + "eslint-plugin-prettier": "^5.2.6", + "eslint-plugin-promise": "^7.2.1", + "prettier": "^3.5.3", + "rimraf": "^6.0.1", + "ses": "^1.14.0", + "turbo": "^2.9.1", + "typedoc": "^0.28.1", + "typescript": "~5.8.2", + "typescript-eslint": "^8.29.0", + "vite": "^8.0.6", + "vitest": "^4.1.3" + }, + "engines": { + "node": ">=22" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/kernel-utils/src/sheaf/compose.test.ts b/packages/sheaves/src/compose.test.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/compose.test.ts rename to packages/sheaves/src/compose.test.ts diff --git a/packages/kernel-utils/src/sheaf/compose.ts b/packages/sheaves/src/compose.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/compose.ts rename to packages/sheaves/src/compose.ts diff --git a/packages/kernel-utils/src/sheaf/guard.test.ts b/packages/sheaves/src/guard.test.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/guard.test.ts rename to packages/sheaves/src/guard.test.ts diff --git a/packages/kernel-utils/src/sheaf/guard.ts b/packages/sheaves/src/guard.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/guard.ts rename to packages/sheaves/src/guard.ts diff --git a/packages/kernel-utils/src/sheaf/index.ts b/packages/sheaves/src/index.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/index.ts rename to packages/sheaves/src/index.ts diff --git a/packages/kernel-utils/src/sheaf/metadata.test.ts b/packages/sheaves/src/metadata.test.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/metadata.test.ts rename to packages/sheaves/src/metadata.test.ts diff --git a/packages/kernel-utils/src/sheaf/metadata.ts b/packages/sheaves/src/metadata.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/metadata.ts rename to packages/sheaves/src/metadata.ts diff --git a/packages/kernel-utils/src/sheaf/remote.test.ts b/packages/sheaves/src/remote.test.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/remote.test.ts rename to packages/sheaves/src/remote.test.ts diff --git a/packages/kernel-utils/src/sheaf/remote.ts b/packages/sheaves/src/remote.ts similarity index 97% rename from packages/kernel-utils/src/sheaf/remote.ts rename to packages/sheaves/src/remote.ts index 1a3f388f3b..709f1a48c3 100644 --- a/packages/kernel-utils/src/sheaf/remote.ts +++ b/packages/sheaves/src/remote.ts @@ -2,8 +2,8 @@ import { E } from '@endo/eventual-send'; import { GET_INTERFACE_GUARD } from '@endo/exo'; import { getInterfaceGuardPayload } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; +import { ifDefined } from '@metamask/kernel-utils'; -import { ifDefined } from '../misc.ts'; import { makeSection } from './section.ts'; import type { MetadataSpec, PresheafSection } from './types.ts'; diff --git a/packages/kernel-utils/src/sheaf/section.ts b/packages/sheaves/src/section.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/section.ts rename to packages/sheaves/src/section.ts diff --git a/packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts b/packages/sheaves/src/sheafify.e2e.test.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/sheafify.e2e.test.ts rename to packages/sheaves/src/sheafify.e2e.test.ts diff --git a/packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts b/packages/sheaves/src/sheafify.string-metadata.test.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/sheafify.string-metadata.test.ts rename to packages/sheaves/src/sheafify.string-metadata.test.ts diff --git a/packages/kernel-utils/src/sheaf/sheafify.test.ts b/packages/sheaves/src/sheafify.test.ts similarity index 99% rename from packages/kernel-utils/src/sheaf/sheafify.test.ts rename to packages/sheaves/src/sheafify.test.ts index b66c71cccd..1ffcea0b0a 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.test.ts +++ b/packages/sheaves/src/sheafify.test.ts @@ -1,8 +1,8 @@ import { GET_INTERFACE_GUARD } from '@endo/exo'; import { M, getInterfaceGuardPayload } from '@endo/patterns'; +import { GET_DESCRIPTION } from '@metamask/kernel-utils'; import { describe, it, expect } from 'vitest'; -import { GET_DESCRIPTION } from '../discoverable.ts'; import { constant } from './metadata.ts'; import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; diff --git a/packages/kernel-utils/src/sheaf/sheafify.ts b/packages/sheaves/src/sheafify.ts similarity index 98% rename from packages/kernel-utils/src/sheaf/sheafify.ts rename to packages/sheaves/src/sheafify.ts index 1a3331b300..6eecbd16e9 100644 --- a/packages/kernel-utils/src/sheaf/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -15,10 +15,10 @@ import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; import type { InterfaceGuard } from '@endo/patterns'; +import type { MethodSchema } from '@metamask/kernel-utils'; +import { makeDiscoverableExo } from '@metamask/kernel-utils'; +import { stringify } from '@metamask/kernel-utils'; -import { makeDiscoverableExo } from '../discoverable.ts'; -import type { MethodSchema } from '../schema.ts'; -import { stringify } from '../stringify.ts'; import { asyncifyMethodGuards, collectSheafGuard } from './guard.ts'; import { evaluateMetadata, resolveMetadataSpec } from './metadata.ts'; import type { ResolvedMetadataSpec } from './metadata.ts'; diff --git a/packages/kernel-utils/src/sheaf/stalk.test.ts b/packages/sheaves/src/stalk.test.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/stalk.test.ts rename to packages/sheaves/src/stalk.test.ts diff --git a/packages/kernel-utils/src/sheaf/stalk.ts b/packages/sheaves/src/stalk.ts similarity index 100% rename from packages/kernel-utils/src/sheaf/stalk.ts rename to packages/sheaves/src/stalk.ts diff --git a/packages/kernel-utils/src/sheaf/types.ts b/packages/sheaves/src/types.ts similarity index 98% rename from packages/kernel-utils/src/sheaf/types.ts rename to packages/sheaves/src/types.ts index afc9d98746..be4cdebe24 100644 --- a/packages/kernel-utils/src/sheaf/types.ts +++ b/packages/sheaves/src/types.ts @@ -9,8 +9,7 @@ import type { GET_INTERFACE_GUARD, Methods } from '@endo/exo'; import type { InterfaceGuard } from '@endo/patterns'; - -import type { MethodSchema } from '../schema.ts'; +import type { MethodSchema } from '@metamask/kernel-utils'; /** A section: a capability covering a region of the interface topology. */ export type Section = Partial & { diff --git a/packages/sheaves/tsconfig.build.json b/packages/sheaves/tsconfig.build.json new file mode 100644 index 0000000000..85fa65ecb9 --- /dev/null +++ b/packages/sheaves/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "types": ["ses"] + }, + "references": [{ "path": "../kernel-utils/tsconfig.build.json" }], + "files": [], + "include": ["./src"] +} diff --git a/packages/sheaves/tsconfig.json b/packages/sheaves/tsconfig.json new file mode 100644 index 0000000000..41d8556750 --- /dev/null +++ b/packages/sheaves/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "lib": ["ES2022"], + "types": ["ses", "vitest"] + }, + "references": [ + { "path": "../repo-tools" }, + { "path": "../kernel-utils" }, + { "path": "../kernel-shims" } + ], + "include": ["../../vitest.config.ts", "./src", "./vitest.config.ts"] +} diff --git a/packages/sheaves/typedoc.json b/packages/sheaves/typedoc.json new file mode 100644 index 0000000000..f8eb78ae1a --- /dev/null +++ b/packages/sheaves/typedoc.json @@ -0,0 +1,8 @@ +{ + "entryPoints": [], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json", + "projectDocuments": ["documents/*.md"] +} diff --git a/packages/sheaves/vitest.config.ts b/packages/sheaves/vitest.config.ts new file mode 100644 index 0000000000..8714c6afe1 --- /dev/null +++ b/packages/sheaves/vitest.config.ts @@ -0,0 +1,22 @@ +import { mergeConfig } from '@ocap/repo-tools/vitest-config'; +import { fileURLToPath } from 'node:url'; +import { defineConfig, defineProject } from 'vitest/config'; + +import defaultConfig from '../../vitest.config.ts'; + +export default defineConfig((args) => { + return mergeConfig( + args, + defaultConfig, + defineProject({ + test: { + name: 'sheaves', + setupFiles: [ + fileURLToPath( + import.meta.resolve('@ocap/repo-tools/test-utils/mock-endoify'), + ), + ], + }, + }), + ); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index e261b4d894..3eacbee1fd 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -23,6 +23,7 @@ { "path": "./packages/sample-services/tsconfig.build.json" }, { "path": "./packages/service-discovery-types/tsconfig.build.json" }, { "path": "./packages/service-matcher/tsconfig.build.json" }, + { "path": "./packages/sheaves/tsconfig.build.json" }, { "path": "./packages/streams/tsconfig.build.json" }, { "path": "./packages/template-package/tsconfig.build.json" } ] diff --git a/tsconfig.json b/tsconfig.json index 1b965924c5..fca57f6805 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,6 +39,7 @@ { "path": "./packages/sample-services" }, { "path": "./packages/service-discovery-types" }, { "path": "./packages/service-matcher" }, + { "path": "./packages/sheaves" }, { "path": "./packages/streams" }, { "path": "./packages/template-package" } ] diff --git a/yarn.lock b/yarn.lock index 50c4244ac0..eb6fab2e18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -138,36 +138,27 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^5.1.11": - version: 5.1.11 - resolution: "@asamuzakjp/css-color@npm:5.1.11" +"@asamuzakjp/css-color@npm:^5.1.5": + version: 5.1.6 + resolution: "@asamuzakjp/css-color@npm:5.1.6" dependencies: - "@asamuzakjp/generational-cache": "npm:^1.0.1" - "@csstools/css-calc": "npm:^3.2.0" - "@csstools/css-color-parser": "npm:^4.1.0" + "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-color-parser": "npm:^4.0.2" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" - checksum: 10/2e337cc94b5a3f9741a27f92b4e4b7dc467a76b1dcf66c40e71808fed71695f10c8cf07c8b13313cbb637154314ca1d8626bb9a045fe94b404b242a390cf3bd3 + checksum: 10/5151369d9369e478e03c0eee0f171b8f86306ebbdf5b352544cd745c360d97343f437bdd0690ff658e47d2876b466bffc8811fcef7f0347cb243c6483a7e95a0 languageName: node linkType: hard -"@asamuzakjp/dom-selector@npm:^7.1.1": - version: 7.1.1 - resolution: "@asamuzakjp/dom-selector@npm:7.1.1" +"@asamuzakjp/dom-selector@npm:^7.0.6": + version: 7.0.7 + resolution: "@asamuzakjp/dom-selector@npm:7.0.7" dependencies: - "@asamuzakjp/generational-cache": "npm:^1.0.1" "@asamuzakjp/nwsapi": "npm:^2.3.9" bidi-js: "npm:^1.0.3" css-tree: "npm:^3.2.1" is-potential-custom-element-name: "npm:^1.0.1" - checksum: 10/49a065a64db5f53a3008c231d09606e4b67f509fa20148a67419451c2dc91a421202ed17bfc4bc679ad2f0432d7260720d602c1d5c9c5e165931fff5199c3f12 - languageName: node - linkType: hard - -"@asamuzakjp/generational-cache@npm:^1.0.1": - version: 1.0.1 - resolution: "@asamuzakjp/generational-cache@npm:1.0.1" - checksum: 10/e1cf3f1916a334c6153f624982f0eb3d50fa3048435ea5c5b0f441f8f1ab74a0fe992dac214b612d22c0acafad3cd1a1f6b45d99c7b6e3b63cfdf7f6ca5fc144 + checksum: 10/18f40def8c775c6008c8fcd75d7d049ff92d99a494929ab2bf742341b348c78cbf4808d29c13b9cd87ca4fd272773cf5aa9e58fee48603c286df48148be8cb67 languageName: node linkType: hard @@ -314,13 +305,13 @@ __metadata: linkType: hard "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.29.0": - version: 7.29.3 - resolution: "@babel/parser@npm:7.29.3" + version: 7.29.2 + resolution: "@babel/parser@npm:7.29.2" dependencies: "@babel/types": "npm:^7.29.0" bin: parser: ./bin/babel-parser.js - checksum: 10/10e8f34e0fdaa495b9db8be71f4eb29b16d8a57e0818c1bb1c4084015b0383803fd77812ed41597760cbf3d9ab3ae9f4af54f39ff5e5d8e081ba43593232f0ca + checksum: 10/45d050bf75aa5194b3255f156173e8553d615ff5a2434674cc4a10cdc7c261931befb8618c996a1c449b87f0ef32a3407879af2ac967d95dc7b4fdbae7037efa languageName: node linkType: hard @@ -465,9 +456,9 @@ __metadata: linkType: hard "@chainsafe/as-sha256@npm:^1.2.0": - version: 1.2.4 - resolution: "@chainsafe/as-sha256@npm:1.2.4" - checksum: 10/3197a7695c89f532afca7345e29a2aea02426404690b760d5627bcf5f00e83e28c1728f8edca79f65270fc3525020813e7aacaebbcc81c82b06200d68094cea1 + version: 1.2.0 + resolution: "@chainsafe/as-sha256@npm:1.2.0" + checksum: 10/9c1bf0009954fc07a288b4a920d2289cec21f74662805b60e78f7449fdb7743a4d23875eb292695f12d95fdfa1d44e3f78af8c8e1d1703601a5585bd964e9f5b languageName: node linkType: hard @@ -621,26 +612,26 @@ __metadata: languageName: node linkType: hard -"@csstools/css-calc@npm:^3.2.0": - version: 3.2.0 - resolution: "@csstools/css-calc@npm:3.2.0" +"@csstools/css-calc@npm:^3.1.1": + version: 3.1.1 + resolution: "@csstools/css-calc@npm:3.1.1" peerDependencies: "@csstools/css-parser-algorithms": ^4.0.0 "@csstools/css-tokenizer": ^4.0.0 - checksum: 10/7eec51a21945a74aa6a407d1e6290d0f4c5d01829a42c01a56ce2055216398540cc3120837b15a0db38601bcb40cf97f1d991fefb3ee9d00d9cec03d67beba4c + checksum: 10/faa3aa2736b20757ceafd76e3d2841e8726ec9e7ae78e387684eb462aba73d533ba384039338685c3a52196196300ccdfecb051e59864b1d3b457fe927b7f53b languageName: node linkType: hard -"@csstools/css-color-parser@npm:^4.1.0": - version: 4.1.0 - resolution: "@csstools/css-color-parser@npm:4.1.0" +"@csstools/css-color-parser@npm:^4.0.2": + version: 4.0.2 + resolution: "@csstools/css-color-parser@npm:4.0.2" dependencies: "@csstools/color-helpers": "npm:^6.0.2" - "@csstools/css-calc": "npm:^3.2.0" + "@csstools/css-calc": "npm:^3.1.1" peerDependencies: "@csstools/css-parser-algorithms": ^4.0.0 "@csstools/css-tokenizer": ^4.0.0 - checksum: 10/794508011a95ebac3856e67e0333ca4174604d2dfddc101d991f2ebfd52b3c99cd36a08462675c2a07d057ca3787187fcd7eac98bced2eefdd9040b37853426d + checksum: 10/6418bfadc8c15d3a65c1e80278df383b542f0437446c0ba21d591dd564bcc19ab0b11243edf62672f4c62cc778f9b386fa4349e9a8d1de2b414148ea8a1ac775 languageName: node linkType: hard @@ -653,15 +644,15 @@ __metadata: languageName: node linkType: hard -"@csstools/css-syntax-patches-for-csstree@npm:^1.1.3": - version: 1.1.3 - resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.3" +"@csstools/css-syntax-patches-for-csstree@npm:^1.1.1": + version: 1.1.2 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.2" peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: css-tree: optional: true - checksum: 10/1c91dc03b64ca913eed5064ca0e434da1c0be8def6ce20f932d1db10f9b478ac3830c99a033b0edf75954cf9164c7c267b220ed9faffbc3342bf320870c3bb4b + checksum: 10/6ac57afa549ea3df11b9341730b2bec56c5383f229a9eb9db6c7b86ab46c9e145498ab285fe2a6ea8bcd74ac7f5f746c086083603da42128d915c984f797a281 languageName: node linkType: hard @@ -681,31 +672,31 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:1.10.0, @emnapi/core@npm:^1.4.3": - version: 1.10.0 - resolution: "@emnapi/core@npm:1.10.0" +"@emnapi/core@npm:1.9.1, @emnapi/core@npm:^1.4.3": + version: 1.9.1 + resolution: "@emnapi/core@npm:1.9.1" dependencies: - "@emnapi/wasi-threads": "npm:1.2.1" + "@emnapi/wasi-threads": "npm:1.2.0" tslib: "npm:^2.4.0" - checksum: 10/d32f386084e64deaf2609aabb8295d1ad5af6144d0f46d2060b76cc53f1f3b486df54bec9b0f33c37d85a3822e1193ebcd4e3deb4a5f0e4cd650aa2ffc631715 + checksum: 10/c44cfe471702b43306b84d0f4f2f1506dac0065dbd73dc5a41bd99a2c39802ca7e2d7ebfbfae8997468d1ff0420603596bf35b19eabd5951bad1eb630d2d4574 languageName: node linkType: hard -"@emnapi/runtime@npm:1.10.0, @emnapi/runtime@npm:^1.4.3": - version: 1.10.0 - resolution: "@emnapi/runtime@npm:1.10.0" +"@emnapi/runtime@npm:1.9.1, @emnapi/runtime@npm:^1.4.3": + version: 1.9.1 + resolution: "@emnapi/runtime@npm:1.9.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10/d21083d07fa0c2da171c142e78ef986b66b07d45b06accc0bcaf49fcc61bb4dbc10e1c1760813070165b9f49b054376a931045347f21c0f42ff1eb2d2040faac + checksum: 10/337767fa44ec1f6277494342664be8773f16aad4086e9e49423a9f06c5eee7495e2e1b0b50dcd764c5a5cc4c15c9d80c13fba2da6763a97c06a48115cd7ccd14 languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.2.1": - version: 1.2.1 - resolution: "@emnapi/wasi-threads@npm:1.2.1" +"@emnapi/wasi-threads@npm:1.2.0": + version: 1.2.0 + resolution: "@emnapi/wasi-threads@npm:1.2.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10/57cd4292be81c05d26aa886d68a9e4c449ff666e8503fed6463dfc6b64a4e4213f03c152d53296b7cda32840271e38cd33347332070658f01befeb9bf4e59f36 + checksum: 10/c8e48c7200530744dc58170d2e25933b61433e4a0c50b4f192f5d8d4b065c7023dbfc48dac0afadbc29bd239013f2ae454c6e54e0ca6e8248402bf95c9e77e22 languageName: node linkType: hard @@ -1546,7 +1537,7 @@ __metadata: languageName: node linkType: hard -"@libp2p/crypto@npm:5.1.14": +"@libp2p/crypto@npm:5.1.14, @libp2p/crypto@npm:^5.1.14, @libp2p/crypto@npm:^5.1.7, @libp2p/crypto@npm:^5.1.9": version: 5.1.14 resolution: "@libp2p/crypto@npm:5.1.14" dependencies: @@ -1561,21 +1552,6 @@ __metadata: languageName: node linkType: hard -"@libp2p/crypto@npm:^5.1.14, @libp2p/crypto@npm:^5.1.17, @libp2p/crypto@npm:^5.1.7, @libp2p/crypto@npm:^5.1.9": - version: 5.1.17 - resolution: "@libp2p/crypto@npm:5.1.17" - dependencies: - "@libp2p/interface": "npm:^3.2.2" - "@noble/curves": "npm:^2.0.1" - "@noble/hashes": "npm:^2.0.1" - multiformats: "npm:^13.4.0" - protons-runtime: "npm:^6.0.1" - uint8arraylist: "npm:^2.4.8" - uint8arrays: "npm:^5.1.0" - checksum: 10/1f00ca9998fc598689fd0450c685db5740db91f8843d22950480d6f3df16c7bc61bf1f99bc9f064dfbf1b801e7644d5663a401e0eef6615413c665433c1899b9 - languageName: node - linkType: hard - "@libp2p/identify@npm:4.0.14": version: 4.0.14 resolution: "@libp2p/identify@npm:4.0.14" @@ -1599,14 +1575,14 @@ __metadata: linkType: hard "@libp2p/interface-internal@npm:^3.0.14": - version: 3.1.3 - resolution: "@libp2p/interface-internal@npm:3.1.3" + version: 3.0.14 + resolution: "@libp2p/interface-internal@npm:3.0.14" dependencies: - "@libp2p/interface": "npm:^3.2.2" - "@libp2p/peer-collections": "npm:^7.0.18" + "@libp2p/interface": "npm:^3.1.1" + "@libp2p/peer-collections": "npm:^7.0.14" "@multiformats/multiaddr": "npm:^13.0.1" progress-events: "npm:^1.0.1" - checksum: 10/5e115ce0d2151f364430ad177162b3947f3dd2fc165d10b205d58ba6b71111ffddd29626b81cc771c97323f5c4db6d631ec46750fa1fb31854158eb51577cf34 + checksum: 10/bbac479d193af0ab175d49d0041376c040c42c870f69d3e479cf617d24d5aa94aab736b7ebcdc50464bd0b4886e3992a36dd638ba67183895bd3451f41875ae3 languageName: node linkType: hard @@ -1625,60 +1601,60 @@ __metadata: linkType: hard "@libp2p/keychain@npm:^6.0.11": - version: 6.1.0 - resolution: "@libp2p/keychain@npm:6.1.0" + version: 6.0.11 + resolution: "@libp2p/keychain@npm:6.0.11" dependencies: - "@libp2p/crypto": "npm:^5.1.17" - "@libp2p/interface": "npm:^3.2.2" + "@libp2p/crypto": "npm:^5.1.14" + "@libp2p/interface": "npm:^3.1.1" "@noble/hashes": "npm:^2.0.1" asn1js: "npm:^3.0.6" interface-datastore: "npm:^9.0.1" multiformats: "npm:^13.4.0" sanitize-filename: "npm:^1.6.3" uint8arrays: "npm:^5.1.0" - checksum: 10/0f859eeade21b2a741228f57cae54c1eb91391fbd187a9d1c5c1fadad0306c1d17fa6c1d6c7cce88343366e035019facdc581733776ca5c325020149d1e95420 + checksum: 10/4e7465497fdd160d12ba2a2bd3640a6b0aeeecd7e76f9296b043635fc2525d35b802f349717af2a8db97663867ce9087d0d233da8b95b0e660bfa318f9743f6d languageName: node linkType: hard -"@libp2p/logger@npm:^6.2.3, @libp2p/logger@npm:^6.2.4, @libp2p/logger@npm:^6.2.6": - version: 6.2.6 - resolution: "@libp2p/logger@npm:6.2.6" +"@libp2p/logger@npm:^6.0.0, @libp2p/logger@npm:^6.2.3": + version: 6.2.3 + resolution: "@libp2p/logger@npm:6.2.3" dependencies: - "@libp2p/interface": "npm:^3.2.2" + "@libp2p/interface": "npm:^3.1.1" "@multiformats/multiaddr": "npm:^13.0.1" interface-datastore: "npm:^9.0.1" multiformats: "npm:^13.4.0" weald: "npm:^1.1.0" - checksum: 10/bba88dcce8f8db241f1fc0e205cea38a0b2cb068d23cb83e225e9aead2fb00da75dcc109397ee17c23114a7844bc8509243167af6d343d6b6ea2b47d5f6e6d79 + checksum: 10/a216365fd0a941ae045a9a223089ec996aeccaf5c37ec4262d3cf4f06a57edebb8bf3138ea882e480858e228252e258c223ee79672912744b6c0e3b538ef66f2 languageName: node linkType: hard "@libp2p/multistream-select@npm:^7.0.14": - version: 7.0.18 - resolution: "@libp2p/multistream-select@npm:7.0.18" + version: 7.0.14 + resolution: "@libp2p/multistream-select@npm:7.0.14" dependencies: - "@libp2p/interface": "npm:^3.2.2" - "@libp2p/utils": "npm:^7.1.0" + "@libp2p/interface": "npm:^3.1.1" + "@libp2p/utils": "npm:^7.0.14" it-length-prefixed: "npm:^10.0.1" uint8arraylist: "npm:^2.4.8" uint8arrays: "npm:^5.1.0" - checksum: 10/d7360076692def4beb382627d026a6d3c1074f28a74adacf4ea2fd13a426bcecfc0111264e03b7ad16af0ff865de05ae8253d6db9054ed30b457268438b6ca34 + checksum: 10/73d89bfde71f3ef9bf0c24af96d8062aa7b296d32a26f08c0da0f80172de1ea40054bb5cccc5adc1dfd7374e7aafff99e089b0a61d51b7efc5522b1fdddce037 languageName: node linkType: hard -"@libp2p/peer-collections@npm:^7.0.14, @libp2p/peer-collections@npm:^7.0.18": - version: 7.0.18 - resolution: "@libp2p/peer-collections@npm:7.0.18" +"@libp2p/peer-collections@npm:^7.0.14": + version: 7.0.14 + resolution: "@libp2p/peer-collections@npm:7.0.14" dependencies: - "@libp2p/interface": "npm:^3.2.2" - "@libp2p/peer-id": "npm:^6.0.8" - "@libp2p/utils": "npm:^7.1.0" + "@libp2p/interface": "npm:^3.1.1" + "@libp2p/peer-id": "npm:^6.0.5" + "@libp2p/utils": "npm:^7.0.14" multiformats: "npm:^13.4.0" - checksum: 10/0223f5b55618ee2d6b943bc6272bc2a1eaa77a98743a0e8c15d2b6de6a6f865ad6a307b7fb88624130c4cab286732e90f32c7f7f7f8d4f8b35c84df549c67532 + checksum: 10/5b71bcfdffadf7fdf241c0bf4346b6db133acc2049aafa86b24bcd7171bb6c607340b48abb9e57cc464f397c09b64aaa5b2ef83b715102f0b5296bce2fb322bb languageName: node linkType: hard -"@libp2p/peer-id@npm:6.0.5": +"@libp2p/peer-id@npm:6.0.5, @libp2p/peer-id@npm:^6.0.0, @libp2p/peer-id@npm:^6.0.4, @libp2p/peer-id@npm:^6.0.5": version: 6.0.5 resolution: "@libp2p/peer-id@npm:6.0.5" dependencies: @@ -1690,44 +1666,32 @@ __metadata: languageName: node linkType: hard -"@libp2p/peer-id@npm:^6.0.0, @libp2p/peer-id@npm:^6.0.4, @libp2p/peer-id@npm:^6.0.5, @libp2p/peer-id@npm:^6.0.8": - version: 6.0.8 - resolution: "@libp2p/peer-id@npm:6.0.8" - dependencies: - "@libp2p/crypto": "npm:^5.1.17" - "@libp2p/interface": "npm:^3.2.2" - multiformats: "npm:^13.4.0" - uint8arrays: "npm:^5.1.0" - checksum: 10/009c02bae10573adbf61159f6dd043ae21ae31e90c702cdd4b26a0b2e0fbcfd7f60f09a182e3bd9d14e3d56f78d9e1f5285c478496fd7617c89d08eeb78131f4 - languageName: node - linkType: hard - -"@libp2p/peer-record@npm:^9.0.6, @libp2p/peer-record@npm:^9.0.9": - version: 9.0.9 - resolution: "@libp2p/peer-record@npm:9.0.9" +"@libp2p/peer-record@npm:^9.0.6": + version: 9.0.6 + resolution: "@libp2p/peer-record@npm:9.0.6" dependencies: - "@libp2p/crypto": "npm:^5.1.17" - "@libp2p/interface": "npm:^3.2.2" - "@libp2p/peer-id": "npm:^6.0.8" + "@libp2p/crypto": "npm:^5.1.14" + "@libp2p/interface": "npm:^3.1.1" + "@libp2p/peer-id": "npm:^6.0.5" "@multiformats/multiaddr": "npm:^13.0.1" multiformats: "npm:^13.4.0" protons-runtime: "npm:^6.0.1" uint8-varint: "npm:^2.0.4" uint8arraylist: "npm:^2.4.8" uint8arrays: "npm:^5.1.0" - checksum: 10/153bd775f4b8f0e4bd03ea9c52139dc1f45b88a3cbaa0d1faadd5ae479232cd65154821657a2a008aeeb74622cd5e7cf648a420c15de3e829ad4f3d409547473 + checksum: 10/3b9560a5eacf8718b36e63b7c7b0ed82b52466e6c26964893de4851cccde5c24f46fb0733b2150dfadf757feacb9e2625da10fd513f1e7bd9203d0695a039ce4 languageName: node linkType: hard "@libp2p/peer-store@npm:^12.0.14": - version: 12.0.18 - resolution: "@libp2p/peer-store@npm:12.0.18" - dependencies: - "@libp2p/crypto": "npm:^5.1.17" - "@libp2p/interface": "npm:^3.2.2" - "@libp2p/peer-collections": "npm:^7.0.18" - "@libp2p/peer-id": "npm:^6.0.8" - "@libp2p/peer-record": "npm:^9.0.9" + version: 12.0.14 + resolution: "@libp2p/peer-store@npm:12.0.14" + dependencies: + "@libp2p/crypto": "npm:^5.1.14" + "@libp2p/interface": "npm:^3.1.1" + "@libp2p/peer-collections": "npm:^7.0.14" + "@libp2p/peer-id": "npm:^6.0.5" + "@libp2p/peer-record": "npm:^9.0.6" "@multiformats/multiaddr": "npm:^13.0.1" interface-datastore: "npm:^9.0.1" it-all: "npm:^3.0.9" @@ -1737,7 +1701,7 @@ __metadata: protons-runtime: "npm:^6.0.1" uint8arraylist: "npm:^2.4.8" uint8arrays: "npm:^5.1.0" - checksum: 10/041b75d0f7891413b182058b33f7c4c524ac02838c1e33a10ceff770ab55d43f3174b1bc6fb390f7ccf730d79ea17d11fa084523b2a794ec0a383ee965cedb09 + checksum: 10/39b79cf77dede2e10633f052860516ff1457e5dd4e68589a81051e7ad88f63583ab60fe6ef460cd40759858bc1b0e0c081a1f5f71ea27f959c00cbf02eb6b5b1 languageName: node linkType: hard @@ -1774,7 +1738,7 @@ __metadata: languageName: node linkType: hard -"@libp2p/utils@npm:7.0.14": +"@libp2p/utils@npm:7.0.14, @libp2p/utils@npm:^7.0.0, @libp2p/utils@npm:^7.0.11, @libp2p/utils@npm:^7.0.14": version: 7.0.14 resolution: "@libp2p/utils@npm:7.0.14" dependencies: @@ -1805,38 +1769,6 @@ __metadata: languageName: node linkType: hard -"@libp2p/utils@npm:^7.0.0, @libp2p/utils@npm:^7.0.11, @libp2p/utils@npm:^7.0.14, @libp2p/utils@npm:^7.1.0": - version: 7.1.0 - resolution: "@libp2p/utils@npm:7.1.0" - dependencies: - "@chainsafe/is-ip": "npm:^2.1.0" - "@chainsafe/netmask": "npm:^2.0.0" - "@libp2p/crypto": "npm:^5.1.17" - "@libp2p/interface": "npm:^3.2.2" - "@libp2p/logger": "npm:^6.2.6" - "@multiformats/multiaddr": "npm:^13.0.1" - "@sindresorhus/fnv1a": "npm:^3.1.0" - any-signal: "npm:^4.1.1" - cborg: "npm:^5.1.0" - delay: "npm:^7.0.0" - is-loopback-addr: "npm:^2.0.2" - it-length-prefixed: "npm:^10.0.1" - it-pipe: "npm:^3.0.1" - it-pushable: "npm:^3.2.3" - it-stream-types: "npm:^2.0.2" - main-event: "npm:^1.0.1" - netmask: "npm:^2.0.2" - p-defer: "npm:^4.0.1" - p-event: "npm:^7.0.0" - progress-events: "npm:^1.1.0" - race-signal: "npm:^2.0.0" - uint8-varint: "npm:^2.0.4" - uint8arraylist: "npm:^2.4.8" - uint8arrays: "npm:^5.1.0" - checksum: 10/04128e650dbcd5923e0d6d169906129bff8bce01d4d7c7db161b54d8167fc64bd6fd0590cd09ef3dec77a6638018ffd9fefe433b8c2d54620f5366886f325fcb - languageName: node - linkType: hard - "@libp2p/webrtc@npm:6.0.15": version: 6.0.15 resolution: "@libp2p/webrtc@npm:6.0.15" @@ -1966,11 +1898,10 @@ __metadata: languageName: node linkType: hard -"@metamask/auto-changelog@npm:^5.3.0": - version: 5.3.0 - resolution: "@metamask/auto-changelog@npm:5.3.0" +"@metamask/auto-changelog@npm:^4.0.0": + version: 4.1.0 + resolution: "@metamask/auto-changelog@npm:4.1.0" dependencies: - "@octokit/rest": "npm:^20.0.0" diff: "npm:^5.0.0" execa: "npm:^5.1.1" semver: "npm:^7.3.5" @@ -1978,14 +1909,14 @@ __metadata: peerDependencies: prettier: ">=3.0.0" bin: - auto-changelog: dist/cli.mjs - checksum: 10/5381c2b1efbade000bafbbee7b1becbee1787b9f24849352d16ddd3b14f511f865b3478250301f3d22f98fe0208690f62f166a476e64d38ed58361d816a673b6 + auto-changelog: dist/cli.js + checksum: 10/fe31a9eb364939c83bc5098482b761ca93593081680c4cba17b221150b4d32636cb25fd708e3692c198feddc95d8bcf524e19fa93567fb5aa30b03ea93249250 languageName: node linkType: hard -"@metamask/auto-changelog@npm:^6.1.0": - version: 6.1.0 - resolution: "@metamask/auto-changelog@npm:6.1.0" +"@metamask/auto-changelog@npm:^5.3.0": + version: 5.3.0 + resolution: "@metamask/auto-changelog@npm:5.3.0" dependencies: "@octokit/rest": "npm:^20.0.0" diff: "npm:^5.0.0" @@ -1993,16 +1924,10 @@ __metadata: semver: "npm:^7.3.5" yargs: "npm:^17.0.1" peerDependencies: - oxfmt: ^0.45.0 prettier: ">=3.0.0" - peerDependenciesMeta: - oxfmt: - optional: true - prettier: - optional: true bin: auto-changelog: dist/cli.mjs - checksum: 10/d4b086ea609f1395e111d8d124d74ac94e31a872f26eba5b65906f6e634f61e328264c940e7a603cb823d58a7140f8eeafec4521b85ee6584917e2c5ac90b684 + checksum: 10/5381c2b1efbade000bafbbee7b1becbee1787b9f24849352d16ddd3b14f511f865b3478250301f3d22f98fe0208690f62f166a476e64d38ed58361d816a673b6 languageName: node linkType: hard @@ -2039,11 +1964,11 @@ __metadata: linkType: hard "@metamask/create-release-branch@npm:^4.1.4": - version: 4.2.0 - resolution: "@metamask/create-release-branch@npm:4.2.0" + version: 4.1.4 + resolution: "@metamask/create-release-branch@npm:4.1.4" dependencies: "@metamask/action-utils": "npm:^1.0.0" - "@metamask/auto-changelog": "npm:^6.1.0" + "@metamask/auto-changelog": "npm:^4.0.0" "@metamask/utils": "npm:^9.0.0" debug: "npm:^4.3.4" execa: "npm:^8.0.1" @@ -2056,16 +1981,10 @@ __metadata: yaml: "npm:^2.2.2" yargs: "npm:^17.7.1" peerDependencies: - oxfmt: ^0.45.0 prettier: ">=3.0.0" - peerDependenciesMeta: - oxfmt: - optional: true - prettier: - optional: true bin: create-release-branch: bin/create-release-branch.js - checksum: 10/84a1aab8efd7da0fc94c880d98988b555b4c4fcea5ea0bf8f4b3ea8aac37dd0c2b1b7d7547fe1f8228710538769188c07fd761ffde2967562f043968cf1ed12c + checksum: 10/91282f9f20f576332bd88771988e58739e1dc7088068c74d54c5a3910bdab2e74c1f75b2205bdfa59a114dd18329d1080e04aada344b671348017c021edc82bc languageName: node linkType: hard @@ -2235,17 +2154,16 @@ __metadata: linkType: hard "@metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.2.0, @metamask/json-rpc-engine@npm:^10.2.4": - version: 10.3.0 - resolution: "@metamask/json-rpc-engine@npm:10.3.0" + version: 10.2.4 + resolution: "@metamask/json-rpc-engine@npm:10.2.4" dependencies: - "@metamask/messenger": "npm:^1.2.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^11.9.0" "@types/deep-freeze-strict": "npm:^1.1.0" deep-freeze-strict: "npm:^1.1.1" klona: "npm:^2.0.6" - checksum: 10/8d4da5d933e4be2a85783871b6f1282763cbb5bc559e3228da099c75517530e3ac42a040109f17a4d4ff768f1c8cbcc4358f5e06b820b893af29a13f95180bd6 + checksum: 10/b207dd2a9a44674c141c2e027c082974464a37beada98a27e80fe59c9bd44e2c2a992edf8a8d7e3ed461fa27ed372c95d4e27df18752b558c10bf540b7fe7bcd languageName: node linkType: hard @@ -2837,9 +2755,9 @@ __metadata: languageName: unknown linkType: soft -"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@npm:^1.2.0": - version: 1.2.0 - resolution: "@metamask/messenger@npm:1.2.0" +"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.1": + version: 1.1.1 + resolution: "@metamask/messenger@npm:1.1.1" dependencies: "@metamask/utils": "npm:^11.9.0" yargs: "npm:^17.7.2" @@ -2847,7 +2765,7 @@ __metadata: typescript: ">=5.0.0" bin: messenger-generate-action-types: ./dist/generate-action-types/cli.mjs - checksum: 10/6818e4609d6162a436cc07955905f9e57ff6dbef841e9066a5fb9cc0538e981526fbcb5eef1fa1968d79212d57ddda2fce4dda5f87eb64d8d98f7db1216a6a98 + checksum: 10/a959af95e9e117aa0f7ad1c280f7817fef2c0b575c76837b1a6c884c9c9ef1dd0faeaef0c2c0c2035f68c7638d1f87cd172956ee962dec97d8ab6176fa6964e3 languageName: node linkType: hard @@ -3161,6 +3079,48 @@ __metadata: languageName: unknown linkType: soft +"@metamask/sheaves@workspace:packages/sheaves": + version: 0.0.0-use.local + resolution: "@metamask/sheaves@workspace:packages/sheaves" + dependencies: + "@arethetypeswrong/cli": "npm:^0.17.4" + "@endo/eventual-send": "npm:^1.3.4" + "@endo/exo": "npm:^1.5.12" + "@endo/patterns": "npm:^1.7.0" + "@metamask/auto-changelog": "npm:^5.3.0" + "@metamask/eslint-config": "npm:^15.0.0" + "@metamask/eslint-config-nodejs": "npm:^15.0.0" + "@metamask/eslint-config-typescript": "npm:^15.0.0" + "@metamask/kernel-shims": "workspace:^" + "@metamask/kernel-utils": "workspace:^" + "@ocap/repo-tools": "workspace:^" + "@ts-bridge/cli": "npm:^0.6.3" + "@ts-bridge/shims": "npm:^0.1.1" + "@typescript-eslint/eslint-plugin": "npm:^8.29.0" + "@typescript-eslint/parser": "npm:^8.29.0" + "@typescript-eslint/utils": "npm:^8.29.0" + "@vitest/eslint-plugin": "npm:^1.6.14" + depcheck: "npm:^1.4.7" + eslint: "npm:^9.23.0" + eslint-config-prettier: "npm:^10.1.1" + eslint-import-resolver-typescript: "npm:^4.3.1" + eslint-plugin-import-x: "npm:^4.10.0" + eslint-plugin-jsdoc: "npm:^50.6.9" + eslint-plugin-n: "npm:^17.17.0" + eslint-plugin-prettier: "npm:^5.2.6" + eslint-plugin-promise: "npm:^7.2.1" + prettier: "npm:^3.5.3" + rimraf: "npm:^6.0.1" + ses: "npm:^1.14.0" + turbo: "npm:^2.9.1" + typedoc: "npm:^0.28.1" + typescript: "npm:~5.8.2" + typescript-eslint: "npm:^8.29.0" + vite: "npm:^8.0.6" + vitest: "npm:^4.1.3" + languageName: unknown + linkType: soft + "@metamask/slip44@npm:^4.4.0": version: 4.4.0 resolution: "@metamask/slip44@npm:4.4.0" @@ -3435,11 +3395,11 @@ __metadata: linkType: hard "@multiformats/multiaddr-matcher@npm:^3.0.1": - version: 3.0.2 - resolution: "@multiformats/multiaddr-matcher@npm:3.0.2" + version: 3.0.1 + resolution: "@multiformats/multiaddr-matcher@npm:3.0.1" dependencies: "@multiformats/multiaddr": "npm:^13.0.0" - checksum: 10/7758f76e51caff5d9da0df94ceedea705d1fde3ee60a735e169325558ba2750acabb50cda7d449b10eeaec0a175967a405f0026ee884726c38cdfe7be06dee12 + checksum: 10/4778cd268b7acb604d9edbe4316dd68e648e5711a5b35cdb0db181e28d7dc75263331e7fce2fb43c20977bea059df77542ab18dd2817f42e35e61723794e0421 languageName: node linkType: hard @@ -3475,15 +3435,15 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.4": - version: 1.1.4 - resolution: "@napi-rs/wasm-runtime@npm:1.1.4" +"@napi-rs/wasm-runtime@npm:^1.1.2": + version: 1.1.2 + resolution: "@napi-rs/wasm-runtime@npm:1.1.2" dependencies: "@tybys/wasm-util": "npm:^0.10.1" peerDependencies: "@emnapi/core": ^1.7.1 "@emnapi/runtime": ^1.7.1 - checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af + checksum: 10/fcb8a5cff65dfb6c44277a1f7a16da5a1be2ed609c83e13f4bb621db97b511129b8ccf808794c8906abd3561e10c2e66d3ba550f0a1a0db18f53f1e399a0a5f8 languageName: node linkType: hard @@ -3495,9 +3455,9 @@ __metadata: linkType: hard "@noble/ciphers@npm:^2.0.1": - version: 2.2.0 - resolution: "@noble/ciphers@npm:2.2.0" - checksum: 10/d75348aa682b41ad3e24cdd0a56c6d9ca033fb629ab93f37d6690be41c4882359b27598a11af0f5439ba82df4f9e3875dea1f875064310f68fef63cf24e3481a + version: 2.1.1 + resolution: "@noble/ciphers@npm:2.1.1" + checksum: 10/efca189b2719ed7309616a6824328d713bd37156e1bcead453725e51fc93311fb2f54e4d46bef0391fa17ce18bd9c2364511290774504a8600264b572f6a1db5 languageName: node linkType: hard @@ -4641,10 +4601,10 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.127.0": - version: 0.127.0 - resolution: "@oxc-project/types@npm:0.127.0" - checksum: 10/f154f4720367186aed63a16fb1395f9039d4e6872265fe9e6b5eacc02fb2b948f9ea6c5f85efd3a015ea28aa8c31232b7a8301218ae28651659e46dd0c4f2031 +"@oxc-project/types@npm:=0.123.0": + version: 0.123.0 + resolution: "@oxc-project/types@npm:0.123.0" + checksum: 10/9ce1df2b9cc43b64049c983567abf5369502eb4722742f883d080438ea0939df0c6c8a234067e5c033450315242984acf59bf3648dbaeef6cb524ceedd9965b2 languageName: node linkType: hard @@ -4799,129 +4759,129 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-cms@npm:^2.6.0, @peculiar/asn1-cms@npm:^2.7.0": - version: 2.7.0 - resolution: "@peculiar/asn1-cms@npm:2.7.0" +"@peculiar/asn1-cms@npm:^2.6.0, @peculiar/asn1-cms@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-cms@npm:2.6.1" dependencies: - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/asn1-x509": "npm:^2.7.0" - "@peculiar/asn1-x509-attr": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-x509-attr": "npm:^2.6.1" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/01515f46db97b1d5f8b0d2c181f10571efb91e11e0c034b49121c8f50ff76d6538eba04311d04347f180639a72818f3ac1c96a089eea9a1689e5cc5ee31a4117 + checksum: 10/e431f6229b98c63a929538d266488e8c2dddc895936117da8f9ec775558e08c20ded6a4adcca4bb88bfea282e7204d4f6bba7a46da2cced162c174e1e6964f36 languageName: node linkType: hard "@peculiar/asn1-csr@npm:^2.6.0": - version: 2.7.0 - resolution: "@peculiar/asn1-csr@npm:2.7.0" + version: 2.6.1 + resolution: "@peculiar/asn1-csr@npm:2.6.1" dependencies: - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/asn1-x509": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/d966d58d0104a6833b442080ef2663172730fd95928b95c5a84f5b86963eeeaa13435ee8e8d97612a3a1fef532c714b868ceb082a6f253186606efecfaa88a0c + checksum: 10/4ac2f1c3a2cb392fcdd5aa602140abe90f849af0a9e8296aab9aaf1712ee2e0c4f5fa86b0fe83975e771b0aba91fc848670f9c2008ea1e850c849fae6e181179 languageName: node linkType: hard "@peculiar/asn1-ecc@npm:^2.6.0": - version: 2.7.0 - resolution: "@peculiar/asn1-ecc@npm:2.7.0" + version: 2.6.1 + resolution: "@peculiar/asn1-ecc@npm:2.6.1" dependencies: - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/asn1-x509": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/c30736a867dd6facf7cf5090c9eace582b927c92cbc0dfd8787b3485e3012dc846b608d5d0c3c38df9e621e0c7fbe0bb2b96e7fa34c03b67f4b59fd3d6d4e8f4 + checksum: 10/baa646c1c86283d5876230b1cfbd80cf42f97b3bb8d8b23cd5830f6f8d6466e6a06887c6838f3c4c61c87df9ffd2abe905f555472e8e70d722ce964a8074d838 languageName: node linkType: hard -"@peculiar/asn1-pfx@npm:^2.7.0": - version: 2.7.0 - resolution: "@peculiar/asn1-pfx@npm:2.7.0" +"@peculiar/asn1-pfx@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-pfx@npm:2.6.1" dependencies: - "@peculiar/asn1-cms": "npm:^2.7.0" - "@peculiar/asn1-pkcs8": "npm:^2.7.0" - "@peculiar/asn1-rsa": "npm:^2.7.0" - "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-cms": "npm:^2.6.1" + "@peculiar/asn1-pkcs8": "npm:^2.6.1" + "@peculiar/asn1-rsa": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.6.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/ced7e9c4308d427ae9107df654f28b6e28b1bc963b059e1430459c49cee8d8da74338d251ac98d1af5efc98a0ff49fbec9f71073cdbf5e0ee7f93a40ba043f7e + checksum: 10/50adc7db96928d98b85a1a2e6765ba1d4ec708f937b8172ea6a22e3b92137ea36d656aded64b3be661db39f924102c5a80da54ee647e2441af3bc19c55a183ef languageName: node linkType: hard -"@peculiar/asn1-pkcs8@npm:^2.7.0": - version: 2.7.0 - resolution: "@peculiar/asn1-pkcs8@npm:2.7.0" +"@peculiar/asn1-pkcs8@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-pkcs8@npm:2.6.1" dependencies: - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/asn1-x509": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/9e8049e5fd9e35676c313ff0b88decb767c4d7ff09a0ab098ea1affa4b5076573b4e10fcd7968b1931b921b3107ede521c1b1bf91a1040ce454e6b44f32f3aa8 + checksum: 10/99c4326da30e7ef17bb8e92d8a9525b78c101e4d743493000e220f3da6bbc4755371f1dbcc2a36951fb15769c2efead20d90a08918fd268c21bebcac26e71053 languageName: node linkType: hard "@peculiar/asn1-pkcs9@npm:^2.6.0": - version: 2.7.0 - resolution: "@peculiar/asn1-pkcs9@npm:2.7.0" - dependencies: - "@peculiar/asn1-cms": "npm:^2.7.0" - "@peculiar/asn1-pfx": "npm:^2.7.0" - "@peculiar/asn1-pkcs8": "npm:^2.7.0" - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/asn1-x509": "npm:^2.7.0" - "@peculiar/asn1-x509-attr": "npm:^2.7.0" + version: 2.6.1 + resolution: "@peculiar/asn1-pkcs9@npm:2.6.1" + dependencies: + "@peculiar/asn1-cms": "npm:^2.6.1" + "@peculiar/asn1-pfx": "npm:^2.6.1" + "@peculiar/asn1-pkcs8": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-x509-attr": "npm:^2.6.1" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/61b55f4b98e98ca659bb6ad5f043ca2fc9aa1c333372d91419251180d4332746aaed0f15975b6d9a131af3b6b066b90705b54b508c6854b6f2117fd3ab66a7a8 + checksum: 10/61759a50d6adf108a0376735b2e76cdfc9c41db39a7abed23ca332f7699d831aa6324534aa38153018a31e6ee5e8fef85534c92b68067f6afcb90787e953c449 languageName: node linkType: hard -"@peculiar/asn1-rsa@npm:^2.6.0, @peculiar/asn1-rsa@npm:^2.7.0": - version: 2.7.0 - resolution: "@peculiar/asn1-rsa@npm:2.7.0" +"@peculiar/asn1-rsa@npm:^2.6.0, @peculiar/asn1-rsa@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-rsa@npm:2.6.1" dependencies: - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/asn1-x509": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/b314b77246a7bee0a2a76978ca08983a016afad78c74dc02b5ae6296f8a71f0146c521d431a9d6e9ba7faec42b5a8bba046ee4c9010cbd1b37067398e05e05af + checksum: 10/e91efe57017feac71c69ee5950e9c323b45aaf10baa32153fe88f237948f9d906ba04c645d085c4293c90440cad95392a91b3760251cd0ebc8e4c1a383fc331a languageName: node linkType: hard -"@peculiar/asn1-schema@npm:^2.3.13, @peculiar/asn1-schema@npm:^2.3.8, @peculiar/asn1-schema@npm:^2.6.0, @peculiar/asn1-schema@npm:^2.7.0": - version: 2.7.0 - resolution: "@peculiar/asn1-schema@npm:2.7.0" +"@peculiar/asn1-schema@npm:^2.3.13, @peculiar/asn1-schema@npm:^2.3.8, @peculiar/asn1-schema@npm:^2.6.0": + version: 2.6.0 + resolution: "@peculiar/asn1-schema@npm:2.6.0" dependencies: - "@peculiar/utils": "npm:^2.0.2" asn1js: "npm:^3.0.6" + pvtsutils: "npm:^1.3.6" tslib: "npm:^2.8.1" - checksum: 10/2b18ee2f3de2b68a36b964721e5101f589d6a1db765c450ce5a929829bfc8c0819e0b128145f65639952b257b9bdaa6ce7d1a54cd93c7bf6e694fed4c36d6c98 + checksum: 10/af9b1094d0e020f0fd828777488578322d62a41f597ead7d80939dafcfe35b672fcb0ec7460ef66b2a155f9614d4340a98896d417a830aff1685cb4c21d5bbe4 languageName: node linkType: hard -"@peculiar/asn1-x509-attr@npm:^2.7.0": - version: 2.7.0 - resolution: "@peculiar/asn1-x509-attr@npm:2.7.0" +"@peculiar/asn1-x509-attr@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-x509-attr@npm:2.6.1" dependencies: - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/asn1-x509": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-x509": "npm:^2.6.1" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/7a1c4f707224cf8ebf33bb231049069cf6a64902d7b7b317b4a2f4f8a0fb606bb39f6af5944d408c4eb930e8ae1435c0049cc42490131cf991383714c179f1dd + checksum: 10/86f7d5495459dee81daadd830ebb7d26ec15a98f6479c88b90a915ac9f28105b0d5003ba0c382b4aa8f7fa42e399f7cc37e4fe73c26cbaacd47e63a50b132e25 languageName: node linkType: hard -"@peculiar/asn1-x509@npm:^2.6.0, @peculiar/asn1-x509@npm:^2.7.0": - version: 2.7.0 - resolution: "@peculiar/asn1-x509@npm:2.7.0" +"@peculiar/asn1-x509@npm:^2.6.0, @peculiar/asn1-x509@npm:^2.6.1": + version: 2.6.1 + resolution: "@peculiar/asn1-x509@npm:2.6.1" dependencies: - "@peculiar/asn1-schema": "npm:^2.7.0" - "@peculiar/utils": "npm:^2.0.2" + "@peculiar/asn1-schema": "npm:^2.6.0" asn1js: "npm:^3.0.6" + pvtsutils: "npm:^1.3.6" tslib: "npm:^2.8.1" - checksum: 10/6e6b1124076487e46d1b9f7237f173bc7aab92230e3a7a8b3841fdc84009ece0221624bd88fe16a478aec5b4ba21a9393735038ca4e38245d7f0c1be91f00e8c + checksum: 10/e3187ad04d397cdd6a946895a51202b67f57992dfef55e40acc7e7ea325e2854267ed2581c4b1ea729d7147e9e8e6f34af77f1ffb48e3e8b25b2216b213b4641 languageName: node linkType: hard @@ -4934,15 +4894,6 @@ __metadata: languageName: node linkType: hard -"@peculiar/utils@npm:^2.0.2": - version: 2.0.3 - resolution: "@peculiar/utils@npm:2.0.3" - dependencies: - tslib: "npm:^2.8.1" - checksum: 10/e6b212db06e15f0ffa33482336f0e41108ce2d95fa69fa2c6f001120df1056404dc007b62bc6c90cc58d743f0cf4b23bfff89cdb8c121415d36ff0f9d60aead2 - languageName: node - linkType: hard - "@peculiar/webcrypto@npm:^1.5.0": version: 1.5.0 resolution: "@peculiar/webcrypto@npm:1.5.0" @@ -5042,111 +4993,111 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.17" +"@rolldown/binding-android-arm64@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.13" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.17" +"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.13" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.17" +"@rolldown/binding-darwin-x64@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.13" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.17" +"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.13" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.17" +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.13" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.13" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.17" +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.13" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.13" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.13" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.13" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.17" +"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.13" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.17" +"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.13" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.17" +"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.13" dependencies: - "@emnapi/core": "npm:1.10.0" - "@emnapi/runtime": "npm:1.10.0" - "@napi-rs/wasm-runtime": "npm:^1.1.4" + "@emnapi/core": "npm:1.9.1" + "@emnapi/runtime": "npm:1.9.1" + "@napi-rs/wasm-runtime": "npm:^1.1.2" conditions: cpu=wasm32 languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.17" +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.13" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.17" +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.13" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5158,10 +5109,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/pluginutils@npm:1.0.0-rc.17" - checksum: 10/d659ea756ee6d360a015708d1035c07047e08db99a4160c74c7f22a7ece5611efcc18ad56db4a63b69edb506ded47596d9c0d301919242470d8c412d916b9750 +"@rolldown/pluginutils@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.13" + checksum: 10/ffc6cdfac897c3fb7c5544560d2aaf2bd55acfb4f082295844a899bdccdd0c7baa11e27fe2b806427bd636d43aa2b22b9ec6b837bab9f416d1e29c4dd4c52516 languageName: node linkType: hard @@ -5511,54 +5462,54 @@ __metadata: languageName: node linkType: hard -"@turbo/darwin-64@npm:2.9.9": - version: 2.9.9 - resolution: "@turbo/darwin-64@npm:2.9.9" +"@turbo/darwin-64@npm:2.9.1": + version: 2.9.1 + resolution: "@turbo/darwin-64@npm:2.9.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@turbo/darwin-arm64@npm:2.9.9": - version: 2.9.9 - resolution: "@turbo/darwin-arm64@npm:2.9.9" +"@turbo/darwin-arm64@npm:2.9.1": + version: 2.9.1 + resolution: "@turbo/darwin-arm64@npm:2.9.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@turbo/linux-64@npm:2.9.9": - version: 2.9.9 - resolution: "@turbo/linux-64@npm:2.9.9" +"@turbo/linux-64@npm:2.9.1": + version: 2.9.1 + resolution: "@turbo/linux-64@npm:2.9.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@turbo/linux-arm64@npm:2.9.9": - version: 2.9.9 - resolution: "@turbo/linux-arm64@npm:2.9.9" +"@turbo/linux-arm64@npm:2.9.1": + version: 2.9.1 + resolution: "@turbo/linux-arm64@npm:2.9.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@turbo/windows-64@npm:2.9.9": - version: 2.9.9 - resolution: "@turbo/windows-64@npm:2.9.9" +"@turbo/windows-64@npm:2.9.1": + version: 2.9.1 + resolution: "@turbo/windows-64@npm:2.9.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@turbo/windows-arm64@npm:2.9.9": - version: 2.9.9 - resolution: "@turbo/windows-arm64@npm:2.9.9" +"@turbo/windows-arm64@npm:2.9.1": + version: 2.9.1 + resolution: "@turbo/windows-arm64@npm:2.9.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@tybys/wasm-util@npm:^0.10.0, @tybys/wasm-util@npm:^0.10.1": - version: 0.10.2 - resolution: "@tybys/wasm-util@npm:0.10.2" + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10/d12f1dafe12d7a573c406b35ffef0038042b9cc9fbcc74d657267eb635499b956276afc05eebdbd81bea582e1c4c921421a1dd7243a93daaa8c8216b19395c23 + checksum: 10/7fe0d239397aebb002ac4855d30c197c06a05ea8df8511350a3a5b1abeefe26167c60eda8a5508337571161e4c4b53d7c1342296123f9607af8705369de9fa7f languageName: node linkType: hard @@ -5949,16 +5900,16 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/project-service@npm:8.59.2" +"@typescript-eslint/project-service@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/project-service@npm:8.58.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.59.2" - "@typescript-eslint/types": "npm:^8.59.2" + "@typescript-eslint/tsconfig-utils": "npm:^8.58.0" + "@typescript-eslint/types": "npm:^8.58.0" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/768d311bdf366519549a3806b16eb3be030328b7cda9882e60ea2a6c112111a531ef94289ec88225b70ca61d2071f1bddf2c5faa841a837d44992c918d198d7b + checksum: 10/fab2601f76b2df61b09e3b7ff364d0e17e6d80e65e84e8a8d11f6a0813748bed3912da098659d00f46b1f277d462bd7529157182b72b5e2e0b41ee6176a0edd7 languageName: node linkType: hard @@ -5982,13 +5933,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.59.2, @typescript-eslint/scope-manager@npm:^8.58.0": - version: 8.59.2 - resolution: "@typescript-eslint/scope-manager@npm:8.59.2" +"@typescript-eslint/scope-manager@npm:8.58.0, @typescript-eslint/scope-manager@npm:^8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/scope-manager@npm:8.58.0" dependencies: - "@typescript-eslint/types": "npm:8.59.2" - "@typescript-eslint/visitor-keys": "npm:8.59.2" - checksum: 10/9a63eb5d4ae26235ce2d3348eb45ff0e1a8cc7b198f622c48e921c7bfe0f1f7fa29a8cd3856547a4d42c08b7ed334315c682a714cb3b8b62841b5d59a1d08fd4 + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" + checksum: 10/97293f1215faa785a3c1ee8d630591db9dcd5fb6bdcdd0b2e818c80478d41e59a05003fb33000530780dc466fb8cf662352932080ee7406c4aaac72af4000541 languageName: node linkType: hard @@ -6001,12 +5952,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.59.2, @typescript-eslint/tsconfig-utils@npm:^8.38.0, @typescript-eslint/tsconfig-utils@npm:^8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.59.2" +"@typescript-eslint/tsconfig-utils@npm:8.58.0, @typescript-eslint/tsconfig-utils@npm:^8.38.0, @typescript-eslint/tsconfig-utils@npm:^8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/42479906a01469322d22e8d45c6200998382f19c1c2dcb59d6adb2e796238a0476f1aa8fd1a1b2c3b36c0c7aa77ebb72ffc958bd11b6efadd36cd175646d13de + checksum: 10/4f47212c0e26e6b06e97044ec5e483007d5145ef6b205393a0b43cbc0b385c75c14ba5749d01cf7d1ff100332c2cf1d336f060f7d2191bb67fb892bb4446afaa languageName: node linkType: hard @@ -6055,10 +6006,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.59.2, @typescript-eslint/types@npm:^8.38.0, @typescript-eslint/types@npm:^8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/types@npm:8.59.2" - checksum: 10/dc828a5c50debac37047a30ec5bfdc21e2b410c7c8c517c1ab01164fa9a0197f4f6b829f502dd992d21044442277029bfacf0c0b70d7ac9446977cbc8d375e13 +"@typescript-eslint/types@npm:8.58.0, @typescript-eslint/types@npm:^8.38.0, @typescript-eslint/types@npm:^8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/types@npm:8.58.0" + checksum: 10/c68eac0bc25812fdbb2ed4a121e42bfca9f24f3c6be95f6a9c4e7b9af767f1bcfacd6d496e358166143e0a1801dc7d042ce1b5e69946ac2768d9114ff6b8d375 languageName: node linkType: hard @@ -6100,14 +6051,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/typescript-estree@npm:8.59.2" +"@typescript-eslint/typescript-estree@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.58.0" dependencies: - "@typescript-eslint/project-service": "npm:8.59.2" - "@typescript-eslint/tsconfig-utils": "npm:8.59.2" - "@typescript-eslint/types": "npm:8.59.2" - "@typescript-eslint/visitor-keys": "npm:8.59.2" + "@typescript-eslint/project-service": "npm:8.58.0" + "@typescript-eslint/tsconfig-utils": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/visitor-keys": "npm:8.58.0" debug: "npm:^4.4.3" minimatch: "npm:^10.2.2" semver: "npm:^7.7.3" @@ -6115,7 +6066,7 @@ __metadata: ts-api-utils: "npm:^2.5.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/54a2689e5c08f35364214a542e328745401951e94526c9f95d68b14c57521e9aade1e946074a02ed2c9cc95e94fc1866c3f725f820263759a1ee2072e3ed146f + checksum: 10/4d6c4175e8a4d5c097393d161016836cc322f090c3f69fd751f5bbc25afce64df9ea0c97cee8b36ac060e06dc2cca2a4de7a0c7e04e19727cc4bd98ab3291fed languageName: node linkType: hard @@ -6150,17 +6101,17 @@ __metadata: linkType: hard "@typescript-eslint/utils@npm:^8.29.0, @typescript-eslint/utils@npm:^8.30.1, @typescript-eslint/utils@npm:^8.58.0": - version: 8.59.2 - resolution: "@typescript-eslint/utils@npm:8.59.2" + version: 8.58.0 + resolution: "@typescript-eslint/utils@npm:8.58.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.59.2" - "@typescript-eslint/types": "npm:8.59.2" - "@typescript-eslint/typescript-estree": "npm:8.59.2" + "@typescript-eslint/scope-manager": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/typescript-estree": "npm:8.58.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/4e157a18b28d656b13ae07583765cc871d992abad0ae0aeb2cde819dd632d62b89da9f9e468dfefead18b9440aa2b9040ca36841525dff4ea97479583114afe0 + checksum: 10/936433b761a990147612d78bb4afc79244239541b4a4061fbbc2de1810b40ec7f78eb4e9181e5d9c5ab7acbd9bf49fc6195dbb1d823370f717f07ad492ad6c7e languageName: node linkType: hard @@ -6184,13 +6135,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.59.2": - version: 8.59.2 - resolution: "@typescript-eslint/visitor-keys@npm:8.59.2" +"@typescript-eslint/visitor-keys@npm:8.58.0": + version: 8.58.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.58.0" dependencies: - "@typescript-eslint/types": "npm:8.59.2" + "@typescript-eslint/types": "npm:8.58.0" eslint-visitor-keys: "npm:^5.0.0" - checksum: 10/ec8d797272c12b53b9eb2b508c326823b2cb17f6bcf57606238b812bb73854675919a5e772a42499ec1ac7787a16d018fffd94e6b168cc0b58a9872b16d6f1da + checksum: 10/50b0779e19079dedf3723323a4dfa398c639b3da48f2fcf071c22ca69342e03592f1726d68ea59b9b5a51f14ab112eabc5c93fd2579c84b02a3320042ae20066 languageName: node linkType: hard @@ -6346,46 +6297,46 @@ __metadata: linkType: hard "@vitest/browser-playwright@npm:^4.1.3": - version: 4.1.5 - resolution: "@vitest/browser-playwright@npm:4.1.5" + version: 4.1.3 + resolution: "@vitest/browser-playwright@npm:4.1.3" dependencies: - "@vitest/browser": "npm:4.1.5" - "@vitest/mocker": "npm:4.1.5" + "@vitest/browser": "npm:4.1.3" + "@vitest/mocker": "npm:4.1.3" tinyrainbow: "npm:^3.1.0" peerDependencies: playwright: "*" - vitest: 4.1.5 + vitest: 4.1.3 peerDependenciesMeta: playwright: optional: false - checksum: 10/a69c8e9f8efd3dc3e28a9faaa3656c9e5713f93093fbaeda2f9b268ea3c30de08d8c5fe28001e54a9adcb0d1c66f7298803f9451751c7cd5c4d6274b68f5555c + checksum: 10/d2b4fa81df2f220495c309804e0b91e4398f2d36475dd2d77a61f9627b3d35ce496ca8be260dae1bf8ea3eeb94ae3a704aad0b375e27295263a03c5188570946 languageName: node linkType: hard -"@vitest/browser@npm:4.1.5, @vitest/browser@npm:^4.1.3": - version: 4.1.5 - resolution: "@vitest/browser@npm:4.1.5" +"@vitest/browser@npm:4.1.3, @vitest/browser@npm:^4.1.3": + version: 4.1.3 + resolution: "@vitest/browser@npm:4.1.3" dependencies: "@blazediff/core": "npm:1.9.1" - "@vitest/mocker": "npm:4.1.5" - "@vitest/utils": "npm:4.1.5" + "@vitest/mocker": "npm:4.1.3" + "@vitest/utils": "npm:4.1.3" magic-string: "npm:^0.30.21" pngjs: "npm:^7.0.0" sirv: "npm:^3.0.2" tinyrainbow: "npm:^3.1.0" ws: "npm:^8.19.0" peerDependencies: - vitest: 4.1.5 - checksum: 10/830e4fff6eda823ad16e4336a67350aa1928ed5131d60290ec1e2dd0f5bc39431317047d550a415462ac0600d48c6684f9c2b4b3d3e48edd18adba080e3f1667 + vitest: 4.1.3 + checksum: 10/313424318a62628aa1773f8dad1d3e9cc24233eaa45fe9de8bb2098251ff7f667f934af3146394164aab6640180fce2d64e7c74430e93a354acf12272da541e5 languageName: node linkType: hard "@vitest/coverage-v8@npm:^4.1.3": - version: 4.1.5 - resolution: "@vitest/coverage-v8@npm:4.1.5" + version: 4.1.3 + resolution: "@vitest/coverage-v8@npm:4.1.3" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.1.5" + "@vitest/utils": "npm:4.1.3" ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" @@ -6395,18 +6346,18 @@ __metadata: std-env: "npm:^4.0.0-rc.1" tinyrainbow: "npm:^3.1.0" peerDependencies: - "@vitest/browser": 4.1.5 - vitest: 4.1.5 + "@vitest/browser": 4.1.3 + vitest: 4.1.3 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10/378e1d85a1c4670af15a18b544995a43d320460b418c188d7000f96518859e4537e00ea5e38a563c42b6183437252f0ecc92b471ede30c6d43ae87b7c8e09ed3 + checksum: 10/8353c9e1acd08654976ae92565459433b1f035752278dffb1a27207cdc5e60a8308616e8f2db4cfb72b29e718beccab9a4dce4c44084eeb34ba08b645a80f7ba languageName: node linkType: hard "@vitest/eslint-plugin@npm:^1.6.14": - version: 1.6.16 - resolution: "@vitest/eslint-plugin@npm:1.6.16" + version: 1.6.14 + resolution: "@vitest/eslint-plugin@npm:1.6.14" dependencies: "@typescript-eslint/scope-manager": "npm:^8.58.0" "@typescript-eslint/utils": "npm:^8.58.0" @@ -6422,29 +6373,29 @@ __metadata: optional: true vitest: optional: true - checksum: 10/422fdc9a80ad88adcd7b1e07fc88866784e57a00e562e1711988264f8912850c5ebf0efec815280b5f6258fcb481133c9fd2c32d04a82ff429e9b976aa2b04db + checksum: 10/78b9129dad8c6c81f7a417e6d4e00198cb237d21b2604ac7b7311a8c419a2b32d0d327c4874aa6ae75106ffdc63309385fd5a8e7c2d066be28761a82057e2719 languageName: node linkType: hard -"@vitest/expect@npm:4.1.5": - version: 4.1.5 - resolution: "@vitest/expect@npm:4.1.5" +"@vitest/expect@npm:4.1.3": + version: 4.1.3 + resolution: "@vitest/expect@npm:4.1.3" dependencies: "@standard-schema/spec": "npm:^1.1.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.5" - "@vitest/utils": "npm:4.1.5" + "@vitest/spy": "npm:4.1.3" + "@vitest/utils": "npm:4.1.3" chai: "npm:^6.2.2" tinyrainbow: "npm:^3.1.0" - checksum: 10/3e94d2d0cf4f7018ed6a7a9394bff971353ea0cc85bcbcff39212279156840b8c533be99e2fd52112e4904c4a5190bdaaf441db7c6b17e356c18577072a3f057 + checksum: 10/1fdd2e772674ceed7229e34ceb4ea119ccc7f9f3529b444e9d1f8255163515051fda5111615affd60ac2e82f65f2b7d414dec5e7e2979f930fac06d4a201d0f8 languageName: node linkType: hard -"@vitest/mocker@npm:4.1.5": - version: 4.1.5 - resolution: "@vitest/mocker@npm:4.1.5" +"@vitest/mocker@npm:4.1.3": + version: 4.1.3 + resolution: "@vitest/mocker@npm:4.1.3" dependencies: - "@vitest/spy": "npm:4.1.5" + "@vitest/spy": "npm:4.1.3" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -6455,7 +6406,7 @@ __metadata: optional: true vite: optional: true - checksum: 10/949784ba08996543a313459a36a730d4b0847e42ee56cfda07a3e2add67c7adf8acbd59dcf9f75b1e4bc3fe7cc487f9f260905ff9a334866d389478112e5ae82 + checksum: 10/55c1b39e7a1226ed54beefb31341240e937f271cfbe661b6894cb331739585010ad51a93a1ac1b9c4be70967696c65fce420385a8ea2ad3f829e3af3cc302670 languageName: node linkType: hard @@ -6468,15 +6419,6 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:4.1.5": - version: 4.1.5 - resolution: "@vitest/pretty-format@npm:4.1.5" - dependencies: - tinyrainbow: "npm:^3.1.0" - checksum: 10/783f8c4a0e419d1024446ae8593411c95443ea09b50c4a378986b48893998acda34429b2d1deebc065405a7ef40bb19e19c68fdeb93acd46ae98b156c42d5f39 - languageName: node - linkType: hard - "@vitest/runner@npm:4.1.3": version: 4.1.3 resolution: "@vitest/runner@npm:4.1.3" @@ -6487,32 +6429,22 @@ __metadata: languageName: node linkType: hard -"@vitest/runner@npm:4.1.5": - version: 4.1.5 - resolution: "@vitest/runner@npm:4.1.5" - dependencies: - "@vitest/utils": "npm:4.1.5" - pathe: "npm:^2.0.3" - checksum: 10/ba19d84a9f7bcc3102ae5304c23e5dae789aaf8fd283f826e3fd4aca87ea2687ed606cf89869773d15799666553fd265524f7d9a0869e2869e00ebd8fd53af5b - languageName: node - linkType: hard - -"@vitest/snapshot@npm:4.1.5": - version: 4.1.5 - resolution: "@vitest/snapshot@npm:4.1.5" +"@vitest/snapshot@npm:4.1.3": + version: 4.1.3 + resolution: "@vitest/snapshot@npm:4.1.3" dependencies: - "@vitest/pretty-format": "npm:4.1.5" - "@vitest/utils": "npm:4.1.5" + "@vitest/pretty-format": "npm:4.1.3" + "@vitest/utils": "npm:4.1.3" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10/cf70530d8a7320c012bdf7f6ca4f3ddbbb47c9aeb9ff5d28319e552ce64db93423d0c4facff3e112c6d711ed4228369c8fa73c88350fe6c16cf04f9ac2558caf + checksum: 10/92613a8482da99aef0ae6c9e53cf517bbf4c84cb5b481cb3f4503893fa63b03e03b97262ac629a6eead7364113cbd6b9a2c54ac34295b568a2ee7abf2b1fa9dc languageName: node linkType: hard -"@vitest/spy@npm:4.1.5": - version: 4.1.5 - resolution: "@vitest/spy@npm:4.1.5" - checksum: 10/4db4bb3aea01cd737fdb06d8f498bcd2127b8c2afeaa78ff9df4147e1474aa26dd16f42dc0512c31385824e94dbb17b17fa0f4c60b7595b7b4ab946f098220ab +"@vitest/spy@npm:4.1.3": + version: 4.1.3 + resolution: "@vitest/spy@npm:4.1.3" + checksum: 10/e867364f7d43072a3580e3cb2d8e3b9b9709370ae50da8cb738f48e8d4b36502b712de4b9ff376f1e91ed1eeb299329142e00047769aeb6dd09bd0c9f0a69b29 languageName: node linkType: hard @@ -6527,17 +6459,6 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:4.1.5": - version: 4.1.5 - resolution: "@vitest/utils@npm:4.1.5" - dependencies: - "@vitest/pretty-format": "npm:4.1.5" - convert-source-map: "npm:^2.0.0" - tinyrainbow: "npm:^3.1.0" - checksum: 10/4f75a2df6f910578a361ae92eb92a2b6921f50cc748994f3b2e5900d0ae687b6683f33b090dedf9b96eaca23bac117817d9448a4a333c7a96b94ee767399f18c - languageName: node - linkType: hard - "@volar/language-core@npm:2.4.17, @volar/language-core@npm:~2.4.11": version: 2.4.17 resolution: "@volar/language-core@npm:2.4.17" @@ -6737,10 +6658,10 @@ __metadata: languageName: node linkType: hard -"abort-error@npm:^1.0.0, abort-error@npm:^1.0.1, abort-error@npm:^1.0.2": - version: 1.0.2 - resolution: "abort-error@npm:1.0.2" - checksum: 10/f28f961fe4ce2f27dfab38c70b90e9157d649912c6083a0c3659dbe0b9604fd41a7676d9a4416af9c1b73c3a2986f172e7441e4b61997943bb59f253fe6665b2 +"abort-error@npm:^1.0.0, abort-error@npm:^1.0.1": + version: 1.0.1 + resolution: "abort-error@npm:1.0.1" + checksum: 10/75a878035d478e7270ef99bd81012daae7914d66a417651976ab9e0cec562cb493366eafa6dfd844b7e49a50e3cef70883170b00329db58df507ed834af9dc8f languageName: node linkType: hard @@ -7536,15 +7457,6 @@ __metadata: languageName: node linkType: hard -"cborg@npm:^5.1.0": - version: 5.1.1 - resolution: "cborg@npm:5.1.1" - bin: - cborg: lib/bin.js - checksum: 10/e8be4314ab7cc9bba8be86089e195735d7df8ef32d07f64f7084cc9a9627628aea873648da0392cd84064eb6148f72927607b8ca062bafe660577ac62cdabda2 - languageName: node - linkType: hard - "chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" @@ -8094,20 +8006,20 @@ __metadata: linkType: hard "datastore-core@npm:^11.0.1": - version: 11.0.4 - resolution: "datastore-core@npm:11.0.4" + version: 11.0.2 + resolution: "datastore-core@npm:11.0.2" dependencies: - "@libp2p/logger": "npm:^6.2.4" + "@libp2p/logger": "npm:^6.0.0" interface-datastore: "npm:^9.0.0" interface-store: "npm:^7.0.0" - it-drain: "npm:^3.0.10" - it-filter: "npm:^3.1.4" - it-map: "npm:^3.1.4" - it-merge: "npm:^3.0.12" + it-drain: "npm:^3.0.9" + it-filter: "npm:^3.1.3" + it-map: "npm:^3.1.3" + it-merge: "npm:^3.0.11" it-pipe: "npm:^3.0.1" - it-sort: "npm:^3.0.9" - it-take: "npm:^3.0.9" - checksum: 10/f979cf7634c843684475fd5e08a2777a8b8acc31974cbd1eb37724c1d42aef9ccd098c075490f3df5425ca8054df1e5cbcca21975ab33ded89b11fd80fa4e7b8 + it-sort: "npm:^3.0.8" + it-take: "npm:^3.0.8" + checksum: 10/a2ed3ea71d81c20a57fa52a75fd9b0fe417dc4856a88c4ee67af8408187d621f710bcc6d1f9720cc0f6c5f6cd0d42d005ffd64ed1712a22b23744ec57715c9fe languageName: node linkType: hard @@ -8632,13 +8544,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:^8.0.0": - version: 8.0.0 - resolution: "entities@npm:8.0.0" - checksum: 10/d6e2ba75e444fb101ee2fbb07c839e687306c8a509426b75186619c19196f97c1db9932ca083f823c03e4a20e7407b654aa34de8cbb7770468e20fb2d4573a0e - languageName: node - linkType: hard - "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -8767,9 +8672,9 @@ __metadata: linkType: hard "es-module-lexer@npm:^2.0.0": - version: 2.1.0 - resolution: "es-module-lexer@npm:2.1.0" - checksum: 10/554c4374e78a812a1fa3673871ce7d42236438c414ea80c2ec35521cd9bb26d1d9155287529057d07431fd91df50d6a26d9bee5afd755fb7f6f7c81905a03956 + version: 2.0.0 + resolution: "es-module-lexer@npm:2.0.0" + checksum: 10/b075855289b5f40ee496f3d7525c5c501d029c3da15c22298a0030d625bf36d1da0768b26278f7f4bada2a602459b505888e20b77c414fba5da5619b0e84dbd1 languageName: node linkType: hard @@ -9551,26 +9456,26 @@ __metadata: languageName: node linkType: hard -"fast-xml-builder@npm:^1.1.7": - version: 1.1.9 - resolution: "fast-xml-builder@npm:1.1.9" +"fast-xml-builder@npm:^1.1.5": + version: 1.1.5 + resolution: "fast-xml-builder@npm:1.1.5" dependencies: path-expression-matcher: "npm:^1.1.3" - checksum: 10/44ef553aec4581b0fcc1b21bfd285aa9224eeb507620220228a9dd3dfd495068daaaf408b4cf9fd67354df558888680121c9d099229fdfd9672982531623781b + checksum: 10/377c4ef816972e67192fd32757c50d2a9d4cccf352ceac48bda6681a0ee24fb0b1f1c892810f77886db760681f23fe0b8f62c7c0cc9469c0d2863c5c529ac1d2 languageName: node linkType: hard "fast-xml-parser@npm:^5.5.6": - version: 5.7.3 - resolution: "fast-xml-parser@npm:5.7.3" + version: 5.7.1 + resolution: "fast-xml-parser@npm:5.7.1" dependencies: "@nodable/entities": "npm:^2.1.0" - fast-xml-builder: "npm:^1.1.7" + fast-xml-builder: "npm:^1.1.5" path-expression-matcher: "npm:^1.5.0" strnum: "npm:^2.2.3" bin: fxparser: src/cli/cli.js - checksum: 10/00a58655d0d58c1f914c7fd8e3a94e88799c3d473e29a6d2231dc02103df069e8c6043137cbec8df1cda6525a39914d1b84455a79530f63be266876a2211251c + checksum: 10/ce7de013cae7707d12b9da8cb294265da3780bb8bfa36b17f98053654628a0142159d78746747b1ed38bdefca8b6817f051171183e69a527ba18e1df067e9bce languageName: node linkType: hard @@ -10453,19 +10358,19 @@ __metadata: linkType: hard "interface-datastore@npm:^9.0.0, interface-datastore@npm:^9.0.1": - version: 9.0.3 - resolution: "interface-datastore@npm:9.0.3" + version: 9.0.2 + resolution: "interface-datastore@npm:9.0.2" dependencies: interface-store: "npm:^7.0.0" uint8arrays: "npm:^5.1.0" - checksum: 10/b26b9667489f2c7ee565deb21f16579fcced08c9e488835c8519ebe9efe1a5603351bd5f7c69066c95f32ae5fca5756ca7d25f002622fccf5984570516a699b5 + checksum: 10/830cc462db520f977ca1cd42db937ed7c4d598a0790e335f7292389117b7d56de9270e6e20a9f6b260977fe27e65077d8ce3b62fe1c10609eefc9a68aa99f99f languageName: node linkType: hard "interface-store@npm:^7.0.0": - version: 7.0.2 - resolution: "interface-store@npm:7.0.2" - checksum: 10/9e2ee7f3e97c387ecc3d78bdd9963d2ce6605c69f0bd719e7ad7255ed1ed7f2f9520fce18a34a2a38200b5c7a28a824e526434aabf320675658809b7ed3cef62 + version: 7.0.1 + resolution: "interface-store@npm:7.0.1" + checksum: 10/d1bc7f05110dafabf70b3409c20a372c2d8b96204bcf281da5fe4fe74d1ec894bda67c10010c259efc6dc292cc06bc3df9465f8e3f2c637ba81c8ddd48a34ee8 languageName: node linkType: hard @@ -10978,9 +10883,9 @@ __metadata: linkType: hard "it-all@npm:^3.0.0, it-all@npm:^3.0.9": - version: 3.0.11 - resolution: "it-all@npm:3.0.11" - checksum: 10/e6fe254d5c889d18779c271420fd6f5f838949bd186d463393decae1b64348f7cbc61547a4183a6ccf099fecf1ebc6c82b61549f3ec0fa6b0353b5b020f2f34e + version: 3.0.9 + resolution: "it-all@npm:3.0.9" + checksum: 10/7aa16a375dc077b7b4f71308a74877144146f057b3e9d360eb28393d0040a7f69f5fd5ead51cbaa9f347044a4bd0945d783259e62e1905ce947d46c8e7953026 languageName: node linkType: hard @@ -10997,19 +10902,19 @@ __metadata: languageName: node linkType: hard -"it-drain@npm:^3.0.10": - version: 3.0.12 - resolution: "it-drain@npm:3.0.12" - checksum: 10/e7fd32863546acd8656f055d909a6fa260c3846b30ae7763904d3ab0757ad59d9af2d0c3af7e7f7bff1031bc2fe65854e09ba986476da7f6c9e29ebec334d72f +"it-drain@npm:^3.0.10, it-drain@npm:^3.0.9": + version: 3.0.10 + resolution: "it-drain@npm:3.0.10" + checksum: 10/f6ed3261aa4a9f7f371c2eefa1fa0288e86e38fbf219b6394c78d2e7eeb1415592321f30909b29dada982255938942573001070c6797c0369b434c2b99cc4b82 languageName: node linkType: hard -"it-filter@npm:^3.1.4": - version: 3.1.6 - resolution: "it-filter@npm:3.1.6" +"it-filter@npm:^3.1.3": + version: 3.1.4 + resolution: "it-filter@npm:3.1.4" dependencies: it-peekable: "npm:^3.0.0" - checksum: 10/e9d35b0991d87bc5273240409c4cf8e8c23683d10e81a17252c3ad0ad90e14241a19ffeddfe5259a2ae2e7514548279b66fd2b134c544f0b5959047e3e2650fb + checksum: 10/40dfc8d6808ea456330fba9db2aaa9346a90fde845ebd944c6fadaabe313ead82df414734df20a447aead9a7e28db6db2a6cf1c0cd9a994d1b8f096394178bf8 languageName: node linkType: hard @@ -11039,30 +10944,30 @@ __metadata: languageName: node linkType: hard -"it-map@npm:^3.1.4": - version: 3.1.6 - resolution: "it-map@npm:3.1.6" +"it-map@npm:^3.1.3": + version: 3.1.4 + resolution: "it-map@npm:3.1.4" dependencies: it-peekable: "npm:^3.0.0" - checksum: 10/de6dd74e8dfebe9c12f02d1e124897d07647e40abb07f1826c007f0d4bb7cb7a8788a6bca24991bdea42573be49231d329a32c2b0b7b6d93045a2d028bd5e325 + checksum: 10/556654e0e3047ed4aca72eb942a86a288f33b9568fc122aa616e7a12a1dcf75ecbd5c4040211dc223d827dffb365db2b98397f5f4f7ef42c65d7babe8e469a0d languageName: node linkType: hard -"it-merge@npm:^3.0.0, it-merge@npm:^3.0.12": - version: 3.0.14 - resolution: "it-merge@npm:3.0.14" +"it-merge@npm:^3.0.0, it-merge@npm:^3.0.11, it-merge@npm:^3.0.12": + version: 3.0.12 + resolution: "it-merge@npm:3.0.12" dependencies: it-queueless-pushable: "npm:^2.0.0" - checksum: 10/85e002d200890e0963017c421725f62d939f39273bd435029e6178f76110255bdd9a4ad9a6fa398ef077adb150dcc0b341b3cd014a5c0c2143135e99aa9d417b + checksum: 10/b9d8e76d01d3251c9e36c5dbd19c13a94f3761ae44f7911efcce42c4c5fc025d70de5dae9e1ba9b52a1dfbf1913ab597c22a70a51d41740642e69fbae8111ec7 languageName: node linkType: hard "it-parallel@npm:^3.0.13": - version: 3.0.15 - resolution: "it-parallel@npm:3.0.15" + version: 3.0.13 + resolution: "it-parallel@npm:3.0.13" dependencies: p-defer: "npm:^4.0.1" - checksum: 10/bcbe9185a1437b140e0f390821374523fc93d01f2d7041528ac622108885f2efb54a7165ebeb5f7d9ec98cc9f5fb986ce66a7e42922c1e216b0773a7721d7286 + checksum: 10/3e036e48c08e98d1e8eb22e8ffa0213f04a731baff6ebc0e026963eca5c6af4a571458bf07bec29269950e56636e4477bfe50302b83e15f4f38ee7488c8e97e5 languageName: node linkType: hard @@ -11085,14 +10990,14 @@ __metadata: linkType: hard "it-protobuf-stream@npm:^2.0.3": - version: 2.0.6 - resolution: "it-protobuf-stream@npm:2.0.6" + version: 2.0.3 + resolution: "it-protobuf-stream@npm:2.0.3" dependencies: - abort-error: "npm:^1.0.2" + abort-error: "npm:^1.0.1" it-length-prefixed-stream: "npm:^2.0.0" it-stream-types: "npm:^2.0.2" uint8arraylist: "npm:^2.4.8" - checksum: 10/6ebdb4c123cdab82d26010017b69625d5039b5ab53f50f9d3ce7fbe7e8d25806b2a7dd8a5182eda4f453d65e638ecbfb8e3d6611e2cadfaf28d87d946a6fa761 + checksum: 10/53ea22d9a382a4de9de885b95799c1ad83fea6b9f5cec73688ca7fd909d8660e9baaa159b8fe1be61ff9fb19ade98edf8dcde0231cc65508a525cb7dcb6de08f languageName: node linkType: hard @@ -11139,12 +11044,12 @@ __metadata: languageName: node linkType: hard -"it-sort@npm:^3.0.9": - version: 3.0.11 - resolution: "it-sort@npm:3.0.11" +"it-sort@npm:^3.0.8": + version: 3.0.9 + resolution: "it-sort@npm:3.0.9" dependencies: it-all: "npm:^3.0.0" - checksum: 10/63383ad249f77515ddc3fe1ed72e58e0adfe14cd5864c17a13ae387c53d0bd75e4c1b0d4fb39218a454b1d4a5c8dc245e002e0d81dc9b4ee038755726ad1d115 + checksum: 10/8390eb0a1e799d1abb2e0d9ec910d4e1d5fb4ace58a462400d966872e881cba9ed5bde7c6fb89c33694d74d002717a593727e96a78cea4326724de7e279c9fe1 languageName: node linkType: hard @@ -11155,10 +11060,10 @@ __metadata: languageName: node linkType: hard -"it-take@npm:^3.0.9": - version: 3.0.11 - resolution: "it-take@npm:3.0.11" - checksum: 10/ed5a3719b56adc57b9a31e76b6097404069ee53f95d5a1182cfab26711ed56a6c315dfaf1d97ed5b576ad8606eb6ab49267ecbb52802c06852b36e60f0375164 +"it-take@npm:^3.0.8": + version: 3.0.9 + resolution: "it-take@npm:3.0.9" + checksum: 10/e09f7223ee006ff2a6638c270e48b73614737b1c84b0177d7a628b67be894203eb14c5bdfe757a4fab59df27283e9cb7e4a4c1c232debfe7ca51dd63e13d33e9 languageName: node linkType: hard @@ -11282,25 +11187,25 @@ __metadata: linkType: hard "jsdom@npm:^29.0.2": - version: 29.1.1 - resolution: "jsdom@npm:29.1.1" + version: 29.0.2 + resolution: "jsdom@npm:29.0.2" dependencies: - "@asamuzakjp/css-color": "npm:^5.1.11" - "@asamuzakjp/dom-selector": "npm:^7.1.1" + "@asamuzakjp/css-color": "npm:^5.1.5" + "@asamuzakjp/dom-selector": "npm:^7.0.6" "@bramus/specificity": "npm:^2.4.2" - "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.1" "@exodus/bytes": "npm:^1.15.0" css-tree: "npm:^3.2.1" data-urls: "npm:^7.0.0" decimal.js: "npm:^10.6.0" html-encoding-sniffer: "npm:^6.0.0" is-potential-custom-element-name: "npm:^1.0.1" - lru-cache: "npm:^11.3.5" - parse5: "npm:^8.0.1" + lru-cache: "npm:^11.2.7" + parse5: "npm:^8.0.0" saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" tough-cookie: "npm:^6.0.1" - undici: "npm:^7.25.0" + undici: "npm:^7.24.5" w3c-xmlserializer: "npm:^5.0.0" webidl-conversions: "npm:^8.0.1" whatwg-mimetype: "npm:^5.0.0" @@ -11311,7 +11216,7 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: 10/344aed7f91839b6c7d1b40778c5542d6ded7d42d88e1b787e10bf12d4ccd65464a5f23f774eb84350885c75a48efc99f6972adbb94dffe324a1b065d3650843c + checksum: 10/3ad1d9a5b6aba067427bc43be98e1c51fab489bf689a6530e596278c6326fe053c94fc47a9c133f126fbe914f421283ae723fb92214dfe4959ca6cf2ee1666f6 languageName: node linkType: hard @@ -11746,10 +11651,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.3.5": - version: 11.3.6 - resolution: "lru-cache@npm:11.3.6" - checksum: 10/d69ab552776954c7d310a6b2843e7d6be3a1c36c0ce45fca373c225ce5a06b95fd4ed724305d9d15fa55aa24d3cd42f854aa061bc481241225b3accf32993eab +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.7": + version: 11.3.2 + resolution: "lru-cache@npm:11.3.2" + checksum: 10/045b709782593d3f4ecb69340280717fd7c685b0d36f5976466995bd668ebf1af9e6540b9647b140b0ec4de95a48e2c80ae73ea6c4449e2f4d16a617e8760bf2 languageName: node linkType: hard @@ -13095,12 +13000,12 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^8.0.1": - version: 8.0.1 - resolution: "parse5@npm:8.0.1" +"parse5@npm:^8.0.0": + version: 8.0.0 + resolution: "parse5@npm:8.0.0" dependencies: - entities: "npm:^8.0.0" - checksum: 10/671dedfe7cbf4714414317bc8c6b2a14c61ef44f8fd90c983b5b1870653af5aa2e3b4e25e38e9538a7120ea2b688c50908830da2bd0930d8fd4bce34aed024eb + entities: "npm:^6.0.0" + checksum: 10/1973850932bb1cbd52ab64502761489fbe1bb43a52dee7ce41aac0b6c33a51a92aaee04661590b0912b739ae9ee316bce4c78c8ea34af42a7e522c983c3c6cf5 languageName: node linkType: hard @@ -13436,14 +13341,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.47, postcss@npm:^8.4.48, postcss@npm:^8.5.10, postcss@npm:^8.5.6": - version: 8.5.14 - resolution: "postcss@npm:8.5.14" +"postcss@npm:^8.4.47, postcss@npm:^8.4.48, postcss@npm:^8.5.6, postcss@npm:^8.5.8": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/2e3f4dea69692918fe9df5402beb0e54df84499995a094f2fbf63d1a9e38bc1b7a42854df47f09e02593213e01a5eb0627b1d1bd6d1b0ea90767b2e072f7167c + checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff08d1f57b1a7defce9ae5efcf31981890d9697a972a64e9965de677932ef28e4c8ba23a87aad45b82c459 languageName: node linkType: hard @@ -13557,10 +13462,10 @@ __metadata: languageName: node linkType: hard -"progress-events@npm:^1.0.0, progress-events@npm:^1.0.1, progress-events@npm:^1.1.0": - version: 1.1.0 - resolution: "progress-events@npm:1.1.0" - checksum: 10/ea37578b3e5dd6e60832b874fd66edf43398427256327f58e7f8228f6365294ed8be53818418a642833b7c611e58b650331bb691d852d538a236aade0ec6d1dd +"progress-events@npm:^1.0.0, progress-events@npm:^1.0.1": + version: 1.0.1 + resolution: "progress-events@npm:1.0.1" + checksum: 10/21e8ba984e6c6f6764279fabdf7b34d8110c1720757360fc8cad56b1622e67857fe543619652b64cee51a880a2a4a5febdcb4ff86e4c2969ed90048e2264f42f languageName: node linkType: hard @@ -14089,27 +13994,27 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "rolldown@npm:1.0.0-rc.17" - dependencies: - "@oxc-project/types": "npm:=0.127.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.17" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.17" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.17" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.17" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.17" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.17" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.17" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.17" - "@rolldown/pluginutils": "npm:1.0.0-rc.17" +"rolldown@npm:1.0.0-rc.13": + version: 1.0.0-rc.13 + resolution: "rolldown@npm:1.0.0-rc.13" + dependencies: + "@oxc-project/types": "npm:=0.123.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.13" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.13" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.13" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.13" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.13" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.13" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.13" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.13" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.13" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.13" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.13" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.13" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.13" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.13" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.13" + "@rolldown/pluginutils": "npm:1.0.0-rc.13" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -14143,7 +14048,7 @@ __metadata: optional: true bin: rolldown: bin/cli.mjs - checksum: 10/5e7415a7cb732c4f7168ab6dcc841ed9ec4ad614058294a53d94821a762c274a69b009e41e9c8e4983a059907f02d462030a36b42543c0f41ce702fcd68d10d5 + checksum: 10/d30c816b11f712f24966a4dde34997ef6e755c1613bb59c9e2ad89e5fffffb6eb0e52a4678261f2401efe69ada168a51d93b1cae1f23cc24498030aee67b0fb7 languageName: node linkType: hard @@ -14754,9 +14659,9 @@ __metadata: linkType: hard "std-env@npm:^4.0.0-rc.1": - version: 4.1.0 - resolution: "std-env@npm:4.1.0" - checksum: 10/008146cdb834010383138d356e0dd3e3b0ac127a8229f711b8c518bb22940813cc0dcd654fc76b17f0b18179f56089f8b8e52bd6a7ffa0041a966581e7a44dbe + version: 4.0.0 + resolution: "std-env@npm:4.0.0" + checksum: 10/19ef21cd85da52dc1178288d1b69e242b6579c0a76ddba2374f859aa58678797ec4a34c4f1fe6b9007a032e04d6fd3fca4e27349c88809265a9cbd90d924203f languageName: node linkType: hard @@ -15190,13 +15095,13 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.16, tinyglobby@npm:^0.2.9": - version: 0.2.16 - resolution: "tinyglobby@npm:0.2.16" +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" dependencies: fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.4" - checksum: 10/5c2c41b572ada38449e7c86a5fe034f204a1dbba577225a761a14f29f48dc3f2fc0d81a6c56fcc67c5a742cc3aa9fb5e2ca18dbf22b610b0bc0e549b34d5a0f8 + picomatch: "npm:^4.0.3" + checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 languageName: node linkType: hard @@ -15390,15 +15295,15 @@ __metadata: linkType: hard "turbo@npm:^2.9.1": - version: 2.9.9 - resolution: "turbo@npm:2.9.9" - dependencies: - "@turbo/darwin-64": "npm:2.9.9" - "@turbo/darwin-arm64": "npm:2.9.9" - "@turbo/linux-64": "npm:2.9.9" - "@turbo/linux-arm64": "npm:2.9.9" - "@turbo/windows-64": "npm:2.9.9" - "@turbo/windows-arm64": "npm:2.9.9" + version: 2.9.1 + resolution: "turbo@npm:2.9.1" + dependencies: + "@turbo/darwin-64": "npm:2.9.1" + "@turbo/darwin-arm64": "npm:2.9.1" + "@turbo/linux-64": "npm:2.9.1" + "@turbo/linux-arm64": "npm:2.9.1" + "@turbo/windows-64": "npm:2.9.1" + "@turbo/windows-arm64": "npm:2.9.1" dependenciesMeta: "@turbo/darwin-64": optional: true @@ -15414,7 +15319,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/9fa919c66fa67f42f1f811daf27ca6598d9bef22a6087d89b893daa8905693095461f0887b88cd765d7d2266c7ec42a1178aeb9e906a2612b23d12b857b57e81 + checksum: 10/bedcd8b17dda58c384ecd70fae5895894cb64d4f37677604500c72b94b4a02674ccc11c7d3065775eeac1048900c4f0db067ba2bd024cf469976ac367ad6436e languageName: node linkType: hard @@ -15676,10 +15581,10 @@ __metadata: languageName: node linkType: hard -"undici@npm:^7.25.0": - version: 7.25.0 - resolution: "undici@npm:7.25.0" - checksum: 10/038d3568c72bb976e3cc389284f7f1cc64cd70d578300e4676a449fbcb624a35fe99ac127b5f3729f18b8246d6c090444ab61b1b67736bb88f52a3e913d76bf8 +"undici@npm:^7.24.5": + version: 7.24.7 + resolution: "undici@npm:7.24.7" + checksum: 10/bce7b75fe2656bbd1f9c9d5d1b6b89670773281343be25d0b1f4d808dcce97d81509987d1f3183d37a63d3a57f5f217ed8ed15ee3e103384c54e190f4e360c48 languageName: node linkType: hard @@ -16038,8 +15943,8 @@ __metadata: linkType: hard "vite-plugin-static-copy@npm:^4.0.1": - version: 4.1.0 - resolution: "vite-plugin-static-copy@npm:4.1.0" + version: 4.0.1 + resolution: "vite-plugin-static-copy@npm:4.0.1" dependencies: chokidar: "npm:^3.6.0" p-map: "npm:^7.0.4" @@ -16047,20 +15952,20 @@ __metadata: tinyglobby: "npm:^0.2.15" peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/22aee7bdf17dba47f933ceffb559e2aff7366a79013a619d41dfb84913d7f68b7bdd277d4bb4636aeacf7bc740b003284d04f0a2aed2e1fc70b6d9e6235440e4 + checksum: 10/fb3fc7942034ee71f90fb874efb63b50a3103ab47e06bc9f9df47eb2dbc6440f005c1ea6045dbf95e4575fcbead0c134cb1007d7383e791b9db30176e126e7f5 languageName: node linkType: hard "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.6": - version: 8.0.10 - resolution: "vite@npm:8.0.10" + version: 8.0.6 + resolution: "vite@npm:8.0.6" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.10" - rolldown: "npm:1.0.0-rc.17" - tinyglobby: "npm:^0.2.16" + postcss: "npm:^8.5.8" + rolldown: "npm:1.0.0-rc.13" + tinyglobby: "npm:^0.2.15" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 "@vitejs/devtools": ^0.1.0 @@ -16104,7 +16009,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/64c6fa4efa1a9ca3e1cacbcca16487b75ea25d62efbfb99c4e571b5f716296dc4f8af825eb624e273b11c3bee4e87daec35815fb6a56e01c843659c003ed2bcd + checksum: 10/d7cb3f41c5c0b4b20abebdd4fd1db3cfa7b7813406cc9f7d378b01c9a8c156e1f0e016eb320a4aebecece1dfeb7070b268c02c5ea53df90c0abb82c0ba86dc39 languageName: node linkType: hard @@ -16118,16 +16023,16 @@ __metadata: linkType: hard "vitest@npm:^4.1.3": - version: 4.1.5 - resolution: "vitest@npm:4.1.5" - dependencies: - "@vitest/expect": "npm:4.1.5" - "@vitest/mocker": "npm:4.1.5" - "@vitest/pretty-format": "npm:4.1.5" - "@vitest/runner": "npm:4.1.5" - "@vitest/snapshot": "npm:4.1.5" - "@vitest/spy": "npm:4.1.5" - "@vitest/utils": "npm:4.1.5" + version: 4.1.3 + resolution: "vitest@npm:4.1.3" + dependencies: + "@vitest/expect": "npm:4.1.3" + "@vitest/mocker": "npm:4.1.3" + "@vitest/pretty-format": "npm:4.1.3" + "@vitest/runner": "npm:4.1.3" + "@vitest/snapshot": "npm:4.1.3" + "@vitest/spy": "npm:4.1.3" + "@vitest/utils": "npm:4.1.3" es-module-lexer: "npm:^2.0.0" expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" @@ -16145,12 +16050,12 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.5 - "@vitest/browser-preview": 4.1.5 - "@vitest/browser-webdriverio": 4.1.5 - "@vitest/coverage-istanbul": 4.1.5 - "@vitest/coverage-v8": 4.1.5 - "@vitest/ui": 4.1.5 + "@vitest/browser-playwright": 4.1.3 + "@vitest/browser-preview": 4.1.3 + "@vitest/browser-webdriverio": 4.1.3 + "@vitest/coverage-istanbul": 4.1.3 + "@vitest/coverage-v8": 4.1.3 + "@vitest/ui": 4.1.3 happy-dom: "*" jsdom: "*" vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -16181,7 +16086,7 @@ __metadata: optional: false bin: vitest: vitest.mjs - checksum: 10/8b768514993d8908fc9b5f2d619943d23b81aaba9443132583bd58aeb441bf76d152961326de9ca328ff0efcddbf8a58f4568a7b66a4391202542ed772613d81 + checksum: 10/c12755abfd2fc0dad20894c9c156fb552dfb157d2f6997a6e00dad4ff215f21e09c5c8e8ff33cf54e122bda21b2c5500031587e35930593d880ae4ba293477bc languageName: node linkType: hard From 0bfd32c8d05bbf909b9b6fe8c52f12897d5c34bf Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 7 May 2026 15:30:09 -0400 Subject: [PATCH 48/68] refactor(sheaves): apply updated terminology --- packages/sheaves/README.md | 116 ++++--- packages/sheaves/package.json | 2 +- packages/sheaves/src/compose.test.ts | 328 ++++++++++-------- packages/sheaves/src/compose.ts | 107 +++--- packages/sheaves/src/guard.test.ts | 28 +- packages/sheaves/src/guard.ts | 16 +- packages/sheaves/src/index.ts | 16 +- packages/sheaves/src/remote.test.ts | 34 +- packages/sheaves/src/remote.ts | 18 +- packages/sheaves/src/section.ts | 16 +- packages/sheaves/src/sheafify.e2e.test.ts | 148 ++++---- .../src/sheafify.string-metadata.test.ts | 16 +- packages/sheaves/src/sheafify.test.ts | 315 ++++++++--------- packages/sheaves/src/sheafify.ts | 164 ++++----- packages/sheaves/src/stalk.test.ts | 112 +++--- packages/sheaves/src/stalk.ts | 20 +- packages/sheaves/src/types.ts | 77 ++-- 17 files changed, 789 insertions(+), 744 deletions(-) diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index 45e10449ce..a9b2164856 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -2,111 +2,121 @@ Runtime capability routing adapted from sheaf theory in algebraic topology. -`sheafify({ name, sections })` produces a **sheaf** — an authority manager -over a presheaf of capabilities. The sheaf produces dispatch sections via -`getSection`, each of which routes invocations through the presheaf. +`sheafify({ name, providers })` produces a **sheaf** — an authority manager +over a collection of capability providers. The sheaf produces dispatch handlers via +`getSection`, each of which routes invocations through the provider set. See [USAGE.md](./USAGE.md) for annotated examples and [LIFT.md](./LIFT.md) for -the lift coroutine protocol and semantic equivalence assumption. +the policy coroutine protocol and semantic equivalence assumption. + +## Install + +```sh +yarn add @metamask/sheaves +``` + +```sh +npm install @metamask/sheaves +``` ## Concepts -**Presheaf section** (`PresheafSection`) — The input data: a capability (exo) -paired with operational metadata, assigned over the open set defined by the -exo's guard. This is an element of the presheaf F = F_sem x F_op. +**Provider** (`Provider`) — The input data: a capability handler paired with +operational metadata, assigned over the open set defined by the handler's guard. +This is an element of the presheaf F = F_sem x F_op. -> A `getBalance(string)` provider with `{ cost: 100 }` is one presheaf -> section. A `getBalance("alice")` provider with `{ cost: 1 }` is another, -> covering a narrower open set. +> A `getBalance(string)` provider with `{ cost: 100 }` is one provider. A +> `getBalance("alice")` provider with `{ cost: 1 }` is another, covering a +> narrower open set. -**Germ** — An equivalence class of presheaf sections at an invocation point, -identified by metadata. At dispatch time, sections in the stalk with identical -metadata are collapsed into a single germ; the system picks an arbitrary +**Candidate** — An equivalence class of providers at an invocation point, +identified by metadata. At dispatch time, providers in the stalk with identical +metadata are collapsed into a single candidate; the system picks an arbitrary representative for dispatch. If two capabilities are indistinguishable by metadata, the sheaf has no data to prefer one over the other. > Two `getBalance(string)` providers both with `{ cost: 1 }` collapse into -> one germ. The lift never sees both — it receives one representative. +> one candidate. The policy never sees both — it receives one representative. -**Stalk** — The set of germs matching a specific `(method, args)` invocation, +**Stalk** — The set of candidates matching a specific `(method, args)` invocation, computed at dispatch time by guard filtering and then collapsing equivalent entries. -> Stalk at `("getBalance", "alice")` might contain two germs (cost 1 vs 100); +> Stalk at `("getBalance", "alice")` might contain two candidates (cost 1 vs 100); > stalk at `("transfer", ...)` might contain one. -**Lift** — An `async function*` coroutine that yields candidates from a -multi-germ stalk in preference order. See [LIFT.md](./LIFT.md) for the -coroutine protocol, `LiftContext`, and the semantic equivalence assumption -required of all lifts. +**Policy** — An `async function*` coroutine that yields candidates from a +multi-candidate stalk in preference order. See [LIFT.md](./LIFT.md) for the +coroutine protocol, `PolicyContext`, and the semantic equivalence assumption +required of all policies. At dispatch time, metadata is decomposed into **constraints** (keys with the -same value across every germ — topologically determined, not a choice) and -**options** (the remaining keys — the lift's actual decision space). The lift -receives only options on each germ; constraints arrive separately in the +same value across every candidate — topologically determined, not a choice) and +**options** (the remaining keys — the policy's actual decision space). The policy +receives only options on each candidate; constraints arrive separately in the context. > `argmin` by cost, `argmin` by latency, or any custom selection logic. The -> lift is never invoked when the stalk resolves to a single germ — either -> because only one section matched, or because all matching sections had +> policy is never invoked when the stalk resolves to a single candidate — either +> because only one provider matched, or because all matching providers had > identical metadata and collapsed to one representative. -**Sheaf** — The authority manager returned by `sheafify`. Holds the presheaf -data (sections frozen at construction time) and exposes factory methods that -produce dispatch exos on demand. +**Sheaf** — The authority manager returned by `sheafify`. Holds the provider +data (frozen at construction time) and exposes factory methods that +produce dispatch handlers on demand. ``` -const sheaf = sheafify({ name: 'Wallet', sections }); +const sheaf = sheafify({ name: 'Wallet', providers }); ``` -- `sheaf.getSection({ guard, lift })` — produce a dispatch exo -- `sheaf.getDiscoverableSection({ guard, lift, schema })` — same, but the exo exposes its guard +- `sheaf.getSection({ guard, lift })` — produce a dispatch handler +- `sheaf.getDiscoverableSection({ guard, lift, schema })` — same, but the handler exposes its guard ## Dispatch pipeline At each invocation point `(method, args)` within a granted section: ``` -getStalk(sections, method, args) presheaf → stalk (filter by guard) +getStalk(providers, method, args) presheaf → stalk (filter by guard) evaluateMetadata(stalk, args) metadata specs → concrete values collapseEquivalent(stalk) locality condition (quotient by metadata) decomposeMetadata(collapsed) restriction map (constraints / options) -lift(stripped, { method, args, operational selection (extra-theoretic) - constraints }) -dispatch to chosen.exo evaluation +policy(candidates, { method, args, operational selection (extra-theoretic) + constraints }) +dispatch to chosen.handler evaluation ``` -The pipeline short-circuits at two points: if only one section matches the -guard, it is invoked directly without evaluate/collapse/lift; if all matching -sections collapse to an identical germ, the single representative is invoked -without calling the lift. +The pipeline short-circuits at two points: if only one provider matches the +guard, it is invoked directly without evaluate/collapse/policy; if all matching +providers collapse to an identical candidate, the single representative is invoked +without calling the policy. `callable` and `source` metadata specs make the stalk shape depend on the -invocation arguments. A `swap(amount)` section can produce `{ cost: 'low' }` +invocation arguments. A `swap(amount)` provider can produce `{ cost: 'low' }` for small amounts and `{ cost: 'high' }` for large ones, yielding a different -set of germs — and potentially a different lift outcome — for the same method -called with different arguments. +set of candidates — and potentially a different policy outcome — for the same +method called with different arguments. ## Design choices -**Germ identity is metadata identity.** The collapse step quotients by -metadata: if two sections should be distinguishable, the caller must give them -distinguishable metadata. Sections with identical metadata are treated as +**Candidate identity is metadata identity.** The collapse step quotients by +metadata: if two providers should be distinguishable, the caller must give them +distinguishable metadata. Providers with identical metadata are treated as interchangeable. Under the sheaf condition (effect-equivalence), this recovers the classical equivalence relation on germs. **Pseudosheafification.** The sheafification functor would precompute the full etale space. This system defers to invocation time: compute the stalk, -collapse, decompose, lift. The trade-off is that global coherence (a lift -choosing consistently across points) is not guaranteed. +collapse, decompose, select via policy. The trade-off is that global coherence +(a policy choosing consistently across points) is not guaranteed. **Restriction and gluing are implicit.** Guard restriction induces a restriction map on metadata: restricting to a point filters the presheaf to -covering sections (`getStalk`), then `decomposeMetadata` strips the metadata +covering providers (`getStalk`), then `decomposeMetadata` strips the metadata to distinguishing keys — the restricted metadata over that point. The join -works dually: the union of two sections has the join of their metadata, and +works dually: the union of two providers has the join of their metadata, and restriction at any point recovers the local distinguishing keys in O(n). -Gluing follows: compatible sections (equal metadata on their overlap) produce a +Gluing follows: compatible providers (equal metadata on their overlap) produce a well-defined join. The dispatch pipeline computes all of this implicitly. The remaining gap is `revokeSite` (revoking over an open set rather than a point), which requires an `intersects` operator on guards not yet available. @@ -115,7 +125,7 @@ which requires an `intersects` operator on guards not yet available. This construction is more properly a **stack** in algebraic geometry. We call it a sheaf because engineers already know "stack" as a LIFO data structure, and -the algebraic geometry term is unrelated. Within a germ, any representative +the algebraic geometry term is unrelated. Within a candidate, any representative will do — authority-equivalence is asserted by constructor contract, not -verified at runtime. Between germs, metadata distinguishes them and the lift -resolves the choice. +verified at runtime. Between candidates, metadata distinguishes them and the +policy resolves the choice. diff --git a/packages/sheaves/package.json b/packages/sheaves/package.json index 7c678ec516..26ce3c8102 100644 --- a/packages/sheaves/package.json +++ b/packages/sheaves/package.json @@ -42,7 +42,7 @@ "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/sheaves", "changelog:update": "../../scripts/update-changelog.sh @metamask/sheaves", - "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo", + "clean": "rimraf --glob './*.tsbuildinfo' ./.eslintcache ./coverage ./dist ./.turbo ./logs", "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", "lint:dependencies": "depcheck --quiet", "lint:eslint": "eslint . --cache", diff --git a/packages/sheaves/src/compose.test.ts b/packages/sheaves/src/compose.test.ts index 446221c807..87642475d2 100644 --- a/packages/sheaves/src/compose.test.ts +++ b/packages/sheaves/src/compose.test.ts @@ -1,45 +1,50 @@ import { describe, it, expect, vi } from 'vitest'; -import { fallthrough, proxyLift, withFilter, withRanking } from './compose.ts'; -import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; +import { + fallthrough, + proxyPolicy, + withFilter, + withRanking, +} from './compose.ts'; +import type { Candidate, Policy, PolicyContext } from './types.ts'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- type Meta = { id: string; cost: number }; -type G = EvaluatedSection>; +type C = Candidate>; -const makeGerm = (id: string, cost = 0): G => ({ - exo: {} as G['exo'], +const makeCandidate = (id: string, cost = 0): C => ({ + handler: {} as C['handler'], metadata: { id, cost }, }); -const ctx: LiftContext = { +const ctx: PolicyContext = { method: 'transfer', args: ['alice', 100n], constraints: {}, }; /** - * Drive a lift to exhaustion, simulating a failure after each yielded - * candidate. Returns all yielded germs in order and the error arrays + * Drive a policy to exhaustion, simulating a failure after each yielded + * candidate. Returns all yielded candidates in order and the error arrays * the generator received. * - * @param lift - The lift to drive. - * @param germs - The germs to pass to the lift. - * @param context - The lift context. - * @returns Yielded germs and error snapshots received by the generator. + * @param policy - The policy to drive. + * @param candidates - The candidates to pass to the policy. + * @param context - The policy context. + * @returns Yielded candidates and error snapshots received by the generator. */ const driveToExhaustion = async ( - lift: Lift, - germs: G[], - context: LiftContext = ctx, -): Promise<{ yielded: G[]; receivedErrors: unknown[][] }> => { - const yielded: G[] = []; + policy: Policy, + candidates: C[], + context: PolicyContext = ctx, +): Promise<{ yielded: C[]; receivedErrors: unknown[][] }> => { + const yielded: C[] = []; const receivedErrors: unknown[][] = []; const errors: unknown[] = []; - const gen = lift(germs, context); + const gen = policy(candidates, context); let next = await gen.next([...errors]); while (!next.done) { yielded.push(next.value); @@ -51,23 +56,23 @@ const driveToExhaustion = async ( }; /** - * Drive a lift, succeeding on the nth candidate (1-based). - * Returns the winning germ. + * Drive a policy, succeeding on the nth candidate (1-based). + * Returns the winning candidate. * - * @param lift - The lift to drive. - * @param germs - The germs to pass to the lift. + * @param policy - The policy to drive. + * @param candidates - The candidates to pass to the policy. * @param successOn - Which attempt number (1-based) should succeed. - * @param context - The lift context. - * @returns The germ that won on attempt `successOn`. + * @param context - The policy context. + * @returns The candidate that won on attempt `successOn`. */ const driveWithSuccessOn = async ( - lift: Lift, - germs: G[], + policy: Policy, + candidates: C[], successOn: number, - context: LiftContext = ctx, -): Promise => { + context: PolicyContext = ctx, +): Promise => { const errors: unknown[] = []; - const gen = lift(germs, context); + const gen = policy(candidates, context); let attempt = 0; let next = await gen.next([...errors]); while (!next.done) { @@ -83,34 +88,38 @@ const driveWithSuccessOn = async ( }; // --------------------------------------------------------------------------- -// proxyLift +// proxyPolicy // --------------------------------------------------------------------------- -describe('proxyLift', () => { +describe('proxyPolicy', () => { it('forwards all yielded values from inner generator', async () => { - const [germA, germB, germC] = [makeGerm('a'), makeGerm('b'), makeGerm('c')]; - const inner = async function* (): AsyncGenerator { - yield germA; - yield germB; - yield germC; + const [candidateA, candidateB, candidateC] = [ + makeCandidate('a'), + makeCandidate('b'), + makeCandidate('c'), + ]; + const inner = async function* (): AsyncGenerator { + yield candidateA; + yield candidateB; + yield candidateC; }; - const { yielded } = await driveToExhaustion(() => proxyLift(inner()), []); - expect(yielded).toStrictEqual([germA, germB, germC]); + const { yielded } = await driveToExhaustion(() => proxyPolicy(inner()), []); + expect(yielded).toStrictEqual([candidateA, candidateB, candidateC]); }); it('forwards error arrays down to inner generator', async () => { - const [germA, germB] = [makeGerm('a'), makeGerm('b')]; + const [candidateA, candidateB] = [makeCandidate('a'), makeCandidate('b')]; const receivedByInner: unknown[][] = []; - const inner = async function* (): AsyncGenerator { - const errors1: unknown[] = yield germA; + const inner = async function* (): AsyncGenerator { + const errors1: unknown[] = yield candidateA; receivedByInner.push(errors1); - const errors2: unknown[] = yield germB; + const errors2: unknown[] = yield candidateB; receivedByInner.push(errors2); }; - await driveToExhaustion(() => proxyLift(inner()), []); + await driveToExhaustion(() => proxyPolicy(inner()), []); expect(receivedByInner).toHaveLength(2); expect(receivedByInner[0]).toHaveLength(1); // one error after first attempt @@ -118,33 +127,37 @@ describe('proxyLift', () => { }); it('stops when inner generator is done', async () => { - const inner = async function* (): AsyncGenerator { + const inner = async function* (): AsyncGenerator { // immediately done }; - const { yielded } = await driveToExhaustion(() => proxyLift(inner()), []); + const { yielded } = await driveToExhaustion(() => proxyPolicy(inner()), []); expect(yielded).toHaveLength(0); }); it('allows inner generator to stop early based on errors', async () => { - const [germA, germB, germC] = [makeGerm('a'), makeGerm('b'), makeGerm('c')]; + const [candidateA, candidateB, candidateC] = [ + makeCandidate('a'), + makeCandidate('b'), + makeCandidate('c'), + ]; - const inner = async function* (): AsyncGenerator { - let errors: unknown[] = yield germA; + const inner = async function* (): AsyncGenerator { + let errors: unknown[] = yield candidateA; // stop after first failure if (errors.length > 0) { return; } - errors = yield germB; + errors = yield candidateB; if (errors.length > 0) { return; } - yield germC; + yield candidateC; }; - const { yielded } = await driveToExhaustion(() => proxyLift(inner()), []); + const { yielded } = await driveToExhaustion(() => proxyPolicy(inner()), []); // Only 'a' yielded — inner stops after receiving the first error - expect(yielded).toStrictEqual([germA]); + expect(yielded).toStrictEqual([candidateA]); }); }); @@ -153,58 +166,64 @@ describe('proxyLift', () => { // --------------------------------------------------------------------------- describe('withFilter', () => { - it('passes only matching germs to the inner lift', async () => { - const germs = [makeGerm('a', 1), makeGerm('b', 2), makeGerm('c', 3)]; + it('passes only matching candidates to the inner policy', async () => { + const candidates = [ + makeCandidate('a', 1), + makeCandidate('b', 2), + makeCandidate('c', 3), + ]; const received = vi.fn(); - const inner: Lift = async function* (allGerms) { - received(allGerms.map((item) => item.metadata.id)); - yield* allGerms; + const inner: Policy = async function* (allCandidates) { + received(allCandidates.map((item) => item.metadata.id)); + yield* allCandidates; }; - const lift = withFilter((germ) => (germ.metadata.cost ?? 0) >= 2)( - inner, - ); - await driveToExhaustion(lift, germs); + const policy = withFilter( + (candidate) => (candidate.metadata.cost ?? 0) >= 2, + )(inner); + await driveToExhaustion(policy, candidates); expect(received).toHaveBeenCalledWith(['b', 'c']); }); it('passes context to the predicate', async () => { - const germs = [makeGerm('alice'), makeGerm('bob')]; - const contextUsed: LiftContext[] = []; + const candidates = [makeCandidate('alice'), makeCandidate('bob')]; + const contextUsed: PolicyContext[] = []; - const lift = withFilter((_germ, liftContext) => { - contextUsed.push(liftContext); + const policy = withFilter((_candidate, policyContext) => { + contextUsed.push(policyContext); return true; - })(async function* (allGerms) { - yield* allGerms; + })(async function* (allCandidates) { + yield* allCandidates; }); - await driveToExhaustion(lift, germs); + await driveToExhaustion(policy, candidates); expect(contextUsed.length).toBeGreaterThan(0); expect(contextUsed[0]).toStrictEqual(ctx); }); - it('yields nothing when no germs match', async () => { - const germs = [makeGerm('a', 1)]; - const lift = withFilter(() => false)(async function* (allGerms) { - yield* allGerms; - }); + it('yields nothing when no candidates match', async () => { + const candidates = [makeCandidate('a', 1)]; + const policy = withFilter(() => false)( + async function* (allCandidates) { + yield* allCandidates; + }, + ); - const { yielded } = await driveToExhaustion(lift, germs); + const { yielded } = await driveToExhaustion(policy, candidates); expect(yielded).toHaveLength(0); }); - it('returns the inner lift generator directly (no extra wrapping)', () => { - // withFilter is a pure input transform — it returns the inner lift's + it('returns the inner policy generator directly (no extra wrapping)', () => { + // withFilter is a pure input transform — it returns the inner policy's // generator, not a new proxy generator. - const innerGen = {} as AsyncGenerator; - const inner: Lift = vi.fn(() => innerGen); - const lift = withFilter(() => true)(inner); + const innerGen = {} as AsyncGenerator; + const inner: Policy = vi.fn(() => innerGen); + const policy = withFilter(() => true)(inner); - const result = lift([], ctx); + const result = policy([], ctx); expect(result).toBe(innerGen); }); }); @@ -214,43 +233,47 @@ describe('withFilter', () => { // --------------------------------------------------------------------------- describe('withRanking', () => { - it('sorts germs before passing to inner lift', async () => { - const germs = [makeGerm('a', 3), makeGerm('b', 1), makeGerm('c', 2)]; + it('sorts candidates before passing to inner policy', async () => { + const candidates = [ + makeCandidate('a', 3), + makeCandidate('b', 1), + makeCandidate('c', 2), + ]; const received = vi.fn(); - const inner: Lift = async function* (allGerms) { - received(allGerms.map((item) => item.metadata.id)); - yield* allGerms; + const inner: Policy = async function* (allCandidates) { + received(allCandidates.map((item) => item.metadata.id)); + yield* allCandidates; }; - const lift = withRanking( + const policy = withRanking( (a, b) => (a.metadata.cost ?? 0) - (b.metadata.cost ?? 0), )(inner); - await driveToExhaustion(lift, germs); + await driveToExhaustion(policy, candidates); expect(received).toHaveBeenCalledWith(['b', 'c', 'a']); }); - it('does not mutate the original germs array', async () => { - const germs = [makeGerm('a', 3), makeGerm('b', 1)]; - const original = [...germs]; + it('does not mutate the original candidates array', async () => { + const candidates = [makeCandidate('a', 3), makeCandidate('b', 1)]; + const original = [...candidates]; - const lift = withRanking( + const policy = withRanking( (a, b) => (a.metadata.cost ?? 0) - (b.metadata.cost ?? 0), - )(async function* (allGerms) { - yield* allGerms; + )(async function* (allCandidates) { + yield* allCandidates; }); - await driveToExhaustion(lift, germs); - expect(germs).toStrictEqual(original); + await driveToExhaustion(policy, candidates); + expect(candidates).toStrictEqual(original); }); - it('returns the inner lift generator directly (no extra wrapping)', () => { - const innerGen = {} as AsyncGenerator; - const inner: Lift = vi.fn(() => innerGen); - const lift = withRanking(() => 0)(inner); + it('returns the inner policy generator directly (no extra wrapping)', () => { + const innerGen = {} as AsyncGenerator; + const inner: Policy = vi.fn(() => innerGen); + const policy = withRanking(() => 0)(inner); - const result = lift([], ctx); + const result = policy([], ctx); expect(result).toBe(innerGen); }); }); @@ -260,80 +283,91 @@ describe('withRanking', () => { // --------------------------------------------------------------------------- describe('fallthrough', () => { - it('yields all candidates from liftA then liftB', async () => { + it('yields all candidates from policyA then policyB', async () => { const [a1, a2, b1, b2] = [ - makeGerm('a1'), - makeGerm('a2'), - makeGerm('b1'), - makeGerm('b2'), + makeCandidate('a1'), + makeCandidate('a2'), + makeCandidate('b1'), + makeCandidate('b2'), ]; - const liftA: Lift = async function* () { + const policyA: Policy = async function* () { yield a1; yield a2; }; - const liftB: Lift = async function* () { + const policyB: Policy = async function* () { yield b1; yield b2; }; - const { yielded } = await driveToExhaustion(fallthrough(liftA, liftB), []); + const { yielded } = await driveToExhaustion( + fallthrough(policyA, policyB), + [], + ); expect(yielded).toStrictEqual([a1, a2, b1, b2]); }); - it('stops at liftA winner and does not invoke liftB', async () => { - const [a1, a2] = [makeGerm('a1'), makeGerm('a2')]; - const liftBInvoked = vi.fn(); + it('stops at policyA winner and does not invoke policyB', async () => { + const [a1, a2] = [makeCandidate('a1'), makeCandidate('a2')]; + const policyBInvoked = vi.fn(); - const liftA: Lift = async function* () { + const policyA: Policy = async function* () { yield a1; yield a2; }; - const liftB: Lift = async function* () { - liftBInvoked(); - yield makeGerm('b1'); + const policyB: Policy = async function* () { + policyBInvoked(); + yield makeCandidate('b1'); }; // Succeed on first candidate - const winner = await driveWithSuccessOn(fallthrough(liftA, liftB), [], 1); + const winner = await driveWithSuccessOn( + fallthrough(policyA, policyB), + [], + 1, + ); expect(winner).toBe(a1); - expect(liftBInvoked).not.toHaveBeenCalled(); + expect(policyBInvoked).not.toHaveBeenCalled(); }); - it('falls through to liftB when liftA is exhausted', async () => { - const [a1, b1] = [makeGerm('a1'), makeGerm('b1')]; + it('falls through to policyB when policyA is exhausted', async () => { + const [a1, b1] = [makeCandidate('a1'), makeCandidate('b1')]; - const liftA: Lift = async function* () { + const policyA: Policy = async function* () { yield a1; }; - const liftB: Lift = async function* () { + const policyB: Policy = async function* () { yield b1; }; - // liftA has one candidate (a1), fail it, then liftB kicks in - const winner = await driveWithSuccessOn(fallthrough(liftA, liftB), [], 2); + // policyA has one candidate (a1), fail it, then policyB kicks in + const winner = await driveWithSuccessOn( + fallthrough(policyA, policyB), + [], + 2, + ); expect(winner).toBe(b1); }); - it('forwards error arrays through yield* to each inner lift', async () => { - const [a1, b1] = [makeGerm('a1'), makeGerm('b1')]; + it('forwards error arrays through yield* to each inner policy', async () => { + const [a1, b1] = [makeCandidate('a1'), makeCandidate('b1')]; const errorsReceivedByA: unknown[][] = []; const errorsReceivedByB: unknown[][] = []; - const liftA: Lift = async function* () { + const policyA: Policy = async function* () { const errors: unknown[] = yield a1; errorsReceivedByA.push(errors); }; - const liftB: Lift = async function* () { + const policyB: Policy = async function* () { const errors: unknown[] = yield b1; errorsReceivedByB.push(errors); }; - await driveToExhaustion(fallthrough(liftA, liftB), []); + await driveToExhaustion(fallthrough(policyA, policyB), []); - // liftA's first yield received one error (a1 failed) + // policyA's first yield received one error (a1 failed) expect(errorsReceivedByA[0]).toHaveLength(1); - // liftB's first yield received two errors (a1 + b1 both failed) + // policyB's first yield received two errors (a1 + b1 both failed) expect(errorsReceivedByB[0]).toHaveLength(2); }); }); @@ -344,33 +378,35 @@ describe('fallthrough', () => { describe('composition', () => { it('withFilter composed with withRanking applies both transforms', async () => { - const germs = [ - makeGerm('a', 3), - makeGerm('b', 1), - makeGerm('c', 2), - makeGerm('d', 4), // filtered out (cost > 3) + const candidates = [ + makeCandidate('a', 3), + makeCandidate('b', 1), + makeCandidate('c', 2), + makeCandidate('d', 4), // filtered out (cost > 3) ]; const received = vi.fn(); - const base: Lift = async function* (allGerms) { - received(allGerms.map((item) => item.metadata.id)); - yield* allGerms; + const base: Policy = async function* (allCandidates) { + received(allCandidates.map((item) => item.metadata.id)); + yield* allCandidates; }; - const lift = withFilter((germ) => (germ.metadata.cost ?? 0) <= 3)( + const policy = withFilter( + (candidate) => (candidate.metadata.cost ?? 0) <= 3, + )( withRanking( (a, b) => (a.metadata.cost ?? 0) - (b.metadata.cost ?? 0), )(base), ); - await driveToExhaustion(lift, germs); + await driveToExhaustion(policy, candidates); // filtered to a/b/c, sorted by cost ascending expect(received).toHaveBeenCalledWith(['b', 'c', 'a']); }); - it('proxyLift wrapping fallthrough threads errors through both layers', async () => { - const [a1, b1] = [makeGerm('a1'), makeGerm('b1')]; - const inner: Lift = fallthrough( + it('proxyPolicy wrapping fallthrough threads errors through both layers', async () => { + const [a1, b1] = [makeCandidate('a1'), makeCandidate('b1')]; + const inner: Policy = fallthrough( async function* () { yield a1; }, @@ -379,10 +415,10 @@ describe('composition', () => { }, ); - // proxyLift wrapping the whole fallthrough - const lift: Lift = () => proxyLift(inner([], ctx)); + // proxyPolicy wrapping the whole fallthrough + const policy: Policy = () => proxyPolicy(inner([], ctx)); - const { yielded } = await driveToExhaustion(lift, []); + const { yielded } = await driveToExhaustion(policy, []); expect(yielded).toStrictEqual([a1, b1]); }); }); diff --git a/packages/sheaves/src/compose.ts b/packages/sheaves/src/compose.ts index 754d8cbed6..ef1d7fc5c7 100644 --- a/packages/sheaves/src/compose.ts +++ b/packages/sheaves/src/compose.ts @@ -1,28 +1,28 @@ -import type { EvaluatedSection, Lift, LiftContext } from './types.ts'; +import type { Candidate, Policy, PolicyContext } from './types.ts'; /** - * A lift that yields all germs in their original order without filtering. + * A policy that yields all candidates in their original order without filtering. * - * Use as a placeholder when the sheaf always has a single-section stalk - * (the lift is never actually called) or to express "try everything in + * Use as a placeholder when the sheaf always has a single-candidate stalk + * (the policy is never actually called) or to express "try everything in * declaration order" as an explicit policy. * - * @param germs - Evaluated sections to yield in order. - * @yields Each germ in the original array order. + * @param candidates - Candidates to yield in order. + * @yields Each candidate in the original array order. */ -export async function* noopLift>( - germs: EvaluatedSection>[], -): AsyncGenerator>, void, unknown[]> { - yield* germs; +export async function* noopPolicy>( + candidates: Candidate>[], +): AsyncGenerator>, void, unknown[]> { + yield* candidates; } /** - * Proxy a lift coroutine, forwarding yielded candidates up and received + * Proxy a policy coroutine, forwarding yielded candidates up and received * error arrays down to the inner generator. * * Note: async generator `yield*` DOES forward `.next(value)` to the * delegated async iterator, so for simple sequential composition (e.g. - * `fallthrough`) you can use `yield*` directly. `proxyLift` is the right + * `fallthrough`) you can use `yield*` directly. `proxyPolicy` is the right * primitive when you need to add logic between yields — for example, * logging, counting attempts, or conditionally stopping early based on the * error history. @@ -31,10 +31,10 @@ export async function* noopLift>( * @yields Candidates from the inner generator. * @returns void when the inner generator is exhausted. * @example - * // Lift that logs each retry - * const withLogging = (inner: Lift): Lift => - * async function*(germs, context) { - * const gen = inner(germs, context); + * // Policy that logs each retry + * const withLogging = (inner: Policy): Policy => + * async function*(candidates, context) { + * const gen = inner(candidates, context); * let next = await gen.next([]); * while (!next.done) { * const errors: unknown[] = yield next.value; @@ -42,11 +42,11 @@ export async function* noopLift>( * next = await gen.next(errors); * } * }; - * // The above pattern is exactly proxyLift with a side-effect added. + * // The above pattern is exactly proxyPolicy with a side-effect added. */ -export async function* proxyLift>( - gen: AsyncGenerator>, void, unknown[]>, -): AsyncGenerator>, void, unknown[]> { +export async function* proxyPolicy>( + gen: AsyncGenerator>, void, unknown[]>, +): AsyncGenerator>, void, unknown[]> { let next = await gen.next([]); while (!next.done) { const errors: unknown[] = yield next.value; @@ -55,69 +55,66 @@ export async function* proxyLift>( } /** - * Filter germs before passing to a lift. + * Filter candidates before passing to a policy. * - * Returns the inner lift's generator directly — no proxying needed since - * this is a pure input transform that delegates entirely to the inner lift. + * Returns the inner policy's generator directly — no proxying needed since + * this is a pure input transform that delegates entirely to the inner policy. * - * @param predicate - Returns true for germs that should be passed to the inner lift. - * @returns A lift combinator that filters its germs before delegating. + * @param predicate - Returns true for candidates that should be passed to the inner policy. + * @returns A policy combinator that filters its candidates before delegating. */ export const withFilter = >( predicate: ( - germ: EvaluatedSection>, - ctx: LiftContext, + candidate: Candidate>, + ctx: PolicyContext, ) => boolean, ) => - (inner: Lift): Lift => - (germs, context) => + (inner: Policy): Policy => + (candidates, context) => inner( - germs.filter((germ) => predicate(germ, context)), + candidates.filter((candidate) => predicate(candidate, context)), context, ); /** - * Sort germs by a comparator before passing to a lift. + * Sort candidates by a comparator before passing to a policy. * - * Returns the inner lift's generator directly — no proxying needed since - * this is a pure input transform that delegates entirely to the inner lift. - * The original germs array is not mutated. + * Returns the inner policy's generator directly — no proxying needed since + * this is a pure input transform that delegates entirely to the inner policy. + * The original candidates array is not mutated. * * @param comparator - Comparator function for sorting (same signature as Array.sort). - * @returns A lift combinator that sorts its germs before delegating. + * @returns A policy combinator that sorts its candidates before delegating. */ export const withRanking = >( - comparator: ( - a: EvaluatedSection>, - b: EvaluatedSection>, - ) => number, + comparator: (a: Candidate>, b: Candidate>) => number, ) => - (inner: Lift): Lift => - (germs, context) => - inner([...germs].sort(comparator), context); + (inner: Policy): Policy => + (candidates, context) => + inner([...candidates].sort(comparator), context); /** - * Try all candidates from liftA, then all candidates from liftB. + * Try all candidates from policyA, then all candidates from policyB. * * Uses `yield*` directly since async generator delegation forwards * `.next(value)` to the inner iterator, so error arrays are correctly - * threaded through each inner lift. + * threaded through each inner policy. * - * liftB is not informed of liftA's failures at its prime call, but via - * `yield*` it receives all accumulated errors (including liftA's) as the + * policyB is not informed of policyA's failures at its prime call, but via + * `yield*` it receives all accumulated errors (including policyA's) as the * argument to each subsequent `next(errors)` after its own failed attempts. * - * @param liftA - First lift; its candidates are tried before liftB's. - * @param liftB - Fallback lift; only invoked after liftA is exhausted. - * @returns A combined lift that sequences liftA then liftB. + * @param policyA - First policy; its candidates are tried before policyB's. + * @param policyB - Fallback policy; only invoked after policyA is exhausted. + * @returns A combined policy that sequences policyA then policyB. */ export const fallthrough = >( - liftA: Lift, - liftB: Lift, -): Lift => - async function* (germs, context) { - yield* liftA(germs, context); - yield* liftB(germs, context); + policyA: Policy, + policyB: Policy, +): Policy => + async function* (candidates, context) { + yield* policyA(candidates, context); + yield* policyB(candidates, context); }; diff --git a/packages/sheaves/src/guard.test.ts b/packages/sheaves/src/guard.test.ts index b2cbbea38c..e98b3edcf7 100644 --- a/packages/sheaves/src/guard.test.ts +++ b/packages/sheaves/src/guard.test.ts @@ -6,25 +6,25 @@ import { getInterfaceMethodGuards, getMethodPayload, } from './guard.ts'; -import { makeSection } from './section.ts'; +import { makeHandler } from './section.ts'; import { guardCoversPoint } from './stalk.ts'; describe('collectSheafGuard', () => { it('variable arity: add with 1, 2, and 3 args', () => { const sections = [ - makeSection( + makeHandler( 'Calc:0', M.interface('Calc:0', { add: M.call(M.number()).returns(M.number()) }), { add: (a: number) => a }, ), - makeSection( + makeHandler( 'Calc:1', M.interface('Calc:1', { add: M.call(M.number(), M.number()).returns(M.number()), }), { add: (a: number, b: number) => a + b }, ), - makeSection( + makeHandler( 'Calc:2', M.interface('Calc:2', { add: M.call(M.number(), M.number(), M.number()).returns(M.number()), @@ -44,12 +44,12 @@ describe('collectSheafGuard', () => { it('return guard union', () => { const sections = [ - makeSection( + makeHandler( 'S:0', M.interface('S:0', { f: M.call(M.eq(0)).returns(M.eq(0)) }), { f: (_: number) => 0 }, ), - makeSection( + makeHandler( 'S:1', M.interface('S:1', { f: M.call(M.eq(1)).returns(M.eq(1)) }), { f: (_: number) => 1 }, @@ -67,7 +67,7 @@ describe('collectSheafGuard', () => { it('section with its own optional args: optional preserved in union', () => { const sections = [ - makeSection( + makeHandler( 'Greeter', M.interface('Greeter', { greet: M.callWhen(M.string()) @@ -88,7 +88,7 @@ describe('collectSheafGuard', () => { it('rest arg guard preserved in collected union', () => { const sections = [ - makeSection( + makeHandler( 'Logger', M.interface('Logger', { log: M.call(M.string()).rest(M.string()).returns(M.any()), @@ -108,14 +108,14 @@ describe('collectSheafGuard', () => { it('rest arg guards unioned across sections', () => { const sections = [ - makeSection( + makeHandler( 'A', M.interface('A', { log: M.call(M.string()).rest(M.string()).returns(M.any()), }), { log: (..._args: string[]) => undefined }, ), - makeSection( + makeHandler( 'B', M.interface('B', { log: M.call(M.string()).rest(M.number()).returns(M.any()), @@ -137,12 +137,12 @@ describe('collectSheafGuard', () => { // number of strings via rest. A call ['hello'] is covered by B — the // collected guard must pass it too. const sections = [ - makeSection( + makeHandler( 'AB:0', M.interface('AB:0', { f: M.call(M.number()).returns(M.any()) }), { f: (_: number) => undefined }, ), - makeSection( + makeHandler( 'AB:1', M.interface('AB:1', { f: M.call().rest(M.string()).returns(M.any()) }), { f: (..._args: string[]) => undefined }, @@ -158,7 +158,7 @@ describe('collectSheafGuard', () => { it('multi-method guard collection', () => { const sections = [ - makeSection( + makeHandler( 'Multi:0', M.interface('Multi:0', { translate: M.call(M.string(), M.string()).returns(M.string()), @@ -167,7 +167,7 @@ describe('collectSheafGuard', () => { translate: (from: string, to: string) => `${from}->${to}`, }, ), - makeSection( + makeHandler( 'Multi:1', M.interface('Multi:1', { translate: M.call(M.string(), M.string()).returns(M.string()), diff --git a/packages/sheaves/src/guard.ts b/packages/sheaves/src/guard.ts index cabe70dd94..bb99a2756b 100644 --- a/packages/sheaves/src/guard.ts +++ b/packages/sheaves/src/guard.ts @@ -7,7 +7,7 @@ import { } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard, Pattern } from '@endo/patterns'; -import type { Section } from './types.ts'; +import type { Handler } from './types.ts'; export type MethodGuardPayload = { argGuards: Pattern[]; @@ -87,23 +87,23 @@ const unionGuard = (guards: Pattern[]): Pattern => { }; /** - * Compute the union of all section guards — the open set covered by the sheafified facade. + * Compute the union of all handler guards — the open set covered by the sheafified facade. * - * For each method name across all sections, collects the arg guards at each - * position and produces a union via M.or. Sections with fewer args than + * For each method name across all handlers, collects the arg guards at each + * position and produces a union via M.or. Handlers with fewer args than * the maximum contribute to required args; the remainder become optional. * * @param name - The name for the collected interface guard. - * @param sections - The sections whose guards are collected. - * @returns An interface guard covering all sections. + * @param handlers - The handlers whose guards are collected. + * @returns An interface guard covering all handlers. */ export const collectSheafGuard = ( name: string, - sections: Section[], + handlers: Handler[], ): InterfaceGuard => { const payloadsByMethod = new Map(); - for (const section of sections) { + for (const section of handlers) { const interfaceGuard = section[GET_INTERFACE_GUARD]?.(); if (!interfaceGuard) { continue; diff --git a/packages/sheaves/src/index.ts b/packages/sheaves/src/index.ts index 1f735d3083..23d3f981de 100644 --- a/packages/sheaves/src/index.ts +++ b/packages/sheaves/src/index.ts @@ -1,20 +1,20 @@ export type { - Section, - PresheafSection, - EvaluatedSection, + Handler, + Provider, + Candidate, MetadataSpec, - Lift, - LiftContext, + Policy, + PolicyContext, Sheaf, } from './types.ts'; export { constant, source, callable } from './metadata.ts'; export { sheafify } from './sheafify.ts'; export { - noopLift, - proxyLift, + noopPolicy, + proxyPolicy, withFilter, withRanking, fallthrough, } from './compose.ts'; export { makeRemoteSection } from './remote.ts'; -export { makeSection } from './section.ts'; +export { makeHandler } from './section.ts'; diff --git a/packages/sheaves/src/remote.test.ts b/packages/sheaves/src/remote.test.ts index e4439c8f15..8039e7da0b 100644 --- a/packages/sheaves/src/remote.test.ts +++ b/packages/sheaves/src/remote.test.ts @@ -4,18 +4,18 @@ import { describe, it, expect, vi } from 'vitest'; import { constant } from './metadata.ts'; import { makeRemoteSection } from './remote.ts'; -import { makeSection } from './section.ts'; +import { makeHandler } from './section.ts'; // Mirrors the local-E pattern used throughout sheaf tests: the test // environment has no HandledPromise, so we mock E as a transparent cast. // With this mock, E(exo) === exo, so [GET_INTERFACE_GUARD] and method calls -// resolve locally against the exo — equivalent to a local CapTP loopback. +// resolve locally against the handler — equivalent to a local CapTP loopback. vi.mock('@endo/eventual-send', () => ({ E: (ref: unknown) => ref, })); -const makeRemoteExo = (tag: string) => - makeSection( +const makeRemoteHandler = (tag: string) => + makeHandler( tag, M.interface( tag, @@ -33,16 +33,16 @@ const makeRemoteExo = (tag: string) => describe('makeRemoteSection', () => { it('fetches the interface guard from the remote ref', async () => { - const remoteExo = makeRemoteExo('Remote'); - const { exo } = await makeRemoteSection('Wrapper', remoteExo); - expect(exo[GET_INTERFACE_GUARD]?.()).toStrictEqual( - remoteExo[GET_INTERFACE_GUARD]?.(), + const remoteHandler = makeRemoteHandler('Remote'); + const { handler } = await makeRemoteSection('Wrapper', remoteHandler); + expect(handler[GET_INTERFACE_GUARD]?.()).toStrictEqual( + remoteHandler[GET_INTERFACE_GUARD]?.(), ); }); it('forwards method calls to the remote ref', async () => { const greet = vi.fn(async (name: string) => `Hello, ${name}!`); - const remoteExo = makeSection( + const remoteHandler = makeHandler( 'Remote', M.interface( 'Remote', @@ -52,8 +52,8 @@ describe('makeRemoteSection', () => { { greet }, ); - const { exo } = await makeRemoteSection('Wrapper', remoteExo); - const wrapper = exo as Record< + const { handler } = await makeRemoteSection('Wrapper', remoteHandler); + const wrapper = handler as Record< string, (...a: unknown[]) => Promise >; @@ -66,7 +66,7 @@ describe('makeRemoteSection', () => { it('forwards all methods declared in the guard', async () => { const greet = vi.fn(async (_: string) => ''); const add = vi.fn(async (a: number, b: number) => a + b); - const remoteExo = makeSection( + const remoteHandler = makeHandler( 'Remote', M.interface( 'Remote', @@ -79,8 +79,8 @@ describe('makeRemoteSection', () => { { greet, add }, ); - const { exo } = await makeRemoteSection('Wrapper', remoteExo); - const wrapper = exo as Record< + const { handler } = await makeRemoteSection('Wrapper', remoteHandler); + const wrapper = handler as Record< string, (...a: unknown[]) => Promise >; @@ -91,11 +91,11 @@ describe('makeRemoteSection', () => { expect(add).toHaveBeenCalledWith(2, 3); }); - it('passes metadata through to the section', async () => { + it('passes metadata through to the provider', async () => { const metadata = constant({ mode: 'remote' as const }); const { metadata: actual } = await makeRemoteSection( 'Wrapper', - makeRemoteExo('Remote'), + makeRemoteHandler('Remote'), metadata, ); expect(actual).toBe(metadata); @@ -104,7 +104,7 @@ describe('makeRemoteSection', () => { it('metadata is undefined when not provided', async () => { const { metadata } = await makeRemoteSection( 'Wrapper', - makeRemoteExo('Remote'), + makeRemoteHandler('Remote'), ); expect(metadata).toBeUndefined(); }); diff --git a/packages/sheaves/src/remote.ts b/packages/sheaves/src/remote.ts index 709f1a48c3..a6ae1c29e7 100644 --- a/packages/sheaves/src/remote.ts +++ b/packages/sheaves/src/remote.ts @@ -4,13 +4,13 @@ import { getInterfaceGuardPayload } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; import { ifDefined } from '@metamask/kernel-utils'; -import { makeSection } from './section.ts'; -import type { MetadataSpec, PresheafSection } from './types.ts'; +import { makeHandler } from './section.ts'; +import type { MetadataSpec, Provider } from './types.ts'; /** - * Wrap a remote (CapTP) reference as a PresheafSection. + * Wrap a remote (CapTP) reference as a Provider. * - * The sheaf requires synchronous [GET_INTERFACE_GUARD] access on every section, + * The sheaf requires synchronous [GET_INTERFACE_GUARD] access on every handler, * but remote references are opaque CapTP handles that cannot provide this * synchronously. This function fetches the guard from the remote via E() once * at construction time, then creates a local wrapper exo that carries it and @@ -18,14 +18,14 @@ import type { MetadataSpec, PresheafSection } from './types.ts'; * * @param name - Name for the wrapper exo. * @param remoteRef - The remote reference to forward calls to. - * @param metadata - Optional metadata spec for the presheaf section. - * @returns A PresheafSection whose exo forwards method calls to the remote. + * @param metadata - Optional metadata spec for the provider. + * @returns A Provider whose handler forwards method calls to the remote. */ export const makeRemoteSection = async >( name: string, remoteRef: object, metadata?: MetadataSpec, -): Promise> => { +): Promise> => { const interfaceGuard: InterfaceGuard = await ( E(remoteRef) as unknown as { [GET_INTERFACE_GUARD](): Promise; @@ -52,6 +52,6 @@ export const makeRemoteSection = async >( ]!(...args); } - const exo = makeSection(name, interfaceGuard, handlers); - return ifDefined({ exo, metadata }) as PresheafSection; + const handler = makeHandler(name, interfaceGuard, handlers); + return ifDefined({ handler, metadata }) as Provider; }; diff --git a/packages/sheaves/src/section.ts b/packages/sheaves/src/section.ts index 9b9581b7ac..ff7699fcac 100644 --- a/packages/sheaves/src/section.ts +++ b/packages/sheaves/src/section.ts @@ -1,22 +1,22 @@ import { makeExo } from '@endo/exo'; import type { InterfaceGuard } from '@endo/patterns'; -import type { Section } from './types.ts'; +import type { Handler } from './types.ts'; /** - * Create a local presheaf section from a name, guard, and handler map. + * Create a local handler from a name, guard, and method map. * - * Encapsulates the cast from makeExo's opaque return type to Section. - * Use this when constructing sections for a presheaf; do not use it for + * Encapsulates the cast from makeExo's opaque return type to Handler. + * Use this when constructing handlers for a sheaf; do not use it for * the dispatch exo produced by sheafify itself. * * @param name - Exo tag name. - * @param guard - Interface guard describing the section's methods. + * @param guard - Interface guard describing the handler's methods. * @param handlers - Method handler map. - * @returns A Section suitable for inclusion in a presheaf. + * @returns A Handler suitable for inclusion in a sheaf. */ -export const makeSection = ( +export const makeHandler = ( name: string, guard: InterfaceGuard, handlers: Record unknown>, -): Section => makeExo(name, guard, handlers) as unknown as Section; +): Handler => makeExo(name, guard, handlers) as unknown as Handler; diff --git a/packages/sheaves/src/sheafify.e2e.test.ts b/packages/sheaves/src/sheafify.e2e.test.ts index 05115d40dc..80b307513a 100644 --- a/packages/sheaves/src/sheafify.e2e.test.ts +++ b/packages/sheaves/src/sheafify.e2e.test.ts @@ -2,9 +2,9 @@ import { M } from '@endo/patterns'; import { describe, expect, it, vi } from 'vitest'; import { callable, constant } from './metadata.ts'; -import { makeSection } from './section.ts'; +import { makeHandler } from './section.ts'; import { sheafify } from './sheafify.ts'; -import type { Lift, PresheafSection } from './types.ts'; +import type { Policy, Provider } from './types.ts'; // Thin cast for calling exo methods directly in tests without going through // HandledPromise (which is not available in the test environment). @@ -18,8 +18,8 @@ const E = (obj: unknown) => describe('e2e: cost-optimal routing', () => { it('argmin picks cheapest section, re-sheafification expands landscape', async () => { - const argmin: Lift<{ cost: number }> = async function* (germs) { - yield* [...germs].sort( + const argmin: Policy<{ cost: number }> = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); @@ -28,10 +28,10 @@ describe('e2e: cost-optimal routing', () => { const remote0GetBalance = vi.fn((_acct: string): number => 0); const local1GetBalance = vi.fn((_acct: string): number => 0); - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { // Remote: covers all accounts, expensive - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -42,7 +42,7 @@ describe('e2e: cost-optimal routing', () => { }, { // Local cache: covers only 'alice', cheap - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -53,11 +53,11 @@ describe('e2e: cost-optimal routing', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); - // alice: both sections match, argmin picks local (cost=1) + // alice: both handlers match, argmin picks local (cost=1) await E(wallet).getBalance('alice'); expect(local1GetBalance).toHaveBeenCalledWith('alice'); expect(remote0GetBalance).not.toHaveBeenCalled(); @@ -71,8 +71,8 @@ describe('e2e: cost-optimal routing', () => { // Expand with a broader local cache (cost=2), re-sheafify. const local2GetBalance = vi.fn((_acct: string): number => 0); - sections.push({ - exo: makeSection( + providers.push({ + handler: makeHandler( 'Wallet:2', M.interface('Wallet:2', { getBalance: M.call(M.string()).returns(M.number()), @@ -81,7 +81,7 @@ describe('e2e: cost-optimal routing', () => { ), metadata: constant({ cost: 2 }), }); - wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); @@ -91,7 +91,7 @@ describe('e2e: cost-optimal routing', () => { expect(remote0GetBalance).not.toHaveBeenCalled(); local2GetBalance.mockClear(); - // alice: three sections match, argmin still picks cost=1 + // alice: three handlers match, argmin still picks cost=1 await E(wallet).getBalance('alice'); expect(local1GetBalance).toHaveBeenCalledWith('alice'); expect(remote0GetBalance).not.toHaveBeenCalled(); @@ -112,8 +112,8 @@ describe('e2e: multi-tier capability routing', () => { type Tier = { latencyMs: number; label: string }; - const fastest: Lift = async function* (germs) { - yield* [...germs].sort( + const fastest: Policy = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.latencyMs ?? Infinity) - (b.metadata?.latencyMs ?? Infinity), @@ -121,7 +121,7 @@ describe('e2e: multi-tier capability routing', () => { }; it('routes reads to the fastest matching tier and writes to the only capable section', async () => { - // Shared ledger — all sections read from this, so the sheaf condition + // Shared ledger — all handlers read from this, so the sheaf condition // (effect-equivalence) holds by construction. const ledger: Record = { alice: 1000, @@ -149,12 +149,12 @@ describe('e2e: multi-tier capability routing', () => { }, ); - const sections: PresheafSection[] = []; + const providers: Provider[] = []; // ── Tier 1: Network RPC ────────────────────────────────── // Covers ALL accounts (M.string()), but slow (500ms). - sections.push({ - exo: makeSection( + providers.push({ + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -164,7 +164,7 @@ describe('e2e: multi-tier capability routing', () => { metadata: constant({ latencyMs: 500, label: 'network' }), }); - let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: fastest, }); @@ -180,8 +180,8 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 2: Local state for owned account ──────────────── // Only covers 'alice' (M.eq), 1ms. - sections.push({ - exo: makeSection( + providers.push({ + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -190,7 +190,7 @@ describe('e2e: multi-tier capability routing', () => { ), metadata: constant({ latencyMs: 1, label: 'local' }), }); - wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: fastest, }); @@ -206,8 +206,8 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 3: In-memory cache for specific accounts ──────── // Covers bob and carol via M.or, instant (0ms). - sections.push({ - exo: makeSection( + providers.push({ + handler: makeHandler( 'Wallet:2', M.interface('Wallet:2', { getBalance: M.call(M.or(M.eq('bob'), M.eq('carol'))).returns( @@ -218,7 +218,7 @@ describe('e2e: multi-tier capability routing', () => { ), metadata: constant({ latencyMs: 0, label: 'cache' }), }); - wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: fastest, }); @@ -242,8 +242,8 @@ describe('e2e: multi-tier capability routing', () => { // A write-capable section that declares `transfer`. None of the // read-only tiers above declared it, so writes route here // automatically — the guard algebra handles it, no config needed. - sections.push({ - exo: makeSection( + providers.push({ + handler: makeHandler( 'Wallet:3', M.interface('Wallet:3', { getBalance: M.call(M.string()).returns(M.number()), @@ -258,7 +258,7 @@ describe('e2e: multi-tier capability routing', () => { ), metadata: constant({ latencyMs: 200, label: 'write-backend' }), }); - wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: fastest, }); @@ -284,15 +284,15 @@ describe('e2e: multi-tier capability routing', () => { expect(ledger.bob).toBe(500); }); - it('same germ structure, different lifts, different routing', async () => { + it('same candidate structure, different policies, different routing', async () => { // The lift is the operational policy — swap it and the same - // set of sections produces different routing behavior. + // set of providers produces different routing behavior. const networkGetBalance = vi.fn((_acct: string): number => 0); const mirrorGetBalance = vi.fn((_acct: string): number => 0); - const makeSections = (): PresheafSection[] => [ + const makeProviders = (): Provider[] => [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -302,7 +302,7 @@ describe('e2e: multi-tier capability routing', () => { metadata: constant({ latencyMs: 500, label: 'network' }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -316,7 +316,7 @@ describe('e2e: multi-tier capability routing', () => { // Policy A: fastest wins (mirror at 50ms < network at 500ms). const walletA = sheafify({ name: 'Wallet', - sections: makeSections(), + providers: makeProviders(), }).getGlobalSection({ lift: fastest }); await E(walletA).getBalance('alice'); expect(mirrorGetBalance).toHaveBeenCalledWith('alice'); @@ -324,14 +324,14 @@ describe('e2e: multi-tier capability routing', () => { mirrorGetBalance.mockClear(); // Policy B: highest latency wins (simulate "prefer-canonical-source"). - const slowest: Lift = async function* (germs) { - yield* [...germs].sort( + const slowest: Policy = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (b.metadata?.latencyMs ?? 0) - (a.metadata?.latencyMs ?? 0), ); }; const walletB = sheafify({ name: 'Wallet', - sections: makeSections(), + providers: makeProviders(), }).getGlobalSection({ lift: slowest }); await E(walletB).getBalance('alice'); expect(networkGetBalance).toHaveBeenCalledWith('alice'); @@ -345,18 +345,18 @@ describe('e2e: multi-tier capability routing', () => { describe('e2e: preferAutonomous recovered as degenerate case', () => { it('binary push metadata recovers push-pull lift rule', async () => { - const preferPush: Lift<{ push: boolean }> = async function* (germs) { - yield* germs.filter((germ) => germ.metadata?.push); - yield* germs.filter((germ) => !germ.metadata?.push); + const preferPush: Policy<{ push: boolean }> = async function* (candidates) { + yield* candidates.filter((candidate) => candidate.metadata?.push); + yield* candidates.filter((candidate) => !candidate.metadata?.push); }; const pullGetBalance = vi.fn((_acct: string): number => 0); const pushGetBalance = vi.fn((_acct: string): number => 0); - const sections: PresheafSection<{ push: boolean }>[] = [ + const providers: Provider<{ push: boolean }>[] = [ { // Pull section: M.string() guards, push=false - exo: makeSection( + handler: makeHandler( 'PushPull:0', M.interface('PushPull:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -367,7 +367,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }, { // Push section: narrow guard, push=true - exo: makeSection( + handler: makeHandler( 'PushPull:1', M.interface('PushPull:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -378,7 +378,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }, ]; - const wallet = sheafify({ name: 'PushPull', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'PushPull', providers }).getGlobalSection({ lift: preferPush, }); @@ -400,14 +400,14 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { // --------------------------------------------------------------------------- describe('e2e: callable metadata — cost varies with invocation args', () => { - // Two swap sections whose cost is a function of the swap amount. + // Two swap handlers whose cost is a function of the swap amount. // Swap A is cheaper for small amounts; Swap B is cheaper for large amounts. // Breakeven ≈ 90.9 (1 + 0.1x = 10 + 0.001x → 0.099x = 9 → x ≈ 90.9) type SwapCost = { cost: number }; - const cheapest: Lift = async function* (germs) { - yield* [...germs].sort( + const cheapest: Policy = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); }; @@ -420,9 +420,9 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { (_amount: number, _from: string, _to: string): boolean => true, ); - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'SwapA', M.interface('SwapA', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -437,7 +437,7 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { })), }, { - exo: makeSection( + handler: makeHandler( 'SwapB', M.interface('SwapB', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -453,7 +453,7 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { }, ]; - const facade = sheafify({ name: 'Swap', sections }).getGlobalSection({ + const facade = sheafify({ name: 'Swap', providers }).getGlobalSection({ lift: cheapest, }) as unknown as Record Promise>; @@ -483,9 +483,9 @@ describe('e2e: lift retry on handler failure', () => { }); const fallbackFn = vi.fn((_acct: string): number => 99); - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Primary', M.interface('Primary', { getBalance: M.call(M.string()).returns(M.number()), @@ -495,7 +495,7 @@ describe('e2e: lift retry on handler failure', () => { metadata: constant({ priority: 0 }), }, { - exo: makeSection( + handler: makeHandler( 'Fallback', M.interface('Fallback', { getBalance: M.call(M.string()).returns(M.number()), @@ -508,17 +508,17 @@ describe('e2e: lift retry on handler failure', () => { // Track the error-array length the lift receives after each failed attempt. const errorCountsSeenByLift: number[] = []; - const priorityFirst: Lift = async function* (germs) { - const ordered = [...germs].sort( + const priorityFirst: Policy = async function* (candidates) { + const ordered = [...candidates].sort( (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), ); - for (const germ of ordered) { - const errors: unknown[] = yield germ; + for (const candidate of ordered) { + const errors: unknown[] = yield candidate; errorCountsSeenByLift.push(errors.length); } }; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: priorityFirst, }); @@ -547,8 +547,8 @@ describe('e2e: lift retry on handler failure', () => { vi.fn((_acct: string): number => 99), ]; - const sections: PresheafSection[] = handlers.map((fn, i) => ({ - exo: makeSection( + const providers: Provider[] = handlers.map((fn, i) => ({ + handler: makeHandler( `Section:${i}`, M.interface(`Section:${i}`, { getBalance: M.call(M.string()).returns(M.number()), @@ -559,17 +559,17 @@ describe('e2e: lift retry on handler failure', () => { })); let errorsAfterFirst: unknown[] | undefined; - const priorityFirst: Lift = async function* (germs) { - const ordered = [...germs].sort( + const priorityFirst: Policy = async function* (candidates) { + const ordered = [...candidates].sort( (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), ); - for (const germ of ordered) { - const errors: unknown[] = yield germ; + for (const candidate of ordered) { + const errors: unknown[] = yield candidate; errorsAfterFirst ??= errors; } }; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: priorityFirst, }); @@ -584,9 +584,9 @@ describe('e2e: lift retry on handler failure', () => { it('throws accumulated errors when all candidates fail', async () => { type RouteMeta = { priority: number }; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'A', M.interface('A', { getBalance: M.call(M.string()).returns(M.number()), @@ -600,7 +600,7 @@ describe('e2e: lift retry on handler failure', () => { metadata: constant({ priority: 0 }), }, { - exo: makeSection( + handler: makeHandler( 'B', M.interface('B', { getBalance: M.call(M.string()).returns(M.number()), @@ -615,16 +615,16 @@ describe('e2e: lift retry on handler failure', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs) { - yield* [...germs].sort( + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), ); }, }); await expect(E(wallet).getBalance('alice')).rejects.toThrow( - 'No viable section', + 'No viable handler', ); }); }); diff --git a/packages/sheaves/src/sheafify.string-metadata.test.ts b/packages/sheaves/src/sheafify.string-metadata.test.ts index 501adb3968..78697bc35d 100644 --- a/packages/sheaves/src/sheafify.string-metadata.test.ts +++ b/packages/sheaves/src/sheafify.string-metadata.test.ts @@ -14,9 +14,9 @@ import { M } from '@endo/patterns'; import { describe, it, expect, vi } from 'vitest'; import { source } from './metadata.ts'; -import { makeSection } from './section.ts'; +import { makeHandler } from './section.ts'; import { sheafify } from './sheafify.ts'; -import type { Lift, PresheafSection } from './types.ts'; +import type { Policy, Provider } from './types.ts'; // Thin cast for calling exo methods directly in tests without going through // HandledPromise (which is not available in the test environment). @@ -38,8 +38,8 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { type SwapCost = { cost: number }; - const cheapest: Lift = async function* (germs) { - yield* [...germs].sort( + const cheapest: Policy = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); }; @@ -52,9 +52,9 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { (_amount: number, _from: string, _to: string): boolean => true, ); - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'SwapA', M.interface('SwapA', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -67,7 +67,7 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { metadata: source(`(args) => ({ cost: 1 + 0.1 * args[0] })`), }, { - exo: makeSection( + handler: makeHandler( 'SwapB', M.interface('SwapB', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -83,7 +83,7 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { const facade = sheafify({ name: 'Swap', - sections, + providers, compartment: makeTestCompartment(), }).getGlobalSection({ lift: cheapest }) as unknown as Record< string, diff --git a/packages/sheaves/src/sheafify.test.ts b/packages/sheaves/src/sheafify.test.ts index 1ffcea0b0a..08e9f75607 100644 --- a/packages/sheaves/src/sheafify.test.ts +++ b/packages/sheaves/src/sheafify.test.ts @@ -4,14 +4,9 @@ import { GET_DESCRIPTION } from '@metamask/kernel-utils'; import { describe, it, expect } from 'vitest'; import { constant } from './metadata.ts'; -import { makeSection } from './section.ts'; +import { makeHandler } from './section.ts'; import { sheafify } from './sheafify.ts'; -import type { - EvaluatedSection, - Lift, - LiftContext, - PresheafSection, -} from './types.ts'; +import type { Candidate, Policy, PolicyContext, Provider } from './types.ts'; // Thin cast for calling exo methods directly in tests without going through // HandledPromise (which is not available in the test environment). @@ -27,14 +22,14 @@ describe('sheafify', () => { it('single-section bypass: lift not invoked', async () => { let liftCalled = false; // eslint-disable-next-line require-yield - const lift: Lift<{ cost: number }> = async function* (_germs) { + const lift: Policy<{ cost: number }> = async function* (_candidates) { liftCalled = true; // unreachable — fast path bypasses lift for single section }; - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -45,7 +40,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift, }); expect(await E(wallet).getBalance('alice')).toBe(42); @@ -53,9 +48,9 @@ describe('sheafify', () => { }); it('zero-coverage throws', async () => { - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -66,27 +61,27 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(_germs) { + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(_candidates) { // unreachable — zero-coverage path throws before reaching lift }, }); await expect(E(wallet).getBalance('bob')).rejects.toThrow( - 'No section covers', + 'No handler covers', ); }); it('lift receives metadata and picks winner', async () => { - const argmin: Lift<{ cost: number }> = async function* (germs) { - yield* [...germs].sort( + const argmin: Policy<{ cost: number }> = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); }; - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -96,7 +91,7 @@ describe('sheafify', () => { metadata: constant({ cost: 100 }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -107,7 +102,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); // argmin picks cost=1 section which returns 42 @@ -116,9 +111,9 @@ describe('sheafify', () => { // eslint-disable-next-line vitest/prefer-lowercase-title it('GET_INTERFACE_GUARD returns collected guard', () => { - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -128,7 +123,7 @@ describe('sheafify', () => { metadata: constant({ cost: 100 }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('bob')).returns(M.number()), @@ -139,9 +134,9 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs) { - yield germs[0]!; + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates) { + yield candidates[0]!; }, }); const guard = wallet[GET_INTERFACE_GUARD](); @@ -151,17 +146,17 @@ describe('sheafify', () => { expect(methodGuards).toHaveProperty('getBalance'); }); - it('re-sheafification picks up new sections and methods', async () => { - const argmin: Lift<{ cost: number }> = async function* (germs) { - yield* [...germs].sort( + it('re-sheafification picks up new providers and methods', async () => { + const argmin: Policy<{ cost: number }> = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); }; - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -172,14 +167,14 @@ describe('sheafify', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); expect(await E(wallet).getBalance('alice')).toBe(100); - // Add a cheaper section with a new method to the sections array, re-sheafify. - sections.push({ - exo: makeSection( + // Add a cheaper provider with a new method to the providers array, re-sheafify. + providers.push({ + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -194,7 +189,7 @@ describe('sheafify', () => { ), metadata: constant({ cost: 1 }), }); - wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); @@ -209,36 +204,36 @@ describe('sheafify', () => { }); it('pre-built exo dispatches correctly', async () => { - const exo = makeSection( + const handler = makeHandler( 'bal', M.interface('bal', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, ); - const sections: PresheafSection<{ cost: number }>[] = [ - { exo, metadata: constant({ cost: 1 }) }, + const providers: Provider<{ cost: number }>[] = [ + { handler, metadata: constant({ cost: 1 }) }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs) { - yield germs[0]!; + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates) { + yield candidates[0]!; }, }); expect(await E(wallet).getBalance('alice')).toBe(42); }); it('re-sheafification with pre-built exo picks up new methods', async () => { - const argmin: Lift<{ cost: number }> = async function* (germs) { - yield* [...germs].sort( + const argmin: Policy<{ cost: number }> = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); }; - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -249,13 +244,13 @@ describe('sheafify', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); expect(await E(wallet).getBalance('alice')).toBe(100); // Add a pre-built exo with a cheaper getBalance + new transfer method - const exo = makeSection( + const handler = makeHandler( 'cheap', M.interface('cheap', { getBalance: M.call(M.string()).returns(M.number()), @@ -268,11 +263,11 @@ describe('sheafify', () => { transfer: (_from: string, _to: string, _amt: number) => true, }, ); - sections.push({ - exo, + providers.push({ + handler, metadata: constant({ cost: 1 }), }); - wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); @@ -287,20 +282,20 @@ describe('sheafify', () => { }); it('guard reflected in GET_INTERFACE_GUARD for pre-built exo', () => { - const exo = makeSection( + const handler = makeHandler( 'bal', M.interface('bal', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, ); - const sections: PresheafSection<{ cost: number }>[] = [ - { exo, metadata: constant({ cost: 1 }) }, + const providers: Provider<{ cost: number }>[] = [ + { handler, metadata: constant({ cost: 1 }) }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs) { - yield germs[0]!; + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates) { + yield candidates[0]!; }, }); const guard = wallet[GET_INTERFACE_GUARD](); @@ -312,18 +307,18 @@ describe('sheafify', () => { it('lift receives constraints in context and only distinguishing metadata', async () => { type Meta = { region: string; cost: number }; - let capturedGerms: EvaluatedSection>[] = []; - let capturedContext: LiftContext | undefined; + let capturedCandidates: Candidate>[] = []; + let capturedContext: PolicyContext | undefined; - const spy: Lift = async function* (germs, context) { - capturedGerms = germs; + const spy: Policy = async function* (candidates, context) { + capturedCandidates = candidates; capturedContext = context; - yield germs[0]!; + yield candidates[0]!; }; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -333,7 +328,7 @@ describe('sheafify', () => { metadata: constant({ region: 'us', cost: 100 }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -344,7 +339,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: spy, }); await E(wallet).getBalance('alice'); @@ -354,26 +349,25 @@ describe('sheafify', () => { args: ['alice'], constraints: { region: 'us' }, }); - expect(capturedGerms.map((germ) => germ.metadata)).toStrictEqual([ - { cost: 100 }, - { cost: 1 }, - ]); + expect( + capturedCandidates.map((candidate) => candidate.metadata), + ).toStrictEqual([{ cost: 100 }, { cost: 1 }]); }); it('all-shared metadata yields empty distinguishing metadata', async () => { type Meta = { region: string }; - let capturedGerms: EvaluatedSection>[] = []; - let capturedContext: LiftContext | undefined; + let capturedCandidates: Candidate>[] = []; + let capturedContext: PolicyContext | undefined; - const spy: Lift = async function* (germs, context) { - capturedGerms = germs; + const spy: Policy = async function* (candidates, context) { + capturedCandidates = candidates; capturedContext = context; - yield germs[0]!; + yield candidates[0]!; }; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -383,7 +377,7 @@ describe('sheafify', () => { metadata: constant({ region: 'us' }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -394,23 +388,23 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: spy, }); await E(wallet).getBalance('alice'); - // Both sections collapsed to one germ → lift not invoked + // Both providers collapsed to one candidate → policy not invoked expect(capturedContext).toBeUndefined(); - expect(capturedGerms).toHaveLength(0); + expect(capturedCandidates).toHaveLength(0); }); - it('collapses equivalent presheaf sections by metadata', async () => { + it('collapses equivalent providers by metadata', async () => { type Meta = { cost: number }; let liftCalled = false; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -420,7 +414,7 @@ describe('sheafify', () => { metadata: constant({ cost: 1 }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -431,26 +425,26 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ // eslint-disable-next-line require-yield - async *lift(_germs) { + async *lift(_candidates) { liftCalled = true; }, }); await E(wallet).getBalance('alice'); - // Both sections have identical metadata → collapsed to one germ → lift bypassed + // Both providers have identical metadata → collapsed to one candidate → policy bypassed expect(liftCalled).toBe(false); }); it('extracts shared NaN metadata values into constraints', async () => { type Meta = { cost: number; priority: number }; - let capturedGerms: EvaluatedSection>[] = []; - let capturedContext: LiftContext | undefined; + let capturedCandidates: Candidate>[] = []; + let capturedContext: PolicyContext | undefined; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -460,7 +454,7 @@ describe('sheafify', () => { metadata: constant({ cost: NaN, priority: 0 }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -471,32 +465,31 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs, context) { - capturedGerms = germs; + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates, context) { + capturedCandidates = candidates; capturedContext = context; - yield germs[0]!; + yield candidates[0]!; }, }); await E(wallet).getBalance('alice'); - // NaN is shared across all germs, so it should be extracted as a constraint - // — not left as distinguishing metadata in each germ's options. + // NaN is shared across all candidates, so it should be extracted as a constraint + // — not left as distinguishing metadata in each candidate's options. expect(Number.isNaN(capturedContext?.constraints.cost)).toBe(true); - expect(capturedGerms.map((germ) => germ.metadata)).toStrictEqual([ - { priority: 0 }, - { priority: 1 }, - ]); + expect( + capturedCandidates.map((candidate) => candidate.metadata), + ).toStrictEqual([{ priority: 0 }, { priority: 1 }]); }); it('does not collapse +0 and -0 metadata as equivalent', async () => { type Meta = { cost: number }; - let germCount = 0; + let candidateCount = 0; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -506,7 +499,7 @@ describe('sheafify', () => { metadata: constant({ cost: +0 }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -517,24 +510,24 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs) { - germCount = germs.length; - yield germs[0]!; + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates) { + candidateCount = candidates.length; + yield candidates[0]!; }, }); await E(wallet).getBalance('alice'); - expect(germCount).toBe(2); + expect(candidateCount).toBe(2); }); it('does not collapse Infinity and null metadata as equivalent', async () => { type Meta = { cost: number | null }; - let germCount = 0; + let candidateCount = 0; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -544,7 +537,7 @@ describe('sheafify', () => { metadata: constant({ cost: Infinity }), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -555,24 +548,24 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs) { - germCount = germs.length; - yield germs[0]!; + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates) { + candidateCount = candidates.length; + yield candidates[0]!; }, }); await E(wallet).getBalance('alice'); - expect(germCount).toBe(2); + expect(candidateCount).toBe(2); }); it('collapses no-metadata and empty-object metadata as equivalent', async () => { type Meta = Record; let liftCalled = false; - const sections: PresheafSection[] = [ + const providers: Provider[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -581,7 +574,7 @@ describe('sheafify', () => { ), }, { - exo: makeSection( + handler: makeHandler( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -592,9 +585,9 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ // eslint-disable-next-line require-yield - async *lift(_germs) { + async *lift(_candidates) { liftCalled = true; }, }); @@ -603,24 +596,24 @@ describe('sheafify', () => { expect(liftCalled).toBe(false); }); - it('mixed sections participate in lift', async () => { - const argmin: Lift<{ cost: number }> = async function* (germs) { - yield* [...germs].sort( + it('mixed providers participate in policy', async () => { + const argmin: Policy<{ cost: number }> = async function* (candidates) { + yield* [...candidates].sort( (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), ); }; - const exo = makeSection( + const handler = makeHandler( 'cheap', M.interface('cheap', { getBalance: M.call(M.string()).returns(M.number()), }), { getBalance: (_acct: string) => 42 }, ); - const sections: PresheafSection<{ cost: number }>[] = [ + const providers: Provider<{ cost: number }>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -629,10 +622,10 @@ describe('sheafify', () => { ), metadata: constant({ cost: 100 }), }, - { exo, metadata: constant({ cost: 1 }) }, + { handler, metadata: constant({ cost: 1 }) }, ]; - const wallet = sheafify({ name: 'Wallet', sections }).getGlobalSection({ + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ lift: argmin, }); // argmin picks the exo section (cost=1) @@ -647,9 +640,9 @@ describe('sheafify', () => { returns: { type: 'number' as const, description: 'Balance.' }, }, }; - const sections: PresheafSection>[] = [ + const providers: Provider>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -661,10 +654,10 @@ describe('sheafify', () => { const section = sheafify({ name: 'Wallet', - sections, + providers, }).getDiscoverableGlobalSection({ - async *lift(germs) { - yield germs[0]!; + async *lift(candidates) { + yield candidates[0]!; }, schema, }); @@ -673,9 +666,9 @@ describe('sheafify', () => { }); it('getSection does not expose __getDescription__', () => { - const sections: PresheafSection>[] = [ + const providers: Provider>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -685,9 +678,9 @@ describe('sheafify', () => { }, ]; - const section = sheafify({ name: 'Wallet', sections }).getGlobalSection({ - async *lift(germs) { - yield germs[0]!; + const section = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + async *lift(candidates) { + yield candidates[0]!; }, }); @@ -703,9 +696,9 @@ describe('sheafify', () => { describe('getSection with explicit guard', () => { it('dispatches calls that fall within the explicit guard', async () => { - const sections: PresheafSection>[] = [ + const providers: Provider>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -723,10 +716,10 @@ describe('getSection with explicit guard', () => { getBalance: M.call(M.string()).returns(M.number()), }); - const section = sheafify({ name: 'Wallet', sections }).getSection({ + const section = sheafify({ name: 'Wallet', providers }).getSection({ guard: readGuard, - async *lift(germs) { - yield germs[0]!; + async *lift(candidates) { + yield candidates[0]!; }, }); @@ -734,9 +727,9 @@ describe('getSection with explicit guard', () => { }); it('rejects method calls outside the explicit guard', async () => { - const sections: PresheafSection>[] = [ + const providers: Provider>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -754,10 +747,10 @@ describe('getSection with explicit guard', () => { getBalance: M.call(M.string()).returns(M.number()), }); - const section = sheafify({ name: 'Wallet', sections }).getSection({ + const section = sheafify({ name: 'Wallet', providers }).getSection({ guard: readGuard, - async *lift(germs) { - yield germs[0]!; + async *lift(candidates) { + yield candidates[0]!; }, }); @@ -766,9 +759,9 @@ describe('getSection with explicit guard', () => { }); it('getDiscoverableSection exposes __getDescription__ and obeys explicit guard', async () => { - const sections: PresheafSection>[] = [ + const providers: Provider>[] = [ { - exo: makeSection( + handler: makeHandler( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -790,11 +783,11 @@ describe('getSection with explicit guard', () => { const section = sheafify({ name: 'Wallet', - sections, + providers, }).getDiscoverableSection({ guard: readGuard, - async *lift(germs) { - yield germs[0]!; + async *lift(candidates) { + yield candidates[0]!; }, schema, }); diff --git a/packages/sheaves/src/sheafify.ts b/packages/sheaves/src/sheafify.ts index 6eecbd16e9..11b2ac3fb6 100644 --- a/packages/sheaves/src/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -1,15 +1,15 @@ /** - * Sheafify a presheaf into an authority manager. + * Sheafify a set of providers into an authority manager. * - * `sheafify({ name, sections })` returns a `Sheaf` — an immutable object - * that produces dispatch sections over a fixed presheaf. + * `sheafify({ name, providers })` returns a `Sheaf` — an immutable object + * that produces dispatch sections over a fixed set of providers. * * Each dispatch through a granted section: - * 1. Computes the stalk (getStalk — presheaf sections matching the point) - * 2. Collapses equivalent germs (same metadata → one representative) + * 1. Computes the matching providers (getStalk — providers whose guard covers the point) + * 2. Collapses equivalent candidates (same metadata → one representative) * 3. Decomposes metadata into constraints + options - * 4. Invokes the lift on the distinguished options - * 5. Dispatches to some element of the opted germ + * 4. Invokes the policy on the distinguished options + * 5. Dispatches to some element of the chosen candidate */ import { makeExo } from '@endo/exo'; @@ -24,11 +24,11 @@ import { evaluateMetadata, resolveMetadataSpec } from './metadata.ts'; import type { ResolvedMetadataSpec } from './metadata.ts'; import { getStalk } from './stalk.ts'; import type { - EvaluatedSection, - Lift, - LiftContext, - PresheafSection, - Section, + Candidate, + Handler, + Policy, + PolicyContext, + Provider, Sheaf, } from './types.ts'; @@ -79,18 +79,18 @@ const metadataKey = (metadata: Record): string => { }; /** - * Collapse stalk entries into equivalence classes (germs) by metadata identity. + * Collapse candidates into equivalence classes by metadata identity. * Returns one representative per class; the choice within a class is arbitrary. * - * @param stalk - The stalk entries to collapse. + * @param candidates - The candidates to collapse. * @returns One representative per equivalence class. */ const collapseEquivalent = >( - stalk: EvaluatedSection[], -): EvaluatedSection[] => { + candidates: Candidate[], +): Candidate[] => { const seen = new Set(); - const representatives: EvaluatedSection[] = []; - for (const entry of stalk) { + const representatives: Candidate[] = []; + for (const entry of candidates) { const key = metadataKey(entry.metadata); if (!seen.has(key)) { seen.add(key); @@ -101,28 +101,28 @@ const collapseEquivalent = >( }; /** - * Decompose stalk metadata into constraints (shared by all germs) and - * stripped germs (carrying only distinguishing keys). + * Decompose candidate metadata into constraints (shared by all) and + * stripped candidates (carrying only distinguishing keys). * - * @param stalk - The collapsed stalk entries. - * @returns Constraints and stripped germs. + * @param candidates - The collapsed candidates. + * @returns Constraints and stripped candidates. */ const decomposeMetadata = >( - stalk: EvaluatedSection[], + candidates: Candidate[], ): { constraints: Partial; - stripped: EvaluatedSection>[]; + stripped: Candidate>[]; } => { const constraints: Record = {}; - const head = stalk[0]; + const head = candidates[0]; if (head === undefined) { return { constraints: {} as Partial, stripped: [] }; } const first = head.metadata; for (const key of Object.keys(first)) { const val = first[key]; - const shared = stalk.every((entry) => { + const shared = candidates.every((entry) => { const meta = entry.metadata; return key in meta && Object.is(meta[key], val); }); @@ -131,49 +131,53 @@ const decomposeMetadata = >( } } - const stripped = stalk.map((entry) => { + const stripped = candidates.map((entry) => { const remaining: Record = {}; for (const [key, val] of Object.entries(entry.metadata)) { if (!(key in constraints)) { remaining[key] = val; } } - return { exo: entry.exo, metadata: remaining as Partial }; + return { handler: entry.handler, metadata: remaining as Partial }; }); return { constraints: constraints as Partial, stripped }; }; /** - * Invoke a method on a section exo, throwing if the handler is missing. + * Invoke a method on a handler, throwing if the method is missing. * - * @param exo - The section exo to invoke. + * @param handler - The handler to invoke. * @param method - The method name to call. * @param args - The positional arguments. * @returns The synchronous return value of the method (typically a Promise). */ -const invokeExo = (exo: Section, method: string, args: unknown[]): unknown => { - const obj = exo as Record unknown>; +const invokeHandler = ( + handler: Handler, + method: string, + args: unknown[], +): unknown => { + const obj = handler as Record unknown>; const fn = obj[method]; if (fn === undefined) { - throw new Error(`Section has guard for '${method}' but no handler`); + throw new Error(`Handler has guard for '${method}' but no method`); } return fn.call(obj, ...args); }; -type ResolvedSection> = { - exo: Section; +type ResolvedProvider> = { + handler: Handler; spec: ResolvedMetadataSpec | undefined; }; -const driveLift = async >( - lift: Lift, - germs: EvaluatedSection>[], - context: LiftContext, - invoke: (germ: EvaluatedSection>) => Promise, +const drivePolicy = async >( + policy: Policy, + candidates: Candidate>[], + context: PolicyContext, + invoke: (candidate: Candidate>) => Promise, ): Promise => { const errors: unknown[] = []; - const gen = lift(germs, context); + const gen = policy(candidates, context); let next = await gen.next([...errors]); while (!next.done) { try { @@ -185,7 +189,7 @@ const driveLift = async >( next = await gen.next([...errors]); } } - throw new Error(`No viable section for ${context.method}`, { + throw new Error(`No viable handler for ${context.method}`, { cause: errors, }); }; @@ -194,20 +198,20 @@ export const sheafify = < MetaData extends Record = Record, >({ name, - sections, + providers, compartment, }: { name: string; - sections: PresheafSection[]; + providers: Provider[]; compartment?: { evaluate: (src: string) => unknown }; }): Sheaf => { - const frozenSections: readonly ResolvedSection[] = harden( - sections.map((section) => ({ - exo: section.exo, + const frozenProviders: readonly ResolvedProvider[] = harden( + providers.map((provider) => ({ + handler: provider.handler, spec: - section.metadata === undefined + provider.metadata === undefined ? undefined - : resolveMetadataSpec(section.metadata, compartment), + : resolveMetadataSpec(provider.metadata, compartment), })), ); const buildSection = ({ @@ -216,7 +220,7 @@ export const sheafify = < schema, }: { guard: InterfaceGuard; - lift: Lift; + lift: Policy; schema?: Record; }): object => { const asyncMethodGuards = asyncifyMethodGuards(guard); @@ -231,53 +235,53 @@ export const sheafify = < method: string, args: unknown[], ): Promise => { - const stalk = getStalk(frozenSections, method, args); - const evaluatedStalk: EvaluatedSection[] = stalk.map( - (section) => ({ - exo: section.exo, - metadata: evaluateMetadata(section.spec, args), + const candidates = getStalk(frozenProviders, method, args); + const evaluatedCandidates: Candidate[] = candidates.map( + (provider) => ({ + handler: provider.handler, + metadata: evaluateMetadata(provider.spec, args), }), ); - switch (evaluatedStalk.length) { + switch (evaluatedCandidates.length) { case 0: - throw new Error(`No section covers ${method}(${stringify(args, 0)})`); + throw new Error(`No handler covers ${method}(${stringify(args, 0)})`); case 1: - return invokeExo( - (evaluatedStalk[0] as EvaluatedSection).exo, + return invokeHandler( + (evaluatedCandidates[0] as Candidate).handler, method, args, ); default: { - const collapsed = collapseEquivalent(evaluatedStalk); + const collapsed = collapseEquivalent(evaluatedCandidates); if (collapsed.length === 1) { - return invokeExo( - (collapsed[0] as EvaluatedSection).exo, + return invokeHandler( + (collapsed[0] as Candidate).handler, method, args, ); } const { constraints, stripped } = decomposeMetadata(collapsed); const strippedToCollapsed = new Map( - stripped.map((strippedGerm, i) => [ - strippedGerm, - collapsed[i] as EvaluatedSection, + stripped.map((strippedCandidate, i) => [ + strippedCandidate, + collapsed[i] as Candidate, ]), ); - return driveLift( + return drivePolicy( lift, stripped, { method, args, constraints }, - async (germ) => { - const section = strippedToCollapsed.get(germ); - if (section === undefined) { + async (candidate) => { + const resolved = strippedToCollapsed.get(candidate); + if (resolved === undefined) { throw new Error( - `Lift yielded an unrecognized germ for '${method}'. ` + - `The yielded value must be one of the EvaluatedSection objects ` + - `passed into the lift (object identity, not structural equality). ` + - `Did the lift construct a new object instead of yielding from the germs array?`, + `Policy yielded an unrecognized candidate for '${method}'. ` + + `The yielded value must be one of the Candidate objects ` + + `passed into the policy (object identity, not structural equality). ` + + `Did the policy construct a new object instead of yielding from the candidates array?`, ); } - return invokeExo(section.exo, method, args); + return invokeHandler(resolved.handler, method, args); }, ); } @@ -297,7 +301,7 @@ export const sheafify = < handlers, schema, asyncGuard, - )) as unknown as Section; + )) as unknown as Handler; return exo; }; @@ -305,7 +309,7 @@ export const sheafify = < const unionGuard = (): InterfaceGuard => collectSheafGuard( name, - frozenSections.map(({ exo }) => exo), + frozenProviders.map(({ handler }) => handler), ); const getSection = ({ @@ -313,7 +317,7 @@ export const sheafify = < lift, }: { guard: InterfaceGuard; - lift: Lift; + lift: Policy; }): object => buildSection({ guard, lift }); const getDiscoverableSection = ({ @@ -322,18 +326,18 @@ export const sheafify = < schema, }: { guard: InterfaceGuard; - lift: Lift; + lift: Policy; schema: Record; }): object => buildSection({ guard, lift, schema }); - const getGlobalSection = ({ lift }: { lift: Lift }): object => + const getGlobalSection = ({ lift }: { lift: Policy }): object => buildSection({ guard: unionGuard(), lift }); const getDiscoverableGlobalSection = ({ lift, schema, }: { - lift: Lift; + lift: Policy; schema: Record; }): object => buildSection({ guard: unionGuard(), lift, schema }); diff --git a/packages/sheaves/src/stalk.test.ts b/packages/sheaves/src/stalk.test.ts index 3a7c9abb2f..89266b24c4 100644 --- a/packages/sheaves/src/stalk.test.ts +++ b/packages/sheaves/src/stalk.test.ts @@ -3,30 +3,30 @@ import type { MethodGuard } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; import { constant } from './metadata.ts'; -import { makeSection } from './section.ts'; +import { makeHandler } from './section.ts'; import { getStalk } from './stalk.ts'; -import type { PresheafSection } from './types.ts'; +import type { Provider } from './types.ts'; -const makePresheafSection = ( +const makeProvider = ( tag: string, guards: Record, methods: Record unknown>, metadata: { cost: number }, -): PresheafSection<{ cost: number }> => ({ - exo: makeSection(tag, M.interface(tag, guards), methods), +): Provider<{ cost: number }> => ({ + handler: makeHandler(tag, M.interface(tag, guards), methods), metadata: constant(metadata), }); describe('getStalk', () => { - it('returns matching sections for a method and args', () => { - const sections = [ - makePresheafSection( + it('returns matching providers for a method and args', () => { + const providers = [ + makeProvider( 'A', { add: M.call(M.number(), M.number()).returns(M.number()) }, { add: (a: number, b: number) => a + b }, { cost: 1 }, ), - makePresheafSection( + makeProvider( 'B', { add: M.call(M.number(), M.number()).returns(M.number()) }, { add: (a: number, b: number) => a + b }, @@ -34,19 +34,19 @@ describe('getStalk', () => { ), ]; - const stalk = getStalk(sections, 'add', [1, 2]); - expect(stalk).toHaveLength(2); + const candidates = getStalk(providers, 'add', [1, 2]); + expect(candidates).toHaveLength(2); }); - it('filters out sections without matching method', () => { - const sections = [ - makePresheafSection( + it('filters out providers without matching method', () => { + const providers = [ + makeProvider( 'A', { add: M.call(M.number()).returns(M.number()) }, { add: (a: number) => a }, { cost: 1 }, ), - makePresheafSection( + makeProvider( 'B', { sub: M.call(M.number()).returns(M.number()) }, { sub: (a: number) => -a }, @@ -54,14 +54,14 @@ describe('getStalk', () => { ), ]; - const stalk = getStalk(sections, 'add', [1]); - expect(stalk).toHaveLength(1); - expect(stalk[0]!.metadata).toStrictEqual(constant({ cost: 1 })); + const candidates = getStalk(providers, 'add', [1]); + expect(candidates).toHaveLength(1); + expect(candidates[0]!.metadata).toStrictEqual(constant({ cost: 1 })); }); - it('filters out sections with arg count mismatch', () => { - const sections = [ - makePresheafSection( + it('filters out providers with arg count mismatch', () => { + const providers = [ + makeProvider( 'A', { add: M.call(M.number(), M.number()).returns(M.number()) }, { add: (a: number, b: number) => a + b }, @@ -69,13 +69,13 @@ describe('getStalk', () => { ), ]; - const stalk = getStalk(sections, 'add', [1]); - expect(stalk).toHaveLength(0); + const candidates = getStalk(providers, 'add', [1]); + expect(candidates).toHaveLength(0); }); - it('filters out sections with arg type mismatch', () => { - const sections = [ - makePresheafSection( + it('filters out providers with arg type mismatch', () => { + const providers = [ + makeProvider( 'A', { add: M.call(M.number()).returns(M.number()) }, { add: (a: number) => a }, @@ -83,13 +83,13 @@ describe('getStalk', () => { ), ]; - const stalk = getStalk(sections, 'add', ['not-a-number']); - expect(stalk).toHaveLength(0); + const candidates = getStalk(providers, 'add', ['not-a-number']); + expect(candidates).toHaveLength(0); }); - it('returns empty array when no sections match', () => { - const sections = [ - makePresheafSection( + it('returns empty array when no providers match', () => { + const providers = [ + makeProvider( 'A', { add: M.call(M.eq('alice')).returns(M.number()) }, { add: (_a: string) => 42 }, @@ -97,13 +97,13 @@ describe('getStalk', () => { ), ]; - const stalk = getStalk(sections, 'add', ['bob']); - expect(stalk).toHaveLength(0); + const candidates = getStalk(providers, 'add', ['bob']); + expect(candidates).toHaveLength(0); }); - it('matches sections with optional args when optional arg is provided', () => { - const sections = [ - makePresheafSection( + it('matches providers with optional args when optional arg is provided', () => { + const providers = [ + makeProvider( 'A', { greet: M.callWhen(M.string()) @@ -115,17 +115,17 @@ describe('getStalk', () => { ), ]; - expect(getStalk(sections, 'greet', ['alice'])).toHaveLength(1); - expect(getStalk(sections, 'greet', ['alice', 'hi'])).toHaveLength(1); - expect(getStalk(sections, 'greet', [])).toHaveLength(0); - expect(getStalk(sections, 'greet', ['alice', 'hi', 'extra'])).toHaveLength( + expect(getStalk(providers, 'greet', ['alice'])).toHaveLength(1); + expect(getStalk(providers, 'greet', ['alice', 'hi'])).toHaveLength(1); + expect(getStalk(providers, 'greet', [])).toHaveLength(0); + expect(getStalk(providers, 'greet', ['alice', 'hi', 'extra'])).toHaveLength( 0, ); }); - it('matches sections with rest args', () => { - const sections = [ - makePresheafSection( + it('matches providers with rest args', () => { + const providers = [ + makeProvider( 'A', { log: M.call(M.string()).rest(M.string()).returns(M.any()) }, { log: (..._args: string[]) => undefined }, @@ -133,28 +133,30 @@ describe('getStalk', () => { ), ]; - expect(getStalk(sections, 'log', ['info'])).toHaveLength(1); - expect(getStalk(sections, 'log', ['info', 'msg'])).toHaveLength(1); - expect(getStalk(sections, 'log', ['info', 'msg', 'extra'])).toHaveLength(1); - expect(getStalk(sections, 'log', [])).toHaveLength(0); - expect(getStalk(sections, 'log', [42])).toHaveLength(0); + expect(getStalk(providers, 'log', ['info'])).toHaveLength(1); + expect(getStalk(providers, 'log', ['info', 'msg'])).toHaveLength(1); + expect(getStalk(providers, 'log', ['info', 'msg', 'extra'])).toHaveLength( + 1, + ); + expect(getStalk(providers, 'log', [])).toHaveLength(0); + expect(getStalk(providers, 'log', [42])).toHaveLength(0); }); - it('returns all sections when all match', () => { - const sections = [ - makePresheafSection( + it('returns all providers when all match', () => { + const providers = [ + makeProvider( 'A', { f: M.call(M.string()).returns(M.number()) }, { f: () => 1 }, { cost: 1 }, ), - makePresheafSection( + makeProvider( 'B', { f: M.call(M.string()).returns(M.number()) }, { f: () => 2 }, { cost: 2 }, ), - makePresheafSection( + makeProvider( 'C', { f: M.call(M.string()).returns(M.number()) }, { f: () => 3 }, @@ -162,7 +164,7 @@ describe('getStalk', () => { ), ]; - const stalk = getStalk(sections, 'f', ['hello']); - expect(stalk).toHaveLength(3); + const candidates = getStalk(providers, 'f', ['hello']); + expect(candidates).toHaveLength(3); }); }); diff --git a/packages/sheaves/src/stalk.ts b/packages/sheaves/src/stalk.ts index 01d50c2208..37f0c4f0ec 100644 --- a/packages/sheaves/src/stalk.ts +++ b/packages/sheaves/src/stalk.ts @@ -1,5 +1,5 @@ /** - * Stalk computation: filter presheaf sections by guard matching. + * Stalk computation: filter providers by guard matching. */ import { GET_INTERFACE_GUARD } from '@endo/exo'; @@ -7,7 +7,7 @@ import { matches } from '@endo/patterns'; import type { InterfaceGuard } from '@endo/patterns'; import { getInterfaceMethodGuards, getMethodPayload } from './guard.ts'; -import type { Section } from './types.ts'; +import type { Handler } from './types.ts'; /** * Check whether an interface guard covers the invocation point (method, args). @@ -49,22 +49,22 @@ export const guardCoversPoint = ( }; /** - * Get the stalk at an invocation point. + * Get the matching providers at an invocation point. * - * Returns the presheaf sections whose guards accept the given method + args. + * Returns the providers whose guards accept the given method + args. * - * @param sections - The presheaf sections to filter. + * @param providers - The providers to filter. * @param method - The method name being invoked. * @param args - The arguments to the method invocation. - * @returns The presheaf sections whose guards accept the invocation. + * @returns The providers whose guards accept the invocation. */ -export const getStalk = ( - sections: readonly T[], +export const getStalk = ( + providers: readonly T[], method: string, args: unknown[], ): T[] => { - return sections.filter(({ exo }) => { - const interfaceGuard = exo[GET_INTERFACE_GUARD]?.(); + return providers.filter(({ handler }) => { + const interfaceGuard = handler[GET_INTERFACE_GUARD]?.(); if (!interfaceGuard) { return false; } diff --git a/packages/sheaves/src/types.ts b/packages/sheaves/src/types.ts index be4cdebe24..936d4cd56f 100644 --- a/packages/sheaves/src/types.ts +++ b/packages/sheaves/src/types.ts @@ -1,18 +1,18 @@ /** * Sheaf types: the product decomposition F_sem x F_op. * - * The section (guard + behavior) is the semantic component F_sem. + * The handler (guard + behavior) is the semantic component F_sem. * The metadata is the operational component F_op. * Effect-equivalence (the sheaf condition) is asserted by the interface: - * sections covering the same open set produce the same observable result. + * handlers covering the same open set produce the same observable result. */ import type { GET_INTERFACE_GUARD, Methods } from '@endo/exo'; import type { InterfaceGuard } from '@endo/patterns'; import type { MethodSchema } from '@metamask/kernel-utils'; -/** A section: a capability covering a region of the interface topology. */ -export type Section = Partial & { +/** A handler: a capability covering a region of the interface topology. */ +export type Handler = Partial & { [K in typeof GET_INTERFACE_GUARD]?: (() => InterfaceGuard) | undefined; }; @@ -28,46 +28,46 @@ export type MetadataSpec> = | { kind: 'callable'; fn: (args: unknown[]) => M }; /** - * A presheaf section: a section (F_sem) paired with an optional metadata spec (F_op). + * A provider: a handler (F_sem) paired with an optional metadata spec (F_op). * - * This is the input data to sheafify — an (exo, metadata) pair assigned over - * the open set defined by the exo's guard. + * This is the input data to sheafify — a (handler, metadata) pair assigned over + * the open set defined by the handler's guard. */ -export type PresheafSection> = { - exo: Section; +export type Provider> = { + handler: Handler; metadata?: MetadataSpec; }; /** - * A section with evaluated metadata: the metadata spec has been computed against - * the invocation args, yielding a concrete plain object. Used internally during dispatch - * and as the element type of the `germs` array received by Lift (where each entry - * is already a representative of an equivalence class after collapsing). - * Empty `{}` means no metadata. + * A candidate: a provider with evaluated metadata. The metadata spec has been + * computed against the invocation args, yielding a concrete plain object. Used + * internally during dispatch and as the element type of the array received by + * Policy (where each entry is already a representative of an equivalence class + * after collapsing). Empty `{}` means no metadata. */ -export type EvaluatedSection> = { - exo: Section; +export type Candidate> = { + handler: Handler; metadata: MetaData; }; /** - * Context passed to the lift alongside the stalk. + * Context passed to the policy alongside the candidates. * * `constraints` holds metadata keys whose values are identical across every - * germ in the stalk — these are topologically determined and not a choice. + * candidate — these are topologically determined and not a choice. * Typed as `Partial` because the actual partition is runtime-dependent. */ -export type LiftContext> = { +export type PolicyContext> = { method: string; args: unknown[]; constraints: Partial; }; /** - * Lift: a coroutine that yields candidates in preference order and receives + * Policy: a coroutine that yields candidates in preference order and receives * the accumulated error list after each failed attempt. * - * Each germ carries only distinguishing metadata (options); shared metadata + * Each candidate carries only distinguishing metadata (options); shared metadata * (constraints) is delivered separately in the context. * * The sheaf calls gen.next([]) to prime the coroutine, then gen.next(errors) @@ -76,19 +76,19 @@ export type LiftContext> = { * to yield another candidate or return (signal exhaustion). The sheaf * rethrows the last error when the generator is done. * - * Simple lifts that do not need retry logic can ignore the error input: - * async function*(germs) { yield* [...germs].sort(comparator); } + * Simple policies that do not need retry logic can ignore the error input: + * async function*(candidates) { yield* [...candidates].sort(comparator); } */ -export type Lift> = ( - germs: EvaluatedSection>[], - context: LiftContext, -) => AsyncGenerator>, void, unknown[]>; +export type Policy> = ( + candidates: Candidate>[], + context: PolicyContext, +) => AsyncGenerator>, void, unknown[]>; /** - * A sheaf: an authority manager over a presheaf. + * A sheaf: an authority manager over a set of providers. * * Produces dispatch sections via `getSection`, each routing invocations - * through the presheaf sections supplied at construction time. + * through the providers supplied at construction time. */ export type Sheaf> = { /** @@ -99,7 +99,10 @@ export type Sheaf> = { * signatures through `Sheaf` without knowing the specific guard. * Cast to the interface type at the call site once you know the guard. */ - getSection: (opts: { guard: InterfaceGuard; lift: Lift }) => object; + getSection: (opts: { + guard: InterfaceGuard; + lift: Policy; + }) => object; /** * Produce a discoverable dispatch exo over the given guard. * @@ -107,31 +110,31 @@ export type Sheaf> = { */ getDiscoverableSection: (opts: { guard: InterfaceGuard; - lift: Lift; + lift: Policy; schema: Record; }) => object; /** - * Produce a dispatch exo over the full union guard of all presheaf sections. + * Produce a dispatch exo over the full union guard of all providers. * * Prefer `getSection` with an explicit guard when the guard is statically * known — it makes the capability's scope visible at the call site. Use the - * global variant when sections are assembled dynamically at runtime and the + * global variant when providers are assembled dynamically at runtime and the * union guard is not known until after `sheafify` runs. * * @deprecated Provide an explicit guard via getSection instead. */ - getGlobalSection: (opts: { lift: Lift }) => object; + getGlobalSection: (opts: { lift: Policy }) => object; /** - * Produce a discoverable dispatch exo over the full union guard of all presheaf sections. + * Produce a discoverable dispatch exo over the full union guard of all providers. * * Prefer `getDiscoverableSection` with an explicit guard when the guard is - * statically known. Use the global variant when sections are assembled + * statically known. Use the global variant when providers are assembled * dynamically and the union guard is not known until after `sheafify` runs. * * @deprecated Provide an explicit guard via getDiscoverableSection instead. */ getDiscoverableGlobalSection: (opts: { - lift: Lift; + lift: Policy; schema: Record; }) => object; }; From 148e38f4ebe2571165f51c89bb56344ef37297b2 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 7 May 2026 16:11:40 -0400 Subject: [PATCH 49/68] chore(kernel-utils): remove @endo/eventual-send dep and stale sheaf export --- packages/kernel-utils/package.json | 11 ----------- yarn.lock | 1 - 2 files changed, 12 deletions(-) diff --git a/packages/kernel-utils/package.json b/packages/kernel-utils/package.json index d32b2df934..b57ed11fbc 100644 --- a/packages/kernel-utils/package.json +++ b/packages/kernel-utils/package.json @@ -79,16 +79,6 @@ "default": "./dist/nodejs/index.cjs" } }, - "./sheaf": { - "import": { - "types": "./dist/sheaf/index.d.mts", - "default": "./dist/sheaf/index.mjs" - }, - "require": { - "types": "./dist/sheaf/index.d.cts", - "default": "./dist/sheaf/index.cjs" - } - }, "./vite-plugins": { "import": { "types": "./dist/vite-plugins/index.d.mts", @@ -131,7 +121,6 @@ "@chainsafe/libp2p-yamux": "8.0.1", "@endo/captp": "^4.4.8", "@endo/errors": "^1.2.13", - "@endo/eventual-send": "^1.3.4", "@endo/exo": "^1.5.12", "@endo/patterns": "^1.7.0", "@endo/promise-kit": "^1.1.13", diff --git a/yarn.lock b/yarn.lock index eb6fab2e18..b47de1b5da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2639,7 +2639,6 @@ __metadata: "@chainsafe/libp2p-yamux": "npm:8.0.1" "@endo/captp": "npm:^4.4.8" "@endo/errors": "npm:^1.2.13" - "@endo/eventual-send": "npm:^1.3.4" "@endo/exo": "npm:^1.5.12" "@endo/patterns": "npm:^1.7.0" "@endo/promise-kit": "npm:^1.1.13" From 93a7e9fb473b9b2c6798b4907e9f103b7c104e87 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 7 May 2026 15:30:22 -0400 Subject: [PATCH 50/68] docs(sheaves): rename LIFT.md to POLICY.md --- packages/sheaves/README.md | 2 +- packages/sheaves/documents/LIFT.md | 145 --------------------------- packages/sheaves/documents/POLICY.md | 145 +++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 146 deletions(-) delete mode 100644 packages/sheaves/documents/LIFT.md create mode 100644 packages/sheaves/documents/POLICY.md diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index a9b2164856..edbe01811f 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -6,7 +6,7 @@ Runtime capability routing adapted from sheaf theory in algebraic topology. over a collection of capability providers. The sheaf produces dispatch handlers via `getSection`, each of which routes invocations through the provider set. -See [USAGE.md](./USAGE.md) for annotated examples and [LIFT.md](./LIFT.md) for +See [USAGE.md](./USAGE.md) for annotated examples and [POLICY.md](./POLICY.md) for the policy coroutine protocol and semantic equivalence assumption. ## Install diff --git a/packages/sheaves/documents/LIFT.md b/packages/sheaves/documents/LIFT.md deleted file mode 100644 index 8f0abd9526..0000000000 --- a/packages/sheaves/documents/LIFT.md +++ /dev/null @@ -1,145 +0,0 @@ -# Lift - -The lift is the caller-supplied selection policy in the sheaf dispatch -pipeline. It runs when the stalk at an invocation point contains more than one -germ and the sheaf has no data to resolve the ambiguity on its own. The caller -is responsible for writing a lift that is correct for the sections it will -receive. - -## Coroutine protocol - -The lift is an `async function*` generator, not a plain async function: - -```ts -type Lift = ( - germs: EvaluatedSection>[], - context: LiftContext, -) => AsyncGenerator>, void, unknown[]>; -``` - -The sheaf drives it with the following protocol: - -1. **Prime** — `gen.next([])` starts the coroutine. The empty array is - discarded; it exists only to satisfy the generator type. -2. **Yield** — the coroutine yields a candidate germ to try next. The yielded - value must be an element of the `germs` array received on entry — the sheaf - uses object identity to map it back to the original section. Constructing a - new object with the same shape will throw with a message like "Lift yielded - an unrecognized germ". Sorting with `[...germs].sort(...)` is safe because - sort preserves references; mapping to new objects is not. -3. **Attempt** — the sheaf calls the candidate's exo method. -4. **Success** — the result is returned; the generator is abandoned. -5. **Failure** — the sheaf calls `gen.next(errors)`, passing the ordered list - of every error thrown so far (cumulative, not just the last). The coroutine - receives this as the resolved value of its `yield` expression. -6. **Exhausted** — if the generator returns without yielding, the sheaf - throws `new Error('No viable section for ', { cause: errors })` - where `errors` is the full accumulated list of every failure so far. - -Most lifts express a fixed priority order and can ignore the error input: - -```ts -const awayLift: Lift = async function* (germs) { - yield* germs.filter((g) => g.metadata?.mode === 'delegation'); - yield* germs.filter((g) => g.metadata?.mode === 'call-home'); -}; -``` - -A lift that inspects failure history can read the errors from yield: - -```ts -const cautious: Lift = async function* (germs) { - for (const germ of germs) { - const errors: unknown[] = yield germ; - // errors is the cumulative list of all failures so far, including the one - // just returned for this germ. Inspect to decide whether to continue. - if (errors.some(isUnrecoverable)) return; - } -}; -``` - -## LiftContext - -The second argument to the lift is a `LiftContext`: - -```ts -type LiftContext = { - method: string; // the method being dispatched - args: unknown[]; // the invocation arguments - constraints: Partial; // metadata keys identical across every germ -}; -``` - -**`constraints`** are metadata keys whose values are the same on every germ in -the stalk. Because all candidates agree on these keys, they carry no -information useful for choosing between them — the sheaf strips them from each -germ and delivers them separately. A lift that needs to know, say, the agreed -`protocol` version reads it from `context.constraints.protocol` rather than -from any individual germ. - -**`args`** is available for cases where the lift itself must inspect the call. -Most of the time, however, arg-dependent selection is better expressed as -`callable` metadata on the sections than as conditional logic in the lift. - -Consider a swap where each provider has a different cost curve over volume. -Encode each provider's cost as `callable` metadata evaluated at dispatch time: - -```ts -const sections: PresheafSection[] = [ - { - exo: providerAExo, - metadata: callable((args) => ({ cost: providerACost(Number(args[0])) })), - }, - { - exo: providerBExo, - metadata: callable((args) => ({ cost: providerBCost(Number(args[0])) })), - }, -]; -``` - -By the time the lift runs, `germ.metadata.cost` already holds the concrete -cost for this specific invocation — the swap amount has been applied. A lift -that sorts by cost needs no knowledge of `args` at all: - -```ts -const cheapestFirst: Lift = async function* (germs) { - yield* [...germs].sort( - (a, b) => (a.metadata?.cost ?? 0) - (b.metadata?.cost ?? 0), - ); -}; -``` - -This is why evaluable metadata exists: the arg-dependent logic lives with the -sections that own it, and the lift stays a pure selection policy. - -## Semantic equivalence assumption - -Two sections may differ in real ways — one might use TCP and the other UDP; one -might be a Rust implementation and the other JavaScript. The semantic -equivalence contract does not require that two sections be identical. It -requires only that **if two sections are indistinguishable by metadata, their -differences are immaterial to the authority invoker**. - -The sheaf relies on the following separation of responsibilities: - -- **Section constructors** are responsible for advertising every feature that - matters to callers. If transport protocol, latency tier, cost curve, or - freshness guarantee could affect the invoker's decision, it belongs in the - section's metadata. Omitting a distinguishing feature is a declaration that - callers need not care about it. - -- **Lift constructors** are responsible for selecting among the features that - section constructors have chosen to expose. The lift cannot see what was not - advertised. - -This is a semantic contract, not a runtime enforcement — the sheaf cannot -verify it. When a section constructor omits a feature from metadata, they are -asserting: for any authority invoker using this sheaf, that feature is -irrelevant. If the assertion is wrong, the collapse step may silently discard a -candidate that the lift would have ranked differently. - -> One `getBalance` provider uses a fully-synced node; another uses a lagging -> replica. If both are tagged `{ cost: 1 }` with no freshness field, the -> section constructors are asserting that freshness is immaterial to callers of -> this sheaf. If that is not true, `{ cost: 1, freshness: 'lagging' }` vs -> `{ cost: 1, freshness: 'live' }` would let the lift choose. diff --git a/packages/sheaves/documents/POLICY.md b/packages/sheaves/documents/POLICY.md new file mode 100644 index 0000000000..bc8afd9f85 --- /dev/null +++ b/packages/sheaves/documents/POLICY.md @@ -0,0 +1,145 @@ +# Policy + +The policy is the caller-supplied selection coroutine in the sheaf dispatch +pipeline. It runs when the stalk at an invocation point contains more than one +candidate and the sheaf has no data to resolve the ambiguity on its own. The +caller is responsible for writing a policy that is correct for the providers it +will receive. + +## Coroutine protocol + +The policy is an `async function*` generator, not a plain async function: + +```ts +type Policy = ( + candidates: Candidate>[], + context: PolicyContext, +) => AsyncGenerator>, void, unknown[]>; +``` + +The sheaf drives it with the following protocol: + +1. **Prime** — `gen.next([])` starts the coroutine. The empty array is + discarded; it exists only to satisfy the generator type. +2. **Yield** — the coroutine yields a candidate to try next. The yielded value + must be an element of the `candidates` array received on entry — the sheaf + uses object identity to map it back to the original provider. Constructing a + new object with the same shape will throw with a message like "Policy yielded + an unrecognized candidate". Sorting with `[...candidates].sort(...)` is safe + because sort preserves references; mapping to new objects is not. +3. **Attempt** — the sheaf calls the candidate's handler method. +4. **Success** — the result is returned; the generator is abandoned. +5. **Failure** — the sheaf calls `gen.next(errors)`, passing the ordered list + of every error thrown so far (cumulative, not just the last). The coroutine + receives this as the resolved value of its `yield` expression. +6. **Exhausted** — if the generator returns without yielding, the sheaf throws + `new Error('No viable handler for ', { cause: errors })` where + `errors` is the full accumulated list of every failure so far. + +Most policies express a fixed priority order and can ignore the error input: + +```ts +const awayPolicy: Policy = async function* (candidates) { + yield* candidates.filter((c) => c.metadata?.mode === 'delegation'); + yield* candidates.filter((c) => c.metadata?.mode === 'call-home'); +}; +``` + +A policy that inspects failure history can read the errors from yield: + +```ts +const cautious: Policy = async function* (candidates) { + for (const candidate of candidates) { + const errors: unknown[] = yield candidate; + // errors is the cumulative list of all failures so far, including the one + // just returned for this candidate. Inspect to decide whether to continue. + if (errors.some(isUnrecoverable)) return; + } +}; +``` + +## PolicyContext + +The second argument to the policy is a `PolicyContext`: + +```ts +type PolicyContext = { + method: string; // the method being dispatched + args: unknown[]; // the invocation arguments + constraints: Partial; // metadata keys identical across every candidate +}; +``` + +**`constraints`** are metadata keys whose values are the same on every +candidate in the stalk. Because all candidates agree on these keys, they carry +no information useful for choosing between them — the sheaf strips them from +each candidate and delivers them separately. A policy that needs to know, say, +the agreed `protocol` version reads it from `context.constraints.protocol` +rather than from any individual candidate. + +**`args`** is available for cases where the policy itself must inspect the +call. Most of the time, however, arg-dependent selection is better expressed as +`callable` metadata on the providers than as conditional logic in the policy. + +Consider a swap where each provider has a different cost curve over volume. +Encode each provider's cost as `callable` metadata evaluated at dispatch time: + +```ts +const providers: Provider[] = [ + { + handler: providerAHandler, + metadata: callable((args) => ({ cost: providerACost(Number(args[0])) })), + }, + { + handler: providerBHandler, + metadata: callable((args) => ({ cost: providerBCost(Number(args[0])) })), + }, +]; +``` + +By the time the policy runs, `candidate.metadata.cost` already holds the +concrete cost for this specific invocation — the swap amount has been applied. +A policy that sorts by cost needs no knowledge of `args` at all: + +```ts +const cheapestFirst: Policy = async function* (candidates) { + yield* [...candidates].sort( + (a, b) => (a.metadata?.cost ?? 0) - (b.metadata?.cost ?? 0), + ); +}; +``` + +This is why evaluable metadata exists: the arg-dependent logic lives with the +providers that own it, and the policy stays a pure selection coroutine. + +## Semantic equivalence assumption + +Two providers may differ in real ways — one might use TCP and the other UDP; +one might be a Rust implementation and the other JavaScript. The semantic +equivalence contract does not require that two providers be identical. It +requires only that **if two providers are indistinguishable by metadata, their +differences are immaterial to the authority invoker**. + +The sheaf relies on the following separation of responsibilities: + +- **Provider constructors** are responsible for advertising every feature that + matters to callers. If transport protocol, latency tier, cost curve, or + freshness guarantee could affect the invoker's decision, it belongs in the + provider's metadata. Omitting a distinguishing feature is a declaration that + callers need not care about it. + +- **Policy constructors** are responsible for selecting among the features that + provider constructors have chosen to expose. The policy cannot see what was + not advertised. + +This is a semantic contract, not a runtime enforcement — the sheaf cannot +verify it. When a provider constructor omits a feature from metadata, they are +asserting: for any authority invoker using this sheaf, that feature is +irrelevant. If the assertion is wrong, the collapse step may silently discard a +candidate that the policy would have ranked differently. + +> One `getBalance` provider uses a fully-synced node; another uses a lagging +> replica. If both are tagged `{ cost: 1 }` with no freshness field, the +> provider constructors are asserting that freshness is immaterial to callers +> of this sheaf. If that is not true, `{ cost: 1, freshness: 'lagging' }` vs +> `{ cost: 1, freshness: 'live' }` would let the policy choose. From de059c71d95cbbde0cabef93dea80aa9f8bd0e1e Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 8 May 2026 07:31:15 -0400 Subject: [PATCH 51/68] docs: update changelogs for sheaves and kernel-utils --- packages/kernel-utils/CHANGELOG.md | 7 ----- packages/sheaves/CHANGELOG.md | 42 +++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/kernel-utils/CHANGELOG.md b/packages/kernel-utils/CHANGELOG.md index 3bb536593a..e03168b0f3 100644 --- a/packages/kernel-utils/CHANGELOG.md +++ b/packages/kernel-utils/CHANGELOG.md @@ -11,13 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `getLibp2pRelayHome()` to the `./nodejs` exports, returning the libp2p relay's bookkeeping directory (default `~/.libp2p-relay`, overridable via `$LIBP2P_RELAY_HOME`) — kept separate from `$OCAP_HOME` so one relay can serve daemons with different OCAP_HOMEs ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) - `startRelay()` accepts an optional `publicIp` that is fed to libp2p's `appendAnnounce`, so a relay running on a NAT-backed host can announce its publicly-reachable IPv4 alongside its bound NIC addresses ([#952](https://github.com/MetaMask/ocap-kernel/pull/952)) -- Add `@metamask/kernel-utils/sheaf` subpath export ([#870](https://github.com/MetaMask/ocap-kernel/pull/870)) - - `sheafify()` for building a `Sheaf` capability authority from a collection of `PresheafSection`s, each an exo with optional invocation-dependent metadata - - `constant()`, `source()`, `callable()` for constructing metadata specs (static value, compartment-evaluated code string, and per-call function respectively) - - `noopLift()`, `proxyLift()`, `withFilter()`, `withRanking()`, `fallthrough()` for composing lifts to route and rank sections at dispatch time - - `makeSection()` for constructing a typed exo section from a guard and handler map - - `makeRemoteSection()` for wrapping a remote CapTP reference as a `PresheafSection`, fetching its interface guard once at construction and forwarding method calls via `E()` - - Types: `Sheaf`, `Section`, `PresheafSection`, `EvaluatedSection`, `MetadataSpec`, `Lift`, `LiftContext` ## [0.5.0] diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index 927ff7d580..030edf689d 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -11,4 +11,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial release, extracted from `@metamask/kernel-utils` +- Initial release, extracted from `@metamask/kernel-utils`. +- `sheafify({ name, providers })` — constructs a sheaf authority manager over a + set of capability providers. +- `Provider` type — an input to `sheafify`: a `{ handler, metadata? }` pair + where `handler` is an exo and `metadata` is an optional `MetadataSpec`. +- `Candidate` type — a post-evaluation entry in the stalk: `{ handler, +metadata }` with metadata already resolved from its spec. +- `Handler` type — an exo capability covering a region of the interface + topology. +- `Policy` type — an `async function*` coroutine that receives candidates + and yields them in preference order; drives the sheaf dispatch loop. +- `PolicyContext` type — context passed to the policy: `{ method, args, +constraints }`. +- `MetadataSpec` discriminated union with three variants: `constant`, + `source`, and `callable`. +- `constant(value)` — static metadata spec; value is fixed at construction. +- `source(src)` — source-string metadata spec; compiled via the optional + compartment at `sheafify` construction time. +- `callable(fn)` — callable metadata spec; evaluated per-dispatch with the + invocation arguments. +- `makeHandler(name, guard, methods)` — creates a named, guarded exo handler. +- `makeRemoteSection(tag, remoteRef, metadata?)` — builds a provider that + wraps a remote capability, fetching its interface guard via `E`. +- `noopPolicy` — a policy that yields candidates in the order received. +- `proxyPolicy(gen)` — wraps an existing generator to satisfy the `Policy` + call signature. +- `withFilter(predicate)` — higher-order policy combinator that pre-filters + the candidate list before passing it to the inner policy. +- `withRanking(comparator)` — higher-order policy combinator that pre-sorts + the candidate list before passing it to the inner policy. +- `fallthrough(policyA, policyB)` — composes two policies so that `policyB` + is tried only after `policyA` is exhausted. +- `Sheaf` type — the authority manager returned by `sheafify`; exposes + `getSection`, `getDiscoverableSection`, `getGlobalSection`, and + `getDiscoverableGlobalSection`. +- `documents/POLICY.md` — documents the policy coroutine protocol, + `PolicyContext`, and the semantic equivalence assumption. +- `documents/USAGE.md` — annotated usage examples. + +[Unreleased]: https://github.com/MetaMask/ocap-kernel/compare/@metamask/sheaves@0.1.0...HEAD +[0.1.0]: https://github.com/MetaMask/ocap-kernel/releases/tag/@metamask/sheaves@0.1.0 From 58d8ab3cf2c12010ac8eb1685f1cca554035de08 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 7 May 2026 15:38:56 -0400 Subject: [PATCH 52/68] docs(sheaves): update USAGE.md and POLICY.md terminology --- packages/sheaves/documents/POLICY.md | 10 +-- packages/sheaves/documents/USAGE.md | 109 +++++++++++++-------------- 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/packages/sheaves/documents/POLICY.md b/packages/sheaves/documents/POLICY.md index bc8afd9f85..d6da6fc7a7 100644 --- a/packages/sheaves/documents/POLICY.md +++ b/packages/sheaves/documents/POLICY.md @@ -1,8 +1,8 @@ # Policy The policy is the caller-supplied selection coroutine in the sheaf dispatch -pipeline. It runs when the stalk at an invocation point contains more than one -candidate and the sheaf has no data to resolve the ambiguity on its own. The +pipeline. It runs when more than one candidate matches an invocation and the sheaf +has no data to resolve the ambiguity on its own. The caller is responsible for writing a policy that is correct for the providers it will receive. @@ -70,9 +70,9 @@ type PolicyContext = { }; ``` -**`constraints`** are metadata keys whose values are the same on every -candidate in the stalk. Because all candidates agree on these keys, they carry -no information useful for choosing between them — the sheaf strips them from +**`constraints`** are metadata keys whose values are the same across every +candidate. Because all candidates agree on these keys, they carry no +information useful for choosing between them — the sheaf strips them from each candidate and delivers them separately. A policy that needs to know, say, the agreed `protocol` version reads it from `context.constraints.protocol` rather than from any individual candidate. diff --git a/packages/sheaves/documents/USAGE.md b/packages/sheaves/documents/USAGE.md index 307972faee..847ac11b7e 100644 --- a/packages/sheaves/documents/USAGE.md +++ b/packages/sheaves/documents/USAGE.md @@ -2,19 +2,19 @@ ## Single provider -When there is only one section per invocation point, no lift is needed — the -dispatch short-circuits before the lift is ever called. Provide a no-op lift -as a placeholder: +When there is only one provider per invocation point, no policy is needed — +the dispatch short-circuits before the policy is ever called. Provide a no-op +policy as a placeholder: ```ts import { M } from '@endo/patterns'; -import { sheafify, makeSection, noopLift } from '@metamask/kernel-utils'; +import { sheafify, makeHandler, noopPolicy } from '@metamask/sheaves'; const priceGuard = M.interface('PriceService', { getPrice: M.callWhen(M.await(M.string())).returns(M.await(M.number())), }); -const priceExo = makeSection('PriceService', priceGuard, { +const priceHandler = makeHandler('PriceService', priceGuard, { async getPrice(token) { return fetchPrice(token); }, @@ -22,40 +22,40 @@ const priceExo = makeSection('PriceService', priceGuard, { const sheaf = sheafify({ name: 'PriceService', - sections: [{ exo: priceExo }], + providers: [{ handler: priceHandler }], }); -const section = sheaf.getSection({ guard: priceGuard, lift: noopLift }); -// section is a dispatch exo; call it like any capability +const section = sheaf.getSection({ guard: priceGuard, lift: noopPolicy }); +// section is a dispatch handler; call it like any capability const price = await E(section).getPrice('ETH'); ``` -## Multiple providers with a lift +## Multiple providers with a policy -When the stalk at a given invocation point contains more than one germ, the -sheaf calls the lift to choose. The lift is an `async function*` coroutine that -yields candidates in preference order; it receives accumulated errors as the -argument to each subsequent `.next()` so it can adapt its ranking. +When more than one candidate matches an invocation, the sheaf calls the policy +to choose. The policy is an `async function*` coroutine that yields candidates +in preference order; it receives accumulated errors as the argument to each +subsequent `.next()` so it can adapt its ranking. The idiomatic pattern is a generator that `yield*`s candidates filtered by metadata, expressing priority tiers in source order: ```ts -import { sheafify, constant } from '@metamask/kernel-utils'; -import type { Lift } from '@metamask/kernel-utils'; +import { sheafify, constant } from '@metamask/sheaves'; +import type { Policy } from '@metamask/sheaves'; type WalletMeta = { mode: 'fast' | 'reliable' }; -const preferFast: Lift = async function* (germs) { - yield* germs.filter((g) => g.metadata?.mode === 'fast'); - yield* germs.filter((g) => g.metadata?.mode === 'reliable'); +const preferFast: Policy = async function* (candidates) { + yield* candidates.filter((c) => c.metadata?.mode === 'fast'); + yield* candidates.filter((c) => c.metadata?.mode === 'reliable'); }; const sheaf = sheafify({ name: 'Wallet', - sections: [ - { exo: fastExo, metadata: constant({ mode: 'fast' }) }, - { exo: reliableExo, metadata: constant({ mode: 'reliable' }) }, + providers: [ + { handler: fastHandler, metadata: constant({ mode: 'fast' }) }, + { handler: reliableHandler, metadata: constant({ mode: 'reliable' }) }, ], }); @@ -65,12 +65,12 @@ const section = sheaf.getSection({ guard: clientGuard, lift: preferFast }); The sheaf drives the generator: it primes it with `gen.next([])`, calls the chosen candidate, then passes any thrown errors back as `gen.next(errors)` so -the lift can adapt before yielding the next candidate. +the policy can adapt before yielding the next candidate. Use the `constant`, `source`, or `callable` helpers to build metadata specs: ```ts -import { constant, source, callable } from '@metamask/kernel-utils'; +import { constant, source, callable } from '@metamask/sheaves'; // static value known at construction time constant({ mode: 'fast' }); @@ -86,10 +86,11 @@ callable((args) => ({ cost: Number(args[0]) > 9000 ? 'high' : 'low' })); ## Discoverable sections -`getDiscoverableSection` works like `getSection` but the returned exo exposes -its guard — it can be introspected by the caller to discover what methods and -argument shapes it accepts. Use this when the recipient needs to advertise -capability to a third party. It requires a `schema` map describing each method: +`getDiscoverableSection` works like `getSection` but the returned handler +exposes its guard — it can be introspected by the caller to discover what +methods and argument shapes it accepts. Use this when the recipient needs to +advertise capability to a third party. It requires a `schema` map describing +each method: ```ts import type { MethodSchema } from '@metamask/kernel-utils'; @@ -108,60 +109,56 @@ const section = sheaf.getDiscoverableSection({ `getSection` is the non-discoverable variant (no `schema` required). `getGlobalSection` and `getDiscoverableGlobalSection` derive the guard -automatically from the union of all presheaf sections. They are `@deprecated` -as a nudge toward explicit guards once the caller knows the section set — -explicit guards make the capability's scope visible at the call site. When -sections are assembled dynamically (e.g., rebuilt at runtime from a set of -grants that changes) and the union guard isn't known until after `sheafify` -runs, the global variants are the right choice. +automatically from the union of all providers. They are `@deprecated` as a +nudge toward explicit guards once the caller knows the provider set — explicit +guards make the capability's scope visible at the call site. When providers are +assembled dynamically (e.g., rebuilt at runtime from a set of grants that +changes) and the union guard isn't known until after `sheafify` runs, the +global variants are the right choice. -## Remote sections +## Remote providers -`makeRemoteSection` wraps a CapTP remote reference as a `PresheafSection`, -fetching the remote's guard once at construction and forwarding all calls via -`E()`. This lets you mix local exos and remote capabilities in the same sheaf: +`makeRemoteSection` wraps a CapTP remote reference as a `Provider`, fetching +the remote's guard once at construction and forwarding all calls via `E()`. +This lets you mix local handlers and remote capabilities in the same sheaf: ```ts -import { - makeSection, - makeRemoteSection, - constant, -} from '@metamask/kernel-utils'; +import { makeHandler, makeRemoteSection, constant } from '@metamask/sheaves'; -const remoteSection = await makeRemoteSection( - 'RemoteWallet', // name for the wrapper exo +const remoteProvider = await makeRemoteSection( + 'RemoteWallet', // name for the wrapper handler remoteCapRef, // CapTP reference constant({ mode: 'remote' }), // optional metadata ); const sheaf = sheafify({ name: 'Mixed', - sections: [localSection, remoteSection], + providers: [localProvider, remoteProvider], }); ``` -## Lift composition +## Policy composition -`@metamask/kernel-utils` exports helpers for building lifts from composable -parts, useful when lift logic would otherwise be duplicated across callers: +`@metamask/sheaves` exports helpers for building policies from composable +parts, useful when policy logic would otherwise be duplicated across callers: ```ts import { - proxyLift, + proxyPolicy, withFilter, withRanking, fallthrough, -} from '@metamask/kernel-utils'; +} from '@metamask/sheaves'; ``` -- **`withRanking(comparator)(inner)`** — sort germs by comparator before +- **`withRanking(comparator)(inner)`** — sort candidates by comparator before passing to `inner` -- **`withFilter(predicate)(inner)`** — remove germs that fail `predicate` +- **`withFilter(predicate)(inner)`** — remove candidates that fail `predicate` before passing to `inner` -- **`fallthrough(liftA, liftB)`** — try all candidates from `liftA` first; - if all fail, try `liftB` -- **`proxyLift(gen)`** — forward yielded candidates up and error arrays down +- **`fallthrough(policyA, policyB)`** — try all candidates from `policyA` + first; if all fail, try `policyB` +- **`proxyPolicy(gen)`** — forward yielded candidates up and error arrays down to an already-started generator; useful when you need to add logic between yields (logging, counting, conditional abort). For simple sequential - composition (`fallthrough`, `withFilter`) you do not need `proxyLift` — + composition (`fallthrough`, `withFilter`) you do not need `proxyPolicy` — `yield*` forwards `.next(value)` to the delegated iterator automatically. From fd8f96993f291c23c156482edbb2fb69da4b568e Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 8 May 2026 12:23:47 -0400 Subject: [PATCH 53/68] test(sheaves): add failing tests for prototype-chain bugs in decomposeMetadata Two bugs in decomposeMetadata: 1. `key in constraints` matches prototype-inherited names (e.g. 'constructor') on an empty {} object, causing distinguishing metadata keys to be silently dropped from stripped candidates. 2. `key in meta` matches prototype-inherited names, and when the inherited value happens to equal Object.is the candidate value, the key is wrongly treated as shared and placed in constraints. Co-Authored-By: Claude Sonnet 4.6 --- packages/sheaves/src/sheafify.test.ts | 100 ++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/packages/sheaves/src/sheafify.test.ts b/packages/sheaves/src/sheafify.test.ts index 08e9f75607..c92e3a3068 100644 --- a/packages/sheaves/src/sheafify.test.ts +++ b/packages/sheaves/src/sheafify.test.ts @@ -688,6 +688,106 @@ describe('sheafify', () => { (section as Record)[GET_DESCRIPTION], ).toBeUndefined(); }); + + it('does not drop prototype-named distinguishing metadata keys from stripped candidates', async () => { + // 'constructor' matches Object.prototype.constructor. Naive `key in constraints` + // returns true on an empty {} because of the prototype chain, causing the key to be + // silently dropped from every stripped candidate even though it was never a constraint. + type Meta = Record; + let capturedCandidates: Candidate>[] = []; + let capturedContext: PolicyContext | undefined; + + const spy: Policy = async function* (candidates, context) { + capturedCandidates = candidates; + capturedContext = context; + yield candidates[0]!; + }; + + const providers: Provider[] = [ + { + handler: makeHandler( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ), + metadata: constant({ constructor: 'typeA', cost: 100 }), + }, + { + handler: makeHandler( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ), + metadata: constant({ constructor: 'typeB', cost: 1 }), + }, + ]; + + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + lift: spy, + }); + await E(wallet).getBalance('alice'); + + expect(capturedContext).toStrictEqual({ + method: 'getBalance', + args: ['alice'], + constraints: {}, + }); + expect( + capturedCandidates.map((candidate) => candidate.metadata), + ).toStrictEqual([ + { constructor: 'typeA', cost: 100 }, + { constructor: 'typeB', cost: 1 }, + ]); + }); + + it('does not treat prototype-inherited value as shared when key is absent from some candidates', async () => { + // Provider A has { constructor: Object, cost: 100 }. Provider B has { cost: 1 }. + // Naive `key in meta` finds 'constructor' in B via Object.prototype, and + // Object.is(meta_B['constructor'], Object) is true ({}.constructor === Object), + // so the key is wrongly counted as shared and moved into constraints. + type Meta = Record; + let capturedContext: PolicyContext | undefined; + + const spy: Policy = async function* (candidates, context) { + capturedContext = context; + yield candidates[0]!; + }; + + const providers: Provider[] = [ + { + handler: makeHandler( + 'Wallet:0', + M.interface('Wallet:0', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 100 }, + ), + metadata: constant({ constructor: Object, cost: 100 }), + }, + { + handler: makeHandler( + 'Wallet:1', + M.interface('Wallet:1', { + getBalance: M.call(M.string()).returns(M.number()), + }), + { getBalance: (_acct: string) => 42 }, + ), + metadata: constant({ cost: 1 }), + }, + ]; + + const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + lift: spy, + }); + await E(wallet).getBalance('alice'); + + // 'constructor' is only owned by provider A — must not appear in constraints + expect(capturedContext?.constraints).not.toHaveProperty('constructor'); + }); }); // --------------------------------------------------------------------------- From c2f807a104c1ad3ee32da70696efc295eefed391 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 8 May 2026 12:24:14 -0400 Subject: [PATCH 54/68] fix(sheaves): use Object.hasOwn in decomposeMetadata to avoid prototype chain Replace `key in obj` with `Object.hasOwn(obj, key)` in two places inside decomposeMetadata: - Sharing check (line 127): `key in meta` would pass for prototype-inherited names (e.g. 'constructor'), causing a key to be treated as present in a candidate that does not own it. If the inherited value also happens to match via Object.is, the key is wrongly promoted to a shared constraint. - Stripping step (line 137): `key in constraints` is true for prototype property names on an empty {} object, so any metadata key that shadows a prototype name gets silently dropped from stripped candidates even when it was never added to constraints. Co-Authored-By: Claude Sonnet 4.6 --- packages/sheaves/src/sheafify.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sheaves/src/sheafify.ts b/packages/sheaves/src/sheafify.ts index 11b2ac3fb6..f165579321 100644 --- a/packages/sheaves/src/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -124,7 +124,7 @@ const decomposeMetadata = >( const val = first[key]; const shared = candidates.every((entry) => { const meta = entry.metadata; - return key in meta && Object.is(meta[key], val); + return Object.hasOwn(meta, key) && Object.is(meta[key], val); }); if (shared) { constraints[key] = val; @@ -134,7 +134,7 @@ const decomposeMetadata = >( const stripped = candidates.map((entry) => { const remaining: Record = {}; for (const [key, val] of Object.entries(entry.metadata)) { - if (!(key in constraints)) { + if (!Object.hasOwn(constraints, key)) { remaining[key] = val; } } From 1c1261feecee283b6d1b7679576f129dec4c7266 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:58:58 -0500 Subject: [PATCH 55/68] =?UTF-8?q?refactor(sheaves):=20revert=20section=20?= =?UTF-8?q?=E2=86=92=20handler=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores the original section vocabulary in the package: type Handler becomes Section, makeHandler becomes makeSection, the field on Provider and Candidate becomes exo, and the internal helper invokeHandler becomes invokeExo. Error messages, doc comments, README, USAGE.md, POLICY.md, and CHANGELOG are updated to match. The presheaf → provider, lift → policy, and germ → candidate renames from the prior terminology refactor are kept in place. --- packages/sheaves/CHANGELOG.md | 10 +-- packages/sheaves/README.md | 14 +-- packages/sheaves/documents/POLICY.md | 8 +- packages/sheaves/documents/USAGE.md | 20 ++--- packages/sheaves/src/compose.test.ts | 2 +- packages/sheaves/src/guard.test.ts | 28 +++--- packages/sheaves/src/guard.ts | 16 ++-- packages/sheaves/src/index.ts | 4 +- packages/sheaves/src/remote.test.ts | 32 +++---- packages/sheaves/src/remote.ts | 10 +-- packages/sheaves/src/section.ts | 16 ++-- packages/sheaves/src/sheafify.e2e.test.ts | 40 ++++----- .../src/sheafify.string-metadata.test.ts | 6 +- packages/sheaves/src/sheafify.test.ts | 86 +++++++++---------- packages/sheaves/src/sheafify.ts | 42 ++++----- packages/sheaves/src/stalk.test.ts | 4 +- packages/sheaves/src/stalk.ts | 8 +- packages/sheaves/src/types.ts | 18 ++-- 18 files changed, 180 insertions(+), 184 deletions(-) diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index 030edf689d..653ba60399 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -14,11 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release, extracted from `@metamask/kernel-utils`. - `sheafify({ name, providers })` — constructs a sheaf authority manager over a set of capability providers. -- `Provider` type — an input to `sheafify`: a `{ handler, metadata? }` pair - where `handler` is an exo and `metadata` is an optional `MetadataSpec`. -- `Candidate` type — a post-evaluation entry in the stalk: `{ handler, +- `Provider` type — an input to `sheafify`: a `{ exo, metadata? }` pair + where `exo` is a `Section` and `metadata` is an optional `MetadataSpec`. +- `Candidate` type — a post-evaluation entry in the stalk: `{ exo, metadata }` with metadata already resolved from its spec. -- `Handler` type — an exo capability covering a region of the interface +- `Section` type — an exo capability covering a region of the interface topology. - `Policy` type — an `async function*` coroutine that receives candidates and yields them in preference order; drives the sheaf dispatch loop. @@ -31,7 +31,7 @@ constraints }`. compartment at `sheafify` construction time. - `callable(fn)` — callable metadata spec; evaluated per-dispatch with the invocation arguments. -- `makeHandler(name, guard, methods)` — creates a named, guarded exo handler. +- `makeSection(name, guard, handlers)` — creates a named, guarded `Section` from a method-handler map. - `makeRemoteSection(tag, remoteRef, metadata?)` — builds a provider that wraps a remote capability, fetching its interface guard via `E`. - `noopPolicy` — a policy that yields candidates in the order received. diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index edbe01811f..8e76c2d726 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -3,7 +3,7 @@ Runtime capability routing adapted from sheaf theory in algebraic topology. `sheafify({ name, providers })` produces a **sheaf** — an authority manager -over a collection of capability providers. The sheaf produces dispatch handlers via +over a collection of capability providers. The sheaf produces dispatch sections via `getSection`, each of which routes invocations through the provider set. See [USAGE.md](./USAGE.md) for annotated examples and [POLICY.md](./POLICY.md) for @@ -21,8 +21,8 @@ npm install @metamask/sheaves ## Concepts -**Provider** (`Provider`) — The input data: a capability handler paired with -operational metadata, assigned over the open set defined by the handler's guard. +**Provider** (`Provider`) — The input data: a capability `Section` (exo) paired with +operational metadata, assigned over the open set defined by the exo's guard. This is an element of the presheaf F = F_sem x F_op. > A `getBalance(string)` provider with `{ cost: 100 }` is one provider. A @@ -63,14 +63,14 @@ context. **Sheaf** — The authority manager returned by `sheafify`. Holds the provider data (frozen at construction time) and exposes factory methods that -produce dispatch handlers on demand. +produce dispatch sections on demand. ``` const sheaf = sheafify({ name: 'Wallet', providers }); ``` -- `sheaf.getSection({ guard, lift })` — produce a dispatch handler -- `sheaf.getDiscoverableSection({ guard, lift, schema })` — same, but the handler exposes its guard +- `sheaf.getSection({ guard, lift })` — produce a dispatch section +- `sheaf.getDiscoverableSection({ guard, lift, schema })` — same, but the section exposes its guard ## Dispatch pipeline @@ -83,7 +83,7 @@ collapseEquivalent(stalk) locality condition (quotient by metadata) decomposeMetadata(collapsed) restriction map (constraints / options) policy(candidates, { method, args, operational selection (extra-theoretic) constraints }) -dispatch to chosen.handler evaluation +dispatch to chosen.exo evaluation ``` The pipeline short-circuits at two points: if only one provider matches the diff --git a/packages/sheaves/documents/POLICY.md b/packages/sheaves/documents/POLICY.md index d6da6fc7a7..1f9ad5489e 100644 --- a/packages/sheaves/documents/POLICY.md +++ b/packages/sheaves/documents/POLICY.md @@ -27,13 +27,13 @@ The sheaf drives it with the following protocol: new object with the same shape will throw with a message like "Policy yielded an unrecognized candidate". Sorting with `[...candidates].sort(...)` is safe because sort preserves references; mapping to new objects is not. -3. **Attempt** — the sheaf calls the candidate's handler method. +3. **Attempt** — the sheaf calls the candidate's exo method. 4. **Success** — the result is returned; the generator is abandoned. 5. **Failure** — the sheaf calls `gen.next(errors)`, passing the ordered list of every error thrown so far (cumulative, not just the last). The coroutine receives this as the resolved value of its `yield` expression. 6. **Exhausted** — if the generator returns without yielding, the sheaf throws - `new Error('No viable handler for ', { cause: errors })` where + `new Error('No viable section for ', { cause: errors })` where `errors` is the full accumulated list of every failure so far. Most policies express a fixed priority order and can ignore the error input: @@ -87,11 +87,11 @@ Encode each provider's cost as `callable` metadata evaluated at dispatch time: ```ts const providers: Provider[] = [ { - handler: providerAHandler, + exo: providerASection, metadata: callable((args) => ({ cost: providerACost(Number(args[0])) })), }, { - handler: providerBHandler, + exo: providerBSection, metadata: callable((args) => ({ cost: providerBCost(Number(args[0])) })), }, ]; diff --git a/packages/sheaves/documents/USAGE.md b/packages/sheaves/documents/USAGE.md index 847ac11b7e..7be843e2f9 100644 --- a/packages/sheaves/documents/USAGE.md +++ b/packages/sheaves/documents/USAGE.md @@ -8,13 +8,13 @@ policy as a placeholder: ```ts import { M } from '@endo/patterns'; -import { sheafify, makeHandler, noopPolicy } from '@metamask/sheaves'; +import { sheafify, makeSection, noopPolicy } from '@metamask/sheaves'; const priceGuard = M.interface('PriceService', { getPrice: M.callWhen(M.await(M.string())).returns(M.await(M.number())), }); -const priceHandler = makeHandler('PriceService', priceGuard, { +const priceSection = makeSection('PriceService', priceGuard, { async getPrice(token) { return fetchPrice(token); }, @@ -22,11 +22,11 @@ const priceHandler = makeHandler('PriceService', priceGuard, { const sheaf = sheafify({ name: 'PriceService', - providers: [{ handler: priceHandler }], + providers: [{ exo: priceSection }], }); const section = sheaf.getSection({ guard: priceGuard, lift: noopPolicy }); -// section is a dispatch handler; call it like any capability +// section is a dispatch section; call it like any capability const price = await E(section).getPrice('ETH'); ``` @@ -54,8 +54,8 @@ const preferFast: Policy = async function* (candidates) { const sheaf = sheafify({ name: 'Wallet', providers: [ - { handler: fastHandler, metadata: constant({ mode: 'fast' }) }, - { handler: reliableHandler, metadata: constant({ mode: 'reliable' }) }, + { exo: fastSection, metadata: constant({ mode: 'fast' }) }, + { exo: reliableSection, metadata: constant({ mode: 'reliable' }) }, ], }); @@ -86,7 +86,7 @@ callable((args) => ({ cost: Number(args[0]) > 9000 ? 'high' : 'low' })); ## Discoverable sections -`getDiscoverableSection` works like `getSection` but the returned handler +`getDiscoverableSection` works like `getSection` but the returned section exposes its guard — it can be introspected by the caller to discover what methods and argument shapes it accepts. Use this when the recipient needs to advertise capability to a third party. It requires a `schema` map describing @@ -120,13 +120,13 @@ global variants are the right choice. `makeRemoteSection` wraps a CapTP remote reference as a `Provider`, fetching the remote's guard once at construction and forwarding all calls via `E()`. -This lets you mix local handlers and remote capabilities in the same sheaf: +This lets you mix local sections and remote capabilities in the same sheaf: ```ts -import { makeHandler, makeRemoteSection, constant } from '@metamask/sheaves'; +import { makeSection, makeRemoteSection, constant } from '@metamask/sheaves'; const remoteProvider = await makeRemoteSection( - 'RemoteWallet', // name for the wrapper handler + 'RemoteWallet', // name for the wrapper section remoteCapRef, // CapTP reference constant({ mode: 'remote' }), // optional metadata ); diff --git a/packages/sheaves/src/compose.test.ts b/packages/sheaves/src/compose.test.ts index 87642475d2..5f2dc3c4f6 100644 --- a/packages/sheaves/src/compose.test.ts +++ b/packages/sheaves/src/compose.test.ts @@ -16,7 +16,7 @@ type Meta = { id: string; cost: number }; type C = Candidate>; const makeCandidate = (id: string, cost = 0): C => ({ - handler: {} as C['handler'], + exo: {} as C['exo'], metadata: { id, cost }, }); diff --git a/packages/sheaves/src/guard.test.ts b/packages/sheaves/src/guard.test.ts index e98b3edcf7..b2cbbea38c 100644 --- a/packages/sheaves/src/guard.test.ts +++ b/packages/sheaves/src/guard.test.ts @@ -6,25 +6,25 @@ import { getInterfaceMethodGuards, getMethodPayload, } from './guard.ts'; -import { makeHandler } from './section.ts'; +import { makeSection } from './section.ts'; import { guardCoversPoint } from './stalk.ts'; describe('collectSheafGuard', () => { it('variable arity: add with 1, 2, and 3 args', () => { const sections = [ - makeHandler( + makeSection( 'Calc:0', M.interface('Calc:0', { add: M.call(M.number()).returns(M.number()) }), { add: (a: number) => a }, ), - makeHandler( + makeSection( 'Calc:1', M.interface('Calc:1', { add: M.call(M.number(), M.number()).returns(M.number()), }), { add: (a: number, b: number) => a + b }, ), - makeHandler( + makeSection( 'Calc:2', M.interface('Calc:2', { add: M.call(M.number(), M.number(), M.number()).returns(M.number()), @@ -44,12 +44,12 @@ describe('collectSheafGuard', () => { it('return guard union', () => { const sections = [ - makeHandler( + makeSection( 'S:0', M.interface('S:0', { f: M.call(M.eq(0)).returns(M.eq(0)) }), { f: (_: number) => 0 }, ), - makeHandler( + makeSection( 'S:1', M.interface('S:1', { f: M.call(M.eq(1)).returns(M.eq(1)) }), { f: (_: number) => 1 }, @@ -67,7 +67,7 @@ describe('collectSheafGuard', () => { it('section with its own optional args: optional preserved in union', () => { const sections = [ - makeHandler( + makeSection( 'Greeter', M.interface('Greeter', { greet: M.callWhen(M.string()) @@ -88,7 +88,7 @@ describe('collectSheafGuard', () => { it('rest arg guard preserved in collected union', () => { const sections = [ - makeHandler( + makeSection( 'Logger', M.interface('Logger', { log: M.call(M.string()).rest(M.string()).returns(M.any()), @@ -108,14 +108,14 @@ describe('collectSheafGuard', () => { it('rest arg guards unioned across sections', () => { const sections = [ - makeHandler( + makeSection( 'A', M.interface('A', { log: M.call(M.string()).rest(M.string()).returns(M.any()), }), { log: (..._args: string[]) => undefined }, ), - makeHandler( + makeSection( 'B', M.interface('B', { log: M.call(M.string()).rest(M.number()).returns(M.any()), @@ -137,12 +137,12 @@ describe('collectSheafGuard', () => { // number of strings via rest. A call ['hello'] is covered by B — the // collected guard must pass it too. const sections = [ - makeHandler( + makeSection( 'AB:0', M.interface('AB:0', { f: M.call(M.number()).returns(M.any()) }), { f: (_: number) => undefined }, ), - makeHandler( + makeSection( 'AB:1', M.interface('AB:1', { f: M.call().rest(M.string()).returns(M.any()) }), { f: (..._args: string[]) => undefined }, @@ -158,7 +158,7 @@ describe('collectSheafGuard', () => { it('multi-method guard collection', () => { const sections = [ - makeHandler( + makeSection( 'Multi:0', M.interface('Multi:0', { translate: M.call(M.string(), M.string()).returns(M.string()), @@ -167,7 +167,7 @@ describe('collectSheafGuard', () => { translate: (from: string, to: string) => `${from}->${to}`, }, ), - makeHandler( + makeSection( 'Multi:1', M.interface('Multi:1', { translate: M.call(M.string(), M.string()).returns(M.string()), diff --git a/packages/sheaves/src/guard.ts b/packages/sheaves/src/guard.ts index bb99a2756b..cabe70dd94 100644 --- a/packages/sheaves/src/guard.ts +++ b/packages/sheaves/src/guard.ts @@ -7,7 +7,7 @@ import { } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard, Pattern } from '@endo/patterns'; -import type { Handler } from './types.ts'; +import type { Section } from './types.ts'; export type MethodGuardPayload = { argGuards: Pattern[]; @@ -87,23 +87,23 @@ const unionGuard = (guards: Pattern[]): Pattern => { }; /** - * Compute the union of all handler guards — the open set covered by the sheafified facade. + * Compute the union of all section guards — the open set covered by the sheafified facade. * - * For each method name across all handlers, collects the arg guards at each - * position and produces a union via M.or. Handlers with fewer args than + * For each method name across all sections, collects the arg guards at each + * position and produces a union via M.or. Sections with fewer args than * the maximum contribute to required args; the remainder become optional. * * @param name - The name for the collected interface guard. - * @param handlers - The handlers whose guards are collected. - * @returns An interface guard covering all handlers. + * @param sections - The sections whose guards are collected. + * @returns An interface guard covering all sections. */ export const collectSheafGuard = ( name: string, - handlers: Handler[], + sections: Section[], ): InterfaceGuard => { const payloadsByMethod = new Map(); - for (const section of handlers) { + for (const section of sections) { const interfaceGuard = section[GET_INTERFACE_GUARD]?.(); if (!interfaceGuard) { continue; diff --git a/packages/sheaves/src/index.ts b/packages/sheaves/src/index.ts index 23d3f981de..ab591132b5 100644 --- a/packages/sheaves/src/index.ts +++ b/packages/sheaves/src/index.ts @@ -1,5 +1,5 @@ export type { - Handler, + Section, Provider, Candidate, MetadataSpec, @@ -17,4 +17,4 @@ export { fallthrough, } from './compose.ts'; export { makeRemoteSection } from './remote.ts'; -export { makeHandler } from './section.ts'; +export { makeSection } from './section.ts'; diff --git a/packages/sheaves/src/remote.test.ts b/packages/sheaves/src/remote.test.ts index 8039e7da0b..138b1c1323 100644 --- a/packages/sheaves/src/remote.test.ts +++ b/packages/sheaves/src/remote.test.ts @@ -4,18 +4,18 @@ import { describe, it, expect, vi } from 'vitest'; import { constant } from './metadata.ts'; import { makeRemoteSection } from './remote.ts'; -import { makeHandler } from './section.ts'; +import { makeSection } from './section.ts'; // Mirrors the local-E pattern used throughout sheaf tests: the test // environment has no HandledPromise, so we mock E as a transparent cast. // With this mock, E(exo) === exo, so [GET_INTERFACE_GUARD] and method calls -// resolve locally against the handler — equivalent to a local CapTP loopback. +// resolve locally against the exo — equivalent to a local CapTP loopback. vi.mock('@endo/eventual-send', () => ({ E: (ref: unknown) => ref, })); -const makeRemoteHandler = (tag: string) => - makeHandler( +const makeRemoteExo = (tag: string) => + makeSection( tag, M.interface( tag, @@ -33,16 +33,16 @@ const makeRemoteHandler = (tag: string) => describe('makeRemoteSection', () => { it('fetches the interface guard from the remote ref', async () => { - const remoteHandler = makeRemoteHandler('Remote'); - const { handler } = await makeRemoteSection('Wrapper', remoteHandler); - expect(handler[GET_INTERFACE_GUARD]?.()).toStrictEqual( - remoteHandler[GET_INTERFACE_GUARD]?.(), + const remoteExo = makeRemoteExo('Remote'); + const { exo } = await makeRemoteSection('Wrapper', remoteExo); + expect(exo[GET_INTERFACE_GUARD]?.()).toStrictEqual( + remoteExo[GET_INTERFACE_GUARD]?.(), ); }); it('forwards method calls to the remote ref', async () => { const greet = vi.fn(async (name: string) => `Hello, ${name}!`); - const remoteHandler = makeHandler( + const remoteExo = makeSection( 'Remote', M.interface( 'Remote', @@ -52,8 +52,8 @@ describe('makeRemoteSection', () => { { greet }, ); - const { handler } = await makeRemoteSection('Wrapper', remoteHandler); - const wrapper = handler as Record< + const { exo } = await makeRemoteSection('Wrapper', remoteExo); + const wrapper = exo as Record< string, (...a: unknown[]) => Promise >; @@ -66,7 +66,7 @@ describe('makeRemoteSection', () => { it('forwards all methods declared in the guard', async () => { const greet = vi.fn(async (_: string) => ''); const add = vi.fn(async (a: number, b: number) => a + b); - const remoteHandler = makeHandler( + const remoteExo = makeSection( 'Remote', M.interface( 'Remote', @@ -79,8 +79,8 @@ describe('makeRemoteSection', () => { { greet, add }, ); - const { handler } = await makeRemoteSection('Wrapper', remoteHandler); - const wrapper = handler as Record< + const { exo } = await makeRemoteSection('Wrapper', remoteExo); + const wrapper = exo as Record< string, (...a: unknown[]) => Promise >; @@ -95,7 +95,7 @@ describe('makeRemoteSection', () => { const metadata = constant({ mode: 'remote' as const }); const { metadata: actual } = await makeRemoteSection( 'Wrapper', - makeRemoteHandler('Remote'), + makeRemoteExo('Remote'), metadata, ); expect(actual).toBe(metadata); @@ -104,7 +104,7 @@ describe('makeRemoteSection', () => { it('metadata is undefined when not provided', async () => { const { metadata } = await makeRemoteSection( 'Wrapper', - makeRemoteHandler('Remote'), + makeRemoteExo('Remote'), ); expect(metadata).toBeUndefined(); }); diff --git a/packages/sheaves/src/remote.ts b/packages/sheaves/src/remote.ts index a6ae1c29e7..09285b4c51 100644 --- a/packages/sheaves/src/remote.ts +++ b/packages/sheaves/src/remote.ts @@ -4,13 +4,13 @@ import { getInterfaceGuardPayload } from '@endo/patterns'; import type { InterfaceGuard, MethodGuard } from '@endo/patterns'; import { ifDefined } from '@metamask/kernel-utils'; -import { makeHandler } from './section.ts'; +import { makeSection } from './section.ts'; import type { MetadataSpec, Provider } from './types.ts'; /** * Wrap a remote (CapTP) reference as a Provider. * - * The sheaf requires synchronous [GET_INTERFACE_GUARD] access on every handler, + * The sheaf requires synchronous [GET_INTERFACE_GUARD] access on every section, * but remote references are opaque CapTP handles that cannot provide this * synchronously. This function fetches the guard from the remote via E() once * at construction time, then creates a local wrapper exo that carries it and @@ -19,7 +19,7 @@ import type { MetadataSpec, Provider } from './types.ts'; * @param name - Name for the wrapper exo. * @param remoteRef - The remote reference to forward calls to. * @param metadata - Optional metadata spec for the provider. - * @returns A Provider whose handler forwards method calls to the remote. + * @returns A Provider whose exo forwards method calls to the remote. */ export const makeRemoteSection = async >( name: string, @@ -52,6 +52,6 @@ export const makeRemoteSection = async >( ]!(...args); } - const handler = makeHandler(name, interfaceGuard, handlers); - return ifDefined({ handler, metadata }) as Provider; + const exo = makeSection(name, interfaceGuard, handlers); + return ifDefined({ exo, metadata }) as Provider; }; diff --git a/packages/sheaves/src/section.ts b/packages/sheaves/src/section.ts index ff7699fcac..c7a59635ac 100644 --- a/packages/sheaves/src/section.ts +++ b/packages/sheaves/src/section.ts @@ -1,22 +1,22 @@ import { makeExo } from '@endo/exo'; import type { InterfaceGuard } from '@endo/patterns'; -import type { Handler } from './types.ts'; +import type { Section } from './types.ts'; /** - * Create a local handler from a name, guard, and method map. + * Create a local section from a name, guard, and handler map. * - * Encapsulates the cast from makeExo's opaque return type to Handler. - * Use this when constructing handlers for a sheaf; do not use it for + * Encapsulates the cast from makeExo's opaque return type to Section. + * Use this when constructing sections for a sheaf; do not use it for * the dispatch exo produced by sheafify itself. * * @param name - Exo tag name. - * @param guard - Interface guard describing the handler's methods. + * @param guard - Interface guard describing the section's methods. * @param handlers - Method handler map. - * @returns A Handler suitable for inclusion in a sheaf. + * @returns A Section suitable for inclusion in a sheaf. */ -export const makeHandler = ( +export const makeSection = ( name: string, guard: InterfaceGuard, handlers: Record unknown>, -): Handler => makeExo(name, guard, handlers) as unknown as Handler; +): Section => makeExo(name, guard, handlers) as unknown as Section; diff --git a/packages/sheaves/src/sheafify.e2e.test.ts b/packages/sheaves/src/sheafify.e2e.test.ts index 80b307513a..7d730ec3b7 100644 --- a/packages/sheaves/src/sheafify.e2e.test.ts +++ b/packages/sheaves/src/sheafify.e2e.test.ts @@ -2,7 +2,7 @@ import { M } from '@endo/patterns'; import { describe, expect, it, vi } from 'vitest'; import { callable, constant } from './metadata.ts'; -import { makeHandler } from './section.ts'; +import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; import type { Policy, Provider } from './types.ts'; @@ -31,7 +31,7 @@ describe('e2e: cost-optimal routing', () => { const providers: Provider<{ cost: number }>[] = [ { // Remote: covers all accounts, expensive - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -42,7 +42,7 @@ describe('e2e: cost-optimal routing', () => { }, { // Local cache: covers only 'alice', cheap - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -72,7 +72,7 @@ describe('e2e: cost-optimal routing', () => { // Expand with a broader local cache (cost=2), re-sheafify. const local2GetBalance = vi.fn((_acct: string): number => 0); providers.push({ - handler: makeHandler( + exo: makeSection( 'Wallet:2', M.interface('Wallet:2', { getBalance: M.call(M.string()).returns(M.number()), @@ -154,7 +154,7 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 1: Network RPC ────────────────────────────────── // Covers ALL accounts (M.string()), but slow (500ms). providers.push({ - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -181,7 +181,7 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 2: Local state for owned account ──────────────── // Only covers 'alice' (M.eq), 1ms. providers.push({ - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -207,7 +207,7 @@ describe('e2e: multi-tier capability routing', () => { // ── Tier 3: In-memory cache for specific accounts ──────── // Covers bob and carol via M.or, instant (0ms). providers.push({ - handler: makeHandler( + exo: makeSection( 'Wallet:2', M.interface('Wallet:2', { getBalance: M.call(M.or(M.eq('bob'), M.eq('carol'))).returns( @@ -243,7 +243,7 @@ describe('e2e: multi-tier capability routing', () => { // read-only tiers above declared it, so writes route here // automatically — the guard algebra handles it, no config needed. providers.push({ - handler: makeHandler( + exo: makeSection( 'Wallet:3', M.interface('Wallet:3', { getBalance: M.call(M.string()).returns(M.number()), @@ -292,7 +292,7 @@ describe('e2e: multi-tier capability routing', () => { const makeProviders = (): Provider[] => [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -302,7 +302,7 @@ describe('e2e: multi-tier capability routing', () => { metadata: constant({ latencyMs: 500, label: 'network' }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -356,7 +356,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { const providers: Provider<{ push: boolean }>[] = [ { // Pull section: M.string() guards, push=false - handler: makeHandler( + exo: makeSection( 'PushPull:0', M.interface('PushPull:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -367,7 +367,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }, { // Push section: narrow guard, push=true - handler: makeHandler( + exo: makeSection( 'PushPull:1', M.interface('PushPull:1', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -422,7 +422,7 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'SwapA', M.interface('SwapA', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -437,7 +437,7 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { })), }, { - handler: makeHandler( + exo: makeSection( 'SwapB', M.interface('SwapB', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -485,7 +485,7 @@ describe('e2e: lift retry on handler failure', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Primary', M.interface('Primary', { getBalance: M.call(M.string()).returns(M.number()), @@ -495,7 +495,7 @@ describe('e2e: lift retry on handler failure', () => { metadata: constant({ priority: 0 }), }, { - handler: makeHandler( + exo: makeSection( 'Fallback', M.interface('Fallback', { getBalance: M.call(M.string()).returns(M.number()), @@ -548,7 +548,7 @@ describe('e2e: lift retry on handler failure', () => { ]; const providers: Provider[] = handlers.map((fn, i) => ({ - handler: makeHandler( + exo: makeSection( `Section:${i}`, M.interface(`Section:${i}`, { getBalance: M.call(M.string()).returns(M.number()), @@ -586,7 +586,7 @@ describe('e2e: lift retry on handler failure', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'A', M.interface('A', { getBalance: M.call(M.string()).returns(M.number()), @@ -600,7 +600,7 @@ describe('e2e: lift retry on handler failure', () => { metadata: constant({ priority: 0 }), }, { - handler: makeHandler( + exo: makeSection( 'B', M.interface('B', { getBalance: M.call(M.string()).returns(M.number()), @@ -624,7 +624,7 @@ describe('e2e: lift retry on handler failure', () => { }); await expect(E(wallet).getBalance('alice')).rejects.toThrow( - 'No viable handler', + 'No viable section', ); }); }); diff --git a/packages/sheaves/src/sheafify.string-metadata.test.ts b/packages/sheaves/src/sheafify.string-metadata.test.ts index 78697bc35d..d04ff029ef 100644 --- a/packages/sheaves/src/sheafify.string-metadata.test.ts +++ b/packages/sheaves/src/sheafify.string-metadata.test.ts @@ -14,7 +14,7 @@ import { M } from '@endo/patterns'; import { describe, it, expect, vi } from 'vitest'; import { source } from './metadata.ts'; -import { makeHandler } from './section.ts'; +import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; import type { Policy, Provider } from './types.ts'; @@ -54,7 +54,7 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'SwapA', M.interface('SwapA', { swap: M.call(M.number(), M.string(), M.string()).returns( @@ -67,7 +67,7 @@ describe('e2e: source metadata — compartment evaluates cost function', () => { metadata: source(`(args) => ({ cost: 1 + 0.1 * args[0] })`), }, { - handler: makeHandler( + exo: makeSection( 'SwapB', M.interface('SwapB', { swap: M.call(M.number(), M.string(), M.string()).returns( diff --git a/packages/sheaves/src/sheafify.test.ts b/packages/sheaves/src/sheafify.test.ts index c92e3a3068..c7aae61e13 100644 --- a/packages/sheaves/src/sheafify.test.ts +++ b/packages/sheaves/src/sheafify.test.ts @@ -4,7 +4,7 @@ import { GET_DESCRIPTION } from '@metamask/kernel-utils'; import { describe, it, expect } from 'vitest'; import { constant } from './metadata.ts'; -import { makeHandler } from './section.ts'; +import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; import type { Candidate, Policy, PolicyContext, Provider } from './types.ts'; @@ -29,7 +29,7 @@ describe('sheafify', () => { const providers: Provider<{ cost: number }>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -50,7 +50,7 @@ describe('sheafify', () => { it('zero-coverage throws', async () => { const providers: Provider<{ cost: number }>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -67,7 +67,7 @@ describe('sheafify', () => { }, }); await expect(E(wallet).getBalance('bob')).rejects.toThrow( - 'No handler covers', + 'No section covers', ); }); @@ -81,7 +81,7 @@ describe('sheafify', () => { const providers: Provider<{ cost: number }>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -91,7 +91,7 @@ describe('sheafify', () => { metadata: constant({ cost: 100 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -113,7 +113,7 @@ describe('sheafify', () => { it('GET_INTERFACE_GUARD returns collected guard', () => { const providers: Provider<{ cost: number }>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.eq('alice')).returns(M.number()), @@ -123,7 +123,7 @@ describe('sheafify', () => { metadata: constant({ cost: 100 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.eq('bob')).returns(M.number()), @@ -156,7 +156,7 @@ describe('sheafify', () => { const providers: Provider<{ cost: number }>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -174,7 +174,7 @@ describe('sheafify', () => { // Add a cheaper provider with a new method to the providers array, re-sheafify. providers.push({ - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -204,7 +204,7 @@ describe('sheafify', () => { }); it('pre-built exo dispatches correctly', async () => { - const handler = makeHandler( + const exo = makeSection( 'bal', M.interface('bal', { getBalance: M.call(M.string()).returns(M.number()), @@ -212,7 +212,7 @@ describe('sheafify', () => { { getBalance: (_acct: string) => 42 }, ); const providers: Provider<{ cost: number }>[] = [ - { handler, metadata: constant({ cost: 1 }) }, + { exo, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ @@ -233,7 +233,7 @@ describe('sheafify', () => { const providers: Provider<{ cost: number }>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -250,7 +250,7 @@ describe('sheafify', () => { expect(await E(wallet).getBalance('alice')).toBe(100); // Add a pre-built exo with a cheaper getBalance + new transfer method - const handler = makeHandler( + const exo = makeSection( 'cheap', M.interface('cheap', { getBalance: M.call(M.string()).returns(M.number()), @@ -264,7 +264,7 @@ describe('sheafify', () => { }, ); providers.push({ - handler, + exo, metadata: constant({ cost: 1 }), }); wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ @@ -282,7 +282,7 @@ describe('sheafify', () => { }); it('guard reflected in GET_INTERFACE_GUARD for pre-built exo', () => { - const handler = makeHandler( + const exo = makeSection( 'bal', M.interface('bal', { getBalance: M.call(M.string()).returns(M.number()), @@ -290,7 +290,7 @@ describe('sheafify', () => { { getBalance: (_acct: string) => 42 }, ); const providers: Provider<{ cost: number }>[] = [ - { handler, metadata: constant({ cost: 1 }) }, + { exo, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ @@ -318,7 +318,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -328,7 +328,7 @@ describe('sheafify', () => { metadata: constant({ region: 'us', cost: 100 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -367,7 +367,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -377,7 +377,7 @@ describe('sheafify', () => { metadata: constant({ region: 'us' }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -404,7 +404,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -414,7 +414,7 @@ describe('sheafify', () => { metadata: constant({ cost: 1 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -444,7 +444,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -454,7 +454,7 @@ describe('sheafify', () => { metadata: constant({ cost: NaN, priority: 0 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -489,7 +489,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -499,7 +499,7 @@ describe('sheafify', () => { metadata: constant({ cost: +0 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -527,7 +527,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -537,7 +537,7 @@ describe('sheafify', () => { metadata: constant({ cost: Infinity }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -565,7 +565,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -574,7 +574,7 @@ describe('sheafify', () => { ), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -604,7 +604,7 @@ describe('sheafify', () => { ); }; - const handler = makeHandler( + const exo = makeSection( 'cheap', M.interface('cheap', { getBalance: M.call(M.string()).returns(M.number()), @@ -613,7 +613,7 @@ describe('sheafify', () => { ); const providers: Provider<{ cost: number }>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -622,7 +622,7 @@ describe('sheafify', () => { ), metadata: constant({ cost: 100 }), }, - { handler, metadata: constant({ cost: 1 }) }, + { exo, metadata: constant({ cost: 1 }) }, ]; const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ @@ -642,7 +642,7 @@ describe('sheafify', () => { }; const providers: Provider>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -668,7 +668,7 @@ describe('sheafify', () => { it('getSection does not expose __getDescription__', () => { const providers: Provider>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -705,7 +705,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -715,7 +715,7 @@ describe('sheafify', () => { metadata: constant({ constructor: 'typeA', cost: 100 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -759,7 +759,7 @@ describe('sheafify', () => { const providers: Provider[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -769,7 +769,7 @@ describe('sheafify', () => { metadata: constant({ constructor: Object, cost: 100 }), }, { - handler: makeHandler( + exo: makeSection( 'Wallet:1', M.interface('Wallet:1', { getBalance: M.call(M.string()).returns(M.number()), @@ -798,7 +798,7 @@ describe('getSection with explicit guard', () => { it('dispatches calls that fall within the explicit guard', async () => { const providers: Provider>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -829,7 +829,7 @@ describe('getSection with explicit guard', () => { it('rejects method calls outside the explicit guard', async () => { const providers: Provider>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), @@ -861,7 +861,7 @@ describe('getSection with explicit guard', () => { it('getDiscoverableSection exposes __getDescription__ and obeys explicit guard', async () => { const providers: Provider>[] = [ { - handler: makeHandler( + exo: makeSection( 'Wallet:0', M.interface('Wallet:0', { getBalance: M.call(M.string()).returns(M.number()), diff --git a/packages/sheaves/src/sheafify.ts b/packages/sheaves/src/sheafify.ts index f165579321..a3bba90a4c 100644 --- a/packages/sheaves/src/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -25,7 +25,7 @@ import type { ResolvedMetadataSpec } from './metadata.ts'; import { getStalk } from './stalk.ts'; import type { Candidate, - Handler, + Section, Policy, PolicyContext, Provider, @@ -138,35 +138,31 @@ const decomposeMetadata = >( remaining[key] = val; } } - return { handler: entry.handler, metadata: remaining as Partial }; + return { exo: entry.exo, metadata: remaining as Partial }; }); return { constraints: constraints as Partial, stripped }; }; /** - * Invoke a method on a handler, throwing if the method is missing. + * Invoke a method on a section exo, throwing if the handler is missing. * - * @param handler - The handler to invoke. + * @param exo - The section exo to invoke. * @param method - The method name to call. * @param args - The positional arguments. * @returns The synchronous return value of the method (typically a Promise). */ -const invokeHandler = ( - handler: Handler, - method: string, - args: unknown[], -): unknown => { - const obj = handler as Record unknown>; +const invokeExo = (exo: Section, method: string, args: unknown[]): unknown => { + const obj = exo as Record unknown>; const fn = obj[method]; if (fn === undefined) { - throw new Error(`Handler has guard for '${method}' but no method`); + throw new Error(`Section has guard for '${method}' but no handler`); } return fn.call(obj, ...args); }; type ResolvedProvider> = { - handler: Handler; + exo: Section; spec: ResolvedMetadataSpec | undefined; }; @@ -189,7 +185,7 @@ const drivePolicy = async >( next = await gen.next([...errors]); } } - throw new Error(`No viable handler for ${context.method}`, { + throw new Error(`No viable section for ${context.method}`, { cause: errors, }); }; @@ -207,7 +203,7 @@ export const sheafify = < }): Sheaf => { const frozenProviders: readonly ResolvedProvider[] = harden( providers.map((provider) => ({ - handler: provider.handler, + exo: provider.exo, spec: provider.metadata === undefined ? undefined @@ -238,24 +234,24 @@ export const sheafify = < const candidates = getStalk(frozenProviders, method, args); const evaluatedCandidates: Candidate[] = candidates.map( (provider) => ({ - handler: provider.handler, + exo: provider.exo, metadata: evaluateMetadata(provider.spec, args), }), ); switch (evaluatedCandidates.length) { case 0: - throw new Error(`No handler covers ${method}(${stringify(args, 0)})`); + throw new Error(`No section covers ${method}(${stringify(args, 0)})`); case 1: - return invokeHandler( - (evaluatedCandidates[0] as Candidate).handler, + return invokeExo( + (evaluatedCandidates[0] as Candidate).exo, method, args, ); default: { const collapsed = collapseEquivalent(evaluatedCandidates); if (collapsed.length === 1) { - return invokeHandler( - (collapsed[0] as Candidate).handler, + return invokeExo( + (collapsed[0] as Candidate).exo, method, args, ); @@ -281,7 +277,7 @@ export const sheafify = < `Did the policy construct a new object instead of yielding from the candidates array?`, ); } - return invokeHandler(resolved.handler, method, args); + return invokeExo(resolved.exo, method, args); }, ); } @@ -301,7 +297,7 @@ export const sheafify = < handlers, schema, asyncGuard, - )) as unknown as Handler; + )) as unknown as Section; return exo; }; @@ -309,7 +305,7 @@ export const sheafify = < const unionGuard = (): InterfaceGuard => collectSheafGuard( name, - frozenProviders.map(({ handler }) => handler), + frozenProviders.map(({ exo }) => exo), ); const getSection = ({ diff --git a/packages/sheaves/src/stalk.test.ts b/packages/sheaves/src/stalk.test.ts index 89266b24c4..7b840dfdea 100644 --- a/packages/sheaves/src/stalk.test.ts +++ b/packages/sheaves/src/stalk.test.ts @@ -3,7 +3,7 @@ import type { MethodGuard } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; import { constant } from './metadata.ts'; -import { makeHandler } from './section.ts'; +import { makeSection } from './section.ts'; import { getStalk } from './stalk.ts'; import type { Provider } from './types.ts'; @@ -13,7 +13,7 @@ const makeProvider = ( methods: Record unknown>, metadata: { cost: number }, ): Provider<{ cost: number }> => ({ - handler: makeHandler(tag, M.interface(tag, guards), methods), + exo: makeSection(tag, M.interface(tag, guards), methods), metadata: constant(metadata), }); diff --git a/packages/sheaves/src/stalk.ts b/packages/sheaves/src/stalk.ts index 37f0c4f0ec..bc36a5a246 100644 --- a/packages/sheaves/src/stalk.ts +++ b/packages/sheaves/src/stalk.ts @@ -7,7 +7,7 @@ import { matches } from '@endo/patterns'; import type { InterfaceGuard } from '@endo/patterns'; import { getInterfaceMethodGuards, getMethodPayload } from './guard.ts'; -import type { Handler } from './types.ts'; +import type { Section } from './types.ts'; /** * Check whether an interface guard covers the invocation point (method, args). @@ -58,13 +58,13 @@ export const guardCoversPoint = ( * @param args - The arguments to the method invocation. * @returns The providers whose guards accept the invocation. */ -export const getStalk = ( +export const getStalk = ( providers: readonly T[], method: string, args: unknown[], ): T[] => { - return providers.filter(({ handler }) => { - const interfaceGuard = handler[GET_INTERFACE_GUARD]?.(); + return providers.filter(({ exo }) => { + const interfaceGuard = exo[GET_INTERFACE_GUARD]?.(); if (!interfaceGuard) { return false; } diff --git a/packages/sheaves/src/types.ts b/packages/sheaves/src/types.ts index 936d4cd56f..35953df392 100644 --- a/packages/sheaves/src/types.ts +++ b/packages/sheaves/src/types.ts @@ -1,18 +1,18 @@ /** * Sheaf types: the product decomposition F_sem x F_op. * - * The handler (guard + behavior) is the semantic component F_sem. + * The section (guard + behavior) is the semantic component F_sem. * The metadata is the operational component F_op. * Effect-equivalence (the sheaf condition) is asserted by the interface: - * handlers covering the same open set produce the same observable result. + * sections covering the same open set produce the same observable result. */ import type { GET_INTERFACE_GUARD, Methods } from '@endo/exo'; import type { InterfaceGuard } from '@endo/patterns'; import type { MethodSchema } from '@metamask/kernel-utils'; -/** A handler: a capability covering a region of the interface topology. */ -export type Handler = Partial & { +/** A section: a capability covering a region of the interface topology. */ +export type Section = Partial & { [K in typeof GET_INTERFACE_GUARD]?: (() => InterfaceGuard) | undefined; }; @@ -28,13 +28,13 @@ export type MetadataSpec> = | { kind: 'callable'; fn: (args: unknown[]) => M }; /** - * A provider: a handler (F_sem) paired with an optional metadata spec (F_op). + * A provider: a section (F_sem) paired with an optional metadata spec (F_op). * - * This is the input data to sheafify — a (handler, metadata) pair assigned over - * the open set defined by the handler's guard. + * This is the input data to sheafify — an (exo, metadata) pair assigned over + * the open set defined by the exo's guard. */ export type Provider> = { - handler: Handler; + exo: Section; metadata?: MetadataSpec; }; @@ -46,7 +46,7 @@ export type Provider> = { * after collapsing). Empty `{}` means no metadata. */ export type Candidate> = { - handler: Handler; + exo: Section; metadata: MetaData; }; From 01db469f34d51fba54ec8dcbd27195289a432966 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:52:33 -0500 Subject: [PATCH 56/68] docs(sheaves): add INTRODUCTION.md and fix doc links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit INTRODUCTION.md frames the sheaf as the construction that asserts alignment of overlapping ocap surfaces — the case where you want attenuation-style coherence but don't share a common base. README is updated to point at it and to use the correct documents/ subpath for USAGE.md and POLICY.md (the LIFT.md link was stale after the earlier rename). --- packages/sheaves/README.md | 12 +-- packages/sheaves/documents/INTRODUCTION.md | 85 ++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 packages/sheaves/documents/INTRODUCTION.md diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index 8e76c2d726..7c6e4a5dc7 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -6,8 +6,10 @@ Runtime capability routing adapted from sheaf theory in algebraic topology. over a collection of capability providers. The sheaf produces dispatch sections via `getSection`, each of which routes invocations through the provider set. -See [USAGE.md](./USAGE.md) for annotated examples and [POLICY.md](./POLICY.md) for -the policy coroutine protocol and semantic equivalence assumption. +See [INTRODUCTION.md](./documents/INTRODUCTION.md) for what a sheaf is and when to +reach for one, [USAGE.md](./documents/USAGE.md) for annotated examples, and +[POLICY.md](./documents/POLICY.md) for the policy coroutine protocol and semantic +equivalence assumption. ## Install @@ -46,9 +48,9 @@ entries. > stalk at `("transfer", ...)` might contain one. **Policy** — An `async function*` coroutine that yields candidates from a -multi-candidate stalk in preference order. See [LIFT.md](./LIFT.md) for the -coroutine protocol, `PolicyContext`, and the semantic equivalence assumption -required of all policies. +multi-candidate stalk in preference order. See [POLICY.md](./documents/POLICY.md) +for the coroutine protocol, `PolicyContext`, and the semantic equivalence +assumption required of all policies. At dispatch time, metadata is decomposed into **constraints** (keys with the same value across every candidate — topologically determined, not a choice) and diff --git a/packages/sheaves/documents/INTRODUCTION.md b/packages/sheaves/documents/INTRODUCTION.md new file mode 100644 index 0000000000..a362cb7097 --- /dev/null +++ b/packages/sheaves/documents/INTRODUCTION.md @@ -0,0 +1,85 @@ +# What a sheaf is + +`@metamask/sheaves` lets you stitch a single dispatch surface from a +collection of capabilities that _ought to_ do the same thing — even when +they don't literally share an implementation. + +This doc explains the problem the sheaf solves and when you'd reach for one. + +## Attenuation: the ocap baseline + +In object-capability programming, you restrict authority by **attenuating** a +capability: wrapping it in a proxy that exposes a strict subset of its powers. +A `FileSystem` capability becomes a `Read("/home/alice")` capability by +wrapping it in something that forwards only read operations under +`/home/alice` and refuses everything else. + +The attenuator never adds authority. An attenuated capability is a narrower +projection of the same underlying object. + +## Strict attenuations compose for free + +When two capabilities are strict proxy attenuations of the **same** base, +their overlapping surfaces necessarily agree — both forward to the same +underlying implementation, so behavior is identical wherever their scopes +intersect. + +Composition is then a matter of bookkeeping. If `aliceCap = Read("foo/bar")` +and `bobCap = Read("foo/baz")` are both attenuations of the same +`FileSystem`, their union is `Read("foo/{bar,baz}")`. Where the scopes +overlap (here: the `foo/` prefix), the shared base ensures coherent behavior +— there is nothing to reconcile. + +This is the easy case, and the one ocap programming is built around. + +## Sheaves: alignment without a shared base + +Often you want to behave _as if_ you had a common base when you don't. Two +implementations of a wallet API; a local exo and a remote capability over +CapTP; replicas with different cost profiles. No shared origin to inherit +alignment from — but the surfaces are supposed to mean the same thing, and +the caller wants a single capability that routes invocations to whichever +provider is right. + +A **sheaf** is the construction that lets you assert this alignment by +contract instead of proving it by attenuation. You hand `sheafify` a set +of providers — each a capability with a guard describing the open set of +invocations it supports, plus optional metadata distinguishing it from its +peers — and you get back an authority manager that glues these pieces into +a single dispatch surface. + +The alignment is the load-bearing assumption (the **sheaf condition**): +two providers that both cover the same `(method, args)` point are presumed +to produce equivalent observable effects. The system trusts that contract; +that trust is what makes the framework work without a literal shared base. + +## The dispatch surface is a section + +What you get back from the sheaf is a **section** — a capability covering +some open set of the combined surface, restricted by an explicit guard: + +```ts +const sheaf = sheafify({ name: 'Wallet', providers }); +const userFacing = sheaf.getSection({ guard: userGuard, lift: policy }); +``` + +`getSection` is itself attenuation: it takes the full combined surface that +the sheaf has glued together and hands back a narrower view restricted by +`userGuard`. The sheaf has done the hard part — asserting alignment so the +providers can be treated as one — and `getSection` carves a slice out of +that unified surface for the caller. The result is that you can attenuate +a composition of capabilities the same way you would attenuate a single +one. + +The guard determines what is invokable through `userFacing`. Anything +outside the guard is simply not in the interface — there is no extra +authorization step, no access check. Unauthorized invocations are +unsupported in the same flat sense that calling a missing method on any +ocap is unsupported. + +Where multiple providers cover the same invocation, a caller-supplied +**policy** selects which one runs (see [POLICY.md](./POLICY.md)). Where +exactly one covers it, the choice is forced. And because the returned +section is itself a capability, it can be a provider to another sheaf — +the construction composes with itself. See [USAGE.md](./USAGE.md) for +worked examples. From 12332c86fb46d696bbf4bfa5c6a7c53fb34d0f53 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:07:17 -0500 Subject: [PATCH 57/68] docs(sheaves): rename documents/ to docs/ for convention --- packages/sheaves/CHANGELOG.md | 4 ++-- packages/sheaves/README.md | 8 ++++---- packages/sheaves/{documents => docs}/INTRODUCTION.md | 0 packages/sheaves/{documents => docs}/POLICY.md | 0 packages/sheaves/{documents => docs}/USAGE.md | 0 5 files changed, 6 insertions(+), 6 deletions(-) rename packages/sheaves/{documents => docs}/INTRODUCTION.md (100%) rename packages/sheaves/{documents => docs}/POLICY.md (100%) rename packages/sheaves/{documents => docs}/USAGE.md (100%) diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index 653ba60399..e932379c1a 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -46,9 +46,9 @@ constraints }`. - `Sheaf` type — the authority manager returned by `sheafify`; exposes `getSection`, `getDiscoverableSection`, `getGlobalSection`, and `getDiscoverableGlobalSection`. -- `documents/POLICY.md` — documents the policy coroutine protocol, +- `docs/POLICY.md` — documents the policy coroutine protocol, `PolicyContext`, and the semantic equivalence assumption. -- `documents/USAGE.md` — annotated usage examples. +- `docs/USAGE.md` — annotated usage examples. [Unreleased]: https://github.com/MetaMask/ocap-kernel/compare/@metamask/sheaves@0.1.0...HEAD [0.1.0]: https://github.com/MetaMask/ocap-kernel/releases/tag/@metamask/sheaves@0.1.0 diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index 7c6e4a5dc7..ce362e71f4 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -6,9 +6,9 @@ Runtime capability routing adapted from sheaf theory in algebraic topology. over a collection of capability providers. The sheaf produces dispatch sections via `getSection`, each of which routes invocations through the provider set. -See [INTRODUCTION.md](./documents/INTRODUCTION.md) for what a sheaf is and when to -reach for one, [USAGE.md](./documents/USAGE.md) for annotated examples, and -[POLICY.md](./documents/POLICY.md) for the policy coroutine protocol and semantic +See [INTRODUCTION.md](./docs/INTRODUCTION.md) for what a sheaf is and when to +reach for one, [USAGE.md](./docs/USAGE.md) for annotated examples, and +[POLICY.md](./docs/POLICY.md) for the policy coroutine protocol and semantic equivalence assumption. ## Install @@ -48,7 +48,7 @@ entries. > stalk at `("transfer", ...)` might contain one. **Policy** — An `async function*` coroutine that yields candidates from a -multi-candidate stalk in preference order. See [POLICY.md](./documents/POLICY.md) +multi-candidate stalk in preference order. See [POLICY.md](./docs/POLICY.md) for the coroutine protocol, `PolicyContext`, and the semantic equivalence assumption required of all policies. diff --git a/packages/sheaves/documents/INTRODUCTION.md b/packages/sheaves/docs/INTRODUCTION.md similarity index 100% rename from packages/sheaves/documents/INTRODUCTION.md rename to packages/sheaves/docs/INTRODUCTION.md diff --git a/packages/sheaves/documents/POLICY.md b/packages/sheaves/docs/POLICY.md similarity index 100% rename from packages/sheaves/documents/POLICY.md rename to packages/sheaves/docs/POLICY.md diff --git a/packages/sheaves/documents/USAGE.md b/packages/sheaves/docs/USAGE.md similarity index 100% rename from packages/sheaves/documents/USAGE.md rename to packages/sheaves/docs/USAGE.md From 0eaf5045233e70908a4e3f1c9ccd58221213bc7f Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:07:46 -0500 Subject: [PATCH 58/68] refactor(sheaves): remove unused 'source' metadata kind --- packages/sheaves/CHANGELOG.md | 6 +- packages/sheaves/README.md | 10 +- packages/sheaves/docs/USAGE.md | 8 +- packages/sheaves/src/index.ts | 2 +- packages/sheaves/src/metadata.test.ts | 92 +++------------- packages/sheaves/src/metadata.ts | 52 +-------- .../src/sheafify.string-metadata.test.ts | 104 ------------------ packages/sheaves/src/sheafify.ts | 13 +-- packages/sheaves/src/types.ts | 4 +- 9 files changed, 36 insertions(+), 255 deletions(-) delete mode 100644 packages/sheaves/src/sheafify.string-metadata.test.ts diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index e932379c1a..e24c4aace5 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -24,11 +24,9 @@ metadata }` with metadata already resolved from its spec. and yields them in preference order; drives the sheaf dispatch loop. - `PolicyContext` type — context passed to the policy: `{ method, args, constraints }`. -- `MetadataSpec` discriminated union with three variants: `constant`, - `source`, and `callable`. +- `MetadataSpec` discriminated union with two variants: `constant` and + `callable`. - `constant(value)` — static metadata spec; value is fixed at construction. -- `source(src)` — source-string metadata spec; compiled via the optional - compartment at `sheafify` construction time. - `callable(fn)` — callable metadata spec; evaluated per-dispatch with the invocation arguments. - `makeSection(name, guard, handlers)` — creates a named, guarded `Section` from a method-handler map. diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index ce362e71f4..0837d3a641 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -93,11 +93,11 @@ guard, it is invoked directly without evaluate/collapse/policy; if all matching providers collapse to an identical candidate, the single representative is invoked without calling the policy. -`callable` and `source` metadata specs make the stalk shape depend on the -invocation arguments. A `swap(amount)` provider can produce `{ cost: 'low' }` -for small amounts and `{ cost: 'high' }` for large ones, yielding a different -set of candidates — and potentially a different policy outcome — for the same -method called with different arguments. +`callable` metadata specs make the stalk shape depend on the invocation +arguments. A `swap(amount)` provider can produce `{ cost: 'low' }` for small +amounts and `{ cost: 'high' }` for large ones, yielding a different set of +candidates — and potentially a different policy outcome — for the same method +called with different arguments. ## Design choices diff --git a/packages/sheaves/docs/USAGE.md b/packages/sheaves/docs/USAGE.md index 7be843e2f9..8eab7f429c 100644 --- a/packages/sheaves/docs/USAGE.md +++ b/packages/sheaves/docs/USAGE.md @@ -67,18 +67,14 @@ The sheaf drives the generator: it primes it with `gen.next([])`, calls the chosen candidate, then passes any thrown errors back as `gen.next(errors)` so the policy can adapt before yielding the next candidate. -Use the `constant`, `source`, or `callable` helpers to build metadata specs: +Use the `constant` or `callable` helpers to build metadata specs: ```ts -import { constant, source, callable } from '@metamask/sheaves'; +import { constant, callable } from '@metamask/sheaves'; // static value known at construction time constant({ mode: 'fast' }); -// @experimental — prefer callable unless the function must cross a trust boundary -// or be serialized. Compiled once in the sheaf's compartment at construction time. -source(`(args) => ({ cost: args[0] > 9000 ? 'high' : 'low' })`); - // live function evaluated at each dispatch — useful when cost varies by argument, // e.g. a swap whose metadata encodes volume-based cost tiers callable((args) => ({ cost: Number(args[0]) > 9000 ? 'high' : 'low' })); diff --git a/packages/sheaves/src/index.ts b/packages/sheaves/src/index.ts index ab591132b5..735f045faf 100644 --- a/packages/sheaves/src/index.ts +++ b/packages/sheaves/src/index.ts @@ -7,7 +7,7 @@ export type { PolicyContext, Sheaf, } from './types.ts'; -export { constant, source, callable } from './metadata.ts'; +export { constant, callable } from './metadata.ts'; export { sheafify } from './sheafify.ts'; export { noopPolicy, diff --git a/packages/sheaves/src/metadata.test.ts b/packages/sheaves/src/metadata.test.ts index 8257f03cad..2c6458f76e 100644 --- a/packages/sheaves/src/metadata.test.ts +++ b/packages/sheaves/src/metadata.test.ts @@ -1,12 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; -import { - callable, - constant, - evaluateMetadata, - resolveMetadataSpec, - source, -} from './metadata.ts'; +import { callable, constant, evaluateMetadata } from './metadata.ts'; describe('constant', () => { it('returns a constant spec with the given value', () => { @@ -17,7 +11,7 @@ describe('constant', () => { }); it('evaluateMetadata returns the value regardless of args', () => { - const spec = resolveMetadataSpec(constant({ cost: 7 })); + const spec = constant({ cost: 7 }); expect(evaluateMetadata(spec, [])).toStrictEqual({ cost: 7 }); expect(evaluateMetadata(spec, [1, 2, 3])).toStrictEqual({ cost: 7 }); }); @@ -34,54 +28,12 @@ describe('callable', () => { const fn = vi.fn((args: unknown[]) => ({ value: (args[0] as number) * 2, })); - const spec = resolveMetadataSpec(callable(fn)); + const spec = callable(fn); expect(evaluateMetadata(spec, [5])).toStrictEqual({ value: 10 }); expect(fn).toHaveBeenCalledWith([5]); }); }); -describe('source', () => { - it('returns a source spec with the src string', () => { - expect(source('(args) => ({ x: args[0] })')).toStrictEqual({ - kind: 'source', - src: '(args) => ({ x: args[0] })', - }); - }); - - it('resolveMetadataSpec compiles source to callable via compartment', () => { - const mockFn = (args: unknown[]) => ({ value: args[0] as number }); - const compartment = { evaluate: vi.fn(() => mockFn) }; - const spec = resolveMetadataSpec( - source<{ value: number }>('(args) => ({ value: args[0] })'), - compartment, - ); - expect(spec.kind).toBe('callable'); - expect(compartment.evaluate).toHaveBeenCalledWith( - '(args) => ({ value: args[0] })', - ); - expect(evaluateMetadata(spec, [99])).toStrictEqual({ value: 99 }); - }); -}); - -describe('resolveMetadataSpec', () => { - it('passes constant spec through unchanged', () => { - const spec = constant({ answer: 42 }); - expect(resolveMetadataSpec(spec)).toStrictEqual(spec); - }); - - it('passes callable spec through unchanged', () => { - const fn = (_args: unknown[]) => ({ count: 0 }); - const spec = callable(fn); - expect(resolveMetadataSpec(spec)).toStrictEqual(spec); - }); - - it("throws if kind is 'source' and no compartment supplied", () => { - expect(() => resolveMetadataSpec(source('() => ({})'))).toThrow( - "compartment required to evaluate 'source' metadata", - ); - }); -}); - describe('evaluateMetadata', () => { it('returns empty object when spec is undefined', () => { expect(evaluateMetadata(undefined, [])).toStrictEqual({}); @@ -89,44 +41,36 @@ describe('evaluateMetadata', () => { }); it('normalizes null from callable to empty object', () => { - const spec = resolveMetadataSpec( - callable( - ((_args: unknown[]) => null) as unknown as ( - args: unknown[], - ) => Record, - ), + const spec = callable( + ((_args: unknown[]) => null) as unknown as ( + args: unknown[], + ) => Record, ); expect(evaluateMetadata(spec, [])).toStrictEqual({}); }); it('throws when callable returns a primitive', () => { - const spec = resolveMetadataSpec( - callable( - ((_args: unknown[]) => 7) as unknown as ( - args: unknown[], - ) => Record, - ), + const spec = callable( + ((_args: unknown[]) => 7) as unknown as ( + args: unknown[], + ) => Record, ); expect(() => evaluateMetadata(spec, [])).toThrow(/cannot be a primitive/u); expect(() => evaluateMetadata(spec, [])).toThrow(/value: myValue/u); }); it('throws when callable returns an array', () => { - const spec = resolveMetadataSpec( - callable(((_args: unknown[]) => [1, 2]) as unknown as ( - args: unknown[], - ) => Record), - ); + const spec = callable(((_args: unknown[]) => [1, 2]) as unknown as ( + args: unknown[], + ) => Record); expect(() => evaluateMetadata(spec, [])).toThrow(/cannot be an array/u); }); it('throws when callable returns a Date', () => { - const spec = resolveMetadataSpec( - callable( - ((_args: unknown[]) => new Date()) as unknown as ( - args: unknown[], - ) => Record, - ), + const spec = callable( + ((_args: unknown[]) => new Date()) as unknown as ( + args: unknown[], + ) => Record, ); expect(() => evaluateMetadata(spec, [])).toThrow(/must be a plain object/u); }); diff --git a/packages/sheaves/src/metadata.ts b/packages/sheaves/src/metadata.ts index 3a7aa03f79..2306475ee9 100644 --- a/packages/sheaves/src/metadata.ts +++ b/packages/sheaves/src/metadata.ts @@ -4,11 +4,6 @@ import type { MetadataSpec } from './types.ts'; -/** Resolved spec: 'source' has been compiled away; only constant or callable remain. */ -export type ResolvedMetadataSpec> = - | { kind: 'constant'; value: M } - | { kind: 'callable'; fn: (args: unknown[]) => M }; - const metadataPlainObjectHint = 'Sheaf metadata must be a plain object; use e.g. { value: myValue } if you need to attach a primitive.'; @@ -57,20 +52,6 @@ export const constant = >( value: M, ): MetadataSpec => harden({ kind: 'constant', value }); -/** - * Wrap JS function source. Evaluated in a Compartment at sheafify construction time. - * - * Prefer `callable` unless the metadata function must be supplied as a - * serializable source string — for example, when crossing a trust boundary or - * deserializing from storage. Requires a `compartment` passed to `sheafify`. - * - * @param src - JS source string of the form `(args) => M`. - * @returns A source MetadataSpec wrapping the source string. - */ -export const source = >( - src: string, -): MetadataSpec => harden({ kind: 'source', src }); - /** * Wrap a live function as a callable metadata spec. * @@ -82,45 +63,18 @@ export const callable = >( ): MetadataSpec => harden({ kind: 'callable', fn }); /** - * Compile a 'source' spec to 'callable' using the supplied compartment. - * 'constant' and 'callable' pass through unchanged. - * - * @param spec - The MetadataSpec to resolve. - * @param compartment - Compartment used to evaluate 'source' specs. Required when spec is 'source'. - * @param compartment.evaluate - Evaluate a JS source string and return the result. - * @returns A ResolvedMetadataSpec with no 'source' variant. - */ -export const resolveMetadataSpec = >( - spec: MetadataSpec, - compartment?: { evaluate: (src: string) => unknown }, -): ResolvedMetadataSpec => { - if (spec.kind === 'source') { - if (!compartment) { - throw new Error( - `sheafify: compartment required to evaluate 'source' metadata`, - ); - } - return { - kind: 'callable', - fn: compartment.evaluate(spec.src) as (args: unknown[]) => M, - }; - } - return spec; -}; - -/** - * Evaluate a resolved metadata spec against the invocation args. + * Evaluate a metadata spec against the invocation args. * * Missing spec yields `{}` (no metadata). Callable/constant results must be plain objects; * `undefined`/`null` from the producer normalize to `{}`. Primitives, arrays, and non-plain * objects throw with guidance to use an explicit record such as `{ value: myValue }`. * - * @param spec - The resolved spec to evaluate, or undefined. + * @param spec - The spec to evaluate, or undefined. * @param args - The invocation arguments. * @returns The evaluated metadata object (possibly empty). */ export const evaluateMetadata = >( - spec: ResolvedMetadataSpec | undefined, + spec: MetadataSpec | undefined, args: unknown[], ): MetaData => { if (spec === undefined) { diff --git a/packages/sheaves/src/sheafify.string-metadata.test.ts b/packages/sheaves/src/sheafify.string-metadata.test.ts deleted file mode 100644 index d04ff029ef..0000000000 --- a/packages/sheaves/src/sheafify.string-metadata.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -// This test verifies that source-kind metadata specs are compiled via a -// compartment at sheafify construction time and evaluated at dispatch time. -// -// We use a new Function()-based compartment rather than a real SES Compartment -// because importing 'ses' alongside '@endo/exo' triggers a module-evaluation -// ordering conflict in the test environment: @endo/patterns module initialization -// calls assertPattern() under SES lockdown before its internal objects are frozen. -// That conflict is an environment limitation, not a feature limitation. -// -// The functional properties under test are identical regardless of which -// Compartment implementation compiles the source string. - -import { M } from '@endo/patterns'; -import { describe, it, expect, vi } from 'vitest'; - -import { source } from './metadata.ts'; -import { makeSection } from './section.ts'; -import { sheafify } from './sheafify.ts'; -import type { Policy, Provider } from './types.ts'; - -// Thin cast for calling exo methods directly in tests without going through -// HandledPromise (which is not available in the test environment). -// eslint-disable-next-line id-length -const E = (obj: unknown) => - obj as Record Promise>; - -// A Compartment-shaped object that actually evaluates JS source strings. -/* eslint-disable @typescript-eslint/no-implied-eval, no-new-func */ -const makeTestCompartment = () => ({ - evaluate: (src: string) => new Function(`return (${src})`)(), -}); -/* eslint-enable @typescript-eslint/no-implied-eval, no-new-func */ - -describe('e2e: source metadata — compartment evaluates cost function', () => { - // Same two-swap scenario as the callable e2e test, but cost functions are - // provided as JS source strings and compiled via the test compartment. - // Breakeven ≈ 90.9 (same arithmetic as callable variant). - - type SwapCost = { cost: number }; - - const cheapest: Policy = async function* (candidates) { - yield* [...candidates].sort( - (a, b) => (a.metadata?.cost ?? Infinity) - (b.metadata?.cost ?? Infinity), - ); - }; - - it('routes swap(50) to A and swap(100) to B using source-kind metadata', async () => { - const swapAFn = vi.fn( - (_amount: number, _from: string, _to: string): boolean => true, - ); - const swapBFn = vi.fn( - (_amount: number, _from: string, _to: string): boolean => true, - ); - - const providers: Provider[] = [ - { - exo: makeSection( - 'SwapA', - M.interface('SwapA', { - swap: M.call(M.number(), M.string(), M.string()).returns( - M.boolean(), - ), - }), - { swap: swapAFn }, - ), - // cost(amount) = 1 + 0.1 * amount - metadata: source(`(args) => ({ cost: 1 + 0.1 * args[0] })`), - }, - { - exo: makeSection( - 'SwapB', - M.interface('SwapB', { - swap: M.call(M.number(), M.string(), M.string()).returns( - M.boolean(), - ), - }), - { swap: swapBFn }, - ), - // cost(amount) = 10 + 0.001 * amount - metadata: source(`(args) => ({ cost: 10 + 0.001 * args[0] })`), - }, - ]; - - const facade = sheafify({ - name: 'Swap', - providers, - compartment: makeTestCompartment(), - }).getGlobalSection({ lift: cheapest }) as unknown as Record< - string, - (...args: unknown[]) => Promise - >; - - // swap(50): A costs 6, B costs 10.05 → A wins - await E(facade).swap(50, 'FUZ', 'BIZ'); - expect(swapAFn).toHaveBeenCalledWith(50, 'FUZ', 'BIZ'); - expect(swapBFn).not.toHaveBeenCalled(); - swapAFn.mockClear(); - - // swap(100): A costs 11, B costs 10.1 → B wins - await E(facade).swap(100, 'FUZ', 'BIZ'); - expect(swapBFn).toHaveBeenCalledWith(100, 'FUZ', 'BIZ'); - expect(swapAFn).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/sheaves/src/sheafify.ts b/packages/sheaves/src/sheafify.ts index a3bba90a4c..dc2992ddc0 100644 --- a/packages/sheaves/src/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -20,11 +20,11 @@ import { makeDiscoverableExo } from '@metamask/kernel-utils'; import { stringify } from '@metamask/kernel-utils'; import { asyncifyMethodGuards, collectSheafGuard } from './guard.ts'; -import { evaluateMetadata, resolveMetadataSpec } from './metadata.ts'; -import type { ResolvedMetadataSpec } from './metadata.ts'; +import { evaluateMetadata } from './metadata.ts'; import { getStalk } from './stalk.ts'; import type { Candidate, + MetadataSpec, Section, Policy, PolicyContext, @@ -163,7 +163,7 @@ const invokeExo = (exo: Section, method: string, args: unknown[]): unknown => { type ResolvedProvider> = { exo: Section; - spec: ResolvedMetadataSpec | undefined; + spec: MetadataSpec | undefined; }; const drivePolicy = async >( @@ -195,19 +195,14 @@ export const sheafify = < >({ name, providers, - compartment, }: { name: string; providers: Provider[]; - compartment?: { evaluate: (src: string) => unknown }; }): Sheaf => { const frozenProviders: readonly ResolvedProvider[] = harden( providers.map((provider) => ({ exo: provider.exo, - spec: - provider.metadata === undefined - ? undefined - : resolveMetadataSpec(provider.metadata, compartment), + spec: provider.metadata, })), ); const buildSection = ({ diff --git a/packages/sheaves/src/types.ts b/packages/sheaves/src/types.ts index 35953df392..5c0b84be56 100644 --- a/packages/sheaves/src/types.ts +++ b/packages/sheaves/src/types.ts @@ -17,14 +17,12 @@ export type Section = Partial & { }; /** - * A metadata specification: either a static value, a JS source string, or a - * live function. Source strings are compiled once at sheafify construction time. + * A metadata specification: either a static value or a live function. * Evaluated metadata must be a plain object (`{}` means no metadata; primitives * must be wrapped, e.g. `{ value: n }`). */ export type MetadataSpec> = | { kind: 'constant'; value: M } - | { kind: 'source'; src: string } | { kind: 'callable'; fn: (args: unknown[]) => M }; /** From d3992fe22c134a4e6cce841f996b0b4a918a0f76 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:26:18 -0500 Subject: [PATCH 59/68] chore(sheaves): rebase yarn.lock onto main to drop spurious downresolutions The branch's lockfile had re-resolved unrelated transitive deps (vitest, rolldown, undici, postcss, jsdom, turbo, typescript-eslint) to older versions than main has. Reset yarn.lock to main and re-run yarn install so only the new @metamask/sheaves workspace entry is added on top. Co-Authored-By: Claude Opus 4.7 --- yarn.lock | 1141 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 639 insertions(+), 502 deletions(-) diff --git a/yarn.lock b/yarn.lock index b47de1b5da..f5c8836960 100644 --- a/yarn.lock +++ b/yarn.lock @@ -138,27 +138,36 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^5.1.5": - version: 5.1.6 - resolution: "@asamuzakjp/css-color@npm:5.1.6" +"@asamuzakjp/css-color@npm:^5.1.11": + version: 5.1.11 + resolution: "@asamuzakjp/css-color@npm:5.1.11" dependencies: - "@csstools/css-calc": "npm:^3.1.1" - "@csstools/css-color-parser": "npm:^4.0.2" + "@asamuzakjp/generational-cache": "npm:^1.0.1" + "@csstools/css-calc": "npm:^3.2.0" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" - checksum: 10/5151369d9369e478e03c0eee0f171b8f86306ebbdf5b352544cd745c360d97343f437bdd0690ff658e47d2876b466bffc8811fcef7f0347cb243c6483a7e95a0 + checksum: 10/2e337cc94b5a3f9741a27f92b4e4b7dc467a76b1dcf66c40e71808fed71695f10c8cf07c8b13313cbb637154314ca1d8626bb9a045fe94b404b242a390cf3bd3 languageName: node linkType: hard -"@asamuzakjp/dom-selector@npm:^7.0.6": - version: 7.0.7 - resolution: "@asamuzakjp/dom-selector@npm:7.0.7" +"@asamuzakjp/dom-selector@npm:^7.1.1": + version: 7.1.1 + resolution: "@asamuzakjp/dom-selector@npm:7.1.1" dependencies: + "@asamuzakjp/generational-cache": "npm:^1.0.1" "@asamuzakjp/nwsapi": "npm:^2.3.9" bidi-js: "npm:^1.0.3" css-tree: "npm:^3.2.1" is-potential-custom-element-name: "npm:^1.0.1" - checksum: 10/18f40def8c775c6008c8fcd75d7d049ff92d99a494929ab2bf742341b348c78cbf4808d29c13b9cd87ca4fd272773cf5aa9e58fee48603c286df48148be8cb67 + checksum: 10/49a065a64db5f53a3008c231d09606e4b67f509fa20148a67419451c2dc91a421202ed17bfc4bc679ad2f0432d7260720d602c1d5c9c5e165931fff5199c3f12 + languageName: node + linkType: hard + +"@asamuzakjp/generational-cache@npm:^1.0.1": + version: 1.0.1 + resolution: "@asamuzakjp/generational-cache@npm:1.0.1" + checksum: 10/e1cf3f1916a334c6153f624982f0eb3d50fa3048435ea5c5b0f441f8f1ab74a0fe992dac214b612d22c0acafad3cd1a1f6b45d99c7b6e3b63cfdf7f6ca5fc144 languageName: node linkType: hard @@ -305,13 +314,13 @@ __metadata: linkType: hard "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.5, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.29.0": - version: 7.29.2 - resolution: "@babel/parser@npm:7.29.2" + version: 7.29.3 + resolution: "@babel/parser@npm:7.29.3" dependencies: "@babel/types": "npm:^7.29.0" bin: parser: ./bin/babel-parser.js - checksum: 10/45d050bf75aa5194b3255f156173e8553d615ff5a2434674cc4a10cdc7c261931befb8618c996a1c449b87f0ef32a3407879af2ac967d95dc7b4fdbae7037efa + checksum: 10/10e8f34e0fdaa495b9db8be71f4eb29b16d8a57e0818c1bb1c4084015b0383803fd77812ed41597760cbf3d9ab3ae9f4af54f39ff5e5d8e081ba43593232f0ca languageName: node linkType: hard @@ -456,9 +465,9 @@ __metadata: linkType: hard "@chainsafe/as-sha256@npm:^1.2.0": - version: 1.2.0 - resolution: "@chainsafe/as-sha256@npm:1.2.0" - checksum: 10/9c1bf0009954fc07a288b4a920d2289cec21f74662805b60e78f7449fdb7743a4d23875eb292695f12d95fdfa1d44e3f78af8c8e1d1703601a5585bd964e9f5b + version: 1.2.4 + resolution: "@chainsafe/as-sha256@npm:1.2.4" + checksum: 10/3197a7695c89f532afca7345e29a2aea02426404690b760d5627bcf5f00e83e28c1728f8edca79f65270fc3525020813e7aacaebbcc81c82b06200d68094cea1 languageName: node linkType: hard @@ -612,26 +621,26 @@ __metadata: languageName: node linkType: hard -"@csstools/css-calc@npm:^3.1.1": - version: 3.1.1 - resolution: "@csstools/css-calc@npm:3.1.1" +"@csstools/css-calc@npm:^3.2.0": + version: 3.2.0 + resolution: "@csstools/css-calc@npm:3.2.0" peerDependencies: "@csstools/css-parser-algorithms": ^4.0.0 "@csstools/css-tokenizer": ^4.0.0 - checksum: 10/faa3aa2736b20757ceafd76e3d2841e8726ec9e7ae78e387684eb462aba73d533ba384039338685c3a52196196300ccdfecb051e59864b1d3b457fe927b7f53b + checksum: 10/7eec51a21945a74aa6a407d1e6290d0f4c5d01829a42c01a56ce2055216398540cc3120837b15a0db38601bcb40cf97f1d991fefb3ee9d00d9cec03d67beba4c languageName: node linkType: hard -"@csstools/css-color-parser@npm:^4.0.2": - version: 4.0.2 - resolution: "@csstools/css-color-parser@npm:4.0.2" +"@csstools/css-color-parser@npm:^4.1.0": + version: 4.1.0 + resolution: "@csstools/css-color-parser@npm:4.1.0" dependencies: "@csstools/color-helpers": "npm:^6.0.2" - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" peerDependencies: "@csstools/css-parser-algorithms": ^4.0.0 "@csstools/css-tokenizer": ^4.0.0 - checksum: 10/6418bfadc8c15d3a65c1e80278df383b542f0437446c0ba21d591dd564bcc19ab0b11243edf62672f4c62cc778f9b386fa4349e9a8d1de2b414148ea8a1ac775 + checksum: 10/794508011a95ebac3856e67e0333ca4174604d2dfddc101d991f2ebfd52b3c99cd36a08462675c2a07d057ca3787187fcd7eac98bced2eefdd9040b37853426d languageName: node linkType: hard @@ -644,15 +653,15 @@ __metadata: languageName: node linkType: hard -"@csstools/css-syntax-patches-for-csstree@npm:^1.1.1": - version: 1.1.2 - resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.2" +"@csstools/css-syntax-patches-for-csstree@npm:^1.1.3": + version: 1.1.3 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.3" peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: css-tree: optional: true - checksum: 10/6ac57afa549ea3df11b9341730b2bec56c5383f229a9eb9db6c7b86ab46c9e145498ab285fe2a6ea8bcd74ac7f5f746c086083603da42128d915c984f797a281 + checksum: 10/1c91dc03b64ca913eed5064ca0e434da1c0be8def6ce20f932d1db10f9b478ac3830c99a033b0edf75954cf9164c7c267b220ed9faffbc3342bf320870c3bb4b languageName: node linkType: hard @@ -672,31 +681,31 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:1.9.1, @emnapi/core@npm:^1.4.3": - version: 1.9.1 - resolution: "@emnapi/core@npm:1.9.1" +"@emnapi/core@npm:1.10.0, @emnapi/core@npm:^1.4.3": + version: 1.10.0 + resolution: "@emnapi/core@npm:1.10.0" dependencies: - "@emnapi/wasi-threads": "npm:1.2.0" + "@emnapi/wasi-threads": "npm:1.2.1" tslib: "npm:^2.4.0" - checksum: 10/c44cfe471702b43306b84d0f4f2f1506dac0065dbd73dc5a41bd99a2c39802ca7e2d7ebfbfae8997468d1ff0420603596bf35b19eabd5951bad1eb630d2d4574 + checksum: 10/d32f386084e64deaf2609aabb8295d1ad5af6144d0f46d2060b76cc53f1f3b486df54bec9b0f33c37d85a3822e1193ebcd4e3deb4a5f0e4cd650aa2ffc631715 languageName: node linkType: hard -"@emnapi/runtime@npm:1.9.1, @emnapi/runtime@npm:^1.4.3": - version: 1.9.1 - resolution: "@emnapi/runtime@npm:1.9.1" +"@emnapi/runtime@npm:1.10.0, @emnapi/runtime@npm:^1.4.3": + version: 1.10.0 + resolution: "@emnapi/runtime@npm:1.10.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10/337767fa44ec1f6277494342664be8773f16aad4086e9e49423a9f06c5eee7495e2e1b0b50dcd764c5a5cc4c15c9d80c13fba2da6763a97c06a48115cd7ccd14 + checksum: 10/d21083d07fa0c2da171c142e78ef986b66b07d45b06accc0bcaf49fcc61bb4dbc10e1c1760813070165b9f49b054376a931045347f21c0f42ff1eb2d2040faac languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.2.0": - version: 1.2.0 - resolution: "@emnapi/wasi-threads@npm:1.2.0" +"@emnapi/wasi-threads@npm:1.2.1": + version: 1.2.1 + resolution: "@emnapi/wasi-threads@npm:1.2.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10/c8e48c7200530744dc58170d2e25933b61433e4a0c50b4f192f5d8d4b065c7023dbfc48dac0afadbc29bd239013f2ae454c6e54e0ca6e8248402bf95c9e77e22 + checksum: 10/57cd4292be81c05d26aa886d68a9e4c449ff666e8503fed6463dfc6b64a4e4213f03c152d53296b7cda32840271e38cd33347332070658f01befeb9bf4e59f36 languageName: node linkType: hard @@ -1537,7 +1546,7 @@ __metadata: languageName: node linkType: hard -"@libp2p/crypto@npm:5.1.14, @libp2p/crypto@npm:^5.1.14, @libp2p/crypto@npm:^5.1.7, @libp2p/crypto@npm:^5.1.9": +"@libp2p/crypto@npm:5.1.14": version: 5.1.14 resolution: "@libp2p/crypto@npm:5.1.14" dependencies: @@ -1552,6 +1561,21 @@ __metadata: languageName: node linkType: hard +"@libp2p/crypto@npm:^5.1.14, @libp2p/crypto@npm:^5.1.17, @libp2p/crypto@npm:^5.1.7, @libp2p/crypto@npm:^5.1.9": + version: 5.1.17 + resolution: "@libp2p/crypto@npm:5.1.17" + dependencies: + "@libp2p/interface": "npm:^3.2.2" + "@noble/curves": "npm:^2.0.1" + "@noble/hashes": "npm:^2.0.1" + multiformats: "npm:^13.4.0" + protons-runtime: "npm:^6.0.1" + uint8arraylist: "npm:^2.4.8" + uint8arrays: "npm:^5.1.0" + checksum: 10/1f00ca9998fc598689fd0450c685db5740db91f8843d22950480d6f3df16c7bc61bf1f99bc9f064dfbf1b801e7644d5663a401e0eef6615413c665433c1899b9 + languageName: node + linkType: hard + "@libp2p/identify@npm:4.0.14": version: 4.0.14 resolution: "@libp2p/identify@npm:4.0.14" @@ -1575,14 +1599,14 @@ __metadata: linkType: hard "@libp2p/interface-internal@npm:^3.0.14": - version: 3.0.14 - resolution: "@libp2p/interface-internal@npm:3.0.14" + version: 3.1.3 + resolution: "@libp2p/interface-internal@npm:3.1.3" dependencies: - "@libp2p/interface": "npm:^3.1.1" - "@libp2p/peer-collections": "npm:^7.0.14" + "@libp2p/interface": "npm:^3.2.2" + "@libp2p/peer-collections": "npm:^7.0.18" "@multiformats/multiaddr": "npm:^13.0.1" progress-events: "npm:^1.0.1" - checksum: 10/bbac479d193af0ab175d49d0041376c040c42c870f69d3e479cf617d24d5aa94aab736b7ebcdc50464bd0b4886e3992a36dd638ba67183895bd3451f41875ae3 + checksum: 10/5e115ce0d2151f364430ad177162b3947f3dd2fc165d10b205d58ba6b71111ffddd29626b81cc771c97323f5c4db6d631ec46750fa1fb31854158eb51577cf34 languageName: node linkType: hard @@ -1601,60 +1625,60 @@ __metadata: linkType: hard "@libp2p/keychain@npm:^6.0.11": - version: 6.0.11 - resolution: "@libp2p/keychain@npm:6.0.11" + version: 6.1.0 + resolution: "@libp2p/keychain@npm:6.1.0" dependencies: - "@libp2p/crypto": "npm:^5.1.14" - "@libp2p/interface": "npm:^3.1.1" + "@libp2p/crypto": "npm:^5.1.17" + "@libp2p/interface": "npm:^3.2.2" "@noble/hashes": "npm:^2.0.1" asn1js: "npm:^3.0.6" interface-datastore: "npm:^9.0.1" multiformats: "npm:^13.4.0" sanitize-filename: "npm:^1.6.3" uint8arrays: "npm:^5.1.0" - checksum: 10/4e7465497fdd160d12ba2a2bd3640a6b0aeeecd7e76f9296b043635fc2525d35b802f349717af2a8db97663867ce9087d0d233da8b95b0e660bfa318f9743f6d + checksum: 10/0f859eeade21b2a741228f57cae54c1eb91391fbd187a9d1c5c1fadad0306c1d17fa6c1d6c7cce88343366e035019facdc581733776ca5c325020149d1e95420 languageName: node linkType: hard -"@libp2p/logger@npm:^6.0.0, @libp2p/logger@npm:^6.2.3": - version: 6.2.3 - resolution: "@libp2p/logger@npm:6.2.3" +"@libp2p/logger@npm:^6.2.3, @libp2p/logger@npm:^6.2.4, @libp2p/logger@npm:^6.2.6": + version: 6.2.6 + resolution: "@libp2p/logger@npm:6.2.6" dependencies: - "@libp2p/interface": "npm:^3.1.1" + "@libp2p/interface": "npm:^3.2.2" "@multiformats/multiaddr": "npm:^13.0.1" interface-datastore: "npm:^9.0.1" multiformats: "npm:^13.4.0" weald: "npm:^1.1.0" - checksum: 10/a216365fd0a941ae045a9a223089ec996aeccaf5c37ec4262d3cf4f06a57edebb8bf3138ea882e480858e228252e258c223ee79672912744b6c0e3b538ef66f2 + checksum: 10/bba88dcce8f8db241f1fc0e205cea38a0b2cb068d23cb83e225e9aead2fb00da75dcc109397ee17c23114a7844bc8509243167af6d343d6b6ea2b47d5f6e6d79 languageName: node linkType: hard "@libp2p/multistream-select@npm:^7.0.14": - version: 7.0.14 - resolution: "@libp2p/multistream-select@npm:7.0.14" + version: 7.0.18 + resolution: "@libp2p/multistream-select@npm:7.0.18" dependencies: - "@libp2p/interface": "npm:^3.1.1" - "@libp2p/utils": "npm:^7.0.14" + "@libp2p/interface": "npm:^3.2.2" + "@libp2p/utils": "npm:^7.1.0" it-length-prefixed: "npm:^10.0.1" uint8arraylist: "npm:^2.4.8" uint8arrays: "npm:^5.1.0" - checksum: 10/73d89bfde71f3ef9bf0c24af96d8062aa7b296d32a26f08c0da0f80172de1ea40054bb5cccc5adc1dfd7374e7aafff99e089b0a61d51b7efc5522b1fdddce037 + checksum: 10/d7360076692def4beb382627d026a6d3c1074f28a74adacf4ea2fd13a426bcecfc0111264e03b7ad16af0ff865de05ae8253d6db9054ed30b457268438b6ca34 languageName: node linkType: hard -"@libp2p/peer-collections@npm:^7.0.14": - version: 7.0.14 - resolution: "@libp2p/peer-collections@npm:7.0.14" +"@libp2p/peer-collections@npm:^7.0.14, @libp2p/peer-collections@npm:^7.0.18": + version: 7.0.18 + resolution: "@libp2p/peer-collections@npm:7.0.18" dependencies: - "@libp2p/interface": "npm:^3.1.1" - "@libp2p/peer-id": "npm:^6.0.5" - "@libp2p/utils": "npm:^7.0.14" + "@libp2p/interface": "npm:^3.2.2" + "@libp2p/peer-id": "npm:^6.0.8" + "@libp2p/utils": "npm:^7.1.0" multiformats: "npm:^13.4.0" - checksum: 10/5b71bcfdffadf7fdf241c0bf4346b6db133acc2049aafa86b24bcd7171bb6c607340b48abb9e57cc464f397c09b64aaa5b2ef83b715102f0b5296bce2fb322bb + checksum: 10/0223f5b55618ee2d6b943bc6272bc2a1eaa77a98743a0e8c15d2b6de6a6f865ad6a307b7fb88624130c4cab286732e90f32c7f7f7f8d4f8b35c84df549c67532 languageName: node linkType: hard -"@libp2p/peer-id@npm:6.0.5, @libp2p/peer-id@npm:^6.0.0, @libp2p/peer-id@npm:^6.0.4, @libp2p/peer-id@npm:^6.0.5": +"@libp2p/peer-id@npm:6.0.5": version: 6.0.5 resolution: "@libp2p/peer-id@npm:6.0.5" dependencies: @@ -1666,32 +1690,44 @@ __metadata: languageName: node linkType: hard -"@libp2p/peer-record@npm:^9.0.6": - version: 9.0.6 - resolution: "@libp2p/peer-record@npm:9.0.6" +"@libp2p/peer-id@npm:^6.0.0, @libp2p/peer-id@npm:^6.0.4, @libp2p/peer-id@npm:^6.0.5, @libp2p/peer-id@npm:^6.0.8": + version: 6.0.8 + resolution: "@libp2p/peer-id@npm:6.0.8" dependencies: - "@libp2p/crypto": "npm:^5.1.14" - "@libp2p/interface": "npm:^3.1.1" - "@libp2p/peer-id": "npm:^6.0.5" + "@libp2p/crypto": "npm:^5.1.17" + "@libp2p/interface": "npm:^3.2.2" + multiformats: "npm:^13.4.0" + uint8arrays: "npm:^5.1.0" + checksum: 10/009c02bae10573adbf61159f6dd043ae21ae31e90c702cdd4b26a0b2e0fbcfd7f60f09a182e3bd9d14e3d56f78d9e1f5285c478496fd7617c89d08eeb78131f4 + languageName: node + linkType: hard + +"@libp2p/peer-record@npm:^9.0.6, @libp2p/peer-record@npm:^9.0.9": + version: 9.0.9 + resolution: "@libp2p/peer-record@npm:9.0.9" + dependencies: + "@libp2p/crypto": "npm:^5.1.17" + "@libp2p/interface": "npm:^3.2.2" + "@libp2p/peer-id": "npm:^6.0.8" "@multiformats/multiaddr": "npm:^13.0.1" multiformats: "npm:^13.4.0" protons-runtime: "npm:^6.0.1" uint8-varint: "npm:^2.0.4" uint8arraylist: "npm:^2.4.8" uint8arrays: "npm:^5.1.0" - checksum: 10/3b9560a5eacf8718b36e63b7c7b0ed82b52466e6c26964893de4851cccde5c24f46fb0733b2150dfadf757feacb9e2625da10fd513f1e7bd9203d0695a039ce4 + checksum: 10/153bd775f4b8f0e4bd03ea9c52139dc1f45b88a3cbaa0d1faadd5ae479232cd65154821657a2a008aeeb74622cd5e7cf648a420c15de3e829ad4f3d409547473 languageName: node linkType: hard "@libp2p/peer-store@npm:^12.0.14": - version: 12.0.14 - resolution: "@libp2p/peer-store@npm:12.0.14" - dependencies: - "@libp2p/crypto": "npm:^5.1.14" - "@libp2p/interface": "npm:^3.1.1" - "@libp2p/peer-collections": "npm:^7.0.14" - "@libp2p/peer-id": "npm:^6.0.5" - "@libp2p/peer-record": "npm:^9.0.6" + version: 12.0.18 + resolution: "@libp2p/peer-store@npm:12.0.18" + dependencies: + "@libp2p/crypto": "npm:^5.1.17" + "@libp2p/interface": "npm:^3.2.2" + "@libp2p/peer-collections": "npm:^7.0.18" + "@libp2p/peer-id": "npm:^6.0.8" + "@libp2p/peer-record": "npm:^9.0.9" "@multiformats/multiaddr": "npm:^13.0.1" interface-datastore: "npm:^9.0.1" it-all: "npm:^3.0.9" @@ -1701,7 +1737,7 @@ __metadata: protons-runtime: "npm:^6.0.1" uint8arraylist: "npm:^2.4.8" uint8arrays: "npm:^5.1.0" - checksum: 10/39b79cf77dede2e10633f052860516ff1457e5dd4e68589a81051e7ad88f63583ab60fe6ef460cd40759858bc1b0e0c081a1f5f71ea27f959c00cbf02eb6b5b1 + checksum: 10/041b75d0f7891413b182058b33f7c4c524ac02838c1e33a10ceff770ab55d43f3174b1bc6fb390f7ccf730d79ea17d11fa084523b2a794ec0a383ee965cedb09 languageName: node linkType: hard @@ -1738,7 +1774,7 @@ __metadata: languageName: node linkType: hard -"@libp2p/utils@npm:7.0.14, @libp2p/utils@npm:^7.0.0, @libp2p/utils@npm:^7.0.11, @libp2p/utils@npm:^7.0.14": +"@libp2p/utils@npm:7.0.14": version: 7.0.14 resolution: "@libp2p/utils@npm:7.0.14" dependencies: @@ -1769,6 +1805,38 @@ __metadata: languageName: node linkType: hard +"@libp2p/utils@npm:^7.0.0, @libp2p/utils@npm:^7.0.11, @libp2p/utils@npm:^7.0.14, @libp2p/utils@npm:^7.1.0": + version: 7.1.0 + resolution: "@libp2p/utils@npm:7.1.0" + dependencies: + "@chainsafe/is-ip": "npm:^2.1.0" + "@chainsafe/netmask": "npm:^2.0.0" + "@libp2p/crypto": "npm:^5.1.17" + "@libp2p/interface": "npm:^3.2.2" + "@libp2p/logger": "npm:^6.2.6" + "@multiformats/multiaddr": "npm:^13.0.1" + "@sindresorhus/fnv1a": "npm:^3.1.0" + any-signal: "npm:^4.1.1" + cborg: "npm:^5.1.0" + delay: "npm:^7.0.0" + is-loopback-addr: "npm:^2.0.2" + it-length-prefixed: "npm:^10.0.1" + it-pipe: "npm:^3.0.1" + it-pushable: "npm:^3.2.3" + it-stream-types: "npm:^2.0.2" + main-event: "npm:^1.0.1" + netmask: "npm:^2.0.2" + p-defer: "npm:^4.0.1" + p-event: "npm:^7.0.0" + progress-events: "npm:^1.1.0" + race-signal: "npm:^2.0.0" + uint8-varint: "npm:^2.0.4" + uint8arraylist: "npm:^2.4.8" + uint8arrays: "npm:^5.1.0" + checksum: 10/04128e650dbcd5923e0d6d169906129bff8bce01d4d7c7db161b54d8167fc64bd6fd0590cd09ef3dec77a6638018ffd9fefe433b8c2d54620f5366886f325fcb + languageName: node + linkType: hard + "@libp2p/webrtc@npm:6.0.15": version: 6.0.15 resolution: "@libp2p/webrtc@npm:6.0.15" @@ -1898,10 +1966,11 @@ __metadata: languageName: node linkType: hard -"@metamask/auto-changelog@npm:^4.0.0": - version: 4.1.0 - resolution: "@metamask/auto-changelog@npm:4.1.0" +"@metamask/auto-changelog@npm:^5.3.0": + version: 5.3.0 + resolution: "@metamask/auto-changelog@npm:5.3.0" dependencies: + "@octokit/rest": "npm:^20.0.0" diff: "npm:^5.0.0" execa: "npm:^5.1.1" semver: "npm:^7.3.5" @@ -1909,14 +1978,14 @@ __metadata: peerDependencies: prettier: ">=3.0.0" bin: - auto-changelog: dist/cli.js - checksum: 10/fe31a9eb364939c83bc5098482b761ca93593081680c4cba17b221150b4d32636cb25fd708e3692c198feddc95d8bcf524e19fa93567fb5aa30b03ea93249250 + auto-changelog: dist/cli.mjs + checksum: 10/5381c2b1efbade000bafbbee7b1becbee1787b9f24849352d16ddd3b14f511f865b3478250301f3d22f98fe0208690f62f166a476e64d38ed58361d816a673b6 languageName: node linkType: hard -"@metamask/auto-changelog@npm:^5.3.0": - version: 5.3.0 - resolution: "@metamask/auto-changelog@npm:5.3.0" +"@metamask/auto-changelog@npm:^6.1.0": + version: 6.1.0 + resolution: "@metamask/auto-changelog@npm:6.1.0" dependencies: "@octokit/rest": "npm:^20.0.0" diff: "npm:^5.0.0" @@ -1924,10 +1993,16 @@ __metadata: semver: "npm:^7.3.5" yargs: "npm:^17.0.1" peerDependencies: + oxfmt: ^0.45.0 prettier: ">=3.0.0" + peerDependenciesMeta: + oxfmt: + optional: true + prettier: + optional: true bin: auto-changelog: dist/cli.mjs - checksum: 10/5381c2b1efbade000bafbbee7b1becbee1787b9f24849352d16ddd3b14f511f865b3478250301f3d22f98fe0208690f62f166a476e64d38ed58361d816a673b6 + checksum: 10/d4b086ea609f1395e111d8d124d74ac94e31a872f26eba5b65906f6e634f61e328264c940e7a603cb823d58a7140f8eeafec4521b85ee6584917e2c5ac90b684 languageName: node linkType: hard @@ -1964,11 +2039,11 @@ __metadata: linkType: hard "@metamask/create-release-branch@npm:^4.1.4": - version: 4.1.4 - resolution: "@metamask/create-release-branch@npm:4.1.4" + version: 4.2.0 + resolution: "@metamask/create-release-branch@npm:4.2.0" dependencies: "@metamask/action-utils": "npm:^1.0.0" - "@metamask/auto-changelog": "npm:^4.0.0" + "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/utils": "npm:^9.0.0" debug: "npm:^4.3.4" execa: "npm:^8.0.1" @@ -1981,10 +2056,16 @@ __metadata: yaml: "npm:^2.2.2" yargs: "npm:^17.7.1" peerDependencies: + oxfmt: ^0.45.0 prettier: ">=3.0.0" + peerDependenciesMeta: + oxfmt: + optional: true + prettier: + optional: true bin: create-release-branch: bin/create-release-branch.js - checksum: 10/91282f9f20f576332bd88771988e58739e1dc7088068c74d54c5a3910bdab2e74c1f75b2205bdfa59a114dd18329d1080e04aada344b671348017c021edc82bc + checksum: 10/84a1aab8efd7da0fc94c880d98988b555b4c4fcea5ea0bf8f4b3ea8aac37dd0c2b1b7d7547fe1f8228710538769188c07fd761ffde2967562f043968cf1ed12c languageName: node linkType: hard @@ -2154,16 +2235,17 @@ __metadata: linkType: hard "@metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.2.0, @metamask/json-rpc-engine@npm:^10.2.4": - version: 10.2.4 - resolution: "@metamask/json-rpc-engine@npm:10.2.4" + version: 10.3.0 + resolution: "@metamask/json-rpc-engine@npm:10.3.0" dependencies: + "@metamask/messenger": "npm:^1.2.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^11.9.0" "@types/deep-freeze-strict": "npm:^1.1.0" deep-freeze-strict: "npm:^1.1.1" klona: "npm:^2.0.6" - checksum: 10/b207dd2a9a44674c141c2e027c082974464a37beada98a27e80fe59c9bd44e2c2a992edf8a8d7e3ed461fa27ed372c95d4e27df18752b558c10bf540b7fe7bcd + checksum: 10/8d4da5d933e4be2a85783871b6f1282763cbb5bc559e3228da099c75517530e3ac42a040109f17a4d4ff768f1c8cbcc4358f5e06b820b893af29a13f95180bd6 languageName: node linkType: hard @@ -2754,9 +2836,9 @@ __metadata: languageName: unknown linkType: soft -"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.1": - version: 1.1.1 - resolution: "@metamask/messenger@npm:1.1.1" +"@metamask/messenger@npm:^1.0.0, @metamask/messenger@npm:^1.1.1, @metamask/messenger@npm:^1.2.0": + version: 1.2.0 + resolution: "@metamask/messenger@npm:1.2.0" dependencies: "@metamask/utils": "npm:^11.9.0" yargs: "npm:^17.7.2" @@ -2764,7 +2846,7 @@ __metadata: typescript: ">=5.0.0" bin: messenger-generate-action-types: ./dist/generate-action-types/cli.mjs - checksum: 10/a959af95e9e117aa0f7ad1c280f7817fef2c0b575c76837b1a6c884c9c9ef1dd0faeaef0c2c0c2035f68c7638d1f87cd172956ee962dec97d8ab6176fa6964e3 + checksum: 10/6818e4609d6162a436cc07955905f9e57ff6dbef841e9066a5fb9cc0538e981526fbcb5eef1fa1968d79212d57ddda2fce4dda5f87eb64d8d98f7db1216a6a98 languageName: node linkType: hard @@ -3394,11 +3476,11 @@ __metadata: linkType: hard "@multiformats/multiaddr-matcher@npm:^3.0.1": - version: 3.0.1 - resolution: "@multiformats/multiaddr-matcher@npm:3.0.1" + version: 3.0.2 + resolution: "@multiformats/multiaddr-matcher@npm:3.0.2" dependencies: "@multiformats/multiaddr": "npm:^13.0.0" - checksum: 10/4778cd268b7acb604d9edbe4316dd68e648e5711a5b35cdb0db181e28d7dc75263331e7fce2fb43c20977bea059df77542ab18dd2817f42e35e61723794e0421 + checksum: 10/7758f76e51caff5d9da0df94ceedea705d1fde3ee60a735e169325558ba2750acabb50cda7d449b10eeaec0a175967a405f0026ee884726c38cdfe7be06dee12 languageName: node linkType: hard @@ -3434,15 +3516,15 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.2": - version: 1.1.2 - resolution: "@napi-rs/wasm-runtime@npm:1.1.2" +"@napi-rs/wasm-runtime@npm:^1.1.4": + version: 1.1.4 + resolution: "@napi-rs/wasm-runtime@npm:1.1.4" dependencies: "@tybys/wasm-util": "npm:^0.10.1" peerDependencies: "@emnapi/core": ^1.7.1 "@emnapi/runtime": ^1.7.1 - checksum: 10/fcb8a5cff65dfb6c44277a1f7a16da5a1be2ed609c83e13f4bb621db97b511129b8ccf808794c8906abd3561e10c2e66d3ba550f0a1a0db18f53f1e399a0a5f8 + checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af languageName: node linkType: hard @@ -3454,9 +3536,9 @@ __metadata: linkType: hard "@noble/ciphers@npm:^2.0.1": - version: 2.1.1 - resolution: "@noble/ciphers@npm:2.1.1" - checksum: 10/efca189b2719ed7309616a6824328d713bd37156e1bcead453725e51fc93311fb2f54e4d46bef0391fa17ce18bd9c2364511290774504a8600264b572f6a1db5 + version: 2.2.0 + resolution: "@noble/ciphers@npm:2.2.0" + checksum: 10/d75348aa682b41ad3e24cdd0a56c6d9ca033fb629ab93f37d6690be41c4882359b27598a11af0f5439ba82df4f9e3875dea1f875064310f68fef63cf24e3481a languageName: node linkType: hard @@ -4600,10 +4682,10 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.123.0": - version: 0.123.0 - resolution: "@oxc-project/types@npm:0.123.0" - checksum: 10/9ce1df2b9cc43b64049c983567abf5369502eb4722742f883d080438ea0939df0c6c8a234067e5c033450315242984acf59bf3648dbaeef6cb524ceedd9965b2 +"@oxc-project/types@npm:=0.127.0": + version: 0.127.0 + resolution: "@oxc-project/types@npm:0.127.0" + checksum: 10/f154f4720367186aed63a16fb1395f9039d4e6872265fe9e6b5eacc02fb2b948f9ea6c5f85efd3a015ea28aa8c31232b7a8301218ae28651659e46dd0c4f2031 languageName: node linkType: hard @@ -4758,129 +4840,129 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-cms@npm:^2.6.0, @peculiar/asn1-cms@npm:^2.6.1": - version: 2.6.1 - resolution: "@peculiar/asn1-cms@npm:2.6.1" +"@peculiar/asn1-cms@npm:^2.6.0, @peculiar/asn1-cms@npm:^2.7.0": + version: 2.7.0 + resolution: "@peculiar/asn1-cms@npm:2.7.0" dependencies: - "@peculiar/asn1-schema": "npm:^2.6.0" - "@peculiar/asn1-x509": "npm:^2.6.1" - "@peculiar/asn1-x509-attr": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-x509": "npm:^2.7.0" + "@peculiar/asn1-x509-attr": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/e431f6229b98c63a929538d266488e8c2dddc895936117da8f9ec775558e08c20ded6a4adcca4bb88bfea282e7204d4f6bba7a46da2cced162c174e1e6964f36 + checksum: 10/01515f46db97b1d5f8b0d2c181f10571efb91e11e0c034b49121c8f50ff76d6538eba04311d04347f180639a72818f3ac1c96a089eea9a1689e5cc5ee31a4117 languageName: node linkType: hard "@peculiar/asn1-csr@npm:^2.6.0": - version: 2.6.1 - resolution: "@peculiar/asn1-csr@npm:2.6.1" + version: 2.7.0 + resolution: "@peculiar/asn1-csr@npm:2.7.0" dependencies: - "@peculiar/asn1-schema": "npm:^2.6.0" - "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-x509": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/4ac2f1c3a2cb392fcdd5aa602140abe90f849af0a9e8296aab9aaf1712ee2e0c4f5fa86b0fe83975e771b0aba91fc848670f9c2008ea1e850c849fae6e181179 + checksum: 10/d966d58d0104a6833b442080ef2663172730fd95928b95c5a84f5b86963eeeaa13435ee8e8d97612a3a1fef532c714b868ceb082a6f253186606efecfaa88a0c languageName: node linkType: hard "@peculiar/asn1-ecc@npm:^2.6.0": - version: 2.6.1 - resolution: "@peculiar/asn1-ecc@npm:2.6.1" + version: 2.7.0 + resolution: "@peculiar/asn1-ecc@npm:2.7.0" dependencies: - "@peculiar/asn1-schema": "npm:^2.6.0" - "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-x509": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/baa646c1c86283d5876230b1cfbd80cf42f97b3bb8d8b23cd5830f6f8d6466e6a06887c6838f3c4c61c87df9ffd2abe905f555472e8e70d722ce964a8074d838 + checksum: 10/c30736a867dd6facf7cf5090c9eace582b927c92cbc0dfd8787b3485e3012dc846b608d5d0c3c38df9e621e0c7fbe0bb2b96e7fa34c03b67f4b59fd3d6d4e8f4 languageName: node linkType: hard -"@peculiar/asn1-pfx@npm:^2.6.1": - version: 2.6.1 - resolution: "@peculiar/asn1-pfx@npm:2.6.1" +"@peculiar/asn1-pfx@npm:^2.7.0": + version: 2.7.0 + resolution: "@peculiar/asn1-pfx@npm:2.7.0" dependencies: - "@peculiar/asn1-cms": "npm:^2.6.1" - "@peculiar/asn1-pkcs8": "npm:^2.6.1" - "@peculiar/asn1-rsa": "npm:^2.6.1" - "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-cms": "npm:^2.7.0" + "@peculiar/asn1-pkcs8": "npm:^2.7.0" + "@peculiar/asn1-rsa": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/50adc7db96928d98b85a1a2e6765ba1d4ec708f937b8172ea6a22e3b92137ea36d656aded64b3be661db39f924102c5a80da54ee647e2441af3bc19c55a183ef + checksum: 10/ced7e9c4308d427ae9107df654f28b6e28b1bc963b059e1430459c49cee8d8da74338d251ac98d1af5efc98a0ff49fbec9f71073cdbf5e0ee7f93a40ba043f7e languageName: node linkType: hard -"@peculiar/asn1-pkcs8@npm:^2.6.1": - version: 2.6.1 - resolution: "@peculiar/asn1-pkcs8@npm:2.6.1" +"@peculiar/asn1-pkcs8@npm:^2.7.0": + version: 2.7.0 + resolution: "@peculiar/asn1-pkcs8@npm:2.7.0" dependencies: - "@peculiar/asn1-schema": "npm:^2.6.0" - "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-x509": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/99c4326da30e7ef17bb8e92d8a9525b78c101e4d743493000e220f3da6bbc4755371f1dbcc2a36951fb15769c2efead20d90a08918fd268c21bebcac26e71053 + checksum: 10/9e8049e5fd9e35676c313ff0b88decb767c4d7ff09a0ab098ea1affa4b5076573b4e10fcd7968b1931b921b3107ede521c1b1bf91a1040ce454e6b44f32f3aa8 languageName: node linkType: hard "@peculiar/asn1-pkcs9@npm:^2.6.0": - version: 2.6.1 - resolution: "@peculiar/asn1-pkcs9@npm:2.6.1" - dependencies: - "@peculiar/asn1-cms": "npm:^2.6.1" - "@peculiar/asn1-pfx": "npm:^2.6.1" - "@peculiar/asn1-pkcs8": "npm:^2.6.1" - "@peculiar/asn1-schema": "npm:^2.6.0" - "@peculiar/asn1-x509": "npm:^2.6.1" - "@peculiar/asn1-x509-attr": "npm:^2.6.1" + version: 2.7.0 + resolution: "@peculiar/asn1-pkcs9@npm:2.7.0" + dependencies: + "@peculiar/asn1-cms": "npm:^2.7.0" + "@peculiar/asn1-pfx": "npm:^2.7.0" + "@peculiar/asn1-pkcs8": "npm:^2.7.0" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-x509": "npm:^2.7.0" + "@peculiar/asn1-x509-attr": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/61759a50d6adf108a0376735b2e76cdfc9c41db39a7abed23ca332f7699d831aa6324534aa38153018a31e6ee5e8fef85534c92b68067f6afcb90787e953c449 + checksum: 10/61b55f4b98e98ca659bb6ad5f043ca2fc9aa1c333372d91419251180d4332746aaed0f15975b6d9a131af3b6b066b90705b54b508c6854b6f2117fd3ab66a7a8 languageName: node linkType: hard -"@peculiar/asn1-rsa@npm:^2.6.0, @peculiar/asn1-rsa@npm:^2.6.1": - version: 2.6.1 - resolution: "@peculiar/asn1-rsa@npm:2.6.1" +"@peculiar/asn1-rsa@npm:^2.6.0, @peculiar/asn1-rsa@npm:^2.7.0": + version: 2.7.0 + resolution: "@peculiar/asn1-rsa@npm:2.7.0" dependencies: - "@peculiar/asn1-schema": "npm:^2.6.0" - "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-x509": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/e91efe57017feac71c69ee5950e9c323b45aaf10baa32153fe88f237948f9d906ba04c645d085c4293c90440cad95392a91b3760251cd0ebc8e4c1a383fc331a + checksum: 10/b314b77246a7bee0a2a76978ca08983a016afad78c74dc02b5ae6296f8a71f0146c521d431a9d6e9ba7faec42b5a8bba046ee4c9010cbd1b37067398e05e05af languageName: node linkType: hard -"@peculiar/asn1-schema@npm:^2.3.13, @peculiar/asn1-schema@npm:^2.3.8, @peculiar/asn1-schema@npm:^2.6.0": - version: 2.6.0 - resolution: "@peculiar/asn1-schema@npm:2.6.0" +"@peculiar/asn1-schema@npm:^2.3.13, @peculiar/asn1-schema@npm:^2.3.8, @peculiar/asn1-schema@npm:^2.6.0, @peculiar/asn1-schema@npm:^2.7.0": + version: 2.7.0 + resolution: "@peculiar/asn1-schema@npm:2.7.0" dependencies: + "@peculiar/utils": "npm:^2.0.2" asn1js: "npm:^3.0.6" - pvtsutils: "npm:^1.3.6" tslib: "npm:^2.8.1" - checksum: 10/af9b1094d0e020f0fd828777488578322d62a41f597ead7d80939dafcfe35b672fcb0ec7460ef66b2a155f9614d4340a98896d417a830aff1685cb4c21d5bbe4 + checksum: 10/2b18ee2f3de2b68a36b964721e5101f589d6a1db765c450ce5a929829bfc8c0819e0b128145f65639952b257b9bdaa6ce7d1a54cd93c7bf6e694fed4c36d6c98 languageName: node linkType: hard -"@peculiar/asn1-x509-attr@npm:^2.6.1": - version: 2.6.1 - resolution: "@peculiar/asn1-x509-attr@npm:2.6.1" +"@peculiar/asn1-x509-attr@npm:^2.7.0": + version: 2.7.0 + resolution: "@peculiar/asn1-x509-attr@npm:2.7.0" dependencies: - "@peculiar/asn1-schema": "npm:^2.6.0" - "@peculiar/asn1-x509": "npm:^2.6.1" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/asn1-x509": "npm:^2.7.0" asn1js: "npm:^3.0.6" tslib: "npm:^2.8.1" - checksum: 10/86f7d5495459dee81daadd830ebb7d26ec15a98f6479c88b90a915ac9f28105b0d5003ba0c382b4aa8f7fa42e399f7cc37e4fe73c26cbaacd47e63a50b132e25 + checksum: 10/7a1c4f707224cf8ebf33bb231049069cf6a64902d7b7b317b4a2f4f8a0fb606bb39f6af5944d408c4eb930e8ae1435c0049cc42490131cf991383714c179f1dd languageName: node linkType: hard -"@peculiar/asn1-x509@npm:^2.6.0, @peculiar/asn1-x509@npm:^2.6.1": - version: 2.6.1 - resolution: "@peculiar/asn1-x509@npm:2.6.1" +"@peculiar/asn1-x509@npm:^2.6.0, @peculiar/asn1-x509@npm:^2.7.0": + version: 2.7.0 + resolution: "@peculiar/asn1-x509@npm:2.7.0" dependencies: - "@peculiar/asn1-schema": "npm:^2.6.0" + "@peculiar/asn1-schema": "npm:^2.7.0" + "@peculiar/utils": "npm:^2.0.2" asn1js: "npm:^3.0.6" - pvtsutils: "npm:^1.3.6" tslib: "npm:^2.8.1" - checksum: 10/e3187ad04d397cdd6a946895a51202b67f57992dfef55e40acc7e7ea325e2854267ed2581c4b1ea729d7147e9e8e6f34af77f1ffb48e3e8b25b2216b213b4641 + checksum: 10/6e6b1124076487e46d1b9f7237f173bc7aab92230e3a7a8b3841fdc84009ece0221624bd88fe16a478aec5b4ba21a9393735038ca4e38245d7f0c1be91f00e8c languageName: node linkType: hard @@ -4893,6 +4975,15 @@ __metadata: languageName: node linkType: hard +"@peculiar/utils@npm:^2.0.2": + version: 2.0.3 + resolution: "@peculiar/utils@npm:2.0.3" + dependencies: + tslib: "npm:^2.8.1" + checksum: 10/e6b212db06e15f0ffa33482336f0e41108ce2d95fa69fa2c6f001120df1056404dc007b62bc6c90cc58d743f0cf4b23bfff89cdb8c121415d36ff0f9d60aead2 + languageName: node + linkType: hard + "@peculiar/webcrypto@npm:^1.5.0": version: 1.5.0 resolution: "@peculiar/webcrypto@npm:1.5.0" @@ -4992,111 +5083,111 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.13" +"@rolldown/binding-android-arm64@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.17" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.13" +"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.17" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.13" +"@rolldown/binding-darwin-x64@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.17" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.13" +"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.17" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.13" +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.17" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.13" +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.17" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.13" +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.17" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.13" +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.17" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.13" +"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.17" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.13" +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.17" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.13" +"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.17" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.13" +"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.17" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.13" +"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.17" dependencies: - "@emnapi/core": "npm:1.9.1" - "@emnapi/runtime": "npm:1.9.1" - "@napi-rs/wasm-runtime": "npm:^1.1.2" + "@emnapi/core": "npm:1.10.0" + "@emnapi/runtime": "npm:1.10.0" + "@napi-rs/wasm-runtime": "npm:^1.1.4" conditions: cpu=wasm32 languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.13" +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.17" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.13" +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.17" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5108,10 +5199,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "@rolldown/pluginutils@npm:1.0.0-rc.13" - checksum: 10/ffc6cdfac897c3fb7c5544560d2aaf2bd55acfb4f082295844a899bdccdd0c7baa11e27fe2b806427bd636d43aa2b22b9ec6b837bab9f416d1e29c4dd4c52516 +"@rolldown/pluginutils@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.17" + checksum: 10/d659ea756ee6d360a015708d1035c07047e08db99a4160c74c7f22a7ece5611efcc18ad56db4a63b69edb506ded47596d9c0d301919242470d8c412d916b9750 languageName: node linkType: hard @@ -5461,54 +5552,54 @@ __metadata: languageName: node linkType: hard -"@turbo/darwin-64@npm:2.9.1": - version: 2.9.1 - resolution: "@turbo/darwin-64@npm:2.9.1" +"@turbo/darwin-64@npm:2.9.9": + version: 2.9.9 + resolution: "@turbo/darwin-64@npm:2.9.9" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@turbo/darwin-arm64@npm:2.9.1": - version: 2.9.1 - resolution: "@turbo/darwin-arm64@npm:2.9.1" +"@turbo/darwin-arm64@npm:2.9.9": + version: 2.9.9 + resolution: "@turbo/darwin-arm64@npm:2.9.9" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@turbo/linux-64@npm:2.9.1": - version: 2.9.1 - resolution: "@turbo/linux-64@npm:2.9.1" +"@turbo/linux-64@npm:2.9.9": + version: 2.9.9 + resolution: "@turbo/linux-64@npm:2.9.9" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@turbo/linux-arm64@npm:2.9.1": - version: 2.9.1 - resolution: "@turbo/linux-arm64@npm:2.9.1" +"@turbo/linux-arm64@npm:2.9.9": + version: 2.9.9 + resolution: "@turbo/linux-arm64@npm:2.9.9" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@turbo/windows-64@npm:2.9.1": - version: 2.9.1 - resolution: "@turbo/windows-64@npm:2.9.1" +"@turbo/windows-64@npm:2.9.9": + version: 2.9.9 + resolution: "@turbo/windows-64@npm:2.9.9" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@turbo/windows-arm64@npm:2.9.1": - version: 2.9.1 - resolution: "@turbo/windows-arm64@npm:2.9.1" +"@turbo/windows-arm64@npm:2.9.9": + version: 2.9.9 + resolution: "@turbo/windows-arm64@npm:2.9.9" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard "@tybys/wasm-util@npm:^0.10.0, @tybys/wasm-util@npm:^0.10.1": - version: 0.10.1 - resolution: "@tybys/wasm-util@npm:0.10.1" + version: 0.10.2 + resolution: "@tybys/wasm-util@npm:0.10.2" dependencies: tslib: "npm:^2.4.0" - checksum: 10/7fe0d239397aebb002ac4855d30c197c06a05ea8df8511350a3a5b1abeefe26167c60eda8a5508337571161e4c4b53d7c1342296123f9607af8705369de9fa7f + checksum: 10/d12f1dafe12d7a573c406b35ffef0038042b9cc9fbcc74d657267eb635499b956276afc05eebdbd81bea582e1c4c921421a1dd7243a93daaa8c8216b19395c23 languageName: node linkType: hard @@ -5899,16 +5990,16 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.58.0": - version: 8.58.0 - resolution: "@typescript-eslint/project-service@npm:8.58.0" +"@typescript-eslint/project-service@npm:8.59.2": + version: 8.59.2 + resolution: "@typescript-eslint/project-service@npm:8.59.2" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.58.0" - "@typescript-eslint/types": "npm:^8.58.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.59.2" + "@typescript-eslint/types": "npm:^8.59.2" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/fab2601f76b2df61b09e3b7ff364d0e17e6d80e65e84e8a8d11f6a0813748bed3912da098659d00f46b1f277d462bd7529157182b72b5e2e0b41ee6176a0edd7 + checksum: 10/768d311bdf366519549a3806b16eb3be030328b7cda9882e60ea2a6c112111a531ef94289ec88225b70ca61d2071f1bddf2c5faa841a837d44992c918d198d7b languageName: node linkType: hard @@ -5932,13 +6023,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.58.0, @typescript-eslint/scope-manager@npm:^8.58.0": - version: 8.58.0 - resolution: "@typescript-eslint/scope-manager@npm:8.58.0" +"@typescript-eslint/scope-manager@npm:8.59.2, @typescript-eslint/scope-manager@npm:^8.58.0": + version: 8.59.2 + resolution: "@typescript-eslint/scope-manager@npm:8.59.2" dependencies: - "@typescript-eslint/types": "npm:8.58.0" - "@typescript-eslint/visitor-keys": "npm:8.58.0" - checksum: 10/97293f1215faa785a3c1ee8d630591db9dcd5fb6bdcdd0b2e818c80478d41e59a05003fb33000530780dc466fb8cf662352932080ee7406c4aaac72af4000541 + "@typescript-eslint/types": "npm:8.59.2" + "@typescript-eslint/visitor-keys": "npm:8.59.2" + checksum: 10/9a63eb5d4ae26235ce2d3348eb45ff0e1a8cc7b198f622c48e921c7bfe0f1f7fa29a8cd3856547a4d42c08b7ed334315c682a714cb3b8b62841b5d59a1d08fd4 languageName: node linkType: hard @@ -5951,12 +6042,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.58.0, @typescript-eslint/tsconfig-utils@npm:^8.38.0, @typescript-eslint/tsconfig-utils@npm:^8.58.0": - version: 8.58.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.0" +"@typescript-eslint/tsconfig-utils@npm:8.59.2, @typescript-eslint/tsconfig-utils@npm:^8.38.0, @typescript-eslint/tsconfig-utils@npm:^8.59.2": + version: 8.59.2 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.59.2" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/4f47212c0e26e6b06e97044ec5e483007d5145ef6b205393a0b43cbc0b385c75c14ba5749d01cf7d1ff100332c2cf1d336f060f7d2191bb67fb892bb4446afaa + checksum: 10/42479906a01469322d22e8d45c6200998382f19c1c2dcb59d6adb2e796238a0476f1aa8fd1a1b2c3b36c0c7aa77ebb72ffc958bd11b6efadd36cd175646d13de languageName: node linkType: hard @@ -6005,10 +6096,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.58.0, @typescript-eslint/types@npm:^8.38.0, @typescript-eslint/types@npm:^8.58.0": - version: 8.58.0 - resolution: "@typescript-eslint/types@npm:8.58.0" - checksum: 10/c68eac0bc25812fdbb2ed4a121e42bfca9f24f3c6be95f6a9c4e7b9af767f1bcfacd6d496e358166143e0a1801dc7d042ce1b5e69946ac2768d9114ff6b8d375 +"@typescript-eslint/types@npm:8.59.2, @typescript-eslint/types@npm:^8.38.0, @typescript-eslint/types@npm:^8.59.2": + version: 8.59.2 + resolution: "@typescript-eslint/types@npm:8.59.2" + checksum: 10/dc828a5c50debac37047a30ec5bfdc21e2b410c7c8c517c1ab01164fa9a0197f4f6b829f502dd992d21044442277029bfacf0c0b70d7ac9446977cbc8d375e13 languageName: node linkType: hard @@ -6050,14 +6141,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.58.0": - version: 8.58.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.58.0" +"@typescript-eslint/typescript-estree@npm:8.59.2": + version: 8.59.2 + resolution: "@typescript-eslint/typescript-estree@npm:8.59.2" dependencies: - "@typescript-eslint/project-service": "npm:8.58.0" - "@typescript-eslint/tsconfig-utils": "npm:8.58.0" - "@typescript-eslint/types": "npm:8.58.0" - "@typescript-eslint/visitor-keys": "npm:8.58.0" + "@typescript-eslint/project-service": "npm:8.59.2" + "@typescript-eslint/tsconfig-utils": "npm:8.59.2" + "@typescript-eslint/types": "npm:8.59.2" + "@typescript-eslint/visitor-keys": "npm:8.59.2" debug: "npm:^4.4.3" minimatch: "npm:^10.2.2" semver: "npm:^7.7.3" @@ -6065,7 +6156,7 @@ __metadata: ts-api-utils: "npm:^2.5.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/4d6c4175e8a4d5c097393d161016836cc322f090c3f69fd751f5bbc25afce64df9ea0c97cee8b36ac060e06dc2cca2a4de7a0c7e04e19727cc4bd98ab3291fed + checksum: 10/54a2689e5c08f35364214a542e328745401951e94526c9f95d68b14c57521e9aade1e946074a02ed2c9cc95e94fc1866c3f725f820263759a1ee2072e3ed146f languageName: node linkType: hard @@ -6100,17 +6191,17 @@ __metadata: linkType: hard "@typescript-eslint/utils@npm:^8.29.0, @typescript-eslint/utils@npm:^8.30.1, @typescript-eslint/utils@npm:^8.58.0": - version: 8.58.0 - resolution: "@typescript-eslint/utils@npm:8.58.0" + version: 8.59.2 + resolution: "@typescript-eslint/utils@npm:8.59.2" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.58.0" - "@typescript-eslint/types": "npm:8.58.0" - "@typescript-eslint/typescript-estree": "npm:8.58.0" + "@typescript-eslint/scope-manager": "npm:8.59.2" + "@typescript-eslint/types": "npm:8.59.2" + "@typescript-eslint/typescript-estree": "npm:8.59.2" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/936433b761a990147612d78bb4afc79244239541b4a4061fbbc2de1810b40ec7f78eb4e9181e5d9c5ab7acbd9bf49fc6195dbb1d823370f717f07ad492ad6c7e + checksum: 10/4e157a18b28d656b13ae07583765cc871d992abad0ae0aeb2cde819dd632d62b89da9f9e468dfefead18b9440aa2b9040ca36841525dff4ea97479583114afe0 languageName: node linkType: hard @@ -6134,13 +6225,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.58.0": - version: 8.58.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.58.0" +"@typescript-eslint/visitor-keys@npm:8.59.2": + version: 8.59.2 + resolution: "@typescript-eslint/visitor-keys@npm:8.59.2" dependencies: - "@typescript-eslint/types": "npm:8.58.0" + "@typescript-eslint/types": "npm:8.59.2" eslint-visitor-keys: "npm:^5.0.0" - checksum: 10/50b0779e19079dedf3723323a4dfa398c639b3da48f2fcf071c22ca69342e03592f1726d68ea59b9b5a51f14ab112eabc5c93fd2579c84b02a3320042ae20066 + checksum: 10/ec8d797272c12b53b9eb2b508c326823b2cb17f6bcf57606238b812bb73854675919a5e772a42499ec1ac7787a16d018fffd94e6b168cc0b58a9872b16d6f1da languageName: node linkType: hard @@ -6296,46 +6387,46 @@ __metadata: linkType: hard "@vitest/browser-playwright@npm:^4.1.3": - version: 4.1.3 - resolution: "@vitest/browser-playwright@npm:4.1.3" + version: 4.1.5 + resolution: "@vitest/browser-playwright@npm:4.1.5" dependencies: - "@vitest/browser": "npm:4.1.3" - "@vitest/mocker": "npm:4.1.3" + "@vitest/browser": "npm:4.1.5" + "@vitest/mocker": "npm:4.1.5" tinyrainbow: "npm:^3.1.0" peerDependencies: playwright: "*" - vitest: 4.1.3 + vitest: 4.1.5 peerDependenciesMeta: playwright: optional: false - checksum: 10/d2b4fa81df2f220495c309804e0b91e4398f2d36475dd2d77a61f9627b3d35ce496ca8be260dae1bf8ea3eeb94ae3a704aad0b375e27295263a03c5188570946 + checksum: 10/a69c8e9f8efd3dc3e28a9faaa3656c9e5713f93093fbaeda2f9b268ea3c30de08d8c5fe28001e54a9adcb0d1c66f7298803f9451751c7cd5c4d6274b68f5555c languageName: node linkType: hard -"@vitest/browser@npm:4.1.3, @vitest/browser@npm:^4.1.3": - version: 4.1.3 - resolution: "@vitest/browser@npm:4.1.3" +"@vitest/browser@npm:4.1.5, @vitest/browser@npm:^4.1.3": + version: 4.1.5 + resolution: "@vitest/browser@npm:4.1.5" dependencies: "@blazediff/core": "npm:1.9.1" - "@vitest/mocker": "npm:4.1.3" - "@vitest/utils": "npm:4.1.3" + "@vitest/mocker": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" magic-string: "npm:^0.30.21" pngjs: "npm:^7.0.0" sirv: "npm:^3.0.2" tinyrainbow: "npm:^3.1.0" ws: "npm:^8.19.0" peerDependencies: - vitest: 4.1.3 - checksum: 10/313424318a62628aa1773f8dad1d3e9cc24233eaa45fe9de8bb2098251ff7f667f934af3146394164aab6640180fce2d64e7c74430e93a354acf12272da541e5 + vitest: 4.1.5 + checksum: 10/830e4fff6eda823ad16e4336a67350aa1928ed5131d60290ec1e2dd0f5bc39431317047d550a415462ac0600d48c6684f9c2b4b3d3e48edd18adba080e3f1667 languageName: node linkType: hard "@vitest/coverage-v8@npm:^4.1.3": - version: 4.1.3 - resolution: "@vitest/coverage-v8@npm:4.1.3" + version: 4.1.5 + resolution: "@vitest/coverage-v8@npm:4.1.5" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.1.3" + "@vitest/utils": "npm:4.1.5" ast-v8-to-istanbul: "npm:^1.0.0" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" @@ -6345,18 +6436,18 @@ __metadata: std-env: "npm:^4.0.0-rc.1" tinyrainbow: "npm:^3.1.0" peerDependencies: - "@vitest/browser": 4.1.3 - vitest: 4.1.3 + "@vitest/browser": 4.1.5 + vitest: 4.1.5 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10/8353c9e1acd08654976ae92565459433b1f035752278dffb1a27207cdc5e60a8308616e8f2db4cfb72b29e718beccab9a4dce4c44084eeb34ba08b645a80f7ba + checksum: 10/378e1d85a1c4670af15a18b544995a43d320460b418c188d7000f96518859e4537e00ea5e38a563c42b6183437252f0ecc92b471ede30c6d43ae87b7c8e09ed3 languageName: node linkType: hard "@vitest/eslint-plugin@npm:^1.6.14": - version: 1.6.14 - resolution: "@vitest/eslint-plugin@npm:1.6.14" + version: 1.6.16 + resolution: "@vitest/eslint-plugin@npm:1.6.16" dependencies: "@typescript-eslint/scope-manager": "npm:^8.58.0" "@typescript-eslint/utils": "npm:^8.58.0" @@ -6372,29 +6463,29 @@ __metadata: optional: true vitest: optional: true - checksum: 10/78b9129dad8c6c81f7a417e6d4e00198cb237d21b2604ac7b7311a8c419a2b32d0d327c4874aa6ae75106ffdc63309385fd5a8e7c2d066be28761a82057e2719 + checksum: 10/422fdc9a80ad88adcd7b1e07fc88866784e57a00e562e1711988264f8912850c5ebf0efec815280b5f6258fcb481133c9fd2c32d04a82ff429e9b976aa2b04db languageName: node linkType: hard -"@vitest/expect@npm:4.1.3": - version: 4.1.3 - resolution: "@vitest/expect@npm:4.1.3" +"@vitest/expect@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/expect@npm:4.1.5" dependencies: "@standard-schema/spec": "npm:^1.1.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.3" - "@vitest/utils": "npm:4.1.3" + "@vitest/spy": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" chai: "npm:^6.2.2" tinyrainbow: "npm:^3.1.0" - checksum: 10/1fdd2e772674ceed7229e34ceb4ea119ccc7f9f3529b444e9d1f8255163515051fda5111615affd60ac2e82f65f2b7d414dec5e7e2979f930fac06d4a201d0f8 + checksum: 10/3e94d2d0cf4f7018ed6a7a9394bff971353ea0cc85bcbcff39212279156840b8c533be99e2fd52112e4904c4a5190bdaaf441db7c6b17e356c18577072a3f057 languageName: node linkType: hard -"@vitest/mocker@npm:4.1.3": - version: 4.1.3 - resolution: "@vitest/mocker@npm:4.1.3" +"@vitest/mocker@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/mocker@npm:4.1.5" dependencies: - "@vitest/spy": "npm:4.1.3" + "@vitest/spy": "npm:4.1.5" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -6405,7 +6496,7 @@ __metadata: optional: true vite: optional: true - checksum: 10/55c1b39e7a1226ed54beefb31341240e937f271cfbe661b6894cb331739585010ad51a93a1ac1b9c4be70967696c65fce420385a8ea2ad3f829e3af3cc302670 + checksum: 10/949784ba08996543a313459a36a730d4b0847e42ee56cfda07a3e2add67c7adf8acbd59dcf9f75b1e4bc3fe7cc487f9f260905ff9a334866d389478112e5ae82 languageName: node linkType: hard @@ -6418,6 +6509,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/pretty-format@npm:4.1.5" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10/783f8c4a0e419d1024446ae8593411c95443ea09b50c4a378986b48893998acda34429b2d1deebc065405a7ef40bb19e19c68fdeb93acd46ae98b156c42d5f39 + languageName: node + linkType: hard + "@vitest/runner@npm:4.1.3": version: 4.1.3 resolution: "@vitest/runner@npm:4.1.3" @@ -6428,22 +6528,32 @@ __metadata: languageName: node linkType: hard -"@vitest/snapshot@npm:4.1.3": - version: 4.1.3 - resolution: "@vitest/snapshot@npm:4.1.3" +"@vitest/runner@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/runner@npm:4.1.5" dependencies: - "@vitest/pretty-format": "npm:4.1.3" - "@vitest/utils": "npm:4.1.3" + "@vitest/utils": "npm:4.1.5" + pathe: "npm:^2.0.3" + checksum: 10/ba19d84a9f7bcc3102ae5304c23e5dae789aaf8fd283f826e3fd4aca87ea2687ed606cf89869773d15799666553fd265524f7d9a0869e2869e00ebd8fd53af5b + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/snapshot@npm:4.1.5" + dependencies: + "@vitest/pretty-format": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10/92613a8482da99aef0ae6c9e53cf517bbf4c84cb5b481cb3f4503893fa63b03e03b97262ac629a6eead7364113cbd6b9a2c54ac34295b568a2ee7abf2b1fa9dc + checksum: 10/cf70530d8a7320c012bdf7f6ca4f3ddbbb47c9aeb9ff5d28319e552ce64db93423d0c4facff3e112c6d711ed4228369c8fa73c88350fe6c16cf04f9ac2558caf languageName: node linkType: hard -"@vitest/spy@npm:4.1.3": - version: 4.1.3 - resolution: "@vitest/spy@npm:4.1.3" - checksum: 10/e867364f7d43072a3580e3cb2d8e3b9b9709370ae50da8cb738f48e8d4b36502b712de4b9ff376f1e91ed1eeb299329142e00047769aeb6dd09bd0c9f0a69b29 +"@vitest/spy@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/spy@npm:4.1.5" + checksum: 10/4db4bb3aea01cd737fdb06d8f498bcd2127b8c2afeaa78ff9df4147e1474aa26dd16f42dc0512c31385824e94dbb17b17fa0f4c60b7595b7b4ab946f098220ab languageName: node linkType: hard @@ -6458,6 +6568,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:4.1.5": + version: 4.1.5 + resolution: "@vitest/utils@npm:4.1.5" + dependencies: + "@vitest/pretty-format": "npm:4.1.5" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10/4f75a2df6f910578a361ae92eb92a2b6921f50cc748994f3b2e5900d0ae687b6683f33b090dedf9b96eaca23bac117817d9448a4a333c7a96b94ee767399f18c + languageName: node + linkType: hard + "@volar/language-core@npm:2.4.17, @volar/language-core@npm:~2.4.11": version: 2.4.17 resolution: "@volar/language-core@npm:2.4.17" @@ -6657,10 +6778,10 @@ __metadata: languageName: node linkType: hard -"abort-error@npm:^1.0.0, abort-error@npm:^1.0.1": - version: 1.0.1 - resolution: "abort-error@npm:1.0.1" - checksum: 10/75a878035d478e7270ef99bd81012daae7914d66a417651976ab9e0cec562cb493366eafa6dfd844b7e49a50e3cef70883170b00329db58df507ed834af9dc8f +"abort-error@npm:^1.0.0, abort-error@npm:^1.0.1, abort-error@npm:^1.0.2": + version: 1.0.2 + resolution: "abort-error@npm:1.0.2" + checksum: 10/f28f961fe4ce2f27dfab38c70b90e9157d649912c6083a0c3659dbe0b9604fd41a7676d9a4416af9c1b73c3a2986f172e7441e4b61997943bb59f253fe6665b2 languageName: node linkType: hard @@ -7456,6 +7577,15 @@ __metadata: languageName: node linkType: hard +"cborg@npm:^5.1.0": + version: 5.1.1 + resolution: "cborg@npm:5.1.1" + bin: + cborg: lib/bin.js + checksum: 10/e8be4314ab7cc9bba8be86089e195735d7df8ef32d07f64f7084cc9a9627628aea873648da0392cd84064eb6148f72927607b8ca062bafe660577ac62cdabda2 + languageName: node + linkType: hard + "chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" @@ -8005,20 +8135,20 @@ __metadata: linkType: hard "datastore-core@npm:^11.0.1": - version: 11.0.2 - resolution: "datastore-core@npm:11.0.2" + version: 11.0.4 + resolution: "datastore-core@npm:11.0.4" dependencies: - "@libp2p/logger": "npm:^6.0.0" + "@libp2p/logger": "npm:^6.2.4" interface-datastore: "npm:^9.0.0" interface-store: "npm:^7.0.0" - it-drain: "npm:^3.0.9" - it-filter: "npm:^3.1.3" - it-map: "npm:^3.1.3" - it-merge: "npm:^3.0.11" + it-drain: "npm:^3.0.10" + it-filter: "npm:^3.1.4" + it-map: "npm:^3.1.4" + it-merge: "npm:^3.0.12" it-pipe: "npm:^3.0.1" - it-sort: "npm:^3.0.8" - it-take: "npm:^3.0.8" - checksum: 10/a2ed3ea71d81c20a57fa52a75fd9b0fe417dc4856a88c4ee67af8408187d621f710bcc6d1f9720cc0f6c5f6cd0d42d005ffd64ed1712a22b23744ec57715c9fe + it-sort: "npm:^3.0.9" + it-take: "npm:^3.0.9" + checksum: 10/f979cf7634c843684475fd5e08a2777a8b8acc31974cbd1eb37724c1d42aef9ccd098c075490f3df5425ca8054df1e5cbcca21975ab33ded89b11fd80fa4e7b8 languageName: node linkType: hard @@ -8543,6 +8673,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^8.0.0": + version: 8.0.0 + resolution: "entities@npm:8.0.0" + checksum: 10/d6e2ba75e444fb101ee2fbb07c839e687306c8a509426b75186619c19196f97c1db9932ca083f823c03e4a20e7407b654aa34de8cbb7770468e20fb2d4573a0e + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -8671,9 +8808,9 @@ __metadata: linkType: hard "es-module-lexer@npm:^2.0.0": - version: 2.0.0 - resolution: "es-module-lexer@npm:2.0.0" - checksum: 10/b075855289b5f40ee496f3d7525c5c501d029c3da15c22298a0030d625bf36d1da0768b26278f7f4bada2a602459b505888e20b77c414fba5da5619b0e84dbd1 + version: 2.1.0 + resolution: "es-module-lexer@npm:2.1.0" + checksum: 10/554c4374e78a812a1fa3673871ce7d42236438c414ea80c2ec35521cd9bb26d1d9155287529057d07431fd91df50d6a26d9bee5afd755fb7f6f7c81905a03956 languageName: node linkType: hard @@ -9455,26 +9592,26 @@ __metadata: languageName: node linkType: hard -"fast-xml-builder@npm:^1.1.5": - version: 1.1.5 - resolution: "fast-xml-builder@npm:1.1.5" +"fast-xml-builder@npm:^1.1.7": + version: 1.1.9 + resolution: "fast-xml-builder@npm:1.1.9" dependencies: path-expression-matcher: "npm:^1.1.3" - checksum: 10/377c4ef816972e67192fd32757c50d2a9d4cccf352ceac48bda6681a0ee24fb0b1f1c892810f77886db760681f23fe0b8f62c7c0cc9469c0d2863c5c529ac1d2 + checksum: 10/44ef553aec4581b0fcc1b21bfd285aa9224eeb507620220228a9dd3dfd495068daaaf408b4cf9fd67354df558888680121c9d099229fdfd9672982531623781b languageName: node linkType: hard "fast-xml-parser@npm:^5.5.6": - version: 5.7.1 - resolution: "fast-xml-parser@npm:5.7.1" + version: 5.7.3 + resolution: "fast-xml-parser@npm:5.7.3" dependencies: "@nodable/entities": "npm:^2.1.0" - fast-xml-builder: "npm:^1.1.5" + fast-xml-builder: "npm:^1.1.7" path-expression-matcher: "npm:^1.5.0" strnum: "npm:^2.2.3" bin: fxparser: src/cli/cli.js - checksum: 10/ce7de013cae7707d12b9da8cb294265da3780bb8bfa36b17f98053654628a0142159d78746747b1ed38bdefca8b6817f051171183e69a527ba18e1df067e9bce + checksum: 10/00a58655d0d58c1f914c7fd8e3a94e88799c3d473e29a6d2231dc02103df069e8c6043137cbec8df1cda6525a39914d1b84455a79530f63be266876a2211251c languageName: node linkType: hard @@ -10357,19 +10494,19 @@ __metadata: linkType: hard "interface-datastore@npm:^9.0.0, interface-datastore@npm:^9.0.1": - version: 9.0.2 - resolution: "interface-datastore@npm:9.0.2" + version: 9.0.3 + resolution: "interface-datastore@npm:9.0.3" dependencies: interface-store: "npm:^7.0.0" uint8arrays: "npm:^5.1.0" - checksum: 10/830cc462db520f977ca1cd42db937ed7c4d598a0790e335f7292389117b7d56de9270e6e20a9f6b260977fe27e65077d8ce3b62fe1c10609eefc9a68aa99f99f + checksum: 10/b26b9667489f2c7ee565deb21f16579fcced08c9e488835c8519ebe9efe1a5603351bd5f7c69066c95f32ae5fca5756ca7d25f002622fccf5984570516a699b5 languageName: node linkType: hard "interface-store@npm:^7.0.0": - version: 7.0.1 - resolution: "interface-store@npm:7.0.1" - checksum: 10/d1bc7f05110dafabf70b3409c20a372c2d8b96204bcf281da5fe4fe74d1ec894bda67c10010c259efc6dc292cc06bc3df9465f8e3f2c637ba81c8ddd48a34ee8 + version: 7.0.2 + resolution: "interface-store@npm:7.0.2" + checksum: 10/9e2ee7f3e97c387ecc3d78bdd9963d2ce6605c69f0bd719e7ad7255ed1ed7f2f9520fce18a34a2a38200b5c7a28a824e526434aabf320675658809b7ed3cef62 languageName: node linkType: hard @@ -10882,9 +11019,9 @@ __metadata: linkType: hard "it-all@npm:^3.0.0, it-all@npm:^3.0.9": - version: 3.0.9 - resolution: "it-all@npm:3.0.9" - checksum: 10/7aa16a375dc077b7b4f71308a74877144146f057b3e9d360eb28393d0040a7f69f5fd5ead51cbaa9f347044a4bd0945d783259e62e1905ce947d46c8e7953026 + version: 3.0.11 + resolution: "it-all@npm:3.0.11" + checksum: 10/e6fe254d5c889d18779c271420fd6f5f838949bd186d463393decae1b64348f7cbc61547a4183a6ccf099fecf1ebc6c82b61549f3ec0fa6b0353b5b020f2f34e languageName: node linkType: hard @@ -10901,19 +11038,19 @@ __metadata: languageName: node linkType: hard -"it-drain@npm:^3.0.10, it-drain@npm:^3.0.9": - version: 3.0.10 - resolution: "it-drain@npm:3.0.10" - checksum: 10/f6ed3261aa4a9f7f371c2eefa1fa0288e86e38fbf219b6394c78d2e7eeb1415592321f30909b29dada982255938942573001070c6797c0369b434c2b99cc4b82 +"it-drain@npm:^3.0.10": + version: 3.0.12 + resolution: "it-drain@npm:3.0.12" + checksum: 10/e7fd32863546acd8656f055d909a6fa260c3846b30ae7763904d3ab0757ad59d9af2d0c3af7e7f7bff1031bc2fe65854e09ba986476da7f6c9e29ebec334d72f languageName: node linkType: hard -"it-filter@npm:^3.1.3": - version: 3.1.4 - resolution: "it-filter@npm:3.1.4" +"it-filter@npm:^3.1.4": + version: 3.1.6 + resolution: "it-filter@npm:3.1.6" dependencies: it-peekable: "npm:^3.0.0" - checksum: 10/40dfc8d6808ea456330fba9db2aaa9346a90fde845ebd944c6fadaabe313ead82df414734df20a447aead9a7e28db6db2a6cf1c0cd9a994d1b8f096394178bf8 + checksum: 10/e9d35b0991d87bc5273240409c4cf8e8c23683d10e81a17252c3ad0ad90e14241a19ffeddfe5259a2ae2e7514548279b66fd2b134c544f0b5959047e3e2650fb languageName: node linkType: hard @@ -10943,30 +11080,30 @@ __metadata: languageName: node linkType: hard -"it-map@npm:^3.1.3": - version: 3.1.4 - resolution: "it-map@npm:3.1.4" +"it-map@npm:^3.1.4": + version: 3.1.6 + resolution: "it-map@npm:3.1.6" dependencies: it-peekable: "npm:^3.0.0" - checksum: 10/556654e0e3047ed4aca72eb942a86a288f33b9568fc122aa616e7a12a1dcf75ecbd5c4040211dc223d827dffb365db2b98397f5f4f7ef42c65d7babe8e469a0d + checksum: 10/de6dd74e8dfebe9c12f02d1e124897d07647e40abb07f1826c007f0d4bb7cb7a8788a6bca24991bdea42573be49231d329a32c2b0b7b6d93045a2d028bd5e325 languageName: node linkType: hard -"it-merge@npm:^3.0.0, it-merge@npm:^3.0.11, it-merge@npm:^3.0.12": - version: 3.0.12 - resolution: "it-merge@npm:3.0.12" +"it-merge@npm:^3.0.0, it-merge@npm:^3.0.12": + version: 3.0.14 + resolution: "it-merge@npm:3.0.14" dependencies: it-queueless-pushable: "npm:^2.0.0" - checksum: 10/b9d8e76d01d3251c9e36c5dbd19c13a94f3761ae44f7911efcce42c4c5fc025d70de5dae9e1ba9b52a1dfbf1913ab597c22a70a51d41740642e69fbae8111ec7 + checksum: 10/85e002d200890e0963017c421725f62d939f39273bd435029e6178f76110255bdd9a4ad9a6fa398ef077adb150dcc0b341b3cd014a5c0c2143135e99aa9d417b languageName: node linkType: hard "it-parallel@npm:^3.0.13": - version: 3.0.13 - resolution: "it-parallel@npm:3.0.13" + version: 3.0.15 + resolution: "it-parallel@npm:3.0.15" dependencies: p-defer: "npm:^4.0.1" - checksum: 10/3e036e48c08e98d1e8eb22e8ffa0213f04a731baff6ebc0e026963eca5c6af4a571458bf07bec29269950e56636e4477bfe50302b83e15f4f38ee7488c8e97e5 + checksum: 10/bcbe9185a1437b140e0f390821374523fc93d01f2d7041528ac622108885f2efb54a7165ebeb5f7d9ec98cc9f5fb986ce66a7e42922c1e216b0773a7721d7286 languageName: node linkType: hard @@ -10989,14 +11126,14 @@ __metadata: linkType: hard "it-protobuf-stream@npm:^2.0.3": - version: 2.0.3 - resolution: "it-protobuf-stream@npm:2.0.3" + version: 2.0.6 + resolution: "it-protobuf-stream@npm:2.0.6" dependencies: - abort-error: "npm:^1.0.1" + abort-error: "npm:^1.0.2" it-length-prefixed-stream: "npm:^2.0.0" it-stream-types: "npm:^2.0.2" uint8arraylist: "npm:^2.4.8" - checksum: 10/53ea22d9a382a4de9de885b95799c1ad83fea6b9f5cec73688ca7fd909d8660e9baaa159b8fe1be61ff9fb19ade98edf8dcde0231cc65508a525cb7dcb6de08f + checksum: 10/6ebdb4c123cdab82d26010017b69625d5039b5ab53f50f9d3ce7fbe7e8d25806b2a7dd8a5182eda4f453d65e638ecbfb8e3d6611e2cadfaf28d87d946a6fa761 languageName: node linkType: hard @@ -11043,12 +11180,12 @@ __metadata: languageName: node linkType: hard -"it-sort@npm:^3.0.8": - version: 3.0.9 - resolution: "it-sort@npm:3.0.9" +"it-sort@npm:^3.0.9": + version: 3.0.11 + resolution: "it-sort@npm:3.0.11" dependencies: it-all: "npm:^3.0.0" - checksum: 10/8390eb0a1e799d1abb2e0d9ec910d4e1d5fb4ace58a462400d966872e881cba9ed5bde7c6fb89c33694d74d002717a593727e96a78cea4326724de7e279c9fe1 + checksum: 10/63383ad249f77515ddc3fe1ed72e58e0adfe14cd5864c17a13ae387c53d0bd75e4c1b0d4fb39218a454b1d4a5c8dc245e002e0d81dc9b4ee038755726ad1d115 languageName: node linkType: hard @@ -11059,10 +11196,10 @@ __metadata: languageName: node linkType: hard -"it-take@npm:^3.0.8": - version: 3.0.9 - resolution: "it-take@npm:3.0.9" - checksum: 10/e09f7223ee006ff2a6638c270e48b73614737b1c84b0177d7a628b67be894203eb14c5bdfe757a4fab59df27283e9cb7e4a4c1c232debfe7ca51dd63e13d33e9 +"it-take@npm:^3.0.9": + version: 3.0.11 + resolution: "it-take@npm:3.0.11" + checksum: 10/ed5a3719b56adc57b9a31e76b6097404069ee53f95d5a1182cfab26711ed56a6c315dfaf1d97ed5b576ad8606eb6ab49267ecbb52802c06852b36e60f0375164 languageName: node linkType: hard @@ -11186,25 +11323,25 @@ __metadata: linkType: hard "jsdom@npm:^29.0.2": - version: 29.0.2 - resolution: "jsdom@npm:29.0.2" + version: 29.1.1 + resolution: "jsdom@npm:29.1.1" dependencies: - "@asamuzakjp/css-color": "npm:^5.1.5" - "@asamuzakjp/dom-selector": "npm:^7.0.6" + "@asamuzakjp/css-color": "npm:^5.1.11" + "@asamuzakjp/dom-selector": "npm:^7.1.1" "@bramus/specificity": "npm:^2.4.2" - "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.1" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3" "@exodus/bytes": "npm:^1.15.0" css-tree: "npm:^3.2.1" data-urls: "npm:^7.0.0" decimal.js: "npm:^10.6.0" html-encoding-sniffer: "npm:^6.0.0" is-potential-custom-element-name: "npm:^1.0.1" - lru-cache: "npm:^11.2.7" - parse5: "npm:^8.0.0" + lru-cache: "npm:^11.3.5" + parse5: "npm:^8.0.1" saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" tough-cookie: "npm:^6.0.1" - undici: "npm:^7.24.5" + undici: "npm:^7.25.0" w3c-xmlserializer: "npm:^5.0.0" webidl-conversions: "npm:^8.0.1" whatwg-mimetype: "npm:^5.0.0" @@ -11215,7 +11352,7 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: 10/3ad1d9a5b6aba067427bc43be98e1c51fab489bf689a6530e596278c6326fe053c94fc47a9c133f126fbe914f421283ae723fb92214dfe4959ca6cf2ee1666f6 + checksum: 10/344aed7f91839b6c7d1b40778c5542d6ded7d42d88e1b787e10bf12d4ccd65464a5f23f774eb84350885c75a48efc99f6972adbb94dffe324a1b065d3650843c languageName: node linkType: hard @@ -11650,10 +11787,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.7": - version: 11.3.2 - resolution: "lru-cache@npm:11.3.2" - checksum: 10/045b709782593d3f4ecb69340280717fd7c685b0d36f5976466995bd668ebf1af9e6540b9647b140b0ec4de95a48e2c80ae73ea6c4449e2f4d16a617e8760bf2 +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.3.5": + version: 11.3.6 + resolution: "lru-cache@npm:11.3.6" + checksum: 10/d69ab552776954c7d310a6b2843e7d6be3a1c36c0ce45fca373c225ce5a06b95fd4ed724305d9d15fa55aa24d3cd42f854aa061bc481241225b3accf32993eab languageName: node linkType: hard @@ -12999,12 +13136,12 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^8.0.0": - version: 8.0.0 - resolution: "parse5@npm:8.0.0" +"parse5@npm:^8.0.1": + version: 8.0.1 + resolution: "parse5@npm:8.0.1" dependencies: - entities: "npm:^6.0.0" - checksum: 10/1973850932bb1cbd52ab64502761489fbe1bb43a52dee7ce41aac0b6c33a51a92aaee04661590b0912b739ae9ee316bce4c78c8ea34af42a7e522c983c3c6cf5 + entities: "npm:^8.0.0" + checksum: 10/671dedfe7cbf4714414317bc8c6b2a14c61ef44f8fd90c983b5b1870653af5aa2e3b4e25e38e9538a7120ea2b688c50908830da2bd0930d8fd4bce34aed024eb languageName: node linkType: hard @@ -13340,14 +13477,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.47, postcss@npm:^8.4.48, postcss@npm:^8.5.6, postcss@npm:^8.5.8": - version: 8.5.8 - resolution: "postcss@npm:8.5.8" +"postcss@npm:^8.4.47, postcss@npm:^8.4.48, postcss@npm:^8.5.10, postcss@npm:^8.5.6": + version: 8.5.14 + resolution: "postcss@npm:8.5.14" dependencies: nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/cbacbfd7f767e2c820d4bf09a3a744834dd7d14f69ff08d1f57b1a7defce9ae5efcf31981890d9697a972a64e9965de677932ef28e4c8ba23a87aad45b82c459 + checksum: 10/2e3f4dea69692918fe9df5402beb0e54df84499995a094f2fbf63d1a9e38bc1b7a42854df47f09e02593213e01a5eb0627b1d1bd6d1b0ea90767b2e072f7167c languageName: node linkType: hard @@ -13461,10 +13598,10 @@ __metadata: languageName: node linkType: hard -"progress-events@npm:^1.0.0, progress-events@npm:^1.0.1": - version: 1.0.1 - resolution: "progress-events@npm:1.0.1" - checksum: 10/21e8ba984e6c6f6764279fabdf7b34d8110c1720757360fc8cad56b1622e67857fe543619652b64cee51a880a2a4a5febdcb4ff86e4c2969ed90048e2264f42f +"progress-events@npm:^1.0.0, progress-events@npm:^1.0.1, progress-events@npm:^1.1.0": + version: 1.1.0 + resolution: "progress-events@npm:1.1.0" + checksum: 10/ea37578b3e5dd6e60832b874fd66edf43398427256327f58e7f8228f6365294ed8be53818418a642833b7c611e58b650331bb691d852d538a236aade0ec6d1dd languageName: node linkType: hard @@ -13993,27 +14130,27 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.0-rc.13": - version: 1.0.0-rc.13 - resolution: "rolldown@npm:1.0.0-rc.13" - dependencies: - "@oxc-project/types": "npm:=0.123.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.13" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.13" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.13" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.13" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.13" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.13" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.13" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.13" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.13" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.13" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.13" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.13" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.13" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.13" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.13" - "@rolldown/pluginutils": "npm:1.0.0-rc.13" +"rolldown@npm:1.0.0-rc.17": + version: 1.0.0-rc.17 + resolution: "rolldown@npm:1.0.0-rc.17" + dependencies: + "@oxc-project/types": "npm:=0.127.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.17" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.17" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.17" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.17" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.17" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.17" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.17" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.17" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.17" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.17" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.17" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.17" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.17" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.17" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.17" + "@rolldown/pluginutils": "npm:1.0.0-rc.17" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -14047,7 +14184,7 @@ __metadata: optional: true bin: rolldown: bin/cli.mjs - checksum: 10/d30c816b11f712f24966a4dde34997ef6e755c1613bb59c9e2ad89e5fffffb6eb0e52a4678261f2401efe69ada168a51d93b1cae1f23cc24498030aee67b0fb7 + checksum: 10/5e7415a7cb732c4f7168ab6dcc841ed9ec4ad614058294a53d94821a762c274a69b009e41e9c8e4983a059907f02d462030a36b42543c0f41ce702fcd68d10d5 languageName: node linkType: hard @@ -14658,9 +14795,9 @@ __metadata: linkType: hard "std-env@npm:^4.0.0-rc.1": - version: 4.0.0 - resolution: "std-env@npm:4.0.0" - checksum: 10/19ef21cd85da52dc1178288d1b69e242b6579c0a76ddba2374f859aa58678797ec4a34c4f1fe6b9007a032e04d6fd3fca4e27349c88809265a9cbd90d924203f + version: 4.1.0 + resolution: "std-env@npm:4.1.0" + checksum: 10/008146cdb834010383138d356e0dd3e3b0ac127a8229f711b8c518bb22940813cc0dcd654fc76b17f0b18179f56089f8b8e52bd6a7ffa0041a966581e7a44dbe languageName: node linkType: hard @@ -15094,13 +15231,13 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": - version: 0.2.15 - resolution: "tinyglobby@npm:0.2.15" +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.16, tinyglobby@npm:^0.2.9": + version: 0.2.16 + resolution: "tinyglobby@npm:0.2.16" dependencies: fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.3" - checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 + picomatch: "npm:^4.0.4" + checksum: 10/5c2c41b572ada38449e7c86a5fe034f204a1dbba577225a761a14f29f48dc3f2fc0d81a6c56fcc67c5a742cc3aa9fb5e2ca18dbf22b610b0bc0e549b34d5a0f8 languageName: node linkType: hard @@ -15294,15 +15431,15 @@ __metadata: linkType: hard "turbo@npm:^2.9.1": - version: 2.9.1 - resolution: "turbo@npm:2.9.1" - dependencies: - "@turbo/darwin-64": "npm:2.9.1" - "@turbo/darwin-arm64": "npm:2.9.1" - "@turbo/linux-64": "npm:2.9.1" - "@turbo/linux-arm64": "npm:2.9.1" - "@turbo/windows-64": "npm:2.9.1" - "@turbo/windows-arm64": "npm:2.9.1" + version: 2.9.9 + resolution: "turbo@npm:2.9.9" + dependencies: + "@turbo/darwin-64": "npm:2.9.9" + "@turbo/darwin-arm64": "npm:2.9.9" + "@turbo/linux-64": "npm:2.9.9" + "@turbo/linux-arm64": "npm:2.9.9" + "@turbo/windows-64": "npm:2.9.9" + "@turbo/windows-arm64": "npm:2.9.9" dependenciesMeta: "@turbo/darwin-64": optional: true @@ -15318,7 +15455,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/bedcd8b17dda58c384ecd70fae5895894cb64d4f37677604500c72b94b4a02674ccc11c7d3065775eeac1048900c4f0db067ba2bd024cf469976ac367ad6436e + checksum: 10/9fa919c66fa67f42f1f811daf27ca6598d9bef22a6087d89b893daa8905693095461f0887b88cd765d7d2266c7ec42a1178aeb9e906a2612b23d12b857b57e81 languageName: node linkType: hard @@ -15580,10 +15717,10 @@ __metadata: languageName: node linkType: hard -"undici@npm:^7.24.5": - version: 7.24.7 - resolution: "undici@npm:7.24.7" - checksum: 10/bce7b75fe2656bbd1f9c9d5d1b6b89670773281343be25d0b1f4d808dcce97d81509987d1f3183d37a63d3a57f5f217ed8ed15ee3e103384c54e190f4e360c48 +"undici@npm:^7.25.0": + version: 7.25.0 + resolution: "undici@npm:7.25.0" + checksum: 10/038d3568c72bb976e3cc389284f7f1cc64cd70d578300e4676a449fbcb624a35fe99ac127b5f3729f18b8246d6c090444ab61b1b67736bb88f52a3e913d76bf8 languageName: node linkType: hard @@ -15942,8 +16079,8 @@ __metadata: linkType: hard "vite-plugin-static-copy@npm:^4.0.1": - version: 4.0.1 - resolution: "vite-plugin-static-copy@npm:4.0.1" + version: 4.1.0 + resolution: "vite-plugin-static-copy@npm:4.1.0" dependencies: chokidar: "npm:^3.6.0" p-map: "npm:^7.0.4" @@ -15951,20 +16088,20 @@ __metadata: tinyglobby: "npm:^0.2.15" peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/fb3fc7942034ee71f90fb874efb63b50a3103ab47e06bc9f9df47eb2dbc6440f005c1ea6045dbf95e4575fcbead0c134cb1007d7383e791b9db30176e126e7f5 + checksum: 10/22aee7bdf17dba47f933ceffb559e2aff7366a79013a619d41dfb84913d7f68b7bdd277d4bb4636aeacf7bc740b003284d04f0a2aed2e1fc70b6d9e6235440e4 languageName: node linkType: hard "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.6": - version: 8.0.6 - resolution: "vite@npm:8.0.6" + version: 8.0.10 + resolution: "vite@npm:8.0.10" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.8" - rolldown: "npm:1.0.0-rc.13" - tinyglobby: "npm:^0.2.15" + postcss: "npm:^8.5.10" + rolldown: "npm:1.0.0-rc.17" + tinyglobby: "npm:^0.2.16" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 "@vitejs/devtools": ^0.1.0 @@ -16008,7 +16145,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/d7cb3f41c5c0b4b20abebdd4fd1db3cfa7b7813406cc9f7d378b01c9a8c156e1f0e016eb320a4aebecece1dfeb7070b268c02c5ea53df90c0abb82c0ba86dc39 + checksum: 10/64c6fa4efa1a9ca3e1cacbcca16487b75ea25d62efbfb99c4e571b5f716296dc4f8af825eb624e273b11c3bee4e87daec35815fb6a56e01c843659c003ed2bcd languageName: node linkType: hard @@ -16022,16 +16159,16 @@ __metadata: linkType: hard "vitest@npm:^4.1.3": - version: 4.1.3 - resolution: "vitest@npm:4.1.3" - dependencies: - "@vitest/expect": "npm:4.1.3" - "@vitest/mocker": "npm:4.1.3" - "@vitest/pretty-format": "npm:4.1.3" - "@vitest/runner": "npm:4.1.3" - "@vitest/snapshot": "npm:4.1.3" - "@vitest/spy": "npm:4.1.3" - "@vitest/utils": "npm:4.1.3" + version: 4.1.5 + resolution: "vitest@npm:4.1.5" + dependencies: + "@vitest/expect": "npm:4.1.5" + "@vitest/mocker": "npm:4.1.5" + "@vitest/pretty-format": "npm:4.1.5" + "@vitest/runner": "npm:4.1.5" + "@vitest/snapshot": "npm:4.1.5" + "@vitest/spy": "npm:4.1.5" + "@vitest/utils": "npm:4.1.5" es-module-lexer: "npm:^2.0.0" expect-type: "npm:^1.3.0" magic-string: "npm:^0.30.21" @@ -16049,12 +16186,12 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.3 - "@vitest/browser-preview": 4.1.3 - "@vitest/browser-webdriverio": 4.1.3 - "@vitest/coverage-istanbul": 4.1.3 - "@vitest/coverage-v8": 4.1.3 - "@vitest/ui": 4.1.3 + "@vitest/browser-playwright": 4.1.5 + "@vitest/browser-preview": 4.1.5 + "@vitest/browser-webdriverio": 4.1.5 + "@vitest/coverage-istanbul": 4.1.5 + "@vitest/coverage-v8": 4.1.5 + "@vitest/ui": 4.1.5 happy-dom: "*" jsdom: "*" vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -16085,7 +16222,7 @@ __metadata: optional: false bin: vitest: vitest.mjs - checksum: 10/c12755abfd2fc0dad20894c9c156fb552dfb157d2f6997a6e00dad4ff215f21e09c5c8e8ff33cf54e122bda21b2c5500031587e35930593d880ae4ba293477bc + checksum: 10/8b768514993d8908fc9b5f2d619943d23b81aaba9443132583bd58aeb441bf76d152961326de9ca328ff0efcddbf8a58f4568a7b66a4391202542ed772613d81 languageName: node linkType: hard From 5c18801df931f90ff8f91618c46cc6d15e4d6034 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:40:09 -0500 Subject: [PATCH 60/68] docs(sheaves): Correct 'overlapping' example --- packages/sheaves/docs/INTRODUCTION.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sheaves/docs/INTRODUCTION.md b/packages/sheaves/docs/INTRODUCTION.md index a362cb7097..fb40a11236 100644 --- a/packages/sheaves/docs/INTRODUCTION.md +++ b/packages/sheaves/docs/INTRODUCTION.md @@ -26,11 +26,12 @@ intersect. Composition is then a matter of bookkeeping. If `aliceCap = Read("foo/bar")` and `bobCap = Read("foo/baz")` are both attenuations of the same -`FileSystem`, their union is `Read("foo/{bar,baz}")`. Where the scopes -overlap (here: the `foo/` prefix), the shared base ensures coherent behavior -— there is nothing to reconcile. +`FileSystem`, their union is `Read("foo/{bar,baz}")`. And unions of unions +are coherent, too: `Read("foo/{bar,baz}")` composes with `Read("foo/{baz,bux}")` +into `Read("foo/{bar,baz,bux}")`. Where the scopes overlap (here: `foo/baz`), +the shared base ensures coherent behavior — there is nothing to reconcile. -This is the easy case, and the one ocap programming is built around. +This is the easy case, ocap composition of related attenuations. ## Sheaves: alignment without a shared base @@ -69,7 +70,8 @@ the sheaf has glued together and hands back a narrower view restricted by providers can be treated as one — and `getSection` carves a slice out of that unified surface for the caller. The result is that you can attenuate a composition of capabilities the same way you would attenuate a single -one. +one. And because the returned section is itself a capability, it can be +a provider to another sheaf - the construction composes with itself. The guard determines what is invokable through `userFacing`. Anything outside the guard is simply not in the interface — there is no extra @@ -79,7 +81,5 @@ ocap is unsupported. Where multiple providers cover the same invocation, a caller-supplied **policy** selects which one runs (see [POLICY.md](./POLICY.md)). Where -exactly one covers it, the choice is forced. And because the returned -section is itself a capability, it can be a provider to another sheaf — -the construction composes with itself. See [USAGE.md](./USAGE.md) for +exactly one covers it, the choice is forced. See [USAGE.md](./USAGE.md) for worked examples. From aff1278949c7f33235b5a1a02ce6ad6d3a851da3 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:03:12 -0500 Subject: [PATCH 61/68] refactor(sheaves): drop 'stalk' terminology in favor of 'candidate' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames stalk.ts → match.ts and getStalk → getMatchingProviders. The function returns providers (pre-evaluation), not candidates, so the new name agrees with the type. Drops the Stalk glossary entry from the README — every other doc and code comment already used candidate(s) for both the element and the set. --- packages/sheaves/CHANGELOG.md | 2 +- packages/sheaves/README.md | 53 +++++++++---------- packages/sheaves/src/compose.ts | 2 +- packages/sheaves/src/guard.test.ts | 2 +- .../src/{stalk.test.ts => match.test.ts} | 46 ++++++++-------- packages/sheaves/src/{stalk.ts => match.ts} | 4 +- packages/sheaves/src/metadata.ts | 2 +- packages/sheaves/src/sheafify.e2e.test.ts | 8 +-- packages/sheaves/src/sheafify.ts | 6 +-- 9 files changed, 62 insertions(+), 63 deletions(-) rename packages/sheaves/src/{stalk.test.ts => match.test.ts} (73%) rename packages/sheaves/src/{stalk.ts => match.ts} (94%) diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index e24c4aace5..d1935cfbd3 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 set of capability providers. - `Provider` type — an input to `sheafify`: a `{ exo, metadata? }` pair where `exo` is a `Section` and `metadata` is an optional `MetadataSpec`. -- `Candidate` type — a post-evaluation entry in the stalk: `{ exo, +- `Candidate` type — a post-evaluation entry in the candidate set: `{ exo, metadata }` with metadata already resolved from its spec. - `Section` type — an exo capability covering a region of the interface topology. diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index 0837d3a641..c51484422f 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -32,25 +32,20 @@ This is an element of the presheaf F = F_sem x F_op. > narrower open set. **Candidate** — An equivalence class of providers at an invocation point, -identified by metadata. At dispatch time, providers in the stalk with identical +identified by metadata. At dispatch time, matching providers with identical metadata are collapsed into a single candidate; the system picks an arbitrary representative for dispatch. If two capabilities are indistinguishable by metadata, the sheaf has no data to prefer one over the other. -> Two `getBalance(string)` providers both with `{ cost: 1 }` collapse into -> one candidate. The policy never sees both — it receives one representative. +> At `("getBalance", "alice")` the candidate set might contain two entries +> (cost 1 vs 100); at `("transfer", ...)` it might contain one. Two +> `getBalance(string)` providers both with `{ cost: 1 }` collapse into one +> candidate — the policy never sees both, it receives one representative. -**Stalk** — The set of candidates matching a specific `(method, args)` invocation, -computed at dispatch time by guard filtering and then collapsing equivalent -entries. - -> Stalk at `("getBalance", "alice")` might contain two candidates (cost 1 vs 100); -> stalk at `("transfer", ...)` might contain one. - -**Policy** — An `async function*` coroutine that yields candidates from a -multi-candidate stalk in preference order. See [POLICY.md](./docs/POLICY.md) -for the coroutine protocol, `PolicyContext`, and the semantic equivalence -assumption required of all policies. +**Policy** — An `async function*` coroutine that yields candidates in +preference order when more than one matches an invocation. See +[POLICY.md](./docs/POLICY.md) for the coroutine protocol, `PolicyContext`, and +the semantic equivalence assumption required of all policies. At dispatch time, metadata is decomposed into **constraints** (keys with the same value across every candidate — topologically determined, not a choice) and @@ -59,9 +54,9 @@ receives only options on each candidate; constraints arrive separately in the context. > `argmin` by cost, `argmin` by latency, or any custom selection logic. The -> policy is never invoked when the stalk resolves to a single candidate — either -> because only one provider matched, or because all matching providers had -> identical metadata and collapsed to one representative. +> policy is never invoked when only one candidate remains — either because +> only one provider matched, or because all matching providers had identical +> metadata and collapsed to one representative. **Sheaf** — The authority manager returned by `sheafify`. Holds the provider data (frozen at construction time) and exposes factory methods that @@ -79,13 +74,13 @@ const sheaf = sheafify({ name: 'Wallet', providers }); At each invocation point `(method, args)` within a granted section: ``` -getStalk(providers, method, args) presheaf → stalk (filter by guard) -evaluateMetadata(stalk, args) metadata specs → concrete values -collapseEquivalent(stalk) locality condition (quotient by metadata) -decomposeMetadata(collapsed) restriction map (constraints / options) -policy(candidates, { method, args, operational selection (extra-theoretic) +getMatchingProviders(providers, method, args) presheaf → matches (filter by guard) +evaluateMetadata(matches, args) metadata specs → concrete values +collapseEquivalent(candidates) locality condition (quotient by metadata) +decomposeMetadata(collapsed) restriction map (constraints / options) +policy(candidates, { method, args, operational selection (extra-theoretic) constraints }) -dispatch to chosen.exo evaluation +dispatch to chosen.exo evaluation ``` The pipeline short-circuits at two points: if only one provider matches the @@ -93,7 +88,7 @@ guard, it is invoked directly without evaluate/collapse/policy; if all matching providers collapse to an identical candidate, the single representative is invoked without calling the policy. -`callable` metadata specs make the stalk shape depend on the invocation +`callable` metadata specs make the candidate set depend on the invocation arguments. A `swap(amount)` provider can produce `{ cost: 'low' }` for small amounts and `{ cost: 'high' }` for large ones, yielding a different set of candidates — and potentially a different policy outcome — for the same method @@ -108,14 +103,14 @@ interchangeable. Under the sheaf condition (effect-equivalence), this recovers the classical equivalence relation on germs. **Pseudosheafification.** The sheafification functor would precompute the full -etale space. This system defers to invocation time: compute the stalk, -collapse, decompose, select via policy. The trade-off is that global coherence -(a policy choosing consistently across points) is not guaranteed. +etale space. This system defers to invocation time: match by guard, evaluate +metadata, collapse, decompose, select via policy. The trade-off is that global +coherence (a policy choosing consistently across points) is not guaranteed. **Restriction and gluing are implicit.** Guard restriction induces a restriction map on metadata: restricting to a point filters the presheaf to -covering providers (`getStalk`), then `decomposeMetadata` strips the metadata -to distinguishing keys — the restricted metadata over that point. The join +covering providers (`getMatchingProviders`), then `decomposeMetadata` strips +the metadata to distinguishing keys — the restricted metadata over that point. The join works dually: the union of two providers has the join of their metadata, and restriction at any point recovers the local distinguishing keys in O(n). Gluing follows: compatible providers (equal metadata on their overlap) produce a diff --git a/packages/sheaves/src/compose.ts b/packages/sheaves/src/compose.ts index ef1d7fc5c7..4386b34ed7 100644 --- a/packages/sheaves/src/compose.ts +++ b/packages/sheaves/src/compose.ts @@ -3,7 +3,7 @@ import type { Candidate, Policy, PolicyContext } from './types.ts'; /** * A policy that yields all candidates in their original order without filtering. * - * Use as a placeholder when the sheaf always has a single-candidate stalk + * Use as a placeholder when the sheaf always resolves to a single candidate * (the policy is never actually called) or to express "try everything in * declaration order" as an explicit policy. * diff --git a/packages/sheaves/src/guard.test.ts b/packages/sheaves/src/guard.test.ts index b2cbbea38c..f881a0eaa4 100644 --- a/packages/sheaves/src/guard.test.ts +++ b/packages/sheaves/src/guard.test.ts @@ -6,8 +6,8 @@ import { getInterfaceMethodGuards, getMethodPayload, } from './guard.ts'; +import { guardCoversPoint } from './match.ts'; import { makeSection } from './section.ts'; -import { guardCoversPoint } from './stalk.ts'; describe('collectSheafGuard', () => { it('variable arity: add with 1, 2, and 3 args', () => { diff --git a/packages/sheaves/src/stalk.test.ts b/packages/sheaves/src/match.test.ts similarity index 73% rename from packages/sheaves/src/stalk.test.ts rename to packages/sheaves/src/match.test.ts index 7b840dfdea..9dcf412158 100644 --- a/packages/sheaves/src/stalk.test.ts +++ b/packages/sheaves/src/match.test.ts @@ -2,9 +2,9 @@ import { M } from '@endo/patterns'; import type { MethodGuard } from '@endo/patterns'; import { describe, it, expect } from 'vitest'; +import { getMatchingProviders } from './match.ts'; import { constant } from './metadata.ts'; import { makeSection } from './section.ts'; -import { getStalk } from './stalk.ts'; import type { Provider } from './types.ts'; const makeProvider = ( @@ -17,7 +17,7 @@ const makeProvider = ( metadata: constant(metadata), }); -describe('getStalk', () => { +describe('getMatchingProviders', () => { it('returns matching providers for a method and args', () => { const providers = [ makeProvider( @@ -34,7 +34,7 @@ describe('getStalk', () => { ), ]; - const candidates = getStalk(providers, 'add', [1, 2]); + const candidates = getMatchingProviders(providers, 'add', [1, 2]); expect(candidates).toHaveLength(2); }); @@ -54,7 +54,7 @@ describe('getStalk', () => { ), ]; - const candidates = getStalk(providers, 'add', [1]); + const candidates = getMatchingProviders(providers, 'add', [1]); expect(candidates).toHaveLength(1); expect(candidates[0]!.metadata).toStrictEqual(constant({ cost: 1 })); }); @@ -69,7 +69,7 @@ describe('getStalk', () => { ), ]; - const candidates = getStalk(providers, 'add', [1]); + const candidates = getMatchingProviders(providers, 'add', [1]); expect(candidates).toHaveLength(0); }); @@ -83,7 +83,7 @@ describe('getStalk', () => { ), ]; - const candidates = getStalk(providers, 'add', ['not-a-number']); + const candidates = getMatchingProviders(providers, 'add', ['not-a-number']); expect(candidates).toHaveLength(0); }); @@ -97,7 +97,7 @@ describe('getStalk', () => { ), ]; - const candidates = getStalk(providers, 'add', ['bob']); + const candidates = getMatchingProviders(providers, 'add', ['bob']); expect(candidates).toHaveLength(0); }); @@ -115,12 +115,14 @@ describe('getStalk', () => { ), ]; - expect(getStalk(providers, 'greet', ['alice'])).toHaveLength(1); - expect(getStalk(providers, 'greet', ['alice', 'hi'])).toHaveLength(1); - expect(getStalk(providers, 'greet', [])).toHaveLength(0); - expect(getStalk(providers, 'greet', ['alice', 'hi', 'extra'])).toHaveLength( - 0, - ); + expect(getMatchingProviders(providers, 'greet', ['alice'])).toHaveLength(1); + expect( + getMatchingProviders(providers, 'greet', ['alice', 'hi']), + ).toHaveLength(1); + expect(getMatchingProviders(providers, 'greet', [])).toHaveLength(0); + expect( + getMatchingProviders(providers, 'greet', ['alice', 'hi', 'extra']), + ).toHaveLength(0); }); it('matches providers with rest args', () => { @@ -133,13 +135,15 @@ describe('getStalk', () => { ), ]; - expect(getStalk(providers, 'log', ['info'])).toHaveLength(1); - expect(getStalk(providers, 'log', ['info', 'msg'])).toHaveLength(1); - expect(getStalk(providers, 'log', ['info', 'msg', 'extra'])).toHaveLength( - 1, - ); - expect(getStalk(providers, 'log', [])).toHaveLength(0); - expect(getStalk(providers, 'log', [42])).toHaveLength(0); + expect(getMatchingProviders(providers, 'log', ['info'])).toHaveLength(1); + expect( + getMatchingProviders(providers, 'log', ['info', 'msg']), + ).toHaveLength(1); + expect( + getMatchingProviders(providers, 'log', ['info', 'msg', 'extra']), + ).toHaveLength(1); + expect(getMatchingProviders(providers, 'log', [])).toHaveLength(0); + expect(getMatchingProviders(providers, 'log', [42])).toHaveLength(0); }); it('returns all providers when all match', () => { @@ -164,7 +168,7 @@ describe('getStalk', () => { ), ]; - const candidates = getStalk(providers, 'f', ['hello']); + const candidates = getMatchingProviders(providers, 'f', ['hello']); expect(candidates).toHaveLength(3); }); }); diff --git a/packages/sheaves/src/stalk.ts b/packages/sheaves/src/match.ts similarity index 94% rename from packages/sheaves/src/stalk.ts rename to packages/sheaves/src/match.ts index bc36a5a246..04d75b12cc 100644 --- a/packages/sheaves/src/stalk.ts +++ b/packages/sheaves/src/match.ts @@ -1,5 +1,5 @@ /** - * Stalk computation: filter providers by guard matching. + * Filter providers by guard matching at an invocation point. */ import { GET_INTERFACE_GUARD } from '@endo/exo'; @@ -58,7 +58,7 @@ export const guardCoversPoint = ( * @param args - The arguments to the method invocation. * @returns The providers whose guards accept the invocation. */ -export const getStalk = ( +export const getMatchingProviders = ( providers: readonly T[], method: string, args: unknown[], diff --git a/packages/sheaves/src/metadata.ts b/packages/sheaves/src/metadata.ts index 2306475ee9..6b28a1c864 100644 --- a/packages/sheaves/src/metadata.ts +++ b/packages/sheaves/src/metadata.ts @@ -16,7 +16,7 @@ const isPlainObjectRecord = (value: object): boolean => { * Normalize evaluated metadata: empty sentinel is `{}`; invalid shapes throw. * * @param raw - Result from constant value or callable, before validation. - * @returns A plain object suitable for stalk metadata. + * @returns A plain object suitable for candidate metadata. */ const normalizeEvaluatedSheafMetadata = ( raw: unknown, diff --git a/packages/sheaves/src/sheafify.e2e.test.ts b/packages/sheaves/src/sheafify.e2e.test.ts index 7d730ec3b7..febd4eb854 100644 --- a/packages/sheaves/src/sheafify.e2e.test.ts +++ b/packages/sheaves/src/sheafify.e2e.test.ts @@ -63,7 +63,7 @@ describe('e2e: cost-optimal routing', () => { expect(remote0GetBalance).not.toHaveBeenCalled(); local1GetBalance.mockClear(); - // bob: only remote matches (stalk=1, lift not invoked) + // bob: only remote matches (one candidate, lift not invoked) await E(wallet).getBalance('bob'); expect(remote0GetBalance).toHaveBeenCalledWith('bob'); expect(local1GetBalance).not.toHaveBeenCalled(); @@ -168,7 +168,7 @@ describe('e2e: multi-tier capability routing', () => { lift: fastest, }); - // Phase 1 — single backend: stalk is always 1, lift never fires. + // Phase 1 — single backend: always one candidate, lift never fires. await E(wallet).getBalance('alice'); await E(wallet).getBalance('bob'); await E(wallet).getBalance('dave'); @@ -262,7 +262,7 @@ describe('e2e: multi-tier capability routing', () => { lift: fastest, }); - // transfer: only write-backend declares it → stalk=1, lift bypassed. + // transfer: only write-backend declares it → one candidate, lift bypassed. const facade = wallet as unknown as Record< string, (...args: unknown[]) => unknown @@ -388,7 +388,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { expect(pullGetBalance).not.toHaveBeenCalled(); pushGetBalance.mockClear(); - // bob: only pull matches (stalk=1, lift bypassed) + // bob: only pull matches (one candidate, lift bypassed) await E(wallet).getBalance('bob'); expect(pullGetBalance).toHaveBeenCalledWith('bob'); expect(pushGetBalance).not.toHaveBeenCalled(); diff --git a/packages/sheaves/src/sheafify.ts b/packages/sheaves/src/sheafify.ts index dc2992ddc0..dd6d2d4044 100644 --- a/packages/sheaves/src/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -5,7 +5,7 @@ * that produces dispatch sections over a fixed set of providers. * * Each dispatch through a granted section: - * 1. Computes the matching providers (getStalk — providers whose guard covers the point) + * 1. Filters to providers whose guard covers the point (getMatchingProviders) * 2. Collapses equivalent candidates (same metadata → one representative) * 3. Decomposes metadata into constraints + options * 4. Invokes the policy on the distinguished options @@ -20,8 +20,8 @@ import { makeDiscoverableExo } from '@metamask/kernel-utils'; import { stringify } from '@metamask/kernel-utils'; import { asyncifyMethodGuards, collectSheafGuard } from './guard.ts'; +import { getMatchingProviders } from './match.ts'; import { evaluateMetadata } from './metadata.ts'; -import { getStalk } from './stalk.ts'; import type { Candidate, MetadataSpec, @@ -226,7 +226,7 @@ export const sheafify = < method: string, args: unknown[], ): Promise => { - const candidates = getStalk(frozenProviders, method, args); + const candidates = getMatchingProviders(frozenProviders, method, args); const evaluatedCandidates: Candidate[] = candidates.map( (provider) => ({ exo: provider.exo, From 2c8897134bfcff0ffd75b1a890f3ba29489b5553 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:21:44 -0500 Subject: [PATCH 62/68] refactor(sheaves): drop deprecated global section constructors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes Sheaf.getGlobalSection and Sheaf.getDiscoverableGlobalSection. Callers now always pass an explicit guard to getSection/ getDiscoverableSection, making each capability's scope visible at the call site. For callers that assemble providers dynamically and don't know the union guard until after sheafify runs, collectSheafGuard is now exported from the package — the same primitive the deprecated methods used internally. Co-Authored-By: Claude Opus 4.7 --- packages/sheaves/CHANGELOG.md | 6 +- packages/sheaves/docs/USAGE.md | 21 ++- packages/sheaves/src/index.ts | 1 + packages/sheaves/src/sheafify.e2e.test.ts | 79 ++++++----- packages/sheaves/src/sheafify.test.ts | 157 ++++++++++++---------- packages/sheaves/src/sheafify.ts | 21 +-- packages/sheaves/src/types.ts | 24 ---- 7 files changed, 146 insertions(+), 163 deletions(-) diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index d1935cfbd3..4df2ba569a 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -42,8 +42,10 @@ constraints }`. - `fallthrough(policyA, policyB)` — composes two policies so that `policyB` is tried only after `policyA` is exhausted. - `Sheaf` type — the authority manager returned by `sheafify`; exposes - `getSection`, `getDiscoverableSection`, `getGlobalSection`, and - `getDiscoverableGlobalSection`. + `getSection` and `getDiscoverableSection`. +- `collectSheafGuard(name, sections)` — compute the union interface guard of + a set of sections, for callers that assemble providers dynamically and need + to pass an explicit guard to `getSection`. - `docs/POLICY.md` — documents the policy coroutine protocol, `PolicyContext`, and the semantic equivalence assumption. - `docs/USAGE.md` — annotated usage examples. diff --git a/packages/sheaves/docs/USAGE.md b/packages/sheaves/docs/USAGE.md index 8eab7f429c..de3e6203b2 100644 --- a/packages/sheaves/docs/USAGE.md +++ b/packages/sheaves/docs/USAGE.md @@ -104,13 +104,20 @@ const section = sheaf.getDiscoverableSection({ `getSection` is the non-discoverable variant (no `schema` required). -`getGlobalSection` and `getDiscoverableGlobalSection` derive the guard -automatically from the union of all providers. They are `@deprecated` as a -nudge toward explicit guards once the caller knows the provider set — explicit -guards make the capability's scope visible at the call site. When providers are -assembled dynamically (e.g., rebuilt at runtime from a set of grants that -changes) and the union guard isn't known until after `sheafify` runs, the -global variants are the right choice. +The guard is always explicit at the call site — it makes the capability's +scope visible to the reader. When providers are assembled dynamically and the +guard isn't known until after `sheafify` runs, compute the union with +`collectSheafGuard` and pass it in: + +```ts +import { collectSheafGuard } from '@metamask/sheaves'; + +const guard = collectSheafGuard( + 'Wallet', + providers.map(({ exo }) => exo), +); +const section = sheaf.getSection({ guard, lift }); +``` ## Remote providers diff --git a/packages/sheaves/src/index.ts b/packages/sheaves/src/index.ts index 735f045faf..fe188809ad 100644 --- a/packages/sheaves/src/index.ts +++ b/packages/sheaves/src/index.ts @@ -7,6 +7,7 @@ export type { PolicyContext, Sheaf, } from './types.ts'; +export { collectSheafGuard } from './guard.ts'; export { constant, callable } from './metadata.ts'; export { sheafify } from './sheafify.ts'; export { diff --git a/packages/sheaves/src/sheafify.e2e.test.ts b/packages/sheaves/src/sheafify.e2e.test.ts index febd4eb854..8efa53ea2e 100644 --- a/packages/sheaves/src/sheafify.e2e.test.ts +++ b/packages/sheaves/src/sheafify.e2e.test.ts @@ -1,6 +1,7 @@ import { M } from '@endo/patterns'; import { describe, expect, it, vi } from 'vitest'; +import { collectSheafGuard } from './guard.ts'; import { callable, constant } from './metadata.ts'; import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; @@ -12,6 +13,22 @@ import type { Policy, Provider } from './types.ts'; const E = (obj: unknown) => obj as Record Promise>; +// Sheafify and build a section over the union guard of all providers. Used by +// tests that exercise dispatch behavior without caring which guard variant is +// presented at the call site. +const buildUnionSection = >( + name: string, + providers: Provider[], + lift: Policy, +): object => + sheafify({ name, providers }).getSection({ + guard: collectSheafGuard( + name, + providers.map(({ exo }) => exo), + ), + lift, + }); + // --------------------------------------------------------------------------- // E2E: cost-optimal routing // --------------------------------------------------------------------------- @@ -53,9 +70,7 @@ describe('e2e: cost-optimal routing', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + let wallet = buildUnionSection('Wallet', providers, argmin); // alice: both handlers match, argmin picks local (cost=1) await E(wallet).getBalance('alice'); @@ -81,9 +96,7 @@ describe('e2e: cost-optimal routing', () => { ), metadata: constant({ cost: 2 }), }); - wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + wallet = buildUnionSection('Wallet', providers, argmin); // bob: now remote (cost=100) and new local (cost=2) both match, argmin picks cost=2 await E(wallet).getBalance('bob'); @@ -164,9 +177,7 @@ describe('e2e: multi-tier capability routing', () => { metadata: constant({ latencyMs: 500, label: 'network' }), }); - let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: fastest, - }); + let wallet = buildUnionSection('Wallet', providers, fastest); // Phase 1 — single backend: always one candidate, lift never fires. await E(wallet).getBalance('alice'); @@ -190,9 +201,7 @@ describe('e2e: multi-tier capability routing', () => { ), metadata: constant({ latencyMs: 1, label: 'local' }), }); - wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: fastest, - }); + wallet = buildUnionSection('Wallet', providers, fastest); // Phase 2 — alice routes to local (1ms < 500ms), bob still hits network. await E(wallet).getBalance('alice'); @@ -218,9 +227,7 @@ describe('e2e: multi-tier capability routing', () => { ), metadata: constant({ latencyMs: 0, label: 'cache' }), }); - wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: fastest, - }); + wallet = buildUnionSection('Wallet', providers, fastest); // Phase 3 — every known account hits its optimal tier. await E(wallet).getBalance('alice'); // local (1ms) @@ -258,9 +265,7 @@ describe('e2e: multi-tier capability routing', () => { ), metadata: constant({ latencyMs: 200, label: 'write-backend' }), }); - wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: fastest, - }); + wallet = buildUnionSection('Wallet', providers, fastest); // transfer: only write-backend declares it → one candidate, lift bypassed. const facade = wallet as unknown as Record< @@ -314,10 +319,7 @@ describe('e2e: multi-tier capability routing', () => { ]; // Policy A: fastest wins (mirror at 50ms < network at 500ms). - const walletA = sheafify({ - name: 'Wallet', - providers: makeProviders(), - }).getGlobalSection({ lift: fastest }); + const walletA = buildUnionSection('Wallet', makeProviders(), fastest); await E(walletA).getBalance('alice'); expect(mirrorGetBalance).toHaveBeenCalledWith('alice'); expect(networkGetBalance).not.toHaveBeenCalled(); @@ -329,10 +331,7 @@ describe('e2e: multi-tier capability routing', () => { (a, b) => (b.metadata?.latencyMs ?? 0) - (a.metadata?.latencyMs ?? 0), ); }; - const walletB = sheafify({ - name: 'Wallet', - providers: makeProviders(), - }).getGlobalSection({ lift: slowest }); + const walletB = buildUnionSection('Wallet', makeProviders(), slowest); await E(walletB).getBalance('alice'); expect(networkGetBalance).toHaveBeenCalledWith('alice'); expect(mirrorGetBalance).not.toHaveBeenCalled(); @@ -378,9 +377,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }, ]; - const wallet = sheafify({ name: 'PushPull', providers }).getGlobalSection({ - lift: preferPush, - }); + const wallet = buildUnionSection('PushPull', providers, preferPush); // alice: both match, preferPush picks push section await E(wallet).getBalance('alice'); @@ -453,9 +450,11 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { }, ]; - const facade = sheafify({ name: 'Swap', providers }).getGlobalSection({ - lift: cheapest, - }) as unknown as Record Promise>; + const facade = buildUnionSection( + 'Swap', + providers, + cheapest, + ) as unknown as Record Promise>; // swap(50): A costs 6, B costs 10.05 → A wins await facade.swap(50, 'FUZ', 'BIZ'); @@ -518,9 +517,7 @@ describe('e2e: lift retry on handler failure', () => { } }; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: priorityFirst, - }); + const wallet = buildUnionSection('Wallet', providers, priorityFirst); const result = await E(wallet).getBalance('alice'); @@ -569,9 +566,7 @@ describe('e2e: lift retry on handler failure', () => { } }; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: priorityFirst, - }); + const wallet = buildUnionSection('Wallet', providers, priorityFirst); await E(wallet).getBalance('alice'); @@ -615,13 +610,15 @@ describe('e2e: lift retry on handler failure', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates) { + const wallet = buildUnionSection<{ priority: number }>( + 'Wallet', + providers, + async function* (candidates) { yield* [...candidates].sort( (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), ); }, - }); + ); await expect(E(wallet).getBalance('alice')).rejects.toThrow( 'No viable section', diff --git a/packages/sheaves/src/sheafify.test.ts b/packages/sheaves/src/sheafify.test.ts index c7aae61e13..b32ca00079 100644 --- a/packages/sheaves/src/sheafify.test.ts +++ b/packages/sheaves/src/sheafify.test.ts @@ -1,8 +1,10 @@ import { GET_INTERFACE_GUARD } from '@endo/exo'; import { M, getInterfaceGuardPayload } from '@endo/patterns'; import { GET_DESCRIPTION } from '@metamask/kernel-utils'; +import type { MethodSchema } from '@metamask/kernel-utils'; import { describe, it, expect } from 'vitest'; +import { collectSheafGuard } from './guard.ts'; import { constant } from './metadata.ts'; import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; @@ -14,6 +16,25 @@ import type { Candidate, Policy, PolicyContext, Provider } from './types.ts'; const E = (obj: unknown) => obj as Record Promise>; +// Sheafify and build a section over the union guard of all providers. Used by +// tests that exercise dispatch behavior without caring which guard variant is +// presented at the call site. +const buildUnionSection = >( + name: string, + providers: Provider[], + lift: Policy, + schema?: Record, +): object => { + const sheaf = sheafify({ name, providers }); + const guard = collectSheafGuard( + name, + providers.map(({ exo }) => exo), + ); + return schema === undefined + ? sheaf.getSection({ guard, lift }) + : sheaf.getDiscoverableSection({ guard, lift, schema }); +}; + // --------------------------------------------------------------------------- // Unit: sheafify // --------------------------------------------------------------------------- @@ -40,9 +61,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift, - }); + const wallet = buildUnionSection('Wallet', providers, lift); expect(await E(wallet).getBalance('alice')).toBe(42); expect(liftCalled).toBe(false); }); @@ -61,11 +80,14 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(_candidates) { + const wallet = buildUnionSection<{ cost: number }>( + 'Wallet', + providers, + + async function* (_candidates) { // unreachable — zero-coverage path throws before reaching lift }, - }); + ); await expect(E(wallet).getBalance('bob')).rejects.toThrow( 'No section covers', ); @@ -102,9 +124,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + const wallet = buildUnionSection('Wallet', providers, argmin); // argmin picks cost=1 section which returns 42 expect(await E(wallet).getBalance('alice')).toBe(42); }); @@ -134,11 +154,13 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates) { + const wallet = buildUnionSection<{ cost: number }>( + 'Wallet', + providers, + async function* (candidates) { yield candidates[0]!; }, - }); + ); const guard = wallet[GET_INTERFACE_GUARD](); expect(guard).toBeDefined(); @@ -167,9 +189,7 @@ describe('sheafify', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + let wallet = buildUnionSection('Wallet', providers, argmin); expect(await E(wallet).getBalance('alice')).toBe(100); // Add a cheaper provider with a new method to the providers array, re-sheafify. @@ -189,9 +209,7 @@ describe('sheafify', () => { ), metadata: constant({ cost: 1 }), }); - wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + wallet = buildUnionSection('Wallet', providers, argmin); // argmin picks the cheaper section expect(await E(wallet).getBalance('alice')).toBe(42); @@ -215,11 +233,13 @@ describe('sheafify', () => { { exo, metadata: constant({ cost: 1 }) }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates) { + const wallet = buildUnionSection<{ cost: number }>( + 'Wallet', + providers, + async function* (candidates) { yield candidates[0]!; }, - }); + ); expect(await E(wallet).getBalance('alice')).toBe(42); }); @@ -244,9 +264,7 @@ describe('sheafify', () => { }, ]; - let wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + let wallet = buildUnionSection('Wallet', providers, argmin); expect(await E(wallet).getBalance('alice')).toBe(100); // Add a pre-built exo with a cheaper getBalance + new transfer method @@ -267,9 +285,7 @@ describe('sheafify', () => { exo, metadata: constant({ cost: 1 }), }); - wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + wallet = buildUnionSection('Wallet', providers, argmin); // argmin picks the cheaper section expect(await E(wallet).getBalance('alice')).toBe(42); @@ -293,11 +309,13 @@ describe('sheafify', () => { { exo, metadata: constant({ cost: 1 }) }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates) { + const wallet = buildUnionSection<{ cost: number }>( + 'Wallet', + providers, + async function* (candidates) { yield candidates[0]!; }, - }); + ); const guard = wallet[GET_INTERFACE_GUARD](); expect(guard).toBeDefined(); @@ -339,9 +357,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: spy, - }); + const wallet = buildUnionSection('Wallet', providers, spy); await E(wallet).getBalance('alice'); expect(capturedContext).toStrictEqual({ @@ -388,9 +404,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: spy, - }); + const wallet = buildUnionSection('Wallet', providers, spy); await E(wallet).getBalance('alice'); // Both providers collapsed to one candidate → policy not invoked @@ -425,12 +439,14 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + const wallet = buildUnionSection( + 'Wallet', + providers, // eslint-disable-next-line require-yield - async *lift(_candidates) { + async function* (_candidates) { liftCalled = true; }, - }); + ); await E(wallet).getBalance('alice'); // Both providers have identical metadata → collapsed to one candidate → policy bypassed @@ -465,13 +481,15 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates, context) { + const wallet = buildUnionSection( + 'Wallet', + providers, + async function* (candidates, context) { capturedCandidates = candidates; capturedContext = context; yield candidates[0]!; }, - }); + ); await E(wallet).getBalance('alice'); @@ -510,12 +528,14 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates) { + const wallet = buildUnionSection( + 'Wallet', + providers, + async function* (candidates) { candidateCount = candidates.length; yield candidates[0]!; }, - }); + ); await E(wallet).getBalance('alice'); expect(candidateCount).toBe(2); @@ -548,12 +568,14 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates) { + const wallet = buildUnionSection( + 'Wallet', + providers, + async function* (candidates) { candidateCount = candidates.length; yield candidates[0]!; }, - }); + ); await E(wallet).getBalance('alice'); expect(candidateCount).toBe(2); @@ -585,12 +607,14 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ + const wallet = buildUnionSection( + 'Wallet', + providers, // eslint-disable-next-line require-yield - async *lift(_candidates) { + async function* (_candidates) { liftCalled = true; }, - }); + ); await E(wallet).getBalance('alice'); expect(liftCalled).toBe(false); @@ -625,14 +649,12 @@ describe('sheafify', () => { { exo, metadata: constant({ cost: 1 }) }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: argmin, - }); + const wallet = buildUnionSection('Wallet', providers, argmin); // argmin picks the exo section (cost=1) expect(await E(wallet).getBalance('alice')).toBe(42); }); - it('getDiscoverableGlobalSection exposes __getDescription__', async () => { + it('getDiscoverableSection exposes __getDescription__ over union guard', async () => { const schema = { getBalance: { description: 'Get account balance.', @@ -652,15 +674,14 @@ describe('sheafify', () => { }, ]; - const section = sheafify({ - name: 'Wallet', + const section = buildUnionSection>( + 'Wallet', providers, - }).getDiscoverableGlobalSection({ - async *lift(candidates) { + async function* (candidates) { yield candidates[0]!; }, schema, - }); + ); expect(E(section)[GET_DESCRIPTION]()).toStrictEqual(schema); }); @@ -678,11 +699,13 @@ describe('sheafify', () => { }, ]; - const section = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - async *lift(candidates) { + const section = buildUnionSection>( + 'Wallet', + providers, + async function* (candidates) { yield candidates[0]!; }, - }); + ); expect( (section as Record)[GET_DESCRIPTION], @@ -726,9 +749,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: spy, - }); + const wallet = buildUnionSection('Wallet', providers, spy); await E(wallet).getBalance('alice'); expect(capturedContext).toStrictEqual({ @@ -780,9 +801,7 @@ describe('sheafify', () => { }, ]; - const wallet = sheafify({ name: 'Wallet', providers }).getGlobalSection({ - lift: spy, - }); + const wallet = buildUnionSection('Wallet', providers, spy); await E(wallet).getBalance('alice'); // 'constructor' is only owned by provider A — must not appear in constraints diff --git a/packages/sheaves/src/sheafify.ts b/packages/sheaves/src/sheafify.ts index dd6d2d4044..ecefc446e1 100644 --- a/packages/sheaves/src/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -19,7 +19,7 @@ import type { MethodSchema } from '@metamask/kernel-utils'; import { makeDiscoverableExo } from '@metamask/kernel-utils'; import { stringify } from '@metamask/kernel-utils'; -import { asyncifyMethodGuards, collectSheafGuard } from './guard.ts'; +import { asyncifyMethodGuards } from './guard.ts'; import { getMatchingProviders } from './match.ts'; import { evaluateMetadata } from './metadata.ts'; import type { @@ -297,12 +297,6 @@ export const sheafify = < return exo; }; - const unionGuard = (): InterfaceGuard => - collectSheafGuard( - name, - frozenProviders.map(({ exo }) => exo), - ); - const getSection = ({ guard, lift, @@ -321,21 +315,8 @@ export const sheafify = < schema: Record; }): object => buildSection({ guard, lift, schema }); - const getGlobalSection = ({ lift }: { lift: Policy }): object => - buildSection({ guard: unionGuard(), lift }); - - const getDiscoverableGlobalSection = ({ - lift, - schema, - }: { - lift: Policy; - schema: Record; - }): object => buildSection({ guard: unionGuard(), lift, schema }); - return harden({ getSection, getDiscoverableSection, - getGlobalSection, - getDiscoverableGlobalSection, }); }; diff --git a/packages/sheaves/src/types.ts b/packages/sheaves/src/types.ts index 5c0b84be56..3cd092ce5f 100644 --- a/packages/sheaves/src/types.ts +++ b/packages/sheaves/src/types.ts @@ -111,28 +111,4 @@ export type Sheaf> = { lift: Policy; schema: Record; }) => object; - /** - * Produce a dispatch exo over the full union guard of all providers. - * - * Prefer `getSection` with an explicit guard when the guard is statically - * known — it makes the capability's scope visible at the call site. Use the - * global variant when providers are assembled dynamically at runtime and the - * union guard is not known until after `sheafify` runs. - * - * @deprecated Provide an explicit guard via getSection instead. - */ - getGlobalSection: (opts: { lift: Policy }) => object; - /** - * Produce a discoverable dispatch exo over the full union guard of all providers. - * - * Prefer `getDiscoverableSection` with an explicit guard when the guard is - * statically known. Use the global variant when providers are assembled - * dynamically and the union guard is not known until after `sheafify` runs. - * - * @deprecated Provide an explicit guard via getDiscoverableSection instead. - */ - getDiscoverableGlobalSection: (opts: { - lift: Policy; - schema: Record; - }) => object; }; From 7815efecff98d3ecd8e5afa3f6a8175909010d61 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:00:16 -0500 Subject: [PATCH 63/68] refactor(sheaves): rename 'lift' option key to 'policy' --- packages/sheaves/CHANGELOG.md | 7 ++++ packages/sheaves/README.md | 4 +-- packages/sheaves/docs/INTRODUCTION.md | 2 +- packages/sheaves/docs/USAGE.md | 8 ++--- packages/sheaves/src/sheafify.e2e.test.ts | 38 ++++++++++---------- packages/sheaves/src/sheafify.test.ts | 44 +++++++++++------------ packages/sheaves/src/sheafify.ts | 18 +++++----- packages/sheaves/src/types.ts | 4 +-- 8 files changed, 66 insertions(+), 59 deletions(-) diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index 4df2ba569a..47808332fa 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Renamed the `lift` option key on `Sheaf.getSection` and + `Sheaf.getDiscoverableSection` to `policy`, completing the earlier + `Lift` → `Policy` type rename. Callers must update their call sites: + `sheaf.getSection({ guard, lift })` → `sheaf.getSection({ guard, policy })`. + ## [0.1.0] ### Added diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index c51484422f..fa7bd4297b 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -66,8 +66,8 @@ produce dispatch sections on demand. const sheaf = sheafify({ name: 'Wallet', providers }); ``` -- `sheaf.getSection({ guard, lift })` — produce a dispatch section -- `sheaf.getDiscoverableSection({ guard, lift, schema })` — same, but the section exposes its guard +- `sheaf.getSection({ guard, policy })` — produce a dispatch section +- `sheaf.getDiscoverableSection({ guard, policy, schema })` — same, but the section exposes its guard ## Dispatch pipeline diff --git a/packages/sheaves/docs/INTRODUCTION.md b/packages/sheaves/docs/INTRODUCTION.md index fb40a11236..a91c6aa2ff 100644 --- a/packages/sheaves/docs/INTRODUCTION.md +++ b/packages/sheaves/docs/INTRODUCTION.md @@ -61,7 +61,7 @@ some open set of the combined surface, restricted by an explicit guard: ```ts const sheaf = sheafify({ name: 'Wallet', providers }); -const userFacing = sheaf.getSection({ guard: userGuard, lift: policy }); +const userFacing = sheaf.getSection({ guard: userGuard, policy }); ``` `getSection` is itself attenuation: it takes the full combined surface that diff --git a/packages/sheaves/docs/USAGE.md b/packages/sheaves/docs/USAGE.md index de3e6203b2..00688dd06f 100644 --- a/packages/sheaves/docs/USAGE.md +++ b/packages/sheaves/docs/USAGE.md @@ -25,7 +25,7 @@ const sheaf = sheafify({ providers: [{ exo: priceSection }], }); -const section = sheaf.getSection({ guard: priceGuard, lift: noopPolicy }); +const section = sheaf.getSection({ guard: priceGuard, policy: noopPolicy }); // section is a dispatch section; call it like any capability const price = await E(section).getPrice('ETH'); ``` @@ -60,7 +60,7 @@ const sheaf = sheafify({ }); // guard restricts which methods callers may invoke -const section = sheaf.getSection({ guard: clientGuard, lift: preferFast }); +const section = sheaf.getSection({ guard: clientGuard, policy: preferFast }); ``` The sheaf drives the generator: it primes it with `gen.next([])`, calls the @@ -97,7 +97,7 @@ const schema: Record = { const section = sheaf.getDiscoverableSection({ guard: clientGuard, - lift, + policy, schema, }); ``` @@ -116,7 +116,7 @@ const guard = collectSheafGuard( 'Wallet', providers.map(({ exo }) => exo), ); -const section = sheaf.getSection({ guard, lift }); +const section = sheaf.getSection({ guard, policy }); ``` ## Remote providers diff --git a/packages/sheaves/src/sheafify.e2e.test.ts b/packages/sheaves/src/sheafify.e2e.test.ts index 8efa53ea2e..1546bcc56f 100644 --- a/packages/sheaves/src/sheafify.e2e.test.ts +++ b/packages/sheaves/src/sheafify.e2e.test.ts @@ -19,14 +19,14 @@ const E = (obj: unknown) => const buildUnionSection = >( name: string, providers: Provider[], - lift: Policy, + policy: Policy, ): object => sheafify({ name, providers }).getSection({ guard: collectSheafGuard( name, providers.map(({ exo }) => exo), ), - lift, + policy, }); // --------------------------------------------------------------------------- @@ -78,7 +78,7 @@ describe('e2e: cost-optimal routing', () => { expect(remote0GetBalance).not.toHaveBeenCalled(); local1GetBalance.mockClear(); - // bob: only remote matches (one candidate, lift not invoked) + // bob: only remote matches (one candidate, policy not invoked) await E(wallet).getBalance('bob'); expect(remote0GetBalance).toHaveBeenCalledWith('bob'); expect(local1GetBalance).not.toHaveBeenCalled(); @@ -121,7 +121,7 @@ describe('e2e: multi-tier capability routing', () => { // via guards and carries latency metadata. The sheaf routes every call // to the fastest matching source — no manual if/else, no strategy // registration, just: - // guards (what can handle it) + metadata (how fast) + lift (pick best) + // guards (what can handle it) + metadata (how fast) + policy (pick best) type Tier = { latencyMs: number; label: string }; @@ -179,7 +179,7 @@ describe('e2e: multi-tier capability routing', () => { let wallet = buildUnionSection('Wallet', providers, fastest); - // Phase 1 — single backend: always one candidate, lift never fires. + // Phase 1 — single backend: always one candidate, policy never fires. await E(wallet).getBalance('alice'); await E(wallet).getBalance('bob'); await E(wallet).getBalance('dave'); @@ -267,7 +267,7 @@ describe('e2e: multi-tier capability routing', () => { }); wallet = buildUnionSection('Wallet', providers, fastest); - // transfer: only write-backend declares it → one candidate, lift bypassed. + // transfer: only write-backend declares it → one candidate, policy bypassed. const facade = wallet as unknown as Record< string, (...args: unknown[]) => unknown @@ -290,7 +290,7 @@ describe('e2e: multi-tier capability routing', () => { }); it('same candidate structure, different policies, different routing', async () => { - // The lift is the operational policy — swap it and the same + // The policy is the operational policy — swap it and the same // set of providers produces different routing behavior. const networkGetBalance = vi.fn((_acct: string): number => 0); const mirrorGetBalance = vi.fn((_acct: string): number => 0); @@ -343,7 +343,7 @@ describe('e2e: multi-tier capability routing', () => { // --------------------------------------------------------------------------- describe('e2e: preferAutonomous recovered as degenerate case', () => { - it('binary push metadata recovers push-pull lift rule', async () => { + it('binary push metadata recovers push-pull policy rule', async () => { const preferPush: Policy<{ push: boolean }> = async function* (candidates) { yield* candidates.filter((candidate) => candidate.metadata?.push); yield* candidates.filter((candidate) => !candidate.metadata?.push); @@ -385,7 +385,7 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { expect(pullGetBalance).not.toHaveBeenCalled(); pushGetBalance.mockClear(); - // bob: only pull matches (one candidate, lift bypassed) + // bob: only pull matches (one candidate, policy bypassed) await E(wallet).getBalance('bob'); expect(pullGetBalance).toHaveBeenCalledWith('bob'); expect(pushGetBalance).not.toHaveBeenCalled(); @@ -470,11 +470,11 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { }); // --------------------------------------------------------------------------- -// E2E: lift retry — first candidate throws, sheaf recovers to fallback +// E2E: policy retry — first candidate throws, sheaf recovers to fallback // --------------------------------------------------------------------------- -describe('e2e: lift retry on handler failure', () => { - it('recovers to next candidate when first throws, lift receives non-empty errors', async () => { +describe('e2e: policy retry on handler failure', () => { + it('recovers to next candidate when first throws, policy receives non-empty errors', async () => { type RouteMeta = { priority: number }; const primaryFn = vi.fn((_acct: string): number => { @@ -505,15 +505,15 @@ describe('e2e: lift retry on handler failure', () => { }, ]; - // Track the error-array length the lift receives after each failed attempt. - const errorCountsSeenByLift: number[] = []; + // Track the error-array length the policy receives after each failed attempt. + const errorCountsSeenByPolicy: number[] = []; const priorityFirst: Policy = async function* (candidates) { const ordered = [...candidates].sort( (a, b) => (a.metadata?.priority ?? 0) - (b.metadata?.priority ?? 0), ); for (const candidate of ordered) { const errors: unknown[] = yield candidate; - errorCountsSeenByLift.push(errors.length); + errorCountsSeenByPolicy.push(errors.length); } }; @@ -526,12 +526,12 @@ describe('e2e: lift retry on handler failure', () => { expect(primaryFn).toHaveBeenCalledWith('alice'); expect(fallbackFn).toHaveBeenCalledWith('alice'); - // after the primary failed the lift received an errors array with one entry - expect(errorCountsSeenByLift).toHaveLength(1); - expect(errorCountsSeenByLift[0]).toBe(1); + // after the primary failed the policy received an errors array with one entry + expect(errorCountsSeenByPolicy).toHaveLength(1); + expect(errorCountsSeenByPolicy[0]).toBe(1); }); - it('lift receives error snapshots not live references', async () => { + it('policy receives error snapshots not live references', async () => { type RouteMeta = { priority: number }; const handlers = [ diff --git a/packages/sheaves/src/sheafify.test.ts b/packages/sheaves/src/sheafify.test.ts index b32ca00079..fab2da6c2e 100644 --- a/packages/sheaves/src/sheafify.test.ts +++ b/packages/sheaves/src/sheafify.test.ts @@ -22,7 +22,7 @@ const E = (obj: unknown) => const buildUnionSection = >( name: string, providers: Provider[], - lift: Policy, + policy: Policy, schema?: Record, ): object => { const sheaf = sheafify({ name, providers }); @@ -31,8 +31,8 @@ const buildUnionSection = >( providers.map(({ exo }) => exo), ); return schema === undefined - ? sheaf.getSection({ guard, lift }) - : sheaf.getDiscoverableSection({ guard, lift, schema }); + ? sheaf.getSection({ guard, policy }) + : sheaf.getDiscoverableSection({ guard, policy, schema }); }; // --------------------------------------------------------------------------- @@ -40,12 +40,12 @@ const buildUnionSection = >( // --------------------------------------------------------------------------- describe('sheafify', () => { - it('single-section bypass: lift not invoked', async () => { - let liftCalled = false; + it('single-section bypass: policy not invoked', async () => { + let policyCalled = false; // eslint-disable-next-line require-yield - const lift: Policy<{ cost: number }> = async function* (_candidates) { - liftCalled = true; - // unreachable — fast path bypasses lift for single section + const policy: Policy<{ cost: number }> = async function* (_candidates) { + policyCalled = true; + // unreachable — fast path bypasses policy for single section }; const providers: Provider<{ cost: number }>[] = [ @@ -61,9 +61,9 @@ describe('sheafify', () => { }, ]; - const wallet = buildUnionSection('Wallet', providers, lift); + const wallet = buildUnionSection('Wallet', providers, policy); expect(await E(wallet).getBalance('alice')).toBe(42); - expect(liftCalled).toBe(false); + expect(policyCalled).toBe(false); }); it('zero-coverage throws', async () => { @@ -85,7 +85,7 @@ describe('sheafify', () => { providers, async function* (_candidates) { - // unreachable — zero-coverage path throws before reaching lift + // unreachable — zero-coverage path throws before reaching policy }, ); await expect(E(wallet).getBalance('bob')).rejects.toThrow( @@ -93,7 +93,7 @@ describe('sheafify', () => { ); }); - it('lift receives metadata and picks winner', async () => { + it('policy receives metadata and picks winner', async () => { const argmin: Policy<{ cost: number }> = async function* (candidates) { yield* [...candidates].sort( (a, b) => @@ -323,7 +323,7 @@ describe('sheafify', () => { expect(methodGuards).toHaveProperty('getBalance'); }); - it('lift receives constraints in context and only distinguishing metadata', async () => { + it('policy receives constraints in context and only distinguishing metadata', async () => { type Meta = { region: string; cost: number }; let capturedCandidates: Candidate>[] = []; let capturedContext: PolicyContext | undefined; @@ -414,7 +414,7 @@ describe('sheafify', () => { it('collapses equivalent providers by metadata', async () => { type Meta = { cost: number }; - let liftCalled = false; + let policyCalled = false; const providers: Provider[] = [ { @@ -444,13 +444,13 @@ describe('sheafify', () => { providers, // eslint-disable-next-line require-yield async function* (_candidates) { - liftCalled = true; + policyCalled = true; }, ); await E(wallet).getBalance('alice'); // Both providers have identical metadata → collapsed to one candidate → policy bypassed - expect(liftCalled).toBe(false); + expect(policyCalled).toBe(false); }); it('extracts shared NaN metadata values into constraints', async () => { @@ -583,7 +583,7 @@ describe('sheafify', () => { it('collapses no-metadata and empty-object metadata as equivalent', async () => { type Meta = Record; - let liftCalled = false; + let policyCalled = false; const providers: Provider[] = [ { @@ -612,12 +612,12 @@ describe('sheafify', () => { providers, // eslint-disable-next-line require-yield async function* (_candidates) { - liftCalled = true; + policyCalled = true; }, ); await E(wallet).getBalance('alice'); - expect(liftCalled).toBe(false); + expect(policyCalled).toBe(false); }); it('mixed providers participate in policy', async () => { @@ -837,7 +837,7 @@ describe('getSection with explicit guard', () => { const section = sheafify({ name: 'Wallet', providers }).getSection({ guard: readGuard, - async *lift(candidates) { + async *policy(candidates) { yield candidates[0]!; }, }); @@ -868,7 +868,7 @@ describe('getSection with explicit guard', () => { const section = sheafify({ name: 'Wallet', providers }).getSection({ guard: readGuard, - async *lift(candidates) { + async *policy(candidates) { yield candidates[0]!; }, }); @@ -905,7 +905,7 @@ describe('getSection with explicit guard', () => { providers, }).getDiscoverableSection({ guard: readGuard, - async *lift(candidates) { + async *policy(candidates) { yield candidates[0]!; }, schema, diff --git a/packages/sheaves/src/sheafify.ts b/packages/sheaves/src/sheafify.ts index ecefc446e1..e8b283c054 100644 --- a/packages/sheaves/src/sheafify.ts +++ b/packages/sheaves/src/sheafify.ts @@ -207,11 +207,11 @@ export const sheafify = < ); const buildSection = ({ guard, - lift, + policy, schema, }: { guard: InterfaceGuard; - lift: Policy; + policy: Policy; schema?: Record; }): object => { const asyncMethodGuards = asyncifyMethodGuards(guard); @@ -259,7 +259,7 @@ export const sheafify = < ]), ); return drivePolicy( - lift, + policy, stripped, { method, args, constraints }, async (candidate) => { @@ -299,21 +299,21 @@ export const sheafify = < const getSection = ({ guard, - lift, + policy, }: { guard: InterfaceGuard; - lift: Policy; - }): object => buildSection({ guard, lift }); + policy: Policy; + }): object => buildSection({ guard, policy }); const getDiscoverableSection = ({ guard, - lift, + policy, schema, }: { guard: InterfaceGuard; - lift: Policy; + policy: Policy; schema: Record; - }): object => buildSection({ guard, lift, schema }); + }): object => buildSection({ guard, policy, schema }); return harden({ getSection, diff --git a/packages/sheaves/src/types.ts b/packages/sheaves/src/types.ts index 3cd092ce5f..be1a56cb2f 100644 --- a/packages/sheaves/src/types.ts +++ b/packages/sheaves/src/types.ts @@ -99,7 +99,7 @@ export type Sheaf> = { */ getSection: (opts: { guard: InterfaceGuard; - lift: Policy; + policy: Policy; }) => object; /** * Produce a discoverable dispatch exo over the given guard. @@ -108,7 +108,7 @@ export type Sheaf> = { */ getDiscoverableSection: (opts: { guard: InterfaceGuard; - lift: Policy; + policy: Policy; schema: Record; }) => object; }; From cd43c592ed025f00b9d40f39183bd1bee5771ff4 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:24:20 -0500 Subject: [PATCH 64/68] docs(sheaves): align README design choices with implementation --- packages/sheaves/README.md | 55 +++++++++++++++----------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/packages/sheaves/README.md b/packages/sheaves/README.md index fa7bd4297b..6ffa2dfade 100644 --- a/packages/sheaves/README.md +++ b/packages/sheaves/README.md @@ -78,15 +78,15 @@ getMatchingProviders(providers, method, args) presheaf → matches (filter by g evaluateMetadata(matches, args) metadata specs → concrete values collapseEquivalent(candidates) locality condition (quotient by metadata) decomposeMetadata(collapsed) restriction map (constraints / options) -policy(candidates, { method, args, operational selection (extra-theoretic) +policy(candidates, { method, args, operational selection constraints }) dispatch to chosen.exo evaluation ``` The pipeline short-circuits at two points: if only one provider matches the -guard, it is invoked directly without evaluate/collapse/policy; if all matching -providers collapse to an identical candidate, the single representative is invoked -without calling the policy. +guard, it is invoked directly; if multiple providers match but all collapse to +an identical candidate, the single representative is invoked without calling +the policy. `callable` metadata specs make the candidate set depend on the invocation arguments. A `swap(amount)` provider can produce `{ cost: 'low' }` for small @@ -96,33 +96,20 @@ called with different arguments. ## Design choices -**Candidate identity is metadata identity.** The collapse step quotients by -metadata: if two providers should be distinguishable, the caller must give them -distinguishable metadata. Providers with identical metadata are treated as -interchangeable. Under the sheaf condition (effect-equivalence), this recovers -the classical equivalence relation on germs. - -**Pseudosheafification.** The sheafification functor would precompute the full -etale space. This system defers to invocation time: match by guard, evaluate -metadata, collapse, decompose, select via policy. The trade-off is that global -coherence (a policy choosing consistently across points) is not guaranteed. - -**Restriction and gluing are implicit.** Guard restriction induces a -restriction map on metadata: restricting to a point filters the presheaf to -covering providers (`getMatchingProviders`), then `decomposeMetadata` strips -the metadata to distinguishing keys — the restricted metadata over that point. The join -works dually: the union of two providers has the join of their metadata, and -restriction at any point recovers the local distinguishing keys in O(n). -Gluing follows: compatible providers (equal metadata on their overlap) produce a -well-defined join. The dispatch pipeline computes all of this implicitly. The -remaining gap is `revokeSite` (revoking over an open set rather than a point), -which requires an `intersects` operator on guards not yet available. - -## Relationship to stacks - -This construction is more properly a **stack** in algebraic geometry. We call -it a sheaf because engineers already know "stack" as a LIFO data structure, and -the algebraic geometry term is unrelated. Within a candidate, any representative -will do — authority-equivalence is asserted by constructor contract, not -verified at runtime. Between candidates, metadata distinguishes them and the -policy resolves the choice. +**Candidate identity is metadata identity.** Within a single equivalence class +(same metadata), the sheaf has no data to prefer one provider over another, so +it picks an arbitrary representative. Callers who need a distinction between +two providers must encode it in metadata. The semantic-equivalence contract +(see [POLICY.md](./docs/POLICY.md)) is the assertion that this is safe. + +**Lazy dispatch.** Match, evaluate, collapse, decompose, and policy selection +all run per invocation rather than being precomputed at `sheafify` time. This +keeps `callable` metadata cheap (only providers surviving the guard filter are +evaluated) and lets the candidate set vary with the arguments to a single +method. + +**Restriction is implicit in the pipeline.** Filtering by guard +(`getMatchingProviders`) and stripping shared metadata (`decomposeMetadata`) +together yield the local view the policy sees — the candidates and their +distinguishing keys over that point. There is no separate "restrict to a +subdomain" operation; restriction falls out of dispatch. From f98aa9ffc024c55e374d05a193a092db6b0eef5f Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:24:23 -0500 Subject: [PATCH 65/68] test(sheaves): rename sheafify.e2e to scenarios.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These tests exercise sheafify + guard + section + metadata together using the same mock-endoify setup as the unit tests — they're scenario coverage, not e2e in the monorepo's test/e2e/ sense. Rename and relabel describe blocks accordingly. Co-Authored-By: Claude Opus 4.7 --- ...sheafify.e2e.test.ts => scenarios.test.ts} | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename packages/sheaves/src/{sheafify.e2e.test.ts => scenarios.test.ts} (97%) diff --git a/packages/sheaves/src/sheafify.e2e.test.ts b/packages/sheaves/src/scenarios.test.ts similarity index 97% rename from packages/sheaves/src/sheafify.e2e.test.ts rename to packages/sheaves/src/scenarios.test.ts index 1546bcc56f..4d7c049c1f 100644 --- a/packages/sheaves/src/sheafify.e2e.test.ts +++ b/packages/sheaves/src/scenarios.test.ts @@ -30,10 +30,10 @@ const buildUnionSection = >( }); // --------------------------------------------------------------------------- -// E2E: cost-optimal routing +// Scenario: cost-optimal routing // --------------------------------------------------------------------------- -describe('e2e: cost-optimal routing', () => { +describe('scenario: cost-optimal routing', () => { it('argmin picks cheapest section, re-sheafification expands landscape', async () => { const argmin: Policy<{ cost: number }> = async function* (candidates) { yield* [...candidates].sort( @@ -113,10 +113,10 @@ describe('e2e: cost-optimal routing', () => { }); // --------------------------------------------------------------------------- -// E2E: multi-tier capability routing +// Scenario: multi-tier capability routing // --------------------------------------------------------------------------- -describe('e2e: multi-tier capability routing', () => { +describe('scenario: multi-tier capability routing', () => { // A wallet integrates multiple data sources. Each declares its coverage // via guards and carries latency metadata. The sheaf routes every call // to the fastest matching source — no manual if/else, no strategy @@ -339,10 +339,10 @@ describe('e2e: multi-tier capability routing', () => { }); // --------------------------------------------------------------------------- -// E2E: preferAutonomous recovered as degenerate case +// Scenario: preferAutonomous recovered as degenerate case // --------------------------------------------------------------------------- -describe('e2e: preferAutonomous recovered as degenerate case', () => { +describe('scenario: preferAutonomous recovered as degenerate case', () => { it('binary push metadata recovers push-pull policy rule', async () => { const preferPush: Policy<{ push: boolean }> = async function* (candidates) { yield* candidates.filter((candidate) => candidate.metadata?.push); @@ -393,10 +393,10 @@ describe('e2e: preferAutonomous recovered as degenerate case', () => { }); // --------------------------------------------------------------------------- -// E2E: callable metadata — cost varies with invocation args +// Scenario: callable metadata — cost varies with invocation args // --------------------------------------------------------------------------- -describe('e2e: callable metadata — cost varies with invocation args', () => { +describe('scenario: callable metadata — cost varies with invocation args', () => { // Two swap handlers whose cost is a function of the swap amount. // Swap A is cheaper for small amounts; Swap B is cheaper for large amounts. // Breakeven ≈ 90.9 (1 + 0.1x = 10 + 0.001x → 0.099x = 9 → x ≈ 90.9) @@ -470,10 +470,10 @@ describe('e2e: callable metadata — cost varies with invocation args', () => { }); // --------------------------------------------------------------------------- -// E2E: policy retry — first candidate throws, sheaf recovers to fallback +// Scenario: policy retry — first candidate throws, sheaf recovers to fallback // --------------------------------------------------------------------------- -describe('e2e: policy retry on handler failure', () => { +describe('scenario: policy retry on handler failure', () => { it('recovers to next candidate when first throws, policy receives non-empty errors', async () => { type RouteMeta = { priority: number }; From 90476236d9e101b12e47c558d8359f3c30634a1c Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:43:04 -0500 Subject: [PATCH 66/68] test(sheaves): raise coverage to 99% (stmts) / 98% (branches) Add index.test.ts (standard exports check) and targeted tests for previously-uncovered paths: noopPolicy, guardless-section/provider skip, encodeMetadataEntry's undefined/bigint/-Infinity arms, the no-handler and no-coverage throws in dispatch, and the unrecognized-candidate guard in drivePolicy. Co-Authored-By: Claude Opus 4.7 --- packages/sheaves/src/compose.test.ts | 22 +++ packages/sheaves/src/guard.test.ts | 18 +++ packages/sheaves/src/index.test.ts | 21 +++ packages/sheaves/src/match.test.ts | 17 ++ packages/sheaves/src/sheafify.test.ts | 220 +++++++++++++++++++++++++- 5 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 packages/sheaves/src/index.test.ts diff --git a/packages/sheaves/src/compose.test.ts b/packages/sheaves/src/compose.test.ts index 5f2dc3c4f6..d2dd2e3268 100644 --- a/packages/sheaves/src/compose.test.ts +++ b/packages/sheaves/src/compose.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest'; import { fallthrough, + noopPolicy, proxyPolicy, withFilter, withRanking, @@ -87,6 +88,27 @@ const driveWithSuccessOn = async ( throw new Error('generator exhausted before success'); }; +// --------------------------------------------------------------------------- +// noopPolicy +// --------------------------------------------------------------------------- + +describe('noopPolicy', () => { + it('yields candidates in original order', async () => { + const candidates = [ + makeCandidate('a'), + makeCandidate('b'), + makeCandidate('c'), + ]; + const { yielded } = await driveToExhaustion(noopPolicy, candidates); + expect(yielded).toStrictEqual(candidates); + }); + + it('yields nothing when given no candidates', async () => { + const { yielded } = await driveToExhaustion(noopPolicy, []); + expect(yielded).toHaveLength(0); + }); +}); + // --------------------------------------------------------------------------- // proxyPolicy // --------------------------------------------------------------------------- diff --git a/packages/sheaves/src/guard.test.ts b/packages/sheaves/src/guard.test.ts index f881a0eaa4..0840f35f17 100644 --- a/packages/sheaves/src/guard.test.ts +++ b/packages/sheaves/src/guard.test.ts @@ -8,6 +8,7 @@ import { } from './guard.ts'; import { guardCoversPoint } from './match.ts'; import { makeSection } from './section.ts'; +import type { Section } from './types.ts'; describe('collectSheafGuard', () => { it('variable arity: add with 1, 2, and 3 args', () => { @@ -156,6 +157,23 @@ describe('collectSheafGuard', () => { expect(guardCoversPoint(guard, 'f', [])).toBe(true); // covered by B (0 required) }); + it('skips sections without an interface guard', () => { + const guardedSection = makeSection( + 'Guarded', + M.interface('Guarded', { f: M.call(M.string()).returns(M.any()) }), + { f: (_: string) => undefined }, + ); + const guardlessSection: Section = { f: (_: string) => undefined }; + + const guard = collectSheafGuard('Mixed', [ + guardlessSection, + guardedSection, + ]); + const methodGuards = getInterfaceMethodGuards(guard); + + expect('f' in methodGuards).toBe(true); + }); + it('multi-method guard collection', () => { const sections = [ makeSection( diff --git a/packages/sheaves/src/index.test.ts b/packages/sheaves/src/index.test.ts new file mode 100644 index 0000000000..2e11d7fad4 --- /dev/null +++ b/packages/sheaves/src/index.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; + +import * as indexModule from './index.ts'; + +describe('index', () => { + it('has the expected exports', () => { + expect(Object.keys(indexModule).sort()).toStrictEqual([ + 'callable', + 'collectSheafGuard', + 'constant', + 'fallthrough', + 'makeRemoteSection', + 'makeSection', + 'noopPolicy', + 'proxyPolicy', + 'sheafify', + 'withFilter', + 'withRanking', + ]); + }); +}); diff --git a/packages/sheaves/src/match.test.ts b/packages/sheaves/src/match.test.ts index 9dcf412158..7c8a5a219a 100644 --- a/packages/sheaves/src/match.test.ts +++ b/packages/sheaves/src/match.test.ts @@ -146,6 +146,23 @@ describe('getMatchingProviders', () => { expect(getMatchingProviders(providers, 'log', [42])).toHaveLength(0); }); + it('filters out providers without an interface guard', () => { + const guarded = makeProvider( + 'A', + { add: M.call(M.number()).returns(M.number()) }, + { add: (a: number) => a }, + { cost: 1 }, + ); + const guardless: Provider<{ cost: number }> = { + exo: { add: (_a: number) => 0 }, + metadata: constant({ cost: 2 }), + }; + + const candidates = getMatchingProviders([guardless, guarded], 'add', [1]); + expect(candidates).toHaveLength(1); + expect(candidates[0]).toBe(guarded); + }); + it('returns all providers when all match', () => { const providers = [ makeProvider( diff --git a/packages/sheaves/src/sheafify.test.ts b/packages/sheaves/src/sheafify.test.ts index fab2da6c2e..7b13df3fd3 100644 --- a/packages/sheaves/src/sheafify.test.ts +++ b/packages/sheaves/src/sheafify.test.ts @@ -8,7 +8,13 @@ import { collectSheafGuard } from './guard.ts'; import { constant } from './metadata.ts'; import { makeSection } from './section.ts'; import { sheafify } from './sheafify.ts'; -import type { Candidate, Policy, PolicyContext, Provider } from './types.ts'; +import type { + Candidate, + Policy, + PolicyContext, + Provider, + Section, +} from './types.ts'; // Thin cast for calling exo methods directly in tests without going through // HandledPromise (which is not available in the test environment). @@ -914,4 +920,216 @@ describe('getSection with explicit guard', () => { expect(E(section)[GET_DESCRIPTION]()).toStrictEqual(schema); expect(await E(section).getBalance('alice')).toBe(42); }); + + it('does not collapse -Infinity and Infinity metadata as equivalent', async () => { + type Meta = { cost: number }; + let candidateCount = 0; + + const providers: Provider[] = [ + { + exo: makeSection( + 'W:0', + M.interface('W:0', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 0 }, + ), + metadata: constant({ cost: -Infinity }), + }, + { + exo: makeSection( + 'W:1', + M.interface('W:1', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 0 }, + ), + metadata: constant({ cost: Infinity }), + }, + ]; + + const wallet = buildUnionSection( + 'W', + providers, + async function* (candidates) { + candidateCount = candidates.length; + yield candidates[0]!; + }, + ); + + await E(wallet).f('alice'); + expect(candidateCount).toBe(2); + }); + + it('does not collapse undefined and null metadata values as equivalent', async () => { + type Meta = { tag: string | null | undefined }; + let candidateCount = 0; + + const providers: Provider[] = [ + { + exo: makeSection( + 'W:0', + M.interface('W:0', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 0 }, + ), + metadata: constant({ tag: undefined }), + }, + { + exo: makeSection( + 'W:1', + M.interface('W:1', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 0 }, + ), + metadata: constant({ tag: null }), + }, + ]; + + const wallet = buildUnionSection( + 'W', + providers, + async function* (candidates) { + candidateCount = candidates.length; + yield candidates[0]!; + }, + ); + + await E(wallet).f('alice'); + expect(candidateCount).toBe(2); + }); + + it('does not collapse bigint and equal-magnitude number metadata as equivalent', async () => { + type Meta = { weight: bigint | number }; + let candidateCount = 0; + + const providers: Provider[] = [ + { + exo: makeSection( + 'W:0', + M.interface('W:0', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 0 }, + ), + metadata: constant({ weight: 1n }), + }, + { + exo: makeSection( + 'W:1', + M.interface('W:1', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 0 }, + ), + metadata: constant({ weight: 1 }), + }, + ]; + + const wallet = buildUnionSection( + 'W', + providers, + async function* (candidates) { + candidateCount = candidates.length; + yield candidates[0]!; + }, + ); + + await E(wallet).f('alice'); + expect(candidateCount).toBe(2); + }); + + it('throws when a section advertises a method via guard but has no handler', async () => { + const fakeGuard = M.interface('Faux', { + f: M.call(M.string()).returns(M.string()), + }); + const handlerlessSection: Section = { + [GET_INTERFACE_GUARD]: () => fakeGuard, + }; + const providers: Provider>[] = [ + { exo: handlerlessSection }, + ]; + + const wallet = buildUnionSection>( + 'Faux', + providers, + async function* (candidates) { + yield candidates[0]!; + }, + ); + + await expect(E(wallet).f('x')).rejects.toThrow( + "Section has guard for 'f' but no handler", + ); + }); + + it('throws "No section covers" when explicit guard admits args no provider matches', async () => { + const providers: Provider>[] = [ + { + exo: makeSection( + 'W:0', + M.interface('W:0', { + f: M.call(M.eq('alice')).returns(M.number()), + }), + { f: (_acct: string) => 42 }, + ), + }, + ]; + + const wideGuard = M.interface('Wide', { + f: M.call(M.string()).returns(M.number()), + }); + + const section = sheafify({ name: 'W', providers }).getSection({ + guard: wideGuard, + async *policy(candidates) { + yield candidates[0]!; + }, + }); + + await expect(E(section).f('bob')).rejects.toThrow('No section covers'); + }); + + it('throws when policy yields a candidate object not from the candidates array', async () => { + type Meta = { tier: string }; + + const providers: Provider[] = [ + { + exo: makeSection( + 'W:0', + M.interface('W:0', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 1 }, + ), + metadata: constant({ tier: 'a' }), + }, + { + exo: makeSection( + 'W:1', + M.interface('W:1', { + f: M.call(M.string()).returns(M.number()), + }), + { f: (_acct: string) => 2 }, + ), + metadata: constant({ tier: 'b' }), + }, + ]; + + const wallet = buildUnionSection( + 'W', + providers, + + async function* (candidates) { + // structurally equivalent but not the same object reference + yield { ...candidates[0]! }; + }, + ); + + await expect(E(wallet).f('alice')).rejects.toThrow( + 'unrecognized candidate', + ); + }); }); From cc4f92f786299c45ee163789591f1ddcf2fb2314 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:48:49 -0500 Subject: [PATCH 67/68] chore(sheaves): reset to unreleased state (0.0.0, empty changelog) --- packages/sheaves/CHANGELOG.md | 53 +---------------------------------- packages/sheaves/package.json | 2 +- 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/packages/sheaves/CHANGELOG.md b/packages/sheaves/CHANGELOG.md index 47808332fa..0c82cb1ed6 100644 --- a/packages/sheaves/CHANGELOG.md +++ b/packages/sheaves/CHANGELOG.md @@ -7,55 +7,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed - -- **BREAKING:** Renamed the `lift` option key on `Sheaf.getSection` and - `Sheaf.getDiscoverableSection` to `policy`, completing the earlier - `Lift` → `Policy` type rename. Callers must update their call sites: - `sheaf.getSection({ guard, lift })` → `sheaf.getSection({ guard, policy })`. - -## [0.1.0] - -### Added - -- Initial release, extracted from `@metamask/kernel-utils`. -- `sheafify({ name, providers })` — constructs a sheaf authority manager over a - set of capability providers. -- `Provider` type — an input to `sheafify`: a `{ exo, metadata? }` pair - where `exo` is a `Section` and `metadata` is an optional `MetadataSpec`. -- `Candidate` type — a post-evaluation entry in the candidate set: `{ exo, -metadata }` with metadata already resolved from its spec. -- `Section` type — an exo capability covering a region of the interface - topology. -- `Policy` type — an `async function*` coroutine that receives candidates - and yields them in preference order; drives the sheaf dispatch loop. -- `PolicyContext` type — context passed to the policy: `{ method, args, -constraints }`. -- `MetadataSpec` discriminated union with two variants: `constant` and - `callable`. -- `constant(value)` — static metadata spec; value is fixed at construction. -- `callable(fn)` — callable metadata spec; evaluated per-dispatch with the - invocation arguments. -- `makeSection(name, guard, handlers)` — creates a named, guarded `Section` from a method-handler map. -- `makeRemoteSection(tag, remoteRef, metadata?)` — builds a provider that - wraps a remote capability, fetching its interface guard via `E`. -- `noopPolicy` — a policy that yields candidates in the order received. -- `proxyPolicy(gen)` — wraps an existing generator to satisfy the `Policy` - call signature. -- `withFilter(predicate)` — higher-order policy combinator that pre-filters - the candidate list before passing it to the inner policy. -- `withRanking(comparator)` — higher-order policy combinator that pre-sorts - the candidate list before passing it to the inner policy. -- `fallthrough(policyA, policyB)` — composes two policies so that `policyB` - is tried only after `policyA` is exhausted. -- `Sheaf` type — the authority manager returned by `sheafify`; exposes - `getSection` and `getDiscoverableSection`. -- `collectSheafGuard(name, sections)` — compute the union interface guard of - a set of sections, for callers that assemble providers dynamically and need - to pass an explicit guard to `getSection`. -- `docs/POLICY.md` — documents the policy coroutine protocol, - `PolicyContext`, and the semantic equivalence assumption. -- `docs/USAGE.md` — annotated usage examples. - -[Unreleased]: https://github.com/MetaMask/ocap-kernel/compare/@metamask/sheaves@0.1.0...HEAD -[0.1.0]: https://github.com/MetaMask/ocap-kernel/releases/tag/@metamask/sheaves@0.1.0 +[Unreleased]: https://github.com/MetaMask/ocap-kernel/ diff --git a/packages/sheaves/package.json b/packages/sheaves/package.json index 26ce3c8102..72d2e24a97 100644 --- a/packages/sheaves/package.json +++ b/packages/sheaves/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/sheaves", - "version": "0.1.0", + "version": "0.0.0", "description": "Capability routing via sheaf theory", "keywords": [ "MetaMask", From 3793f61015edc5f328803ff200d4e9d7555fa206 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:22:25 -0500 Subject: [PATCH 68/68] ci: re-trigger Main