# 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](https://github.com/trungdong/prov/blob/master/ROADMAP.md) (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 {doc}`../explanation/prov-dm`; 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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/224)** (XML): the PROV-XML serializer
  silently drops any `other_attributes` entry whose value is the empty string `""`, regardless
  of record type.
- **[#225](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/244)** (XML) /
  **[#249](https://github.com/trungdong/prov/issues/249)** (PROV-N) /
  **[#256](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/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 | {py:class}`~prov.model.ProvEntity` | `entity()` | `entity` | ✓ | ✓ | ✓ |
| Activity §5.1.2 | {py:class}`~prov.model.ProvActivity` | `activity()` | `activity` | ✓ | ✓ | ✓ |
| Generation §5.1.3 | {py:class}`~prov.model.ProvGeneration` | `generation()` / `wasGeneratedBy()` | `wasGeneratedBy` | ✓ | ✓ | ✓ ([#217](https://github.com/trungdong/prov/issues/217) for the 2 same-id/differing-time cases) |
| Usage §5.1.4 | {py:class}`~prov.model.ProvUsage` | `usage()` / `used()` | `used` | ✓ | ✓ | ✓ ([#217](https://github.com/trungdong/prov/issues/217) for the 2 same-id/differing-time cases) |
| Communication §5.1.5 | {py:class}`~prov.model.ProvCommunication` | `communication()` / `wasInformedBy()` | `wasInformedBy` | ✓ | ✓ | ✓ (anonymous *qualified* communications omit `prov:activity`: [#250](https://github.com/trungdong/prov/issues/250)) |
| Start §5.1.6 | {py:class}`~prov.model.ProvStart` | `start()` / `wasStartedBy()` | `wasStartedBy` | ✓ | ✓ | ✓ ([#217](https://github.com/trungdong/prov/issues/217) for the 4 same-id/differing-time cases) |
| End §5.1.7 | {py:class}`~prov.model.ProvEnd` | `end()` / `wasEndedBy()` | `wasEndedBy` | ✓ | ✓ | ✓ ([#217](https://github.com/trungdong/prov/issues/217) for the 4 same-id/differing-time cases) |
| Invalidation §5.1.8 | {py:class}`~prov.model.ProvInvalidation` | `invalidation()` / `wasInvalidatedBy()` | `wasInvalidatedBy` | ✓ | ✓ | ✓ ([#217](https://github.com/trungdong/prov/issues/217) for the 2 same-id/differing-time cases) |

{py:class}`~prov.model.ProvEntity` additionally exposes `wasGeneratedBy()`/`wasInvalidatedBy()`
and {py:class}`~prov.model.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 | {py:class}`~prov.model.ProvDerivation` | `derivation()` / `wasDerivedFrom()` | `wasDerivedFrom` | ✓ | ✓ | ✓ |
| Revision §5.2.2 | {py:class}`~prov.model.ProvDerivation` + `prov:Revision` type | `revision()` / `wasRevisionOf()` | `wasDerivedFrom` (plus `[prov:type='prov:Revision']`) | ✓ | ✓ | ✓ |
| Quotation §5.2.3 | {py:class}`~prov.model.ProvDerivation` + `prov:Quotation` type | `quotation()` / `wasQuotedFrom()` | `wasDerivedFrom` (plus `[prov:type='prov:Quotation']`) | ✓ | ✓ | ✓ |
| Primary Source §5.2.4 | {py:class}`~prov.model.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 {py:class}`~prov.model.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 | {py:class}`~prov.model.ProvAgent` | `agent()` | `agent` | ✓ | ✓ | ✓ |
| Person / Organization / SoftwareAgent §5.3.1 | via `prov:type` on {py:class}`~prov.model.ProvAgent` | none — see finding below | `person` / `organization` / `softwareAgent` (`ADDITIONAL_N_MAP`, not emitted directly by this library) | ✓ | ✓ | ✓ |
| Attribution §5.3.2 | {py:class}`~prov.model.ProvAttribution` | `attribution()` / `wasAttributedTo()` | `wasAttributedTo` | ✓ | ✓ | ✓ (anonymous *qualified* attributions omit `prov:agent`: [#250](https://github.com/trungdong/prov/issues/250)) |
| Association §5.3.3 | {py:class}`~prov.model.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 | {py:class}`~prov.model.ProvDelegation` | `delegation()` / `actedOnBehalfOf()` | `actedOnBehalfOf` | ✓ | ✓ | ✓ (anonymous qualified delegations sharing (delegate, activity): [#226](https://github.com/trungdong/prov/issues/226); the encode-side root cause — the qualified node omits `prov:agent` — is [#250](https://github.com/trungdong/prov/issues/250)) |
| Influence §5.3.5 | {py:class}`~prov.model.ProvInfluence` | `influence()` / `wasInfluencedBy()` | `wasInfluencedBy` | ✓ | ✓ | ✓ (anonymous *qualified* influences omit `prov:influencer`: [#250](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/260).

## Component 4 — Bundles

| Concept (PROV-DM §) | Model class | Factory / alias | PROV-N keyword | JSON | XML | RDF |
| --- | --- | --- | --- | --- | --- | --- |
| Bundle constructor §5.4.1 | {py:class}`~prov.model.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
{py:class}`~prov.model.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
{py:class}`~prov.model.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](https://github.com/trungdong/prov/issues/261).

## Component 5 — Alternate Entities

| Concept (PROV-DM §) | Model class | Factory / alias | PROV-N keyword | JSON | XML | RDF |
| --- | --- | --- | --- | --- | --- | --- |
| Specialization §5.5.1 | {py:class}`~prov.model.ProvSpecialization` | `specialization()` / `specializationOf()` | `specializationOf` | ✓ | ✓ | ✓ |
| Alternate §5.5.2 | {py:class}`~prov.model.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](https://github.com/trungdong/prov/issues/258)) |
| Mention (PROV-LINKS) | {py:class}`~prov.model.ProvMention` (subclass of {py:class}`~prov.model.ProvSpecialization`) | `mention()` / `mentionOf()` | `mentionOf` (emitted *without* the `prov:` prefix the PROV-Links grammar requires: [#248](https://github.com/trungdong/prov/issues/248)) | ✓ | ✓ | ✓ |

## Component 6 — Collections

| Concept (PROV-DM §) | Model class | Factory / alias | PROV-N keyword | JSON | XML | RDF |
| --- | --- | --- | --- | --- | --- | --- |
| Collection §5.6 | {py:class}`~prov.model.ProvEntity` + `prov:Collection` type | `collection()` | `entity` (plus `[prov:type='prov:Collection']`) | ✓ | ✓ | ✓ |
| EmptyCollection §5.6 | {py:class}`~prov.model.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 | {py:class}`~prov.model.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](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/77) (`xsd:decimal` becomes `10.0`) and [#218](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/224)** (XML drops the empty string `""`) and
**[#225](https://github.com/trungdong/prov/issues/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](https://github.com/trungdong/prov/issues/235),
[#238](https://github.com/trungdong/prov/issues/238),
[#244](https://github.com/trungdong/prov/issues/244)/[#249](https://github.com/trungdong/prov/issues/249)/[#256](https://github.com/trungdong/prov/issues/256),
[#246](https://github.com/trungdong/prov/issues/246),
[#251](https://github.com/trungdong/prov/issues/251),
[#259](https://github.com/trungdong/prov/issues/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](https://www.w3.org/TR/prov-constraints/) merging — tracked as the umbrella
issue [#253](https://github.com/trungdong/prov/issues/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 {doc}`../explanation/prov-dm` for the conceptual background behind
each component, and {doc}`model` for the full class/method API reference.
