Conformance matrix¶
This page tracks how each PROV-DM concept maps onto prov’s classes and factory methods, and
how well each serializer round-trips it. It is the audit artefact for Phase 3.5 of the
modernisation roadmap (roadmap step
28) and is revisited at every release as behaviour changes — last revised for the 3.0.0
release (2026-07-26). A JSON-LD column is planned for 3.1.0 once the PROV-JSONLD serializer
lands. For prose background on PROV-DM’s six components and
how they group in prov.model, see The PROV Data Model; this page is the detailed,
verified-against-source reference underneath that explanation.
Every cell below was checked directly against the current source: model classes against
src/prov/model/records.py, factory methods and camelCase aliases against
src/prov/model/bundle.py, PROV-N keywords against PROV_N_MAP / ADDITIONAL_N_MAP in
src/prov/constants.py, and round-trip status against the shared test matrix
(src/prov/tests/conftest.py::SHARED_TARGETS, test_statements.py, test_attributes.py).
Round-trip column key¶
JSON / XML / RDF — whether the type round-trips (
deserialize(serialize(doc)) == doc) under the sharedfmttest matrix (SHARED_TARGETS = ("model", "json", "xml", "rdf")inconftest.py). ✓ means every shared case for that concept passes cleanly; a caveat cites the tracking issue and says what still fails.PROV-N — PROV-N is output-only:
provhas no PROV-N parser (issue #122, planned for 3.2.0), so there is no PROV-N round trip to test, only the keywordget_provn()emits.
More caveats apply across many rows rather than to one:
PROV-O representational limitation (RDF, permanent — closed as #217): 14 statement-level test cases assert two relations that share one identifier but differ only in
prov:time; PROV-O has no conformant way to encode that. PROV-O reifies a relation as a single qualified node (e.g.prov:qualifiedGeneration) named directly by the relation’s own identifier — one identifier is one RDF node — so a second relation asserting the same identifier can only add more triples to that one node, not create a second, distinguishable node; bothprov:atTimevalues end up on the sameprov:qualifiedGenerationnode with no way to tell which value belongs to which asserted relation. There is no encoding that avoids this without either minting a synthetic per-statement IRI (losing the asserted identifier ↔ node correspondence) or fabricating unasserted statements by permuting attribute values on decode — both rejected as options during the 3.0 audit. Accordingly this is documented as a permanent limitation of therdftarget, not an open bug: the 14 test cases stay skipped forrdf(RDF_SCRUFFY_SKIPintest_statements.py), and decoding third-party RDF with this shape raisesprov.model.ProvExceptionnaming the limitation and pointing back at this page. The historical Java reference implementation (ProvToolbox) collapses identically on encode — its own “scruffy” test fixtures produce one qualified node carrying both values as repeated triples, with no IRI minting — and dropped RDF support entirely in its 2.x line rather than keep the permutation-based decoder it once had. Only this same-identifier/differing-attribute construct is affected:generation/usage/start/end/invalidationround-trip cleanly in the general case, JSON/XML/PROV-N and the in-memory model are unaffected (the limitation is specific to the PROV-O encoding, not toprov’s object model), and plain serialization of such documents remains legal in 3.0 (provnever enforces structural constraints at assertion time, #257). Onlyunified()detects records that share an identifier but hold conflicting formal attributes, as part of the separate PROV-CONSTRAINTS rework described in Upgrading to 3.0.XML attribute-name escaping (XML, permanent convention — closed as #289): an attribute name is written as a PROV-XML child element tag, but its local part is not guaranteed to be a legal XML NCName (prov never enforces structural constraints at assertion time, #257) — it may start with a digit or contain characters such as
' ( ) , : ; [ ] =. Rather than raising, the serializer escapes each NCName-illegal character using the_xHHHH_convention (the same one used by the OpenXML/SQL Server ecosystems for this exact problem:_xfollowed by 4 uppercase hex digits — 8 for codepoints beyond the Basic Multilingual Plane — then_), and the deserializer applies the inverse, so such names round-trip losslessly. A literal run that already looks like an escape sequence has its introducing_self-escaped as_x005F_, so the transform is always exactly invertible, including for prov’s own output. Names that are already legal NCNames (including non-ASCII letters, which are legal NCName characters) are emitted unchanged — this is not a behaviour change for existing users. The one caveat: a third-party XML document containing a literal_xHHHH_-shaped attribute name will be unescaped on read, since the convention cannot distinguish an intentional escape sequence from one that merely looks like one.PROV-O attribute-key metacharacter/namespace defect (RDF, open — tracked as #341, a sibling of the #217 limitation above but not a decided/closed matter — it is a known defect, unfixed in 3.0): a qualified name used as an attribute key whose local part ends in one of the seven characters
=',:;[]can fail to survive the PROV-O round trip. An IRI ending in one of these characters makes rdflib’scompute_qname()raise, since nothing follows the character to serve as a local part; decoding an identifier tolerates this via a fallback split inProvRDFSerializer._resolve_iri, but decoding an attribute key has no equivalent fallback — it can only resolve such an IRI if that key’s namespace is already registered by the time the key is decoded, which happens only when some other qualified name under the same namespace has already been decoded from an IRI that splits cleanly. A qualified name whose own local part carries the character mid-string (e.g.http://example.org/e:0) does not help register the namespace —compute_qname()silently mis-splits it into an over-narrow namespace (http://example.org/e:) instead of raising — but it does no active harm either: a clean sibling elsewhere in the same namespace still rescues the key regardless of whether a mid-string occurrence is also present. So the failure condition is registration-based: an attribute key whose local part ends in one of these seven characters, in a namespace that no cleanly-splitting qualified name has registered by the time the key is decoded — a mid-string occurrence of the character elsewhere is neither necessary nor sufficient for the failure. The unguarded call lives inProvRecord.add_attributes(src/prov/model/records.py), reached while emitting decoded records inProvRDFSerializer._emit_decoded_records(src/prov/serializers/provrdf.py), with the raise itself coming frommandatory_valid_qname(src/prov/model/bundle.py). Of the nine #223 PROV-N metacharacters, only(and)are unaffected in every position. Qualified names used as attribute values are unaffected. As with #217, PROV-N escapes these characters and PROV-JSON/PROV-XML both round-trip them — PROV-O is the odd one out. #341’s title names only the colon instance; the surface described here is broader. The round-trip property test excludes attribute keys ending in one of these seven characters at generation time (src/prov/tests/strategies.py) so it does not mask other findings — slightly more conservative than the true rule, since such a key is actually safe whenever its namespace happens to already be registered, but the generator cannot guarantee that in general. Unlike #217 and #248 above, this is not a maintainer decision to leave as a permanent limitation; it remains open for a future fix.
The value-typing and literal-semantics gaps the audit recorded here — #77, #89, #168, #218,
#223, #225, #235, #238, #244, #246, #249, #251, #256, #259 — were fixed in 3.0; see
Upgrading to 3.0 and HISTORY.rst for the details.
Component 1 — Entities and Activities¶
Concept (PROV-DM §) |
Model class |
Factory / alias |
PROV-N keyword |
JSON |
XML |
RDF |
|---|---|---|---|---|---|---|
Entity §5.1.1 |
|
|
✓ |
✓ |
✓ |
|
Activity §5.1.2 |
|
|
✓ |
✓ |
✓ |
|
Generation §5.1.3 |
|
|
✓ |
✓ |
✓ (permanent PROV-O representational limitation for the 2 same-id/differing-time cases — see above) |
|
Usage §5.1.4 |
|
|
✓ |
✓ |
✓ (permanent PROV-O representational limitation for the 2 same-id/differing-time cases — see above) |
|
Communication §5.1.5 |
|
|
✓ |
✓ |
✓ |
|
Start §5.1.6 |
|
|
✓ |
✓ |
✓ (permanent PROV-O representational limitation for the 4 same-id/differing-time cases — see above) |
|
End §5.1.7 |
|
|
✓ |
✓ |
✓ (permanent PROV-O representational limitation for the 4 same-id/differing-time cases — see above) |
|
Invalidation §5.1.8 |
|
|
✓ |
✓ |
✓ (permanent PROV-O representational limitation for the 2 same-id/differing-time cases — see above) |
ProvEntity additionally exposes wasGeneratedBy()/wasInvalidatedBy()
and ProvActivity exposes
used()/wasInformedBy()/wasStartedBy()/wasEndedBy() as self-as-subject chaining
methods (records.py) — the table above lists the ProvBundle factories, which every relation
also has.
Component 2 — Derivations¶
Concept (PROV-DM §) |
Model class |
Factory / alias |
PROV-N keyword |
JSON |
XML |
RDF |
|---|---|---|---|---|---|---|
Derivation §5.2.1 |
|
|
✓ |
✓ |
✓ |
|
Revision §5.2.2 |
|
|
|
✓ |
✓ |
✓ |
Quotation §5.2.3 |
|
|
|
✓ |
✓ |
✓ |
Primary Source §5.2.4 |
|
|
|
✓ |
✓ |
✓ |
Revision, quotation, and primary source are PROV-DM subtypes of derivation, not separate PROV-N
records: prov implements all four with the single ProvDerivation class,
and the three subtype factories call derivation() then add the corresponding prov:type
(confirmed by inspection of ProvBundle.derivation, .revision, .quotation, and
.primary_source in bundle.py, and by running get_provn() on a revision()
record — it emits wasDerivedFrom(..., [prov:type='prov:Revision']), not a wasRevisionOf(...)
keyword). ADDITIONAL_N_MAP does carry a wasRevisionOf/wasQuotedFrom/hadPrimarySource
keyword mapping for contexts (such as PROV-XML) that treat these as top-level types; PROV-N
output from this library always uses the base wasDerivedFrom form.
Component 3 — Agents, Responsibility, and Influence¶
Concept (PROV-DM §) |
Model class |
Factory / alias |
PROV-N keyword |
JSON |
XML |
RDF |
|---|---|---|---|---|---|---|
Agent §5.3.1 |
|
|
✓ |
✓ |
✓ |
|
Person / Organization / SoftwareAgent §5.3.1 |
via |
none — see finding below |
|
✓ |
✓ |
✓ |
Attribution §5.3.2 |
|
|
✓ |
✓ |
✓ |
|
Association §5.3.3 |
|
|
✓ |
✓ |
✓ |
|
Plan §5.3.3 |
via |
— |
— (plan is an ordinary entity referenced by the association’s |
✓ |
✓ |
✓ |
Delegation §5.3.4 |
|
|
✓ |
✓ |
✓ |
|
Influence §5.3.5 |
|
|
✓ |
✓ |
✓ |
Finding: PROV-DM defines Person, Organization, and SoftwareAgent as agent subtypes, and Plan
as an entity subtype used with associations. prov has no dedicated classes or factories for the
agent subtypes — you express them with agent("ag", {PROV_TYPE: PROV["Person"]}) — while Plan
needs no special handling at all, since it is just an entity passed as the plan= argument to
association(). This is a documented, intentional design choice
(docs/explanation/prov-dm.md:111-117), not a defect; see finding log for the audit note.
Convenience factories for the three agent subtypes (together with EmptyCollection, see
Component 6) are now tracked as
#260.
Component 4 — Bundles¶
Concept (PROV-DM §) |
Model class |
Factory / alias |
PROV-N keyword |
JSON |
XML |
RDF |
|---|---|---|---|---|---|---|
Bundle constructor §5.4.1 |
|
|
✓ |
✓ |
✓ |
|
Bundle type §5.4.2 |
not implemented — see finding below |
— |
— |
— |
— |
— |
Finding: PROV-DM §5.4.1 defines bundle containment — a named, nestable set of records —
which prov fully implements via ProvDocument.bundle()/add_bundle(); only a
ProvDocument may contain named bundles (is_document()/is_bundle()
in bundle.py distinguish the two at runtime). §5.4.2 additionally lets a bundle’s identifier
denote a first-class entity of type prov:Bundle, so that provenance-of-provenance (e.g. “who
asserted this bundle”) can itself be expressed in PROV. That second half is not implemented:
the PROV_BUNDLE constant and its PROV_N_MAP["bundle"] keyword exist in constants.py but
are consumed only by dot.py (for node styling) — no serializer or
ProvBundle method ever produces a prov:Bundle-typed entity, and
get_provn()’s bundle <id> ... endBundle output is generated structurally (branching on
is_document()), not through that keyword lookup. There is currently no supported way to
attribute a bundle to an agent as a first-class PROV statement. Tracked as
#261.
Component 5 — Alternate Entities¶
Concept (PROV-DM §) |
Model class |
Factory / alias |
PROV-N keyword |
JSON |
XML |
RDF |
|---|---|---|---|---|---|---|
Specialization §5.5.1 |
|
|
✓ |
✓ |
✓ |
|
Alternate §5.5.2 |
|
|
✓ |
✓ |
✓ (the RDF triple follows the PROV-DM argument order — |
|
Mention (PROV-LINKS) |
|
|
|
✓ |
✓ |
✓ |
Component 6 — Collections¶
Concept (PROV-DM §) |
Model class |
Factory / alias |
PROV-N keyword |
JSON |
XML |
RDF |
|---|---|---|---|---|---|---|
Collection §5.6 |
|
|
|
✓ |
✓ |
✓ |
EmptyCollection §5.6 |
|
none — see finding below |
|
✓ |
✓ |
✓ |
Membership §5.6 |
|
|
✓ |
✓ |
✓ |
Finding: like collections, EmptyCollection is a real PROV-DM type with a real
ADDITIONAL_N_MAP/PROV_BASE_CLS entry in constants.py, so the round-trip machinery
understands it — but there is no empty_collection() factory or empty= flag on collection()
to set the type for you; you would add prov:type: PROV["EmptyCollection"] by hand via
other_attributes. Tracked (together with the agent-subtype factories, see Component 3) as
#260.
Additional attributes¶
Five PROV-DM attributes are usable on (almost) any record and are exercised directly by the
shared attribute test matrix (test_attributes.py, ATTRIBUTE_VALUES in
attribute_values.py) and by test_statements.py’s add_label/add_locations/add_types/
add_value helpers:
Attribute |
Constant ( |
Round-trip notes |
|---|---|---|
|
|
✓ JSON/XML/RDF, including language-tagged literals and multiple values on one record. |
|
|
✓ JSON/XML/RDF across the full |
|
|
✓ JSON/XML/RDF; used throughout the qualified-relation tests (association, usage, generation, …). |
|
|
✓ JSON/XML/RDF, including mixed multi-datatype attribute sets on one record ( |
|
|
✓ JSON/XML/RDF. |
Maintenance¶
This matrix reflects the codebase as of the Phase 3.5 conformance audit (roadmap steps 28–32,
completed 2026-07-11), refreshed for the 3.0 value-typing and literal-semantics conformance
fixes, and should be revisited at each release as serializers change or issues close. Beyond
the per-format round trips above, the audit’s gap analysis found that ProvBundle.unified()
performed an identifier-keyed attribute union rather than
PROV-CONSTRAINTS merging — tracked as the umbrella
issue #253; 3.0 lands that rework (see
Unification and flattening for the full write-up and
Upgrading to 3.0 for what changed). Release 3.1.0 adds a
PROV-JSONLD serializer; when that lands, this page gains a JSON-LD column
alongside JSON/XML/RDF. See The PROV Data Model for the conceptual background behind
each component, and prov.model for the full class/method API reference.