8. Conformance and errors¶
8.1 Status of this chapter¶
The error-code taxonomy in §8.3 is normative content that implementations
are still migrating onto. It exists because conformance testing needs
stable, language-independent identifiers — comparing human-readable message
strings across languages is not a test, it is a coincidence. See the
§8.3 error codes row in §9.3
for current per-port adoption status.
None of this is required for the current release; it is required before a version of this spec declares §8.3 mandatory. Until then, an implementation is conformant on error behavior — which inputs fail, and where — without being conformant on error codes. §8.5 separates the two so that can be measured, and §8.5.2 rule 4 lets a harness run in code-agnostic mode for exactly this reason.
8.2 Code format¶
A code is a lowercase, dot-separated path. Each segment is
[a-z][a-z0-9-]*. The first segment is the family. Codes are stable
identifiers: once published, a code's meaning MUST NOT change. Retiring a code
means adding a new one and leaving the old one documented as retired.
Codes are not messages. An implementation MUST emit a human-readable message alongside the code, and the message MAY be localized, reworded, or improved at any time. Conformance tests match on codes and paths, never on message text.
Every diagnostic carries at least:
| Field | Meaning |
|---|---|
code |
The identifier from §8.3 |
path |
Location in the Document or schema; see §8.4 |
message |
Human-readable, actionable, unversioned |
severity |
error, warning, or info |
8.3 The taxonomy¶
8.3.1 parse.* — text to Document, stage 1¶
| Code | Raised when |
|---|---|
parse.unexpected-token |
A token appears where the grammar does not allow it |
parse.trailing-content |
Content remains after the document's single node |
parse.unterminated-string |
A string is not closed before end of input |
parse.invalid-escape |
An unrecognized backslash escape |
parse.unpaired-surrogate |
A \uXXXX surrogate escape without its partner |
parse.control-character |
A literal control character in a context that forbids it |
parse.reserved-word-label |
null, true, or false used as a bare label |
parse.bare-word |
A bare identifier in value position that is not null/true/false |
parse.empty-array |
[] in OML value position |
parse.nested-array |
An array element that is itself an array |
parse.separator-in-array |
A newline or ; used as an array separator |
parse.leading-zero |
A NUMBER/INTEGER literal's integer part has a leading zero |
parse.invalid-date |
A DATE or the date portion of a DATETIME is not a valid calendar date |
parse.invalid-time |
A TIME, the time portion of a DATETIME, or a tz-offset is out of its valid clock range |
parse.codec-syntax |
Input is not well-formed in its own source format (JSON, YAML, TOML, XML) — see the note below |
parse.invalid-encoding |
Input is not valid UTF-8 (§2.5) |
Six of these codes also cover OSD's own lexical stage:
parse.unexpected-token, parse.trailing-content,
parse.unterminated-string, parse.invalid-escape,
parse.unpaired-surrogate, parse.control-character. OSD produces a
Schema rather than a Document, but tokenizing OSD source text is the same
kind of operation as tokenizing OML source text — a lexical failure before
any semantic well-formedness checking has begun (§8.3.3 covers
well-formedness, not lexing) is the same class of error regardless of what
stage-2 structure a successfully-tokenized input eventually becomes. An
unterminated string or an unexpected character in OSD source MUST be
reported with the matching parse.* code above, never a schema.* code
and never an implementation-invented code outside this taxonomy. The
remaining five codes (reserved-word-label, bare-word, empty-array,
nested-array, separator-in-array) describe OML's value grammar
specifically and have no OSD equivalent.
Codec read failures — input that is not well-formed in its own source
format — use parse.codec-syntax. Every other code in this family describes
OML or OSD specifically, so before this existed there was nothing to report
for malformed JSON, YAML, TOML or XML, and an implementation had to invent a
name for the most common failure a codec has. The message SHOULD name the
format and carry through whatever the underlying parser said, since that
detail is what makes the failure actionable; §8.5.2 never compares message
text, so this costs nothing in conformance.
One code covers all four formats deliberately. Splitting it per format would add three codes that convey nothing the message does not already say, and nothing in the algebra or the harness branches on which codec failed.
Distinguish this from the refusal codes in
§8.3.8: those are for input that is
well-formed in its source format and outside the profile Omnist supports.
parse.codec-syntax means the input is genuinely malformed. Reporting a
refusal as a syntax error, or the reverse, sends a user looking in the wrong
place.
8.3.2 document.* — building and limits¶
| Code | Raised when |
|---|---|
document.limit.depth |
Nesting exceeds the implementation's configured depth limit |
document.limit.nodes |
Node count exceeds the implementation's configured node limit |
document.limit.int-digits |
An integer literal exceeds the implementation's configured digit limit |
document.unlabeled-element |
An input construct has no label to become an edge |
These three document.limit.* codes correspond exactly to the three
quantities in §2.4 — no fourth, no
tiers. The codes are fixed; the threshold that triggers each one is not
— an implementation MAY configure any of the three limits to a value other
than the reference default, per §2.4, but whatever value it configures,
crossing it MUST raise exactly this code, never a different one and never
silently.
8.3.3 schema.* — schema well-formedness¶
| Code | Raised when |
|---|---|
schema.no-root |
No root declaration |
schema.duplicate-root |
More than one root declaration |
schema.unknown-type |
A type name resolves to neither a scalar nor a defined record — covers a dangling ref, and equally a scalar naming something outside the fixed keyword set |
schema.duplicate-record |
A record name is defined twice |
schema.duplicate-field |
A label is used by two fields in one record |
schema.reserved-name |
A record is named after a scalar kind or any |
schema.invalid-cardinality |
Negative minimum, or maximum below minimum |
schema.non-integer-cardinality |
A cardinality bound is not a whole number |
schema.empty-cardinality |
[] written as a cardinality |
schema.unquoted-label |
A bare name in field-label position |
schema.empty-label |
A field label is the empty string |
schema.bracket-in-label |
A field label contains [ or ], which could collide with the repeated-label diagnostic-path convention (§3.6.1) |
schema.quoted-type |
A quoted string in type position |
schema.nullable-ref |
? applied to a reference |
schema.nullable-any |
any? |
schema.missing-key |
A required key of the schema-construction input (a field's label or type, a record's name, a type's kind) is absent entirely |
schema.invalid-type |
A value that must be a specific kind (a label, a name) is present but of the wrong kind |
schema.unknown-key |
An unrecognized key appears anywhere in the schema-construction input |
schema.unquoted-label and schema.quoted-type are the two directions of
§5.2's quoting rule — a bare name
belongs only in type position, a quoted string only in label position, and
each direction gets its own code.
schema.missing-key, schema.invalid-type, and schema.unknown-key are
general Schema-construction rules with no OSD-text analog — OSD's own
grammar makes them structurally impossible to trigger from OSD text (a
field with no type, a non-string label, or a stray key cannot be written
in OSD's syntax at all). They become reachable only through an input whose
structure isn't grammatically constrained the way OSD text is — for
example, Extensions: OSD-OML, which builds a
Schema from a generic OML Document instead of an OSD parse tree. They are
defined here, not in that extension's own chapter, because the
schema-construction validator that raises them is shared, unchanged, Core
machinery — the extension supplies a second input shape, not a second set
of rules.
8.3.4 validate.* — document against schema¶
| Code | Raised when |
|---|---|
validate.shape-mismatch |
A value where a node is expected, or the reverse |
validate.type-mismatch |
A value of the wrong scalar kind |
validate.null-not-allowed |
null at a non-nullable scalar |
validate.unexpected-field |
A label no field of the closed record names |
validate.cardinality |
An edge count outside [min, max] |
Non-normative. These five are the codes a user meets most often, since
they fire on ordinary data rather than on a malformed schema or a broken
implementation. Each diagnostic pairs one with a path
(§8.4) locating the offending edge — $.order.items[2].sku
names the sku inside the third items edge — so the useful reading order
is path first, then code. What each typically means in practice:
| Code | Usually means |
|---|---|
validate.shape-mismatch |
A scalar where the schema expects a record or the reverse — often a value that should have been wrapped in an object, or a single-field object flattened by an upstream tool |
validate.type-mismatch |
Right shape, wrong scalar kind — most often a number that arrived as a string, common when data came from CSV, form input, or any format without typed literals |
validate.null-not-allowed |
An explicit null where the field is not nullable. If absence is what you meant, the fix is usually cardinality [0,1], not a nullable type — see §3.5 |
validate.unexpected-field |
A label the record does not declare. Records are closed, so this is a typo, version skew between producer and schema, or a field that genuinely needs adding |
validate.cardinality |
Too few or too many edges carrying one label — a missing required field, or repetition where the schema allows one |
The two most often confused are null-not-allowed and cardinality. "May be
absent" and "may be null" are different questions with different mechanisms,
and reaching for the wrong one is the most frequent modelling error in
Omnist; §3.5 exists for exactly that.
8.3.5 materialize.* — schema-directed deserialization¶
| Code | Raised when |
|---|---|
materialize.inexact-conversion |
A leaf cannot be converted to the declared type without loss or invention |
Shape and cardinality problems found during materialization use the
validate.* codes above — materialization performs the same checks, so
there is no separate set of names for them.
8.3.6 algebra.* — operations over schemas¶
| Code | Raised when |
|---|---|
algebra.extract-invalidates-root |
extract's keep set removes a label the root needs |
algebra.infer-no-samples |
infer called with zero samples |
algebra.infer-scalar-root |
A sample's root is a value rather than a node |
algebra.infer-conflicting-scalars |
Samples disagree on a scalar kind, other than integer/number |
algebra.infer-mixed-shape |
Samples disagree on whether a label's value is a node or a scalar |
8.3.7 lint.* — schema diagnostics¶
| Code | Severity |
|---|---|
lint.unsatisfiable-record |
warning |
lint.unreachable-record |
warning |
lint.duplicate-record |
warning |
lint.any-field |
info |
8.3.8 format.* — codec adjustments¶
| Code | Severity | Meaning |
|---|---|---|
format.temporal-stringified |
warning | A temporal leaf was written as an ISO-8601 string |
format.dtd-forbidden |
error | A DOCTYPE declaration, outside the data-XML profile (XML) |
format.entity-forbidden |
error | An entity reference other than XML's five predefined ones |
format.mixed-content |
error | Text alongside child elements in one element, which has no Document shape |
format.attribute-dropped |
warning | An XML attribute was discarded on read |
format.namespace-dropped |
warning | An XML namespace prefix was discarded on read |
format.interleaving-lost |
warning | Cross-label interleaving could not be written |
format.multiple-roots |
error | A multi-root Document cannot be written to a single-root format |
format.string-line-break-char |
warning | A label or value contains U+0085 (NEL); written quoted so it round-trips |
format.value-stringified |
warning | A non-string scalar was written as text in a format with no native typed literals for it, so it reads back as a string |
format.attribute-dropped, format.namespace-dropped, and
format.interleaving-lost MUST be emitted wherever the codec adjustment
they describe occurs, with a conformance vector for each. Per-implementation
status lives in §9.3's table and
nowhere else.
Every code above describes a write that still succeeds, and — this is the
test that matters, not merely "is there only one available fallback" —
none of them can produce output indistinguishable from some other,
genuinely different, independently-valid Document. format.value-stringified
and format.temporal-stringified look like exceptions on the surface (XML
has no typed literals at all, so an integer and the string that looks like
it do write identically, with no way to tell them apart even with a
schema at read time — confirmed live), but that ambiguity is unavoidable
for every typed scalar XML ever writes, not a choice Omnist's writer is
making; failing here would mean XML could never write a single typed value,
not that it handles a rare edge case more strictly. That's the same
category format.interleaving-lost is already in.
Five adjustments that used to be in this table are not like that, and MUST NOT be treated as ones a writer can choose an arbitrary fallback for and still succeed — each one collides with a genuinely different, independently-valid input, in a way that is narrow (it doesn't affect every write of that kind, only a specific case) and therefore avoidable by simply failing on that case, rather than disabling the format broadly:
- A label isn't a legal identifier in the target format (e.g. a space in
an XML tag name) —
format.key-sanitized. Confirmed live: two different labels can sanitize to the same tag ("my label"and"my_label"both becoming<my_label>), producing, on read-back, a Document that looks like one label legitimately repeated twice. - A string contains a character the target format cannot represent at all
(e.g. a raw C0 control character XML 1.0 forbids) —
format.string-illegal-char. - A null leaf cannot be represented in the target format at all (e.g.
TOML, which has no null token of any kind) —
format.null-unrepresentable. There is no substitute value at all here, only the option to silently drop the edge entirely, which erases the edge's existence rather than merely altering its value. NaNor an infinity has no legal spelling in the target format (e.g. JSON, whose only sentinel for this isnull) —format.float-special. Confirmed live: writing a genuinenullvalue and writingNaNproduce the identical JSON token, and both read back as the identical Document value — there is no way, after the fact, to tell a substitutedNaNfrom an originalnull.- An empty internal node has no distinct spelling from an empty string
leaf in the target format (e.g. XML's self-closing
<x/>) —format.shape-empty-ambiguous. Confirmed live: the internal-node-ness is gone on read-back, silently, with no way to recover it.
A previous version of this spec had all five succeed anyway. The correct
behavior for all five is write.unsupported-value (below): the write
fails, unconditionally, not only under strict.
format.dtd-forbidden, format.entity-forbidden and
format.mixed-content are a different shape from the rest of this table.
They are read-side refusals, not adjustments: the input is well-formed XML
that Omnist declines to support, per the
data-XML profile. They sit here
because they are codec-specific conditions, which is what this family is
for — but note two consequences. They are error severity and the read
fails, so there is no adjusted result to report alongside them. And they
MUST NOT be reported as syntax errors: the document is valid XML, and
telling a user otherwise sends them hunting for a defect that is not there.
A sixth code, format.string-cr-normalized, used to exist in this table
for a related reason — XML mandates line-ending normalization on parse, so
a literal \r byte and a literal \n byte, written as-is, are
indistinguishable on read-back, the same collision shape as the five
above. That code no longer exists, but not because the write now fails:
a literal \r MUST be written as the numeric character reference
, not as a raw byte. XML's mandatory normalization applies only to
literal line-ending bytes in the source text, not to a numeric character
reference — confirmed live that (and \n for \r\n) survives
a compliant parser and reads back as the exact original byte sequence.
Once the writer does this, the write is genuinely lossless, not merely
reported-as-lossy — there is nothing left to adjust or warn about, so no
diagnostic code is needed for this case at all.
8.3.9 write.*¶
| Code | Raised when |
|---|---|
write.unsupported-value |
A value has no representation in the target format and strict mode is in force, or a label/string/null leaf/special-float/empty-node cannot be represented at all in the target format's own syntax without colliding with some other, distinct, valid input (unconditional, regardless of strict) |
8.4 Paths¶
A path locates a diagnostic. Paths are normative and MUST be byte-identical across implementations, because conformance vectors match on them.
Document paths start at $ and descend by label. A repeated label is
disambiguated by a zero-based occurrence index in brackets.
$ the root node
$.name the single edge labeled `name`
$.item[0] the first edge labeled `item`
$.item[2].sku `sku` inside the third `item`
The index MUST be present when the label occurs more than once in that node, and MUST be absent when it occurs exactly once.
Schema paths are RecordName for a record-level diagnostic and
RecordName.label for a field-level one.
The whole-schema fallback is $. Some diagnostics have no specific
record or field to name: schema.no-root, schema.duplicate-root, a
dangling root reference, and algebra.infer-no-samples/
algebra.infer-scalar-root (these fail before any schema exists). All five
use $ — the same sentinel Document paths use for the whole node — as the
schema-side/pre-schema equivalent of "the whole thing, not a part of it."
Text-position paths are for parse.* diagnostics (§8.3.1) — stage 1
fails before any Document exists, so there is no $-rooted structure for a
Document path to descend into. The format is line:col, 1-based, computed
from the byte offset of the failure:
1:1 the very first character
14:8 line 14, column 8
A parse.* diagnostic's path MUST be a text-position path. A document.*,
schema.*, validate.*, materialize.*, algebra.*, or lint.* diagnostic's
path MUST be a Document or Schema path — never a text-position path, since a
Document or Schema already exists by the time any of those families can fire.
8.4.1 Which kind each schema.* code uses¶
Saying "a Document or Schema path" leaves the choice open, and paths are compared byte-for-byte, so the choice has to be pinned per code rather than left to the implementation.
For most schema.* codes the answer was only ever implicit: OSD text's
grammar establishes a record or field's identity before those checks can run,
so a Schema path is the only thing that could be built. That stops being true
for the four codes reachable only from a Document-shaped surface such as
OSD-OML, which is not grammar-constrained the same
way. A record node whose name key is missing has no RecordName from
which to build a Schema path at all.
schema.missing-key,schema.invalid-type,schema.unknown-key, andschema.invalid-nameMUST use a Document path, rooted at the node the violation occurs on —$.record[0]for a record node missing itsname,$.record[1].field[2].type.kindfor an invalidkindvalue.- Every other
schema.*code MUST use a Schema path, unchanged from OSD text: these checks never run until the enclosing record or field has a valid name or label to build one from. - The whole-schema cases above keep
$and take precedence over both rules. Three of §8.4's five fall under this section:schema.no-root,schema.duplicate-root, and a dangling root reference. The other two arealgebra.*codes, outside this section's scope and unaffected by it.
This applies to any surface, present or future, that can construct a Schema without a grammar fixing identity first — it is a property of the codes, not of the extension that first made them reachable.
8.5 Conformance harness protocol¶
A conformant implementation passes the vectors in test-suite/. Vectors are
JSON, one case per object, grouped into files by operation.
8.5.1 Common envelope¶
{
"name": "unique-vector-id",
"spec": "docs/03-schema-model.md#36-validation",
"operation": "validate",
"purpose": "happy-path",
"input": { },
"expect": { }
}
nameMUST be unique across the whole suite. A harness reports results keyed on it.specpoints at the section the vector pins. A vector with no section to point at is a vector testing something unspecified, which is a spec defect.operationselects the driver. It MUST be one of the operation names listed on the Operations & Models Reference page — that page is the vocabulary's single source, not a free-text field vectors can spell differently across files.purposeMUST be one ofhappy-path,edge-case,error-case, ordeterminism-regression— what a vector is actually pinning, so a reader doesn't have to reverse-engineer it from the input/expect pair.happy-pathis an ordinary conforming case with nothing specific being probed;edge-casedeliberately exercises a specific rule, invariant, or boundary (regardless of whether the outcome is success or failure);error-caseis primarily testing that an invalid input is correctly rejected;determinism-regressionpins a specific ordering/reproducibility regression tied to a known bug.expectholds either a success value or adiagnosticslist.
8.5.2 Diagnostics matching¶
"expect": {
"ok": false,
"diagnostics": [
{ "path": "$.port", "code": "validate.type-mismatch" }
]
}
Matching rules, all normative:
- Message text MUST NOT be compared.
- The diagnostic list MUST be compared as a set, not a sequence. Ordering of diagnostics is not specified and implementations may find problems in any order.
- Every expected diagnostic MUST be present, and no unexpected diagnostic may be. Partial matching is not permitted; an implementation reporting three problems where the vector expects two has failed.
- A harness MAY be run in code-agnostic mode, comparing only
okand the set of paths. This is the mode implementations that have not yet adopted §8.3 run in. A run MUST state which mode produced its results.
8.5.3 Operation drivers¶
operation |
input |
success expect |
|---|---|---|
parse |
{format, text} |
{ok, document} |
parse_schema |
{text} |
{ok} — schema: <canonical OSD text> MAY additionally be present, compared byte for byte per §3.3/§5.9, for a vector specifically pinning declaration-order or formatting round-trip fidelity rather than mere acceptance |
validate |
{schema, document} |
{ok} |
materialize |
{schema, document} |
{ok, document} |
write |
{document, format} |
{ok, text} — diagnostics MAY be present alongside a successful {ok: true, ...} result (a write can succeed with a reported adjustment, e.g. format.temporal-stringified; success and a diagnostics list are not mutually exclusive here the way they are for every other operation) |
compatible_with |
{a, b} |
{result: bool} |
equivalent |
{a, b} |
{result: bool} |
normalize |
{schema} |
{schema: <canonical OSD text>} — compared byte for byte per §5.9's canonical-output requirement |
prune |
{schema} |
{schema: <canonical OSD text>} |
is_empty |
{schema} |
{empty: bool} |
extract |
{schema, keep} |
{ok, schema} |
infer |
{samples, allow_any} |
{ok, schema} — allow_any defaults to false when absent |
infer_with_report |
{samples, allow_any} |
{ok, schema, fallbacks} — fallbacks is a list of {location, reason}, always present on success (empty when nothing was opened) |
lint |
{schema} |
{ok, findings} — findings is a list of {code, severity, location}; message text is never compared (§8.5.2 rule 1) so no message field is required |
schema_from_document |
{document} |
{ok, schema: <canonical OSD text>} — compared byte for byte, same rule as parse_schema's implicit round-trip (§3.3, principle 1: nothing merges, so declaration order is preserved from the input Document's own edge order) |
parse_schema_oml |
{text} |
{ok, schema: <canonical OSD text>} — same comparison as schema_from_document; this operation is schema_from_document(read_oml(text)), per §E.11 |
schema_to_document |
{schema} |
{ok, document} — compared as a Document (§8.5.4's canonical encoding), which is order-sensitive per D-1/D-3; this is where §3.3's order principles actually become observable for this direction |
write_schema_oml |
{schema} |
{ok, text} — compared byte for byte as OML text, same rule as write's Document-writer vectors; this is write_oml(schema_to_document(schema)), per §E.11 |
Every operation's failure expect is {ok: false, diagnostics: [...]}, per
§8.5.2 — write is the only operation where ok: true and diagnostics can
coexist, noted above.
The 4 OSD-OML operations split across the same two comparison shapes
every other operation already uses, not a third one. schema_from_document/
parse_schema_oml return a Schema, compared as canonical OSD text —
identical in kind to normalize/prune's existing byte-for-byte rule, not
a new mechanism. schema_to_document/write_schema_oml return
Document-shaped output, compared the same way write's vectors already
are. This is what makes §3.3's
order principles actually conformance-checkable for OSD-OML specifically:
before this table entry existed, a vector like
extensions-osd-oml/parse/worked-example-round-trips-to-equivalent-osd
had no documented comparison rule at all, so nothing guaranteed a harness
would catch a declaration-order regression even though the vector's own
expect.schema field encoded one.
materialize's input is a canonical-JSON Document, not raw format text
plus a format field. This is deliberate: per
Operations & Models Reference,
materialize's abstract signature is materialize(node, S) -> node — it
operates on an already-parsed Document, matching §7.1's two-stage
separation. A vector wanting to exercise the full parse-then-materialize
pipeline for a specific format uses two vectors — one parse, one
materialize — not one vector conflating both stages.
Schemas in input are OSD text. Documents are given in the canonical JSON
encoding of §8.5.4, not in a format-specific text, except where the vector is
specifically testing a parser.
write vector text comparison for XML MUST ignore insignificant
inter-tag whitespace. Unlike JSON/YAML/TOML/OML, this spec places no
normative requirement anywhere on XML writer output formatting (no
indentation, no line-break convention) — codecs and deserialization (§7.3)
and the XML format page (docs/formats/xml.md) both describe only the
structure a writer must produce, never its whitespace. Two conformant
writers may legitimately disagree on this and both be correct. Before
comparing a write vector's text against a target's actual output for
XML, a harness MUST first strip whitespace that occurs strictly between >
and < from both strings, then compare what remains. Whitespace inside
actual element text content (the content between an element's own opening
and closing tags, with no nested elements) MUST NOT be touched — that is
data, not formatting, and stripping it would corrupt what's being compared.
This is safe specifically because an omnist writer never produces XML with
mixed content — the Document model (§2) gives every node either child
edges or a single scalar value, never both at once, so an element's content
is always either entirely sub-elements or entirely one text value, and
"whitespace strictly between > and <" can only ever be inter-tag
formatting, never a fragment of real text data. (General XML documents with
mixed content don't have this guarantee, which is why this rule is scoped
to XML vectors in this conformance suite, not stated as a general-purpose
XML-comparison algorithm.) One reference implementation: strip via
text.replace(/>\s+</g, "><") (or each language's equivalent), then apply
the existing outer-whitespace trim every write vector comparison already
does.
8.5.4 Canonical document encoding¶
A Document must be written into a JSON vector file without JSON's own map-and-array shape smuggling assumptions back in. The encoding is explicit:
{"scalar": {"kind": "integer", "value": 42}}
{"edges": [["name", {"scalar": {"kind": "string", "value": "Ann"}}],
["tag", {"scalar": {"kind": "string", "value": "x"}}],
["tag", {"scalar": {"kind": "string", "value": "y"}}]]}
- A node is
{"edges": [[label, target], ...]}. The outer array preserves order; repeated labels appear as repeated entries. - A scalar is
{"scalar": {"kind": K, "value": V}}whereKis one of the seven kinds. Temporal values are ISO-8601 strings. Forintegervalues see the threshold rule below. nullis{"scalar": {"kind": null, "value": null}}.
The integer threshold is exactly ±(2^53 − 1). An integer whose absolute
value is at most 9007199254740991 MUST be encoded as a JSON number; any
integer outside that range MUST be encoded as a decimal string. Both a vector
author and a vector reader apply the same fixed bound, in both directions.
The bound is stated as a literal rather than by reference to any language's behavior, deliberately. It is the largest integer every IEEE-754 double can represent exactly, which makes it the one threshold that is a property of the interchange format rather than of whichever JSON library a given implementation happens to use — and §2.4's integer-digit cap allows integers far larger than this, so the case is reachable, not theoretical.
This is verbose on purpose, and the threshold is part of that purpose. A
vector file must not depend on the reader's JSON library to decide whether
1 is an integer or a number, and it must not depend on that library to
decide where "fits exactly" stops either.
8.5.5 Reporting¶
Building your own runner? Porting a Conformance Runner collects what all three existing ports learned, including this section's skip/CI discipline in practice.
A harness run reports, per vector: pass, fail, or skip. Skip is a first-class
result. An implementation that has not built extract yet skips those
vectors and reports the count. A run that hides skips as passes is worthless for
tracking convergence, which is the whole point of
chapter 9.
A skip MUST cite a reason, and the reason determines what else is required:
- Not yet implemented. The operation or feature doesn't exist yet in
this implementation. Temporary by nature — expected to become a
passonce the work lands. No ledger entry is required for this category on its own, though the usual issue tracker SHOULD have something open for it. - Documented divergence. The vector's outcome depends on a capability
this implementation's target language or design genuinely cannot provide
— not a missing feature, a structural limit. This category MUST have a
corresponding entry in chapter 9's divergence
ledger (see §9.4 for
the current open entries), and the skip reason a harness reports for the
affected vectors MUST cite that entry by number (e.g.
"skip: D-N") — a skip with no citable reason, or a reason invented ad hoc instead of pointing at a ledger entry, is not acceptable reporting under this section. A divergence this narrow is closed, and its entry removed from the ledger, once the implementation adds real support — it doesn't stay listed once resolved.
CI gating. A conformant CI run MUST fail the build when the fail count
is nonzero. A conformant CI run MUST NOT fail the build merely because the
skip count is nonzero — an implementation with real, cited reasons for
every skip has passed conformance in the sense this section defines.
Passing CI is a statement about fail, not about reaching zero skips;
chapter 9 is where the skip count's honesty is checked, not the CI gate.
8.6 OML/OSD conformance harness¶
§8.5's harness exercises operations through a canonical JSON encoding (§8.5.4) specifically to avoid the representational ambiguity of format text — the same Document can be written as OML or OSD in more than one way (indentation, array sugar versus repeated labels, field declaration order) and still be the identical model. That is the right design for testing operation correctness, but it never touches the OML/OSD text an implementation actually reads and writes, which is a second, independent thing worth testing on its own: codec fidelity at the surface a human or another tool actually uses.
conformance-harness.md specifies a second, complementary conformance track for exactly that: a CLI wrapper contract every implementation exposes one command per operation under, a directory-per-fixture format for OML/OSD input and expected output, and a structural-equality comparison algorithm (not text diffing) for judging whether an implementation's actual output matches. It reuses this chapter's operation vocabulary (§8.5.3) and diagnostic envelope (§8.2) rather than defining either a second time.
This track's fixtures and orchestrator live in this repository, under
conformance/, not in a separate repository — alongside the spec text they
pin, for the same reason test-suite/ does: chapter 10's Spec-TDD workflow
(§10.2) requires a vector and the prose it tests to land in the same PR, and
that is only practical when both live in one place.