Skip to content

feat(kernel-utils): add sheaf programming module#870

Open
grypez wants to merge 46 commits intomainfrom
grypez/bringing-in-the-sheaves
Open

feat(kernel-utils): add sheaf programming module#870
grypez wants to merge 46 commits intomainfrom
grypez/bringing-in-the-sheaves

Conversation

@grypez
Copy link
Copy Markdown
Contributor

@grypez grypez commented Mar 4, 2026

Introduce operational presheaf + sheafify for guard-based dispatch:

  • Section/guard types, presheaf construction, stalk filtering
  • Late decider (lift) selects winner when multiple sections match
  • Modular sheaf/ directory with single-concern files and e2e tests

Note

Medium Risk
Introduces a new dispatch/routing subsystem (sheafify, lift coroutines, guard unioning, remote forwarding) that will be consumed by other packages, so correctness of selection/guard logic and metadata equivalence is important though changes are largely additive.

Overview
Adds a new @metamask/kernel-utils/sheaf module that builds a capability “router” (sheafify) over multiple exo sections, selecting an implementation at call time by filtering sections via interface guards, evaluating optional per-invocation metadata, collapsing metadata-equivalent candidates, and invoking a caller-supplied Lift coroutine to rank/retry candidates.

Includes utilities for authoring sections and policies (makeSection, constant/callable/source metadata specs, and lift combinators like noopLift, withFilter, withRanking, fallthrough, proxyLift), plus makeRemoteSection to wrap CapTP remote references and forward calls via E().

Updates package.json exports and dependencies (adds @endo/eventual-send) and adds extensive unit/e2e tests and documentation for guard collection, stalk filtering, metadata evaluation, and lift retry semantics.

Reviewed by Cursor Bugbot for commit c43872e. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 4, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 71.49%
⬆️ +0.54%
8362 / 11696
🔵 Statements 71.33%
⬆️ +0.54%
8502 / 11918
🔵 Functions 72.5%
⬆️ +0.60%
2033 / 2804
🔵 Branches 65.08%
⬆️ +0.50%
3378 / 5190
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/kernel-utils/src/sheaf/compose.ts 94.44% 100% 90.9% 94.44% 16
packages/kernel-utils/src/sheaf/guard.ts 98.59% 96.15% 100% 98.52% 109
packages/kernel-utils/src/sheaf/index.ts 100% 100% 100% 100%
packages/kernel-utils/src/sheaf/metadata.ts 100% 100% 100% 100%
packages/kernel-utils/src/sheaf/remote.ts 100% 100% 100% 100%
packages/kernel-utils/src/sheaf/section.ts 100% 100% 100% 100%
packages/kernel-utils/src/sheaf/sheafify.ts 93.69% 82.92% 100% 93.63% 39, 42, 52, 120, 159, 243, 273-278
packages/kernel-utils/src/sheaf/stalk.ts 88.23% 86.66% 100% 88.23% 31, 69
packages/kernel-utils/src/sheaf/types.ts 100% 100% 100% 100%
Generated in workflow #4390 for commit c43872e by the Vitest Coverage Report Action

@grypez grypez force-pushed the grypez/schema-dunder branch from 4184513 to 03f6113 Compare March 9, 2026 19:07
@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch from 487dd20 to 282a277 Compare March 9, 2026 19:08
@grypez grypez changed the title feat(kernel-exo): add sheaf programming module feat(kernel-utils): add sheaf programming module Mar 9, 2026
@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch from 4123110 to 2519237 Compare March 10, 2026 14:28
Base automatically changed from grypez/schema-dunder to main March 10, 2026 16:59
@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch from 0a7b40c to f4bb458 Compare March 10, 2026 22:07
@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch from f4bb458 to f59d51b Compare April 1, 2026 15:25
@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch 4 times, most recently from 342233d to 0de9c94 Compare April 27, 2026 13:52
@grypez grypez changed the base branch from main to grypez/evm-wallet-ses-cleanup April 27, 2026 13:53
Base automatically changed from grypez/evm-wallet-ses-cleanup to main April 27, 2026 13:59
grypez and others added 16 commits April 27, 2026 10:52
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ESCRIPTION

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

`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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
…rg 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 <noreply@anthropic.com>
Add MetaDataSpec<M> 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<M> (breaking).
A new EvaluatedSection<M> 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 <T extends { exo: Section }> so it works over
ResolvedSection (the internal post-resolution type) without a cast.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
Made-with: Cursor
- 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<string, unknown>
- simplify metadataKey and decomposeMetadata; drop ifDefined in dispatch

Made-with: Cursor
Replace the one-shot `Lift<M> = (...) => Promise<number>` 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…eaf sections

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 <noreply@anthropic.com>
…ion methods

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 <noreply@anthropic.com>
…terfaces

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
grypez and others added 15 commits April 27, 2026 10:52
…holder lift

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n cast

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ncifyMethodGuards to guard.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tocol violation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…se only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lic exports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ve.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"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 <noreply@anthropic.com>
The alias added a second public name for PresheafSection<M>[] with no
external consumers. Callers write the array type directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The guard is passed dynamically at call time so TypeScript cannot
propagate the method signatures through Sheaf<M>. The comment prevents
future contributors from chasing a phantom improvement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ndler failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch from 0de9c94 to 4f47c89 Compare April 27, 2026 15:25
@grypez grypez marked this pull request as ready for review April 27, 2026 15:51
@grypez grypez requested a review from a team as a code owner April 27, 2026 15:51
Comment thread packages/kernel-utils/src/sheaf/sheafify.ts
Comment thread packages/kernel-utils/src/sheaf/sheafify.ts
grypez and others added 3 commits April 27, 2026 12:06
…dataKey conflation bugs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
Comment thread packages/kernel-utils/src/sheaf/sheafify.ts
grypez and others added 3 commits April 27, 2026 12:21
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 <noreply@anthropic.com>
…decomposeMetadata

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
=== 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 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2d84d4a. Configure here.

Comment thread packages/kernel-utils/src/sheaf/sheafify.ts
grypez and others added 2 commits April 27, 2026 12:40
…aKey

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant