Repository navigation
Description
The Rust decoder's NOTICE for a malformed client frame does not reproduce the capture's
error model. Both implementations refuse these frames; only the detail after invalid:
differs.
Measured divergence
nostream_core::messages::decode (crates/nostream-core/src/messages.rs) reports the
failing field's own message. The capture's messageSchema.parse over a z.union
(src/schemas/message-schema.ts:61, zod 3.25.76) reports one invalid_union issue whose
message is Invalid input, because a wrong-typed member aborts the branch instead of
recording an issue on it.
| frame | port (WireError::Validation) |
capture (issues[0].message) |
|---|---|---|
["EVENT",{…,"created_at":"1700000000",…}] |
Expected number, received string |
Invalid input |
["EVENT",{…,"created_at":null,…}] |
Expected number, received null |
Invalid input |
["EVENT",{…,"content":5,…}] |
Expected string, received number |
Invalid input |
["EVENT",{…}] with a missing sig |
Expected string, received null |
Invalid input |
["EVENT",{…,"tags":{},…}] |
Expected array, received object |
Invalid input |
["REQ","s1",{"unknown":1}] |
Expected array, received number |
Invalid input |
["REQ","s1",{"kinds":[[1,2]]}] |
Expected number, received array |
Invalid input |
["REQ","s1",{…},"junk"] |
Expected object, received string |
Invalid input |
["CLOSE",42] |
Invalid input |
Invalid input (already agrees) |
Constraint failures — a branch that parses and then records an issue — do agree today:
id uppercase → Invalid, a 63-character id → String must contain exactly 64 character(s), created_at: -1 → Number must be greater than or equal to 0,
{"limit":-1} → the same, {"unknown":["x"]} → Unknown key: unknown, an unknown event
key → Unrecognized key(s) in object: 'extra'. That is the same aborted-versus-recorded
split, so the fix is a classification, not a set of string edits.
What closing it costs
Every issue validate_event_value (crates/nostream-core/src/messages.rs) and
validate_filter (crates/nostream-core/src/filter.rs) produce has to be classified as
aborted (report Invalid input) or recorded (report its own message), and the decoder
has to answer the first class as the union does. That rewrites the decoder's error model.
Two consequences ride with it:
decode_reports_the_capture_notice_stringsin
crates/nostream-core/tests/wire_codec.rspins two port-only rows
(Expected array, received number,Expected number, received array); those rows move
in the same change.- The same model decides issue order inside a frame that is both over-long and dirty:
["CLOSE","","junk"]answersString must contain at least 1 character(s)here and the
tuple'sArray must contain at most 2 element(s)there, and
["EVENT",{…,"kind":1.5},"junk"]answersExpected integer, received floathere and
Array must contain at most 2 element(s)there.
Where the port records this today
crates/nostream-core/src/messages.rs's module doc is the authoritative list of the
decoder's differences from the capture; it names this gap as open, with the capture
authoritative.
No frame is admitted by one implementation and refused by the other on account of this
class; the NOTICE text is what differs.
Activity
docs(core): give the arity order, the timestamp window and the open d…