write_admitted messageId: letter or number, no Default_Ignorable, raw compare #69

Merged
libby merged 24 commits from libby/write-admitted-messageid-rule into main 2026-11-09 16:49:35 +00:00
Member

Rule (ruled by Heimdall): a cultnet.write_admitted.v0 frame's messageId is 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.v1 is untouched.

Raw compare: the id is echoed byte for byte, so no decoder trims or normalizes (" a " valid; U+2800 and a U+200B b not). 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/msgpack turns ED A0 80 into a lone surrogate, which the id check refuses; it does not refuse the byte FF, 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; --check fails on a stale or hand-edited file) writes one generated table file per runtime (marked "generated, Unicode 16.0.0") plus contracts/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; then cc931932 tests first (Unicode 16.0 pin, failing: Rust 1, Python 2, Kotlin 4), the generator and per-runtime table commits; then f378d4fd tests 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-rs rudp_document_server); TS cultnet-ts 317 of 318 (the failure is the unrelated esbuild 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 messageId key 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. Vectors admittedMessageIdDuplicates declare 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):

Runtime Use
Rust server cultmesh-rs/src/rudp_document_server.rs:1275,1483 dedupe: remembered-write key (digest of id, content hash), per-session limit, replay; id taken from the write frame it parsed
C# server CultNetWriteReceipts.cs (_writes by digest) same dedupe, per session
C# client CultNetWriteCompletion.cs:92,97 equality with the sent id; null id attributes to the write
TS write-tracker.ts (#answer, #writes.get), idunn-odin-presence-publisher.ts:124 equality with the sent id; an id-less error settles only when exactly one write is unanswered
Kotlin WriteReceipt.kt WriteTracker.answer same as TS
Python cultmesh_py/server.py echo only

Duplicate keys on write frames (the frames the servers dedupe): Rust refuses them on write frames and on write_admitted through serde's duplicate field messageId (cultnet-rs/src/contracts.rs:607, the typed decode), and refuse_duplicate_keys (contracts.rs:1052) covers only the raw-list frames such as document_put_raw; Rust's readable_message_id (contracts.rs:2048) names the FIRST key when it names an unreadable frame, like C#. The Rust test a_repeated_message_id_key_is_refused_whatever_the_values pins the outcome, not the cause. C# does not: I verified a document_delete with messageId "first" and again "last" decodes with id "last", while CultNetReadableMessageId.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,564 parses the write once, CultNetShardWriteForwarding.cs:104-110,189 re-encodes the typed message (one messageId key) for the primary, so the primary parses a canonical frame. The interop receipt-relay (ReceiptHarness.cs:68) and the cultnet_impair UDP 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

Rule (ruled by Heimdall): a `cultnet.write_admitted.v0` frame's `messageId` is 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.v1` is untouched. Raw compare: the id is echoed byte for byte, so no decoder trims or normalizes (`" a "` valid; U+2800 and `a` U+200B `b` not). 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/msgpack` turns ED A0 80 into a lone surrogate, which the id check refuses; it does not refuse the byte `FF`, 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; `--check` fails on a stale or hand-edited file) writes one generated table file per runtime (marked "generated, Unicode 16.0.0") plus `contracts/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; then `cc931932` tests first (Unicode 16.0 pin, failing: Rust 1, Python 2, Kotlin 4), the generator and per-runtime table commits; then `f378d4fd` tests 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-rs `rudp_document_server`); TS cultnet-ts 317 of 318 (the failure is the unrelated `esbuild 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 `messageId` key 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. Vectors `admittedMessageIdDuplicates` declare 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`): | Runtime | Use | |---|---| | Rust server `cultmesh-rs/src/rudp_document_server.rs:1275,1483` | dedupe: remembered-write key (digest of id, content hash), per-session limit, replay; id taken from the write frame it parsed | | C# server `CultNetWriteReceipts.cs` (`_writes` by digest) | same dedupe, per session | | C# client `CultNetWriteCompletion.cs:92,97` | equality with the sent id; null id attributes to the write | | TS `write-tracker.ts` (`#answer`, `#writes.get`), `idunn-odin-presence-publisher.ts:124` | equality with the sent id; an id-less error settles only when exactly one write is unanswered | | Kotlin `WriteReceipt.kt` `WriteTracker.answer` | same as TS | | Python `cultmesh_py/server.py` | echo only | Duplicate keys on write frames (the frames the servers dedupe): Rust refuses them on write frames and on `write_admitted` through serde's `duplicate field messageId` (`cultnet-rs/src/contracts.rs:607`, the typed decode), and `refuse_duplicate_keys` (`contracts.rs:1052`) covers only the raw-list frames such as `document_put_raw`; Rust's `readable_message_id` (`contracts.rs:2048`) names the FIRST key when it names an unreadable frame, like C#. The Rust test `a_repeated_message_id_key_is_refused_whatever_the_values` pins the outcome, not the cause. C# does not: I verified a `document_delete` with `messageId` "first" and again "last" decodes with id "last", while `CultNetReadableMessageId.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,564` parses the write once, `CultNetShardWriteForwarding.cs:104-110,189` re-encodes the typed message (one `messageId` key) for the primary, so the primary parses a canonical frame. The interop `receipt-relay` (`ReceiptHarness.cs:68`) and the `cultnet_impair` UDP 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](https://claude.com/claude-code)
Pins Heimdall's rule: a write_admitted messageId needs a Unicode letter or number and no
Default_Ignorable_Code_Point, compared raw. Shared vectors plus a runner test in TS, Python,
Kotlin, C# and Rust. No decoder changed; the refuse vectors fail.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw compare, no trim. The platform's regex property tables carry both properties; no table embedded.
D800 vector now driven from wireHex.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw compare. unicodedata for L/N, an embedded Default_Ignorable range table (Unicode 16.0).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw compare. JDK Character categories for L/N, an embedded Default_Ignorable range table (Unicode 16.0).
The MessagePack string reader now refuses invalid UTF-8 instead of substituting U+FFFD.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw compare. .NET Unicode categories for L/N, an embedded Default_Ignorable range table (Unicode 16.0).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cultnet-rs: refuse a write_admitted messageId without a letter or number or with a Default_Ignorable code point
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
ba548d192d
Raw compare. std is_numeric for N; embedded letter (Unicode 15.1) and Default_Ignorable (16.0) range tables.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tests first: Unicode 16.0 pin vectors (failing)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m15s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 43s
cc931932fc
Failing before the fix: Rust 1 (16100), Python 2 (16100, 10D40), Kotlin 4 (16100, 10D40, 2EBF0, 2EE5D).
TypeScript and C# already pass: their platform data is 16.0 or later.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scripts/gen-unicode-messageid-tables.py emits each runtime's Letter-or-Number and Default_Ignorable tables from the two UCD 16.0 files; --check fails on a stale or hand-edited table.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cultnet-rs: write_admitted messageId from the generated Unicode 16.0 tables (letters and numbers now 16.0, not 15.1)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
8598fd3d12
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tests first: duplicate messageId, ill-formed id, scoped UTF-8 strictness, every table range pinned (failing)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
f378d4fdb0
Failing before the fix: C# 1 (duplicate messageId, second is U+200B), TypeScript 1 (a + lone surrogate),
Kotlin 1 (a document payload string with a lone surrogate no longer decodes). Python and Rust already pass
the new vectors. The range probes read the generated write-admitted-message-id-ranges.json and pass on the
correct tables; they exist to fail when any range is deleted or narrowed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
write-receipt.md: the messageId rule is on the decoded string; byte-level UTF-8 strictness is the reader's, not required
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 32s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Failing after 30s
a97861945c
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Vectors admittedMessageIdDuplicates carry [invalid,valid], [valid,invalid], [valid,valid]. Decoders that see the
duplicate (C#, Rust) must refuse all three; map-collapsing decoders (Python, TS, Kotlin) judge the last key.
Python, TS, Kotlin and Rust already behave as declared; C# fails: it accepted [valid,valid].

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
write-receipt.md: where a received messageId decides, and the single-parser assumption it relies on
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
27d9fbca94
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Docs and test comments: correct the Rust duplicate-key citation, say what the TS and Rust duplicate tests pin
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m24s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 21s
2aade7fdd3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
libby merged commit 88545542bb into main 2026-11-09 16:49:35 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/CultLib!69
No description provided.