Skip to content

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.
  • NaN or an infinity has no legal spelling in the target format (e.g. JSON, whose only sentinel for this is null) — format.float-special. Confirmed live: writing a genuine null value and writing NaN produce the identical JSON token, and both read back as the identical Document value — there is no way, after the fact, to tell a substituted NaN from an original null.
  • 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 &#13;, 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 &#13; (and &#13;\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, and schema.invalid-name MUST use a Document path, rooted at the node the violation occurs on — $.record[0] for a record node missing its name, $.record[1].field[2].type.kind for an invalid kind value.
  • 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 are algebra.* 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": { }
}
  • name MUST be unique across the whole suite. A harness reports results keyed on it.
  • spec points 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.
  • operation selects 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.
  • purpose MUST be one of happy-path, edge-case, error-case, or determinism-regression — what a vector is actually pinning, so a reader doesn't have to reverse-engineer it from the input/expect pair. happy-path is an ordinary conforming case with nothing specific being probed; edge-case deliberately exercises a specific rule, invariant, or boundary (regardless of whether the outcome is success or failure); error-case is primarily testing that an invalid input is correctly rejected; determinism-regression pins a specific ordering/reproducibility regression tied to a known bug.
  • expect holds either a success value or a diagnostics list.

8.5.2 Diagnostics matching

"expect": {
  "ok": false,
  "diagnostics": [
    { "path": "$.port", "code": "validate.type-mismatch" }
  ]
}

Matching rules, all normative:

  1. Message text MUST NOT be compared.
  2. 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.
  3. 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.
  4. A harness MAY be run in code-agnostic mode, comparing only ok and 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}} where K is one of the seven kinds. Temporal values are ISO-8601 strings. For integer values see the threshold rule below.
  • null is {"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 pass once 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.