Validate Eve getting-started contracts
This commit is contained in:
parent
1fc68a4701
commit
54e4eacd13
7 changed files with 143 additions and 32 deletions
10
README.md
10
README.md
|
|
@ -62,8 +62,14 @@ the stack. If a design needs another peer-to-peer category, first check whether
|
|||
it is really Verse policy, peer exchange, or shard authority wearing a fake
|
||||
mustache.
|
||||
|
||||
If you want the smallest durable "open node -> put typed doc -> get typed doc"
|
||||
path, start with
|
||||
If you want to build an application rather than tour the component drawer,
|
||||
start with the maintained
|
||||
[`CultMesh And Eve Getting Started`](src/GameCult.Mesh/docs/getting-started/README.md)
|
||||
series. Its first executable package-consumer checkpoint is
|
||||
[`samples/eve-two-runtime`](samples/eve-two-runtime/README.md).
|
||||
|
||||
If you want only the smallest durable "open node -> put typed doc -> get typed doc"
|
||||
path, use
|
||||
[`src/GameCult.Mesh/docs/durable-node-quickstart.md`](src/GameCult.Mesh/docs/durable-node-quickstart.md).
|
||||
If you want the lower-level "typed local document -> raw wire document ->
|
||||
mesh/runtime watch surface" handoff, follow it with
|
||||
|
|
|
|||
32
samples/eve-two-runtime/README.md
Normal file
32
samples/eve-two-runtime/README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Eve Two-Consumer Counter
|
||||
|
||||
This is the smallest executable package-consumer checkpoint for CultMesh plus
|
||||
Eve. It intentionally has one owner for each decision:
|
||||
|
||||
- the provider node owns the counter document and command transaction;
|
||||
- Eve owns the validated surface and command-receipt contracts;
|
||||
- CultMesh owns typed observation and the operation boundary;
|
||||
- the DOM lowerer owns HTML only;
|
||||
- the headless observer owns no state.
|
||||
|
||||
Run it from the CultLib repository:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\verify-eve-two-runtime-sample.ps1 `
|
||||
-EveRoot ..\Eve
|
||||
```
|
||||
|
||||
The verifier builds package artifacts, installs them into a new empty temporary
|
||||
consumer, and runs `sample.mjs`. Success proves:
|
||||
|
||||
1. no source-tree import is required by the consumer;
|
||||
2. the provider surface and receipt pass Eve's generated runtime validators;
|
||||
3. a DOM click executes one idempotent provider operation;
|
||||
4. the DOM binding and a separate headless observer converge on the same count;
|
||||
5. the canonical receipt and state survive reopening the `.cc` store.
|
||||
|
||||
This is not yet the promised networked multiplayer tutorial. The DOM lowerer is
|
||||
hosted by jsdom in Node and both consumers share one local CultMesh node. A real
|
||||
browser transport, Odin discovery, process separation, and a second language
|
||||
runtime require distinct executable gates. Keeping that limitation visible is
|
||||
part of the sample contract.
|
||||
|
|
@ -6,6 +6,10 @@ import { JSDOM } from "jsdom";
|
|||
import { z } from "zod";
|
||||
import { defineDocumentType } from "cultcache-ts";
|
||||
import { CultMesh } from "cultmesh-ts";
|
||||
import {
|
||||
parseEveCommandReceipt,
|
||||
parseEveSurfaceDocument,
|
||||
} from "@gamecult/eve-contracts";
|
||||
import { renderEveSurface } from "@gamecult/eve-browser-lowering";
|
||||
|
||||
const providerId = "sample.counter-provider";
|
||||
|
|
@ -22,36 +26,38 @@ const counterDocument = defineDocumentType({
|
|||
const surfaceDocument = defineDocumentType({
|
||||
type: "gamecult.eve.surface",
|
||||
schemaId: "gamecult.eve.surface.v1",
|
||||
schema: z.any(),
|
||||
schema: z.unknown().transform(value => parseEveSurfaceDocument(value)),
|
||||
});
|
||||
|
||||
const receiptDocument = defineDocumentType({
|
||||
type: "sample.counter_receipt",
|
||||
schemaId: "sample.counter_receipt.v1",
|
||||
schema: z.object({
|
||||
receiptId: z.string(),
|
||||
operationId: z.literal("sample.counter.increment"),
|
||||
idempotencyKey: z.string(),
|
||||
count: z.number().int(),
|
||||
}),
|
||||
type: "gamecult.eve.command_receipt",
|
||||
schemaId: "gamecult.eve.command_receipt.v1",
|
||||
schema: z.unknown().transform(value => parseEveCommandReceipt(value)),
|
||||
name: "receiptId",
|
||||
});
|
||||
|
||||
function counterSurface() {
|
||||
return {
|
||||
type: "surface-state",
|
||||
schema: "gamecult.eve.surface.v1",
|
||||
providerId,
|
||||
providerKind: "sample.daemon",
|
||||
title: "CultMesh two-runtime counter",
|
||||
version: 1,
|
||||
updatedAtUtc: "2026-08-17T00:00:00Z",
|
||||
surface: {
|
||||
id: surfaceId,
|
||||
title: "Counter",
|
||||
root: {
|
||||
id: "counter.root",
|
||||
kind: "column",
|
||||
props: {},
|
||||
children: [
|
||||
{
|
||||
id: "counter.value",
|
||||
kind: "metric",
|
||||
props: { label: "Canonical count", value: 0 },
|
||||
children: [],
|
||||
stateBindings: [{
|
||||
targetProp: "value",
|
||||
pointerId: "sample.counter.count",
|
||||
|
|
@ -64,10 +70,21 @@ function counterSurface() {
|
|||
id: "counter.increment",
|
||||
kind: "control.button",
|
||||
props: { label: "Increment", command: "sample.counter.increment" },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
styles: {},
|
||||
},
|
||||
commands: [{
|
||||
schema: "gamecult.eve.command.v1",
|
||||
command: "sample.counter.increment",
|
||||
label: "Increment",
|
||||
surfaceId,
|
||||
transport: "cultmesh",
|
||||
authority: "provider-daemon",
|
||||
result: "gamecult.eve.command_receipt.v1",
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +114,16 @@ async function waitFor(predicate, description) {
|
|||
}
|
||||
}
|
||||
|
||||
const statePath = join(await mkdtemp(join(tmpdir(), "cultmesh-eve-two-runtime-")), "state.ccmp");
|
||||
assert.throws(
|
||||
() => parseEveSurfaceDocument({ providerId, surface: { id: surfaceId } }),
|
||||
/Invalid Eve surface/,
|
||||
);
|
||||
assert.throws(
|
||||
() => parseEveCommandReceipt({ receiptId: "unowned" }),
|
||||
/Invalid Eve commandReceipt/,
|
||||
);
|
||||
|
||||
const statePath = join(await mkdtemp(join(tmpdir(), "cultmesh-eve-two-runtime-")), "state.cc");
|
||||
const node = await CultMesh.startNode(statePath, {
|
||||
documents: [counterDocument, surfaceDocument, receiptDocument],
|
||||
});
|
||||
|
|
@ -120,7 +146,16 @@ const increment = CultMesh.operation("sample.counter.increment", async (_request
|
|||
}));
|
||||
const receipt = {
|
||||
receiptId: `receipt:${idempotencyKey}`,
|
||||
operationId: "sample.counter.increment",
|
||||
schema: "gamecult.eve.command_receipt.v1",
|
||||
commandId: idempotencyKey,
|
||||
command: "sample.counter.increment",
|
||||
state: "accepted",
|
||||
ownerRepo: "CultLib",
|
||||
authority: "provider-daemon",
|
||||
providerId,
|
||||
surfaceId,
|
||||
issuedAtUtc: new Date().toISOString(),
|
||||
sourceVersion: next.count,
|
||||
idempotencyKey,
|
||||
count: next.count,
|
||||
};
|
||||
|
|
@ -162,7 +197,21 @@ assert.equal(duplicate.receiptId, browserReceipt.receiptId);
|
|||
assert.equal(headless.current.count, 1);
|
||||
|
||||
headless.dispose();
|
||||
renderEveSurface({ providerId, surface: { id: "sample.closed" } }, host);
|
||||
renderEveSurface(parseEveSurfaceDocument({
|
||||
type: "surface-state",
|
||||
schema: "gamecult.eve.surface.v1",
|
||||
providerId,
|
||||
providerKind: "sample.daemon",
|
||||
title: "Closed counter",
|
||||
version: 2,
|
||||
updatedAtUtc: "2026-08-17T00:00:01Z",
|
||||
surface: {
|
||||
id: "sample.closed",
|
||||
root: { id: "closed.root", kind: "column", props: {}, children: [] },
|
||||
styles: {},
|
||||
},
|
||||
commands: [],
|
||||
}), host);
|
||||
dom.window.close();
|
||||
await node.flush();
|
||||
const reopened = await CultMesh.startNode(statePath, {
|
||||
|
|
@ -178,6 +227,7 @@ console.log(JSON.stringify({
|
|||
providerId,
|
||||
surfaceId,
|
||||
receiptId: browserReceipt.receiptId,
|
||||
loweringRuntime: "jsdom",
|
||||
browserCount: 1,
|
||||
headlessCount: 1,
|
||||
restartCount: 1,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ $ErrorActionPreference = "Stop"
|
|||
$cultLibRoot = Split-Path -Parent $PSScriptRoot
|
||||
$samplePath = Join-Path $cultLibRoot "samples\eve-two-runtime\sample.mjs"
|
||||
$eveBrowserPackage = Join-Path $EveRoot "packages\eve-browser-lowering"
|
||||
$eveContractsPackage = Join-Path $EveRoot "packages\eve-contracts"
|
||||
$packageRoots = @(
|
||||
(Join-Path $cultLibRoot "packages\cultcache-ts"),
|
||||
(Join-Path $cultLibRoot "packages\cultnet-ts"),
|
||||
(Join-Path $cultLibRoot "packages\cultmesh-ts"),
|
||||
$eveContractsPackage,
|
||||
$eveBrowserPackage
|
||||
)
|
||||
|
||||
|
|
@ -56,7 +58,7 @@ try {
|
|||
throw "CultNet TypeScript contract generation failed: $packageRoot"
|
||||
}
|
||||
}
|
||||
$typescriptPath = if ($packageRoot -eq $eveBrowserPackage) {
|
||||
$typescriptPath = if ($packageRoot -eq $eveBrowserPackage -or $packageRoot -eq $eveContractsPackage) {
|
||||
Join-Path $packageRoot "node_modules\typescript\bin\tsc"
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -12,15 +12,20 @@ using var mesh = new CultMeshClient(new CultMeshClientOptions
|
|||
RendezvousEndpoints = new[] { "rudp://odin.gamecult.net:3076" }
|
||||
});
|
||||
|
||||
using var pilotSurfaceLease = await mesh.LeaseDocumentAsync<EveSurfaceDocument>(
|
||||
"aetheria.daemon",
|
||||
"eve:surface:aetheria.pilot",
|
||||
using var counterLease = await mesh.LeaseDocumentAsync<CounterState>(
|
||||
"sample.counter-provider",
|
||||
"counter:main",
|
||||
cancellationToken);
|
||||
var pilotSurface = pilotSurfaceLease.Handle;
|
||||
var counter = counterLease.Handle;
|
||||
|
||||
using var surfaceWatch = pilotSurface.Watch().Subscribe(Render);
|
||||
using var counterWatch = counter.Watch(value =>
|
||||
Console.WriteLine($"Count: {value.Count}"));
|
||||
```
|
||||
|
||||
`CounterState` is the application document from chapter 1. Eve documents enter
|
||||
in chapter 3, where the sample installs the renderer-neutral Eve contract
|
||||
package explicitly. This identity/session API does not depend on a UI runtime.
|
||||
|
||||
Keep `mesh` for the application lifetime and dispose each document lease when
|
||||
the consuming screen or system closes. Leases for the same identity share one
|
||||
subscription. The document handle is stable while leased: when
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Run the same provider and connect at least two lowerers. Both must report the
|
|||
same provider id, surface id, surface version, and command receipt ids. Native
|
||||
pixels may differ; provider truth may not.
|
||||
|
||||
The package-artifact browser/headless checkpoint is executable from CultLib:
|
||||
The package-artifact DOM-lowerer/headless checkpoint is executable from CultLib:
|
||||
|
||||
```powershell
|
||||
cd CultLib
|
||||
|
|
@ -43,14 +43,16 @@ powershell -ExecutionPolicy Bypass -File .\scripts\verify-eve-two-runtime-sample
|
|||
-EveRoot ..\Eve
|
||||
```
|
||||
|
||||
The verifier builds and packs `cultcache-ts`, `cultnet-ts`, `cultmesh-ts`, and
|
||||
`@gamecult/eve-browser-lowering`, installs the tarballs into an empty temporary
|
||||
consumer, and runs `samples/eve-two-runtime/sample.mjs`. One provider-owned
|
||||
counter surface is lowered by a browser runtime and observed by a headless
|
||||
runtime. A browser command crosses the typed operation boundary, both runtimes
|
||||
converge on the same state and receipt identity, a duplicate idempotency key is
|
||||
not applied twice, and the state plus receipt survive reopening the `.ccmp`
|
||||
store.
|
||||
The verifier builds and packs `cultcache-ts`, `cultnet-ts`, `cultmesh-ts`,
|
||||
`@gamecult/eve-contracts`, and `@gamecult/eve-browser-lowering`, installs the
|
||||
tarballs into an empty temporary consumer, and runs
|
||||
`samples/eve-two-runtime/sample.mjs`. Eve's generated runtime validators reject
|
||||
an invalid surface or receipt before either enters the store. One provider-owned
|
||||
counter surface is lowered into a jsdom DOM host and observed by an independent
|
||||
headless CultMesh observer. A DOM command crosses the typed operation boundary,
|
||||
both consumers converge on the same state and canonical receipt identity, a
|
||||
duplicate idempotency key is not applied twice, and the state plus receipt
|
||||
survive reopening the `.cc` store.
|
||||
|
||||
Current verification for the Unity consumer remains:
|
||||
|
||||
|
|
@ -59,7 +61,9 @@ cd E:\Projects\EveUnity
|
|||
powershell -ExecutionPolicy Bypass -File .\scripts\run-release-consumer-tests.ps1
|
||||
```
|
||||
|
||||
This checkpoint proves clean package consumption and two lowering/observation
|
||||
runtimes. It does not claim an Odin-discovered network hop or cross-language
|
||||
command execution; those remain separate public Verse conformance gates.
|
||||
This checkpoint proves clean package consumption, canonical Eve contract
|
||||
validation, and two independent lowering/observation consumers inside Node. It
|
||||
does **not** claim a real browser process, an Odin-discovered network hop, or
|
||||
cross-language command execution. Those are the next public Verse conformance
|
||||
gates; do not cite this sample as evidence that they exist.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,18 @@ This series builds one typed application state and presents it through multiple
|
|||
Eve runtimes. It is maintained as an API acceptance test: examples use public
|
||||
APIs, name the owner of each decision, and include a verification checkpoint.
|
||||
|
||||
Run the current clean-consumer checkpoint first:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\verify-eve-two-runtime-sample.ps1 `
|
||||
-EveRoot ..\Eve
|
||||
```
|
||||
|
||||
It installs packed artifacts into an empty temporary project, validates the Eve
|
||||
surface and receipt, executes one idempotent counter command, proves two local
|
||||
consumers converge, and reopens the durable `.cc` store. It is deliberately not
|
||||
called a networked-browser proof; chapter 3 names that remaining boundary.
|
||||
|
||||
Follow the chapters in order:
|
||||
|
||||
1. [Persist typed application state](01-persist-typed-state.md)
|
||||
|
|
|
|||
Loading…
Reference in a new issue