Skip to content
Back to top
View this page

# 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; 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 shared `fmt` test matrix (`SHARED_TARGETS = ("model", "json", "xml", "rdf")` in `conftest.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: `prov` has no PROV-N parser (issue #122, planned for 3.2.0), so there is no PROV-N round trip to test, only the keyword `get_provn()` emits. Every PROV-N cell below is additionally subject to issue #223 (`QualifiedName.provn_representation()` does not escape PROV-N metacharacters — `'`, `)`, `,`, `(`, `:`, `;`, `[`, `]`, `=` — in an identifier’s local part), which is a property of identifiers, not of any one record type; it is noted once here rather than on every row.

More caveats apply across many rows rather than to one:

- #217 (RDF): 14 statement-level test cases assert two relations that share one identifier but differ only in `prov:time`; PROV-O has no way to represent that (both times serialize onto the same qualified IRI), so those specific test cases are skipped for the `rdf` target. This is a limitation of same-identifier/differing-time relations, not of the relation types themselves — `generation`/`usage`/`start`/`end`/`invalidation` round-trip cleanly in the general case.

- #224 (XML): the PROV-XML serializer silently drops any `other_attributes` entry whose value is the empty string `""`, regardless of record type.

- #225 (RDF): a Python `float` attribute (typed `xsd:float`) can lose precision through RDF, regardless of record type, because the RDF serializer canonicalises `xsd:float` to a short decimal form.

- #235 (all formats): a `Literal` explicitly typed `xsd:long` is silently collapsed to a plain `int` at assertion time, so every serializer emits it as `xsd:int` — the asserted datatype is lost before serialization.

- #244 (XML) / #249 (PROV-N) / #256 (RDF): plain Python `int` values are always typed `xsd:int` with no magnitude check, so a value outside the 32-bit range produces schema-invalid XML, a value-invalid PROV-N integer literal, and an ill-typed RDF literal, respectively.

- #246 (JSON): plain `int`/`float` attribute values are encoded with a non-string `$` property, violating the PROV-JSON submission’s typed-literal schema (the output still round-trips through `prov` itself).

- #251 (PROV-N): plain Python `float` values are emitted as `%g`-formatted `xsd:float` — a different datatype (and, beyond 6 significant digits, a different value) than the `xsd:double` the JSON/XML/RDF serializers assert for the same document.

- #238 (JSON): `prov:QUALIFIED_NAME`-typed `Literal`s are stored opaquely by the model but resolved to `QualifiedName`s by the JSON decoder, so such a value mutates across a JSON round trip and breaks document equality.

- #237 (model): every factory `time=`/`startTime=`/`endTime=` parameter leaks a raw `dateutil` `ParserError` on unparseable strings (instead of `ProvException`) and rejects the valid `xsd:dateTime` hour-24 lexical form.

- #259 (model): `Literal` language tags compare case-sensitively, diverging from RDF 1.1’s case-insensitive language-tag value space (tags are never normalised, so values do survive round trips unchanged).

## Component 1 — Entities and Activities

Concept (PROV-DM §) | Model class | Factory / alias | PROV-N keyword | JSON | XML | RDF
--- | --- | --- | --- | --- | --- | ---
Entity §5.1.1 | `ProvEntity` | `entity()` | `entity` | ✓ | ✓ | ✓
Activity §5.1.2 | `ProvActivity` | `activity()` | `activity` | ✓ | ✓ | ✓
Generation §5.1.3 | `ProvGeneration` | `generation()` / `wasGeneratedBy()` | `wasGeneratedBy` | ✓ | ✓ | ✓ (#217 for the 2 same-id/differing-time cases)
Usage §5.1.4 | `ProvUsage` | `usage()` / `used()` | `used` | ✓ | ✓ | ✓ (#217 for the 2 same-id/differing-time cases)
Communication §5.1.5 | `ProvCommunication` | `communication()` / `wasInformedBy()` | `wasInformedBy` | ✓ | ✓ | ✓ (anonymous qualified communications omit `prov:activity`: #250)
Start §5.1.6 | `ProvStart` | `start()` / `wasStartedBy()` | `wasStartedBy` | ✓ | ✓ | ✓ (#217 for the 4 same-id/differing-time cases)
End §5.1.7 | `ProvEnd` | `end()` / `wasEndedBy()` | `wasEndedBy` | ✓ | ✓ | ✓ (#217 for the 4 same-id/differing-time cases)
Invalidation §5.1.8 | `ProvInvalidation` | `invalidation()` / `wasInvalidatedBy()` | `wasInvalidatedBy` | ✓ | ✓ | ✓ (#217 for the 2 same-id/differing-time cases)

`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 | `ProvDerivation` | `derivation()` / `wasDerivedFrom()` | `wasDerivedFrom` | ✓ | ✓ | ✓
Revision §5.2.2 | `ProvDerivation` + `prov:Revision` type | `revision()` / `wasRevisionOf()` | `wasDerivedFrom` (plus `[prov:type='prov:Revision']`) | ✓ | ✓ | ✓
Quotation §5.2.3 | `ProvDerivation` + `prov:Quotation` type | `quotation()` / `wasQuotedFrom()` | `wasDerivedFrom` (plus `[prov:type='prov:Quotation']`) | ✓ | ✓ | ✓
Primary Source §5.2.4 | `ProvDerivation` + `prov:PrimarySource` type | `primary_source()` / `hadPrimarySource()` | `wasDerivedFrom` (plus `[prov:type='prov:PrimarySource']`) | ✓ | ✓ | ✓

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 `bundle.py:1011-1146`, 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 | `ProvAgent` | `agent()` | `agent` | ✓ | ✓ | ✓
Person / Organization / SoftwareAgent §5.3.1 | via `prov:type` on `ProvAgent` | none — see finding below | `person` / `organization` / `softwareAgent` (`ADDITIONAL_N_MAP`, not emitted directly by this library) | ✓ | ✓ | ✓
Attribution §5.3.2 | `ProvAttribution` | `attribution()` / `wasAttributedTo()` | `wasAttributedTo` | ✓ | ✓ | ✓ (anonymous qualified attributions omit `prov:agent`: #250)
Association §5.3.3 | `ProvAssociation` | `association()` / `wasAssociatedWith()` | `wasAssociatedWith` | ✓ | ✓ | ✓
Plan §5.3.3 | via `association(plan=...)` | — | — (plan is an ordinary entity referenced by the association’s `plan` formal attribute) | ✓ | ✓ | ✓
Delegation §5.3.4 | `ProvDelegation` | `delegation()` / `actedOnBehalfOf()` | `actedOnBehalfOf` | ✓ | ✓ | ✓ (anonymous qualified delegations sharing (delegate, activity): #226; the encode-side root cause — the qualified node omits `prov:agent` — is #250)
Influence §5.3.5 | `ProvInfluence` | `influence()` / `wasInfluencedBy()` | `wasInfluencedBy` | ✓ | ✓ | ✓ (anonymous qualified influences omit `prov:influencer`: #250)

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 | `ProvBundle` | `ProvDocument.bundle()` / `add_bundle()` | `bundle <id> ... endBundle` (structural, hand-emitted by `get_provn()`) | ✓ | ✓ | ✓
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 | `ProvSpecialization` | `specialization()` / `specializationOf()` | `specializationOf` | ✓ | ✓ | ✓
Alternate §5.5.2 | `ProvAlternate` | `alternate()` / `alternateOf()` | `alternateOf` | ✓ | ✓ | ✓ (the RDF triple is emitted with subject/object transposed relative to the PROV-DM argument order — symmetric-relation-safe, but third-party consumers see the arguments swapped: #258)
Mention (PROV-LINKS) | `ProvMention` (subclass of `ProvSpecialization`) | `mention()` / `mentionOf()` | `mentionOf` (emitted without the `prov:` prefix the PROV-Links grammar requires: #248) | ✓ | ✓ | ✓

## Component 6 — Collections

Concept (PROV-DM §) | Model class | Factory / alias | PROV-N keyword | JSON | XML | RDF
--- | --- | --- | --- | --- | --- | ---
Collection §5.6 | `ProvEntity` + `prov:Collection` type | `collection()` | `entity` (plus `[prov:type='prov:Collection']`) | ✓ | ✓ | ✓
EmptyCollection §5.6 | `ProvEntity` + `prov:EmptyCollection` type — no dedicated factory | none — see finding below | `entity` (plus `[prov:type='prov:EmptyCollection']`, keyword `emptyCollection` in `ADDITIONAL_N_MAP`, not emitted directly) | ✓ | ✓ | ✓
Membership §5.6 | `ProvMembership` | `membership()` / `hadMember()` | `hadMember` | ✓ | ✓ | ✓

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 (`prov.constants`) | Round-trip notes
--- | --- | ---
`prov:label` | `PROV_LABEL` | ✓ JSON/XML/RDF, including language-tagged literals and multiple values on one record.
`prov:location` | `PROV_LOCATION` | ✓ JSON/XML/RDF across the full `ATTRIBUTE_VALUES` datatype corpus.
`prov:role` | `PROV_ROLE` | ✓ JSON/XML/RDF; used throughout the qualified-relation tests (association, usage, generation, …).
`prov:type` | `PROV_TYPE` | ✓ JSON/XML; RDF is clean for a single value but see #77 (`xsd:decimal` becomes `10.0`) and #218 (mixed multi-datatype attribute sets on one record lose fidelity) — both are strict `xfail`s in `test_attributes.py`, not silent failures.
`prov:value` | `PROV_VALUE` | ✓ JSON/XML/RDF.

Any attribute value (regardless of which of the five above it is) is additionally subject to #224 (XML drops the empty string `""`) and #225 (RDF can lose `xsd:float` precision) — both are properties of the value’s type, not of the attribute name — as are the other value-level caveats listed under the round-trip column key above (#235, #238, #244/#249/#256, #246, #251, #259).

## Maintenance

This matrix reflects the codebase as of the Phase 3.5 conformance audit (roadmap steps 28–32, completed 2026-07-11) and should be revisited at each release as serializers change or issues close. Beyond the per-format round trips above, the audit also confirmed that `ProvBundle.unified()` performs an identifier-keyed attribute union rather than PROV-CONSTRAINTS merging — tracked as the umbrella issue #253, with the full gap analysis in the audit findings; the rework is scheduled for 3.0. 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.