From 3b6c30cddf9677221ab561548f78773d62428295 Mon Sep 17 00:00:00 2001 From: jdsika Date: Wed, 5 Aug 2026 09:45:45 +0200 Subject: [PATCH] docs: describe the round-trip guarantee and how it is tested The "how it works" list described idiomatic re-serialization as the final step and said all triples are preserved. Since 0.0.2 there is a fourth step - the output is re-parsed and required to be isomorphic to the input - and the preservation claim is now something the library enforces rather than something the reader has to trust. Worth documenting explicitly because the behaviour it guards against is invisible: Turtle's compact collection syntax cannot express a list whose tail is shared, so for those graphs the idiomatic form silently dropped or duplicated triples while still parsing cleanly (#1). A consumer reading the old README would have had no reason to suspect that, or to know that output is now occasionally more verbose than "idiomatic" implies, and why. Also adds the four properties the test suite asserts - lossless, idempotent, blank-node-label-independent, triple-order-independent - since correctness is what this library is for, and a reader deciding whether to depend on it should be able to see what is actually checked. Signed-off-by: jdsika --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index 1057dcb..0c48e36 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,31 @@ with a standards-based pipeline: 3. **Idiomatic rdflib re-serialization** — inline blank nodes (`[ … ]`), collection syntax (`( … )`), and filtered prefixes (only prefixes actually used are declared). +4. **Verified round-trip** — the rendered Turtle is re-parsed and required to be + isomorphic to the input before it is returned. All triples are preserved; only syntactic form changes. +### Why step 4 exists + +Turtle's compact collection syntax, `( … )`, can only express a list whose tail +is referenced once. Canonicalization readily produces graphs where several lists +share a tail — OWL ontologies do this routinely through `owl:unionOf`, +`owl:oneOf`, and the `sh:in` lists derived from them — and for those, the compact +form silently drops triples or restates a shared tail under a fresh blank node. +The output parses cleanly and looks plausible, which is what makes it dangerous +([#1](https://github.com/ASCS-eV/diffable-rdf/issues/1)). + +So `deterministic_turtle` checks its own work. When the compact form does not +round-trip, it falls back to stating list structure explicitly with +`rdf:first`/`rdf:rest`, which is always faithful. If neither form round-trips it +raises rather than returning a lossy result — a canonical form that silently +rewrites the graph is worse than none. + +In practice this means output is idiomatic for almost every graph, and slightly +more verbose for the ones where idiomatic would be wrong. Consumers do not need +to do anything: the guarantee is that what comes out says what went in. + ## Install ```bash @@ -64,6 +86,25 @@ well_known_prefix_map() # namespace IRI -> standard prefix na | `deterministic_json(obj, indent=3, preserve_list_order_keys=None) -> str` | Recursively sorted JSON; preserves JSON-LD ordered keys (`@context`, `@list`, …). | | `well_known_prefix_map() -> dict[str, str]` | rdflib's curated namespace→prefix bindings. | +## Guarantees, and how they are tested + +Four properties are asserted over seeded pseudo-random graphs and over hand-built +arrangements of shared collections, in `tests/test_canonicalization_properties.py`: + +| | Property | +|---|---| +| P1 | **Lossless** — the output parses back to a graph isomorphic to the input | +| P2 | **Idempotent** — canonicalizing the output reproduces it byte-for-byte | +| P3 | **Label-independent** — renaming blank nodes does not change the output | +| P4 | **Order-independent** — shuffling input triples does not change the output | + +Plus checks that no `sh:in`-style list reference dangles, that list cell counts +survive, and that ten repeated passes produce no byte drift. Comparison is done +under RDF 1.1 literal identity, so `"a"^^xsd:string` and `"a"` are treated as the +same term rather than as a spurious difference. + +The whole suite runs on Python 3.10 through 3.13. + ## Provenance Extracted from the diff-stabilization work in