# TSON Developer Guide

**Status:** Non-normative companion to the TSON specification series, aligned with 2026 Revision 38. This guide carries the standing rationale, extended worked examples, and deployment guidance that the specifications reference but do not carry. It describes the design as it stands; it is not a change history — what changed between revisions, and why, lives in each revision's change log. Nothing here is normative; where this guide appears to disagree with [TSON-DATA], [TSON-SCHEMA] or [TSON-JSON], the specifications govern.


## 1. What TSON Is

TSON is a schema system with its own notation. At its centre is a type system ([TSON-SCHEMA]): immutable, hash-pinned schemas whose definitions are themselves data, resolving down a verified chain — document → schema → meta-schema → kernel — so that one hash authenticates a document together with its entire contract. The TSON text format ([TSON-DATA]) is that system's notation and its reference encoding: a Unicode-first notation in the JSON family, pleasant enough to use on its own, typed even without a schema — and not a JSON superset, a claim the series gave up deliberately (§2.8). Schemas are the point; the text format is how they are written down — and the first of the encodings that carry them.

If you arrived here thinking "another JSON dialect", that is a reasonable first impression and a wrong one. The unquoted names and optional commas are real, and they matter for daily use — but they are the notation's manners, not the system's identity, and JSON itself is read through a reader of its own, as a second encoding of the same model (§1.4). The nearest relatives are not JSON5 or JSONC but Avro and ASN.1: systems where the schema is the product and wire formats serve it. TSON takes that architecture and adds three things those systems never had together: schemas that are ordinary, hash-verifiable documents in the same notation as the data; a schemaless mode that is still typed; and an extension model where new type vocabularies arrive as data, never as grammar changes.

### 1.1 The chain, walked once

Everything in TSON hangs off one picture. Here is a data document:

```
!!schema:"https://example.com/people.tn?sha256=c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5"
!person { name: "Ada Lovelace"  born: 1815-12-10 }
```

The `!!schema` directive names the contract, pinned by content hash. `!person` names which of the schema's types this value instantiates. Note what `born` looks like: an unquoted token, no annotation. The schema declares the field as a `date`, so the date atom parses it — the data stays clean because the contract carries the types.

The schema it names is itself a TSON document:

```
!!id:"https://example.com/people.tn"
!!meta:"https://tson.io/2026/38/m/meta.tn"
!!import:"https://tson.io/2026/38/m/core.tn"
{
  person => { name: text  born: date }
}
```

Same lexer, same tokens, same tooling — a second body grammar behind the same header. And the schema stands on the same relation it offers: its `!!meta` names the meta-schema that validates *its* declarations, and its `!!import` brings in the core type library that defines `text` and `date`. The meta-schema chains to the meta-kernel; the kernel's `!!meta` names itself, and the circle is closed not by resolution but by pre-loading — implementations ship the kernel's resolved structure, and the kernel document is the TSON encoding of it. The in-memory model is authoritative; the file describes it.

Every rung is immutable and hash-pinnable, and hashes attach to canonical identities rather than URLs, so a consumer holding one hashed reference can verify a document together with its schema, that schema's meta-schema, and the kernel — the entire contract, authenticated from a single identifier, with no network access required or trusted (§3.3–§3.4 below detail the identity and verification discipline). Schema URLs are names resolved through a local library; fetching is an opt-in way to populate it, never the meaning of a reference.

That is the product. Everything else in the series — the grammar, the resolver, the encoding rules — exists to make that picture true.

### 1.2 Immutable means versioned

The schema in §1.1 will never change. Not *should not* — cannot: a schema's identity is its exact byte content ([TSON-SCHEMA] §3.5), and the hash in the data document's `!!schema` pins those bytes. Editing `people.tn` does not update the schema; it creates a different document that no existing reference names.

So when the contract needs to grow, you publish:

```
!!id:"https://example.com/people-v2.tn"
!!meta:"https://tson.io/2026/38/m/meta.tn"
!!import:"https://tson.io/2026/38/m/core.tn"
{  person => { name: text  born: date  email: text }
}
```

Look at what just happened: `email` is **required**. In every mutable-schema system, that is the forbidden edit — protobuf marks `required` *"Do not use"* because a required field added to a shared definition breaks every document already written; Avro demands a default; GraphQL says deprecate, never remove or tighten. Every one of those rules exists because a single definition must serve every document ever written against it. Here it doesn't have to: the §1.1 document names `people.tn` by hash and validates against it forever, untouched by v2's existence; new documents name `people-v2.tn`. Two contracts coexist, each at full strength. Neither is "the" schema — *the* schema is not a concept in this system; a *document's* schema is. (The `-v2` in the name is a convention for humans; the system's notion of version is identity plus hash, and nothing parses the suffix.)

Once that lands, the ripples reach nearly everything:

- **Required means required.** No leaving fields optional for future-proofing, no reserved field numbers, no tombstone fields carried forever. Records are closed under their type ([TSON-SCHEMA] §7.2): a v2 document without `email` is invalid, and a v1 document is never asked for one.
- **Unknown fields need no tolerating.** The "must ignore unknown fields" robustness rule exists to survive in-place evolution; with no in-place evolution, closure is safe — and a typo in a field name becomes an error instead of silently discarded data.
- **Acceptance is explicit.** A reader declares which versions it accepts by registering exactly those schemas in its library — and because the version key sits on the document's opening lines, readable before any value parsing, a front door can route v1 and v2 traffic to different servers without opening the body.
- **Migration is a function between two known values.** Both schemas are data (§1.3), so "what changed between v1 and v2" is a structural diff a tool can compute, and a v1→v2 transform has a precise input contract and a precise output contract.

Every design choice in §1.1 was quietly serving this moment: hashes attach to canonical identities so a pin survives mirroring; the id line is excluded from its own hash so a document can carry its name; the header classifies in two directives so routers never parse bodies. The full versioning story — including why routing by schema beats binding one server to every version, and the compatibility-rulebook history this model retires — is §9.1.

### 1.3 Schemas are data

A schema document *resolves* to a value: a namespace from type names to values, every one of them expressible in ordinary TSON, and every entry simply *being* the value it denotes. The `person` declaration above resolves to:

```
person => !record { fields: {
  name => { type: text }
  born => { type: date }
} }
```

Output is source with the sugar expanded, bodies flattened and IS-A chains computed — a refinement such as `age => !integer ^ { min: 0 }` comes back as `!integer_type { chain: { supertypes: [integer] }  min: 0 }` — and a record's fields are a map keyed by name, since a name is what a field is reached by.

This reflexivity is load-bearing, not decorative. It means resolver output can be serialized, diffed, and shipped as fixtures; it means the meta layer can describe itself; and it means the system grows by publishing new schema documents — new type libraries, extended meta-schemas — rather than by amending a grammar. The kernel and the grammar are frozen at version 1; the meta layer is the sanctioned extension point, and extensions travel as data down the same verified chain as everything else.

It also dissolves the old bootstrap riddle. "What validates the meta-kernel?" has a concrete answer here: the pre-loaded meta-kernel, which exists as implementation structure before any document is parsed. The self-reference in the kernel's header is a description of that fact, not a dependency to resolve.

The research behind the design calls this discipline the **proto-schema** (§2): every schema in the system, walked down its chain, flattens to the meta-kernel — a handful of kinds, a few constructors, and one abstract root, `top`, whose subtypes describe every entry in existence including themselves — the kernel's `record` is a `!record`. The closest ancestor in spirit is Lisp. Where Lisp reduced computation to its purest form — programs written in the language's own data structures, an evaluator expressible in the language it evaluates — the proto-schema attempts the same reduction for description: data described by data, all the way down to a kernel that describes itself. Homoiconicity, for contracts rather than code.

### 1.4 Typed without a schema

Most schema systems make you choose: protobuf will not run without an IDL; JSON Schema leaves untouched data untyped. TSON is gradually typed. With no schema in scope, base type resolution gives you JSON's value model plus real numbers — hex, binary, digit separators, arbitrary precision — and the built-in type vocabulary gives you common typed values by annotation alone:

```
{
  id:     !uuid 550e8400-e29b-41d4-a716-446655440000
  price:  !number 19.99
  placed: !datetime "2026-07-18T09:30:00Z"
  digest: !bytes "TyqQ3hHDt6Y="
  lead:   !duration PT36H
}
```

Each annotation invokes a parsing contract — the value becomes a UUID, an exact decimal, an instant, octets, a count of seconds — with no schema anywhere. The schemaless built-ins and the core type library denote the same contracts *and the same value spaces*, so annotations added on day one mean the same thing under the schema bound on day ninety. The adoption path runs from "annotate the values that matter" to "bind a schema and pin it by hash", each step optional and each step reversible.

JSON sits beside this rather than underneath it. A JSON document is not a TSON document — it may carry `null`, which this notation has no word for, and keys that are not names — so a processor that reads JSON does so through a **JSON reader**: one more encoding of the same model, whose rules are [TSON-JSON]'s — JSON `null` is the void sentinel's spelling (the position's declaration decides whether a void value is admitted), a JSON number is `number`, an object is a record where its keys are declared fields and a map where the schema says so ([TSON-DATA] §6). Under a schema the reader validates on the same terms as the text notation; schemaless, it reads on base type resolution's — and base resolution itself is now stated over what *types a position* rather than over the header, so a processor binding a schemaless document into a host type that names a family Part 1 defines reads by that family's contract, exactly as a schema declaring it would ([TSON-DATA] §4.1). What the notation gives up by not being a superset is one keyword and a handful of rules that existed only for the claim; what it gains is that JSON compatibility becomes a reader's job, done properly, instead of a property the notation had to keep at the cost of its own consistency (§2.8).

### 1.5 One model, many encodings

The type system stands above any particular wire form. Inside the specification this shows up as a discipline: a token's quoting is lexical necessity, never meaning (`!number 10.2` and `!number "10.2"` are the same value); a set is a schema property carried in array syntax, because "unordered" is an instruction to the reader, not a property of linear data; whether a `!variant` tag may be omitted rests on a disjointness fact the resolver derives independently of any encoding, with each encoding stating its own discrimination rule over it.

Part 2's §7 — the Text Encoding Rules — is the first instance of the pattern: how the type system's values are carried in TSON text. Other encodings state their own rules against the same model. A JSON encoding must decide what a tuple, a typed map key, or the void sentinel become in a poorer notation; those decisions are parameters, and because schemas are data, the parameters themselves can travel as schema-governed documents beside the schemas they configure. The type system decides *what the value is*; an encoding decides *how it is spelled* — and Revision 35 made that sentence normative as the value-space clause ([TSON-SCHEMA] §5.5): a type denotes a value space, an encoding defines a lexical space over it, and equality, ordering, refinement, disjointness and content addressing are defined over values only. Part 3 ([TSON-JSON]) is the second instance, and the pattern held: it states atom forms, container syntax, void and a discrimination predicate against the same model, and where the two encodings looked as though they would differ — a choice untagged in text and tagged in JSON — the difference turned out to be a fact the model had mis-stated, and moved into Part 2 (`disjoint` now carries class stability, [TSON-SCHEMA] §5.4). The clause also sorts encodings into *classes* — TSON text and JSON both spell `bytes` in an alphabet and an instant with an offset; an octet encoding writes the bytes and the instant and neither. A representation directive in a schema would bind a class, never one member of it, and the class is named by the encoding-rules documents that claim the directive ([TSON-SCHEMA] §6); the series declares none today, its one candidate (`@rest`) having failed to name a class at all, and the facts that were briefly annotations — how a record may be realised, which fields select its members — turned out to change what conforms and became grammar (§2.10).

### 1.6 Reading the series

The series is three documents and nine artifacts:

- **[TSON-DATA] — Part 1: Text Data Format** — stands alone: the lexer (shared by the whole series), the data grammar, base type resolution, and the built-in type vocabulary. If you are writing a parser, an editor mode, or a formatter, it is the whole job — start here and stay here until it passes the data-format test suite. A Class 1 processor needs nothing from Part 2 and remains a complete, useful tool: a notation with the manners of JSON and types of its own.
- **[TSON-SCHEMA] — Part 2: Type System and Schema** — the centre of the series: a second body grammar over the same lexer, the type system, the schema chain, and resolver output. If you are writing a validator or resolver, this is your contract. Read §3 (the schema chain) first — it is the picture above, made normative — then §4–§5 for the type system and its grammar, §8 for what resolution produces, and §7 for how typed values are carried in text.
- **[TSON-JSON] — Part 3: JSON Encoding** — the second encoding: how schema-governed values are carried in RFC 8259 JSON, with three `$` members — used, not reserved — for what JSON has no slot for, `null` as the void sentinel, a sealed family dispatched on its discriminator members, and the `TSON-Schema` header field that names a schema out of band. Read it after Part 2 §7, which is the shape it follows.
- The **companion artifacts** — `meta-kernel.tn`, `meta.tn`, `core.tn`, `net.tn`, `policy.tn` and the four resolved fixtures — are the normative vocabulary and the reference answers: the system describing itself. Implementations pre-load the kernel and meta as in-memory structures; the documents are *descriptions* of those structures, and round-tripping them is the first serious integration test. Reading the kernel after Part 2 §4 is the fastest way to make the type system concrete.

A useful reading order for implementers: Part 1 §7 (the lexer and grammars), Part 1 §2–§4 (documents and base resolution), Part 2 §3 (the schema chain), Part 2 §5 (the type-definition grammar), Part 2 §8 (resolver output), then the fixtures. This guide carries what the specifications exclude on principle — rationale, design history, worked examples, deployment guidance — and nothing in it is normative.


## 2. Design Rationale

TSON's schema model was derived rather than assembled from precedent. The proto-schema research series (tson.io/research/proto-schema/) starts from the physical constraints of serialized data — linear, immutable, finite, divisible — and derives what a schema can be: Sequence and Choice as the structural primitives; Tuple, Record, Array, Map, and Set as the configurations of Sequence worth naming; templates as definitions with blanks on a spectrum of completeness; composition with narrowing rules that preserve substitutability; required-by-default multiplicity with void as a sentinel rather than a type. The kernel is that conclusion made executable.

This section carries the *standing* rationale for the decisions the specifications state without argument — why each rule is what it is today. It is deliberately not a decision archive: superseded designs, the paths not taken, and what changed between revisions live in each revision's change log, not here.

### 2.1 No comments

TSON has no comment syntax. This is not an oversight inherited from JSON but a decision made deliberately: comments are metadata, and TSON already has a metadata channel with defined semantics — annotations. A comment is invisible to the data model, gets lost on round-trip unless implementations invent preservation rules, and inevitably becomes a side-channel for machine-readable content (as happened with JSON parsers that accept `//` and the ecosystem of magic comments in YAML). `@doc:"..."` does everything a comment does, survives round-trips by rule, is attached to a specific value rather than a lexical position, and is typed and validatable when a schema is in scope. The cost — you cannot annotate *nothing*, or comment out a region — is accepted: TSON documents are interchange artifacts, not source code.

### 2.2 No anchors, references, or merge operators

The locality principle ([TSON-DATA] §1.2 principle 5) says a value is fully local: what appears at a position is the complete value. YAML's anchors and merge keys are the counter-model, and their costs are well documented — reference cycles (the "billion laughs" family), non-local reasoning (you cannot review a value without scrolling to its anchor), and merge semantics that differ across implementations. TSON's position is that reuse belongs to the producing application, not the wire format. Where genuine cross-document reference is needed, it is explicit and typed: a `!uri` value plus an application-level dereference policy, or a scoped position in the schema layer (§6.4).

### 2.3 Why `!` takes constructors only

In the type-definition grammar, `!` targets constructors exclusively — entries that IS-A `top`, which is what "describes a type rather than a part of one" comes to once IS-A stops at construction — and `^` is the refinement operator — `T ^ { ... }` for record types and, in a meta-schema, for constructors; `!I ^ { values }` for atom instances. The division exists so that a reader never needs the resolver's lookup order to know what a declaration does: the prefix names the rung, `^` names the operation, and IS-A creation is visible at the head of the line. There is no marker for constructor-ness, and Revision 35 removed the one there was: every question the `~` marker was asked — may this be applied, may this be refined as an atom, who may declare it — turned out to be answerable from the supertype chain, and better, since the marker had been asserting a level over chains that did not support it and needed a hard-coded exception for the kernel's `reference`, which describes no value and is nonetheless a type. It also preserves one invariant across every grammar in the series — `!T x` describes a value shaped by `T`, in schema source, in data documents, and in resolver output alike — because the alternative (letting `!` also target instances for narrowing) gives one surface string opposite meanings in the schema and data grammars. Bare juxtaposition is excluded on the same grounds: `name {` is a parse error whose diagnostic suggests `^` or `&`, so the difference between preserved and absent IS-A is two explicit operators, never one easily-missed ampersand.

### 2.4 Subtraction breaks IS-A on purpose, at the head

Subtraction (`account - { password }` — a removal clause on a construction head) is the operation type systems usually refuse to provide, because removing a field from a subtype violates substitutability. TSON provides it *and* makes it break IS-A, which dissolves the objection: `account_public` is not claiming to be usable where `account` is expected — the resolver leaves its chain empty precisely so no consumer can treat it as one. Where it came from is in the source document, which has it; Revision 37 kept a second "lineage" list in the body for it, and Revision 38 removed it on finding that nothing read it after resolution (§8.2) — what a type *promises* is the chain, and what it was *derived from* is provenance a resolver need not carry. The practical use cases — view types, redaction shapes, public projections of internal records — are common enough that forcing authors to re-declare near-duplicate records would be the worse outcome.

The clause sits at the head, not in the body, so the contract break is visible on the declaration line: an `&` head promises IS-A per parent, and nothing three lines down may silently revoke it. Taxonomically subtraction is composition's sibling — a construction that computes a field set and disclaims the contract — so it declares itself where constructions do, `^` heads reject it outright (an operator that promises IS-A cannot host the operation that revokes it), and empty subtraction is ungrammatical rather than merely prohibited. The clause also keeps removal a full grammar position away from its semantic opposite, `field?: void?` (keep the field, forbid its value, preserve the contract). Subtraction is also the one operation a FINAL record admits (§2.10): FINAL forbids the IS-A edge, and subtraction never mints one.

### 2.5 Why elided modifiers are safer than they look

In a refinement body, `field: = value` (no type-ref) inherits the field's type and changes only its state. Allowing this looks like a convenience with a trap — what if the author typos the field name? — but the grammar closes the trap: a modifier-only entry names no type, so it *cannot* declare a new field, and an unmatched name is a resolver error rather than a silent addition. Compare the alternative, requiring the type to be restated: now a refinement that tightens `spec: uri` to a fixed value must repeat `uri`, and if the source declaration later changes the field's type, every downstream refinement silently pins the *old* type or errors at a distance. Elision makes the common tightening robust against upstream type changes; restating the type is reserved for the case where the tightening genuinely narrows the type, which is exactly when the author should be explicit.

### 2.6 Field groups, and the labelled sum they make derivable

A lower bound that is inclusive *or* exclusive — one location, one occupant, discriminated by field name — is a labelled disjunction, which is a *shape*, and a schema language that cannot state its own recurring shapes in shape is under-serving its meta layer. Field groups ([TSON-SCHEMA] §5.11) make the illegal state (`{ min: 0 exclusive_min: 0 }`) unrepresentable, and they sort coherence rules into a clean taxonomy: exclusion among fields is a group; co-presence dependency is a factored sub-record (`integer_size` pairs `bits` with `signed` structurally); value-level relations (`min ≤ max`) remain schema-load checks, because no shape can capture a relation between values. In resolver output, groups flatten — members become ordinary fields with `optional: true` and the grouping recorded beside them in `record.groups` — following the general storage rule: canonical output stores derived data only when derivation is non-local (`subtypes` earns its storage; group membership, locally derivable, compiles into a per-record table at load).

Groups are also why TSON ships only one *closed* sum primitive (the open one, `scoped`, names namespaces rather than variants, §6.4). `choice` is the *unlabelled* form — distinct variant types, discriminated by the `!variant` tag or by discrimination class where the variants are distinguishable by form ([TSON-SCHEMA] §5.4); the assertion `@disjoint` makes is that weaker, more useful one — distinguishable, not mutually exclusive in inhabitance, since `text` admits every token and `42` inhabits both variants of a disjoint `( int32 | text )`. The labelled form needs no primitive because groups derive it: a record whose entire body is one group that must be chosen, each option one field, admits exactly one field, and `timestamps => { ( created: datetime | modified: datetime | accessed: datetime ) }` *is* the labelled sum — named, referenceable, discriminated by label, encoding as the single-field record `{ modified: … }`. The untagged-untyped corner — JSON Schema's `anyOf`, discrimination by trying every variant against the structure — is deliberately unoccupied. Two residues: the labelled-sum pattern resolves to kind PRODUCT, so lowering it to a native sealed variant is a code-generator recognition (one such group, no other fields); and because records refine where `choice` cannot, the labelled sum acquires a subtyping story — a refinement can pin or forbid variants — that a primitive sum would not have had. The third shape, alternatives told apart by a *member they carry* (OpenAPI's discriminator, serde's internally tagged enum), is neither a choice nor a group but a record family with a selector field (§2.10).

A group is a presence rule, and "exactly one of" and "at most one of" are only two of the presence rules real contracts state. "A contact needs an email, a phone, or both" had no spelling: a group that must be chosen refuses both present, optional fields admit neither, and a choice of records needs one variant per non-empty subset — three for a pair, thirty-one for five — every one of them brace-class and so tagged in JSON, which changes the data. A hand-classified census of the presence rules in SchemaStore found 113 sites of this family: at-least-one-of a set of keys, one key requiring another, and compounds such as "`include` alone, or at least one of `name` and `type`". The answer was not a third group state but a second dimension: **an option may hold several fields**. `|` separates options, whitespace separates the members of one, a `?` on a member's name marks it optional once its option is chosen, and three rules decide the rest — an option is chosen when any member is present, a chosen option must hold every unmarked member, and a group admits exactly one chosen option, or at most one where it is optional ([TSON-SCHEMA] §5.11). `( host: H  port: P | socket: S )` is either-pair-or-single; `( b: B  a?: A )?` is "`a` requires `b`"; `( a: A  b: B )?` is both-or-neither. "At least one of" is the one-option group whose members are all marked — that option must be chosen, and any member chooses it — and it is written `( email: E | phone: P )+`, the sugar being the only spelling so that the kernel form never appears in source. That `+` is the one lexer change of the revision: a bare `+` joins the special tokens under `-`'s boundary rule, `+5` unchanged.

Two disciplines keep the generality honest. **Each presence rule has one spelling**: a group that restates plain fields, or another group, is refused with the spelling it restates — `( a?: A | b: B )` is `( a: A | b: B )`, `( a: A )?` is `a?: A` — a rule set checked by enumerating every group over two, three and four members, so that no accepted group equals plain fields or another accepted group, and every option of a declared group can be chosen on its own. And **the in-option mark belongs to the group, not the field**: a member's `record_field.optional` stays `true`, so that `optional` keeps meaning "the key may be missing", a consumer that ignores groups reads every member as optional — loose, never wrong about a valid document — and presence logic is stated in one place. What the form does not reach is stated rather than stretched to: a requirement triggered by a key's *absence*, a field under two rules, a chain of three, and a condition on a value stay sealed families or migration cost, and nested groups were declined, one site in the census against a recursive grammar, a recursive kernel type and recursive sum types in every generator. 97 of the 113 sites are spelled, and all 55 of the at-least-one shape.

### 2.7 Templates, sugar, and synthetic entries

The container sugar is grammar over parameterless constructors: `[T; 1..100]`, `{K => V ; 1..}`, `[T?]`, `{K => V?}`, and their kin desugar to `!array` and `!map` binding records by a fixed table — the two containers carry the same `voidable` facet, so a void value is opt-in for a map's value exactly as for an array's element — a type slot is an ordinary required field typed by a *kind* (`type_kind`, §6.8) and filled like any other required field, and the value-level residue is the family's own coherence rule (`min_items ≤ max_items` and its kin) — checked at schema load for literal bounds and again at materialisation for parameter-bound ones, since an open bound has no value to relate ([TSON-SCHEMA] §5.3, §8.2). Abstraction lives in **templates**: parameters route into slots at application sites inside template bodies, and the container constructors themselves stay parameterless. Resolved output *does* carry open entries — a record template appears as a `!record` with `parameters` in place, every other template as `!template { parameters: […]  template: "…" }`, the held application as text — but every entry a data document's type can reach is closed, so a consumer of closed entries never meets one ([TSON-SCHEMA] §1.3, §5.10, §8.1).

Every application denotes an entry. A sugar form at a use site lifts to a **synthetic entry** — closed for a concrete form, open (a held application) for a parameter-bearing one — and a fully-bound template application denotes its **expansion** — the closed synthetic the substituted form lands on, or the declaration that names the application; a use site holds a bare reference to its entry, so a consumer walks names, never a second structural interpreter, and recursion has the entry it needs to tie the knot ([TSON-SCHEMA] §5.3, §8.2). Revision 36 settled *whose* entry an instantiation is: a declaration that names an application — `bx => box<text>` — **is** the entry, closed in place under the author's name, and only an application no declaration names mints one. The axis is construction against reference, and an application had been filed by its syntax (a parameterised reference) when it is a construction in substance; the cost of the old filing was a `subtypes` index reading a minted hash where the author had written `dogs`. A sugar form keeps minting, deliberately: an array or map is the same definition wherever it is written and takes part in no IS-A, so a content-keyed entry is the right one to share, where a record's declared name has to be its entry for a family and a tag to reach it. Identity is structural for the *minted* half — closed synthetics by body, open ones up to parameter renaming, a declared instantiation by the canonical application recorded as `@application` on its key, with an argument's reference chain followed so that a rename is transparent — so spelling variance never forks an entry and the two channels dedupe against each other's products; minted names are internal and carry no information, and no consumer contract may key on one (§8.1 below). A *declared* entry's identity is its name, however alike two bodies are.

The theoretical basis is the **spectrum of completeness** developed in the proto-schema research series (Part 5, *Templates* — tson.io/research/proto-schema/part-5-templates/): data and schemas are one continuum distinguished by how many blanks remain, and a template is a definition awaiting completion. The design makes the spectrum mechanical, and Revision 34 made it uniform: an open definition's body is the constructor application **as written, held unread** until its parameters are substituted away — `<T, N> !array { element_type: T  min_items: N  max_items: N }` is stored as exactly that text, and closing it means replacing the parameter tokens and reading the result, once, against `array`'s vocabulary ([TSON-SCHEMA] §5.10). The alternative — quoting an open body slot by slot into a typed vocabulary with a labelled channel per slot kind — has to grow a spelling for every kind of slot a parameter can reach, and a collection slot has no natural one, which is how a design can end up unable to write `result => <T> ( T | error )`, the most ordinary generic of all. Holding needs no vocabulary and has no such boundary: a parameter in a value slot, a type slot, or a variant list is a token like any other, substitution is one walk at any depth, and the only cost is shadowing's usual one (a literal spelled like a live parameter is unreachable — rename the parameter). What the typed quotation bought — that body identity not depend on a spelling choice — holding gets by requirement instead: the open form has one spelling, however many phases produce it. The completeness coordinate is then simply *which tokens still name parameters*; nothing else changes shape as a definition closes. In output an open body is carried as **text** inside the kernel's `template` constructor, because that is what "held" means: a parameter stands wherever a token stands — `min_items: N` as readily as `element_type: T` — so a body holding one is not a value of any constructor's record shape until it closes, and an earlier draft of this revision that wrote it as one validated [TSON-SCHEMA] §5.10's own `vector` example as two errors and read `extern_of`'s `S` as a relative URI. What is compared is always the parsed form, never the text, so whitespace is free and "one spelling" stays a rule about structure. The same cleanup removed two fields a document could lie about: an entry's *kind* is now derived from its value's tag and chain by a four-branch rule every resolver agrees on, and a choice's `disjoint` lives in the `!choice` body beside the variant list it is derived over, where a closed record makes "on every choice and nothing else" structural rather than prose. Two questions are answered *no* for v1, deliberately: a parameter carries no bound on the *constructor* its argument was built with, and every reference binds fully — with one exception Revision 36 admitted on its own reasoning rather than as a type-family reading. A **record** template may stand bare at a type position as a *family base*: no value is ever read against it, since a value there is a value of some member selected by a tag or by a discriminator field, and every member is closed. `pet => <N, T> { type: text = N  pet: T }` with `dog => pet<"dog", dog_type> & { … }` is the labelled sum with a payload type per member; `result => abstract <T> { payload: T }` with `ok => <T> result<T> & { … }` is `abstract class Result<T>`. What stops the bare name meaning `box<?>` is elimination, not policy: a `box` position would have to infer arguments from a payload, and `[]` determines nothing (§2.10, [TSON-SCHEMA] §5.10). A third was settled by removing a rule: a parameter nothing grounds is a *type* parameter, since a value parameter is one that stands in a scalar slot, and `loop => <T> loop<T>` is refused for applying itself forever rather than for its parameter.

**A parameter carries its type.** An open entry once listed its parameters as names, and everything else about an application waited for substitution: a literal where the body wanted a type failed as `type_ref.name` rejecting a non-identifier, located inside the template body with the application as context, and a consumer of resolved output — an importing schema, a second resolver, a host binder generating a generic — could not check an application at all without parsing the held text the format otherwise keeps it from needing. The resolver was already working out what each parameter was and then discarding it. Now it records it: `template_param => { name  type  bound? }`, where `type` is the declared type of the slot the parameter stands in — `type_kind` for a type slot, `non_negative_integer` for `min_items: N`, the *field's* type for a routed default (`w?: int32 ~ N` gives `int32`), a callee's recorded type through a fixed point across templates that apply each other ([TSON-SCHEMA] §5.10). The kind is not recorded, because it follows: a parameter whose type is `type_kind` is a type parameter. An application is then checked at the call site, against the parameter list alone — `vector<pixel, "two">` is refused at the argument — and `e<c>` over `e => <M> !enum [a b M]` reads `c` as a member because the recorded type says so, not because of how it is spelled. A list of records rather than a map, since arguments are positional; rather than two parallel lists, which drift.

The same record is where a bound lives. `<T: text>` restricts a type parameter to types that are IS-A `text`, and `<N: int8>` narrows a value parameter; the written type is read by the kind the uses give, so it can narrow and never contradict. `bound` is its own field because a type parameter carries two facts — the argument is a reference, and the reference is restricted — and folding the second into `type` would make `<T: text> { a: T }` and `<M: text> !enum [a b M]` record alike. Bounds are inherited through another template's argument list, so `rebox<int32>` fails at `rebox` and not deep inside `boxed`. Two edges are worth knowing. A bound follows IS-A edges only, and construction grants none, so `<T: text>` refuses `identifier` and every fresh `!text_type {}` — right for values, where a `stock_code` must not pass as `text`, and a limit for bounds, which ask what a body can *do* with `T`; a bound on the constructor (`<T: !text_type>`) is the recorded answer and is not taken. And a slot-derived type is the governing meta's while a written one is the schema's own, so a schema's `non_negative_integer` meets the kernel's with no edge between them: two same-named entries with the same resolved body count as one type for these checks and for nothing else. A type slot bounded at a *field* by a nominal bound is the larger step, a dependent record the series has not had; what Revision 38 gives a field instead is a *kind* — `name_type: identifier_kind` says the slot holds the name of a type built with `identifier_type` — and a *reference*, `ref<method>`, for a slot that holds a value declared elsewhere by its name (§6.8). Revision 38 also split the templates in two along the line every language with both draws (C++'s alias and class templates, Haskell's `type` and `data`): a **record template** is a `record` with `parameters` — a family, read as the vocabulary, its parameters standing only in a field's type, a field's value or an application's arguments, since a base must have a shape its members share — and every other template is a held `!template`, an abbreviation whose application is its expansion and nothing more ([TSON-SCHEMA] §5.10, §8.2). A parameter is never an operand of `&` or `^`, a selector's type names no parameter of either kind, a routed `= P` is FIXED where it is written, and a parameter pin is a selector whatever its name's mark.

### 2.8 Not a JSON superset, and the rules that went with the claim

For many revisions Part 1 said that every valid JSON document was valid TSON, and the claim was true in the way such claims are: at the structural level, outside two character-level exceptions, and only in schemaless reading. Revision 35 withdrew it, and the removal was worth doing for what it exposed rather than for the sentence itself. Five rules had been carried for the superset and nothing else, and each was decided on its own merits once the claim was gone:

- **`null` is gone.** It had been a second spelling of void that only Class 1 could see: under a schema, `null` was already the string `null` at a `text` position and a synonym for `_` at a `void` position, so the value four sections of prose defended was one no Class 2 processor could observe. The first implementation had quietly modelled the two as one node because no consumer had a use for the difference. Now `_` is the format's one spelling of void, `null` is an ordinary string schemaless and whatever the position's type makes of it under a schema, and a JSON `null` reaches the model as void through the JSON reader — which is where JSON's habits belong. It would have been a mistake to guard the change with a reserved word; "there are no reserved words" now holds without a footnote.
- **`\/` went, and the surrogate pairs went.** A solidus needs no escape anywhere in the format. The four-hex-digit escape could not name a supplementary character, so the format had inherited JSON's UTF-16 workaround and then needed three MUST clauses to forbid its ill-formed halves. Now there are two spellings of one scalar value — `\uXXXX` and `\u{1*6HEXDIG}` — and one predicate, *the value denoted must be a scalar value*; a surrogate pair is two errors, plane 14's variation selectors are spellable by an ASCII-safe generator, and "TSON strings are well-formed" is a property the grammar cannot violate instead of a rule the lexer enforces.
- **A field name is a name.** Class 1 field names had been lexical — `{ "first name": 1 }` was a record — because a JSON object key can be anything. That cost two name rules, a per-class hygiene walk, and a carve-out for `"_id"`. Now a record's fields are the named members of a shape, which is what makes them declarable, and *a key that is not a name belongs in a map*: `{ "Content-Type" => "text/plain" }` was always the honest spelling. Normalisation runs before the match, so a decomposed spelling is a duplicate rather than a malformed name, and `_id` is not writable in a schemaless record at all. Revision 38 kept the principle and moved the judge: a field name is a value of its record's **name type**, an identifier by default and always in a schemaless document, and a schema may declare another identifier family for a record translated from a vocabulary whose names begin with `@` or `$` (§2.11). The grammar asks only that a name be a single-line token; the resolver, or the schema, says which names a record admits.
- **A comma may follow a value.** The trailing-comma ban was RFC 8259's, inherited whole, and it prevents an ambiguity TSON does not have: JavaScript's grammar has elision, so `[1, , 2]` is three elements with a hole and a trailing comma is genuinely ambiguous between two and three; TSON's void value is spellable and occupies a slot, so `[1, 2,]` is two elements and `[1 2 _]` is three and nothing is confusable. The other coherent position — drop the comma altogether — fails on the type expressions, where `pair<uuid, B>` and `[text, int32]` parse through the same separator rule, so the comma had to stay and the ban did not. `[, 1]` and `[1, , 2]` are still refused, not by a rule but because a comma is not a value.
- **What stayed, and why.** `true` and `false` keep their keyword reading under base type resolution — not for JSON's sake but because BOOLEAN is a discrimination class only while the two tokens match ahead of the number grammar, and Part 2's derived disjointness reads that class. The near-miss fall-through stays: `007` is the string `007` because a zero-padded token is data whose zeros are significant, and no digit-initial rule can refuse `007` while admitting `2025-03-13`, the most ordinary unquoted token in configuration data. And records and maps go on sharing `{ }`, because the shared brace was a good idea independently of where it came from.

The one-line summary: the notation is JSON-*like* by design, and everything JSON-*shaped* that stays — braces, brackets, `"`-strings, the `\n \r \t \\ \"` escapes, base type resolution as a mechanism, `number` for a bare numeric token — stays on reasons that survived the claim.

### 2.9 A field answers three questions

Through Revision 35 a record field had one of six states, spelled by two marks — `?` on the type for presence, `~`/`=` for the value — and the states were named for what a consumer *receives*: REQUIRED_DEFAULT was "required" because the reader always got a value, though the document could leave the key out. The names described delivery, and delivery is what a bound object sees. What they could not describe was the document. Pulled apart, a field answers three independent questions — what a missing key means (an error, nothing, or an injected value), what a written `_` means (an error, or a void value), and what a written value may be (anything, only a pin, or nothing) — and with two marks answering three questions, four cells real schemas use had no spelling. Measured over 963 SchemaStore schemas, the commonest field shape in JSON Schema — *key optional, null refused* — was one of them, at four fifths of all declarations; a *required key that may be null* (`fromWeight: null` for "no lower bound", against an omitted `fromWeight` for an incomplete record) was another; a *nullable default* (`timeout: _` for "no timeout", omission for "thirty seconds") was the one that changes what decodes; and a *marker the document must write* (`"jsonrpc": "2.0"`) had only an injecting spelling, which helps the one reader that needed it least.

Revision 36 gives each question its own slot ([TSON-SCHEMA] §5.2): `?` on the *name* answers omission, `?` on the *type* answers `_` — the same thing `?` already meant at an array element, a map value and a tuple slot, the record field having been the one position where it also answered omission — and the modifier answers the value. `a: T`, `a?: T`, `a: T?`, `a?: T?` are the four free cells (TypeScript's `a: T`, `a?: T`, `a: T | null`, `a?: T | null`, three of them to the character); `a?: T ~ v` and `a?: T? ~ v` are the two defaults; `a?: T = v` is the injected pin and `a: T = v` the marker. The resolved `record_field` stores the four facts a reader consults — `optional`, `voidable`, `role`, `value` — and *derives* what omission yields, which is why no `field_state` enum of any size survives: a field that is not optional is missing, an optional one with a value injects it, and the reader's three decisions (a written `_`, a written value, a key never written) each read exactly the facts they need. Three spellings are refused as rules rather than gaps: a default on an unmarked name (a value only omission can reach, on a key that is always written), a pin on a voidable type (`_` is settled before the pin is consulted), and a modifier on `void`. `= _` is gone because the type already spells it — `a?: void?` is a field that may be missing or written `_` and nothing else — and refinement is three orders, one per question, rather than a five-by-five matrix: omission moves nothing → required → injected, voidable true → false, role FREE → DEFAULT → FIXED, and a refinement moves no question backwards. Every cell of the old matrix falls out, and the one cell it disagreed with itself about dissolves.

Delivery is then the mode's, not the field's. A bound object has one null, so `a: T?`, `a?: T` and `a?: T?` hand over the same thing and differ only in what they refuse; a tree keeps which spelling arrived, in every encoding, because a tree is the document and [TSON-DATA] §2.9 says a field written `_` is present with a void value. And an encoder without the schema cannot write every cell: omitting a field with no value is right wherever the two spellings mean the same and wrong at exactly two declarations — `a: T?`, where the re-read document is missing a required key, and `a?: T? ~ v`, where it silently reads back as the default — so those two bind a schema-directed encoder and a schema-less one omits ([TSON-SCHEMA] §5.2, [TSON-JSON] §6.1.2). Part 1's old sentence that `_` versus omission "is a serialisation concern" was true exactly where the two answers coincide, and now says so.

**Two kinds of nothing, one name each.** The field syntax kept the two apart from the day it had three slots, and `record_field` named them `optional` and `voidable`. Everywhere else one enum, `element_state`, served both, under the word the record used for the *other* fact: at an array's element, a map's value and a tuple position OPTIONAL meant `_` may stand there, nothing ever being left out; at a field group it meant the group may be missing. A reader who learned `optional` from a record read `[T?]` as "an element may be omitted", which it never means. Revision 37 gives each fact one name wherever it applies and nowhere else: **`optional`** — the slot may be missing — on a record field and a field group; **`voidable`** — the slot's value may be void — on a record field, an array's element, a map's value and a tuple position. A container's part is never missing, so a container has no `optional`; a map's key is never void, so there is no key `voidable`; both are structural and no rule has to refuse them. The prose follows the type: the token is the *void sentinel*, the value is *void*, a key is *missing* or present — which retires "absent", whose everyday sense was the other fact and is why Part 1 had to say "present with an absent value". Three alternatives lost. Giving `optional` to the value, as Swift's, Rust's and Java's `Optional` do, is as consistent and goes against every schema system — JSON Schema's `required`, JSON Type Definition's `optionalProperties` beside `nullable`, XML Schema's `minOccurs` beside `nillable`, TypeScript's `a?:` — and it moves a word rather than retiring one, so a stale binding record would still read and mean the other fact. Making voidability part of the type, `T?` as `T` with void added, deletes the facet and costs a synthetic choice per `T?`, an IS-A rule between `T` and `T?`, and a rule against void map keys that is structural today. And `nullable` names the concept after one encoding's spelling of it.

The same reasoning settled a smaller case. A default or pin routed from a template parameter — `w?: int32 ~ N` — had been written on an unmarked name, the mark supplied when the template closed, on the argument that nothing is fixed until the value exists. But the name's `?` is the field's presence statement and says nothing about the value, so it does not wait for one: `w?: int32 ~ N` is a default, `w?: int32 = N` an injected pin, `w: int32 = N` a marker, exactly as their literal spellings are, FIXED or DEFAULT where they are written with the parameter in `value` ([TSON-SCHEMA] §5.7); and in a fresh record template a field pinned to a value parameter is a selector whatever its name's mark, the mark being each member's.

### 2.10 Records that say how they may be realised

A base with no instances of its own, a closed set of subtypes each identified by one constant field, and a consumer that switches exhaustively over them is the shape every host language has a word for — `sealed interface` over records, `sealed class`, a Rust enum with payloads, TypeScript's discriminated union — and the shape OpenAPI's `discriminator` describes. Revision 35 put the discriminator on a *choice*, as an annotation naming a field by string, and it could not reach the shape: a choice has no expected record type, so nothing related the variants' copies of the field, the name had to be written out, and the field's type had to be checked uniform across variants because a decoder must parse the member before it knows which one it has. Revision 36 moves the mark to where the relation already is. The base is a record, the members compose onto it, and the selector is field syntax beside `~` and `=`: `pet => abstract { pet_type: text =?  name: text }` reads *pinned, but not here — the members pin it*, and `dog => pet & { pet_type: = "dog"  breed: text }` tightens it FREE → FIXED, the one arrangement [TSON-SCHEMA] §5.7's identity rule permits, since a base that pinned the field would leave nothing for a member to pin ([TSON-SCHEMA] §5.2).

Two facts came with it, and both are kernel fields rather than annotations for one reason: erase them and the set of conforming documents changes. `record.extension` says how a record may be realised — OPEN (the default, and every record until now), ABSTRACT (no direct instances; a position typed by it admits exactly its subtypes, and the tag is required there unless discriminators say otherwise), FINAL (nothing may compose or refine onto it, in any schema). `record.discriminators` lists the selector fields, and whether it is present is what says how a subtype is selected — by reading the fields, or by the tag — so no fourth enum member states it a second time for the two to disagree. `=?` implies ABSTRACT, `abstract` beside it is an admitted assertion on `@disjoint`'s terms, and `final` beside it is a refusal. The marks are words at exactly one grammar position and ordinary identifiers everywhere else, so "no reserved words" still holds without a footnote. What the spelling buys over a mapping table is that there is nothing left to disagree: OpenAPI's `mapping` evaporates into the pins and is reconstructed from them on the way back out, a pin is a *value* so a wire spelling that is not an identifier (`"dog-v2"`, `"urn:acme:dog"`) survives, and the pins are compared as values under the field type's own equality, so `= 255` and `= 0xFF` collide as they should. The family is open across schemas — an importer may add a member under the obligation to pin, refused in the importer if it does not — so a host language's `permits` clause generated from one is relative to a closure, not a promise the schema makes. An abstract base with no member in its own closure is not an error: it is what a library schema publishes when its importers supply the members, and a document that reaches such a position gets a diagnostic naming the missing import rather than "one of ()". Openness has two corollaries the first implementation met as bugs. **A family is judged over the closure that holds it**: two members brought together only by an import merge, each from a schema that linked cleanly, are the importing schema's error, since judging only families with a local member would let that schema dispatch on a mapping that is not a function. And **a family member is declared**: a template application at a use site that composes onto a record would mint a member no declaration names — selectable at its base, and then unreportable, untaggable and unbindable, a minted name being one a document may never write — so it is refused, with the fix in the diagnostic (`dogs => dog_of<text>`). Revision 38 states the same rule from the other side — *a minted record is a member of nothing* — and it gained one document: a minted record composing an application only a use site mints (`u: vip<text>` over `vip => <T> box<T> & { … }`, beside `w: box<text>`) had been a member of that minted entry and refused; it is a member of nothing now, and loads. That rule also retired the one open question the declared-application rule had left: with members always declared, the duplicate forms an import merge can produce are never family members, both entries of such a pair admit the same values, and no merge key beside a declared name is needed.

Two consequences that read like exceptions are not. Subtraction off a FINAL record is admissible, because FINAL constrains the IS-A set and subtraction never joins it. And a record template may be a base (§2.7): its extension is ABSTRACT by construction and its own `extension` is each application's, its selectors are whatever survives erasure of the parameters — a `=?` field, or a field pinned to a value parameter, one value per application being one pin per member — and the one condition is that a selector's declared type mention no parameter of the template at any depth, a value parameter as much as a type one, since a decoder parses the selectors before it knows the member and their pins are exactly what the parameters supply. Both encodings read a member-dispatched position the same way — the marked fields at the base's types, the value or tuple against the pins, the matched member re-verified as an ordinary FIXED check — and differ in spelling alone; JSON asks the selectors to lead the object so that lookahead is bounded by the schema, and text finds them by lookahead over a record whose fields have no order.

### 2.11 A name has a type

**What a name is.** A field name was always a *name* and never a key: the named member of a shape, which is what makes it declarable, where a key is data that happens to index a map. Revision 35 made that a grammar rule — a field name is an identifier — and the rule served TSON's own data well. It failed a schema translated from somewhere else. JSON-LD's objects are records with members `@context`, `@id` and `@type`; OpenAPI's and JSON Schema's are records with `$ref`, `$id` and `$defs`; a tool-calling contract's enum holds `lightly active`. Each is a fixed set of declared members — a record — and each was unwritable: a map loses the declared shape, and renaming the member loses the round trip.

**What Revision 38 does.** It copies a mechanism the enum already had. An enum names the family its members are drawn from; a record now names the family its field names are drawn from, in the same slot under the same name: `record.name_type?: identifier_kind`, and `enum_type.name_type` (renamed from `type`). The three rules are one on both — the type is an identifier family, every declared name is a value of it, no two are one value under its equality — and the default is the series' own `field_name`, so nothing changes unless a record says so. A translation declares the profile its names need and composes it in:

```
json_name   => !identifier_type { start_add: "@$" }
json_record => !record { name_type: json_name  extension: ABSTRACT  fields: {} }
node        => json_record & { "@id": text  label: text }
words       => !identifier_type { medial: " " }
activity    => !enum_type { name_type: words  members: ["sedentary" "lightly active"] }
```

The brace form gained no syntax of its own: a relaxed name type is a translation's tool, not the go-to form for a TSON record, so a translation declares its type once on a fieldless abstract base and its records compose it, at the cost of one IS-A edge that asks nothing of a position typed by a member. Three spellings lost — inferring `text` from a quoted name (which cannot state a narrower type), a mark at the declaration head (which reserves a word as `abstract` does), and an annotation (which would make metadata change what a record admits).

**What it makes possible.** Four things, each a document that could not be written before: a record for a JSON vocabulary whose member names begin with `@` or `$`, round-tripped as the record it is; words as names — `lightly active`, a display string that is still a name, judged by hygiene, equal only to itself; names compared without case, under a name type whose `normalization` folds, so `Content-Type` and `content-type` are one field and two spellings of it in one record are a duplicate; and a house style, a profile that admits exactly a project's naming convention and refuses the rest at schema load.

**When it is the right tool, and when it is not.** It is the right tool when the names are given — by a vocabulary, a protocol, a system of record — and the schema's job is to describe them faithfully. It is the wrong tool for a TSON schema written fresh, where the series' own profile is the better convention and the default costs nothing. And it is never the tool for a key: a member name no identifier profile can state — the empty string, a leading or doubled space — is data, and belongs in a map, as [TSON-DATA] §2.5 always said.

**What it costs.** One IS-A edge to the base, and one rule to know: inheritance keeps one name type. A record's type is set where it is declared and a subtype cannot change it — a record with supertypes takes their one type when it states none, one that states a type beside supertypes must state theirs, and supertypes with different types (`json_record & address`, where `address` is `field_name`'s) do not compose. Taking the non-default type instead would re-judge `address`'s names by a type they were never declared under, and under a type that must begin with `@` none would be a value of it; the record to compose is one declared over the same type. A host component binds a relaxed name through the name the record states (`@Field("@id")`), and a writer quotes a name that is no identifier, so the document reads back. A schema's own references to its fields — a group's members, a restated field, a `discriminators` entry — match the declared spelling exactly, under a folding type too: the schema is one author's text, so a reference spelled differently is a mistake to report, and only a document's names, which another party writes, are matched in the type's form.

**The safety that stays on.** Every name is still judged by [TSON-DATA] §8.2's rules, under its type's profile — the profile's own added characters exempt from `Identifier_Status`, read by the look-alike and script rules — and over a floor no profile lifts: an identifier family may not add a character that renders as nothing or as another character (a `Default_Ignorable_Code_Point`, a control, a space other than U+0020), since without the floor a soft hyphen in `continue_add` makes `pass­word` and `password` two names no rule tells apart, and a profile derived from a hostile source's names would carry it in. A name type's form implies NFC: `NONE` is refused, and `ASCII_CASEFOLD` is admitted only over an all-ASCII profile, since a reader must match a document's name in the type's form and could choose none the type never stated. A document that names a schema has its field names judged by that schema alone; a Class 1 processor reading it holds them to no name rule, since the rule is the schema's — Part 1 decides which documents its own rule reaches and leaves the rest to the layer that owns the type.

**A worked conversion.** A JSON-LD node object, `{ "@context": "https://schema.org", "@id": "…", "@type": "Person", "name": "Ada" }`, converts as `json_name` above, `json_record`, and `person => json_record & { "@context": iri  "@id": iri  "@type": text  name: text }`. In TSON text the names are written quoted, since `@` is outside the token profile; in JSON they are the member names they always were ([TSON-JSON] §3.2), and `$ref` at a record position is the record's field `$ref` where it declares one. Part 3's three annotation members stay — `$schema`, `$type`, `$value` — *used* rather than reserved: a record that declares one obscures its own field, and a schema SHOULD NOT, which is a weaker claim than the old reservation and the only one a `$`-admitting name type leaves true.


## 3. Lexical Design

### 3.1 The unquoted profile, derived

The unquoted-token profile is UAX #31's identifier profile plus exactly three characters — `-`, `+`, `.` — and the digit category `Nd`. Every addition is traceable to a production of the number grammar: `Nd` because scalar tokens, unlike identifiers, include numbers; `-` and `+` because they are the sign and exponent-sign characters; `.` because it is the decimal point and the leading character of `.inf`, `.infinity`, and `.nan`. Nothing is speculative, and all three extension characters are `Pattern_Syntax`, hence immutable across Unicode versions — the profile itself is frozen while its property-based components grow monotonically with new scripts. Their bare single-character forms, though, are not tokens: `-` alone is the subtraction operator, `+` alone the at-least-one mark of a field group (§2.6), `..` is the range token, and a bare `.` is a lexer error — as tokens they would be the accidental single-character strings `"-"`, `"+"`, `"."`. That the same extensions would otherwise let a schema declare `42` or `-` as a type name is the identifier profile's business, not the lexer's (§3.5).

The exclusions follow a single rule: a content kind the profile cannot cover *totally* is excluded *entirely*, so that its quoting rule is "always", never a per-character scan. Paths need `~`, `\`, and spaces; URIs need `:`; monetary amounts need currency symbols, grouping separators, and spaces; rationals and CIDR networks need `/`; percentages need `%`; ranges need `..`, which the lexer claims as the range token. Admitting any of these characters would let *some* paths or *some* URIs go unquoted — and a rule of the form "quote paths, except those without spaces, unless…" is precisely the cognitive load the profile exists to eliminate.

### 3.2 Quoting by kind

The payoff of the total-coverage rule is that quoting becomes a property of what a value *is*:

**Never quoted:** numbers, `true`/`false`, identifier- and enum-like names, `full-date` temporals (`2026-08-01`), durations and periods (`PT36H`, `P3M`), UUIDs, hyphen-form MAC addresses, version strings.

**Always quoted:** anything containing whitespace or prose, times and datetimes (colons — a `full-date` is bare, but the moment a clock time joins it the colon ends the token), URIs with a scheme (colons), email addresses (`@`), paths, rationals and CIDR networks (`/`), IPv6 addresses (colons), monetary amounts and percentages, ranges (`..`), the single-character strings `-`/`+`/`.`, and the string `"null"` where the bare token would be misread by a reader with JSON reflexes (bare `null` is a legal string all the same). Base64 `bytes` values are quoted by convention — `=` padding is outside the profile anyway.

Inside a quoted token, a character is escaped by its scalar value in either of two spellings — `\u0041` or `\u{41}`, the braced form reaching any plane directly — and there are no surrogate pairs and no `\/` (§2.8). A leading-underscore name is not a name at any layer: `_id` belongs in a map.

A generator needs only two clauses: quote if any character falls outside the profile, and quote if the bare token would resolve to something other than the intended string (`"true"`, `"42"`, `"0x71C7…"`). A human writing TSON internalises the kind-level rule after a few documents and never thinks about individual characters again.

ZWNJ and ZWJ deserve their footnote, because the series changed its mind about them. They are `XID_Continue` — UAX #31 made them default identifier characters when it withdrew the old "restricted contexts" requirement and moved the safety rule to UTS #39 — and the token profile admits them. Earlier revisions excluded them and told authors whose orthography needs them to quote, which got the risk exactly backwards: a Persian `کتاب‌ها` was unspellable bare, while a Latin `ad<ZWNJ>min` sailed through the quoted route the same sentence recommended. The joiners are invisible only where they do no shaping work, so the right rule is contextual, and it lives on the identifier layer (§3.5): UTS #39's joining-control contexts admit a joiner where the neighbouring letters give it a job and refuse it where they do not.

### 3.3 Canonical identity: restrict, don't normalize

Reference identity ([TSON-DATA] §2.2.1) stays at RFC 3986's cheapest comparison rung — simple string comparison — by *restricting the input* rather than normalizing it. A document's own `!!id` must already be in canonical form — the host in its canonical text, port-free, userinfo-free, dot-segment-free, fragment-free, the path NFC with no percent-encoding of unreserved characters; anything else is an error, not a candidate for cleanup.

**The host is a `host` value.** Revision 37 made an identity an IRI-reference, so a host may be written in U-labels (`bücher.example`) and a path in characters beyond US-ASCII, and the rules written for US-ASCII stopped meaning anything: "lowercase host" has no case mapping beyond ASCII, a precomposed and a decomposed `é` read alike and compared unequal, one DNS host had two identities (`bücher.example` and `xn--bcher-kva.example`), and a non-ASCII identity could be named in a document and in no HTTP header, an RFC 9651 String holding printable ASCII only. Revision 38 closed all four without retreating to ASCII, by defining the host once, as a type the series already needed: an identity's host is RFC 3987's `ihost` profiled to DNS, which is exactly Part 1's `!host` row — a `hostname` in either label form, equal by name, canonical in lowercase U-labels; an IPv4 address; or an IPv6 address in RFC 5952 text, bracketed as the IRI grammar requires — and the path is NFC. The one-spelling rule then applies to a document's *own name* and the any-spelling rule to a *reference* to it: `!!id` is written in canonical form and only so, which is what serving a document at its identity path relies on, while `!!schema:"https://xn--bcher-kva.example/sch%c3%a9mas/x.tn"` is read to `bücher.example/schémas/x.tn` before comparison — A-labels, ASCII case, an uncompressed IPv6 address and percent-encoded UTF-8 all being spellings of one identity. What has no IRI form (`%FF`, an encoded bidi control, a percent-encoded unreserved character) stays an error. The cost is real and stated: an implementation needs RFC 5892's property to compare identities and can borrow no platform URL parser that applies UTS #46's mapping, since the WHATWG parser answers differently. An identity *atom* — a family whose equality is canonical identity — was built and withdrawn: every atom pins an external standard where this one would pin §2.2.1 itself, `identity` is a name core should not take, and a processor that emits identities in one spelling already gives a `text` field the right equality.

RFC 3986 §6.1 supplies the risk asymmetry that motivates the conservatism: for a schema system, a **false positive** — two distinct documents judged identical — validates data against the wrong contract, silently; a **false negative** merely costs a redundant registration or fetch. Every rung of normalization ladder an implementation climbs (case folding, path resolution, percent-decoding, scheme-specific rules) buys convenience by increasing false-positive risk and by widening the room for two implementations to disagree about identity. TSON climbs one deliberate rung — the scheme is dropped, so `http`/`https` variance names the same document — because transport choice demonstrably should not fork a document's identity, and stops there. The host, by contrast, is load-bearing on purpose: a fetch-endpoint change cannot silently redirect a name.

**Why the pin rides in the query.** Two objections have been raised to `?sha256=`: that a query is request data where a hash is client-side verification, so the fragment is the honest component; and that integrity should not be in the URI at all but in a structured `{ url, hash }` value. Both rest on a premise that is the wrong way round — sending the pin is the point. A pin in the query reaches the origin and the cache, so a content-addressed store can answer a pinned reference with the exact bytes it names long after the unpinned URL has moved on, and a cache can keep a pinned URL forever; a fragment is never sent, and a structured value separates locator from integrity so cleanly that the integrity never reaches the party able to satisfy it — either can detect drift and never repair it. The fragment is also spoken for: it is the natural home of an intra-document reference (`…/core.tn#name`), which the series will want next, so Part 1 reserves it. Canonical identity strips the pin because the two answer different questions — the pin identifies a byte sequence where the rest of the URI identifies a resource — which is a consequence to state, not a smell to design away.

**References restrict; values compare.** Reference identity restricts its input because a URI is a *spelling* with no value space behind it to compare. Every other identity rule in the series goes the other way, and Revision 35 said so once ([TSON-SCHEMA] §5.5): a type denotes a value space, an encoding a lexical space over it, and equality, ordering, refinement, disjointness and content addressing are over values. The clause was forced by a bug: the first implementation compared `bytes` by array identity, so a set of digests accepted every duplicate there was, and fixing it meant deciding — with nothing in the series to decide it from — that two spellings of one octet string are one value, while the temporal families in the same processor compared the offset and called one instant two values. One missing sentence, one processor, two contradictory answers. Now every family states its value space in its own `@doc`: `bytes` is octets and the alphabet a spelling; `1`, `1.0` and `1.00` are one `number`; `2/4` and `1/2` one `rational`; `time` and `datetime` are instants, the mandatory offset a spelling, so `10:00:00+01:00` and `09:00:00Z` are one value and both families are totally ordered; `duration` is seconds and `period` is months. A content-addressed reference is thereby never encoding-dependent, which was the property hash pinning existed to provide.

### 3.4 Content addressing and the verification chain

The hash-parameter convention (`?sha256=…`, verification metadata rather than identity) composes into a Merkle-style dependency graph in the manner of content-addressed stores: a data document pins its schema, the schema pins its meta-schema and imports, and the chain grounds in the pre-loaded bootstrap. A consumer holding a single hashed reference can therefore verify a document *together with its entire contract chain* — every byte that determines how the document will be interpreted — without trusting any intermediary. The rule that the hash input is every byte after the `!!id` line exists to make this work: a document can carry its own name without the name's spelling perturbing its hash, so registrars and mirrors can relocate content while the identity-plus-hash pair stays verifiable.

**Mixed references combine per identity.** In practice, references to one schema will arrive in mixed spellings — one document pins, another does not — and the resolution model is built for that. Within a closure, verification attaches to the canonical identity, not to individual references (the closure already loads one instance per identity, so there is nothing else it *could* attach to): the digests declared across all references to an identity form a set, and an empty set resolves unverified, a single digest verifies the content once — at collection, before any reference resolves — and two distinct digests are a conflict reported as an error, never chosen between. A plain reference in a closure that also pins the identity therefore rides the pin: it resolves to the verified instance, and if verification fails, it fails too — leniency in what authors may write, never a fallback to unverified content. The practical consequence is that pinning is additive rather than viral: any one document in a closure can raise an identity's guarantee for the whole resolution, and no published plain reference can lower it. 

**The one edge that cannot be pinned.** The kernel's `!!meta` references its own URL, and that reference can never carry a hash: the hash input includes the `!!meta` line, so the pin would have to hash bytes containing itself. This is not a defect to route around but the place where the verification chain hands over to the bootstrap — the self-reference is grounded by pre-loading (§1.3), and the mixed-reference rule is what lets every *other* reference to the kernel be pinned while the kernel's own stays plain. The kernel's `!!id` line, being excluded from the hash input, *can* carry the kernel's published digest — a useful registration cross-check, verified at registration per [TSON-SCHEMA] §10.2 — but an embedded hash authenticates nothing by itself: an attacker who can rewrite the body can rewrite the id line to match. Trust always flows from the referencing side or from the pre-load; the embedded digest is a convenience for registrars, not a root.

**Publication discipline: pin everything pinnable.** The mixed-reference rule makes plain references legal everywhere; the published artifacts nonetheless ship maximally pinned, because the Merkle guarantee holds only along edges that are actually pinned — a consumer holding a pinned reference to core has verified meta *through* core only if core's own `!!meta` carries meta's digest. The artifact hashes are computed bottom-up, each stage's pins in place before the next stage hashes over them: the kernel's body first (its self-referencing `!!meta` plain, its `!!id` carrying the resulting digest), then meta with its kernel references pinned, then core with its meta reference pinned. Publishers of type libraries should follow the same discipline: pin `!!meta` and every `!!import` at publication, so that one hashed reference to the library verifies its entire chain.

The specifications stop at verification deliberately. Ordering, consensus, and mutability policy — everything a content-addressed *store* adds on top — are application concerns; TSON supplies the identity discipline they need and nothing more.

### 3.5 Names are not tokens

An unquoted token is a *spelling*; a name is a *thing spelled*. TSON's lexer cannot tell them apart — in `{ name: Alice }` both words are unquoted tokens, and the profile of §3.1 has to admit digits, signs and dots so that a *number* can be one — which is why, for many revisions, the series had no place to state a rule about names at all. The kernel typed every naming position with an atom that carried no contract, the grammar governed those positions with four different productions (one of which, field names, admitted quoted spellings and thereby every character the profile excluded), and the security section could only advise.

Revision 34 gives names their own layer ([TSON-DATA] §7.7). An **identifier** is the decoded text of a token — after unquoting, escaping and NFC — matched in full against a profile that is the token profile *minus* the number grammar's extensions: `XID_Start` to begin, `XID_Continue` plus `-` to continue. The subtraction is the whole design. A name never begins with a digit, a sign or a dot, which makes "numbers are not declarable names" a consequence rather than a rule; `+` goes because it only ever served exponents; `.` goes so that it can be a separator — which Revision 38 made it, in the qualified name `orders.place_order` a reference slot parses (§6.8) — rather than two identifiers that happen to contain dots. And because the identifier profile sits strictly inside the token profile, every identifier is spellable bare — the annotation and type-annotation positions, which admit no quotes, lose nothing by it. Enum members are names, and the enum says *which* names. An enum is a closed set of names and the identifier family they are drawn from: `enum_type => atom & { name_type: identifier_kind  members: enum_set }`, with `enum` pinning `identifier` — every member a name under the series' own profile, carrying the hygiene rules. Revision 36 had spelled this as a two-member `profile`, and no enumeration of profiles can close: everything the profile switched on — which spellings are admitted, when two names are one, whether hygiene runs — is a property of a naming vocabulary, and a schema describing protobuf's names or HTTP's tokens has a vocabulary of its own. So the type is held as data, and an ordinary schema writes `!enum_type { name_type: kebab  members: [make-tea drink-tea] }` with no new constructor. Revision 37 had let the type be any text family and shipped `text_enum` for members that were texts rather than names; Revision 38 withdrew it, because what it admitted needed no text family — `!identifier_type { medial: " " }` admits `lightly active` and refuses a leading, trailing or doubled space, with full hygiene — and a closed set of text that is no name is `text`'s own `members` facet, `!text ^ { members: ["80" "443"] }`. The field was renamed from `type` to `name_type` to match the record's (§2.11), the bare `type` having said nothing about what it typed. Nothing an author had written changes: `!enum [OPEN DONE]` is still the positional form, `name_type` being pinned and injected. A pinned type resolves where it was written — the governing meta — so `!enum [A B]` names the kernel's `identifier` in a schema that imports no core, and one that declares its own `identifier` does not displace it. Numbers are never enum members (`!enum [1 2 3]` is refused; write `!integer ^ { members: [1 2 3] }`): a name is equal only to itself, where `80` and `0x50` are one integer. The kernel's own `boolean => !enum [true false]` is untouched, because `true` is a perfectly good identifier and its base-type class is still read off the token by Part 1 §4. One claim was withdrawn with the profile: that an identifier member is a host-safe constant. It is true of no host — `in-progress` is an identifier, and `true` is refused as a Java constant — so binding is a mapping the binder owns, and a host language is what a schema maps *to*, never a constraint on it. Measured over real tool-calling contracts, a third of string enums cannot be identifiers, almost all for a space in a display string, and an `enum_type` over an identifier family with a medial space is the spelling that admits them as names.

**`identifier` is a type now, and that is what lets a value be a name.** For as long as `identifier` was an atom with no vocabulary, the grammar and the hygiene rules reached the kernel's naming positions through prose, and a map key — a value — was reached by neither: `admin` and `аdmin` as two keys of one `{identifier => handler}` map were admitted, where the same pair as two fields of a record was refused. A rule about maps would have been wrong, since at a map position two look-alike keys may be legitimately distinct data; what says they are names is the key's *type*. So `identifier` became a text family — `identifier_type => text_type & atom_specification & { … }`, a UAX #31 profile stated as data, with `identifier => !identifier_type { continue_add: "-" }` reproducing the series' own grammar exactly — and the line between a name and data moved from the position to the type: a value typed by an identifier family meets the per-name mechanisms, and the keys of a map keyed by one, and the elements of a set of one, are look-alike scopes ([TSON-SCHEMA] §11.4). A document admitted before can be refused, and that is the point. Stating the profile as data buys the second half: a schema describing another system's names declares its own instance — a start set, a continue set, a medial set, a normalization form — and a profile's own additions are exempt from the restricted-character rule, the profile being the statement that they belong. The profile facets are fixed where the profile is constructed, because settable-once would be unsound here: setting `start_add` in a refinement *widens* the profile. Core declares no `identifier` (§4.5); the schema that wants one writes the line.

The layer now sits on every naming position, the data grammar's field names included. Revision 34 had placed it on *declarations* only, leaving a Class 1 field name lexical so that JSON's quoted keys stayed valid; that bought the superset claim at the price of two name rules, a hygiene walk that ran differently by conformance class, and a carve-out for `"_id"` that no declared field could bear. With the claim gone (§2.8) the asymmetry went with it: `{ "first name": 1 }` is refused in a schemaless document, the two spellings of a field name are two spellings of one set of names, and the position that knows it holds a name normalises to NFC and then matches — so a decomposed spelling is the same name, a duplicate rather than a malformed one, and the quoted route is never stricter than the bare one. A key that is not a name belongs in a map, which the format always had. Revision 38 moved the match from the parser to the resolver, and under a schema to the record's own name type (§2.11): the grammar admits any single-line token at a field name, and the layer that owns the record says which names it admits. The one visible cost is the deliberate one: `_` is `XID_Continue` only, so `_id` is not an identifier, and admitting it to the series' own profile would be a change for every naming position at once — argued on its own merits, and not cheaply, since the lexer takes `_` greedily as the sentinel and every identifier is meant to be spellable bare; a schema that must hold `_id` declares a name type that admits it.

**Two layers, and why the line is where it is.** Above the identifier grammar sits a second layer ([TSON-DATA] §8.2): skeleton distinctness within each closed scope (no two names in one record, enum, schema, or import closure may share a UTS #39 skeleton), `Identifier_Status=Allowed`, and a script restriction level. All three are implemented by every conforming processor and enforced by default — and none of them decides validity. The reason is a property, not a preference: the identifier grammar rests on Unicode properties the Consortium has frozen, so a content-addressed schema returns one verdict under every implementation at every Unicode version, which is the promise a hash pin makes; the hygiene data (`confusables.txt`, `IdentifierStatus.txt`) is explicitly unstable between versions, so a verdict resting on it could change under a routine UCD refresh, and those same pinned bytes would flip from valid to invalid. Skeleton distinctness has a second disqualification: it is a relation over a set that spans `!!import`, so two independently published, independently pinned schemas — `list_item` in one, `Iist_item` in the other — can each be fine alone and collide when one imports the other, with the fix in a document the importing author does not control. As validity that makes the import impossible to write; as policy the operator relaxes it, in code, visibly, and proceeds. So a document failing the second layer is *refused by this processor*: the **refused** outcome, a rejection here that is never a finding about validity, distinguishable from the four error categories but reported in the same report as them — a consumer repairs a document in one pass, and the distinction is carried by *which rule refused*, which the report must state anyway. What travels with the report is a property of the processor, not of the refusal: the **UCD version** of the data files (the tables are published as part of the Unicode Character Database and carry its version, not UTS #39's own revision number, which names the prose and is stable across exactly the refreshes the rule exists to make visible) and the two policies the series now names — the **identifier policy** (mechanisms 1 and 2, and the level and unit of mechanism 3, at identifier positions) and the **token policy** (a restriction level over every token, off by default, subsuming the identifier policy when stricter). A processor should be able to state both with no document in hand: a sender that reads the policy before writing never writes the name that would be refused, where one that learns it from a refusal has already spent the round trip the format exists to avoid. A deployment may relax any of the three — but only through the implementation's own configuration, never through the environment, and never silently.

**Where the policy lives, and where it may not.** Two homes suggest themselves and both are wrong. Not the schema: a schema declaring its own strictness would let the artifact being checked choose the check — a homograph-laden schema would declare the level that admits it — and a published schema is immutable and hash-pinned while strictness must move as `confusables.txt` updates, so raising a policy would mint a new identity that every document pinning the old one would never see. Not an API description either, which is a schema governed by a meta layer and inherits both objections. Skeleton distinctness adds a third reason: it does not compose across `!!import`, so the policy is a property of the merged namespace at the *importing* site, which no single schema is positioned to declare. The home is a third artifact kind — a deployment descriptor that is data rather than a schema, named at the call site and never discovered, never resolvable by identity — and the companion artifact `policy.tn` declares its vocabulary: the identifier and token policies and the resource limits, in the shape a processor also states its policy in. No document may name, import or otherwise select the policy it is judged under, which is the self-certification the paragraph above refuses ([TSON-DATA] §8.2).

**Why skeleton distinctness and not the restriction level for names.** The restriction level is free — `Script` is a property every platform exposes — and it is what browsers use for domain names. But a browser judges a name in isolation because it cannot enumerate what the name might be confused with; TSON always can, because every scope is closed and known at the moment the check runs. A per-name rule must guess from the name alone, and it guesses wrong on ordinary names: Highly Restrictive over a whole name refuses `id_пользователя`, `url_адрес` and `χ_index`, which are the *common* way a developer working outside Latin script names things — the Latin abbreviations of the trade inside a name in their own script. The skeleton check fires only on a colliding pair, so it costs such an author nothing while catching every homograph, including the whole-script `aec`/`аес` that no script rule can see. The restriction level still ships, defaulting to Highly Restrictive, because it catches the within-word mixing a skeleton check cannot until a second name arrives; the relaxation to reach for first is its *unit* — apply the level per `_`/`-` delimited segment, which keeps `аdmin` refused and admits `id_пользователя` — not its level. Values no type makes names are the mirror image: they have no scope to be distinct within, so the per-string mechanism is the only one available, and its default is off, because data may legitimately be anything.


## 4. The Schema Ladder, Narratively

### 4.1 What schema-value separation buys

The fundamental rule — a document never resolves type annotations against its own definitions — looks austere until you list what it purchases:

- **Stable meaning.** `!text` in a document means what the referenced schema says, full stop. Two documents referencing the same schema have identical vocabularies; no document can locally shadow a type and change the meaning of data that quotes it.
- **Reviewability.** A schema is a self-contained published artifact. Reviewing a data document requires its schema and nothing else; reviewing a schema requires its meta and imports and nothing else. There is no "definitions section" whose scope leaks into the data.
- **Immutability with teeth.** Because schemas are external, hash-pinnable artifacts, "the contract cannot change under you" is a verifiable property rather than a convention.
- **Uniformity all the way up.** The same rule governs data documents, user schemas, and the meta layer itself, so the resolver has one resolution model rather than a special case per layer.

### 4.2 Why one hop

All resolution against a governing target is one hop: the target's namespace — locals plus imports — is consulted directly, and no further rung of the ladder is walked. The alternative (walking the chain until a name resolves) is how most inheritance systems work, and its failure mode is well known: the effective vocabulary of a document becomes the union of everything above it, so adding a name anywhere up the chain can change the meaning of documents that never mention that rung. One-hop resolution makes every document's vocabulary *finite, explicit, and owned by exactly one schema*. The cost is the "import what you expose" obligation — a meta-schema must import every schema whose entries it intends to offer — and the specification chooses to pay it, because the obligation falls on the few authors of meta-schemas rather than the many authors of documents.

### 4.3 Import what you expose, worked

`meta.tn` is the worked example. Meta's own declarations use kernel types (`type_name`, `value`), so it needs the kernel import for itself. But the import does double duty: because every meta-governed schema resolves constructor roles against *meta's namespace, one hop* — and a namespace is the schema's whole import closure, since imports are transitive ([TSON-SCHEMA] §2.2.3) — the kernel import is also the delivery mechanism that places `enum`, `record`, `array`, the sugar-form desugar targets, `type_chain` and `field_map` in front of every user schema and every resolver-output document. Delete the import and meta's own body still nearly works — but every schema chaining to meta loses the structural vocabulary, and resolver output can no longer name its own body types. The one-hop rule turns a meta-schema's import list into its published API surface, which is exactly the property you want reviewable.

### 4.4 Where annotations live, and why it feels asymmetric

Annotations resolve one hop against the governing target — the `!!meta` target for a schema document, the `!!schema` target for a data document. The asymmetry that surprises authors: an annotation type declared in a user schema is usable in that schema's *data documents* but not in the schema document itself, whose governing target is meta. This is the one-hop rule being consistent rather than an exception to it: the schema document is governed by meta, so its metadata vocabulary is meta's business; its data documents are governed by the schema, so their metadata vocabulary is the schema's business. Custom annotations for schema documents therefore require an extended meta-schema — a heavier act, deliberately, because schema-document metadata is tooling-facing and benefits from ecosystem-wide agreement.

What meta declares is kept to what a schema *states*. `@todo` records work on a schema's source, and a published schema is immutable, so a `@todo` in one is a note nobody can act on; `@since` is untyped text whose versions mean whatever the author's release scheme does, and a schema's prose language is not a property of any one declaration — all three left meta, and a schema that wants one declares it in a meta layer of its own. `@comment` joined, for the maintainers' note JSON Schema calls `$comment` — folding those into `@doc` would show them to everyone reading the documentation, and a converter meets them constantly. `@deprecated` became a bare marker, deprecation being a yes-or-no fact in every host language and a reason being `@doc`'s to give; as `text`, the bare `@deprecated` an author naturally writes had been a resolver error. `@examples` holds text, because an annotation's position hands its value no type to be read by, and `[value]` implied a check no processor could perform. And `@doc`'s text is CommonMark — versioned, where "markdown" has no specification — with no extensions and raw HTML never executed, a doc being written by whoever published the schema: the bundled schemas' docs were markdown already, and OpenAPI made the same choice for `description`. The rule refuses nothing; it says how the text is displayed, which is all an advisory annotation may do.

### 4.5 Why core is small

[TSON-SCHEMA] §2.2.3 makes it an error for a local declaration to reuse a name the import closure binds, with no hiding. So every name core declares is reserved in every schema that imports it, and a name core *adds* breaks every schema already using it — found when that schema migrates. The evidence was this revision's own draft: a core `identifier` stopped twelve converted schemas compiling, because `identifier` is a name a schema reaches for its own type. Core therefore holds what a schema cannot reasonably write for itself — the atoms, the integer widths, the formats, the `scoped` instances and the templates — and leaves out any name that stands for one line: `positive_integer` and its three siblings, `non_empty_text`, `identifier`, and the `annotation` and `documentation` annotation types are gone, a bound being a refinement declared where it is wanted (`count => !integer ^ { min: 0 }`). The built-in vocabulary of Part 1 lost the sign bounds with it — a schemaless name denotes the type its library declares under that name, so a name no library declares has no meaning to keep, and Revision 38 made such a name a resolver error rather than a marker (§6.1). A shadowing rule would have made additions safe for the declaring schema and not for one importing it; restraint in one library is the smaller change.

**The network library.** The same cost argued the other way for five names core *did* declare: `ipv4`, `ipv6`, `cidr4`, `cidr6` and `mac` are what nearly no schema needs and `mac` is a plausible name for a schema's own type, so each reserved a name in every importing schema for the benefit of a few. Revision 38 moved them to a library of their own, `net.tn`, governed by meta and importing nothing, and put the three atoms the HTTP layer asked for beside them — `hostname` (RFC 5890, either label form, equal by name), `host` (a host name or an IP address in one string-class atom, since `( hostname | ipv4 | ipv6 )` is not disjoint and would tag every value) and `media_type` (RFC 6838, parameters only where an instance permits them). A schema importing core alone may now declare a `hostname` of its own, which schemas already did; one that wants the network types imports `net.tn` as well. The principle is stated in [TSON-SCHEMA] §9: a family most schemas never use belongs in a library outside core. Part 1's schemaless vocabulary keeps every name and gained three, each denoting the type its library declares, so `!mac` means the same thing when its document moves under a schema that imports `net.tn` — which is §5.6's guarantee with the library named.


## 5. A Tour of the Type Operations

The type system's operations form a spectrum of completeness — each operation takes a definition some distance further toward concrete data.

**Construction** (`!C { bindings }` in the schema grammar) creates a new type from a constructor. It is the origin point of every atom family and every structural type, and it transfers *kind*, never IS-A: `dogs => !integer_type {}` founds a fresh atom family with the same constraint vocabulary as `integer` and no relation to it. Construction creating siblings rather than subtypes is the load-bearing choice — it is what lets one constructor found unrelated nominal families (`integer` and `dogs` do not accidentally unify), and it is why the atom family's IS-A facts live in `supertypes` rather than being derivable from bodies.

**Refinement** (`T ^ { ... }` or `!I ^ { values }`) copies a definition and tightens it — binding values, fixing defaults, restricting ranges — while preserving IS-A with the source. Refinement never adds fields; each of a field's three questions moves along its own order and never backwards ([TSON-SCHEMA] §5.7 — omission nothing → required → injected, voidable true → false, role FREE → DEFAULT → FIXED), a default may be restated with a different value and a pin may not, and a refinement whose source is FINAL is refused outright. A chain of refinements is a chain of promises, each stronger than the last, and any consumer expecting the source type can accept the refined one.

**Composition** (`A & B & { ... }`) is construction with declared ancestry: new fields are permitted, each listed supertype contributes IS-A — refused where a parent is FINAL (§2.10), and written as an application where a parent is a template, `ok => <T> result<T> & { … }`, a record template's `chain.parents` being a list of `type_ref` so that `ok<text>` is IS-A `result<text>` and not `result<int32>` — and the disjointness rule (no field name from two supertype paths) keeps the merge order-independent and diamond-free. The IS-A is cashed out at the data level by subsumption ([TSON-SCHEMA] §7.2), judged after following both reference chains: `!employee` is valid at a `person`-typed field, and validates in full as an employee — closure, constraints, and defaults are the subtype's — while an unannotated value at that field is exactly a `person`, so the annotation is the sole carrier of the more specific type. That is the point of composition in an application schema: a field typed by a parent admits the whole family the schema declares beneath it, each member naming itself on the wire when it appears. Subtraction stays outside the family by construction — the check reads the type's `chain`, which subtraction empties (§2.) — and choice discrimination is the same surface gesture under a different membership relation: an annotation selecting a more specific type than the position promises, variant-of for choices, IS-A here.

**Subtraction** (`head - { fields }`) removes fields and breaks IS-A — see §2.4 above for why this is a feature and why the clause sits at the head.

**Reference** (`user_id => uuid`) is not an operation on the type at all: it is the same type under another name, and Revision 35 made resolved output say so by stating the chain as written. An earlier design rewrote every use site to the chain's terminal entry and attached `@alias` naming the first hop — a second, lossy representation of a walk every consumer makes anyway (a chain of two hops kept the name of the one that carried nothing). Now a use site names what the author named, `target` names the next hop, and a processor collapses the chain when it compiles readers, after linking, where the diagnostic gets the alias the author wrote for free. What the removal exposed was that there are *three* ways to name a type after another and only one is a rename: `user_id => uuid` accepts `!uuid` and a sibling's tag at its positions; `user_id => !uuid ^ {}` — the empty refinement, legal and the language's one nominal-subtype spelling — IS-A `uuid` and refuses both; `user_id => !uuid_type {}` is a fresh, unrelated instance and refuses both too. Identity follows the first (`box<user_id>` is `box<uuid>`'s entry) and respects the other two, and an author wanting a `stock_id` refused at a `user_id` position now has a table telling them which line to write ([TSON-SCHEMA] §5.7, §8.2, §8.3).

**Instantiation** is the data-level end of the spectrum: `!T value` in a data document produces a concrete value, terminal by definition. There is no construction in data; the same surface shape (`!C { bindings }`) in a data document is a record that *describes* constraints — which is precisely what resolver output stores. One consequence a first implementer meets by parse error: the `name<args>` sugar is schema syntax only, so an application at a `type_ref` slot in *data* is written as the explicit record `{ name: page  arguments: [ { name: order } ] }`, and the way to give it a name is a declaration in the schema, `order_page => page<order>` — which is then the instantiation entry itself, not a hop to one (§2.7).

The one-way flow — construct, refine, instantiate, stop — is what makes the resolver's job tractable: every definition has a finite derivation ending at a constructor, every body desugars to one canonical form, and the data level never feeds back into the type level.


## 6. Atoms in Depth

### 6.1 `value`, `void`, `identifier` and the kinds

Three kernel atoms were once instances of one constructor, `unit`, whose resolved shapes were identical and which a processor had to tell apart *by name* — the one place the series identified a type by what it was called. Each now has a constructor of its own: `value => !value_type {}`, `void => !void_type {}`, and `identifier` an instance of the text family `identifier_type` (§3.5). The contract comes with the constructor, so core's `void` sibling is a void by construction rather than by a rule saying a second entity of that name carries the kernel's contract, and a schema that wants its own writes `!void_type {}`. Revision 38 added two atoms of a new sort beside them — `kind_type`, whose values are the names of the types built with a constructor, and `ref_type`, whose values are qualified names of values — and §6.8 is theirs.

**A schemaless annotation outside the vocabulary is an error now.** For as long as Part 1 preserved `!nonesuch` as an uninterpreted marker, the schemaless document was the one place in the series where a name that resolved to nothing was accepted silently: `!uiid 550e…` and `!Date 2026-01-01` passed a check that reported nothing and validated nothing, and the typo surfaced at the first schema-bound consumer or never. Every reason for the marker had gone — the vocabulary does not grow (new types arrive through schemas, never as built-in names), gradual adoption runs through the built-ins, and the vocabulary is a property of the revision rather than the deployment, so the rejection is a portable finding about the document. A host-typed read is no exception: a tag naming the class a caller happens to bind would make the verdict on a document depend on who read it, so a caller who wants its host types read as a document's type definitions says so explicitly, which is a schema in all but form ([TSON-DATA] §3.2).

**`value`** is the escape hatch: the token, uninterpreted, read by the type the position hands it to. Under a schema there is no base type resolution at all — every value is typed by its position or by its tag — so a `value`-typed field is read by the atom it stands for once that atom is in scope, and its host inhabitants are a boolean, an integer, a float or a string. Its principal legitimate use is the kernel's own bootstrap (constraint fields whose natural type does not exist yet at kernel-load time — see §6.2); a `value` position is a single token and not a scope, so it admits no nested `!!schema`. Reaching for `value` in an application schema is a design smell — the type an author usually wants there is `dynamic` (§6.4); reaching for it in a meta-schema is sometimes forced.

**`identifier`** is the series' own name type: the decoded text of a token matching the identifier grammar ([TSON-DATA] §7.7), put into NFC, equal iff byte-identical. It is a text family, so it is string-class and takes lengths, a pattern and a member set; what it adds to `text` is its profile — an identifier can always be rendered back as an unquoted lexeme, which is why the kernel uses it for `type_name`, `field_name` and `param_name`, pins it as `enum`'s name type, makes it the default name type of every record (§2.11), and why a rule stated on it reaches every name in the series (§3.5). Its `normalization` is `NFC`, a form a value is put *into*: a quoted decomposed spelling reads as its composed value, so "same name" can never depend on Unicode composition happenstance.

**`void`** is the type of a slot that holds no value: one inhabitant, the void sentinel `_`. Its two jobs are being the target type of bare annotations (`@deprecated` is sugar for `@deprecated:_` — presence is the information) and expressing "no value" as a field type, where the field's own marks say the rest: `a?: void?` may be missing or written `_`, `a: void?` must be written as `_`, `a?: void` may only be missing, and `a: void` admits no document and is refused (§2.9). It is deliberately not a valid choice variant: optionality is a property of a position, not a type, so "optional T" is spelled with `?`, never `(T | void)` ([TSON-SCHEMA] §5.4). `void` admits `_` and nothing else: the `null`-at-`void` concession earlier revisions carried for JSON-shaped data is gone with `null` itself (§2.8), and a JSON `null` reaches a `void` position — or any voidable one — as void through the JSON reader.

### 6.2 Constraint fields typed as `value`

`decimal_type.min` cannot be typed `number`, because `number` is a *core* instance of `decimal_type` — the constraint field exists before the type that would naturally fill it. This bootstrap ordering is why several atom constructors type their bounds as `value`. The kernel's `integer_type` is the exception that proves the rule: its bounds and the elements of its `members` are typed `integer` because `integer` lives in the kernel's own namespace — and every facet that *counts* something (a length, an item count, a digit count, a prefix, a precision) is typed by the kernel's `non_negative_integer`, so a negative count fails when the meta-schema is validated rather than in every resolver.

The consequence for implementers is the eager-conversion rule: each constrained atom converts `value`-typed constraint values to its internal representation at schema-load time, never per-validation. The rule sounds like an optimisation but is really a correctness property — a schema either loads cleanly or fails with a clear diagnostic, and there is no such thing as a half-valid schema that parses but mis-validates on the millionth record. The rule has a sharper form for a `value`-typed *set*: `decimal_type.members` is a set of `value` (meta's `decimal_member_set`), so if its elements were compared as whatever a bare token resolves to, `[1 1.0]` would be an integer beside a float and the set's own uniqueness rule would find them distinct — a claim nobody would write down. The resolver reads each member under the constrained atom *before* the set is formed, so `1` and `1.0` are one member and a duplicate, and a member that does not parse as a decimal fails at load. Which host types an implementation accepts for conversion is an implementation choice; the validation semantics *after* conversion are the atom's contract, so two implementations that both load a schema agree on what it validates.

**`members`, and why an enum still refuses numbers.** `!enum [80 443 8080]` is a schema-load error — an enum's members are labels of a text family, names under its default type (§3.5) — and Revision 34's replacements (`min`/`max`, or a choice) left the sparse case badly served: three single-value refinements and a tagged choice, for a shape every schema language this one converts from treats as ordinary. The gap was a facet, not a constructor: `!integer ^ { members: [80 443 8080] }` is an `integer`, refines further, and composes with its bounds as facets of one body, where a `numeric_enum` constructor would mint a family with no IS-A into the numeric tiers. Three things had to land right, and the enum precedent answered each: a *set*, not an array, so uniqueness comes from the set's contract and non-emptiness from `min_items`; coherence, so every member satisfies the body's other facets, the derived width included; and identity by value, so `[80 0x50]` is a duplicate. `float_type` takes no `members` on the same grounds it takes no `multiple_of` — a member set is a claim about a binary grid that cannot be made exactly. Revision 36 gave `text_type` the same facet, since text identity is as well defined as integer identity and the pattern fallback (`a|b|c`, regex-escaped by a converter, diagnosed as "does not match the pattern") was the only spelling for three admitted URIs that are still URIs. It sits beside `pattern` rather than excluding it, because a pattern is two things nothing can tell apart — `a|b|c` is a value set and `[A-Z]{2}` is a shape — and the two share one refinement rule, **settable once**: set where unset, restated verbatim, never changed, since narrowing a pattern would need a regular-language containment oracle the series decides nowhere. That makes text the one tier whose member set does not shrink under refinement, stated so that a reader does not infer it from a diagnostic.

### 6.3 Exact and approximate numeric tiers

The numeric vocabulary is split into an exact tier (`number`, `rational`, the fixed-width integers) and an approximate tier (`float32`, `float64`), and the split is annotated in the type library with `@exact`. The design intent: a bare numeric token carries no width or precision commitment, so it maps to `number` — exact, with scale no part of the value (`1`, `1.0` and `1.00` are one number; whether the spelling survives a round trip is an encoding's promise) — and lossiness enters only when a consumer *chooses* an approximate type, at which point rounding onto the IEEE 754 grid is that type's documented contract rather than a surprise. The special values (`.inf`, `.nan`, signed zeros) belong to the approximate tier only, and NaN payloads are deliberately not information: every NaN denotes the canonical quiet NaN, so value preservation holds by definition and payload-dependent round-trip bugs are excluded by fiat. `complex` sits across the tiers: its `component` selector is a partial order — `INTEGER ⊂ NUMBER ⊂ RATIONAL`, `FLOAT32 ⊂ FLOAT64`, the two families incomparable since binary64 carries ±inf and NaN no exact decimal holds — so `!complex ^ { component: INTEGER }` is a refinement and a float complex is its own instance, not a refinement of `complex`.

### 6.4 Open sums: `scoped`

`choice` is the *closed* sum — it enumerates its variants. Revision 35 gave the *open* sum one constructor, `scoped`, whose instance names the namespaces a value's type may come from, and the value names its own type. Two questions are independent at every position: is the governing namespace (locals and imports) admitted, and which foreign schemas are? Revision 34 had spelled two of the five cells — `extern` (one foreign schema at a time) and `unknown` (anything, from anywhere) — and could not spell the rest: an attachments field admitting a claim from one partner *or* a report from another had no type, and "any type this schema declares, chosen by the data" had none either. `scoped => sum & { scope: scope_set  schemas?: {schema_identity => [type_name; 1..]?; 1..} }` covers the space, and core names the three subsets — `declared` (`[LOCAL]`), `extern` (`[EXTERN]`), `dynamic` (`[LOCAL EXTERN]`) — plus two templates, `extern_of<S>` and `extern_type<S, T>`, so that naming one schema or one type in it is an application rather than a declaration.

Two things fell out. First, the permissive-type list in [TSON-SCHEMA] §7.8 — the positions where a nested `!!schema` was allowed — stopped being a list: a position admits a scope push exactly when its type resolves to a `scoped` instance whose `scope` holds `EXTERN`, the way `disjoint` is a fact of a choice's declarations; and `value` left the list, a single token being no scope. Where a push is not admitted, which rule refuses it follows the position: at a `scoped` position whose `scope` lacks `EXTERN` — a `!!schema` at a `declared` field — the cell rule refuses it as a validation error, nothing having failed to resolve; at a position that is not scoped at all it is a resolver error, there being no cell to be refused by. An earlier text let both rules claim the first case. Second, `unknown`'s untyped half went with base type resolution under a schema. `unknown` had meant "any well-formed value, typed or not", and the untyped case had no reader — Part 1 switched base resolution off under any schema, and `unknown_type` supplied no contract of its own, so an implementation had to invent one or refuse. Now a value at a scoped position must name its type, in every mode, and the successor is called `dynamic` rather than `any` precisely because every language a reader arrives from uses `any` for a value that is *unchecked*, where this one is validated in full against the type it names. The same move closed the "vocabulary-only" root: under `!!schema` the root names its type or the document is invalid, and a document is Class 1 or Class 2 from its header — which is also why a schemaless document opens no schema scope, there being nobody to opt in.

**`namespace`, the kernel's other open construct.** `scoped` names where a value's *type* may come from; `namespace`, new in Revision 38, is the constructor whose instances are *maps keyed by names* — the schema map and a record's fields are the kernel's two (`schema => namespace & { key_type: = type_name  value_type: = top }`, `field_map => namespace & { key_type?: ~ field_name  value_type: = record_field  ordered?: = true }`), and a meta layer declares a third wherever it has a named set of anything, an interface's methods being the motivating case. It replaces `data`, the "non-type kind" Revision 37 kept for a meta layer's own vocabulary: once that vocabulary is a record held by a namespace type, `data` had nothing left to describe. §6.8 takes it up with the kinds and references it exists to serve.

### 6.5 `bytes`, and the design that replaced its own first fix

Revision 34 had four binary types — `base64`, `base64url`, `base32`, `hex` — as siblings over one value space, differing in a selector facet and with no IS-A between them. An author who did not care about the spelling could not say so (`encoding` was required), a choice did not recover it (the four share a class), and a binary encoding writing a `base64` field and a `hex` field produced identical output while the two were different types with different identities — a hash pinned over the schema differed for streams indistinguishable on the wire. The complaint was right and the obvious fix — `bytes` with the four as refinements — was built and abandoned: all four alphabets have the *same* value space, so `hex ^ bytes` narrowed nothing and the four `subtypes` partitioned nothing, and a refinement of `bytes` became unreadable because `!hex` is not its subtype. A directive on the reference was built next and abandoned too: a container element has no annotation position, so `[hexdigest]` cannot carry an alphabet an annotation holds, and a pure rename's directive vanished at `box<hexdigest>` — four hex octets read as six base64 ones, silently.

What runs is the third design. One `bytes_type` whose `encoding` is a selector defaulting to base64; one `bytes` in core and no spelled siblings; another alphabet is another *instance* (`hexdigest => !bytes_type { encoding: HEX  length: 4 }`) and never a refinement, because a spelling narrows nothing and `hexbytes ^ bytes` would claim an IS-A no base64 reader could honour; length facets count octets, so `length: 32` is a sha256 in any alphabet; and the type says nothing about spelling in the model, as `text` says nothing about UTF-8, so an octet encoding ignores the selector and writes the bytes. Part 1 follows with `!bytes` as its only binary tag, base64, a schemaless document having no schema to carry a selector. The lesson generalised into [TSON-SCHEMA] §5.7's selector rule: a selector may move under refinement only along the narrowing relation its members carry — a chain for a width, a partial order for `complex.component`, none at all for an alphabet.

### 6.6 Text: a value is put into its form

`normalization` began as a facet of `identifier_type` alone, and as a *requirement*: text not in the form was refused. That left nothing in the kernel able to say that `Content-Type` and `content-type` are one name, and it was the outlier in a series that otherwise rewrites — an atom maps a spelling to a value (`0x10` is 16), and Part 1 normalises a quoted token at an identifier position before matching it. So the facet moved to `text_type`, where every text family inherits it, and it now states the form a value is *put into*: a value is its token's text, unquoted, unescaped, then put into the form, and every facet and every comparison — lengths, pattern, members, pins, map keys, set uniqueness, look-alikes — judges the value and never the spelling ([TSON-SCHEMA] §5.5). Five members, five distinct equalities: code points (`NONE`), code points without ASCII case (`ASCII_CASEFOLD`), canonical (`NFC`), compatibility (`NFKC`), and compatibility without case (`NFKC_CASEFOLD`). The ASCII fold is the one the case-insensitive naming systems actually state — HTTP field names, URI schemes, DNS names, charset names fold ASCII letters and nothing else — and it needs no Unicode table; JSON Schema documents fake it with patterns of the form `[Pp][Rr][Ee]…`, of which SchemaStore holds over a thousand. `NFKC_CASEFOLD` is looser than any of them — it folds a full-width `Ｃ` and the Kelvin sign *into* ASCII — and is the form for names compared without case across Unicode. NFD and NFKD are not members: each is its composed twin's equivalence stored decomposed, and would change a spelling and no verdict.

The facet is fixed where the type is constructed, because turning a fold on in a refinement changes what a token *means* rather than narrowing which values are admitted. `regex_type`, `uri_type`, `iri_type` and `email_type` pin `NONE`: a pattern put into another form matches something else, and past the scheme and host a URI compares with case, as a mailbox's local part does. The costs are stated: a round trip writes the value, so `Content-Type` comes back `content-type`, as `0x10` comes back `16`; a refusal quotes the token as written and then the value it was judged as, since the written spelling is what a reader has to find; and `NFKC_CASEFOLD`'s fold is not lowercasing beyond ASCII (`ß` becomes `ss`, so a length changes). The alternatives were a form rule — the schema-language consensus, which refuses the spellings HTTP/1.1 writes — and an equality facet that keeps the spelling, which has no precedent and needs a second comparison at every place values are compared.

**URIs and IRIs.** RFC 3986 names two productions with two value spaces — a URI, which has a scheme, and a URI-reference, which may be relative — and the vocabulary had named the first while implementations read the second. They are now two atoms, `uri` and `uri_reference`, the first a refinement of the second in core, and `uri_type` states RFC 3986's distinctions as facets: `allow_relative`, `allow_fragment` (withdrawn together they leave the absolute-URI an identity needs), and `schemes`, a *set* in place of one scheme — a set of an ASCII-folding identifier, so the comparison RFC 3986 states in prose is structure. The kernel's own `uri` requires a scheme, which types `spec` and the keys of `scoped.schemas` by the value space identity already demanded. A URI is US-ASCII; RFC 3987's IRI is a family of its own, `iri_type`, with `iri` and `iri_reference` in core, because JSON Schema and OpenAPI name all four and a `!uri` that admits `é` through a lenient parser is an IRI type by accident — one document passing on one processor and failing on the next. A directive argument is an IRI-reference, so a schema published at a path beyond US-ASCII is written as itself. The error split is the usual one: a relative reference under `uri` is inside the lexical space and outside the value space, a validation error, as a negative integer is under `uint32`; a character outside the grammar is the parser's refusal.

**A leap second is refused.** RFC 3339's grammar admits second 60; a `time` is a time of day on `[00:00:00, 24:00:00)` and a `datetime` an instant on a UTC timeline that counts no leap seconds, so the token has no value in either space, and both parsers refuse it rather than leave it between two statements that disagree.

### 6.7 Containers: order, emptiness, and the one-position tuple

`array` carried `unordered`, which `set_type` pinned true and which read as a double negative at its default; `map` carried nothing, so nothing said whether a map's entry order is part of its value. Order is now a facet both state, named for the property: `ordered` is `true` for an array and `false` for a map unless a body says otherwise, and `set_type` pins it `false`. It says one thing — whether two values differing only in order are one value — and never changes what a document may write; output keeps the order written either way, which is what lets two resolved documents be compared as written. An ordered map, `!map { … ordered: true }`, is the case a host binds to an order-keeping map.

A set was the one container whose empty value had to be asked for: `set_type` defaulted `min_items` to 1, so `tags: set<text>` could not hold "no tags", and the spelling that could — `min_items: 0` on a `!set_type` body — is one the `set<T>` template does not offer. The default is gone: a set's bounds are an array's, and a set that must be non-empty says `min_items: 1`. Every set the kernel and meta relied on being non-empty now says so, as a named entry, a bounded set at a field being a named entry for the same reason any `!C { … }` at a field is.

And `[T]` is an array, so a tuple of exactly one position had no sugar — rare in a schema written as TSON, and real in one converted from JSON Schema, where `prefixItems` with one entry is exactly that and a converter needs a spelling it can emit at a field without minting a declaration. Core declares `tuple1<T>` and `voidable_tuple1<T>`: two, because a type argument cannot carry the `?` that would make the position voidable.

### 6.8 Kinds, namespaces and references

Revision 38's largest change to the kernel answers one question three ways: *how does a slot say what kind of name it holds?* Nearly every kernel slot that named a type carried a rule about what it might name — `enum_type.type` an identifier family, `map.key_type` an atom, `record_field.type` a type a value can have — and no slot stated it: each was prose in one section and a check in a resolver's code, and a meta layer declaring a slot of its own had no way to state one. The gap kept reappearing under different names — a bounded type slot, a constant pool, the HTTP layer's bindings naming methods — and it was one gap.

**Kinds.** The proto-schema reading is the clearest. `type` is the universe of the types a value can have, and `type_name` is its *code* type: a name is a code, and the type-name namespace is the decoding from codes to the types they denote — a universe à la Tarski. A slot typed `type_name` holds a code. `kind<C>` is a predicate on codes carving out a subuniverse — `{ c : type_name | c decodes to a type built with C }`; in System F-omega's terms literally a kind, the classifier of type constructors, so that `name_type: kind<identifier_type>` is a kind signature. Its inhabitants are *names of types*, and it holds no value of any of them. That is why a kind is not a sum: a coproduct's inhabitants are values injected from a variant, never names; the two are related by the dependent sum "some type in the kind and a value of it", whose witness is the tag in `!stock_id 7d3e…`, which a `choice` is over a *listed* set of codes. Read as a sum, `name_type: kind<identifier_type>` would admit the token `NewYork`; read as the predicate it admits only `identifier` or `text_identifier`, names of types built with `identifier_type` — and the conflation reads correctly until an enum is written down, which is how it went unnoticed.

So the kernel declares the set of types built with a constructor as an atom, `kind_type => atom & { of: type_name  members?: [type_name] }`, with `kind => <T> !kind_type { of: T }` as the inline spelling. Its place in the order is `top → type → atom → kind_type`, beside `enum_type`: an enum is an atom whose value set is a *listed* set of names, a kind one whose value set is a *computed* set — one lists, the other walks. Membership is one walk over resolved entries: an entry is a member of `kind<C>` iff, past its alias hops, the constructor its value applies is `C` or IS-A `C`, and the entry is not itself a constructor whose instances are types. A record written in braces is built with `record`, so `kind<record>` holds every record in the closure, abstract ones and record-template bases included (membership is by construction, and whether a kind should admit an abstract type is recorded open in the change log). `of` names a constructor of the *structure* namespace, `type` or `namespace` or below either, and `kind<reference>` is refused; a kind is not a bound — `<T: text>` names a type and walks supertypes, `kind<text_type>` names a constructor and walks construction, two stores and two mechanisms. The members are computed at link over the governing closure, so two schemas importing different libraries have different members for one `kind<uuid_type>`, and resolved output carries them as a derived index on `subtypes`' terms. The kernel declares three — `identifier_kind`, `atom_kind`, `type_kind` — types both `name_type` slots by the first, and **every slot that names the type of a value by the third**: `record_field.type`, the containers' type slots, `choice.variants`, a parameter's `type` and `bound` are all `type_kind` and hold a bare name, judged a member when the schema links, so `field: record` is refused as no member where a coded check once refused it. `type_ref`, the structured form with `arguments`, stands only where an *application* may: a record template's parents, an argument, `@application`, held text, and an alias's `target`. Core declares `kind` as a sibling, so an ordinary schema types a field `kind<uuid_type>` and a data document holds a type's name there — `stock_id`, never a UUID. The one slot a kind cannot type is `kind_type.of` itself: its values are constructors' names, which no kind covers, and the series has no higher kind to classify constructors; the slot is `type_name` and its store is stated in prose, as every constructor role's is.

**Namespaces.** Once a slot can name a *type* by kind, the next question was a *value*: an HTTP binding names the method it serves, and a method is a value inside an entry — a named set of methods — that nothing could reach into. The thing a named set of methods *is* turned out to be the thing the schema map and a record's fields already were: a map whose keys *name* what they hold, which is a constructor's description of its values and not a map type at all. `namespace => abstract top & { key_type: identifier_kind  value_type: type_kind  ordered?: boolean ~ false }` is that constructor, composing `top` directly beside `reference` and `template`; a namespace type pins its facets by composition, and a value of it is written and read as the map it describes, never as a record of the facets. `top` thereby has two families of constructors — `type`'s, whose instances are types, and `namespace`'s, whose instances are maps. A meta layer declares `interface => namespace & { key_type: = method_name  value_type: = method }`, and a schema it governs holds `orders => !interface { place_order => { request: place_order_request  response: order } }` — an entry whose value is a map the schema describes, a value and never a type, refused at every type position and listed by `kind<interface>` and `kind<namespace>` (and, since membership is by construction, whether a namespace *entry* belongs in a kind's members is the second question the change log records open). A namespace type is a constructor, so only a meta layer may declare one, and a meta layer then owns the vocabulary its constructors use rather than importing it from core — which is the import-level rule (§4.3): an import stands at the importer's governing level, kernel-governed with kernel-governed and ordinary with ordinary, so an ordinary schema can never type a field by a kernel record or a meta layer's namespace type, and a meta layer can never take its vocabulary from an ordinary library. A namespace keyed by text, and `&` over two namespace values, were asked for and not taken.

**References.** A reference is the kind's sibling: `ref_type => atom & { of: type_name }`, `ref => <T> !ref_type { of: T }`, an atom whose values are **qualified names** — `orders.place_order`, segments joined by `.`, the first resolving as a type reference does and each further one selecting a key of a namespace value or a field of a record value, every hop written, no index and no quoted segment. The dotted-names family is the one every language with qualified names uses (Java's, XML's QName, protobuf's fully-qualified names, TOML's dotted keys), where each segment is a name and nothing is a position; the address family (JSON Pointer, JSONPath) answers *which position* and is what the series' diagnostics already use, so `authors.0` is not a name and never will be. `.` was reserved in the identifier profile for exactly this (§3.5), and a dotted token already lexed as one unquoted token, so a qualified name is a parse of a token the lexer accepts, performed by the slot's type, with the lexer untouched. The judgement is *found first, judged after*: the referent's type is judged before its value, a name wrong by type is refused for its type, and nothing selects among candidates. The slot declares the spelling — a slot typed `ref<method>` holds a name and nothing else, a slot typed `method` holds the value inline — which closed the interim in which two hosts of one meta admitted different documents. A reference is kernel vocabulary and core declares no `ref`: a reference names an entry that holds a value, and only a kernel-governed schema has one, since only a meta layer can declare the namespace type whose value the entry is. Resolved output writes a name as written, and the two readings a later revision might give it — substitution, where the value replaces the name, and identity, where the referent is the value's identity and what a coverage check reads — both stay available ([TSON-SCHEMA] §5.12).

**Put together.** A meta-schema for a service, as the conformance corpus writes one:

```
!!meta:"https://tson.io/2026/38/m/meta-kernel.tn"
!!import:"https://tson.io/2026/38/m/meta.tn"
{
  method_name => identifier
  method      => { request?: kind<record>  response?: type_kind  errors?: [kind<record>] }
  interface   => namespace & { key_type: = method_name  value_type: = method }
  http_verb   => !enum [GET POST]
  binding     => { method: ref<method>  verb: http_verb  route: text }
  bindings    => namespace & { key_type: = identifier  value_type: = binding }
}
```

A governed schema declares `orders => !interface { place_order => { request: place_order_request  response: order } }`, where `request: place_order_request` is a *kind*-typed slot holding the name of a record type — `request: user_id` is refused, `uuid_type` building no record — and a second schema declares `routes => !bindings { post_orders => { method: orders.place_order  verb: POST  route: "/orders" } }`, where `method` is a *reference* reaching the method value under a key of a namespace entry. `response: orders` is refused (a value where a type is wanted), `method: orders` is refused (an interface where a method is wanted), `method: orders.cancel_order` is refused with the keys listed, and `request: { … }` is refused because a kind-typed slot of a value body holds one bare name and nothing in a value body lifts or mints. Every one of those checks was once the HTTP layer's to run at startup; each is now the schema's.

### 6.9 Host names, hosts and media types

Three atoms joined the series in Revision 38, all in `net.tn` (§4.5), and each is an instance of a new meta constructor pinning an RFC.

**`hostname`** is a domain name as RFC 5890 defines one — LDH labels, A-labels or U-labels in any mix — and its value is the *name*: `Example.COM` and `example.com` are one value, and so are `xn--bcher-kva.example` and `bücher.example`, two views of one value recoverable from each other by Punycode over a valid label. No `text_type` could say this: U-label validity is RFC 5892's property, which no pattern states, and the equivalence of a U-label and its A-label is Punycode, which no normalization reaches. Four boundaries an operator will meet are stated in Part 1 so that none looks like a bug: uppercase beyond ASCII is refused (`BÜCHER.example` is no host name, since IDNA2008 has no uppercase in a U-label and mapping one would bring UTS #46 into validity — the diagnostic names the lowercase spelling), a trailing dot is refused, the last label does not begin with a digit (so a dotted quad is never a host name and `( hostname | ipv4 )` has no value in both), and percent-encoding is refused as URI syntax. Homographs are the allowlist's to stop, and it does: an allowlist typed `[hostname]` compares by name, so `schemаs.example.com` with a Cyrillic `а` is refused unless the operator listed it. The one facet is `allow_idn`, `false` admitting only names with no internationalized label in either spelling — a property of the name, for a system that cannot carry one, not a restriction on spelling. Nothing is mapped, so UTS #46's tables never enter, and RFC 5892's property grows monotonically with the Unicode version, which is what lets an identity's host be a host name (§3.3).

**`host`** is a host name or an IP address in one string-class atom — RFC 3987's `ihost` profiled to DNS. A listener binds `localhost`, `127.0.0.1` or `::1`, and `( hostname | ipv4 | ipv6 )` is not disjoint ([TSON-SCHEMA] §5.4 proves nothing from patterns, deliberately), so every value at the choice needed a tag in a file an operator writes by hand; the answer is one type, not a choice. Brackets are a second spelling of an IPv6 address, since operators copy `[::1]` out of URLs and `listen [::1]:80`; `[::1]` and `::1` are one value, the canonical text unbracketed, and `ipv6` itself still refuses the brackets, an address on its own having no port to separate. Each member keeps its own equality — a name by name, an address numerically — and a name never equals an address, nor the two address families each other: `127.0.0.1` and `::ffff:127.0.0.1` are two values. It has no facets yet; the likely first is `allow_zone`, for a listener binding a link-local address, recorded in the change log and not taken.

**`media_type`** is an RFC 6838 media type: type and subtype as `restricted-name`s, parameters per RFC 9110 §8.3.1 only where an instance says `allow_parameters: true`, so `media_type` proper is a media type and not a `Content-Type` value. The equality is the one `text` could not state: type, subtype and parameter names compare without ASCII case, parameters are a map so order carries no meaning and a repeated name is refused, a quoted value equals its token form, and a parameter value keeps its case except `charset`'s — a closed list of one, since "unless its registration says otherwise" would leave every processor holding the registry and two at different registry states disagreeing. `types` and `suffixes` are member sets (`suffixes: [json]` admits `application/json` and `application/ld+json`), and a media range (`text/*`) is `Accept`'s syntax, a pattern over values and not a value — the same line `uri_template` was refused on. It lives in `net.tn` rather than core so that an OpenAPI conversion's Media Type Object keeps its name.


## 7. A Complete Worked Example

The task-tracking schema from [TSON-SCHEMA] §1.6, extended with a `budget` and a `digest` field and taken all the way through resolution. The schema document:

```
!!id:"https://example.com/task.tn"
!!meta:"https://tson.io/2026/38/m/meta.tn"
!!import:"https://tson.io/2026/38/m/core.tn"
@doc:"Task-tracking example schema."
{
  priority   => !integer ^ { min: 1  max: 5 }
  title_text => !text ^ { min_length: 1 }
  status     => !enum [OPEN ACTIVE DONE]
  flagged    => <T, N> { entry: T  priority?: priority ~ N }
  task => {
    id:        uuid
    title:     title_text
    priority?: priority ~ 3
    status?:   status ~ OPEN
    due?:      date
    budget?:   duration
    digest?:   bytes
    tags?:     [text]
    history?:  [flagged<status, 2>]
  }
}
```

`priority` and `title_text` refine core's `integer` and `text` instances — core declares no bounded sibling of either (§4.5); `status` applies the `enum` constructor, reached through the structure namespace supplied by the `!!meta` target; `task` is a fresh record whose field types resolve through the type-name namespace — `uuid` and `date` from the core import, `priority`, `title_text` and `status` from the local declarations. `id` and `title` must be written; `priority` and `status` may be omitted and are then injected (`?` on the name, `~` a default — a default is admitted only on a name that may be omitted, §2.9); `due`, `budget`, `digest`, `tags` and `history` may be omitted and are then missing, and none admits `_` (no `?` on the type). `tags?: [text]` is a field whose array sugar lifts to a synthetic entry, referenced by name from the field ([TSON-SCHEMA] §5.3, §8.2). `budget` is elapsed time in seconds and `digest` an octet sequence spelled in base64 in text — both value spaces, not spellings (§3.3, §6.5). `flagged` is a record template — a `record` with `parameters`, one a type parameter and one a value parameter — a fresh record whose `priority` field is defaulted by parameter (`priority?: priority ~ N`, the `?` written as it is beside a literal default, [TSON-SCHEMA] §5.7), built entirely from the schema's own names — and `[flagged<status, 2>]` wraps its fully-bound application in the array sugar ([TSON-SCHEMA] §5.10). Because `flagged` is a record template it is also a family base: a field could be typed `flagged` bare, its *declared* applications being the members, and a value there would be placed by its tag — `~ N` is a default, overridable, and so no selector; had the template written `priority?: priority = N`, the pin would have been one, one value per application being one pin per member, whatever the name's mark (§2.10).

A data document binds the schema and instantiates:

```
!!schema:"https://example.com/task.tn"
!task {
  id:       550e8400-e29b-41d4-a716-446655440000
  title:    "Ship revision 38"
  priority: 3
  status:   OPEN
  due:      2026-08-01
  budget:   P2W
  digest:   "3q2+7w=="
  tags:     [spec editorial]
  history:  [{ entry: OPEN }  { entry: ACTIVE  priority: 4 },]
}
```

Note that `priority` and `status` restate their default values (and that the trailing comma in `history` is legal, a comma following a value): a document that states its defaults reads without its schema, and omitting a defaulted field is an encoder optimisation — lossless only because the decoder injects the value back on read.

Resolution derives a schema value, serialized as resolver output — a data document governed by the meta-schema, in which every entry is its value, desugared to the canonical `!C { bindings }` form with its chain computed and its fields a map keyed by name, and fields at their default values are omitted (`optional: false`, `voidable: false`, `role: FREE`, `extension: OPEN`, an empty chain):

```
!!schema:"https://tson.io/2026/38/m/meta.tn"
!schema {
  priority   => !integer_type { chain: { supertypes: [integer] }  min: 1  max: 5 }
  title_text => !text_type { chain: { supertypes: [text] }  min_length: 1 }
  status     => !enum { members: [OPEN ACTIVE DONE] }
  flagged => !record {
    fields: {
      entry    => { type: T }
      priority => { type: priority  optional: true  role: DEFAULT  value: N }
    }
    parameters: [{ name: T  type: type_kind } { name: N  type: priority }]
  }
  task => !record { fields: {
    id       => { type: uuid }
    title    => { type: title_text }
    priority => { type: priority  optional: true  role: DEFAULT  value: 3 }
    status   => { type: status    optional: true  role: DEFAULT  value: OPEN }
    due      => { type: date      optional: true }
    budget   => { type: duration  optional: true }
    digest   => { type: bytes     optional: true }
    tags     => { type: array_text_xxhash  optional: true }
    history  => { type: array_record_entry_status_xxhash  optional: true }
  } }
  @synthetic record_entry_status_xxhash => !record { fields: {
    entry    => { type: status }
    priority => { type: priority  optional: true  role: DEFAULT  value: 2 }
  } }
  @synthetic array_text_xxhash => !array { element_type: text }
  @synthetic array_record_entry_status_xxhash => !array { element_type: record_entry_status_xxhash }
}
```

(The trailing `xxhash` in the synthetic names stands for a content hash of the resolved binding record — a placeholder, since name spelling is the resolver's own business and only the shape is a conformance point. `record_entry_status_xxhash` is the *expansion* of `flagged<status, 2>`, keyed on its content: a use site's application mints no instantiation entry of its own and records nothing of the application, so a second template with the same field list applied the same way would land on the same entry. Had the schema declared `flagged_status => flagged<status, 2>`, that declaration would be the entry, under that name, with `@application:{ name: flagged  arguments: [{ name: status } { value: 2 }] }` on its key and `chain: { supertypes: [flagged] }` in its value — a member of the family, where the minted record is a member of nothing — §2.7, §8.1.)

Reading the output:

- `priority` shows **refinement**: the surface form `!integer ^ { ... }` retargeted to the instance's constructor, which is the value's tag (`!integer_type`), with IS-A recorded in the type's own chain (`chain: { supertypes: [integer] }`). This is the case where the chain carries information the body cannot: compare a hypothetical sibling `port => !integer_type { min: 0  max: 65535 }`, which would serialize under the *same* tag with an identical body shape and no chain.
- `status` shows **construction**: the `enum` constructor's ATOM kind is inherited, the tag is `!enum` (the tightening of `enum_type` that pins the name type to `identifier`, so `name_type` is at its pin and omitted), and there is no chain — construction transfers kind, not IS-A. The positional sugar `!enum [OPEN ACTIVE DONE]` has desugared to the explicit binding `{ members: [...] }`.
- `task` shows the **four field facts** and the **field map**: `fields` is a namespace from each name to its `record_field`, the name written once as the key and nowhere inside, and each `record_field` carries `optional` (the name's `?`), `voidable` (the type's `?`), `role` and `value`, the defaults omitted, and the eagerly-resolved default values, so consumers read defaults from the output without re-parsing modifier tokens. Every `type` is a `type_kind` value — a bare name, judged a member of the kind of the types when the schema links. What omission yields is nowhere in the output: a reader derives it — a missing `id` is an error, a missing `priority` injects `3`, a missing `due` yields nothing — from `optional` and `value` (§2.9).
- `tags` shows a **synthetic entry**: the sugar `[text]` lifts to `array_text_xxhash` — a closed entry headed by the constructor it builds, marked `@synthetic` at its key so tooling can fold it back into nested display — and the field holds a bare reference to it. Any other `[text]` anywhere in the schema lands on the same entry: identity is structural equality of the resolved body, one entry per distinct concrete form ([TSON-SCHEMA] §8.2).
- `flagged` and `record_entry_status_xxhash` show the **template machinery** end to end. The record template is a `!record` with `parameters` — not a held text, since a record body has no slot a parameter can stand in but a field's `type`, a field's `value` and an application's arguments, all of which `record_field` and `type_ref` already hold as names or uninterpreted tokens — and `T` and `N` are ordinary tokens in it, a token being a parameter exactly when its text resolves into the record's own `parameters`. `T` stands in a field's type slot and is recorded `type_kind`; `N` is routed into a default, so its type is the *field's* declared type, `priority`, and `flagged<status, 9>` is refused at the application, `9` being no `priority`, before anything is substituted (§2.7). The default `~ N` is DEFAULT where it is written, with the parameter in the plain `value` slot of a field marked as the author marked it, and only the value waits for the closing ([TSON-SCHEMA] §5.7, §5.10, §8.1). Nothing on the entry says `extension: ABSTRACT`: a record template's base is ABSTRACT *by construction*, and the record's own `extension` is each application's — OPEN here, since the template wrote no `abstract` — so a consumer holding only resolved output can name `flagged` at a type position, dispatch on it by tag (there are no `discriminators`, `~ N` being a default and not a pin), and never read a field that carries a parameter (§2.10). The expansion is the closed form: substitution swaps `type: T` for `type: status` and `value: N` for `value: 2`, and the result is a record keyed on its content and a member of nothing — not of `flagged`'s family, since no declaration names it, which is what keeps a minted member from being selected at a `flagged` position and then found unreportable and unbindable. `history`'s field then references a second synthetic — the array *around* the expansion — showing the two lifts composing, both `@synthetic`. For an instance-form template closing over a constructor with value slots, see [TSON-SCHEMA] §8.2's `vector` example.


## 8. Resolver Output for Consumers

### 8.1 Internal entries: two identities, and names that carry nothing

Resolver output contains internally named entries beyond the declared ones, and a consumer should hold two facts about them. First, **every application denotes an entry**: a sugar form at a use site lifts to a *synthetic entry* — closed for a concrete form, open (a held application) for a parameter-bearing one inside a template — and a fully-bound application of a non-constructor template denotes its *expansion* — the declaration naming it where one exists, and otherwise the closed synthetic the substituted form lands on, keyed on its content, which any other spelling of the same form lands on too; no instantiation entry is minted and nothing records the application, the one exception being a self-reaching expansion, which keeps an entry keyed on the application so that the knot can be tied ([TSON-SCHEMA] §5.3, §8.2). A use site always holds a bare name, so a consumer walking a resolved schema needs exactly one interpreter — follow references, read bodies against constructor vocabularies one hop at a time — and never a second, in-place structural one. The rule also gives recursion the entry it needs: `tree<text>`'s fully expanded form is infinite (its children's element type is itself), and a tree-shaped document cannot express that cycle without a name; the entry is the knot.

Second, **there are two identities, and the names carry no information.** A *declared* entry's identity is its name: two declarations are two types however alike their bodies, which is what makes `stock_id => !uuid_type {}` and `other_id => !uuid_type {}` refuse each other's values, as they should. A *minted* entry has no declared name to be its identity, so identity is structural per family: a declared instantiation, and a self-reaching one, compares by the canonical, fully-bound application recorded as `@application` on its key; closed synthetics by the resolved binding record, one entry per distinct concrete form schema-wide; open synthetics by body structure up to parameter renaming. Spelling variance therefore never forks an entry — `[id]` and `[uuid]` land together because identity follows an argument's reference chain and a reference is the same type under another name, `vector<pixel, 255>` and `vector<pixel, 0xFF>` are one application because a literal argument is compared as the value it denotes even though it is recorded as written (base type stays a boundary: `1` and `1.0` are two arguments), and the same form written directly or arising inside a materialised template lands on one entry, because the resolver re-derives every synthetic's identity from its *resolved* record after names have meaning — while two conforming resolvers may disagree on every internal name and still agree structurally, which is the sense in which resolved output is compared across implementations: equality up to renaming of internal entries. What is canonicalised is identity, not provenance: the name the author wrote survives at the use site that wrote it, since output states reference chains as written. Names should nonetheless be content-derived (a readable head plus a structural hash — the `_xxhash` convention in the shipped fixtures, where the hash spelling is a placeholder precisely because it is not a conformance point): content-derived names keep re-resolution output diff-stable and make independently resolved namespaces agree wherever their structures agree, which is what lets internal entries merge cleanly under the transitive import ([TSON-SCHEMA] §2.2.3, §8.2).

Telling the families apart needs no name either: a synthetic entry's value is headed by the bare constructor it builds and its schema-map key carries the derived `@synthetic` marker (discarded and recomputed on ingest), while a declared instantiation's key carries `@application`, the application itself, read as stated on ingest since it is the one fact the value does not state. The marker exists for display — a tool can fold synthetics back into the nested spelling the author wrote — and never for decoding. Synthetic entries are, beyond that, an implementation's own: their shape is unconstrained past producing the required output, a processor may mint one per schema or share them, and the lift targets are never kernel-declared (a proposal to declare them was declined — it would hard-code one resolver's bookkeeping into an immutable, hash-pinned vocabulary for an artifact no author writes). Two disciplines this asks of tooling, both learned by writing the wrong thing first. A consumer contract — a binding map, a generated class table, a configuration file — may never key on a minted name: its spelling is not stable across implementations and moves within one whenever the form's content does, so it keys on the name some declaration gave the application, and a processor holding both shows and accepts the declared name wherever it shows or accepts one. And a diagnostic never surfaces a minted name as the primary form; the source form is recoverable from the originating position, and from `@application` where an entry carries one, and it is what the user wrote — a minted record is named as the body a record is written with (`unknown field 'nope' on '{ v: int32 }'`), and a minted atom instance by its position and the family's own words. The declared applications of one template are recovered from `@application` — the family a host language spells `Result<T>` is a walk over entries — which is what makes "a template is not a type" workable for a generator. An author who wants an application to have a name of its own declares it, `order_page => page<order>`, and that declaration is the entry ([TSON-SCHEMA] §8.2) — and where the application composes onto a record, and so is a member of a family, declaring it is required (§2.10). A minted name written in data is not refused: such a document was written by reading one processor's output, and it fails at the next.

### 8.2 The chain, once more

Revision 38 removed the wrapper every entry once wore — `person => !type_definition { source: record  supertypes: […]  subtypes: […]  body: !record { … } }` — because each of its four facts had a home of its own: the body is the entry's value; `supertypes` and `subtypes` are facts about a *type*, so they are fields of `type`, which every `!record`, `!integer_type`, `!array` and `!choice` body composes and `reference`, `template` and a namespace value do not; `source` for a construction was the body's tag said twice, for a refinement the chain's nearest member, and for an application the entry's identity under another name, which moved to the key as `@application`. `record.supertypes`, the body's "lineage", went with it: direct parents are the chain's minimal members, operand order is in the flattened `fields`, and nothing read it after resolution — a subtracted record is now a record with an empty chain, and what it was subtracted from is in the source document, which has it. Consumers should internalise what remains: `chain.supertypes` is the **contract** (the complete IS-A chain, closed to a fixed point — use it for "can a value of X go where Y is expected"), and it is primary data rather than a cache, because a flat body and its chain cannot be recomputed from each other and the atom family makes the point sharply (a refinement and a sibling construction serialize identically except for the chain). `chain.parents` survives only on a record template, where a parent may still be an application (`<T> result<T> & { … }`), and is closed with the rest when the template closes. `chain.subtypes`, a kind's `members` and a choice's `disjoint` are the three recomputable caches, never trusted on ingest; `chain.supertypes`, `chain.parents` and `@application` are read as stated and verified. Where a resolver once asked whether an entry was a type by excluding `data`, it now asks whether its value's constructor IS-A `type` — and `top`, `type`, the base kinds and `namespace` being `abstract`, the schema map itself reads by the rule every abstract record without discriminators has: dispatch by tag among the subtypes of a base that has no instances of its own.

### 8.3 Error messages

The specification's error categories are minimal by design; implementations compete on diagnostics. Some conventions that have worked well: report *parse* failures ("`twelve` is not an integer") separately from *validation* failures ("300 exceeds age's max of 150") since users fix them differently; never surface internal entry names as the primary form (show the source form, recovered from the originating position and, for a declared instantiation, its `@application` — the user wrote `[text; 1..]`; show them `[text; 1..]`); on unresolved-type errors under a schema, say *which namespace was searched* — "no `uuid` in schema X (did you mean to import core?)" turns the most common beginner error into a one-line fix; locate a template's deferred failure at the declaration that *wrote the offending name* (the template for a bad type inside its body, the applier for a bad argument), not at whichever entry happened to be materialising ([TSON-SCHEMA] §5.10); and keep the two outcomes beside the categories — **refused**, a rejection by this processor under its own configuration (name hygiene, a resource limit, a schema it does not hold and will not fetch), and **undetermined**, a schema it could not obtain — visibly apart from a validity error *and from each other* while reporting them in the *same* list, distinguished by the rule or limit that refused or the reason the schema could not be obtained, with the report carrying the UCD version and the policies it was judged under (§3.5). Revision 37 had filed both under one "not judged" outcome, and the consumer the series is built for — a developer, or a model, checking a document against a server's schema and policy — asks *will this be accepted*, to which the two give opposite answers: a refusal is a certain rejection whose fix the sender holds, and an unobtainable schema is no answer at all. A report therefore summarises acceptance as accepted, rejected (by a category error or a refusal) or undetermined, with validity carried by the categories alone; a closed library's miss is a refusal and never "not found", the library being the deployment's whole configuration; and a fetch refusal names its rule but not the allowlist, since telling an untrusted sender which hosts a processor will fetch from is reconnaissance for request forgery. An undetermined schema in particular is not a resolver error, whatever the category rule would otherwise assign it: nothing was read to judge by, the next processor along may hold the schema and accept the document whole, and a command line that exits "invalid" on it sends the sender to fix the wrong thing — where a pin that *was* obtained and does not match is a verdict about bytes the document committed to, and stays one ([TSON-SCHEMA] §10.1, §10.2). A limit refusal in particular must never surface as a validity error — the fallback an unspecified category invites (a stack overflow escaping every handler and exiting with "your document is invalid") is the one verdict a document that is merely large must not get; Part 1 §9.1 gives every limit a default and a category so it does not have to.


## 9. Deployment and Encoding Guidance

### 9.1 Versioning: publish, don't mutate

Most schema systems version by *mutating a shared definition* and policing the mutation with compatibility rules. TSON versions by *publishing*: a schema's identity is its exact byte content, so version N and version N+1 are independent, immutable artifacts with different hashes and different identities ([TSON-SCHEMA] §3.5). Nothing evolves in place. A data document locks to the one contract it was written against — the hash-pinned `!!schema` on its opening lines — and that binding never drifts, because neither side can change.

Acceptance then becomes explicit, and it lives where it belongs: at the boundary. A server's code binds to the *set* of schema versions it accepts, registering each in its schema library, and every request is validated against exactly the contract the request names. Because the version key sits in the header, dispatch is cheap: document kind and schema identity are readable from the opening bytes, before any value parsing ([TSON-DATA] §2.2, §7.1).

For major upgrades there is a stronger pattern than binding one server to every version: run version-specific servers and **route on the schema reference**. A front door reads `!!schema`, matches canonical identity and hash, and dispatches; each server behind it holds exactly one contract at full strength, and retiring a version is deleting a route. This is the architecture the industry converged on from the other direction — Stripe's date-versioned API with server-side translation between pinned versions is the best-documented example ([Stripe, *APIs as infinite versions*](https://stripe.com/blog/api-versioning)) — but there the version key is a bespoke HTTP header; here it is a first-class, verifiable property of the document itself.

What this retires is the compatibility rulebook — the folklore every mutable-schema system accumulated because one definition had to serve every point in time:

- **Protocol Buffers.** `required` is marked *"Do not use"* in the [proto2 language guide](https://protobuf.dev/programming-guides/proto2/), the maxim being "required is forever"; Buf documents it as the root cause of early Google outages ([Tip of the Week #8: never use required](https://buf.build/blog/totw-8-never-use-required)). The [*Updating A Message Type*](https://protobuf.dev/programming-guides/proto2/#updating) rules add the rest of the liturgy: never change a field number, never reuse one, mark removals `reserved`, add only optional or repeated fields. Proto3 went further — deleted `required` outright and made every field optional with zero-value defaults — then spent years restoring the [field presence](https://protobuf.dev/programming-guides/field_presence/) that deletion had cost.
- **Avro.** Reader and writer schemas are reconciled pairwise under the specification's [Schema Resolution](https://avro.apache.org/docs/current/specification/) rules — new fields must carry defaults, renames need aliases — and an entire product category, the schema registry, exists to police mutation with [BACKWARD / FORWARD / FULL and transitive compatibility modes](https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html).
- **GraphQL.** The official best practice is a versionless API under ["continuous evolution"](https://graphql.org/learn/best-practices/#versioning): never remove, only `@deprecated`.
- **Beneath them all,** Postel's robustness principle — *"be liberal in what you accept"* (RFC 761) — which the IAB has since formally walked back as a long-term hazard to protocol health (RFC 9413, *Maintaining Robust Protocols*).

Every rule on that list is the same trade: weaken the contract so the timeline stays compatible. Fields end up optional forever, unknown fields must be tolerated, removal is forbidden — and the guarantees the schema no longer makes migrate into application code as defensive checks, validation pushed from a place where it is declared once to a place where it must be reimplemented everywhere. (Part 8 of the proto-schema research traces this history in detail.)

TSON's model dissolves the dilemma rather than picking a side. Records are closed under their type ([TSON-SCHEMA] §7.2) and required means required — *per version*. A field can be required in version 12 and gone in version 13, because 12 and 13 are different artifacts and no rule forces one definition to satisfy both. Compatibility between two versions becomes something tooling can **check** — resolver output is data, so two versions diff structurally — and a policy a team may **choose** at a given boundary, never a constraint the schema language imposes on every author to keep a mutable timeline coherent.

### 9.2 Schema libraries in practice

The library model (lookup, never fetch-by-default) maps onto deployments straightforwardly: production systems register every schema at startup — from files, embedded resources, or an internal registry — and disable runtime fetching entirely; development setups may enable fetching with an allowlist and treat it as a cache-population convenience. Registering under an application-supplied identity (for `!!id`-less development schemas) is handy in tests but should never survive into interchange: publish with `!!id`, pin with hashes at trust boundaries. A reference a deployment *will not* supply — a closed library's miss, a host off the allowlist, a missing required pin — is a refusal, as deployment-specific as a name policy and reported as one; only a reference nothing was behind leaves a document undetermined ([TSON-SCHEMA] §10.1). An allowlist matches hosts as `host` values, so an operator lists a host in either label form and an identity's host matches by name, and a look-alike host is refused unless it was listed; a fetch refusal names its rule and not the list, since the list is what a forger would most like to learn.

**Chain-wide strictness is a resolver policy.** The mixed-reference rule (§3.4) deliberately leaves "every identity in my closure is verified" out of the format; deployments that want it enforce it at the boundary. Two mechanisms cover the need: a *require-pinned* resolver mode, which fails resolution when any identity's digest set is empty (pre-loaded entries excepted, or checked as below); and a resolution report listing which identities resolved unpinned, for audits that prefer visibility to refusal. Together they give a deployment "my entire chain is verified" as a choice made where trust boundaries are actually known — per deployment — rather than a constraint the format imposes on every author in every closure.

**Policies are the deployment's, not the schema's.** The identifier and token policies of [TSON-DATA] §8.2 and the limits policy of §9.1 are configuration of the processor instance, set in code at the call site, and a processor should be able to state all of them with no document in hand — `policy` on a command line, a method on a reader — so that a counterparty can learn what will be accepted before writing. No schema carries a policy and none may (§3.5); a `.well-known` projection of an origin's acceptance profile is the shape discovery will most likely take, and the refusal remains the one report that cannot be stale.

**Ship digests with pre-loaded schemas.** A pinned reference to a pre-loaded identity (`!!meta:"…meta.tn?sha256=…"`) must be checkable, but the pre-loaded model is an in-memory structure, not bytes. Implementations should therefore ship, alongside each pre-loaded schema, its published canonical digest (or the canonical document bytes), and compare declared pins against it — a mismatch is the ordinary verification error, not an unverifiable assertion. Without this, pins to the meta layer degrade into decoration exactly where the chain is supposed to ground.

### 9.3 Defaults on the wire

Encoders should write values for defaulted fields. A document that states its defaults reads without its schema — `priority: 3` means 3 to every reader, schema in hand or not — whereas omission makes the document's meaning schema-relative. Omitting fields at their default values is a legitimate wire-size optimisation precisely because the decoder injects the value back on read, but it should be an explicit encoder option, not the default posture. Two things are not options at all: a marker written on an unmarked name (`version: text = "2.0"`) must be written, since it exists for readers that do not hold the schema; and a void value at `a: T?` or `a?: T? ~ v` must be spelled `_` (JSON `null`), because omission there is a missing key or the default — the two declarations a schema-less encoder cannot serve, and the reason an encoder that must serve them is schema-directed (§2.9). Resolver output is the sanctioned exception: it omits fields at defaults because its consumers are, by definition, schema-aware, and the compression materially improves fixture readability.

### 9.4 Directive-per-line

Scoped `!!schema` directives on array elements read best one element per line, directive first — the layout in [TSON-SCHEMA] §7.8's example. The grammar does not require it, but the convention keeps the scope-opening directive visually attached to the single element it governs, which matters in review: scope changes are the highest-consequence lines in a document.


## References

| Reference | Title |
|-----------|-------|
| TSON-DATA | TSON Part 1: Text Data Format |
| TSON-SCHEMA | TSON Part 2: Type System and Schema |
| TSON-JSON | TSON Part 3: JSON Encoding |
| RFC 3986 | Uniform Resource Identifier (URI): Generic Syntax |
| RFC 3987 | Internationalized Resource Identifiers (IRIs) |
| CommonMark 0.31.2 | CommonMark Spec (the text of `@doc`) |
| UAX #31 | Unicode Identifiers and Syntax |
| UTS #39 | Unicode Security Mechanisms |
| UTS #55 | Unicode Source Code Handling |
| RFC 3339 | Date and Time on the Internet: Timestamps (Appendix A: durations) |
| RFC 3492 | Punycode: A Bootstring encoding of Unicode for IDNA |
| RFC 4648 | The Base16, Base32, and Base64 Data Encodings |
| RFC 5890 | IDNA: Definitions and Document Framework |
| RFC 5891 | IDNA: Protocol |
| RFC 5892 | The Unicode Code Points and IDNA |
| RFC 5893 | Right-to-Left Scripts for IDNA |
| RFC 5952 | A Recommendation for IPv6 Address Text Representation |
| RFC 6838 | Media Type Specifications and Registration Procedures |
| RFC 6839 | Additional Media Type Structured Syntax Suffixes |
| RFC 9110 | HTTP Semantics |
| RFC 8259 | The JavaScript Object Notation (JSON) Data Interchange Format |
| RFC 761 | DoD Standard Transmission Control Protocol (origin of the robustness principle) |
| RFC 9413 | Maintaining Robust Protocols |