# Phase 3.5 — Standards Conformance Audit Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Audit `prov` against W3C PROV-DM and its companion specs (PROV-N, PROV-O, PROV-JSON, PROV-XML, PROV-CONSTRAINTS), producing a published conformance matrix, new schema-validation tests, a unification gap analysis with a test corpus, and a maintainer-approved triage of every finding into 2.x / 3.0 / post-3.0 buckets.

**Architecture:** This is an audit phase, not a feature phase: deliverables are documentation, characterization/validation tests, filed GitHub issues, and triage decisions. All findings accumulate in one working findings doc (`docs/superpowers/specs/2026-07-10-conformance-audit-findings.md`, created in Task 1); the unification gap analysis gets its own spec doc because it is the authority for the 3.0 step-36b reimplementation. The 2.x freeze holds throughout: **no serializer or model behaviour changes** — defects found are filed as issues and locked with characterization tests or strict xfails, never fixed here.

**Tech Stack:** Python 3.10+, uv, pytest, lxml (`XMLSchema` validation), `jsonschema` (new dev dep, Task 4), ProvToolbox `provconvert` 2.0.4 (installed at `/usr/local/bin/provconvert`, used as an external PROV-N grammar oracle — never a CI dependency), local ProvToolbox checkout at `~/projects/ProvToolbox` (unification test corpus).

**User decisions (already made, from the approved roadmap spec and Phase 2/3 history):**
- Phase 3.5 covers roadmap steps 28–32 only; it cuts **no release** — findings feed the 3.0 triage.
- 2.x public-API/output freeze: behaviour- or output-changing fixes are deferred to Phase 4; only documentation and additive tests land here.
- The full PROV-CONSTRAINTS validation engine (#62: inferences, event ordering, typing/impossibility checks) is **out of scope** — features backlog. Only the unification/merging rules behind `unified()` are audited (steps 30b/36b).
- The conformance matrix is published as a public docs reference page (step 28).
- The unification gap analysis ships failing examples as test cases under `src/prov/tests/unification/` (step 30b).
- Process: one focused PR per task, sequential, branch from up-to-date master, green CI before merge, never AI attribution in commits/PRs.

---

## Reference material (all tasks)

| Spec | URL | Used by |
|---|---|---|
| PROV-DM | https://www.w3.org/TR/prov-dm/ | Tasks 1, 2 |
| PROV-N | https://www.w3.org/TR/prov-n/ | Task 5 |
| PROV-O | https://www.w3.org/TR/prov-o/ | Task 5 |
| PROV-XML | https://www.w3.org/TR/prov-xml/ (schema: https://www.w3.org/ns/prov.xsd) | Task 3 |
| PROV-JSON member submission | https://www.w3.org/submissions/prov-json/ | Task 4 |
| PROV-CONSTRAINTS | https://www.w3.org/TR/prov-constraints/ | Task 6 |

Read web pages with `defuddle parse <url> --md` (user preference). The local ProvToolbox checkout (`~/projects/ProvToolbox`) carries the W3C-derived unification corpus at `modules-validation/prov-validation/src/test/resources/validate/unification/` (307 files: ~150 cases as `.provn`+`.xml` pairs, named `*-successN` / `*-failN`).

Baseline commands (run before Task 1, record output in the findings doc):

```bash
uv run pytest                    # record collected/passed/skipped/xfailed tally
uv run ruff check src/           # expect: All checks passed!
uv run mypy src                  # expect: Success: no issues found in 17 source files
uv run --group docs --extra rdf --extra xml sphinx-build -b html docs docs/_build/html
                                 # expect: build succeeded (0 warnings baseline)
```

Issue-filing convention (Tasks 2–6): file findings as GitHub issues **without a milestone** (title prefixed by the affected spec, body cites the spec section, the audit doc section, and a minimal reproduction). Milestones are assigned only in Task 7 after the maintainer checkpoint. Pre-existing conformance issues in scope for triage: #34, #62, #77, #89, #96, #122, #124, #129, #130, #131, #154, #168, #217, #218, #223, #224, #225, #226, #228, plus the `read()`-cannot-autodetect-XML bug noted in the Phase 3 loose ends (unfiled — file it in Task 2 if still unfiled).

---

### Task 1: Conformance matrix reference page + findings scaffold (step 28)

**Goal:** Publish `docs/reference/conformance.md` — one row per PROV-DM type/relation across the six PROV-DM components, with columns for model class, `ProvBundle` factory method, PROV-N keyword output, and JSON/XML/RDF round-trip status — and create the working findings doc that Tasks 2–6 append to.

**Files:**
- Create: `docs/reference/conformance.md`
- Create: `docs/superpowers/specs/2026-07-10-conformance-audit-findings.md`
- Modify: `docs/reference/index.md` (add `conformance` to the toctree after `dot`)

**Acceptance Criteria:**
- [ ] `docs/reference/conformance.md` contains a table (or per-component tables) with a row for every concept listed in the inventory below — no concept omitted.
- [ ] Every cell claim is verified against the current code/tests, not assumed: class names checked against `src/prov/model/records.py`, factory methods against `src/prov/model/bundle.py:539-1365`, PROV-N keywords against `PROV_N_MAP`/`ADDITIONAL_N_MAP` in `src/prov/constants.py:50-91`, round-trip status against the shared test matrix (`src/prov/tests/test_statements.py`, `conftest.py::SHARED_TARGETS`) and the known-issue list.
- [ ] Round-trip caveat cells cite their tracking issues: #217 (scruffy same-id relations, RDF), #218 (multi-datatype fidelity, RDF), #77 (xsd:decimal, RDF), #223 (PROV-N metacharacter escaping), #224 (empty string, XML), #225 (xsd:float, RDF), #226 (anonymous qualified delegations, RDF).
- [ ] Gaps surfaced by building the matrix (e.g. no dedicated factories for Person/Organization/SoftwareAgent agents or EmptyCollection; whatever #154 actually still asks for — re-read the issue, `revision()`/`quotation()`/`primary_source()`/`mention()`/`influence()` all exist at `bundle.py:1011-1242`) are recorded as findings in the findings doc, not silently absorbed.
- [ ] Sphinx build stays at 0 warnings; full test suite unchanged.

**Verify:** `uv run --group docs --extra rdf --extra xml sphinx-build -b html docs docs/_build/html` → `build succeeded.` with 0 warnings, and `uv run pytest` → same tally as baseline.

**Steps:**

- [ ] **Step 1: Create the findings doc scaffold**

`docs/superpowers/specs/2026-07-10-conformance-audit-findings.md`:

```markdown
# Conformance audit findings — Phase 3.5 (roadmap steps 28–32)

**Status:** In progress. Working document; each audit task appends its section.
**Triage:** Section 6 is filled by the triage task (step 31) after maintainer sign-off.

## 0. Baseline (recorded before any audit work)

- Test suite tally: <paste `uv run pytest` summary line>
- ruff / mypy / sphinx: clean, 0 warnings.

## 1. Conformance matrix findings (step 28)
## 2. Formal attributes & PROV-DM §5 semantics (step 29)
## 3. Serializer mappings (step 30)
### 3.1 PROV-XML vs XSD
### 3.2 PROV-JSON vs member submission
### 3.3 PROV-N vs grammar
### 3.4 PROV-O vs mapping tables
## 4. Unification vs PROV-CONSTRAINTS (step 30b) — summary
See docs/superpowers/specs/2026-07-10-unification-gap-analysis.md (authority for step 36b).
## 5. Issues filed by this audit
| Issue | Finding | Section |
|---|---|---|
## 6. Triage (step 31) — proposed → approved
| Issue | Bucket (2.x / 3.0 / backlog) | Rationale |
|---|---|---|

## Out of scope (step 32)
The PROV-CONSTRAINTS validation engine (#62 — inferences, event ordering,
typing and impossibility checks) stays on the post-3.0 features backlog.
Only the unification/merging rules that back `unified()` are in scope.
```

Run the baseline commands from the plan preamble and paste the tally into section 0.

- [ ] **Step 2: Build the matrix page from this row inventory**

Row inventory (from PROV-DM Table 5/6; verify each cell before writing it — the "expected" values below are the planner's reading of the current code and must be confirmed):

| Component | Concept (PROV-DM §) | Expected class | Expected factory / alias |
|---|---|---|---|
| 1 Entities/Activities | Entity §5.1.1 | ProvEntity | `entity()` |
| 1 | Activity §5.1.2 | ProvActivity | `activity()` |
| 1 | Generation §5.1.3 | ProvGeneration | `generation()` / `wasGeneratedBy` |
| 1 | Usage §5.1.4 | ProvUsage | `usage()` / `used` |
| 1 | Communication §5.1.5 | ProvCommunication | `communication()` / `wasInformedBy` |
| 1 | Start §5.1.6 | ProvStart | `start()` / `wasStartedBy` |
| 1 | End §5.1.7 | ProvEnd | `end()` / `wasEndedBy` |
| 1 | Invalidation §5.1.8 | ProvInvalidation | `invalidation()` / `wasInvalidatedBy` |
| 2 Derivations | Derivation §5.2.1 | ProvDerivation | `derivation()` / `wasDerivedFrom` |
| 2 | Revision §5.2.2 | ProvDerivation + `prov:Revision` | `revision()` / `wasRevisionOf` |
| 2 | Quotation §5.2.3 | ProvDerivation + `prov:Quotation` | `quotation()` / `wasQuotedFrom` |
| 2 | Primary Source §5.2.4 | ProvDerivation + `prov:PrimarySource` | `primary_source()` / `hadPrimarySource` |
| 3 Agents | Agent §5.3.1 | ProvAgent | `agent()` |
| 3 | Person / Organization / SoftwareAgent §5.3.1 | via `prov:type` only | none (record as finding) |
| 3 | Attribution §5.3.2 | ProvAttribution | `attribution()` / `wasAttributedTo` |
| 3 | Association §5.3.3 | ProvAssociation | `association()` / `wasAssociatedWith` |
| 3 | Plan §5.3.3 | via `association(plan=…)` | — |
| 3 | Delegation §5.3.4 | ProvDelegation | `delegation()` / `actedOnBehalfOf` |
| 3 | Influence §5.3.5 | ProvInfluence | `influence()` / `wasInfluencedBy` |
| 4 Bundles | Bundle constructor §5.4.1 | ProvBundle | `ProvDocument.bundle()` / `add_bundle()` |
| 4 | Bundle type §5.4.2 | — | how is `prov:Bundle`-typed entity handled? (audit) |
| 5 Alternate | Specialization §5.5.1 | ProvSpecialization | `specialization()` / `specializationOf` |
| 5 | Alternate §5.5.2 | ProvAlternate | `alternate()` / `alternateOf` |
| 5 | Mention (PROV-LINKS) | ProvMention | `mention()` / `mentionOf` |
| 6 Collections | Collection §5.6 | ProvEntity + `prov:Collection` | `collection()` |
| 6 | EmptyCollection §5.6 | type constant only? (audit) | none (record as finding) |
| 6 | Membership §5.6 | ProvMembership | `membership()` / `hadMember` |

Round-trip columns: JSON / XML / RDF (from the shared `fmt` matrix; cite the per-target skips/xfails) plus a **PROV-N (output only)** column noting there is no parser (#122, Phase 5b) and the #223 escaping caveat. Also include a short "additional attributes" section covering `prov:label`, `prov:location`, `prov:role`, `prov:type`, `prov:value` (constants at `src/prov/constants.py:206-210`) and a pointer to `docs/explanation/prov-dm.md` for concept background. State the page's maintenance rule: revisited at each release (roadmap cross-cutting note) and extended with a JSON-LD column in Phase 5.

- [ ] **Step 3: Wire into the reference toctree**

In `docs/reference/index.md`, add `conformance` as the last toctree entry (after `dot`) and mention it in the intro paragraph (the roadmap step 22 text already reserves "plus the conformance matrix (Phase 3.5)" for this section).

- [ ] **Step 4: Verify and record findings**

Run the Verify commands. Any discrepancy discovered while filling cells (expected class/factory missing, keyword mismatch, undocumented round-trip caveat) goes into findings doc section 1; genuinely new defects get issues filed per the convention.

- [ ] **Step 5: Commit and PR**

```bash
git checkout -b audit/conformance-matrix master
git add docs/reference/conformance.md docs/reference/index.md docs/superpowers/specs/2026-07-10-conformance-audit-findings.md
git commit -m "Add PROV-DM conformance matrix reference page (roadmap step 28)"
gh pr create --title "Conformance matrix reference page (roadmap step 28)" --body "..."
```

Then: CI green → merge (`gh pr merge --merge --delete-branch`) → pull master.

```json:metadata
{"files": ["docs/reference/conformance.md", "docs/reference/index.md", "docs/superpowers/specs/2026-07-10-conformance-audit-findings.md"], "verifyCommand": "uv run --group docs --extra rdf --extra xml sphinx-build -b html docs docs/_build/html && uv run pytest", "acceptanceCriteria": ["Matrix page has a verified row for every inventory concept", "Caveat cells cite issues #217/#218/#77/#223/#224/#225/#226", "Findings doc scaffold created with baseline tally", "Sphinx 0 warnings, suite tally unchanged"], "modelTier": "standard"}
```

---

### Task 2: Formal attributes & semantics audit vs PROV-DM §5 (step 29)

**Goal:** Compare every record type's formal attributes, optionality, and datatype handling against PROV-DM §5's normative definitions, with special attention to §5.7 (Values, Qualified Names, Namespace Declarations) where siblings of #89/#77 likely live; document findings and file issues.

**Files:**
- Modify: `docs/superpowers/specs/2026-07-10-conformance-audit-findings.md` (section 2 + section 5 table)
- Possibly create: `src/prov/tests/test_conformance_dm.py` (characterization/xfail tests for confirmed defects — only where a defect is cheaply reproducible; otherwise the issue's reproduction snippet suffices)

**Acceptance Criteria:**
- [ ] Findings doc section 2 has one subsection per PROV-DM §5 component (§5.1–§5.6) plus §5.7, each ending in an explicit verdict per record type: **conformant** / **finding(s) listed**.
- [ ] For each record type, three checks are recorded: (a) `FORMAL_ATTRIBUTES` tuple in `src/prov/model/records.py` matches the spec's attribute list and order; (b) optionality — which formal attributes may be `None`/`-` per spec vs what the class and its `ProvBundle` factory accept; (c) datatypes — `parse_xsd_types()` / `Literal` handling vs the spec's value types (§5.7.4), including the known #89 (datatype-less literals) and #77 (Decimal) as anchors.
- [ ] Every new defect has a filed GitHub issue (no milestone) cross-referenced in findings section 5, and either a strict-xfail/characterization test or a reproduction snippet in the issue body.
- [ ] The Phase-3 loose end "`prov.read()` cannot auto-detect valid PROV-XML" is checked for an existing issue and filed if missing.
- [ ] No production source changes (2.x freeze); suite/lint/mypy/docs stay green.

**Verify:** `uv run pytest && uv run ruff check src/ && uv run mypy src` → all clean; new tests (if any) appear in the tally as passed/xfailed with issue refs in their reasons.

**Steps:**

- [ ] **Step 1: Read PROV-DM §5 component by component** (`defuddle parse https://www.w3.org/TR/prov-dm/ --md`), extracting for each record type its formal attribute table (id, attributes, optionality markers) into findings section 2 as the comparison baseline.

- [ ] **Step 2: Compare against the code**, record type by record type:

```bash
grep -n "FORMAL_ATTRIBUTES" src/prov/model/records.py     # per-class tuples
grep -n "PROV_ATTRIBUTE_QNAMES\|PROV_ATTRIBUTE_LITERALS" src/prov/constants.py
grep -n "def parse_xsd_types\|XSD_DATATYPE_PARSERS\|_parse" src/prov/model/records.py
```

For each type check attribute names/order, which arguments the factory requires vs the spec's optionality, and how time/QName/value datatypes are parsed and emitted. §5.7 checks explicitly include: QName escaping rules (feeds #223), `prov:value` semantics, language-tagged strings, and the datatype table (xsd:anyURI, xsd:QName, dateTime handling incl. timezones).

- [ ] **Step 3: For each defect, file an issue** (spec section + minimal repro + audit-doc pointer), and where a repro is a one-liner add it to `src/prov/tests/test_conformance_dm.py` as a strict xfail or characterization test, e.g.:

```python
@pytest.mark.xfail(strict=True, reason="#<new>: <spec §> — <one-line defect>")
def test_<defect_slug>():
    doc = ProvDocument()
    doc.set_default_namespace("http://example.org/")
    ...
    assert <spec-conformant expectation>
```

- [ ] **Step 4: Write verdicts, update findings section 5, run Verify commands.**

- [ ] **Step 5: Commit and PR** (branch `audit/prov-dm-section5`, commit message `Audit formal attributes and semantics against PROV-DM §5 (roadmap step 29)`), CI green → merge → pull master.

```json:metadata
{"files": ["docs/superpowers/specs/2026-07-10-conformance-audit-findings.md", "src/prov/tests/test_conformance_dm.py"], "verifyCommand": "uv run pytest && uv run ruff check src/ && uv run mypy src", "acceptanceCriteria": ["Per-component verdicts recorded for §5.1–§5.7", "FORMAL_ATTRIBUTES/optionality/datatype checks recorded per record type", "Every defect has a filed issue and repro", "No production source changes"], "modelTier": "frontier"}
```

---

### Task 3: PROV-XML schema-validation test (step 30, XML leg)

**Goal:** Vendor the W3C PROV-XML XSD and add a test validating `prov`'s XML output against it (the roadmap's explicitly named deliverable "add lxml schema-validation test"); failures become filed issues + strict xfails.

**Files:**
- Create: `src/prov/tests/schemas/` — `prov.xsd` plus every transitively referenced schema, and `README.md` (provenance + W3C Document Licence note)
- Create: `src/prov/tests/test_xml_schema.py`
- Modify: `pyproject.toml` (`[tool.setuptools.package-data]` → add `"schemas/*"` to the `"prov.tests"` list at lines 70–76)
- Modify: `docs/superpowers/specs/2026-07-10-conformance-audit-findings.md` (section 3.1)

**Acceptance Criteria:**
- [ ] Schema files vendored with closure: after downloading `https://www.w3.org/ns/prov.xsd`, every `schemaLocation` referenced (grep for `schemaLocation=`) is also vendored and rewritten/relative so `lxml.etree.XMLSchema` compiles **offline** (no network in CI).
- [ ] New test module validates the serialized XML of all 8 `examples.tests` documents and one document built from the full `ATTRIBUTE_VALUES` corpus (`src/prov/tests/attribute_values.py`) against the schema.
- [ ] Any validation failure is triaged: prov output bug → issue filed + that param marked `pytest.mark.xfail(strict=True, reason="#N: ...")`; schema/spec limitation → documented skip with reason. No silent failures.
- [ ] `"schemas/*"` added to package-data so the sdist stays complete (the malformed-corpus precedent, Phase 3 Task 14).
- [ ] Does not touch `src/prov/tests/xml/` (its `*.xml` files are consumed by glob-based tests).

**Verify:** `uv run pytest src/prov/tests/test_xml_schema.py -v` → all params pass/xfail/skip with reasons; full `uv run pytest` green; `uv run ruff check src/` clean. (mypy excludes tests.)

**Steps:**

- [ ] **Step 1: Vendor the schema closure**

```bash
mkdir -p src/prov/tests/schemas
curl -fsSL https://www.w3.org/ns/prov.xsd -o src/prov/tests/schemas/prov.xsd
grep -o 'schemaLocation="[^"]*"' src/prov/tests/schemas/prov.xsd
# fetch each referenced .xsd the same way, repeat until closure;
# adjust schemaLocation values to relative paths if they point at w3.org
```

Write `src/prov/tests/schemas/README.md` recording each file's source URL, retrieval date, and the W3C Software and Document Licence pointer.

- [ ] **Step 2: Write the test module**

```python
"""Validate PROV-XML output against the W3C PROV-XML schema (roadmap step 30)."""

from io import BytesIO
from pathlib import Path

import pytest

etree = pytest.importorskip("lxml.etree")

from prov.model import ProvDocument
from prov.tests import examples
from prov.tests.attribute_values import ATTRIBUTE_VALUES

SCHEMA_DIR = Path(__file__).parent / "schemas"


@pytest.fixture(scope="module")
def prov_xml_schema():
    return etree.XMLSchema(etree.parse(str(SCHEMA_DIR / "prov.xsd")))


def _validate(schema, document):
    xml_bytes = document.serialize(format="xml").encode("utf-8")
    schema.assert_(etree.parse(BytesIO(xml_bytes)))


@pytest.mark.parametrize(
    "make_document", [fn for _name, fn in examples.tests], ids=[n for n, _fn in examples.tests]
)
def test_example_documents_validate_against_prov_xsd(prov_xml_schema, make_document):
    _validate(prov_xml_schema, make_document())


@pytest.mark.parametrize("index", range(len(ATTRIBUTE_VALUES)))
def test_attribute_values_validate_against_prov_xsd(prov_xml_schema, index):
    doc = ProvDocument()
    doc.set_default_namespace("http://example.org/")
    doc.entity("e1", {"prov:value": ATTRIBUTE_VALUES[index]})
    _validate(prov_xml_schema, doc)
```

(Adapt namespace registration to how `attribute_values.py` documents its QName values — mirror `test_attributes.py`'s document construction so every corpus value serializes.)

- [ ] **Step 3: Run, triage failures** — for each failing param decide bug-vs-schema-limitation, file issues, attach `xfail(strict=True)`/`skip` marks per param (the per-param mark pattern from `test_statements.py`), and write section 3.1 of the findings doc including a verdict line ("PROV-XML output is schema-valid except: …").

- [ ] **Step 4: Add the package-data glob, run full Verify commands.**

- [ ] **Step 5: Commit and PR** (branch `audit/xml-xsd-validation`, message `Add PROV-XML schema validation tests against the W3C XSD (roadmap step 30)`), CI green → merge → pull master.

```json:metadata
{"files": ["src/prov/tests/schemas/", "src/prov/tests/test_xml_schema.py", "pyproject.toml", "docs/superpowers/specs/2026-07-10-conformance-audit-findings.md"], "verifyCommand": "uv run pytest src/prov/tests/test_xml_schema.py -v && uv run pytest && uv run ruff check src/", "acceptanceCriteria": ["Schema closure vendored, compiles offline", "8 examples + full ATTRIBUTE_VALUES corpus validated", "Failures filed + xfail'd, none silent", "sdist package-data updated"], "modelTier": "standard"}
```

---

### Task 4: PROV-JSON schema validation + submission audit (step 30, JSON leg)

**Goal:** Validate `prov`'s JSON output against the PROV-JSON member submission's JSON schema and audit `provjson.py` against the submission's prose (where #168's `xsd:QName` finding came from); file issues for gaps.

**Files:**
- Create: `src/prov/tests/schemas/prov-json.schema.json` (+ update `schemas/README.md`)
- Create: `src/prov/tests/test_json_schema.py`
- Modify: `pyproject.toml` (`jsonschema>=4` added to the `dev` dependency group, lines 80–91; refresh `uv.lock` via `uv sync`)
- Modify: `docs/superpowers/specs/2026-07-10-conformance-audit-findings.md` (section 3.2)

**Acceptance Criteria:**
- [ ] The submission's JSON schema is vendored (fetch from the schema link on https://www.w3.org/submissions/prov-json/; record exact URL + date in `schemas/README.md`). The validator is chosen by the schema's declared `$schema` draft via `jsonschema.validators.validator_for(schema)` — do not assume a modern draft; the 2013 submission likely declares draft-03/04. If the schema turns out not to be machine-usable with current `jsonschema` (draft unsupported/schema buggy), the fallback is a documented manual audit in findings section 3.2 and **no test module** — state which path was taken.
- [ ] If the test lands: all 8 `examples.tests` documents' JSON output validates; failures triaged exactly as Task 3 (issue + strict xfail, or documented schema-limitation skip).
- [ ] Prose audit recorded: structural encoding (one section per record type), the `$`/`type` value-encoding rules (anchors #89/#168), bundle encoding, and namespace/prefix handling, each with a verdict.
- [ ] No production source changes; `jsonschema` stays dev-only (not a runtime or extra dependency).

**Verify:** `uv run pytest src/prov/tests/test_json_schema.py -v` (if the test path was taken) and full `uv run pytest` green; `uv run ruff check src/` clean; `git diff uv.lock` shows only the jsonschema addition.

**Steps:**

- [ ] **Step 1: Fetch the submission** (`defuddle parse https://www.w3.org/submissions/prov-json/ --md`), locate and download the schema file, vendor it under `src/prov/tests/schemas/`, update the README.

- [ ] **Step 2: Add the dev dep**: append `"jsonschema>=4"` to `[dependency-groups] dev` in `pyproject.toml`, run `uv sync --extra rdf --extra xml`.

- [ ] **Step 3: Write the test module**

```python
"""Validate PROV-JSON output against the member submission's JSON schema (roadmap step 30)."""

import json
from pathlib import Path

import pytest

jsonschema = pytest.importorskip("jsonschema")

from prov.tests import examples

SCHEMA_PATH = Path(__file__).parent / "schemas" / "prov-json.schema.json"


@pytest.fixture(scope="module")
def prov_json_validator():
    schema = json.loads(SCHEMA_PATH.read_text())
    validator_cls = jsonschema.validators.validator_for(schema)
    validator_cls.check_schema(schema)
    return validator_cls(schema)


@pytest.mark.parametrize(
    "make_document", [fn for _name, fn in examples.tests], ids=[n for n, _fn in examples.tests]
)
def test_example_documents_validate_against_prov_json_schema(prov_json_validator, make_document):
    container = json.loads(make_document().serialize(format="json"))
    errors = sorted(prov_json_validator.iter_errors(container), key=str)
    assert not errors, "\n".join(str(e) for e in errors)
```

- [ ] **Step 4: Prose audit of `src/prov/serializers/provjson.py`** against the submission's sections (record-type encodings, typed-value objects, bundles, prefixes); record verdicts + file issues; note #168 and #228 as already-known anchors.

- [ ] **Step 5: Run Verify commands; commit and PR** (branch `audit/prov-json-schema`, message `Validate PROV-JSON output against the member submission schema (roadmap step 30)`), CI green → merge → pull master.

```json:metadata
{"files": ["src/prov/tests/schemas/prov-json.schema.json", "src/prov/tests/test_json_schema.py", "pyproject.toml", "docs/superpowers/specs/2026-07-10-conformance-audit-findings.md"], "verifyCommand": "uv run pytest && uv run ruff check src/", "acceptanceCriteria": ["Schema vendored with source recorded; validator draft chosen from $schema", "Examples validate or failures are filed+xfail'd (or documented manual-audit fallback)", "provjson.py prose audit verdicts recorded", "jsonschema is dev-group only"], "modelTier": "standard"}
```

---

### Task 5: PROV-N grammar + PROV-O mapping audit (step 30, N/RDF legs)

**Goal:** Check `prov`'s PROV-N output against the normative PROV-N grammar using ProvToolbox's parser as an external oracle, and audit `provrdf.py` against the PROV-O mapping tables; document findings and file issues. **Audit-only: no shipped test code depends on `provconvert`.**

**Files:**
- Modify: `docs/superpowers/specs/2026-07-10-conformance-audit-findings.md` (sections 3.3, 3.4 — including the oracle script and its captured results)

**Acceptance Criteria:**
- [ ] Every `examples.tests` document's `get_provn()` output parses cleanly under `provconvert` (or each failure is recorded with the offending production and a filed/linked issue — #223 is the known anchor for metacharacter local parts).
- [ ] Grammar stress cases beyond the examples are exercised and recorded: metacharacter local parts (#223 repro), non-ASCII/CJK/RTL names and values, language-tagged literals, each XSD datatype in `ATTRIBUTE_VALUES`, and time-zoned datetimes.
- [ ] PROV-O audit: for each of the 15 relation types + 3 element types, the triples/classes emitted by `src/prov/serializers/provrdf.py` are compared against the PROV-O term mappings (qualified and unqualified patterns); a verdict per type is recorded. Known anchors cited: #217, #218, #225, #226, #96 (prefix propagation to turtle).
- [ ] Both sections end with a verdict summary and any new issues cross-referenced in findings section 5.
- [ ] No changes outside the findings doc (docs-only PR; CI trivially green).

**Verify:** `uv run --group docs --extra rdf --extra xml sphinx-build -b html docs docs/_build/html` → 0 warnings (findings doc is not in a toctree, but the build must not regress); the oracle-run transcript is embedded in section 3.3.

**Steps:**

- [ ] **Step 1: Run the PROV-N oracle over the examples corpus** (script goes in the scratchpad, transcript into the findings doc):

```bash
OUT=<scratchpad>/provn-audit && mkdir -p "$OUT"
uv run python - "$OUT" <<'EOF'
import sys
from pathlib import Path
from prov.tests import examples

out = Path(sys.argv[1])
for name, factory in examples.tests:
    (out / (name.replace(" ", "_") + ".provn")).write_text(factory().get_provn())
EOF
for f in "$OUT"/*.provn; do
    provconvert -infile "$f" -outfile "${f%.provn}.xml" \
        && echo "OK   $(basename "$f")" || echo "FAIL $(basename "$f")"
done
```

- [ ] **Step 2: Grammar stress cases** — extend the script with documents built from `ATTRIBUTE_VALUES` plus hand-built docs for #223 metacharacters (`ex:n'ame`, locals containing `( ) , = ;`), CJK/RTL identifiers, and `datetime` values with non-UTC offsets; run through `provconvert`; for each FAIL identify the grammar production violated (PROV-N §3, the EBNF) and file/link an issue.

- [ ] **Step 3: PROV-O mapping audit** — read the PROV-O spec's term tables (`defuddle parse https://www.w3.org/TR/prov-o/ --md`) and walk `src/prov/serializers/provrdf.py` per record type: unqualified property emitted, qualified class + participation properties, formal-attribute→property mapping, blank-node usage for anonymous relations. Record a per-type verdict table in section 3.4.

- [ ] **Step 4: File new issues, update findings section 5, commit and PR** (branch `audit/provn-provo-mappings`, message `Audit PROV-N grammar and PROV-O mapping conformance (roadmap step 30)`), CI green → merge → pull master.

```json:metadata
{"files": ["docs/superpowers/specs/2026-07-10-conformance-audit-findings.md"], "verifyCommand": "uv run --group docs --extra rdf --extra xml sphinx-build -b html docs docs/_build/html", "acceptanceCriteria": ["provconvert oracle run over examples + stress cases, transcript recorded", "Every parse failure mapped to a grammar production and an issue", "Per-type PROV-O mapping verdict table recorded", "Docs-only change"], "modelTier": "frontier"}
```

---

### Task 6: Unification gap analysis vs PROV-CONSTRAINTS (step 30b)

**Goal:** Produce the documented gap analysis of `ProvBundle._unified_records()` (`src/prov/model/bundle.py:384-414`) against PROV-CONSTRAINTS' key constraints and term-unification rules, import the W3C-derived unification corpus from ProvToolbox as test fixtures, and lock current behaviour with characterization tests — the authority document for the 3.0 step-36b reimplementation.

**Files:**
- Create: `docs/superpowers/specs/2026-07-10-unification-gap-analysis.md`
- Create: `src/prov/tests/unification/constraints/` (~150 `.xml` fixtures + `README.md` with provenance/licence)
- Create: `src/prov/tests/test_unification_constraints.py`
- Modify: `pyproject.toml` (package-data: add `"unification/constraints/*"` to the `"prov.tests"` list)
- Modify: `docs/superpowers/specs/2026-07-10-conformance-audit-findings.md` (section 4 summary + section 5)

**Acceptance Criteria:**
- [ ] The gap-analysis doc covers, rule by rule: term unification incl. placeholder (`-`) vs concrete values; key constraints over each record type's formal attributes (PROV-CONSTRAINTS §5.1, the constraint numbers the existing fixture names cite — c22/c23/c28/c29 etc.); rejection of non-unifiable formal attributes and incompatible types; and bundle independence (§8: constraints apply within each bundle; `flattened().unified()` merging across bundle boundaries — as `test_unifying` in `test_model.py:83-97` does — is spec-invalid usage worth calling out). Each rule gets: spec statement → current `_unified_records()` behaviour (attribute union keyed on identifier only) → concrete failing example → 36b requirement.
- [ ] `ProvDocument.unified()` (`bundle.py:1455-1480`) is analysed separately: confirm whether sub-bundles are unified independently of the parent document and of each other, and record the answer with a test.
- [ ] The ProvToolbox corpus (`~/projects/ProvToolbox/modules-validation/prov-validation/src/test/resources/validate/unification/*.xml`) is vendored with a README recording origin (ProvToolbox path + upstream W3C PROV-CONSTRAINTS implementation-report test cases), the ProvToolbox licence (check `~/projects/ProvToolbox/LICENSE*` and quote/attach as required), and the success/fail naming convention.
- [ ] A characterization test runs every corpus case through deserialize→`unified()`: asserting current behaviour (never raises; fail-cases silently merge), with the module docstring stating the 3.0 flip (fail-cases will assert the documented merge-failure exception). Corpus files prov cannot parse are collected as parse-failure findings (skips with reasons), not hidden.
- [ ] Hand-written per-rule gap examples (at least: conflicting `startTime` merge, placeholder-vs-concrete unification, incompatible-element-type merge e.g. same id used by `entity()` and `activity()`) are committed as characterization tests asserting the **current** (wrong) outcome with `# 3.0 triage:` comments referencing the umbrella issue.
- [ ] An umbrella GitHub issue "unified() does not implement PROV-CONSTRAINTS merging" is filed (no milestone; Task 7 proposes 3.0.0), linking the gap-analysis doc.
- [ ] No behaviour changes; the pre-existing 9 `unification/*.json` fixtures and `test_unifying` stay untouched.

**Verify:** `uv run pytest src/prov/tests/test_unification_constraints.py -v` → every corpus case listed as passed (characterization) or skipped (parse failure, with reason); full `uv run pytest` green; `uv run ruff check src/` clean.

**Steps:**

- [ ] **Step 1: Read PROV-CONSTRAINTS** (`defuddle parse https://www.w3.org/TR/prov-constraints/ --md`): the unification/merging definitions, §5.1 uniqueness/key constraints, and §8 bundle scoping. Extract each rule relevant to `unified()` into the gap doc with its constraint number.

- [ ] **Step 2: Vendor the corpus**

```bash
mkdir -p src/prov/tests/unification/constraints
cp ~/projects/ProvToolbox/modules-validation/prov-validation/src/test/resources/validate/unification/*.xml \
   src/prov/tests/unification/constraints/
ls src/prov/tests/unification/constraints | wc -l   # expect ~150
cat ~/projects/ProvToolbox/LICENSE* | head -20      # record licence in the README
```

Add the package-data glob. Write `constraints/README.md` (origin, licence, naming convention, retrieval date).

- [ ] **Step 3: Characterization harness**

```python
"""Characterize unified() against the PROV-CONSTRAINTS unification corpus (roadmap step 30b).

2.x locks CURRENT behaviour: unified() is an identifier-keyed attribute union and
never rejects a merge. In 3.0 (roadmap step 36b) the fail-cases below flip to
asserting the documented merge-failure exception. Authority:
docs/superpowers/specs/2026-07-10-unification-gap-analysis.md
"""

from pathlib import Path

import pytest

pytest.importorskip("lxml")

from prov.model import ProvDocument

CORPUS = Path(__file__).parent / "unification" / "constraints"


def _cases():
    for xml_path in sorted(CORPUS.glob("*.xml")):
        expected = "success" if "-success" in xml_path.stem else "fail"
        yield pytest.param(xml_path, expected, id=xml_path.stem)


@pytest.mark.filterwarnings("ignore::FutureWarning")  # unified() 3.0 signpost
@pytest.mark.parametrize("xml_path, expected", list(_cases()))
def test_unified_corpus_characterization(xml_path, expected):
    with open(xml_path, "rb") as f:
        document = ProvDocument.deserialize(f, format="xml")
    unified = document.unified()  # 3.0 triage (#<umbrella>): fail-cases must raise
    assert unified.get_records()  # currently: silent union, never a rejection
```

(If specific corpus files fail to deserialize, convert those params to `pytest.param(..., marks=pytest.mark.skip(reason="prov cannot parse: <error> — recorded in gap analysis"))` and record each in the gap doc; a parse failure is itself a PROV-XML finding.)

- [ ] **Step 4: Hand-written per-rule gap tests** in the same module, e.g.:

```python
@pytest.mark.filterwarnings("ignore::FutureWarning")
def test_conflicting_start_times_currently_merge_silently():
    # PROV-CONSTRAINTS key constraints: two activity records with the same id
    # and different startTime do NOT unify; spec requires merge failure.
    doc = ProvDocument()
    doc.set_default_namespace("http://example.org/")
    doc.activity("a1", startTime="2011-11-16T16:00:00")
    doc.activity("a1", startTime="2012-01-01T00:00:00")
    unified = doc.unified()
    assert len(unified.get_records()) == 1  # 3.0 triage (#<umbrella>): must raise
```

Capture the exact merged-record contents in the gap doc (what the attribute union actually produces) for each example.

- [ ] **Step 5: Analyse `ProvDocument.unified()` bundle handling** (read `bundle.py:1455-1480` in full), add a characterization test for a document with two bundles sharing record identifiers, and write the §8 bundle-independence section of the gap doc from the observed behaviour.

- [ ] **Step 6: File the umbrella issue; finish the gap doc** (current-behaviour summary, rule-by-rule gaps, 36b requirements incl. "merge failures raise a documented exception"); add the summary + cross-references to findings doc sections 4/5.

- [ ] **Step 7: Run Verify commands; commit and PR** (branch `audit/unification-constraints`, message `Add PROV-CONSTRAINTS unification gap analysis and test corpus (roadmap step 30b)`), CI green → merge → pull master.

```json:metadata
{"files": ["docs/superpowers/specs/2026-07-10-unification-gap-analysis.md", "src/prov/tests/unification/constraints/", "src/prov/tests/test_unification_constraints.py", "pyproject.toml", "docs/superpowers/specs/2026-07-10-conformance-audit-findings.md"], "verifyCommand": "uv run pytest src/prov/tests/test_unification_constraints.py -v && uv run pytest && uv run ruff check src/", "acceptanceCriteria": ["Rule-by-rule gap doc with failing examples and 36b requirements", "~150-case corpus vendored with licence/provenance README", "Characterization tests pass for all cases (or documented skips)", "Umbrella issue filed", "No behaviour changes"], "modelTier": "frontier"}
```

---

### Task 7: Triage findings + publish (steps 31–32)

**Goal:** Classify every audit finding and pre-existing conformance issue into 2.x (cheap, non-breaking) / 3.0.0 (behaviour/output-changing) / post-3.0 features backlog, get maintainer sign-off, apply the classification on GitHub, and close out the phase in the public docs.

**Files:**
- Modify: `docs/superpowers/specs/2026-07-10-conformance-audit-findings.md` (section 6 triage table; status → Complete)
- Modify: `docs/reference/conformance.md` (refresh cells with issue refs from Tasks 2–6)
- Modify: `ROADMAP.md` (mark Phase 3.5 complete, summarize outcomes)

**Acceptance Criteria:**
- [ ] The triage table covers **every** issue in findings section 5 plus the pre-existing set (#34, #62, #77, #89, #96, #122, #124, #129, #130, #131, #154, #168, #217, #218, #223, #224, #225, #226, #228), each with a bucket and one-line rationale. Buckets per the roadmap: cheap non-breaking → 2.x; behaviour/output-changing → Phase 4 (3.0.0 milestone); feature gaps (e.g. #129 PROV-Dictionary, #154 convenience methods, #62 validation engine, #122 → Phase 5b) → post-3.0 features backlog.
- [ ] **Maintainer checkpoint before any GitHub mutation:** the proposed table is presented via `AskUserQuestion` (run by the coordinator session, not a subagent) and only the approved classification is applied. How the backlog is represented (existing milestone? new "Backlog" milestone? label?) is part of this checkpoint — inspect `gh api repos/trungdong/prov/milestones --jq '.[].title'` first and present what exists.
- [ ] After approval: milestones/labels applied via `gh`, a phase-summary comment posted on tracking issue #181, and the step-32 out-of-scope note (validation engine stays backlog) restated in the #181 comment and the findings doc.
- [ ] `ROADMAP.md` Phase 3.5 entry updated (matching the established convention for completed phases) and the conformance matrix page refreshed; Sphinx 0 warnings.
- [ ] Any 2.x-bucket items are **listed, not fixed** — they become follow-up PRs outside this plan (note them in the #181 comment).

**Verify:** `gh issue list --state open --json number,milestone --jq '.[] | select(.milestone == null) | .number'` → no conformance-audit issue left unbucketed (backlog items carry whatever representation the maintainer chose); `uv run --group docs --extra rdf --extra xml sphinx-build -b html docs docs/_build/html` → 0 warnings; `uv run pytest` green.

**Steps:**

- [ ] **Step 1: Collate.** Pull findings section 5 + the pre-existing issue list; `gh issue list --state open --limit 100 --json number,title,milestone`; draft the bucket table with rationales in findings section 6 (marked "proposed").

- [ ] **Step 2: Maintainer checkpoint.** Coordinator presents the proposal via `AskUserQuestion` (bucket disagreements → adjust and re-present only the changed rows). Record the approved table (marked "approved <date>").

- [ ] **Step 3: Apply on GitHub.** For each 3.0.0 item: `gh issue edit <n> --milestone "3.0.0"`. Backlog items: apply the representation chosen at the checkpoint. Comment on #181 with the phase summary + out-of-scope note.

- [ ] **Step 4: Update docs.** Refresh `docs/reference/conformance.md` cells with final issue refs; mark the findings doc Complete; update `ROADMAP.md`.

- [ ] **Step 5: Run Verify commands; commit and PR** (branch `audit/triage`, message `Triage conformance audit findings; close out Phase 3.5 (roadmap steps 31-32)`), CI green → merge → pull master. Update the memory file `phase-3-execution-state.md` successor note (Phase 3.5 complete, next Phase 4) per the established pattern.

```json:metadata
{"files": ["docs/superpowers/specs/2026-07-10-conformance-audit-findings.md", "docs/reference/conformance.md", "ROADMAP.md"], "verifyCommand": "uv run --group docs --extra rdf --extra xml sphinx-build -b html docs docs/_build/html && uv run pytest", "acceptanceCriteria": ["Every finding + pre-existing issue bucketed with rationale", "Maintainer approved via AskUserQuestion before GitHub changes", "Milestones/backlog applied, #181 commented with out-of-scope note", "ROADMAP.md and conformance page updated"], "modelTier": "frontier"}
```

---

## Dependencies & ordering

Strictly sequential (established Phase 2/3 process — single shared checkout, no worktrees): Task 1 → 2 → 3 → 4 → 5 → 6 → 7. Tasks 2–6 all append to the shared findings doc, so parallel execution would conflict; Task 7 is blocked by all of 1–6.

## Notes for the executor

- **2.x freeze is absolute here.** If an audit uncovers something tempting to fix inline (even a one-word docstring), weigh it: docs-accuracy fixes are fine in the same PR; anything touching serializer/model behaviour or output is an issue + xfail, never a fix.
- Codacy CI failures are advisory — required checks are the pytest matrix, lint, typecheck, minimal-install, and package jobs. GitHub Actions may take ~20–30 s to register runs on a fresh branch; poll properly.
- `unified()` emits a `FutureWarning` (2.4.0 signpost) — new tests calling it need the `filterwarnings` mark shown in Task 6, or asserting the warning explicitly.
- New test modules must be pytest-native (module-level `test_*` functions, plain asserts) per the Phase 3 test-suite redesign; mypy excludes `src/prov/tests/`.
