Prove Python prediction reconciliation over CultNet

This commit is contained in:
Meta Vens 2026-06-14 01:12:03 +01:00
commit 0c0f71dfb8
5 changed files with 92 additions and 6 deletions

View file

@ -21,6 +21,10 @@ CultCache, CultNet, and CultMesh. It is evidence, not confetti.
The live interop harness sends Python a simulation observation, receives a
consensus candidate, then requests the committed `gamecult.mesh.simulation_fact`
document back over the raw snapshot lane and checks its MessagePack slots.
- Python CultMesh prediction reconciliation:
The live interop harness runs a Python CultMesh session client, writes a local
prediction, pulls the live Python peer shard log, applies the authoritative
entry, and verifies the session reports the record as reconciled.
- Python package health:
`cultcache-py-verify --json` checks public exports, `py.typed` markers, and
benchmark sanity for Python-owned hot paths.

View file

@ -289,12 +289,13 @@ 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,
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.
committed simulation fact snapshots, Python session prediction reconciliation,
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

@ -113,12 +113,17 @@ def apply_shard_log_response(
change_kind = entry.get("changeKind")
if change_kind in ("added", "updated") and isinstance(entry.get("put"), dict):
put = entry["put"]
schema_id = str(put["document"]["schemaId"])
if schema_id not in documents_by_schema_id:
continue
result = apply_raw_document_record(cache, documents_by_schema_id, put["document"])
applied.append(CultNetAppliedRecord(result.schema_id, result.record_key, str(change_kind), result.value))
elif change_kind == "removed" and isinstance(entry.get("delete"), dict):
delete = entry["delete"]
schema_id = str(delete["schemaId"])
record_key = str(delete["recordKey"])
if schema_id not in documents_by_schema_id:
continue
document = documents_by_schema_id[schema_id]
cache.delete(document, record_key)
applied.append(CultNetAppliedRecord(schema_id, record_key, "removed", None))

View file

@ -486,6 +486,23 @@ class CultCacheTests(unittest.TestCase):
"shardEpoch": 1,
"resyncRequired": False,
"entries": [
{
"sequence": 0,
"changeKind": "updated",
"put": {
"schemaVersion": "cultnet.document_put_raw.v0",
"messageId": "put-unknown",
"document": {
"schemaId": "replica.unknown.v1",
"recordKey": "unknown:1",
"storedAt": "2026-06-13T00:00:01Z",
"payloadEncoding": "messagepack",
"payload": document.encode_payload(Item("ignored", "unknown", 0)),
},
"shardId": "interop",
"shardEpoch": 1,
},
},
{
"sequence": 1,
"changeKind": "updated",

View file

@ -312,6 +312,11 @@ test("CultNet TS/Rust/C#/Python peers discover each other and exchange raw state
assert.equal(pythonCultMeshNodeEmit.note.authorRuntimeId, "python-node-emit");
assert.equal(pythonCultMeshNodeEmit.note.title, "Python node emitted raw put");
const pythonPrediction = await runPythonCultMeshPredictionReconcile(pythonPort);
assert.ok(pythonPrediction.reconciledKeys.includes("note:python-node-emit"));
assert.equal(pythonPrediction.note.title, "Python node emitted raw put");
assert.equal(pythonPrediction.note.authorRuntimeId, "python-node-emit");
const pythonSubscription = await runPythonCultNetSubscription(pythonPort);
assert.equal(pythonSubscription.changeKind, "added");
assert.equal(pythonSubscription.recordKey, "note:python-sub-client");
@ -950,6 +955,60 @@ async function runPythonCultNetSubscription(port: number): Promise<any> {
return JSON.parse(stdout);
}
async function runPythonCultMeshPredictionReconcile(port: number): Promise<any> {
const schemaPathJson = JSON.stringify(interopSchemaPath);
const script = [
"import json",
"import tempfile",
"from pathlib import Path",
"from cultcache_py import define_database_entry_type",
"from cultmesh_py import CultMesh, CultMeshGameSessionOptions",
"from cultnet_py import CultNetClientAuthorityScope, CultNetRawClient",
`schema_json = Path(${schemaPathJson}).read_text(encoding='utf-8')`,
"note_doc = define_database_entry_type(",
" 'cultnet.interop-note',",
" [('schemaVersion', 0), ('documentId', 1), ('authorRuntimeId', 2), ('title', 3), ('body', 4), ('tags', 5, [])],",
` schema_id='${interopNoteSchemaId}',`,
" schema_version='cultnet.interop_note.v0',",
" canonical_schema_json=schema_json,",
")",
"with tempfile.TemporaryDirectory() as tmp:",
" node = CultMesh.create_node(Path(tmp) / 'prediction.cc', runtime_id='python-predict-client')",
" node.register_document(note_doc)",
" session = CultMesh.create_game_session(",
" node,",
" CultMeshGameSessionOptions(client_authority_scopes=(",
` CultNetClientAuthorityScope('python-predict-client', schema_ids=('${interopNoteSchemaId}',), key_prefix='note:python-node-emit'),`,
" )),",
" )",
" session.predict(note_doc, 'note:python-node-emit', {",
" 'schemaVersion': 'cultnet.interop_note.v0',",
" 'documentId': 'note:python-node-emit',",
" 'authorRuntimeId': 'python-predict-client',",
" 'title': 'Predicted Python node emit',",
" 'body': 'local predicted body',",
" 'tags': ['python', 'prediction'],",
" })",
` client = CultNetRawClient('127.0.0.1', ${port}, timeout_seconds=4.0)`,
" changes = session.sync_shard_log(client, shard_id='interop', shard_epoch=1, after_sequence=0)",
" note = node.get_required(note_doc, 'note:python-node-emit')",
" print(json.dumps({",
" 'reconciledKeys': [change.record_key for change in changes if change.change_kind == 'reconciled'],",
" 'changes': [{'kind': change.change_kind, 'key': change.record_key} for change in changes],",
" 'note': note,",
" }))",
].join("\n");
const { stdout } = await execFileAsync(pythonCommand, ["-c", script], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
},
timeout: 8000,
});
return JSON.parse(stdout);
}
async function requestPythonDatabaseChange(port: number, subscribe: Record<string, unknown>, put: Record<string, unknown>): Promise<any> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();