write-receipt: one unreadable text, one blank-field rule (both servers) #64

Merged
libby merged 9 commits from libby/write-receipt-text-blank-rules into main 2026-11-09 14:15:49 +00:00
Member

Closes the two Heimdall low follow-ups heimdall-low-unreadable-text and heimdall-low-blank-field-rule (libby-body).

A. One unreadable text. message_invalid: the message could not be read (the servers' wording, kept over the vectors' unreadable frame: it is what both servers already send, so no wire change). Added to the spec's Fixed texts; the two vectors that carry it (error_v1_unreadable_frame_has_null_message_id, error_v1_empty_schema_id_is_unreadable_and_names_the_write) are re-encoded. The Rust wire never carried decoder text (only the local rejection Display does); a test pins the exact string on a receipt connection. C# has one owner now, CultNetErrorMessage.ForUnreadableMessage.

B. One blank-field rule. Read and run before deciding: Rust refused blank storedAt, empty payload, blank present source ids and blank tags (decoder validate_raw_document_record, every connection); C# refused only blank schemaId/recordKey. No product fork: a MessagePack value is at least one byte, so an empty payload is no document. Rule (spec, Required fields): a put that names nothing is refused as unreadable (message_invalid, field message, the write's id) on every connection, never applied. C# now follows Rust. Absent/nil source ids and tags and an empty tag list stay valid. New vectors section blankFields (15 cases) is replayed by both servers' tests (Rust: receipt and legacy; C#: receipt and legacy, plus a nil payload).

Checks: cultnet-rs write_receipt 6/6, cultmesh-rs targeted tests, C# GameCult.Networking.Tests 639 passed / 4 skipped (before the last test), cultnet-ts vector replay 33/33 and write-receipt.test.js 21/21.

Mutants (hand-rolled, under prlimit --as=4294967296 --cpu=300; cargo-mutants and Stryker not used): Rust 9/9 killed (decoder rules in contracts.rs and the fixed text); C# 13 on the diff, 13 killed after one survivor (nil payload) got its own test, plus a no-op control that survived as it should.

🤖 Generated with Claude Code

Update: blank means Unicode White_Space (Heimdall's ruling)

Spec states the exact White_Space list; vectors add whiteSpaceCodePoints (25), notWhiteSpaceCodePoints (U+200B/C/D, U+2060, U+FEFF, U+180E, U+001C-001F) and named cases. Rust (cultmesh-rs rudp_document_server) and C# (CultNetWriteReceiptServerTests) test every code point in every covered field. C# now checks the listed set in one place (IsBlank), not IsNullOrWhiteSpace. TS: no server, replay runner does not use blankFields; passes unchanged. Python/Kotlin not touched.

Co-Authored-By: Claude Sonnet 5.5 noreply@anthropic.com

Closes the two Heimdall low follow-ups `heimdall-low-unreadable-text` and `heimdall-low-blank-field-rule` (libby-body). **A. One unreadable text.** `message_invalid: the message could not be read` (the servers' wording, kept over the vectors' `unreadable frame`: it is what both servers already send, so no wire change). Added to the spec's Fixed texts; the two vectors that carry it (`error_v1_unreadable_frame_has_null_message_id`, `error_v1_empty_schema_id_is_unreadable_and_names_the_write`) are re-encoded. The Rust wire never carried decoder text (only the local rejection Display does); a test pins the exact string on a receipt connection. C# has one owner now, `CultNetErrorMessage.ForUnreadableMessage`. **B. One blank-field rule.** Read and run before deciding: Rust refused blank `storedAt`, empty payload, blank present source ids and blank tags (decoder `validate_raw_document_record`, every connection); C# refused only blank `schemaId`/`recordKey`. No product fork: a MessagePack value is at least one byte, so an empty payload is no document. Rule (spec, Required fields): a put that names nothing is refused as unreadable (`message_invalid`, field `message`, the write's id) on every connection, never applied. C# now follows Rust. Absent/nil source ids and tags and an empty tag list stay valid. New vectors section `blankFields` (15 cases) is replayed by both servers' tests (Rust: receipt and legacy; C#: receipt and legacy, plus a nil payload). Checks: cultnet-rs `write_receipt` 6/6, cultmesh-rs targeted tests, C# `GameCult.Networking.Tests` 639 passed / 4 skipped (before the last test), cultnet-ts vector replay 33/33 and `write-receipt.test.js` 21/21. Mutants (hand-rolled, under `prlimit --as=4294967296 --cpu=300`; cargo-mutants and Stryker not used): Rust 9/9 killed (decoder rules in contracts.rs and the fixed text); C# 13 on the diff, 13 killed after one survivor (nil payload) got its own test, plus a no-op control that survived as it should. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Update: blank means Unicode White_Space (Heimdall's ruling) Spec states the exact White_Space list; vectors add whiteSpaceCodePoints (25), notWhiteSpaceCodePoints (U+200B/C/D, U+2060, U+FEFF, U+180E, U+001C-001F) and named cases. Rust (cultmesh-rs rudp_document_server) and C# (CultNetWriteReceiptServerTests) test every code point in every covered field. C# now checks the listed set in one place (IsBlank), not IsNullOrWhiteSpace. TS: no server, replay runner does not use blankFields; passes unchanged. Python/Kotlin not touched. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The unreadable-frame text is 'message_invalid: the message could not be read' (the servers' wording; the Rust wire never carried decoder text, only its local rejection display). A put that names nothing (blank schemaId/recordKey/storedAt, empty payload, blank present source ids or tags) is refused as unreadable on every connection; the vectors' blankFields section lists the cases and the Rust server tests replay them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
IsUnreadableDocument follows the spec's Required fields rule (blank storedAt, empty payload, blank source ids and tags join blank schemaId/recordKey); ForUnreadableMessage is the single owner of the fixed text. Tests replay the blankFields vectors on receipt and legacy connections.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
C# tests: a nil payload names nothing (mutant survivor)
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 6m30s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 36s
98989f90ea
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 spec: name the platforms that disagree accurately
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 5m3s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 26s
3b1b769762
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
validateBinaryMessageRoot, requireNonEmptyString and requireOptionalStringArray
used String.prototype.trim(), which takes U+FEFF and misses U+0085, so the TS
decoder and the servers disagreed on what names nothing (write-receipt rule 5).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
write-receipt: seven tracker vectors, a tracker that ends with its session, runners that count
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 7m39s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 22s
f0c0f936ba
New scenario vectors: id-less error.v1 with one and with two writes unanswered,
sendFailed, an id-less error after the window, a receipt one ms inside the window
and at the exact boundary (late), a send after sessionEnd. The last exposed a
TS tracker leaving such a write pending forever; it now settles session_ended.
The file declares counts, and the TS replay runner fails on a count it did not
run or an event key scenarios.semantics does not name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
libby merged commit 69703358c0 into main 2026-11-09 14:15:49 +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!64
No description provided.