Prove Python simulation facts over CultNet
This commit is contained in:
parent
90a669914b
commit
a2779d7493
3 changed files with 102 additions and 7 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue