feat(kernel-utils): add sheaf programming module#870
Conversation
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4184513 to
03f6113
Compare
487dd20 to
282a277
Compare
4123110 to
2519237
Compare
0a7b40c to
f4bb458
Compare
f4bb458 to
f59d51b
Compare
342233d to
0de9c94
Compare
0de9c94 to
4f47c89
Compare
rekmarks
left a comment
There was a problem hiding this comment.
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.
2bd354e to
62f7a94
Compare
Suggestions applied, except any renaming suggestion that included 'tags' in the new name. |
5c536ca to
eed028e
Compare
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>
eed028e to
cc25c7f
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
…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); |
There was a problem hiding this comment.
If args is only ever used if kind is 'constant', should it be an optional parameter?

Introduce operational presheaf + sheafify for guard-based dispatch:
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/sheavespackage that implements guard-based capability routing viasheafify, producing dispatch sections that select among matching providers using evaluated metadata and a caller-supplied async-generatorPolicy(including retry with accumulated errors and constraint/option metadata decomposition).Includes helpers for metadata specs (
constant/callable/sourcewith optional compartment compilation), policy composition utilities (noopPolicy,withFilter,withRanking,fallthrough,proxyPolicy), remote-provider wrapping viamakeRemoteSection, plus extensive unit/e2e tests and documentation. Wires the package into the monorepo build/test setup (newtsconfigrefs,vitestconfig, 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.