From 4f6e95bb3d04373ea57e31176a259cecfdf6d4f0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 22 Aug 2026 00:05:57 +0200 Subject: [PATCH] Carry editable field authority in CultMesh bindings --- packages/cultmesh-ts/src/index.ts | 97 +++++++++++++++++++ packages/cultmesh-ts/test/cultmesh.test.ts | 27 ++++++ src/GameCult.Mesh/CultMesh.cs | 31 +++++- src/GameCult.Mesh/CultMeshPrimitives.cs | 79 ++++++++++++++- .../CultMeshStreamingTests.cs | 31 ++++++ 5 files changed, 257 insertions(+), 8 deletions(-) diff --git a/packages/cultmesh-ts/src/index.ts b/packages/cultmesh-ts/src/index.ts index 76177269..ee1b4893 100644 --- a/packages/cultmesh-ts/src/index.ts +++ b/packages/cultmesh-ts/src/index.ts @@ -300,6 +300,13 @@ export interface CultMeshStateBindingDescriptor { readonly sourceId?: string; readonly schemaId?: string; readonly routeHint: CultMeshRouteHint; + readonly bindingName?: string; + readonly documentId?: string; + readonly fieldPath?: string; + readonly valueKind?: "string" | "number" | "boolean" | "choice" | "string-list"; + readonly accessMode?: "read" | "write" | "read-write" | "local-draft"; + readonly authority?: string; + readonly writeCommand?: string; } export interface CultMeshStateBindingRecord { @@ -309,6 +316,13 @@ export interface CultMeshStateBindingRecord { readonly schemaId: string; readonly routeKind: string; readonly routeDescription: string; + readonly bindingName: string; + readonly documentId: string; + readonly fieldPath: string; + readonly valueKind: string; + readonly accessMode: string; + readonly authority: string; + readonly writeCommand: string; } export interface CultMeshOperationBindingDescriptor { @@ -3351,6 +3365,15 @@ export function cultMeshDescribeStateRefResolver( }; } +function stateBindingMetadata( + options: Pick, +): Partial { + return Object.fromEntries( + Object.entries(options).filter(([, value]) => value !== undefined), + ) as Partial; +} + export function cultMeshStateBinding( targetProp: string, pointer: @@ -3364,6 +3387,13 @@ export function cultMeshStateBinding( sourceId?: string; schemaId?: string; routeHint?: CultMeshRouteHint; + bindingName?: string; + documentId?: string; + fieldPath?: string; + valueKind?: CultMeshStateBindingDescriptor["valueKind"]; + accessMode?: CultMeshStateBindingDescriptor["accessMode"]; + authority?: string; + writeCommand?: string; } = {}, ): CultMeshStateBindingDescriptor { requireNonEmpty(targetProp, "targetProp"); @@ -3375,6 +3405,7 @@ export function cultMeshStateBinding( sourceId: options.sourceId, schemaId: options.schemaId, routeHint: options.routeHint ?? cultMeshRouteHint(), + ...stateBindingMetadata(options), }; } @@ -3386,6 +3417,7 @@ export function cultMeshStateBinding( sourceId: options.sourceId ?? source?.sourceId, schemaId: options.schemaId ?? source?.schemaId, routeHint: options.routeHint ?? pointer.routeHint ?? cultMeshRouteHint(), + ...stateBindingMetadata(options), }; } @@ -3399,6 +3431,13 @@ export function cultMeshStateBindingRecord( schemaId?: string, routeKind?: string, routeDescription?: string, + bindingName?: string, + documentId?: string, + fieldPath?: string, + valueKind?: string, + accessMode?: string, + authority?: string, + writeCommand?: string, ): CultMeshStateBindingRecord; export function cultMeshStateBindingRecord( bindingOrTargetProp?: Partial | string, @@ -3407,6 +3446,13 @@ export function cultMeshStateBindingRecord( schemaId?: string, routeKind?: string, routeDescription?: string, + bindingName?: string, + documentId?: string, + fieldPath?: string, + valueKind?: string, + accessMode?: string, + authority?: string, + writeCommand?: string, ): CultMeshStateBindingRecord { if (typeof bindingOrTargetProp === "object" || bindingOrTargetProp === undefined) { const binding = bindingOrTargetProp; @@ -3418,6 +3464,13 @@ export function cultMeshStateBindingRecord( schemaId: binding?.schemaId ?? "", routeKind: route.kind, routeDescription: route.description, + bindingName: binding?.bindingName ?? "", + documentId: binding?.documentId ?? "", + fieldPath: binding?.fieldPath ?? "", + valueKind: binding?.valueKind ?? "", + accessMode: binding?.accessMode ?? "", + authority: binding?.authority ?? "", + writeCommand: binding?.writeCommand ?? "", }; } @@ -3428,6 +3481,13 @@ export function cultMeshStateBindingRecord( schemaId: schemaId ?? "", routeKind: routeKind ?? "", routeDescription: routeDescription ?? "", + bindingName: bindingName ?? "", + documentId: documentId ?? "", + fieldPath: fieldPath ?? "", + valueKind: valueKind ?? "", + accessMode: accessMode ?? "", + authority: authority ?? "", + writeCommand: writeCommand ?? "", }; } @@ -3451,6 +3511,15 @@ export function cultMeshStateBindingFromRecord( }, options.fallbackRouteHint, ), + ...stateBindingMetadata({ + bindingName: record.bindingName || undefined, + documentId: record.documentId || undefined, + fieldPath: record.fieldPath || undefined, + valueKind: (record.valueKind || undefined) as CultMeshStateBindingDescriptor["valueKind"], + accessMode: (record.accessMode || undefined) as CultMeshStateBindingDescriptor["accessMode"], + authority: record.authority || undefined, + writeCommand: record.writeCommand || undefined, + }), }; } @@ -5278,6 +5347,13 @@ export class CultMesh { sourceId?: string; schemaId?: string; routeHint?: CultMeshRouteHint; + bindingName?: string; + documentId?: string; + fieldPath?: string; + valueKind?: CultMeshStateBindingDescriptor["valueKind"]; + accessMode?: CultMeshStateBindingDescriptor["accessMode"]; + authority?: string; + writeCommand?: string; } = {}, ): CultMeshStateBindingDescriptor { return cultMeshStateBinding(targetProp, pointer, options); @@ -5293,6 +5369,13 @@ export class CultMesh { schemaId?: string, routeKind?: string, routeDescription?: string, + bindingName?: string, + documentId?: string, + fieldPath?: string, + valueKind?: string, + accessMode?: string, + authority?: string, + writeCommand?: string, ): CultMeshStateBindingRecord; public static stateBindingRecord( bindingOrTargetProp?: Partial | string, @@ -5301,6 +5384,13 @@ export class CultMesh { schemaId?: string, routeKind?: string, routeDescription?: string, + bindingName?: string, + documentId?: string, + fieldPath?: string, + valueKind?: string, + accessMode?: string, + authority?: string, + writeCommand?: string, ): CultMeshStateBindingRecord { return typeof bindingOrTargetProp === "object" || bindingOrTargetProp === undefined ? cultMeshStateBindingRecord(bindingOrTargetProp) @@ -5311,6 +5401,13 @@ export class CultMesh { schemaId, routeKind, routeDescription, + bindingName, + documentId, + fieldPath, + valueKind, + accessMode, + authority, + writeCommand, ); } diff --git a/packages/cultmesh-ts/test/cultmesh.test.ts b/packages/cultmesh-ts/test/cultmesh.test.ts index 653a1e95..ab1bc133 100644 --- a/packages/cultmesh-ts/test/cultmesh.test.ts +++ b/packages/cultmesh-ts/test/cultmesh.test.ts @@ -2147,6 +2147,13 @@ test("CultMesh TS flattens and rehydrates UI state binding records", () => { schemaId: "gamecult.aetheria.daemon_frame.v1", routeKind: "shared-memory", routeDescription: "co-located frame slab", + bindingName: "", + documentId: "", + fieldPath: "", + valueKind: "", + accessMode: "", + authority: "", + writeCommand: "", }); assert.deepEqual(CultMesh.stateBindingFromRecord({ @@ -2168,6 +2175,26 @@ test("CultMesh TS flattens and rehydrates UI state binding records", () => { }); }); +test("CultMesh TS preserves editable binding identity and authority through transport records", () => { + const binding = CultMesh.stateBinding("value", "ghostlight.session-zero.composer", { + sourceId: "session-zero:draft-7", + schemaId: "session_zero.v1", + routeHint: CultMesh.routeHint("network", "Ghostlight provider"), + bindingName: "composer.message", + documentId: "session-zero:draft-7", + fieldPath: "channels.private.messages.pending", + valueKind: "string", + accessMode: "local-draft", + authority: "SessionZeroKernel", + writeCommand: "edit.value", + }); + const record = CultMesh.stateBindingRecord(binding); + assert.equal(record.bindingName, "composer.message"); + assert.equal(record.fieldPath, "channels.private.messages.pending"); + assert.equal(record.accessMode, "local-draft"); + assert.deepEqual(CultMesh.stateBindingFromRecord(record), binding); +}); + test("CultMesh TS describes UI command bindings from typed operations", async () => { const operation = CultMesh.operation< { actor: string }, diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs index 232f847e..6dd47f8c 100644 --- a/src/GameCult.Mesh/CultMesh.cs +++ b/src/GameCult.Mesh/CultMesh.cs @@ -1117,9 +1117,18 @@ namespace GameCult.Mesh string pointerId, string? sourceId = null, string? schemaId = null, - CultMeshRouteHint? routeHint = null) + CultMeshRouteHint? routeHint = null, + string? bindingName = null, + string? documentId = null, + string? fieldPath = null, + string? valueKind = null, + string? accessMode = null, + string? authority = null, + string? writeCommand = null) { - return new CultMeshStateBindingDescriptor(targetProp, pointerId, sourceId, schemaId, routeHint); + return new CultMeshStateBindingDescriptor( + targetProp, pointerId, sourceId, schemaId, routeHint, + bindingName, documentId, fieldPath, valueKind, accessMode, authority, writeCommand); } /// @@ -1139,7 +1148,14 @@ namespace GameCult.Mesh string? sourceId = null, string? schemaId = null, string? routeKind = null, - string? routeDescription = null) + string? routeDescription = null, + string? bindingName = null, + string? documentId = null, + string? fieldPath = null, + string? valueKind = null, + string? accessMode = null, + string? authority = null, + string? writeCommand = null) { return new CultMeshStateBindingRecord( targetProp, @@ -1147,7 +1163,14 @@ namespace GameCult.Mesh sourceId, schemaId, routeKind, - routeDescription); + routeDescription, + bindingName, + documentId, + fieldPath, + valueKind, + accessMode, + authority, + writeCommand); } /// diff --git a/src/GameCult.Mesh/CultMeshPrimitives.cs b/src/GameCult.Mesh/CultMeshPrimitives.cs index d136595f..a034e6e6 100644 --- a/src/GameCult.Mesh/CultMeshPrimitives.cs +++ b/src/GameCult.Mesh/CultMeshPrimitives.cs @@ -3329,13 +3329,27 @@ namespace GameCult.Mesh string pointerId, string? sourceId = null, string? schemaId = null, - CultMeshRouteHint? routeHint = null) + CultMeshRouteHint? routeHint = null, + string? bindingName = null, + string? documentId = null, + string? fieldPath = null, + string? valueKind = null, + string? accessMode = null, + string? authority = null, + string? writeCommand = null) { TargetProp = RequireNonEmpty(targetProp, nameof(targetProp)); PointerId = RequireNonEmpty(pointerId, nameof(pointerId)); SourceId = sourceId; SchemaId = schemaId; RouteHint = routeHint ?? CultMeshRouteHint.Automatic; + BindingName = bindingName ?? ""; + DocumentId = documentId ?? ""; + FieldPath = fieldPath ?? ""; + ValueKind = valueKind ?? ""; + AccessMode = accessMode ?? ""; + Authority = authority ?? ""; + WriteCommand = writeCommand ?? ""; } /// Gets the component property receiving the resolved state value. @@ -3353,6 +3367,27 @@ namespace GameCult.Mesh /// Gets the preferred route for resolving and watching the binding. public CultMeshRouteHint RouteHint { get; } + /// Gets the stable renderer-facing name used to capture this binding in an operation. + public string BindingName { get; } + + /// Gets the optional identity of the provider-owned backing document. + public string DocumentId { get; } + + /// Gets the optional typed field path inside the backing document. + public string FieldPath { get; } + + /// Gets the optional portable value kind. + public string ValueKind { get; } + + /// Gets the optional read, write, read-write, or local-draft access mode. + public string AccessMode { get; } + + /// Gets the authority responsible for accepting writes to this binding. + public string Authority { get; } + + /// Gets the optional typed operation id used for direct writes. + public string WriteCommand { get; } + /// Creates a binding from an existing state pointer. public static CultMeshStateBindingDescriptor FromPointer( string targetProp, @@ -3390,7 +3425,14 @@ namespace GameCult.Mesh string? sourceId = null, string? schemaId = null, string? routeKind = null, - string? routeDescription = null) + string? routeDescription = null, + string? bindingName = null, + string? documentId = null, + string? fieldPath = null, + string? valueKind = null, + string? accessMode = null, + string? authority = null, + string? writeCommand = null) { TargetProp = string.IsNullOrWhiteSpace(targetProp) ? "value" : targetProp!; PointerId = pointerId ?? ""; @@ -3398,6 +3440,13 @@ namespace GameCult.Mesh SchemaId = schemaId ?? ""; RouteKind = routeKind ?? ""; RouteDescription = routeDescription ?? ""; + BindingName = bindingName ?? ""; + DocumentId = documentId ?? ""; + FieldPath = fieldPath ?? ""; + ValueKind = valueKind ?? ""; + AccessMode = accessMode ?? ""; + Authority = authority ?? ""; + WriteCommand = writeCommand ?? ""; } /// Gets the component property receiving the resolved state value. @@ -3418,6 +3467,14 @@ namespace GameCult.Mesh /// Gets the flattened route description. [Key(5)] public string RouteDescription { get; } + [Key(6)] public string BindingName { get; } + [Key(7)] public string DocumentId { get; } + [Key(8)] public string FieldPath { get; } + [Key(9)] public string ValueKind { get; } + [Key(10)] public string AccessMode { get; } + [Key(11)] public string Authority { get; } + [Key(12)] public string WriteCommand { get; } + /// Creates transport-friendly fields from a state binding descriptor. public static CultMeshStateBindingRecord FromBinding(CultMeshStateBindingDescriptor? binding) { @@ -3428,7 +3485,14 @@ namespace GameCult.Mesh binding?.SourceId, binding?.SchemaId, route.Kind, - route.Description); + route.Description, + binding?.BindingName, + binding?.DocumentId, + binding?.FieldPath, + binding?.ValueKind, + binding?.AccessMode, + binding?.Authority, + binding?.WriteCommand); } /// Rehydrates this record as a state binding descriptor. @@ -3450,7 +3514,14 @@ namespace GameCult.Mesh pointerId, SourceId, SchemaId, - route); + route, + BindingName, + DocumentId, + FieldPath, + ValueKind, + AccessMode, + Authority, + WriteCommand); } } diff --git a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs index 273d1e87..8ebccf57 100644 --- a/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs +++ b/tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs @@ -298,6 +298,37 @@ public sealed class CultMeshStreamingTests fromTypescript.RouteHint.Description.Should().Be("browser-adjacent cache"); } + [Test] + public void StateBindingRecord_PreservesEditableFieldIdentityAndAuthority() + { + var binding = CultMesh.StateBinding( + "value", + "ghostlight.session-zero.composer", + sourceId: "session-zero:draft-7", + schemaId: "session_zero.v1", + routeHint: new CultMeshRouteHint(CultMeshLocalityKind.Network, "Ghostlight provider"), + bindingName: "composer.message", + documentId: "session-zero:draft-7", + fieldPath: "channels.private.messages.pending", + valueKind: "string", + accessMode: "local-draft", + authority: "SessionZeroKernel", + writeCommand: "edit.value"); + + var record = CultMesh.StateBindingRecord(binding); + record.BindingName.Should().Be("composer.message"); + record.FieldPath.Should().Be("channels.private.messages.pending"); + record.AccessMode.Should().Be("local-draft"); + var restored = record.ToBinding(); + restored.BindingName.Should().Be(binding.BindingName); + restored.DocumentId.Should().Be(binding.DocumentId); + restored.FieldPath.Should().Be(binding.FieldPath); + restored.ValueKind.Should().Be(binding.ValueKind); + restored.AccessMode.Should().Be(binding.AccessMode); + restored.Authority.Should().Be(binding.Authority); + restored.WriteCommand.Should().Be(binding.WriteCommand); + } + [Test] public void OperationBindingDescriptor_BindsUiCommandsToTypedOperations() {