write_admitted messageId: letter or number, no Default_Ignorable, raw compare #69
Loading…
Reference in a new issue
No description provided.
Delete branch "libby/write-admitted-messageid-rule"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Rule (ruled by Heimdall): a
cultnet.write_admitted.v0frame'smessageIdis valid iff it contains at least one Unicode letter (Lu Ll Lt Lm Lo) or number (Nd Nl No) and no Default_Ignorable_Code_Point, all as of Unicode 16.0, pinned in every runtime. Scope: write_admitted decoders only;error.v1is untouched.Raw compare: the id is echoed byte for byte, so no decoder trims or normalizes (
" a "valid; U+2800 andaU+200Bbnot). The rule is on the decoded string, which must be well-formed Unicode (a lone surrogate refuses). A runtime whose MessagePack reader rejects invalid UTF-8 refuses the frame earlier; byte-level UTF-8 strictness is a property of the reader and is not required of a decoder (TS's@msgpack/msgpackturns ED A0 80 into a lone surrogate, which the id check refuses; it does not refuse the byteFF, which it reads as U+00FF).Unicode pin: no runtime asks its platform's Unicode data.
scripts/gen-unicode-messageid-tables.py DerivedGeneralCategory.txt DerivedCoreProperties.txt(stdlib only;--checkfails on a stale or hand-edited file) writes one generated table file per runtime (marked "generated, Unicode 16.0.0") pluscontracts/cultnet/interop/write-admitted-message-id-ranges.json. Each runtime's tests probe the first and last point of every range in that JSON and the points either side, so deleting or narrowing any range of any table fails; named vectors pin 16.0-only (U+16100, U+10D40), 15.1 (U+2EBF0) and boundary points. Bumping the version is one coordinated change (spec paragraph). Kotlin's shared MessagePack reader is unchanged for document payloads; strict UTF-8 applies to the write_admitted frame only.Commit order: tests first
4571a0d4, vector commits, per-runtime fixes; thencc931932tests first (Unicode 16.0 pin, failing: Rust 1, Python 2, Kotlin 4), the generator and per-runtime table commits; thenf378d4fdtests first (duplicate id, ill-formed id, scoped strictness, range probes; failing: C# 1, TS 1, Kotlin 1) and the C#, TS, Kotlin and spec fixes.Results at head
2aade7fd: vectors (59 cases) and range probes pass in all five. Kotlin replay 110/110 and self-test ok; C# Networking 768 passed, 4 skipped, 0 failed; Python 167 OK; Rust 557 pass (cultnet-rs and cultmesh-rsrudp_document_server); TS cultnet-ts 317 of 318 (the failure is the unrelatedesbuild bundles dist/content.js); TS receipt interop 80 pass, 0 fail, 1 todo.Mutation sanity: deleting the U+16100 letter range, and separately the U+1D173..1D17A ignorable range, from each runtime's table fails the range probe (and the named vectors) in all five.
Not run: Kotlin suites beyond the replay and self-test; full cultmesh-rs; cultnet-ts own
test:interop; other C# projects; cultmesh-py. Not merged by me.Duplicate messageId keys (cut 4) and where a received messageId decides (cut 5)
A frame with more than one
messageIdkey is malformed. Decoders that see the duplicate (C#, Rust) refuse it; map-collapsing decoders (Python, TS, Kotlin) judge the last key and refuse if it is invalid, so none echoes an invalid id. VectorsadmittedMessageIdDuplicatesdeclare each policy ([invalid,valid], [valid,invalid], [valid,valid]); every runner asserts its own. Tests first: only C# failed (it accepted [valid,valid]), then fixed.Uses of a received messageId beyond echo (spec paragraph added to
write-receipt.md):cultmesh-rs/src/rudp_document_server.rs:1275,1483CultNetWriteReceipts.cs(_writesby digest)CultNetWriteCompletion.cs:92,97write-tracker.ts(#answer,#writes.get),idunn-odin-presence-publisher.ts:124WriteReceipt.ktWriteTracker.answercultmesh_py/server.pyDuplicate keys on write frames (the frames the servers dedupe): Rust refuses them on write frames and on
write_admittedthrough serde'sduplicate field messageId(cultnet-rs/src/contracts.rs:607, the typed decode), andrefuse_duplicate_keys(contracts.rs:1052) covers only the raw-list frames such asdocument_put_raw; Rust'sreadable_message_id(contracts.rs:2048) names the FIRST key when it names an unreadable frame, like C#. The Rust testa_repeated_message_id_key_is_refused_whatever_the_valuespins the outcome, not the cause. C# does not: I verified adocument_deletewithmessageId"first" and again "last" decodes with id "last", whileCultNetReadableMessageId.From(the id named in a refusal of an unreadable frame) takes "first". This PR does not change write-frame handling. The TS duplicate-key test pins @msgpack/msgpack's last-key behaviour plus the id check on the decoded object; TS has no first-key or duplicate code path, so a refuse-on-first mutation in src is not killed.Topology: the C# shard forwarder does not relay bytes.
CultNetDatabaseServer.cs:483,564parses the write once,CultNetShardWriteForwarding.cs:104-110,189re-encodes the typed message (onemessageIdkey) for the primary, so the primary parses a canonical frame. The interopreceipt-relay(ReceiptHarness.cs:68) and thecultnet_impairUDP proxy move bytes without parsing. I found no path in this checkout where two parsers read the client's original bytes; other repos (Bifrost etc.) are not visible here, so a chain there is OPEN, not closed. The spec states the consequence: any chain of two parsers over one frame, or any consumer needing messageId for authorization, dedupe, replay or ordering beyond echo and equality with a self-sent id, requires all runtimes to refuse duplicate keys first (byte-level duplicate detection in the map-decoding runtimes). No new pin test: the tracker's "names a write not in flight settles nothing" test already exists in each tracker's suite, and a duplicate-key test would only restate the policy vectors.🤖 Generated with Claude Code