Skip to content

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

Open
grypez wants to merge 54 commits into
mainfrom
grypez/bringing-in-the-sheaves
Open

feat(kernel-utils): add sheaf programming module#870
grypez wants to merge 54 commits into
mainfrom
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
Mostly additive new package, but it introduces new dispatch/routing logic (guard matching, metadata evaluation, policy-driven retries) that will affect callers adopting it and could surface subtle runtime edge cases.

Overview
Adds a new @metamask/sheaves package that implements guard-based capability routing via sheafify, producing dispatch sections that select among matching providers using evaluated metadata and a caller-supplied async-generator Policy (including retry with accumulated errors and constraint/option metadata decomposition).

Includes helpers for metadata specs (constant/callable/source with optional compartment compilation), policy composition utilities (noopPolicy, withFilter, withRanking, fallthrough, proxyPolicy), remote-provider wrapping via makeRemoteSection, plus extensive unit/e2e tests and documentation. Wires the package into the monorepo build/test setup (new tsconfig refs, vitest config, lockfile entry) and adds standard package metadata/licensing/changelog.

Reviewed by Cursor Bugbot for commit ba12dbe. 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.96%
⬆️ +0.53%
8503 / 11816
🔵 Statements 71.79%
⬆️ +0.53%
8644 / 12040
🔵 Functions 72.83%
⬆️ +0.58%
2054 / 2820
🔵 Branches 65.58%
⬆️ +0.49%
3434 / 5236
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/sheaves/src/compose.ts 94.44% 100% 90.9% 94.44% 16
packages/sheaves/src/guard.ts 98.59% 96.15% 100% 98.52% 109
packages/sheaves/src/index.ts 100% 100% 100% 100%
packages/sheaves/src/metadata.ts 100% 100% 100% 100%
packages/sheaves/src/remote.ts 100% 100% 100% 100%
packages/sheaves/src/section.ts 100% 100% 100% 100%
packages/sheaves/src/sheafify.ts 93.69% 82.92% 100% 93.63% 39, 42, 52, 120, 163, 247, 277-282
packages/sheaves/src/stalk.ts 88.23% 86.66% 100% 88.23% 31, 69
packages/sheaves/src/types.ts 100% 100% 100% 100%
Generated in workflow #4410 for commit ba12dbe 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 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/sheaves/src/sheafify.ts
Comment thread packages/sheaves/src/sheafify.ts
Comment thread packages/sheaves/src/sheafify.ts
Comment thread packages/sheaves/src/sheafify.ts
Copy link
Copy Markdown
Member

@rekmarks rekmarks left a comment

Choose a reason for hiding this comment

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

I think this is a promising direction for composing object capabilities that we should continue to experiment with. Some notes:

  • The terminology is abstruse for the algebraic topologically challenged. I propose an alternative here
  • Sheaves should be in their own package @metamask/sheaves. We'll have to cut a release once this is merged.

@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch 2 times, most recently from 2bd354e to 62f7a94 Compare May 7, 2026 19:31
@grypez
Copy link
Copy Markdown
Contributor Author

grypez commented May 7, 2026

I think this is a promising direction for composing object capabilities that we should continue to experiment with. Some notes:

  • The terminology is abstruse for the algebraic topologically challenged. I propose an alternative here
  • Sheaves should be in their own package @metamask/sheaves. We'll have to cut a release once this is merged.

Suggestions applied, except any renaming suggestion that included 'tags' in the new name.

@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch 2 times, most recently from 5c536ca to eed028e Compare May 8, 2026 11:33
@grypez grypez added no-changelog Indicates that no changelog updates are required, and that related CI checks should be skipped. labels May 8, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
grypez and others added 25 commits May 8, 2026 07:57
…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>
…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>
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>
…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>
@grypez grypez force-pushed the grypez/bringing-in-the-sheaves branch from eed028e to cc25c7f Compare May 8, 2026 11:57
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 cc25c7f. Configure here.

Comment thread packages/sheaves/src/sheafify.ts Outdated
grypez and others added 2 commits May 8, 2026 12:23
…eMetadata

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 <noreply@anthropic.com>
…pe 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 <noreply@anthropic.com>
if (spec === undefined) {
return {} as MetaData;
}
const raw = spec.kind === 'constant' ? spec.value : spec.fn(args);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If args is only ever used if kind is 'constant', should it be an optional parameter?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog Indicates that no changelog updates are required, and that related CI checks should be skipped.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants