Mirror CultMesh primitives in TypeScript

This commit is contained in:
Meta Vens 2026-06-27 14:35:08 +01:00
commit 6e000a71a9
4 changed files with 4267 additions and 0 deletions

View file

@ -11,6 +11,154 @@ frame body path.
The TypeScript projection keeps the same local catalog ergonomics as the other
runtimes: sorted views, direct lookup, and unsubscribe-able watches.
## Shared Primitives
CultMesh primitives are shared cross-runtime developer affordances, not just
DTOs. They bundle the typed surface, metadata, routing hints, diagnostics, and
fast-path hooks that let application code feel like it is talking to native
state while CultMesh chooses the right local, shared-memory, IPC, network, or
WASM route. Runtime code can ask for a whole XY rect, then adapt it to the
current wire schema at the edge:
```ts
import { CultMesh } from "cultmesh-ts";
const viewport = CultMesh.rectFromBounds(-500, -300, 500, 300);
const request = CultMesh.viewportRequest(viewport, [3, 8]);
```
The request currently projects to the compact `{ minX, minY, maxX, maxY }`
shape used by Aetheria viewport queries, while the authored code keeps the
designer-facing concept as a `CultMeshRect`.
Typed operations follow the same rule: client code holds a method-shaped handle
and a runtime context, while CultMesh hides the route and authority metadata
from call sites that do not need to inspect it.
```ts
const move = CultMesh.operation("game.pilot.move.v1", async (request, context) => {
return submitToBestRoute(request, context);
});
await move.invoke({ direction: CultMesh.vec2(1, 0) }, CultMesh.operationContext("rts-client", {
routeHint: CultMesh.routeHint("network", "starfire Verse"),
}));
```
Typed query surfaces can be read once or watched through the same handle. A
projection recipe keeps its source metadata, route preference, and reactive
watcher when it becomes a query surface:
```ts
const objects = CultMesh.projectionRecipe(
"aetheria.zone.objects.visible",
[CultMesh.projectionSource("daemon:aetheria.frame.latest.v1")],
projectObjects,
{
routeHint: CultMesh.routeHint("shared-memory", "co-located frame slab"),
watchProjection: watchObjects,
},
).asQuerySurface();
const stop = objects.watch(
{ viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-10, -10, 10, 10)) },
"browser-starfire",
(next) => render(next),
);
```
State pointers are the same kind of managed surface for UI and tools. They can
advertise source documents, inherit a Verse route when bound, and resolve
through that Verse without caller-side context plumbing:
```ts
const framePointer = CultMesh.statePointer(
"daemon:aetheria.frame.latest.v1",
async (context) => readFrameViaBestRoute(context),
undefined,
{
sources: [CultMesh.projectionSource("daemon:aetheria.frame.latest.v1")],
routeHint: CultMesh.routeHint("shared-memory", "co-located daemon frame"),
},
);
const frame = await CultMesh
.bindStatePointer(verse.withRoute("ipc", "Bifrost tool bridge"), framePointer)
.resolve();
const stateBinding = CultMesh.stateBinding("value", framePointer);
const stateBindingRecord = CultMesh.stateBindingRecord(stateBinding);
const restoredStateBinding = CultMesh.stateBindingFromRecord(stateBindingRecord);
```
Eve/CultUI lowerers that still receive authored state refs should use a named
state-ref resolver instead of local string utilities. Resolvers compose, carry
source and route metadata, and can be described by tools:
```ts
const daemonRefs = CultMesh.stateRefResolver(
"aetheria.daemon.refs",
(stateRef, context) => resolveDaemonStateRef(stateRef, context),
{
sources: [CultMesh.projectionSource("daemon:aetheria.frame.latest.v1")],
routeHint: CultMesh.routeHint("shared-memory", "co-located daemon state"),
},
);
const catalogRefs = CultMesh.stateRefResolver(
"aetheria.item_stats.refs",
(stateRef) => resolveItemStatRef(stateRef),
);
const resolver = daemonRefs.or(catalogRefs);
const value = resolver.resolve("aetheria:daemon:item-stat:laser.output");
const diagnostic = CultMesh.describeStateRefResolver(resolver);
```
Operation surfaces should preserve typed CultMesh identity all the way through
UI/runtime boundaries. Use invocation records and payload records at persistence
edges instead of local route parsers or string-map conventions:
```ts
const binding = CultMesh.operationBinding("gamecult.aetheria.pilot.set_target.v1", {
schemaId: "gamecult.aetheria.pilot.set_target.request.v1",
routeHint: CultMesh.routeHint("network", "co-op Verse"),
});
const bindingRecord = CultMesh.operationBindingRecord(binding);
const restoredBinding = CultMesh.operationBindingFromRecord(bindingRecord);
const invocation = CultMesh.operationInvocation(binding, {
idempotencyKey: "target:starfire:42",
});
const invocationRecord = CultMesh.operationInvocationRecord(invocation);
const payloadRecord = CultMesh.operationPayload({ targetEntityKey: "bandit-7" }).toRecord();
const restored = CultMesh.operationInvocationFromRecord(invocationRecord);
```
Route fields use the same shared primitive:
```ts
const routeRecord = CultMesh.routeRecord(CultMesh.routeHint("shared-memory", "co-located slab"));
const route = CultMesh.routeFromRecord(routeRecord);
```
When a client needs one coherent live view composed from several query surfaces,
use a live feed instead of putting a refresh loop in the UI:
```ts
const viewportFeed = CultMesh.liveFeed(
"aetheria.rts.viewport.feed",
snapshotViewport,
{
sources: objects.sources,
routeHint: objects.routeHint,
watchFeed: CultMesh.pollingQueryWatcher(snapshotViewport, { intervalMs: 50 }),
},
);
const stopFeed = viewportFeed.watch(request, "browser-starfire", render);
```
```ts
import { CultMesh } from "cultmesh-ts";

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,298 @@
# CultMesh Cross-Runtime Primitives Roadmap
CultMesh should feel like a cozy typed runtime, not a transport kit with good
manners. Application code should read as native state and native operations in
every runtime, while CultMesh chooses the fastest valid path: in-process,
shared memory, IPC, network, or WASM.
In this roadmap, "primitive" does not mean "tiny DTO" or local convenience
wrapper. It means a reusable cross-runtime developer affordance promoted into
the shared library layer: the abstraction, metadata, generated bindings,
runtime routing, diagnostics, and fast path that make an obvious state operation
feel native everywhere. A `Rect` value is a primitive; so is a typed viewport
query surface that can execute against an in-process daemon, a shared CultCache
slab, a remote Verse peer, or a browser/WASM host without changing the caller's
API.
This roadmap is the shared-layer backlog for abstractions that Aetheria,
Brokkr, Odin, Eve, Bifrost, Unity, Electron, Rust, and browser clients should
all be able to use without cloning local facade code.
## Design Rule
If two runtimes need the same semantic shape, hoist it into CultMesh or the
adjacent CultLib foundation layer. Do not leave it as an Aetheria-only wrapper,
an Electron preload convention, a Unity bridge helper, or an MCP-specific
adapter.
CultMesh should be expansive enough to absorb repeated developer-experience
patterns, not just repeated serialization shapes. When Aetheria, Brokkr, Odin,
Bifrost, Eve, Ymir, VoidBot, Unity, TS, Rust, or browser clients all want the
same kind of state pointer, operation, query, watch, authority claim, native
view, or surface binding, that is evidence of a missing shared primitive. The
domain package keeps the nouns and policy; CultMesh owns the managed, cozy
machinery that makes those nouns feel magical and run very fast.
The application layer may name domain operations:
```ts
await verse.aetheria.zone(zoneId).pilot(entityId).move(CultMesh.vec2(1, 0));
```
The shared layer must own the reusable primitives that make that sugar possible:
typed state pointers, operation handles, query surfaces, authority claims,
projection recipes, native slab descriptors, watches, and generated binding
metadata.
The test is whether a downstream runtime can say the domain thing directly. If
the caller has to remember record keys, schema ids, MessagePack slots, endpoint
paths, IPC channel names, local publication files, cache filenames, queue names,
or whether the target is local/remote/WASM, the shared primitive is not cozy
enough yet.
## Managed Magic Contract
Every promoted primitive should carry both a high-level developer promise and a
low-level performance promise:
| Promise | Requirement |
| --- | --- |
| Native-feeling API | Public calls look like typed domain state, operations, queries, or views rather than transport instructions. |
| One semantic surface | C#, TypeScript, Rust, browser/WASM, Unity, Eve/CultUI, and tool runtimes use the same conceptual API even when adapters differ. |
| Fastest valid route | The runtime can choose in-process calls, shared slabs, IPC, RUDP/network, copied snapshots, or WASM views without creating a second application API. |
| Typed safety | State, operation payloads, query parameters, results, authority claims, receipts, and view columns are generated or strongly typed. |
| Reactive by default | Values that can change expose watch/subscription semantics with explicit lifetime and backpressure behavior. |
| Inspectable | Diagnostics can explain the chosen route, authority decision, schema version, cache source, native handle, and copy/fallback cost. |
| Portable fallback | Remote or browser clients keep the same semantic surface when zero-copy locality is unavailable. |
## Primitive Families
| Primitive family | Shared responsibility | First consumers |
| --- | --- | --- |
| Typed state pointer | A stable typed reference to Verse state that can resolve, watch, and be embedded in Eve/CultUI surfaces. | Eve, Bifrost MCP, Unity inspectors, RTS panels |
| State binding descriptor | Surface metadata that binds a UI/tool prop to a typed CultMesh state pointer with source schema and route hints. | Eve/CultUI, Bifrost MCP, Unity inspectors, RTS status panels |
| Typed operation handle | Method-shaped invocation with typed request/response, authority claims, idempotency, route hints, and receipts. | Aetheria pilot/commander verbs, Brokkr editor tools |
| Operation binding descriptor | Surface metadata that binds a UI/tool command to a typed CultMesh operation id with request schema and route hints. | Eve/CultUI, Bifrost MCP, Unity controls, RTS command panels |
| Operation invocation descriptor | Concrete UI/tool invocation metadata that preserves operation id, request schema, route hints, and idempotency across renderer boundaries. | Eve/CultUI lowerers, Unity UI Toolkit, RTS panels, Bifrost tools |
| Operation payload | Shared invocation field bag with typed scalar readers, copy-on-write updates, and legacy string-field ingestion while generated request schemas take over. | Eve/CultUI lowerers, Unity UI Toolkit, RTS panels, Bifrost tools |
| Typed query surface | Derived-state query with typed parameters and result, callable locally or remotely without exposing transport. | Aetheria objects/gravity/selection/inventory viewports |
| Projection recipe | Declarative composition of typed documents into a named local projection, cacheable and watchable by clients. | Unity render shell, Electron RTS map, browser CultMesh-only clients |
| Authority scope | Runtime claim, lease, witness, or delegated simulation ownership expressed as typed policy state. | Trusted co-op, future quorum/witness modes |
| Native slice view | Typed SoA/slab view descriptors that map to Unity `NativeArray<T>`, Rust slices, C# spans, or browser typed arrays. | Aetheria render state, Ymir physics state |
| Schema-generated binding | Cross-runtime generated metadata for document slots, enum ids, schema ids, and ergonomic method surfaces. | C#, TypeScript, Rust, WASM |
| Reactive watch | Subscription primitive over typed documents, pointers, queries, and native views with disposal/backpressure semantics. | CultUI, Unity presentation caches, RTS status panels |
| Managed Verse context | Shared `verse.use(schema)` binding context carrying runtime id, route hints, and authority claims into generated domain operations and queries. | Unity client, RTS client, browser-only CultMesh clients, Rust rebuild |
| Verse-bound surface | Operation, query, and live-feed handles bound once to a Verse context so generated facades call domain methods without repeatedly materializing transport contexts. | Aetheria RTS facade, Unity facade generation, Rust/WASM bindings |
| Schema-stamped cache recovery | Directory-backed CultCache records may recover missing hot-manifest catalog entries from schema-version metadata embedded in the cold payload, then resolve by shared schema name. | Aetheria persisted Verse state, long-lived daemon stores, editor/runtime co-deployment |
## Required Sugar Shape
The shared primitives should let domain packages expose APIs that look like
state, not transport.
TypeScript target:
```ts
const verse = await CultMesh.connectVerse("starbridge");
const aetheria = verse.use(Aetheria);
const viewport = await aetheria.zone(zoneId)
.objects()
.visibleTo(controlledUnits)
.within(CultMesh.rectFromBounds(-400, -250, 400, 250))
.query();
await aetheria.entity(ravenId)
.pilot()
.move(CultMesh.vec2(1, 0), { claim: "pilot-control" });
```
C# target:
```csharp
var verse = await CultMesh.ConnectVerseAsync("starbridge");
var aetheria = verse.Use(AetheriaRuntime.Schema);
var viewport = await aetheria.Zone(zoneId)
.Objects()
.VisibleTo(controlledUnits)
.Within(CultRect.FromBounds(-400, -250, 400, 250))
.QueryAsync();
await aetheria.Entity(ravenId)
.Pilot()
.MoveAsync(CultVec2.Right, claim: "pilot-control");
```
Current seed:
```csharp
var verse = await CultMesh.ConnectVerseAsync(
"starbridge",
"unity-raven",
new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located Verse"),
new[] { new CultMeshAuthorityClaim("pilot-control", shardId: "zone:raven") });
var aetheria = verse.Use(context => new AetheriaGeneratedFacade(context));
await aetheria.Entity(ravenId).Pilot.MoveAsync(CultVec2.Right);
```
```ts
const verse = await CultMesh.connectVerse("starbridge", "browser-starfire", {
routeHint: CultMesh.routeHint("network", "remote Verse peer"),
claims: [CultMesh.authorityClaim("commander-control", { shardId: "zone:frontier" })],
});
const aetheria = verse.use((context) => new AetheriaGeneratedFacade(context));
await aetheria.entity(ravenId).pilot.move(CultMesh.vec2(1, 0));
```
Generated facades should split Verse views by locality and bind surfaces once:
```ts
const queryVerse = verse.withRoute("shared-memory", statePath);
const commandVerse = verse
.withRoute("network", endpoint)
.withClaim("commander-control", { shardId: "aetheria.local" });
const objectsViewport = CultMesh.bindQuery(queryVerse, queries.objectsViewport);
const setMoveVector = CultMesh.bindOperation(commandVerse, operations.setMoveVector);
await objectsViewport.execute(viewport);
await setMoveVector.invoke({ actorEntityKey, directionX: 1, directionY: 0 });
```
That seed is intentionally context-only. Discovery, authority negotiation,
transport selection, native slabs, and WASM adapters can grow behind the same
Verse handle without forcing Unity, Electron, browser, or Rust clients to learn
a second public API.
Rust/WASM target:
```rust
let verse = CultMesh::connect_verse("starbridge").await?;
let aetheria = verse.use_schema(aetheria::schema());
let viewport = aetheria
.zone(zone_id)
.objects()
.visible_to(&controlled_units)
.within(CultRect::from_bounds(-400.0, -250.0, 400.0, 250.0))
.query()
.await?;
```
These examples are intentionally aspirational. They are the shape the primitive
families must support; Aetheria should not keep bespoke client facades once the
shared layer can express the same semantics.
## Staged Build
1. Stabilize handle descriptors.
`CultMeshStatePointer<T>`, `CultMeshOperationHandle<TRequest, TResponse>`,
`CultMeshQuerySurface<TParameters, TResult>`,
`CultMeshProjectionRecipe<TParameters, TResult>`, `CultMeshRouteHint`, and
`CultMeshNativeSliceViewDescriptor` are the current seed. Keep them small,
serializable where useful, and mirrored across C# and TypeScript. Bound
operation, query, and live-feed wrappers are the first managed layer above
those descriptors; generated facades should prefer them over ad hoc
`OperationContext` / `QueryContext` plumbing.
2. Add generated binding descriptors.
Domain packages should generate schema ids, slots, enum ids, state pointer
factories, query handles, and operation handles from the same source
contract. Hand-entered MessagePack slot maps are compatibility debt.
3. Grow projection recipes.
The seed now names source state, route hints, projection execution, and
query-surface conversion. Next it should add generated source handles, local
cache keys, invalidation/watch behavior, and native-view output where
applicable. This turns Aetheria `ObjectsViewport`, `GravityViewport`,
selection, inventory, stats, station stock, and authority status into
portable CultMesh surfaces.
4. Add transparent state refs for UI runtimes.
Eve/CultUI surfaces may carry a typed `CultMeshStatePointer<T>` instead of a
JSON blob or string state ref. The UI runtime resolves and watches it using
the local Verse node, with Bifrost/Odin using the same pointer semantics for
MCP tools. State pointers now carry route hints and typed source metadata in
both C# and TypeScript, so a surface catalog can advertise where a pointer
resolves and which schema-backed document/source it depends on instead of
leaking daemon-specific resolver conventions. They also bind to a Verse like
operations, queries, and live feeds: `CultMesh.StatePointer(...)` /
`CultMesh.statePointer(...)` create pointers, and
`CultMesh.bindStatePointer(...)` / `verse.BindStatePointer(...)` produce a
bound pointer that resolves and watches through the Verse query context
instead of requiring UI or tool hosts to rebuild runtime-id and route
plumbing. Mutable document-style handles use the same shared shape:
`CultMeshMutableStatePointer<T>` / `CultMesh.mutableStatePointer(...)`
add typed replacement to the read/watch state pointer contract so tools,
editor surfaces, and authorized clients do not grow private
`Document<T>` wrappers. `CultMeshStateBindingDescriptor` / `CultMesh.stateBinding(...)`
now hoists the repeatable "component prop -> state pointer" shape into the
shared layer, including pointer id, source id, schema id, and route hint.
Eve documents, Unity inspectors, browser panels, and Bifrost tools should
carry this descriptor instead of inventing local `stateRef` string props.
`CultMeshOperationBindingDescriptor` / `CultMesh.operationBinding(...)`
applies the same rule to controls: a UI command advertises a typed operation
id, optional request schema, label, and route hint instead of making
`command` and `transport` strings the canonical API.
5. Add native slab adapters.
`CultMeshNativeSliceViewDescriptor` must map to Unity `NativeArray<T>`, C#
`ReadOnlySpan<T>`, Rust slices, and TS typed arrays where locality permits.
The descriptor names the column and value type; runtime adapters own handles,
fences, and unavoidable copy accounting.
6. Add authority-aware routing.
Operation/query invocation should accept authority claims and route hints,
then choose local, shared-memory, IPC, network, or WASM execution without
exposing a second public API for each route.
7. Add cross-runtime conformance tests.
The same primitive examples must pass in C# and TypeScript first, then Rust.
Aetheria client tests should assert against these shared primitives rather
than against private Unity/Electron bridges.
8. Harden durable cache ergonomics.
Directory-backed CultCache stores should behave like managed state, not
fragile manifest bookkeeping. A missing catalog entry in the hot manifest
must not strand a readable cold record when the payload carries a schema
version. The C# directory store now infers a minimal catalog entry from a
schema-stamped payload and resolves by schema name, with regression coverage
in `GameCult.Caching.Tests`. The TypeScript and Rust single-file MessagePack
stores now perform the same schema-stamped recovery at the snapshot boundary,
and both cache runtimes normalize recovered schema-name envelopes back to the
registered public document type before hydration. Python now mirrors the
same managed recovery path for stores and raw CultNet replication helpers,
and the TypeScript inspector reports recovered schema names instead of
leaking stale manifest ids. Keep this parity intact before relying on
long-lived cross-runtime Verse stores.
## Aetheria Pressure Tests
Aetheria should exercise the roadmap in this order:
1. Sector map and Starbridge session summary as typed state pointers.
2. Objects/gravity viewport split as typed query surfaces.
3. Selected object, inventory, station stock, and current stats as projection
recipes.
4. Pilot movement, targeting, equipment activation, docking, repair, cooling,
salvage, and commander verbs as typed operation handles.
5. Zone render and Ymir physics views as native slice descriptors.
6. Eve/CultUI panels that embed state pointers and operation handles instead of
local payload maps.
7. Unity and RTS clients consuming the same generated domain sugar.
## Stop Lines
- Do not add new public Aetheria command facades unless they can be expressed as
typed CultMesh operation handles.
- Do not add new viewport APIs unless they can be expressed as typed CultMesh
query surfaces or projection recipes.
- Do not add new UI state-ref strings when a typed state pointer would express
the same thing.
- Do not add native memory sharing as a Unity-only bridge; describe the slab in
CultMesh and let Unity be one adapter.
- Do not accept runtime parity by convention. C#, TypeScript, and Rust need
shared generated contracts or explicit conformance tests.