Two changes. If you serialize only standard RDF, neither alters your output.
Fixed: the rdflib fallback no longer drops graph.base
A document that holds relative references and declares no base is not self-describing. RFC 3986 section 5.1.3 hands resolution to the retrieval URI, so the same bytes read from two directories produced two different graphs, and section 5.1.4 places that responsibility on the sender.
The drop had a real cause: rdflib's Serializer.relativize shortens an IRI by string prefix rather than by the component algorithm RFC 3986 section 5.2.2 defines and Turtle section 6.3 requires, so under a base ending in #, in ?, or mid-path-segment it emits a reference that resolves back to a different IRI. But the defence was too broad -- it also discarded path-segment and authority-only bases, which are the ones ordinary tooling emits.
RFC 3986 specifies resolution and never its inverse, so no static test can decide this. The rendering is now re-read and the base kept only if every absolute IRI of the source survives. Only loss counts: a relative source term is outside the RDF abstract syntax (RDF 1.1 Concepts section 3.2) and always resolves to something on re-reading. Each drop logs a warning naming the base and an IRI that forced it.
A base that is not itself a valid absolute IRI is never declared, since Turtle section 6.5 IRIREF admits no space, brace or quote.
Output bytes change for graphs that take the fallback path and carry a base. Regenerate the affected artifact once; subsequent runs are stable.
Added: canonicalize_rdf_graph(..., diff_stable=True)
The same Weisfeiler-Leman blank-node labelling wl_relabel_quads exposes, without having to build a pipeline around it. RDFC-1.0 labels are a function of the whole graph, so a one-triple edit can renumber every blank node in a document; WL labels depend only on a node's neighbourhood. Measured on a six-branch graph, adding one branch changes 6 lines under RDFC-1.0 and 2 under WL.
Opt-in, deterministic either way, and isomorphic to the input in both cases. The fallback path cannot relabel -- WL consumes pyoxigraph quads that path never produces -- so it warns rather than passing silently.
Full detail in CHANGELOG.md and #60.