oxedyne/fe2o3/fe2o3_sbj/SPEC.md
28.6 KiB, 1 run
created by r1870400018:21940, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | # SBJ v0 — Signed Binary JDAT |
| 2 | |
| 3 | The file format of the oxeweb. An SBJ file is a signed envelope wrapping a tree |
| 4 | of typed nodes, encoded in BDAT (JDAT's binary form). |
| 5 | |
| 6 | This specification is normative. Where it and the implementation disagree, the |
| 7 | implementation is wrong. The conformance fixtures in `fixtures/` are its teeth. |
| 8 | |
| 9 | Companion design document: `~/usr/complement/projects/oxegen/doc/Oxeweb`. |
| 10 | Implementation plan: `~/usr/complement/projects/oxegen/plan/oxeweb_impl.md`. |
| 11 | |
| 12 | --- |
| 13 | |
| 14 | ## 1. File layout |
| 15 | |
| 16 | ``` |
| 17 | +---------+------------+---------------------+---------------+ |
| 18 | | header | envelope | tree, in BDAT | index | |
| 19 | | | key, time, | | (optional, | |
| 20 | | 8 bytes | hash, sig | | derived) | |
| 21 | +---------+------------+---------------------+---------------+ |
| 22 | \___________________/ |
| 23 | hashed and signed |
| 24 | ``` |
| 25 | |
| 26 | Read in order: header, envelope, tree, then anything trailing. |
| 27 | |
| 28 | **The hash covers the tree region only.** Not the header, not the envelope, not |
| 29 | the index. Two files carrying the same tree are the same document at the same |
| 30 | address, whether or not either carries an index, and whoever holds a document may |
| 31 | compute, append, or discard its index freely. |
| 32 | |
| 33 | ### 1.1 Header |
| 34 | |
| 35 | 8 bytes, fixed: |
| 36 | |
| 37 | | Offset | Bytes | Value | Meaning | |
| 38 | |---|---|---|---| |
| 39 | | 0 | 4 | `0x53 0x42 0x4A 0x00` (`SBJ\0`) | Magic | |
| 40 | | 4 | 2 | `u16` big-endian | Format major version. v0 is `0` | |
| 41 | | 6 | 2 | `u16` big-endian | Length of the envelope region, in bytes | |
| 42 | |
| 43 | A reader that does not recognise the magic, or that reads a major version it does |
| 44 | not implement, stops. It does not guess. |
| 45 | |
| 46 | ### 1.2 Envelope |
| 47 | |
| 48 | A BDAT-encoded `Dat::Map` with exactly these keys, all required: |
| 49 | |
| 50 | | Key | Type | Meaning | |
| 51 | |---|---|---| |
| 52 | | `"schema"` | `str` | Payload schema, e.g. `"oxeweb/doc/0"` | |
| 53 | | `"author"` | `bu8` | Author's public key, raw bytes | |
| 54 | | `"sig_scheme"` | `u32` | Namex id of the signature scheme | |
| 55 | | `"hash_scheme"` | `u32` | Namex id of the hash scheme | |
| 56 | | `"time"` | `u64` | Unix milliseconds | |
| 57 | | `"hash"` | `bu8` | Hash of the tree region | |
| 58 | | `"sig"` | `bu8` | Signature over the signing input (§1.3) | |
| 59 | | `"tree_len"` | `c64` | Length of the tree region, in bytes | |
| 60 | |
| 61 | The envelope map obeys the canonical encoding rules of §3, like everything else. |
| 62 | |
| 63 | The `schema` key is what makes SBJ general. An oxeweb document declares |
| 64 | `"oxeweb/doc/0"` and its payload is a node tree (§4). A signed administrative |
| 65 | command would declare its own schema and carry a different payload. The container |
| 66 | does not care. |
| 67 | |
| 68 | v0 defaults: Ed25519 signatures (matching the keys an oxenym already holds) and |
| 69 | SHA3-256 hashes (which is what `fe2o3_hash` implements). Both are named, never |
| 70 | assumed. A signature scheme may be replaced freely, since a signature is checked |
| 71 | once and discarded. A hash scheme may not, because **the hash is the address**. |
| 72 | |
| 73 | ### 1.3 Signing input |
| 74 | |
| 75 | The signature covers, in this order, with no separators: |
| 76 | |
| 77 | ``` |
| 78 | schema length (u32 BE) || schema bytes || sig_scheme (u32 BE) || hash_scheme (u32 BE) |
| 79 | || time (u64 BE) || hash bytes |
| 80 | ``` |
| 81 | |
| 82 | Signing the hash rather than the tree is what binds the document's permanent |
| 83 | address to its author. Including the schema and the scheme ids stops an attacker |
| 84 | re-labelling a signed payload as a different schema, or claiming a weaker hash |
| 85 | function produced the same address. |
| 86 | |
| 87 | **The schema carries its length because it is variable-length and it is not the |
| 88 | last field.** Without the prefix the preimage is ambiguous: `schema` and `hash` |
| 89 | are both variable-length, with only fixed-width fields between them, so a byte |
| 90 | moved from the front of one field into the back of the one before it produces the |
| 91 | same bytes under a different reading. Two envelopes agreeing on nothing — a |
| 92 | different schema, a different pair of scheme ids, a different time and a different |
| 93 | hash — can share one signing input, and therefore one signature. A signature over |
| 94 | an ambiguous preimage does not say what the signer meant it to say, which is |
| 95 | exactly the property §1.3 exists to provide. |
| 96 | |
| 97 | Whether that is reachable depends on what else the format admits, and v0 makes it |
| 98 | hard rather than impossible: one hash scheme, of one digest width, means the |
| 99 | alternative reading needs a hash of the wrong length, which §2 step 4 rejects. |
| 100 | The prefix is here because that is a fact about today's vocabulary and not about |
| 101 | the construction. A second hash scheme of a different width, or a second schema, |
| 102 | removes the accident that is doing the work. The moment to fix a preimage is |
| 103 | before there is more than one thing signed under it. |
| 104 | |
| 105 | Only `schema` needs the prefix. `hash` is variable-length too, but it is the last |
| 106 | field, so its extent is whatever remains — there is nothing after it to steal |
| 107 | from or lend to. |
| 108 | |
| 109 | ### 1.4 Index (optional) |
| 110 | |
| 111 | If present, the region after the tree is a BDAT-encoded `Dat::Map` from node id |
| 112 | (`c64`) to byte offset from the start of the tree region (`c64`). It is derived |
| 113 | data, outside the hash, and is never trusted: whatever it points at is verified |
| 114 | by decoding it. A reader may ignore it entirely. |
| 115 | |
| 116 | --- |
| 117 | |
| 118 | ## 2. Verification order |
| 119 | |
| 120 | A document is verified before it is parsed, and content that fails is never |
| 121 | parsed at all. |
| 122 | |
| 123 | 1. Read the header. Check magic and major version. |
| 124 | 2. Decode the envelope. Check every required key is present and typed correctly. |
| 125 | 3. Check `tree_len` against the bytes available. A tree region shorter or longer |
| 126 | than declared is a rejection, not a truncation. |
| 127 | 4. Hash the tree region with the named hash scheme. Compare with `hash`. |
| 128 | Mismatch is a rejection. |
| 129 | 5. Verify `sig` over the signing input (§1.3) under `author`. Failure is a |
| 130 | rejection. |
| 131 | 6. Only now decode the tree, enforcing the depth limit (§5) *during* decoding. |
| 132 | 7. Validate the decoded tree against the schema and the remaining limits (§5). |
| 133 | |
| 134 | Steps 1 through 5 touch no content. A caller may perform them and never decode. |
| 135 | |
| 136 | --- |
| 137 | |
| 138 | ## 3. Canonical encoding |
| 139 | |
| 140 | The hash is taken over the encoded bytes, so a document must encode to exactly |
| 141 | one byte string, or it has more than one address. Non-canonical bytes are |
| 142 | **rejected**, never accepted and silently re-encoded. |
| 143 | |
| 144 | 1. **Field types are fixed by the schema.** A heading's `level` is a `u8`. JDAT |
| 145 | would happily encode the number 2 as a `u8` (two bytes) or an `i32` (five), |
| 146 | and both decode to the same heading level, giving one document two addresses. |
| 147 | The schema decides, and the decoder checks what it got. |
| 148 | 2. **Maps are `Dat::Map`**, never `Dat::OrdMap`. `Dat::Map` is a `BTreeMap`, so |
| 149 | key order follows the keys. `OrdMap` follows the author's typing. |
| 150 | 3. **Map keys are strings** (`Dat::Str`), lowercase ASCII, and no key may appear |
| 151 | twice. |
| 152 | 4. **No redundant wrappers.** No `Dat::Box`. `Dat::Opt` only where the schema |
| 153 | declares a field optional, and an absent optional field is omitted from the |
| 154 | map rather than encoded as `none`. |
| 155 | 5. **Strings are well-formed UTF-8, and in Unicode NFC**, with no unpaired |
| 156 | surrogates and no control characters (the Unicode `Cc` category: C0, C1, and |
| 157 | delete) other than tab and newline. Carriage return is rejected too, so one |
| 158 | line ending has one encoding and cannot split a document's address from its |
| 159 | twin. |
| 160 | |
| 161 | NFC is what closes the last route by which one logical document could hold two |
| 162 | addresses. The letter é may be written as a single code point, or as an `e` |
| 163 | followed by a combining acute accent. The two display identically, mean the |
| 164 | same thing, and hash differently. Requiring the composed form makes the map |
| 165 | from a document's meaning to its address a function again. The normalisation |
| 166 | is `fe2o3_text`'s, over tables generated from a pinned Unicode Character |
| 167 | Database, and it is verified against the Unicode Consortium's own conformance |
| 168 | suite. |
| 169 | 6. **Integers are exactly the declared width.** No promotion, no demotion. |
| 170 | 7. **Lists are `Dat::List`**, not `Dat::Vek`, even where every element shares a |
| 171 | kind. |
| 172 | 8. **Nothing is carried that has no effect.** A thing a reader would render |
| 173 | identically whether it were present or absent gives one document two |
| 174 | encodings, and so two addresses. Three such things exist, and all are |
| 175 | rejected: |
| 176 | - a `styles` table that is empty, |
| 177 | - a style record that is empty, |
| 178 | - a style defined in the table that no node references. |
| 179 | |
| 180 | This is the same rule as rule 4's "no redundant wrappers", applied to the |
| 181 | style table rather than to the encoding. It is stated separately because it |
| 182 | cannot be checked while decoding: whether a style is referenced is only known |
| 183 | once the whole tree has been walked. |
| 184 | |
| 185 | The authoring compiler canonicalises before signing. The reader pays nothing, |
| 186 | since it is hashing the bytes anyway. |
| 187 | |
| 188 | --- |
| 189 | |
| 190 | ## 4. The node tree (`oxeweb/doc/0`) |
| 191 | |
| 192 | ### 4.1 How a node carries its kind |
| 193 | |
| 194 | A node is a JDAT `usr` daticle: a `u16` kind code, then a `Dat::Map` of fields. |
| 195 | |
| 196 | ``` |
| 197 | (heading|{ |
| 198 | "level": (u8|2), |
| 199 | "children": [(text|"Style without a cascade")], |
| 200 | }) |
| 201 | ``` |
| 202 | |
| 203 | The kind code sits *in front of* the payload on the wire, so the decoder knows a |
| 204 | node is a heading before it reads a byte of the heading. An unknown or forbidden |
| 205 | kind is refused on the spot, and the byte length JDAT puts in front of every |
| 206 | compound says exactly how far to seek to reach the next node. A `"kind"` field |
| 207 | inside the map would require reading the node to find out whether it was allowed |
| 208 | to. |
| 209 | |
| 210 | ### 4.2 v0 node kinds |
| 211 | |
| 212 | Deliberately small. Growth beyond this is handled two ways: a compatible addition |
| 213 | carries a fallback (§4.5), and a breaking change bumps the schema version. |
| 214 | |
| 215 | | Code | Kind | Fields | Children | |
| 216 | |---|---|---|---| |
| 217 | | 1 | `doc` | `title: str`, `lang: str`, `styles: map?` | flow* | |
| 218 | | 2 | `section` | `title: str?` | flow* | |
| 219 | | 3 | `para` | | inline* | |
| 220 | | 4 | `heading` | `level: u8` (1..=6) | inline* | |
| 221 | | 5 | `list` | `ordered: bool` | `item`+ | |
| 222 | | 6 | `item` | | flow* | |
| 223 | | 7 | `box` | | flow* | |
| 224 | | 8 | `image` | `hash: b32`, `alt: str`, `w: u32?`, `h: u32?` | none | |
| 225 | | 9 | `text` | *(the daticle is a `str`, not a map)* | none | |
| 226 | | 10 | `emph` | `strong: bool` | inline* | |
| 227 | | 11 | `link` | `to: address` | inline* | |
| 228 | | 12 | `code` | `lang: str?`, `text: str` | none | |
| 229 | | 13 | `quote` | `cite: str?` | flow* | |
| 230 | |
| 231 | *flow* is `section`, `para`, `heading`, `list`, `box`, `image`, `code`, `quote`. |
| 232 | *inline* is `text`, `emph`, `link`. |
| 233 | |
| 234 | Three further codes are **reserved**. They name facilities the engine has and a |
| 235 | document does not: |
| 236 | |
| 237 | | Code | Kind | Fields | What it is | Where it is legal | |
| 238 | |---|---|---|---|---| |
| 239 | | 14 | `edit` | `name: str`, `placeholder: str?` | An editable text field | A chrome or an application tree | |
| 240 | | 15 | `surface` | `app: b32`, `alt: node+` | A pane an application paints | An application tree | |
| 241 | | 16 | `icon` | `name: str` | One of the engine's own icons | A chrome or an application tree | |
| 242 | |
| 243 | **`oxeweb/doc/0` admits the kinds 1 to 13 and no others.** Its admitted set is |
| 244 | closed. The chrome's address bar and an application's form field are the same engine |
| 245 | facility, reached through the same `edit`, and a document simply cannot name it: a |
| 246 | document carrying any of the three is refused, whole, by the same rule that refuses a |
| 247 | `para` inside a `para`. That is what makes "a document is never a program" structural |
| 248 | rather than conventional. The kinds are reserved here, in the document's own schema, |
| 249 | so that the vocabulary a document is held to cannot grow into them by accident, and |
| 250 | so that a reader meeting one knows what it is refusing. |
| 251 | |
| 252 | An `icon` is reserved for a reason of its own, and it is not that an icon is |
| 253 | dangerous. A document carries a picture as an `image`, which is a content hash: the |
| 254 | picture is the author's, it is held, and it is the same picture wherever it is read. |
| 255 | An icon is the opposite — a name by which a tree reaches the *reader's* drawing. A |
| 256 | document naming one would be letting whichever reader opened it supply the document's |
| 257 | content, and two readers would show two documents. Style may be the reader's, because |
| 258 | style is how a thing looks; content may not, because content is what the author said. |
| 259 | The icon names are also browser actions, and `back` means nothing in a document. |
| 260 | |
| 261 | An `icon` names one of a **closed set**, and a name outside it is refused rather than |
| 262 | drawn as a gap: |
| 263 | |
| 264 | ``` |
| 265 | back forward home add close find |
| 266 | ``` |
| 267 | |
| 268 | The set is closed for the same reason a reserved code is refused where an unknown |
| 269 | code may be admitted by a fallback (§4.5): a fallback exists for what the reader |
| 270 | cannot know, and the reader knows exactly which icons it has. A chrome naming an icon |
| 271 | this version does not draw is a fault in the chrome, and the validator is where it is |
| 272 | caught. Growing the set is a versioned decision, as growing the kinds is. |
| 273 | |
| 274 | An `icon` carries **no geometry and no colour**, as a `surface` carries no geometry. |
| 275 | It is a glyph: it takes its size from `size` and its colour from `fill`, through the |
| 276 | universal `style` field, so an icon in a bar is sized and inked by the same two |
| 277 | properties as the text beside it. |
| 278 | |
| 279 | `text` is the one exception to "payload is a map": its payload is a `Dat::Str` |
| 280 | directly, because a text run wrapping a single string in a map would double the |
| 281 | bytes of the commonest node in every document. `code` carries its source in a |
| 282 | `text` field rather than as children, since a listing is one preserved string |
| 283 | rather than a run of formatted spans. |
| 284 | |
| 285 | A schema fixes two vocabularies, not one: the node kinds above, and the style |
| 286 | properties of §4.4. Both are closed, and a chrome tree is wider than a document in |
| 287 | both — it may carry an `edit`, and it may name `grid`, `border` and `shadow` — |
| 288 | because it is the same engine drawing a real interface. A document may do neither. |
| 289 | |
| 290 | Every node with a map payload may also carry an optional **`style`** field |
| 291 | (§4.4), a string naming an entry in the document's style table. It is left out of |
| 292 | the rows above because it is universal. |
| 293 | |
| 294 | A content hash reference (`image.hash`, and the `hash` form of an address) is a |
| 295 | `b32`, a fixed 32-byte string, matching the width of the v0 hash scheme |
| 296 | (SHA3-256). A variable-length byte string would let the same reference encode two |
| 297 | ways. |
| 298 | |
| 299 | The root node of an `oxeweb/doc/0` payload is always `doc`. |
| 300 | |
| 301 | ### 4.3 Link addresses |
| 302 | |
| 303 | A `link`'s `to` field is a typed address, not a string the renderer parses: a map |
| 304 | with exactly one entry, whose key selects the address kind. |
| 305 | |
| 306 | ``` |
| 307 | (link|{ "to": { "name": "news.cricket" }, "children": [...] }) |
| 308 | (link|{ "to": { "hash": (b32|9f86d081...) }, "children": [...] }) |
| 309 | ``` |
| 310 | |
| 311 | `name` carries a NAMES name (`str`); `hash` carries a content address (`b32`). An |
| 312 | address with no entry, more than one entry, an unknown key, or a mistyped value |
| 313 | is rejected. Making the address typed rather than a string means the decoder |
| 314 | tells a name from a hash, and a malformed target is refused at the door rather |
| 315 | than misread by the renderer. |
| 316 | |
| 317 | ### 4.4 Styling |
| 318 | |
| 319 | The oxeweb replaces the web's cascade with locality. A node names a style; the |
| 320 | style is defined once, in the document's style table; and a short inherited set |
| 321 | flows down the tree. No rule reaches across the document, so a style error cannot |
| 322 | escape the node that made it. |
| 323 | |
| 324 | The **style table** is the `doc` node's optional `styles` field: a `Dat::Map` |
| 325 | from style name (`str`, the same lowercase-ASCII form as a map key) to a **style |
| 326 | record**. A node's `style` field names an entry, which must exist. |
| 327 | |
| 328 | ``` |
| 329 | (doc|{ |
| 330 | "title": "Style without a cascade", "lang": "en", |
| 331 | "styles": { |
| 332 | "callout": { "bg": "muted", "pad": (u8|3), "fill": "ink" }, |
| 333 | "lede": { "size": (i8|1) }, |
| 334 | }, |
| 335 | "children": [ |
| 336 | (box|{ "style": "callout", "children": [ (para|{ "style": "lede", |
| 337 | "children": [(text|"...")] }) ] }), |
| 338 | ], |
| 339 | }) |
| 340 | ``` |
| 341 | |
| 342 | A style record is a `Dat::Map` carrying any of these optional properties, and |
| 343 | nothing else. **`oxeweb/doc/0` admits these eight and no others, and the eight are |
| 344 | frozen.** |
| 345 | |
| 346 | | Property | Type | Inherited | Meaning | |
| 347 | |---|---|---|---| |
| 348 | | `fill` | `str` | yes | Text colour, a palette name | |
| 349 | | `size` | `i8` | yes | Type scale step; 0 is the reader's base | |
| 350 | | `lang` | `str` | yes | Language, BCP-47 | |
| 351 | | `dir` | `str` | yes | `ltr` or `rtl` | |
| 352 | | `bg` | `str` | no | Background colour, a palette name | |
| 353 | | `pad` | `u8` | no | Spacing scale index | |
| 354 | | `align` | `str` | no | `start`, `center`, `end`, or `justify` | |
| 355 | | `radius` | `u8` | no | Corner radius, on the spacing scale `pad` uses; 0 is a square corner | |
| 356 | |
| 357 | `bg` and `pad` make a tinted, padded box a thing a document draws, and `radius` |
| 358 | says what shape that box's corners are. It adds no element, no geometry and no |
| 359 | authority, and a soft-cornered callout, pull-quote or listing is ordinary |
| 360 | typography — which is what tells it from the properties below, whose subject is |
| 361 | the layout of a bar and a shelf and which mean nothing in prose. |
| 362 | |
| 363 | The style vocabulary is the schema's, exactly as the node vocabulary is (§4.2). |
| 364 | A chrome tree and an application tree admit the eight above and five more: |
| 365 | |
| 366 | | Property | Type | Inherited | Meaning | |
| 367 | |---|---|---|---| |
| 368 | | `grid` | `u8` | no | Lay the children out as a grid whose tiles are at least this many base sizes wide, wrapping to as many columns as the width allows, and sharing the width out among them | |
| 369 | | `pack` | `u8` | no | Lay the children out in a row of tiles exactly this many base sizes wide, packed from the start edge and wrapping when they run out of room | |
| 370 | | `grow` | `u8` | no | Take this share of the room a packed row has left over; 0 takes none of it | |
| 371 | | `border` | `[str, u8]` | no | A line round the edge: a palette name, and a width in pixels | |
| 372 | | `shadow` | `u8` | no | How far the node stands off the surface behind it, in whole steps; 0 is lying flat on it | |
| 373 | |
| 374 | ``` |
| 375 | "shelf": { "grid": (u8|14) }, |
| 376 | "tile": { "bg": "muted", "pad": (u8|3), "radius": (u8|2), |
| 377 | "border": ["muted", (u8|1)], "shadow": (u8|1) }, |
| 378 | ``` |
| 379 | |
| 380 | The browser's chrome is an SBJ tree laid out by the same engine as a document, and |
| 381 | it is a real interface: a library of tiles, a navigation bar, a control with an |
| 382 | edge. These five are what it takes to draw one. They are the chrome's and not the |
| 383 | document's because a document is not an interface and must not be able to dress as |
| 384 | one — a published document cannot name them, whatever a later reader learns to |
| 385 | draw, so what a document may look like is settled by the vocabulary it was signed |
| 386 | under. A `shadow` is elevation, the language of a thing that lifts toward the hand, |
| 387 | and a paragraph claiming to float above its page is precisely that dressing-up; a |
| 388 | `border` is the line round the control with an edge. An application's tree admits |
| 389 | them for the same reason it admits `edit` and `surface`: it is a declared program, |
| 390 | drawn by the same engine. |
| 391 | |
| 392 | That rule is enforced by **addressing**, not by the vocabulary. The band and the |
| 393 | bars are painted into pixmaps a document is never handed, so it cannot draw there |
| 394 | whatever it names, and a document that drew a perfect likeness of a control could |
| 395 | still make nothing happen by it — a document carries no program, and a link goes to |
| 396 | an address or nowhere. The vocabulary is drawn where it is because there is nothing |
| 397 | in prose for a shelf's layout to mean, not because a corner is dangerous. All are self-only but `grow`, which is the one property a node names for its |
| 398 | PARENT to read. It is still locality and not cascade -- a parent looks at the |
| 399 | children it is laying and at nothing else, so no node's width depends on anything |
| 400 | outside the row it sits in -- but it is the one place the rule reads sideways |
| 401 | rather than downwards. It is here because a bar of controls cannot be spelt |
| 402 | without it: a bar is some controls and one thing that fills, and something has to |
| 403 | take what the controls did not. A row whose leftover is narrower than one of its |
| 404 | own tiles wraps instead, and the grower, alone on the next line, takes the whole |
| 405 | width -- which is a bar folding on a narrow window, and falls out of the rule |
| 406 | rather than being a case in it. |
| 407 | |
| 408 | `grid` and `pack` are two layouts, and a style naming both is **refused**. A grid |
| 409 | shares its width out among its tiles, which is what a shelf of cards wants -- a |
| 410 | row with a gap at its end reads as a row missing a card. A packed row leaves each |
| 411 | tile at the width it names and ends where its tiles do, which is what a bar of |
| 412 | controls wants -- a lone button stretched across half a window is not a button. A |
| 413 | style asking for both would leave the reader to choose, and which it chose would |
| 414 | be a fact about that reader rather than about the tree. |
| 415 | |
| 416 | A property's **type is the same in every schema; only its admission differs**. |
| 417 | `grid` is a `u8` wherever it is legal, so the canonical encoding rules of §3 pin |
| 418 | its width without consulting the schema, and a document naming `grid` is refused |
| 419 | for naming it, not for how it wrote it. |
| 420 | |
| 421 | A `border` is one property and not two: a colour with no width draws nothing, and |
| 422 | a width with no colour draws nothing, so the two are named together, as a |
| 423 | two-element list, and a half-written border cannot be spelt. Its width is in |
| 424 | pixels rather than on the spacing scale, because a border is a boundary and not a |
| 425 | measure of room: a hairline is one pixel at any text size. |
| 426 | |
| 427 | A `shadow` names an **elevation** and not a shadow: how far the node stands off |
| 428 | what is behind it, and nothing about what that costs in pixels or in ink. The |
| 429 | offset, the softness and the colour are the reader's theme's, for the same reason |
| 430 | the palette is — how a shadow must be drawn depends entirely on what it is drawn |
| 431 | on, and only the theme knows that. A shadow is a dark stain, and a dark stain on a |
| 432 | dark page is nearly nothing, so a theme with a near-black page must stain far |
| 433 | harder than one with a white page to say the same thing. A style that named a |
| 434 | colour would work in one theme and be invisible in the other; a style that names a |
| 435 | height works in both. A shadow is ink and not room: it changes nothing about where |
| 436 | anything sits, so a node that names one occupies exactly the box it would have |
| 437 | occupied without it. |
| 438 | |
| 439 | The **palette** names are `ink`, `muted`, `accent`, and `bg`. They are semantic, |
| 440 | not literal: the reader's theme resolves them to actual colours, so switching to a |
| 441 | dark palette recolours every document without touching one. Sizes are scale |
| 442 | steps, not pixels, so enlarging type reflows rather than clips. There are no raw |
| 443 | pixels and no hex anywhere in v0; that exactness is fixed mode's business, which |
| 444 | v0 defers. |
| 445 | |
| 446 | **Reader preferences are applied after author styles and always win.** An author |
| 447 | declares intent — a scale step, a semantic colour — and the reader's base size, |
| 448 | palette, and direction have the final say. |
| 449 | |
| 450 | A `styles` table with a non-string key, a style record with an unknown property |
| 451 | or an out-of-enum value, or a `style` field naming an entry that does not exist, |
| 452 | is rejected, naming the offending node or style. A record naming a property that |
| 453 | exists but that its schema does not admit — `grid` in a document — is rejected |
| 454 | saying so, and naming the schema that refused it and the trees it is legal in. |
| 455 | "Unknown style property `grid`" would be a lie told to an author whose chrome |
| 456 | draws one. |
| 457 | |
| 458 | ### 4.5 Unknown kinds and forward compatibility |
| 459 | |
| 460 | A reader will meet node kinds it does not implement: a document written against a |
| 461 | later vocabulary, seen by an older client. The web renders unknown tags as their |
| 462 | raw children and never breaks, which is why it can never be versioned or made |
| 463 | strict. Hard rejection is the opposite failure: one unknown node would make a |
| 464 | whole document unreadable to every client that had not yet updated. |
| 465 | |
| 466 | The oxeweb takes neither. A node whose kind code is outside the vocabulary is |
| 467 | permitted **only if** its payload is a map carrying a non-empty **`fallback`**: a |
| 468 | list of nodes drawn from the kinds the reader *does* know. A reader that does not |
| 469 | implement the kind renders and validates the fallback; a reader that does uses |
| 470 | the kind's own fields. An unknown kind whose payload is not a map, or that lacks a |
| 471 | non-empty fallback, is rejected. |
| 472 | |
| 473 | ``` |
| 474 | (table 20|{ |
| 475 | "fallback": [ (list|{ "ordered": (false), "children": [ |
| 476 | (item|{ "children": [(text|"Q1 revenue: 1.2M")] }), |
| 477 | (item|{ "children": [(text|"Q2 revenue: 1.5M")] }), |
| 478 | ] }) ], |
| 479 | "rows": [ ... ] # a reader that knows kind 20 uses this |
| 480 | }) |
| 481 | ``` |
| 482 | |
| 483 | This is the same discipline the `surface` node already carries, a mandatory |
| 484 | semantic alternative, lifted to the vocabulary as a whole. It keeps "reject at the |
| 485 | door" for genuinely malformed content while letting the vocabulary grow within a |
| 486 | major version without stranding readers. The fallback is validated in full, |
| 487 | against the known schema; the unknown kind's other fields are not interpreted, but |
| 488 | are still held to the canonical encoding rules of §3, since they were decoded to |
| 489 | get here. |
| 490 | |
| 491 | **A kind the reader knows, and the schema does not admit, is refused |
| 492 | unconditionally.** A fallback does not admit it, and there is no other way in. |
| 493 | |
| 494 | The rule above is for a code this version has *never heard of*. A fallback earns |
| 495 | such a code its place because the reader cannot know what it means and can still |
| 496 | render something faithful: ignorance is the reason to be generous. The reserved |
| 497 | codes of §4.2 are the opposite case. The reader knows exactly what code 15 is, and |
| 498 | knows that `oxeweb/doc/0` admits it nowhere, so it refuses it on sight, fallback or |
| 499 | no fallback, naming the kind and saying that a document may not carry it. |
| 500 | |
| 501 | The two paths must stay two paths. Collapsing them — treating a reserved code as |
| 502 | merely unknown, and letting a fallback wave it through — would let an author put a |
| 503 | `surface` in a document today, under a fallback that renders innocently, and have |
| 504 | every reader that later learned what code 15 meant begin honouring it: a document |
| 505 | that became a program by waiting. An unknown code is admitted by a fallback because |
| 506 | ignorance is the reason to be generous. A known and inadmissible code is refused |
| 507 | because knowledge is the reason not to be. |
| 508 | |
| 509 | ### 4.6 Node ids |
| 510 | |
| 511 | Nodes are identified by their position in a depth-first, pre-order walk of the |
| 512 | tree, counting from 0 at the root. Ids are not stored in the document. They are |
| 513 | what the optional index (§1.4) maps to byte offsets, and what an error message |
| 514 | names when it rejects a node. |
| 515 | |
| 516 | --- |
| 517 | |
| 518 | ## 5. Limits |
| 519 | |
| 520 | | Limit | Value | Enforced | What it stops | |
| 521 | |---|---|---|---| |
| 522 | | Tree region size | 4 MiB | Before decoding | Runaway documents. Media and fonts are references, not bytes; 100,000 words of prose is ~600 KB | |
| 523 | | Node count | 100,000 | During validation | Layout cost tracks nodes, not bytes | |
| 524 | | Nesting depth | 64 | **During decoding** | Stack exhaustion. A recursive decoder spends a stack frame per level, and a stack overflow aborts the process rather than returning an error, so a legal document must decode within a standard 2 MiB worker-thread stack. 64 levels dwarfs any real document, which nests perhaps 20 deep, and a tiny file describing a million nested boxes is the cheapest attack against a recursive decoder | |
| 525 | | Envelope size | 4 KiB | Before decoding | A header claiming a 64 KiB envelope should not be believed for free | |
| 526 | |
| 527 | The depth limit is the decoder's, not the validator's, and applies before |
| 528 | anything has been verified. The others may be raised on evidence, which is |
| 529 | easier than imposing them later. |
| 530 | |
| 531 | --- |
| 532 | |
| 533 | ## 6. Rejection |
| 534 | |
| 535 | Every rejection names the failing thing: the byte offset, or the node id and its |
| 536 | kind, and the rule broken. "Invalid document" is not an error message. |
| 537 | |
| 538 | A document that fails at any step renders as an error card and is never |
| 539 | partially displayed. There is no repair, no quirks mode, and no best effort. The |
| 540 | web gave that up in 1993 and spent thirty years paying for it. |
| 541 | |
| 542 | --- |
| 543 | |
| 544 | ## 7. Conformance fixtures |
| 545 | |
| 546 | `fixtures/` holds the format's teeth. Each fixture is a directory: |
| 547 | |
| 548 | ``` |
| 549 | fixtures/<name>/ |
| 550 | doc.jdat the document, in JDAT text form (the source of truth) |
| 551 | doc.sbj the canonical signed binary artefact |
| 552 | meta.jdat expected hash, expected node count, expected depth |
| 553 | ``` |
| 554 | |
| 555 | Rejection fixtures carry `reject.jdat` instead, declaring the expected error |
| 556 | (rule broken, offset or node id) so that "it was rejected" cannot pass for "it |
| 557 | was rejected *for the right reason*". |
| 558 | |
| 559 | The v0 suite must include, at minimum: an empty document; one paragraph; every |
| 560 | node kind once; a styled document exercising the style table, an inherited |
| 561 | property, and a self-only property; a link by name and a link by hash; an unknown |
| 562 | kind carrying a valid fallback (accepted); nesting at the depth limit and one past |
| 563 | it; a tree at the size limit and one past it; each canonicalisation rule of §3 |
| 564 | violated exactly once; a truncated tree; a tree one byte longer than `tree_len`; a |
| 565 | corrupted hash; a corrupted signature; a signature by the wrong key; a forbidden |
| 566 | child (a `para` inside a `para`); an empty list; a heading with `level: 0` and one |
| 567 | with `level: 7`; a `style` naming an entry absent from the table; a style record |
| 568 | with an out-of-enum value; an unknown kind with no fallback (rejected); a document |
| 569 | carrying an `edit` node, one carrying a `surface` node, one carrying an `icon` node, |
| 570 | and one carrying a `surface` node that also carries a valid fallback, all four |
| 571 | rejected (§4.2, §4.5); an `icon` naming each of the closed set (accepted in a chrome |
| 572 | and an application tree) and one naming an icon outside it (rejected); a malformed |
| 573 | link address (two entries); and a document whose bytes are valid BDAT but not valid |
| 574 | SBJ. |