Add CultNet simulation observation wire contracts
This commit is contained in:
parent
75ebc727f2
commit
f99edc5c43
6 changed files with 192 additions and 8 deletions
|
|
@ -174,6 +174,7 @@ Shard catalog exchange now exists:
|
|||
- client authority scopes for predicted local input documents
|
||||
- predicted/reconciled change events for client-side prediction
|
||||
- simulation witness observations and deterministic consensus candidates
|
||||
- schema-v0 simulation observation and candidate messages
|
||||
|
||||
## Next Cut
|
||||
|
||||
|
|
@ -184,7 +185,7 @@ Build durable authoritative mutation logs:
|
|||
- return explicit resync-required responses when requested history is gone
|
||||
- then add simulation-frame rollback and resimulation helpers on top of
|
||||
predicted input streams
|
||||
- add observation gossip once the local witness model has earned its shape
|
||||
- add an observation hub that receives gossip and emits consensus candidates
|
||||
|
||||
That is the next foundation needed before membership, Raft-style failover, or
|
||||
CRDT policy work can be honest.
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ Implemented:
|
|||
- predicted and reconciled database change kinds for client-side prediction
|
||||
- simulation witness observations for mesh-side opinions about frame facts
|
||||
- deterministic consensus candidates derived from witness observations
|
||||
- schema-v0 simulation observation and consensus candidate messages
|
||||
|
||||
Not implemented yet:
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ Not implemented yet:
|
|||
- leader election or automatic shard failover
|
||||
- durable log persistence and compaction
|
||||
- snapshot fallback for compacted log history
|
||||
- transport messages/documents for witness observation gossip
|
||||
- observation hub/fanout that receives gossip and emits candidates reactively
|
||||
- rollback/resimulation helpers for simulation frames
|
||||
- declared CRDT merge policies
|
||||
|
||||
|
|
@ -105,14 +106,15 @@ Add durable mutation-log storage and snapshot fallback:
|
|||
- return `ResyncRequired` when a replica asks for compacted history
|
||||
- after that, add simulation-frame rollback/resimulation helpers around
|
||||
predicted input streams
|
||||
- add schema-v0 observation gossip once the local consensus document shape is
|
||||
stable
|
||||
- add an observation hub that receives schema-v0 observation messages and emits
|
||||
consensus candidates reactively
|
||||
|
||||
The log is wire-readable, replicas can apply it explicitly, a pull loop can
|
||||
drive non-primary shards from primary endpoints, and replica cursors can survive
|
||||
restart. Client-owned input documents can now be predicted locally and
|
||||
reconciled when the authoritative log arrives. Nodes can also aggregate witness
|
||||
observations into deterministic consensus candidates for simulation facts like
|
||||
"who shot first." The next useful layer is making authoritative mutation logs
|
||||
durable and compaction-aware. Membership and leader election should wait until
|
||||
basic replica catch-up is boring.
|
||||
"who shot first," and those reports now have schema-v0 wire contracts. The next
|
||||
useful layer is making authoritative mutation logs durable and compaction-aware.
|
||||
Membership and leader election should wait until basic replica catch-up is
|
||||
boring.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ namespace GameCult.Networking
|
|||
CultNetSchemaVersions.ShardCatalogResponse => Deserialize<CultNetShardCatalogResponseMessage>(payload),
|
||||
CultNetSchemaVersions.ShardLogRequest => Deserialize<CultNetShardLogRequestMessage>(payload),
|
||||
CultNetSchemaVersions.ShardLogResponse => Deserialize<CultNetShardLogResponseMessage>(payload),
|
||||
CultNetSchemaVersions.SimulationObservation => Deserialize<CultNetSimulationObservationMessage>(payload),
|
||||
CultNetSchemaVersions.SimulationConsensusCandidate => Deserialize<CultNetSimulationConsensusCandidateMessage>(payload),
|
||||
CultNetSchemaVersions.SchemaCatalogRequest => Deserialize<CultNetSchemaCatalogRequestMessage>(payload),
|
||||
CultNetSchemaVersions.SchemaCatalogResponse => Deserialize<CultNetSchemaCatalogResponseMessage>(payload),
|
||||
_ => throw new MessagePackSerializationException(
|
||||
|
|
|
|||
|
|
@ -107,6 +107,14 @@ namespace GameCult.Networking
|
|||
/// shard mutation log response contract identifier.
|
||||
/// </summary>
|
||||
public const string ShardLogResponse = "cultnet.shard_log_response.v0";
|
||||
/// <summary>
|
||||
/// simulation observation gossip contract identifier.
|
||||
/// </summary>
|
||||
public const string SimulationObservation = "cultnet.simulation_observation.v0";
|
||||
/// <summary>
|
||||
/// simulation consensus candidate gossip contract identifier.
|
||||
/// </summary>
|
||||
public const string SimulationConsensusCandidate = "cultnet.simulation_consensus_candidate.v0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -815,6 +823,116 @@ namespace GameCult.Networking
|
|||
[Key("delete")] public CultNetDocumentDeleteMessage? Delete { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carries one witness observation about a simulation fact.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSimulationObservationMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SimulationObservation;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the observation.
|
||||
/// </summary>
|
||||
[Key("observation")] public CultNetSimulationObservation Observation { get; set; } = new CultNetSimulationObservation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carries one deterministic consensus candidate derived from witness observations.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class CultNetSimulationConsensusCandidateMessage : ICultNetSchemaMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the schema version.
|
||||
/// </summary>
|
||||
[Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.SimulationConsensusCandidate;
|
||||
/// <summary>
|
||||
/// Gets or sets the message id.
|
||||
/// </summary>
|
||||
[Key("messageId")] public string MessageId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the shard id.
|
||||
/// </summary>
|
||||
[Key("shardId")] public string ShardId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the shard epoch.
|
||||
/// </summary>
|
||||
[Key("shardEpoch")] public long ShardEpoch { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the simulation frame.
|
||||
/// </summary>
|
||||
[Key("frame")] public long Frame { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the subject id.
|
||||
/// </summary>
|
||||
[Key("subjectId")] public string SubjectId { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the claim kind.
|
||||
/// </summary>
|
||||
[Key("claimKind")] public string ClaimKind { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets the selected claim hash.
|
||||
/// </summary>
|
||||
[Key("claimHash")] public string ClaimHash { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Gets or sets optional claim detail.
|
||||
/// </summary>
|
||||
[Key("claimSummary")] public string? ClaimSummary { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets distinct supporting witnesses.
|
||||
/// </summary>
|
||||
[Key("witnessCount")] public int WitnessCount { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets supporting witness weight.
|
||||
/// </summary>
|
||||
[Key("supportWeight")] public double SupportWeight { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets total observed witness weight.
|
||||
/// </summary>
|
||||
[Key("totalWeight")] public double TotalWeight { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets whether the candidate crossed quorum.
|
||||
/// </summary>
|
||||
[Key("hasQuorum")] public bool HasQuorum { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets support divided by total observed weight.
|
||||
/// </summary>
|
||||
[Key("confidence")] public double Confidence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a wire candidate message from a local candidate.
|
||||
/// </summary>
|
||||
public static CultNetSimulationConsensusCandidateMessage FromCandidate(
|
||||
string messageId,
|
||||
CultNetSimulationConsensusCandidate candidate)
|
||||
{
|
||||
if (candidate == null) throw new ArgumentNullException(nameof(candidate));
|
||||
return new CultNetSimulationConsensusCandidateMessage
|
||||
{
|
||||
MessageId = string.IsNullOrWhiteSpace(messageId) ? Guid.NewGuid().ToString("N") : messageId,
|
||||
ShardId = candidate.ShardId,
|
||||
ShardEpoch = candidate.ShardEpoch,
|
||||
Frame = candidate.Frame,
|
||||
SubjectId = candidate.SubjectId,
|
||||
ClaimKind = candidate.ClaimKind,
|
||||
ClaimHash = candidate.ClaimHash,
|
||||
ClaimSummary = candidate.ClaimSummary,
|
||||
WitnessCount = candidate.WitnessCount,
|
||||
SupportWeight = candidate.SupportWeight,
|
||||
TotalWeight = candidate.TotalWeight,
|
||||
HasQuorum = candidate.HasQuorum,
|
||||
Confidence = candidate.Confidence
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes one schema exposed through the CultNet schema catalog.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -159,7 +159,9 @@ a witness saw for one shard epoch, frame, subject, and claim kind.
|
|||
`CultNetSimulationConsensus` deterministically aggregates those observations
|
||||
into candidates with support weight, total observed weight, confidence, and
|
||||
quorum status. Those candidates are opinions ready for an authoritative commit
|
||||
path, not committed world state by themselves.
|
||||
path, not committed world state by themselves. The schema-v0 wire contracts are
|
||||
`cultnet.simulation_observation.v0` and
|
||||
`cultnet.simulation_consensus_candidate.v0`.
|
||||
|
||||
The first coherent shard policy is primary ownership. Each shard has one
|
||||
authoritative writer at a time. Clients may connect to any node; a non-owner
|
||||
|
|
|
|||
|
|
@ -319,6 +319,65 @@ namespace GameCult.Networking.Tests
|
|||
Assert.That(roundTrip.Entries[0].Put!.Document.RecordKey, Is.EqualTo("player:42"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CultNetSchemaMessageSerialization_RoundTrips_SimulationObservation()
|
||||
{
|
||||
var claimHash = CultNetSimulationObservation.ComputeClaimHash("hit", "alice", "bob", "frame:100");
|
||||
var message = new CultNetSimulationObservationMessage
|
||||
{
|
||||
MessageId = "observation-1",
|
||||
Observation = new CultNetSimulationObservation
|
||||
{
|
||||
WitnessRuntimeId = "watcher-1",
|
||||
ShardId = "arena",
|
||||
ShardEpoch = 4,
|
||||
Frame = 100,
|
||||
SubjectId = "bob",
|
||||
ClaimKind = "hit",
|
||||
ClaimHash = claimHash,
|
||||
ClaimSummary = "alice hit bob first",
|
||||
ObservedAt = "2026-05-19T12:00:00.0000000Z"
|
||||
}
|
||||
};
|
||||
|
||||
var payload = CultNetSchemaMessageSerialization.Serialize(message);
|
||||
var roundTrip = (CultNetSimulationObservationMessage)CultNetSchemaMessageSerialization.Deserialize(payload);
|
||||
|
||||
Assert.That(roundTrip.MessageId, Is.EqualTo("observation-1"));
|
||||
Assert.That(roundTrip.Observation.WitnessRuntimeId, Is.EqualTo("watcher-1"));
|
||||
Assert.That(roundTrip.Observation.Frame, Is.EqualTo(100));
|
||||
Assert.That(roundTrip.Observation.ClaimHash, Is.EqualTo(claimHash));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CultNetSchemaMessageSerialization_RoundTrips_SimulationConsensusCandidate()
|
||||
{
|
||||
var candidate = new CultNetSimulationConsensusCandidate(
|
||||
"arena",
|
||||
4,
|
||||
100,
|
||||
"bob",
|
||||
"hit",
|
||||
"claim-hash",
|
||||
"alice hit bob first",
|
||||
witnessCount: 3,
|
||||
supportWeight: 3d,
|
||||
totalWeight: 4d,
|
||||
hasQuorum: true);
|
||||
|
||||
var message = CultNetSimulationConsensusCandidateMessage.FromCandidate("candidate-1", candidate);
|
||||
var payload = CultNetSchemaMessageSerialization.Serialize(message);
|
||||
var roundTrip = (CultNetSimulationConsensusCandidateMessage)CultNetSchemaMessageSerialization.Deserialize(payload);
|
||||
|
||||
Assert.That(roundTrip.MessageId, Is.EqualTo("candidate-1"));
|
||||
Assert.That(roundTrip.ShardId, Is.EqualTo("arena"));
|
||||
Assert.That(roundTrip.WitnessCount, Is.EqualTo(3));
|
||||
Assert.That(roundTrip.SupportWeight, Is.EqualTo(3d));
|
||||
Assert.That(roundTrip.TotalWeight, Is.EqualTo(4d));
|
||||
Assert.That(roundTrip.Confidence, Is.EqualTo(0.75d));
|
||||
Assert.That(roundTrip.HasQuorum, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultNetDocumentRegistry_RawSnapshotReplication_PreservesPayloadBytes()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue