Skip to content

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

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

feat(kernel-utils): add sheaf programming module#870
grypez wants to merge 52 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
Adds a brand-new authority-management/routing library (sheafify) that controls which capability handler is invoked at runtime based on guards and policy, so correctness issues could affect dispatch behavior. Risk is mitigated by being a new, isolated package with extensive unit/e2e coverage.

Overview
Introduces a new publishable package, @metamask/sheaves, providing guard-based capability dispatch via sheafify({ name, providers, compartment? }), including metadata evaluation (constant/callable/source), candidate collapsing by metadata equivalence, constraint/option decomposition, and a policy-driven retry loop with accumulated error history.

Adds policy composition utilities (noopPolicy, withFilter, withRanking, fallthrough, proxyPolicy), remote-provider wrapping (makeRemoteSection), guard union/asyncification utilities, and comprehensive unit/e2e tests plus documentation and package/build wiring (tsconfig references, vitest config, typedoc, yarn workspace entry, licenses/changelog/README).

Reviewed by Cursor Bugbot for commit cc25c7f. 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 #4409 for commit cc25c7f 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 28 commits May 8, 2026 07:57
…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>
…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.

const stripped = candidates.map((entry) => {
const remaining: Record<string, unknown> = {};
for (const [key, val] of Object.entries(entry.metadata)) {
if (!(key in constraints)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prototype chain causes incorrect metadata stripping in decompose

Medium Severity

The decomposeMetadata function uses key in constraints to decide which metadata keys to keep on stripped candidates. Because constraints is a plain {} inheriting from Object.prototype, any metadata key matching a prototype property name (e.g., constructor, toString, valueOf, hasOwnProperty) will be incorrectly treated as a constraint and silently dropped from all candidates — even when it was never added to constraints and carries distinguishing values. The policy then receives candidates missing that metadata, leading to incorrect dispatch decisions. Using Object.hasOwn(constraints, key) instead of key in constraints would fix this.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cc25c7f. Configure here.

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.

2 participants