Skip to content
docsv0.30.2

Canonical

One spelling for a structure that gets signed or digested (greenhouse decisions/0615). JSON has many spellings of the same object and a signature is over bytes, not over meaning: a grant a person signed as `{"id":…,"to":…}` and a host rebuilt as `{"to":…,"id":…}` would be two documents. So every map is sorted at every depth and every list is left alone — in a map, key order carries nothing; in a list it is the meaning. Part of the contract, not a convenience: a host that keeps a record of grants and receipts digests its lines with the same spelling, so that a judge written anywhere reads the same bytes. THE ORDER IS THE ORDER OF THE BYTES. A map is sorted by the bytes of its keys, the way any language sorts text — never the way one language compares what looks like a number: `"10"` comes before `"9"`, and `"1.0"` and `"1.00"` are two keys with one place each. (Sorted as numbers they were equal, so the order a document was written in showed in its digest: one grant, two or three digests.) WHAT THIS SPELLING IS NOT: something every value survives. A number that is negative zero is written with its sign and read back without it; a whole float comes back a whole number; a float is written with the precision of the process that writes it (`serialize_precision`), so two processes may spell one differently; a map whose keys are 0, 1, 2… in that order comes back a list; a structure nested deeper than JSON is read cannot be read back at all; and what is not a number or not text — `INF`, `NAN`, bytes that are not UTF-8 — is not spelled: it throws `JsonException`. A grant carries none of these: its values are text, whole numbers and true or false. Nothing in this contract reads back what it digested, so its own digests hold; a host that WRITES a line with this spelling and reads it later has to read it back before it trusts it, and not write one that does not say the same.

Canonical::json()

public static function json(array $value): string

The canonical bytes of a structure.

Parameters

Parameters of json()
NameTypeDescription
$valuearray<array-key, mixed>

Canonical::saidOnce()

public static function saidOnce(string $json, string $where): void

Refuse a JSON text that says a key twice in one object — or that writes a list as an object. A reader of JSON takes the last one and says nothing of the first: the text a person is shown and the document the judge reads are then not the same, and whoever drafts it chooses which the person sees. A document a person signs says each thing once — at every depth, and however a key is spelled. AND A LIST IS WRITTEN AS A LIST. An object whose keys are "0", "1", "2"… in that order IS a list once it is read into an array: a second spelling of the same document, with the same digest, in a contract that asks for one. Which of the two was written can only be told from the text, and this scan is the one reading of the text there is — so it is told here, and nothing decodes the text a second time to ask. It answers for any text. One that does not parse — or that is nested deeper than a document is read — is refused before a key is looked at: the scan below walks a text it can trust to be closed, and it used to walk one it could not, and never came back. And it costs what the text is long: a frame is asked for its name, never kept by a second hand, which copied every key an object had said each time one more was read.

Parameters

Parameters of saidOnce()
NameTypeDescription
$jsonstring
$wherestring

Throws

\InvalidArgumentException naming the key, and the object it is said twice in; naming where the list is; or saying the text is not JSON

Canonical::shown()

public static function shown(?mixed $written): string

What a drafter wrote, the way a refusal may say it back: spelled as JSON spells it, in plain characters, and at most a line of it. A refusal is read by a person, often at a terminal, and what it quotes was written by whoever drafted the document — who may not be that person. Said back raw, a key or an id can clear the line it is printed on and leave «OK: … is in force» in its place, or run for three hundred thousand bytes. So nothing a terminal obeys and no character that reorders what is read is ever said back as it came: every one is written as its escape — every letter that is not plain ASCII too, so that nothing said back can pass for this contract's own «…». And the line it is cut to ends where a character ends: half an escape reads as something else.

Parameters

Parameters of shown()
NameTypeDescription
$written?mixed

Canonical::short()

public static function short(string $plain): string

A name this contract already holds to plain characters — the identity of an operation — the way a sentence says it: whole when it is at most a line, and the start of it with its length when it is longer. Such a name needs no escape: its own pattern leaves nothing in it to obey. It has no length of its own though — a hundred thousand letters are one name — and every sentence that said «run `…`» repeated them all.

Parameters

Parameters of short()
NameTypeDescription
$plainstring

Canonical::said()

public static function said(string $sentence): string

A sentence a HOST hands the judge — why a grant is stopped, why its signature does not hold — the way a verdict repeats it: as it came when it is one line of plain text, and at most {@see self::SAID} characters of it; spelled out by {@see self::shown()} when it is anything else. A reason is what a person reads to know what to do next, and it names things they have to cite — the digest of a suspension, the key that left: cut to the line `shown()` allows and written in escapes, it would no longer say them. So a sentence that is plain is repeated plain. What it never does is reach a terminal with a byte the terminal obeys, or run for as long as whoever wrote it chose.

Parameters

Parameters of said()
NameTypeDescription
$sentencestring

Canonical::only()

public static function only(array $data, array $known, string $where): void

Refuse a key this contract does not read. A document a person signs must not carry a line the judge ignores: someone who writes `"budget": 50` believes they bounded something, and a reader that skipped it in silence would let them keep believing it.

Parameters

Parameters of only()
NameTypeDescription
$dataarray<array-key, mixed>
$knownlist<string>
$wherestring

Canonical::text()

public static function text(array $data, string $key, string $where): string

A required, non-blank string of a document, AS IT WAS WRITTEN — and one line of plain text ({@see self::oneLine()}). Nothing is taken off its ends. A reader that trims a text repairs it: the document a person signed and the one that is kept are then two, and what was trimmed — a line break, a tab, a null byte — was written by someone. A text that is not as it reads is refused, and whoever drafted it writes it again.

Parameters

Parameters of text()
NameTypeDescription
$dataarray<array-key, mixed>
$keystring
$wherestring

Canonical::oneLine()

public static function oneLine(string $text): bool

Whether a text is ONE LINE OF PLAIN TEXT: read as it is written, with nothing in it that is not seen (decided by Rod, 2026-10-12). A grant is read by a person before they sign it, and again every time a house lists it or a judge names it in a refusal. A text that carried a line break, an escape, a character that turns the direction text is read in — or one that is not shown at all — would be read otherwise than its bytes: a condition's name could clear the line a terminal printed it on and leave «OK» in its place, and two names that read the same could be two. WHAT IS KEPT: the letters of every language, their accents, digits, punctuation and symbols — and the ordinary space (U+0020), as many as the sentence has, inside it. WHAT IS REFUSED, exactly — every text a grant keeps is refused when it is read if it carries one: · a control character: the C0 controls, DEL and the C1 controls (U+0000–U+001F, U+007F–U+009F) — the line break and the tab are two of them; · a FORMAT character, every one (general category Cf): the direction controls (U+061C, U+200E, U+200F, U+202A–U+202E, U+2066–U+2069), the zero-width space, non-joiner and joiner (U+200B–U+200D), the soft hyphen (U+00AD), the word joiner and the invisible operators (U+2060–U+2064), U+206A–U+206F, the byte order mark (U+FEFF), the interlinear annotation marks (U+FFF9–U+FFFB), the tags (U+E0001, U+E0020–U+E007F), the Mongolian vowel separator (U+180E) — and the rest of that category, some of which do draw something: U+0600–U+0605, U+06DD, U+070F, U+0890–U+0891, U+08E2, U+110BD, U+110CD, U+13430–U+1343F, U+1BCA0–U+1BCA3, U+1D173–U+1D17A; · the line and the paragraph separator (U+2028, U+2029); · every space that is not U+0020 (general category Zs): U+00A0, U+1680, U+2000–U+200A, U+202F, U+205F, U+3000; · a variation selector (U+180B–U+180D, U+180F, U+FE00–U+FE0F, U+E0100–U+E01EF) and the combining grapheme joiner (U+034F); · a vowel or a filler that prints as nothing: U+17B4, U+17B5 (the Khmer inherent vowels), U+115F, U+1160, U+3164, U+FFA0, U+2800, and four that are blank by their own definition — U+13441 and U+13442 (the full and the half blank of Egyptian hieroglyphs), U+16FE4 (the Khitan small script filler), U+1D159 (the musical null notehead); · a private-use character (Co), a place nothing is assigned to (Cn — the noncharacters and the rest of the tag block among them), and bytes that are not text at all — a surrogate among them, which UTF-8 does not spell; · and a SPACE AT EITHER END: a text is given as it reads, and nothing trims it. AND ({@see self::MARKS}; decided by Rod, 2026-10-12): a text that begins with a combining mark, or carries more than eight of them in a row, is refused too. WHAT THAT COSTS, and was accepted: a no-break space, a soft hyphen, a zero-width joiner or non-joiner and a variation selector are part of how some languages and some emoji are written, and a text that needs one is not taken. WHICH PLACES ARE ASSIGNED, and which characters are format characters, is read from the Unicode tables of the PCRE the host runs (the lists above are those of Unicode 16). A host with older tables refuses a character a newer one takes — a letter added to Unicode since — and a document that uses none of those is read the same everywhere. THE RULE BEHIND THE LIST: no code point Unicode itself says is shown as nothing — its property `Default_Ignorable_Code_Point` — is taken. The list above covers that whole set, and more; the fillers are not of that set, and are named one by one. WHAT THIS IS NOT: a promise that two texts that look the same are one. A letter of another alphabet that looks the same is text, and so is a mark that draws nothing of its own after a letter of another script.

Parameters

Parameters of oneLine()
NameTypeDescription
$textstring

Canonical::notOneLine()

public static function notOneLine(string $what, string $text): string

What a refusal says of a text that is not one line of plain text; `$what` names it, as «X gives «key»».

Parameters

Parameters of notOneLine()
NameTypeDescription
$whatstring
$textstring