The Synapse Conformance Suite
Fifteen checks, A through O, over one adapter. Six of them are the harness's — the same six
cdm-harness has printed since 1.0.0 — and nine were added beside them. Every check returns
PASS, FAIL or SKIP, and a SKIP is never a PASS: it means this was not assessed, and
the report says which declaration made it inapplicable.
pip install synapse-cdm
synapse conformance run --adapter cat021
synapse conformance run --adapter cat021 --format json
synapse conformance run --adapter cat021 --require A,B,C,D,F,G,H,J,K,L
synapse conformance list
python -m synapse_cdm.suite conformance run … is the same entry point, for a caller who would
rather not depend on a name being on PATH.
The fifteen
| check | what it asserts | when it is SKIP | |
|---|---|---|---|
| A | translate | every fixture decodes into CDM objects | never |
| B | schema | every object validates against the published JSON Schema | never |
| C | provenance | every object carries the source, adapter and identifiers §4.5 requires | never |
| D | lossless | with MAPPINGS declared, no declared source leaf is LOST under the path-bound preservation ledger (details.basis: ledger); without them, no scalar in the source payload is absent from the output and undeclared (details.basis: heuristic, which is not proof) | a bytes fixture has no leaf structure to compare |
| E | roundtrip | source → CDM → source reproduces the source under the tolerance the adapter declares: octet for octet (bytes, the default), or no value lost after re-ingest (values, for XML) | the adapter is ingest-only |
| F | golden | the output is byte-identical to the committed golden file | no golden file exists yet |
| G | deterministic | three decodes of one payload under one frozen clock hash equal | never |
| H | malformed-input | every payload under malformed/ is refused, safely, in time — each case parsed in a spawned worker process under a real deadline, and every outcome but PARSER_REJECTED (PARSER_ACCEPTED, PARSER_TIMEOUT, PARSER_CRASH, WORKER_INIT_FAILED, HARNESS_ERROR) FAILS | the adapter declares no malformed/ set |
| I | unknown-preservation | an injected unknown field survives into the output | the adapter declares unknown_fields: none |
| J | temporal | timestamps are RFC 3339 UTC, are not the epoch, and a time taken from now() says so | the adapter emits no timestamp — a DECLARED inapplicability when the adapter carries a structured limitation with id no-source-time (its format states no instant; geojson is the first), an undeclared one otherwise |
| K | identity | identifiers are equal across two decodes and are uuid5, not drawn | the adapter emits no identifier |
| L | version | every object's schema_version is assessed against this package's by version.assess() — the verdict, direction and basis are reported, a malformed version is a finding and not a crash — and is inside the manifest's cdm.supported | the adapter emits no object |
| M | streaming | a payload fed in two chunks produces the output the whole one does; the SKIP carries details.streaming, the four statuses (chunk framing, partial messages, reassembly, backpressure) that say what is not implemented and what is not applicable | no adapter declares the capability today |
| N | parser robustness | a payload truncated at every offset raises rather than crashes — in the same spawned worker as H, with the same outcome codes; a deadline that fires is PARSER_TIMEOUT, never a duration | the adapter ships no byte fixture |
| O | resource limits | one byte over max_input_bytes is refused before decode; every shipped manifest declares the bound with its basis in declared_because | no bound is declared (the manifest's absent_because says why) |
G hashes the serialisation this framework fixed once: json.dumps(obj, sort_keys=True, indent=2), UTF-8, trailing newline, sha256 over those bytes. N truncates at every offset for a
fixture under 4 KiB and at 4096 evenly spaced offsets above it; the cap is printed in the report.
What a SKIP costs, and what it does not
A maturity level is a claim about evidence, so the suite computes eligibility from the results rather than reading a declaration:
L1needsA;L2needsA B;L3needsA B C;L4needsA B C E;L5needs every check applicable to this adapter.- A required check that is
PASSsatisfies its requirement. - A required check that is
SKIPfrom a declared inapplicability does not block the rung. An ingest-only adapter is not blocked by the round-trip check: there is no egress direction for information to be lost in. - A required check that is
FAIL, orSKIPwith no declared inapplicability, blocks that rung and every rung above it. L6is never computed from this repository's own evidence: every fixture here is synthetic.
The two are printed separately on purpose. synapse conformance list shows what each adapter
declares; a run prints MATURITY ELIGIBLE, which is what this repository's evidence supports
today. They are allowed to differ — a declared rung above the eligible one is a defect, a
declared rung below it is a reading — and where they do, the manifest's maturity.basis says what
the difference rests on. Every adapter here computes L5 and declares less: the three ingest-only
adapters declare L3 because E is inapplicable to them rather than passed, and the eleven that
emit declare L4 because L5 rests on M being inapplicable to every adapter in this repository.
A rung passed vacuously is not a rung declared.
E is computed for the eleven emitters, not taken on trust. Until 2026-09-16 the suite could
not read non-JSON egress, reported SKIP, and pointed at a round-trip test in tests/ that a
wheel does not carry — so every emitter declared L4 where the suite computed L3. Now the
harness makes the comparison itself under the tolerance the class declares in
ROUNDTRIP_TOLERANCE: bytes (nine of the eleven, from_cdm(to_cdm(raw)) equals the fixture octet
for octet, AIS's NMEA TAG block excepted by a printed roundtrip_reference) or values (TAK and
STANAG 4676, whose XML cannot promise octet order, so what was emitted is re-ingested and no source
value may be missing, with the two values NITS egress re-stamps declared in ROUNDTRIP_TRANSFORMS).
Each fixture pair carries the verdict on one half — the byte fixture under bytes, the parsed twin
under values — and the other half reports SKIP with a sentence saying so, exactly as D skips
the byte fixture and relies on the twin.
Strict mode
--require names the letters a caller depends on. If any of them is FAIL or SKIP the
command exits non-zero — a check that could not be evaluated is not a check that passed. The
verdicts in the report are never rewritten to match: a reader has to be able to tell an adapter
that failed a check from one nobody could evaluate.
| exit | meaning |
|---|---|
0 | the run completed and no required check failed or was skipped |
1 | a required check FAILed, or a required check came back SKIP |
2 | the invocation was wrong — an unknown adapter, or no fixtures where one was named |
Malformed fixtures
Every adapter ships at least two payloads under <fixtures>/<adapter>/malformed/: a truncation
and one format-specific case — an invalid declared length, a checksum that does not match, XML
that is not well formed, JSON that is not JSON. They are a subdirectory, so checks A–F never
see them: the harness selects immediate children that are files.
A payload must be refused, and refused safely: any exception except SystemExit,
KeyboardInterrupt, MemoryError or RecursionError, inside the time bound, with no object
returned. The exception class is recorded in the report. A declared error hierarchy — so that a
caller can catch one class rather than Exception — is Part 2 of this specification and is a
stated limitation until then.