diff --git a/README.md b/README.md index d6e3778c..013954e5 100644 --- a/README.md +++ b/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 diff --git a/samples/eve-two-runtime/README.md b/samples/eve-two-runtime/README.md new file mode 100644 index 00000000..9e9f1656 --- /dev/null +++ b/samples/eve-two-runtime/README.md @@ -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. diff --git a/samples/eve-two-runtime/sample.mjs b/samples/eve-two-runtime/sample.mjs index a78b765f..e3269618 100644 --- a/samples/eve-two-runtime/sample.mjs +++ b/samples/eve-two-runtime/sample.mjs @@ -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, diff --git a/scripts/verify-eve-two-runtime-sample.ps1 b/scripts/verify-eve-two-runtime-sample.ps1 index f5545d5f..f6d4ebce 100644 --- a/scripts/verify-eve-two-runtime-sample.ps1 +++ b/scripts/verify-eve-two-runtime-sample.ps1 @@ -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 { diff --git a/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md b/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md index dd7a1abd..b7c636d4 100644 --- a/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md +++ b/src/GameCult.Mesh/docs/getting-started/02-connect-by-identity.md @@ -12,15 +12,20 @@ using var mesh = new CultMeshClient(new CultMeshClientOptions RendezvousEndpoints = new[] { "rudp://odin.gamecult.net:3076" } }); -using var pilotSurfaceLease = await mesh.LeaseDocumentAsync( - "aetheria.daemon", - "eve:surface:aetheria.pilot", +using var counterLease = await mesh.LeaseDocumentAsync( + "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 diff --git a/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md b/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md index 95048d3a..d28ef621 100644 --- a/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md +++ b/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md @@ -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. diff --git a/src/GameCult.Mesh/docs/getting-started/README.md b/src/GameCult.Mesh/docs/getting-started/README.md index 7914247c..d5e91d74 100644 --- a/src/GameCult.Mesh/docs/getting-started/README.md +++ b/src/GameCult.Mesh/docs/getting-started/README.md @@ -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)