Skip to content

Commit 24ac309

Browse files
grypezclaude
andcommitted
feat(kernel-utils): metadata as polynomials of invocation data
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>
1 parent 7fa0b4e commit 24ac309

11 files changed

Lines changed: 483 additions & 60 deletions

File tree

packages/kernel-utils/src/index.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ describe('index', () => {
1212
'GET_DESCRIPTION',
1313
'abortableDelay',
1414
'calculateReconnectionBackoff',
15+
'callable',
1516
'collectSheafGuard',
17+
'constant',
1618
'delay',
1719
'fetchValidatedJson',
1820
'fromHex',
@@ -34,6 +36,7 @@ describe('index', () => {
3436
'retry',
3537
'retryWithBackoff',
3638
'sheafify',
39+
'source',
3740
'stringify',
3841
'toHex',
3942
'waitUntilQuiescent',

packages/kernel-utils/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@ export type { RetryBackoffOptions, RetryOnRetryInfo } from './retry.ts';
4040
export type {
4141
Section,
4242
PresheafSection,
43+
EvaluatedSection,
44+
MetaDataSpec,
4345
Lift,
4446
LiftContext,
4547
Presheaf,
4648
Sheaf,
4749
} from './sheaf/types.ts';
50+
export { constant, source, callable } from './sheaf/metadata.ts';
4851
export { sheafify } from './sheaf/sheafify.ts';
4952
export { collectSheafGuard } from './sheaf/guard.ts';
5053
export { getStalk, guardCoversPoint } from './sheaf/stalk.ts';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
3+
import {
4+
callable,
5+
constant,
6+
evaluateMetadata,
7+
resolveMetaDataSpec,
8+
source,
9+
} from './metadata.ts';
10+
11+
describe('constant', () => {
12+
it('returns a constant spec with the given value', () => {
13+
expect(constant(42)).toStrictEqual({ kind: 'constant', value: 42 });
14+
});
15+
16+
it('evaluateMetadata returns the value regardless of args', () => {
17+
const spec = resolveMetaDataSpec(constant({ cost: 7 }));
18+
expect(evaluateMetadata(spec, [])).toStrictEqual({ cost: 7 });
19+
expect(evaluateMetadata(spec, [1, 2, 3])).toStrictEqual({ cost: 7 });
20+
});
21+
});
22+
23+
describe('callable', () => {
24+
it('returns a callable spec wrapping the function', () => {
25+
const fn = (args: unknown[]) => args[0] as number;
26+
const spec = callable(fn);
27+
expect(spec).toStrictEqual({ kind: 'callable', fn });
28+
});
29+
30+
it('evaluateMetadata calls fn with args', () => {
31+
const fn = vi.fn((args: unknown[]) => (args[0] as number) * 2);
32+
const spec = resolveMetaDataSpec(callable(fn));
33+
expect(evaluateMetadata(spec, [5])).toBe(10);
34+
expect(fn).toHaveBeenCalledWith([5]);
35+
});
36+
});
37+
38+
describe('source', () => {
39+
it('returns a source spec with the src string', () => {
40+
expect(source('(args) => args[0]')).toStrictEqual({
41+
kind: 'source',
42+
src: '(args) => args[0]',
43+
});
44+
});
45+
46+
it('resolveMetaDataSpec compiles source to callable via compartment', () => {
47+
const mockFn = (args: unknown[]) => args[0] as number;
48+
const compartment = { evaluate: vi.fn(() => mockFn) };
49+
const spec = resolveMetaDataSpec(source('(args) => args[0]'), compartment);
50+
expect(spec.kind).toBe('callable');
51+
expect(compartment.evaluate).toHaveBeenCalledWith('(args) => args[0]');
52+
expect(evaluateMetadata(spec, [99])).toBe(99);
53+
});
54+
});
55+
56+
describe('resolveMetaDataSpec', () => {
57+
it('passes constant spec through unchanged', () => {
58+
const spec = constant(42);
59+
expect(resolveMetaDataSpec(spec)).toStrictEqual(spec);
60+
});
61+
62+
it('passes callable spec through unchanged', () => {
63+
const fn = (_args: unknown[]) => 0;
64+
const spec = callable(fn);
65+
expect(resolveMetaDataSpec(spec)).toStrictEqual(spec);
66+
});
67+
68+
it("throws if kind is 'source' and no compartment supplied", () => {
69+
expect(() => resolveMetaDataSpec(source('() => 0'))).toThrow(
70+
"compartment required to evaluate 'source' metadata",
71+
);
72+
});
73+
});
74+
75+
describe('evaluateMetadata', () => {
76+
it('returns undefined when spec is undefined', () => {
77+
expect(evaluateMetadata(undefined, [])).toBeUndefined();
78+
expect(evaluateMetadata(undefined, [1, 2])).toBeUndefined();
79+
});
80+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* MetaDataSpec constructors and evaluation helpers.
3+
*/
4+
5+
import type { MetaDataSpec } from './types.ts';
6+
7+
/** Resolved spec: 'source' has been compiled away; only constant or callable remain. */
8+
export type ResolvedMetaDataSpec<M> =
9+
| { kind: 'constant'; value: M }
10+
| { kind: 'callable'; fn: (args: unknown[]) => M };
11+
12+
/**
13+
* Wrap a static value as a constant metadata spec.
14+
*
15+
* @param value - The static metadata value.
16+
* @returns A constant MetaDataSpec wrapping the value.
17+
*/
18+
export const constant = <M>(value: M): MetaDataSpec<M> =>
19+
harden({ kind: 'constant', value });
20+
21+
/**
22+
* Wrap JS function source. Evaluated in a Compartment at sheafify construction time.
23+
*
24+
* @param src - JS source string of the form `(args) => M`.
25+
* @returns A source MetaDataSpec wrapping the source string.
26+
*/
27+
export const source = <M>(src: string): MetaDataSpec<M> =>
28+
harden({ kind: 'source', src });
29+
30+
/**
31+
* Wrap a live function as a callable metadata spec.
32+
*
33+
* @param fn - Function from invocation args to metadata value.
34+
* @returns A callable MetaDataSpec wrapping the function.
35+
*/
36+
export const callable = <M>(fn: (args: unknown[]) => M): MetaDataSpec<M> =>
37+
harden({ kind: 'callable', fn });
38+
39+
/**
40+
* Compile a 'source' spec to 'callable' using the supplied compartment.
41+
* 'constant' and 'callable' pass through unchanged.
42+
*
43+
* @param spec - The MetaDataSpec to resolve.
44+
* @param compartment - Compartment used to evaluate 'source' specs. Required when spec is 'source'.
45+
* @param compartment.evaluate - Evaluate a JS source string and return the result.
46+
* @returns A ResolvedMetaDataSpec with no 'source' variant.
47+
*/
48+
export const resolveMetaDataSpec = <M>(
49+
spec: MetaDataSpec<M>,
50+
compartment?: { evaluate: (src: string) => unknown },
51+
): ResolvedMetaDataSpec<M> => {
52+
if (spec.kind === 'source') {
53+
if (!compartment) {
54+
throw new Error(
55+
`sheafify: compartment required to evaluate 'source' metadata`,
56+
);
57+
}
58+
return {
59+
kind: 'callable',
60+
fn: compartment.evaluate(spec.src) as (args: unknown[]) => M,
61+
};
62+
}
63+
return spec;
64+
};
65+
66+
/**
67+
* Evaluate a resolved metadata spec against the invocation args.
68+
* Returns undefined if spec is undefined (no metadata on the section).
69+
*
70+
* @param spec - The resolved spec to evaluate, or undefined.
71+
* @param args - The invocation arguments.
72+
* @returns The evaluated metadata value, or undefined.
73+
*/
74+
export const evaluateMetadata = <M>(
75+
spec: ResolvedMetaDataSpec<M> | undefined,
76+
args: unknown[],
77+
): M | undefined => {
78+
if (spec === undefined) {
79+
return undefined;
80+
}
81+
if (spec.kind === 'constant') {
82+
return spec.value;
83+
}
84+
return spec.fn(args);
85+
};

0 commit comments

Comments
 (0)