Nota Format
Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols. Nota lives in the shoplib package: use('shoplib::nota').
Nota stands for Network Object Transfer Arrangement.
Value semantics and codec boundary
shoplib::nota owns record and array traversal, cycle rejection, private
actor-value policy, and revivers. Encoding invokes toJSON once on the
original property or root, invokes the replacer once on that result, and then
traverses the replacement. The native internal/nota_raw module sees only a
flat preorder token stream; it validates byte bounds and converts scalar wire
values without invoking Pit hooks or walking user containers.
Empty input is not a document
A Nota document encodes exactly one value, and no value encodes to nothing —
every value begins with a preamble byte, so the shortest document is one byte.
decode therefore refuses empty input rather than answering with a value.
This matters because the alternative is silent: a null returned for zero
bytes is indistinguishable from a successfully decoded null, so a truncated
transfer or a half-written manifest reads as a valid empty record instead of as
an error. Wota has always refused empty input on the same
reasoning, and the two codecs agree.
Zero-length blobs remain entirely legal, both as Nota values and in the runtime — that is a statement about bytes. This is a statement about documents.
Design Philosophy
Nota aims at the intersection of most programming languages and their common data types, expressed in binary: values carry counts, and the encoding is bytes rather than characters.
Nota uses Kim continuation bytes for counts and character encoding. See Kim Encoding for details.
Type Summary
| Bits | Type |
|---|---|
000 | Blob |
001 | Text |
010 | Array |
011 | Record |
100 | Floating Point (positive exponent) |
101 | Floating Point (negative exponent) |
110 | Integer (zero exponent) |
111 | Symbol |
Preambles
Every Nota value starts with a preamble byte that is a Kim value with the three most significant bits used for type information.
Most types provide 3 or 4 data bits in the preamble. If the Kim encoding of the data fits in those bits, it is incorporated directly and the continue bit is off. Otherwise the continue bit is on and the continuation follows.
Blob
C 0 0 0 D D D D
- C — continue the number of bits
- DDDD — the number of bits
A blob is a string of bits. The data produces the number of bits. The number of bytes that follow: floor((number_of_bits + 7) / 8). The final byte is padded with 0 if necessary.
Example: A blob containing 25 bits 1111000011100011001000001:
80 19 F0 E3 20 80
Text
C 0 0 1 D D D D
- C — continue the number of characters
- DDDD — the number of characters
The data produces the number of characters. Kim-encoded characters follow. ASCII characters are 1 byte, first quarter BMP characters are 2 bytes, all other Unicode characters are 3 bytes. Every character is carried literally, so the count alone delimits the text.
Examples:
"" → 10
"cat" → 13 63 61 74
Array
C 0 1 0 D D D D
- C — continue the number of elements
- DDDD — the number of elements
An array is an ordered sequence of values. Following the preamble are the elements, each beginning with its own preamble. Nesting is encouraged.
Record
C 0 1 1 D D D D
- C — continue the number of pairs
- DDDD — the number of pairs
A record is an unordered collection of key/value pairs. Keys must be text and must be unique within the record. Values can be any Nota type.
Unordered means the general Nota encoder writes pairs in whatever order it walks the record, which is a property of that record object and not of the keys it holds. Two records holding the same pairs may encode to different bytes, and decoding then re-encoding may reorder them. So general Nota bytes are an encoding of a value, never an identity for it: a hash or a signature belongs over bytes that were kept, not over bytes re-derived from a decoded value. Where a value’s identity is the point, canonical record artifacts use the same Nota wire types through the one canonical encoder, which sorts keys at every depth before appending the pairs.
Floating Point
C 1 0 E S D D D
- C — continue the exponent
- E — sign of the exponent
- S — sign of the coefficient
- DDD — three bits of the exponent
Nota floating point represents numbers as coefficient * 10^exponent. The coefficient must be an integer. The preamble may contain the first three bits of the exponent, followed by the continuation of the exponent (if any), followed by the coefficient.
Use the integer type when the exponent is zero.
The raw codec exposes the decimal pair directly as
nota_write_decimal(buffer, coefficient, exponent) and
nota_read_decimal(&coefficient, &exponent, bytes). Both fields are signed
64-bit integers. The coefficient therefore crosses the wire without conversion
through binary floating point and can carry every DEC64 coefficient exactly.
Writers remove trailing coefficient zeroes while increasing the exponent; zero
is the integer 0, and a resulting exponent of zero uses the integer type.
These rules make the decimal writer emit one canonical Nota encoding for each
representable coefficient/exponent value.
Examples:
-1.01 → 5A 65
98.6 → 51 87 5A
-0.5772156649 → D8 0A 95 C0 B0 BD 69
-10000000000000 → C8 0D 01
Integer
C 1 1 0 S D D D
- C — continue the integer
- S — sign
- DDD — three bits of the integer
Integers in the range -7 to 7 fit in a single byte. Integers in the range -1023 to 1023 fit in two bytes. Integers in the range -131071 to 131071 fit in three bytes.
Examples:
0 → 60
2023 → E0 8F 67
-1 → 69
Symbol
0 1 1 1 D D D D
- DDDD — the symbol
There are currently five symbols:
null → 70
false → 72
true → 73
private → 78
system → 79
The private prefix must be followed by a record containing a private process address. The system prefix must be followed by a record containing a system message. All other symbols are reserved.