Prove Python prediction reconciliation over CultNet
This commit is contained in:
parent
41c43bb5e2
commit
0c0f71dfb8
5 changed files with 92 additions and 6 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue