Skip to content

Commit 282a277

Browse files
grypezclaude
andcommitted
feat(kernel-utils): add sheaf programming module
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 03f6113 commit 282a277

10 files changed

Lines changed: 2048 additions & 0 deletions

File tree

packages/kernel-utils/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,14 @@ export {
3737
DEFAULT_MAX_DELAY_MS,
3838
} from './retry.ts';
3939
export type { RetryBackoffOptions, RetryOnRetryInfo } from './retry.ts';
40+
export type {
41+
Section,
42+
PresheafSection,
43+
Lift,
44+
LiftContext,
45+
Presheaf,
46+
Sheaf,
47+
} from './sheaf/types.ts';
48+
export { sheafify } from './sheaf/sheafify.ts';
49+
export { collectSheafGuard } from './sheaf/guard.ts';
50+
export { getStalk, guardCoversPoint } from './sheaf/stalk.ts';
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Sheaf
2+
3+
Runtime capability routing adapted from sheaf theory in algebraic topology.
4+
5+
`sheafify({ name, sections })` produces a **sheaf** — an authority manager
6+
over a presheaf of capabilities. The sheaf grants revocable dispatch sections
7+
via `getSection`, tracks all delegated authority, and supports point-wise
8+
revocation.
9+
10+
## Concepts
11+
12+
**Presheaf section** (`PresheafSection`) — The input data: a capability (exo)
13+
paired with operational metadata, assigned over the open set defined by the
14+
exo's guard. This is an element of the presheaf F = F_sem x F_op.
15+
16+
> A `getBalance(string)` provider with `{ cost: 100 }` is one presheaf
17+
> section. A `getBalance("alice")` provider with `{ cost: 1 }` is another,
18+
> covering a narrower open set.
19+
20+
**Germ** — An equivalence class of presheaf sections at an invocation point,
21+
identified by metadata. At dispatch time, sections in the stalk with identical
22+
metadata are collapsed into a single germ; the system picks an arbitrary
23+
representative for dispatch. If two capabilities are indistinguishable by
24+
metadata, the sheaf has no data to prefer one over the other.
25+
26+
> Two `getBalance(string)` providers both with `{ cost: 1 }` collapse into
27+
> one germ. The lift never sees both — it receives one representative.
28+
29+
**Stalk** — The set of germs matching a specific `(method, args)` invocation,
30+
computed at dispatch time by guard filtering and then collapsing equivalent
31+
entries.
32+
33+
> Stalk at `("getBalance", "alice")` might contain two germs (cost 1 vs 100);
34+
> stalk at `("transfer", ...)` might contain one.
35+
36+
**Lift** — An async function that selects one germ from a multi-germ stalk.
37+
At dispatch time, metadata is decomposed into **constraints** (keys with the
38+
same value across every germ — topologically determined, not a choice) and
39+
**options** (the remaining keys — the lift's actual decision space). The lift
40+
receives only options on each germ; constraints arrive separately in the
41+
context.
42+
43+
> `argmin` by cost, `argmin` by latency, or any custom selection logic. The
44+
> lift is never invoked for single-germ stalks.
45+
46+
**Sheaf** — The authority manager returned by `sheafify`. Holds the presheaf
47+
data (captured at construction time) and a registry of all granted sections.
48+
49+
```
50+
const sheaf = sheafify({ name: 'Wallet', sections });
51+
```
52+
53+
- `sheaf.getSection({ guard?, lift })` — produce a revocable dispatch exo
54+
- `sheaf.revokePoint(method, ...args)` — revoke every granted section whose
55+
guard covers the point
56+
- `sheaf.getExported()` — union guard of all active (non-revoked) sections
57+
- `sheaf.revokeAll()` — revoke every granted section
58+
59+
## Dispatch pipeline
60+
61+
At each invocation point `(method, args)` within a granted section:
62+
63+
```
64+
getStalk(sections, method, args) presheaf → stalk (filter by guard)
65+
collapseEquivalent(stalk) locality condition (quotient by metadata)
66+
decomposeMetadata(collapsed) restriction map (constraints / options)
67+
lift(stripped, { method, args, operational selection (extra-theoretic)
68+
constraints })
69+
dispatch to collapsed[index].exo evaluation
70+
```
71+
72+
## Design choices
73+
74+
**Germ identity is metadata identity.** The collapse step quotients by
75+
metadata: if two sections should be distinguishable, the caller must give them
76+
distinguishable metadata. Sections with identical metadata are treated as
77+
interchangeable. Under the sheaf condition (effect-equivalence), this recovers
78+
the classical equivalence relation on germs.
79+
80+
**Pseudosheafification.** The sheafification functor would precompute the full
81+
etale space. This system defers to invocation time: compute the stalk,
82+
collapse, decompose, lift. The trade-off is that global coherence (a lift
83+
choosing consistently across points) is not guaranteed.
84+
85+
**Restriction and gluing are implicit.** Guard restriction induces a
86+
restriction map on metadata: restricting to a point filters the presheaf to
87+
covering sections (`getStalk`), then `decomposeMetadata` strips the metadata
88+
to distinguishing keys — the restricted metadata over that point. The join
89+
works dually: the union of two sections has the join of their metadata, and
90+
restriction at any point recovers the local distinguishing keys in O(n).
91+
Gluing follows: compatible sections (equal metadata on their overlap) produce a
92+
well-defined join. The dispatch pipeline computes all of this implicitly. The
93+
remaining gap is `revokeSite` (revoking over an open set rather than a point),
94+
which requires an `intersects` operator on guards not yet available.
95+
96+
## Relationship to stacks
97+
98+
This construction is more properly a **stack** in algebraic geometry. We call
99+
it a sheaf because engineers already know "stack" as a LIFO data structure, and
100+
the algebraic geometry term is unrelated. Within a germ, any representative
101+
will do — authority-equivalence is asserted by constructor contract, not
102+
verified at runtime. Between germs, metadata distinguishes them and the lift
103+
resolves the choice.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { makeExo } from '@endo/exo';
2+
import {
3+
M,
4+
matches,
5+
getInterfaceGuardPayload,
6+
getMethodGuardPayload,
7+
} from '@endo/patterns';
8+
import type { MethodGuard, Pattern } from '@endo/patterns';
9+
import { describe, it, expect } from 'vitest';
10+
11+
import { collectSheafGuard } from './guard.ts';
12+
import type { Section } from './types.ts';
13+
14+
const makeSection = (
15+
tag: string,
16+
guards: Record<string, MethodGuard>,
17+
methods: Record<string, (...args: unknown[]) => unknown>,
18+
): Section => {
19+
const interfaceGuard = M.interface(tag, guards);
20+
return makeExo(tag, interfaceGuard, methods) as unknown as Section;
21+
};
22+
23+
describe('collectSheafGuard', () => {
24+
it('variable arity: add with 1, 2, and 3 args', () => {
25+
const sections = [
26+
makeSection(
27+
'Calc:0',
28+
{ add: M.call(M.number()).returns(M.number()) },
29+
{ add: (a: number) => a },
30+
),
31+
makeSection(
32+
'Calc:1',
33+
{ add: M.call(M.number(), M.number()).returns(M.number()) },
34+
{ add: (a: number, b: number) => a + b },
35+
),
36+
makeSection(
37+
'Calc:2',
38+
{
39+
add: M.call(M.number(), M.number(), M.number()).returns(M.number()),
40+
},
41+
{ add: (a: number, b: number, cc: number) => a + b + cc },
42+
),
43+
];
44+
45+
const guard = collectSheafGuard('Calc', sections);
46+
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
47+
methodGuards: Record<string, MethodGuard>;
48+
};
49+
const payload = getMethodGuardPayload(methodGuards.add) as unknown as {
50+
argGuards: Pattern[];
51+
optionalArgGuards?: Pattern[];
52+
};
53+
54+
// 1 required arg (present in all), 2 optional (variable arity)
55+
expect(payload.argGuards).toHaveLength(1);
56+
expect(payload.optionalArgGuards).toHaveLength(2);
57+
});
58+
59+
it('return guard union', () => {
60+
const sections = [
61+
makeSection(
62+
'S:0',
63+
{ f: M.call(M.eq(0)).returns(M.eq(0)) },
64+
{ f: (_: number) => 0 },
65+
),
66+
makeSection(
67+
'S:1',
68+
{ f: M.call(M.eq(1)).returns(M.eq(1)) },
69+
{ f: (_: number) => 1 },
70+
),
71+
];
72+
73+
const guard = collectSheafGuard('S', sections);
74+
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
75+
methodGuards: Record<string, MethodGuard>;
76+
};
77+
const { returnGuard } = getMethodGuardPayload(
78+
methodGuards.f,
79+
) as unknown as { returnGuard: Pattern };
80+
81+
// Return guard is union of eq(0) and eq(1)
82+
expect(matches(0, returnGuard)).toBe(true);
83+
expect(matches(1, returnGuard)).toBe(true);
84+
});
85+
86+
it('section with its own optional args: optional preserved in union', () => {
87+
const sections = [
88+
makeSection(
89+
'Greeter',
90+
{
91+
greet: M.callWhen(M.string())
92+
.optional(M.string())
93+
.returns(M.string()),
94+
},
95+
{ greet: (name: string, _greeting?: string) => `hello ${name}` },
96+
),
97+
];
98+
99+
const guard = collectSheafGuard('Greeter', sections);
100+
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
101+
methodGuards: Record<string, MethodGuard>;
102+
};
103+
const payload = getMethodGuardPayload(methodGuards.greet) as unknown as {
104+
argGuards: Pattern[];
105+
optionalArgGuards?: Pattern[];
106+
};
107+
108+
expect(payload.argGuards).toHaveLength(1);
109+
expect(payload.optionalArgGuards).toHaveLength(1);
110+
});
111+
112+
it('rest arg guard preserved in collected union', () => {
113+
const sections = [
114+
makeSection(
115+
'Logger',
116+
{ log: M.call(M.string()).rest(M.string()).returns(M.any()) },
117+
{ log: (..._args: string[]) => undefined },
118+
),
119+
];
120+
121+
const guard = collectSheafGuard('Logger', sections);
122+
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
123+
methodGuards: Record<string, MethodGuard>;
124+
};
125+
const payload = getMethodGuardPayload(methodGuards.log) as unknown as {
126+
argGuards: Pattern[];
127+
optionalArgGuards?: Pattern[];
128+
restArgGuard?: Pattern;
129+
};
130+
131+
expect(payload.argGuards).toHaveLength(1);
132+
expect(payload.optionalArgGuards ?? []).toHaveLength(0);
133+
expect(payload.restArgGuard).toBeDefined();
134+
});
135+
136+
it('rest arg guards unioned across sections', () => {
137+
const sections = [
138+
makeSection(
139+
'A',
140+
{ log: M.call(M.string()).rest(M.string()).returns(M.any()) },
141+
{ log: (..._args: string[]) => undefined },
142+
),
143+
makeSection(
144+
'B',
145+
{ log: M.call(M.string()).rest(M.number()).returns(M.any()) },
146+
{ log: (..._args: unknown[]) => undefined },
147+
),
148+
];
149+
150+
const guard = collectSheafGuard('AB', sections);
151+
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
152+
methodGuards: Record<string, MethodGuard>;
153+
};
154+
const { restArgGuard } = getMethodGuardPayload(
155+
methodGuards.log,
156+
) as unknown as { restArgGuard?: Pattern };
157+
158+
expect(matches('hello', restArgGuard)).toBe(true);
159+
expect(matches(42, restArgGuard)).toBe(true);
160+
});
161+
162+
it('multi-method guard collection', () => {
163+
const sections = [
164+
makeSection(
165+
'Multi:0',
166+
{
167+
translate: M.call(M.string(), M.string()).returns(M.string()),
168+
},
169+
{
170+
translate: (from: string, to: string) => `${from}->${to}`,
171+
},
172+
),
173+
makeSection(
174+
'Multi:1',
175+
{
176+
translate: M.call(M.string(), M.string()).returns(M.string()),
177+
summarize: M.call(M.string()).returns(M.string()),
178+
},
179+
{
180+
translate: (from: string, to: string) => `${from}->${to}`,
181+
summarize: (text: string) => `summary: ${text}`,
182+
},
183+
),
184+
];
185+
186+
const guard = collectSheafGuard('Multi', sections);
187+
const { methodGuards } = getInterfaceGuardPayload(guard) as unknown as {
188+
methodGuards: Record<string, MethodGuard>;
189+
};
190+
expect('translate' in methodGuards).toBe(true);
191+
expect('summarize' in methodGuards).toBe(true);
192+
});
193+
});

0 commit comments

Comments
 (0)