Prove Python simulation facts over CultNet

This commit is contained in:
Meta Vens 2026-06-14 00:56:23 +01:00
commit a2779d7493
3 changed files with 102 additions and 7 deletions

View file

@ -286,12 +286,12 @@ Current receipts:
catalog, wire-message catalog discovery, raw snapshot, raw document
put/delete, mutation receipt, fire-command receipt, database subscription
changes, shard catalog, shard log catch-up, simulation consensus candidates,
and witness artifact bundle round-trips. The same test asks the Python peer
for CultMesh Verse catalog and peer exchange responses over the CultNet pipe,
then verifies the public `CultMeshDiscoveryClient` can fetch typed Python
Verse and peer descriptors from that live endpoint. It also verifies
`CultNetRawClient` can fetch the live Python peer's schema catalog, raw
snapshot, and shard catalog.
committed simulation fact snapshots, and witness artifact bundle round-trips.
The same test asks the Python peer for CultMesh Verse catalog and peer
exchange responses over the CultNet pipe, then verifies the public
`CultMeshDiscoveryClient` can fetch typed Python Verse and peer descriptors
from that live endpoint. It also verifies `CultNetRawClient` can fetch the
live Python peer's schema catalog, raw snapshot, and shard catalog.
- `packages/cultcache-py/tests/test_cultcache.py` covers Python CultMesh
Verse catalog, peer exchange, authority lease, stream negotiation, CultNet
helper shapes, raw snapshot/shard-log application, simulation claim hashing,

View file

@ -27,12 +27,15 @@ from cultcache_py import (
from cultcache_py.documents import DocumentDefinition
from cultmesh_py import (
PEER_EXCHANGE_REQUEST,
SIMULATION_FACT_DOCUMENT_TYPE,
VERSE_CATALOG_REQUEST,
CultMeshPeerCard,
CultMeshPeerCatalog,
CultMeshSimulationFact,
CultMeshVerseCatalog,
CultMeshVerseCompatibility,
CultMeshVerseDescriptor,
simulation_fact_document,
)
from .framing import read_frame, write_frame
@ -54,6 +57,7 @@ INTEROP_FIRE_RECEIPT_SCHEMA_VERSION = "cultnet.interop_fire_weapon_receipt.v0"
WITNESS_ARTIFACT_BUNDLE_DOCUMENT_TYPE = "cultnet.witness_artifact_bundle"
WITNESS_ARTIFACT_BUNDLE_SCHEMA_ID = "https://github.com/GameCult/cultnet-ts/contracts/cultnet.witness-artifact-bundle.schema.json"
WITNESS_ARTIFACT_BUNDLE_SCHEMA_VERSION = "cultnet.witness_artifact_bundle.v0"
SIMULATION_FACT_SCHEMA_ID = simulation_fact_document.catalog_entry().schema_id
INTEROP_WIRE_CONTRACT = "cultnet.schema.v0"
DISCOVERY_PROBE_SCHEMA_VERSION = "cultnet.discovery_probe.v0"
DISCOVERY_ANNOUNCE_SCHEMA_VERSION = "cultnet.discovery_announce.v0"
@ -475,9 +479,21 @@ def simulation_candidate_responses(state: PeerState, message: dict[str, Any]) ->
witnesses = state.observations.setdefault(group, {})
witnesses[witness_id] = observation
candidates = build_simulation_candidates(message.get("messageId", ""), list(witnesses.values()))
commit_simulation_facts(state, candidates)
return candidates
def commit_simulation_facts(state: PeerState, candidates: list[dict[str, Any]]) -> None:
binding = state.bindings_by_schema_id[SIMULATION_FACT_SCHEMA_ID]
for candidate in candidates:
if not candidate.get("hasQuorum"):
continue
key = CultMeshSimulationFact.create_record_key(candidate)
if state.cache.get(binding.document, key) is not None:
continue
state.cache.put(binding.document, key, CultMeshSimulationFact.from_candidate(candidate))
def build_simulation_candidates(message_id: str, observations: list[dict[str, Any]]) -> list[dict[str, Any]]:
by_claim: dict[str, list[dict[str, Any]]] = defaultdict(list)
total_weight = sum(observation_weight(observation) for observation in observations)
@ -751,6 +767,7 @@ def default_peer_catalog(runtime_id: str) -> CultMeshPeerCatalog:
def define_interop_documents(note_schema_id: str, note_schema_json: str) -> dict[str, Binding]:
simulation_entry = simulation_fact_document.catalog_entry()
return {
"note": binding(INTEROP_DOCUMENT_TYPE, note_schema_id, INTEROP_SCHEMA_VERSION, note_schema_json, note_slots, note_from_slots),
"mutationIntent": binding(INTEROP_MUTATION_INTENT_DOCUMENT_TYPE, INTEROP_MUTATION_INTENT_SCHEMA_ID, INTEROP_MUTATION_INTENT_SCHEMA_VERSION, "{}", mutation_intent_slots, mutation_intent_from_slots),
@ -765,6 +782,11 @@ def define_interop_documents(note_schema_id: str, note_schema_json: str) -> dict
witness_artifact_bundle_slots,
witness_artifact_bundle_from_slots,
),
"simulationFact": Binding(
document=simulation_fact_document,
schema_id=simulation_entry.schema_id,
payload_schema_version=simulation_entry.schema_version,
),
}
@ -799,7 +821,7 @@ def hello_message(state: PeerState) -> dict[str, Any]:
"runtimeKind": state.runtime_kind,
"agentId": state.agent_id,
"displayName": state.display_name,
"supportedDocumentTypes": [INTEROP_DOCUMENT_TYPE, WITNESS_ARTIFACT_BUNDLE_DOCUMENT_TYPE],
"supportedDocumentTypes": [INTEROP_DOCUMENT_TYPE, WITNESS_ARTIFACT_BUNDLE_DOCUMENT_TYPE, SIMULATION_FACT_DOCUMENT_TYPE],
"supportedMutationContracts": [{
"documentType": INTEROP_DOCUMENT_TYPE,
"payloadSchemaVersion": INTEROP_SCHEMA_VERSION,
@ -846,6 +868,18 @@ def catalog_response(state: PeerState, request: dict[str, Any]) -> dict[str, Any
"contentHash": WITNESS_ARTIFACT_BUNDLE_SCHEMA_ID,
"schemaJson": witness_artifact_bundle_schema_json() if include_schema_json else None,
})
if (not schema_ids or SIMULATION_FACT_SCHEMA_ID in schema_ids) and (not kinds or "document_payload" in kinds):
entry = simulation_fact_document.catalog_entry()
schemas.append({
"schemaId": entry.schema_id,
"kind": "document_payload",
"schemaVersion": entry.schema_version,
"documentType": SIMULATION_FACT_DOCUMENT_TYPE,
"title": "CultMesh Simulation Fact",
"wireContracts": [INTEROP_WIRE_CONTRACT],
"contentHash": entry.content_hash,
"schemaJson": entry.canonical_schema_json if include_schema_json else None,
})
return {"schemaVersion": "cultnet.schema_catalog_response.v0", "messageId": request.get("messageId", ""), "schemas": schemas}

View file

@ -30,6 +30,7 @@ const tsPeerScript = resolve(cultNetTsRoot, "dist-test", "test", "interop", "cul
const interopSchemaPath = resolve(cultNetTsRoot, "integration", "contracts", "cultnet.interop-note.schema.json");
const interopNoteSchemaId = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-note.schema.json";
const witnessArtifactBundleSchemaId = "https://github.com/GameCult/cultnet-ts/contracts/cultnet.witness-artifact-bundle.schema.json";
const simulationFactSchemaId = "gamecult.mesh.simulation_fact";
const csharpProjectPath = resolve(
cultLibRoot,
"tests",
@ -476,6 +477,24 @@ test("CultNet TS/Rust/C#/Python peers discover each other and exchange raw state
assert.equal(pythonCandidate.totalWeight, 1);
assert.equal(pythonCandidate.hasQuorum, true);
assert.equal(pythonCandidate.confidence, 1);
const simulationFactKey = createSimulationFactKey(pythonCandidate);
const pythonFactSnapshot = await requestPythonSnapshot(pythonPort, {
schemaVersion: "cultnet.snapshot_request.v0",
messageId: "python-simulation-fact-snapshot",
schemaIds: [simulationFactSchemaId],
recordKeys: [simulationFactKey],
});
assert.equal(pythonFactSnapshot.schemaVersion, "cultnet.snapshot_response_raw.v0");
assert.equal(pythonFactSnapshot.documents[0].schemaId, simulationFactSchemaId);
assert.equal(pythonFactSnapshot.documents[0].recordKey, simulationFactKey);
const simulationFactSlots = decode(pythonFactSnapshot.documents[0].payload) as any[];
assert.equal(simulationFactSlots[0], simulationFactKey.slice("simulation:".length));
assert.equal(simulationFactSlots[1], "interop");
assert.equal(simulationFactSlots[3], 42);
assert.equal(simulationFactSlots[4], "python-target");
assert.equal(simulationFactSlots[5], "hit");
assert.equal(simulationFactSlots[6], claimHash);
assert.equal(simulationFactSlots[8], 1);
const pythonWitness = await putAndSnapshotPythonWitnessBundle(pythonPort, {
schemaVersion: "cultnet.document_put_raw.v0",
@ -1083,6 +1102,48 @@ function computeSimulationClaimHash(...parts: string[]): string {
return createHash("sha256").update(parts.join("\x1f"), "utf8").digest("hex");
}
function createSimulationFactKey(candidate: any): string {
const factId = createHash("sha256")
.update([
candidate.shardId,
String(candidate.shardEpoch),
String(candidate.frame),
candidate.subjectId,
candidate.claimKind,
].join("\x1f"), "utf8")
.digest("hex");
return `simulation:${factId}`;
}
async function requestPythonSnapshot(port: number, request: Record<string, unknown>): Promise<any> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error("Timed out waiting for Python snapshot response."));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
const decoded = decode(frame) as any;
if (decoded?.schemaVersion === "cultnet.snapshot_response_raw.v0") {
clearTimeout(timeout);
socket.end();
resolve(decoded);
}
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode(request)));
});
}
async function putAndSnapshotPythonWitnessBundle(port: number, put: Record<string, unknown>): Promise<{ catalog: any; snapshot: any }> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();