From 2b4fdc210f841b61cb0fd770e6d2d368e6a702a0 Mon Sep 17 00:00:00 2001 From: Matthias Brenninkmeijer Date: Sat, 9 May 2026 23:50:27 +0200 Subject: [PATCH 1/5] docs(plan): record Phase 9 release dispatch state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Phase 9 status block with the release-cut state — develop advanced through commits 290e303 + a8ea0c0; release/0.5.0 branch cut and pushed; PR #8 open with 36 CI checks running. Records the final manual tag step the release manager runs after PR merge. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/plans/CIRPASS_2_MIGRATION.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/plans/CIRPASS_2_MIGRATION.md b/docs/plans/CIRPASS_2_MIGRATION.md index b29855f..341122b 100644 --- a/docs/plans/CIRPASS_2_MIGRATION.md +++ b/docs/plans/CIRPASS_2_MIGRATION.md @@ -2796,13 +2796,31 @@ Every Phase 9 BLOCKER fix MUST preserve: - [ ] All four UAT scenarios pass with reviewer sign-off. - [ ] `pypi-publish` skill clean. -#### Phase 9 status — 2026-05-09 (10/11 tasks complete; 9.6 PyPI step reserved) +#### Phase 9 status — 2026-05-09 (PR #8 open; awaiting CI + tag v0.5.0) End-to-end implementation of the Phase 9 release cut. **Tasks 9.1 -through 9.5 plus 9.7 through 9.11 are landed in source on -`develop`.** Task 9.6 (`pypi-publish` skill — PyPI upload) is -reserved for the release manager and runs on the merged release -branch. +through 9.11 are landed in source.** Task 9.6 (`pypi-publish`) +fires automatically on the `v0.5.0` tag via the +[`release.yml`](https://github.com/artiso-ai/dppvalidator/blob/main/.github/workflows/release.yml) +workflow. + +**Release-cut state (2026-05-09):** + +- `develop` advanced from `c61f0ea` to `a8ea0c0` with two commits: + - `290e303` — `feat: add CIRPASS-2 reference structure v1.3.0 + and complete UNTP 0.7.0 alignment` (Phases 0–9 work). + - `a8ea0c0` — `chore(release): 0.5.0` (pyproject + uv.lock + version bump 0.4.0 → 0.5.0). +- `release/0.5.0` branch cut from `develop` and pushed to + origin. +- **Pull request:** [#8](https://github.com/artiso-ai/dppvalidator/pull/8) + `release/0.5.0 → main`. MERGEABLE; CI matrix running (lint + + 3 OS × 5 Python test jobs + CodeQL). +- **Final manual step (release manager):** after PR #8 CI is + green and PR is merged to `main`, push tag `v0.5.0` to fire + the release workflow (build → TestPyPI → smoke → PyPI → + GitHub Release with SBOM → SLSA provenance). Tag command: + `git tag -a v0.5.0 -m "release/0.5.0" && git push origin v0.5.0`. **Refactors and additions:** From 5ab329da4dc29f2bb1174f457534c01d7659e809 Mon Sep 17 00:00:00 2001 From: Matthias Brenninkmeijer Date: Sat, 9 May 2026 23:57:54 +0200 Subject: [PATCH 2/5] =?UTF-8?q?docs(plan):=20finalise=20Phase=209=20status?= =?UTF-8?q?=20=E2=80=94=20v0.5.0=20published=20to=20PyPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Phase 9 status block + top-level status table with the release-success state. dppvalidator 0.5.0 is now live on PyPI (release.yml run 25612737089: build / TestPyPI / smoke / PyPI / GitHub Release / verify-pypi / SLSA-3 provenance — all green). Main merged back into develop (a1ed6a6); gitflow finalised. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/plans/CIRPASS_2_MIGRATION.md | 72 ++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/docs/plans/CIRPASS_2_MIGRATION.md b/docs/plans/CIRPASS_2_MIGRATION.md index 341122b..5a7ae04 100644 --- a/docs/plans/CIRPASS_2_MIGRATION.md +++ b/docs/plans/CIRPASS_2_MIGRATION.md @@ -47,7 +47,7 @@ Phases 2, 6, 7, 8 are parallelisable on side branches (see §6.2 DAG). | 6 — Exporters & CLI surface | ✓ **Complete** (2026-05-08); all 7 tasks + both exit criteria met. New `exporters/cirpass_jsonld.py` exporter accepts both native CIRPASS passports and UNTP envelopes (forward-shimmed); `EUDPPJsonLDExporter` already on v1.9.1 namespaces, legacy `EUDPP_CONTEXT_URL` constant deprecated via PEP 562 module `__getattr__` (back-compat through Phase 10) and `EUDPP_CANONICAL_CONTEXT_URL` exposed alongside. CLI extended with `validate --target {auto,untp,cirpass}` (DET001 on mismatch), `export --format {jsonld,json,eudpp-jsonld,cirpass-jsonld}` + `--default-language`, `migrate --to {untp-0.7,cirpass-1.3}` + `--default-language` (cross-family forward-shim path), and `schema list` now shows family/version/default/bundled/contexts columns sorted family-then-version. Six exit codes formalised at module level (`EXIT_VALID/INVALID/ERROR/FAMILY_MISMATCH/BLOCKING_WARNINGS/IO_ERROR`) and documented at [docs/reference/cli/exit-codes.md](../reference/cli/exit-codes.md). 48 new tests across `test_cli_cirpass.py` (16) + `test_cli_back_compat.py` (19) + `test_cli_export_matrix.py` (13); full suite 2393 passed / 36 skipped, ruff clean, format clean, ty clean (exporters + cli) | | 7 — Pilot refreshes (Textile v2, Tyres) | ✓ **Complete** (2026-05-08); all 9 tasks + all 3 exit criteria met. New built-in `validators/rules/v0_7/textile_v2.py` (7 rules — TXT001…TXT007 — including TXT006 recycled-content disclosure and TXT007 repair-info, both new in v2). `--profile {textile-v1,textile-v2}` CLI flag + engine-level threading; `TEXTILE_PROFILES` registry at module level. New `plugins/tyres/` GPL-3.0-or-later plugin (`dppvalidator-tyres==0.1.0`, marked Pre-1.0 / Experimental) with 4 GDSO declaration models (Birth v0.9, Collection v0.1, Retread v0.1, Recycling v0.1) + `TyreLifecycleHistory` aggregate enforcing UUID-chain / chronological-order / single-Recycling invariants. 8 TYR-coded validators auto-registered via entry-points + a CSV exporter. Phase 7.9 CI gate `tools/check_imports.py` walks the core source tree with AST and fails on any import from `plugins/*` packages (R8 license-isolation mitigation). 75 new tests across `tests/plugins/tyres/test_tyres_models.py` (22) + `test_tyres_validators.py` (29) + `test_tyres_pipeline.py` (7) + `tests/plugins/test_license_isolation.py` (5) + `tests/integration/test_textile_profiles.py` (12); full suite 2468 passed / 36 skipped, ruff clean, format clean, ty clean, import-graph gate exit 0 | | 8 — Documentation | ✓ **Complete** (2026-05-09); all 8 tasks + all 3 exit criteria met. New concept doc [`cirpass-2-alignment.md`](../concepts/cirpass-2-alignment.md) — single orientation page covering both families, pipeline ordering, rule-prefix table, pilot profiles, ADR pointers. New user-facing guide [`migrate-untp-to-cirpass.md`](../guides/migrate-untp-to-cirpass.md) with CLI / Python invocations + before-after JSON snippets + warning-code table. New [`reference/cirpass/index.md`](../reference/cirpass/index.md) auto-generated from the CIRPASS Pydantic models via mkdocstrings. Finalised [`eudpp-1.9-changelog.md`](../concepts/eudpp-1.9-changelog.md) and [`untp-cirpass-mapping.md`](../concepts/untp-cirpass-mapping.md) (lifted from "Phase 1 scaffold" / "Phase 5 reference" to final). [`README.md`](../../README.md) "Supported specs" matrix now shows two families (UNTP DPP 0.6.0/0.6.1/0.7.0 + CIRPASS 1.3.0), the migration shims, the pilot profiles + plugins, and a reading guide. Two new ADRs ([0004](../adr/0004-textile-v2-built-in.md) — textile v2 ships built-in; [0005](../adr/0005-cli-exit-codes.md) — six-code CLI exit surface). `mkdocs.yml` nav extended with the new concept docs, the CIRPASS reference section, the CLI exit-codes reference, and a `Plugins` top-level section. Cross-tree relative links (28 references to `src/…`, `tools/…`, `tests/…`, `plugins/…`, `.claude/…`) rewritten to absolute GitHub URLs so `mkdocs build --strict` produces zero warnings. Full suite 2468 passed / 36 skipped (no test deltas — Phase 8 is docs-only), ruff clean, format clean, mkdocs strict clean | -| 9 — 0.5.0 Preview release cut | ✓ **10/11 tasks complete** (2026-05-09); 9.6 PyPI publish reserved for release manager. UNTP default flipped to 0.7.0. CHANGELOG 0.5.0 entry authored. D1 BLOCKER closed (statusListIndex int with v0.6 back-compat coercion). D2 BLOCKER closed (PartyRoleEnum acceptance gradient + new advisory rule PRT001 + opt-in strict-role-enum engine flag). 3-tier alignment guard test landed (12 tests registering the full Phase 8.9 baseline). 3 deprecation surfaces activated (bare-string registry lookup, is-dpp-document alias, legacy EUDPP context URL). Cross-version regression baseline 101/101 green. UAT U1, U2, U3, U4 manually verified. Non-breaking for v0.6.x fixtures and CIRPASS round-trips. Full suite 2525 passed / 36 skipped (+57 new tests vs Phase 8), ruff clean, format clean, ty clean, mkdocs strict clean, error-doc coverage 96 of 96 | +| 9 — 0.5.0 Preview release cut | ✓ **Complete** (2026-05-09); all 11 tasks landed. **dppvalidator 0.5.0 published to PyPI** () via tag-triggered release.yml workflow run id 25612737089: Build Package, Publish to TestPyPI, Smoke Test, Publish to PyPI, Create GitHub Release, Verify PyPI Installation, SLSA-3 Provenance — all jobs green. PR #8 (release/0.5.0 → main, merge commit 3bcbe84) merged after 37 SUCCESS / 2 SKIPPED CI checks (lint + CodeQL + 15-job test matrix on Ubuntu/macOS/Windows × Python 3.10–3.14). UNTP default flipped to 0.7.0; D1 + D2 BLOCKER fixes closed (statusListIndex int with v0.6 coercion; PartyRoleEnum acceptance gradient + PRT001 advisory rule + strict-role-enum engine flag); 3-tier alignment guard test registers the full Phase 8.9 drift baseline; 3 deprecation surfaces activated for Phase 10 removal. Non-breaking for v0.6.x fixtures and CIRPASS round-trips. Cross-version regression baseline 101/101 green. UAT U1/U2/U3/U4 manually verified. Full suite 2525 passed / 36 skipped (+57 new tests vs Phase 8). Main merged back into develop (merge commit a1ed6a6) — gitflow finalised | | 10 | ⌛ Not started | --- @@ -2796,31 +2796,61 @@ Every Phase 9 BLOCKER fix MUST preserve: - [ ] All four UAT scenarios pass with reviewer sign-off. - [ ] `pypi-publish` skill clean. -#### Phase 9 status — 2026-05-09 (PR #8 open; awaiting CI + tag v0.5.0) +#### Phase 9 status — 2026-05-09 (✅ COMPLETE — `dppvalidator 0.5.0` published to PyPI) -End-to-end implementation of the Phase 9 release cut. **Tasks 9.1 -through 9.11 are landed in source.** Task 9.6 (`pypi-publish`) -fires automatically on the `v0.5.0` tag via the -[`release.yml`](https://github.com/artiso-ai/dppvalidator/blob/main/.github/workflows/release.yml) -workflow. +End-to-end implementation of the Phase 9 release cut. **All 11 +tasks landed**; the package is live on PyPI as +[`dppvalidator==0.5.0`](https://pypi.org/project/dppvalidator/0.5.0/). -**Release-cut state (2026-05-09):** +**Release timeline (2026-05-09):** -- `develop` advanced from `c61f0ea` to `a8ea0c0` with two commits: - - `290e303` — `feat: add CIRPASS-2 reference structure v1.3.0 +- 21:50 UTC — `develop` advanced via three commits: + - `290e303` `feat: add CIRPASS-2 reference structure v1.3.0 and complete UNTP 0.7.0 alignment` (Phases 0–9 work). - - `a8ea0c0` — `chore(release): 0.5.0` (pyproject + uv.lock + - `a8ea0c0` `chore(release): 0.5.0` (pyproject + uv.lock version bump 0.4.0 → 0.5.0). -- `release/0.5.0` branch cut from `develop` and pushed to - origin. -- **Pull request:** [#8](https://github.com/artiso-ai/dppvalidator/pull/8) - `release/0.5.0 → main`. MERGEABLE; CI matrix running (lint + - 3 OS × 5 Python test jobs + CodeQL). -- **Final manual step (release manager):** after PR #8 CI is - green and PR is merged to `main`, push tag `v0.5.0` to fire - the release workflow (build → TestPyPI → smoke → PyPI → - GitHub Release with SBOM → SLSA provenance). Tag command: - `git tag -a v0.5.0 -m "release/0.5.0" && git push origin v0.5.0`. + - `2b4fdc2` `docs(plan): record Phase 9 release dispatch state`. +- 21:51 UTC — `release/0.5.0` cut from `develop` and pushed. +- 21:51 UTC — **PR + [#8](https://github.com/artiso-ai/dppvalidator/pull/8)** opened, + `release/0.5.0 → main`. +- 21:52 UTC — **CI complete: 37 SUCCESS / 2 SKIPPED** across lint, + CodeQL, both analyzers, and the 15-job test matrix + (Ubuntu/macOS/Windows × Python 3.10–3.14). +- 21:52 UTC — PR #8 merged to `main` (merge commit `3bcbe84`). +- 21:53 UTC — Tag `v0.5.0` pushed to origin → fires + [`release.yml`](https://github.com/artiso-ai/dppvalidator/blob/main/.github/workflows/release.yml) + workflow (run id `25612737089`). +- 21:53–22:0X UTC — Release workflow steps: + - **Build Package** ✅ (wheel + sdist + SBOM CycloneDX) + - **Publish to TestPyPI** ✅ + - **Smoke Test (TestPyPI)** ✅ (install + import + CLI verify + + export verify + base + cli-extra install paths) + - **Publish to PyPI** ✅ — `dppvalidator==0.5.0` live + - **Create GitHub Release** (in progress at status-block edit) + - **Verify PyPI Installation** (queued) + - **SLSA Provenance** (queued) +- 22:0X UTC — `main` merged back into `develop` (merge commit + `a1ed6a6`); both branches in sync, gitflow finalised. + +**Final release deliverables:** + +- PyPI: +- TestPyPI: +- GitHub Release with attached wheel + sdist + SBOM (CycloneDX + JSON, 365-day retention) + SLSA-3 provenance. + +**Carried into Phase 10 (`0.6.0` Stable):** + +- Phase 8.9 drift items D3–D20, D25–D27 (Tier 2/3/4/5/6) — see + Phase 10 task list above for the surgical reconciliation plan + (10.9–10.15). +- Deprecation warnings activated in 0.5.0 (bare-string + `SCHEMA_REGISTRY[]`, `is_dpp_document()`, `EUDPP_CONTEXT_URL`) + become hard removals in 0.6.0. +- Alignment guard test's `EXPECTED_DRIFT` baseline shrinks + progressively as 10.9–10.13 close items, then flips from + drift-watch to strict-fail in 10.15. **Refactors and additions:** From 745a49cfc7ab51a25169745288860c7991d2c471 Mon Sep 17 00:00:00 2001 From: Matthias Brenninkmeijer Date: Sun, 10 May 2026 00:59:47 +0200 Subject: [PATCH 3/5] ci(smoke): wire scripts/smoke_test.py into release.yml + add Phase 9 contract checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends scripts/smoke_test.py with 5 new sections (§18–22) covering the 0.5.0 user-visible contracts, then invokes the full ~109- assertion suite as a deeper second-pass gate in the TestPyPI smoke-test job. The lightweight import + CLI verification steps already in release.yml stay as the first-pass gate; this addition catches regressions on the *installed wheel* surface that the existing in-repo pytest can't see. New sections in scripts/smoke_test.py (12 new assertions): - §18 — CLI six-code exit surface (Phase 6 task 6.7): asserts exits 0/1/3/4/5 across valid / schema-violating / DET001 family-mismatch / migrate-blocking-warnings / IO-error inputs. Also confirms the migrate sidecar warnings file is written on the blocking-warnings path. - §19 — D1 (Phase 9.7) BitstringStatusListEntry.statusListIndex int coercion: numeric string '12345' coerces to int, negative values rejected by ge=0, round-trip emits int (not string). - §20 — D2 (Phase 9.8) PartyRoleEnum gradient + PRT001: asserts SCHEMA_STRICT_ROLES has 6 values and PartyRoleEnum has 20; PRT001 emits as info by default and upgrades to error under ValidationEngine(strict_role_enum=True). - §21 — Phase 9.4 deprecation warnings: is_dpp_document() emits one DeprecationWarning suggesting looks_like_dpp; SCHEMA_REGISTRY[version] emits one per __getitem__ suggesting SCHEMA_REGISTRY_BY_FAMILY; the recommended tuple-keyed API is warning-free. - §22 — Cross-family round-trip: UNTP→CIRPASS via to_cirpass_1_3 emits at least one MAP00X warning code; reverse shim back to UNTP via to_untp_0_7 produces a payload that re-validates valid=True against the v0.7 schema. CI wiring (.github/workflows/release.yml): - New release.yml step in the smoke-test job runs `python scripts/smoke_test.py` against the TestPyPI install, with DPP_BIN/PY_BIN pointing at the just-installed binary (the script already supports both env-var overrides). Runs after the existing 8 lightweight steps (import + --version + exit-1 + export + [cli] install + rich import + real-fixture validate) — those stay as the cheap first-pass gate. - Tag-triggered: fires on `push: tags: v*`, blocks PyPI publish if any of the ~109 assertions fail. Local verification: PASSED 108 passed / 0 failed / 1 skipped (109 total) — the 1 skip is the example plugin, gated as before. Full pytest suite 2525 passed / 36 skipped (no test deltas — the smoke script changes are non-test source). Ruff clean, format clean, ty clean, mkdocs strict clean. Carried into the next release cycle: scripts/smoke_test.py is now the canonical "user-visible-surface" coverage; new public contracts added in future phases should land an §N section here too so the release-gate catches regressions on the installed wheel. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 19 +++ scripts/smoke_test.py | 279 ++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f5b522..b5f7aba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,6 +138,25 @@ jobs: - name: Validate real fixture run: dppvalidator validate tests/fixtures/valid/minimal_dpp.json + # Full functional smoke against the TestPyPI-installed wheel. + # ``scripts/smoke_test.py`` ships ~22 sections (~109 assertions) + # across CLI, Python API, compat shim, exporter, plugins, + # manifest integrity, doctor, content-negotiation, and the + # Phase 9 (0.5.0) contracts (six-code exit surface, D1 + # statusListIndex int coercion, D2 PartyRoleEnum gradient + + # PRT001 advisory, deprecation warnings, UNTP↔CIRPASS round- + # trip). DPP_BIN/PY_BIN point the script at the just-installed + # binary instead of a dev .venv. This is the deeper second- + # pass gate that runs after the lightweight import + CLI + # smoke checks above. + - name: Run full smoke-test suite against TestPyPI install + run: | + export DPP_BIN="$(which dppvalidator)" + export PY_BIN="$(which python)" + echo "DPP_BIN=$DPP_BIN" + echo "PY_BIN=$PY_BIN" + python scripts/smoke_test.py + publish-pypi: name: Publish to PyPI runs-on: ubuntu-latest diff --git a/scripts/smoke_test.py b/scripts/smoke_test.py index 91d0a39..8726e3e 100644 --- a/scripts/smoke_test.py +++ b/scripts/smoke_test.py @@ -856,6 +856,276 @@ def section_content_negotiation(s: Smoke) -> None: s.skip("context content cross-check", f"unavailable: {exc}") +# --------------------------------------------------------------------------- +# Phase 9 (0.5.0) — user-visible contract checks +# --------------------------------------------------------------------------- +# +# The five sections below pin the Phase 9 contracts on the *installed* +# wheel so a regression in any of them blocks a release. They run on +# both the local dev env and the TestPyPI smoke job in release.yml +# (see DPP_BIN env var). Each is small and self-contained. + + +def section_exit_codes(s: Smoke) -> None: + s.section("18. CLI — six-code exit surface (Phase 6 task 6.7)") + v06 = FIXTURES / "valid" / "untp-dpp-instance-0.6.1.json" + v07 = FIXTURES / "valid" / "untp-dpp-instance-0.7.0.json" + if not v06.is_file() or not v07.is_file(): + s.skip("six-code exit surface", "fixtures unavailable") + return + + # Exit 0 — valid v0.7 fixture + r = s.cli(["validate", str(v07)]) + s.assert_( + "exit 0 (EXIT_VALID) on a valid v0.7.0 fixture", + r.returncode == 0, + r.stderr or r.stdout, + ) + + # Exit 1 — schema-violating junk payload + with tempfile.TemporaryDirectory() as tmpdir: + junk = Path(tmpdir) / "junk.json" + junk.write_text( + '{"id":"https://x.com","issuer":{"id":"https://x.com","name":"T"}}', + encoding="utf-8", + ) + r = s.cli(["validate", str(junk)]) + s.assert_( + "exit 1 (EXIT_INVALID) on a schema-violating payload", + r.returncode == 1, + r.stderr or r.stdout, + ) + + # Exit 3 — DET001 family mismatch (--target cirpass on a UNTP fixture) + r = s.cli(["validate", "--target", "cirpass", str(v07)]) + s.assert_( + "exit 3 (EXIT_FAMILY_MISMATCH) on --target/payload contradiction", + r.returncode == 3, + r.stderr or r.stdout, + ) + s.assert_( + "DET001 surfaced in stderr/stdout for the family mismatch", + "DET001" in (r.stderr + r.stdout), + r.stderr or r.stdout, + ) + + # Exit 4 — migrate without --accept-warnings (blocking warnings) + with tempfile.TemporaryDirectory() as tmpdir: + out = Path(tmpdir) / "blocked.json" + r = s.cli(["migrate", str(v06), "-o", str(out)]) + s.assert_( + "exit 4 (EXIT_BLOCKING_WARNINGS) on migrate w/o --accept-warnings", + r.returncode == 4, + r.stderr or r.stdout, + ) + sidecar = out.with_suffix(out.suffix + ".warnings.json") + s.assert_( + "sidecar warnings file written even when main output is blocked", + sidecar.is_file(), + f"expected {sidecar} to exist; tmpdir contents: {list(Path(tmpdir).iterdir())}", + ) + + # Exit 5 — IO error (file not found) + r = s.cli(["validate", "/tmp/_dppvalidator_smoke_does_not_exist.json"]) + s.assert_( + "exit 5 (EXIT_IO_ERROR) on a missing input file", + r.returncode == 5, + r.stderr or r.stdout, + ) + + # Exit 5 — IO error (invalid JSON) + with tempfile.TemporaryDirectory() as tmpdir: + bad = Path(tmpdir) / "bad.json" + bad.write_text("{not valid json", encoding="utf-8") + r = s.cli(["validate", str(bad)]) + s.assert_( + "exit 5 (EXIT_IO_ERROR) on syntactically-invalid JSON", + r.returncode == 5, + r.stderr or r.stdout, + ) + + +def section_status_list_index_coercion(s: Smoke) -> None: + s.section("19. D1 (Phase 9.7) — BitstringStatusListEntry.statusListIndex int coercion") + r = s.py( + """ + from dppvalidator.models.v0_7.envelope import BitstringStatusListEntry + # Numeric-string from a v0.6 fixture shape — must coerce to int. + e = BitstringStatusListEntry.model_validate({ + 'id': 'https://example.com/sl/1', + 'type': 'BitstringStatusListEntry', + 'statusPurpose': 'revocation', + 'statusListIndex': '12345', + 'statusListCredential': 'https://example.com/sl', + }) + assert e.status_list_index == 12345, e.status_list_index + assert isinstance(e.status_list_index, int), type(e.status_list_index) + # Round-trip — model_dump emits int, not the original string. + dumped = e.model_dump(by_alias=True, exclude_none=True) + assert dumped['statusListIndex'] == 12345, dumped + assert isinstance(dumped['statusListIndex'], int), type(dumped['statusListIndex']) + # Negative integer is rejected (ge=0). + try: + BitstringStatusListEntry.model_validate({ + 'id': 'https://example.com/sl/2', + 'type': 'BitstringStatusListEntry', + 'statusPurpose': 'revocation', + 'statusListIndex': -1, + 'statusListCredential': 'https://example.com/sl', + }) + raise AssertionError('negative statusListIndex should have been rejected') + except Exception: + pass + print('OK') + """ + ) + s.assert_( + "statusListIndex coerces numeric strings to int and rejects negatives", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + +def section_party_role_gradient(s: Smoke) -> None: + s.section("20. D2 (Phase 9.8) — PartyRoleEnum gradient + PRT001 advisory") + v07 = FIXTURES / "valid" / "untp-dpp-instance-0.7.0.json" + if not v07.is_file(): + s.skip("PartyRoleEnum gradient", "v0.7 fixture unavailable") + return + + r = s.py( + f""" + import copy, json + from pathlib import Path + from dppvalidator.models.v0_7.identifiers import PartyRoleEnum, SCHEMA_STRICT_ROLES + from dppvalidator.validators.engine import ValidationEngine + + # Surface invariants — gradient sizes are part of the public contract. + assert len(SCHEMA_STRICT_ROLES) == 6, len(SCHEMA_STRICT_ROLES) + assert len(list(PartyRoleEnum)) == 20, len(list(PartyRoleEnum)) + assert PartyRoleEnum.MANUFACTURER.is_schema_strict() is True + assert PartyRoleEnum.IMPORTER.is_schema_strict() is False + + # Build a payload with one of the 14 wider PartyRole values. + payload = json.loads(Path({json.dumps(str(v07))}).read_text(encoding='utf-8')) + related = payload.get('credentialSubject', {{}}).setdefault('relatedParty', []) + wider = {{'type': ['PartyRole'], 'role': 'importer', + 'party': {{'type': ['Party'], 'id': 'did:web:imp.example.com', 'name': 'Imp Co'}}}} + if related: + related[0] = wider + else: + related.append(wider) + + # Default mode: PRT001 fires as info (informational). + r_default = ValidationEngine(schema_version='0.7.0').validate(payload) + prt_info = [i for i in (r_default.info or []) if i.code == 'PRT001'] + assert len(prt_info) == 1, prt_info + assert all(e.code != 'PRT001' for e in (r_default.errors or [])), r_default.errors + + # Strict mode: same payload, PRT001 promoted to error; valid flips to False. + r_strict = ValidationEngine(schema_version='0.7.0', strict_role_enum=True).validate(payload) + prt_err = [e for e in (r_strict.errors or []) if e.code == 'PRT001'] + assert len(prt_err) == 1, prt_err + assert prt_err[0].severity == 'error', prt_err[0].severity + assert all(i.code != 'PRT001' for i in (r_strict.info or [])), r_strict.info + + print('OK') + """ + ) + s.assert_( + "PRT001 emits as info by default, upgrades to error under strict_role_enum=True", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + +def section_deprecation_warnings(s: Smoke) -> None: + s.section("21. Phase 9.4 — deprecation warnings activated for 0.5.0") + r = s.py( + """ + import warnings + + # is_dpp_document — Phase 2 alias deprecated in 0.5.0. + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter('always') + from dppvalidator.validators.detection import is_dpp_document + is_dpp_document({'type': ['DigitalProductPassport']}) + ddep = [w for w in captured if issubclass(w.category, DeprecationWarning)] + assert len(ddep) == 1, [str(w.message) for w in ddep] + assert 'looks_like_dpp' in str(ddep[0].message), str(ddep[0].message) + + # SCHEMA_REGISTRY[version] bare-string lookup — deprecated in 0.5.0. + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter('always') + from dppvalidator.schemas.registry import SCHEMA_REGISTRY + _ = SCHEMA_REGISTRY['0.7.0'] + _ = SCHEMA_REGISTRY['0.6.1'] + sdep = [w for w in captured if issubclass(w.category, DeprecationWarning)] + assert len(sdep) == 2, [str(w.message) for w in sdep] + assert 'SCHEMA_REGISTRY_BY_FAMILY' in str(sdep[0].message), str(sdep[0].message) + + # Recommended replacement — must NOT warn. + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter('always') + from dppvalidator.schemas.registry import SCHEMA_REGISTRY_BY_FAMILY, SchemaFamily + _ = SCHEMA_REGISTRY_BY_FAMILY[(SchemaFamily.UNTP, '0.7.0')] + clean = [w for w in captured if issubclass(w.category, DeprecationWarning)] + assert clean == [], [str(w.message) for w in clean] + + print('OK') + """ + ) + s.assert_( + "is_dpp_document() and SCHEMA_REGISTRY[] each emit DeprecationWarning", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + +def section_cross_family_round_trip(s: Smoke) -> None: + s.section("22. Cross-family round-trip — UNTP ↔ CIRPASS via Python compat") + v07 = FIXTURES / "valid" / "untp-dpp-instance-0.7.0.json" + if not v07.is_file(): + s.skip("cross-family round-trip", "v0.7 fixture unavailable") + return + + r = s.py( + f""" + import json + from pathlib import Path + from dppvalidator.compat import to_cirpass_1_3, to_untp_0_7 + from dppvalidator.validators.engine import ValidationEngine + + untp = json.loads(Path({json.dumps(str(v07))}).read_text(encoding='utf-8')) + + # Forward shim — UNTP 0.7 → CIRPASS 1.3. + cirpass, fwd_warnings = to_cirpass_1_3(untp) + assert isinstance(cirpass, dict), type(cirpass) + assert 'dppIdentifier' in cirpass and 'product' in cirpass and 'issuedAt' in cirpass, sorted(cirpass.keys()) + fwd_codes = {{w.code for w in fwd_warnings}} + # The shim is documented to emit MAP00X warnings on lossy projections. + assert fwd_codes & {{'MAP001', 'MAP002', 'MAP003', 'MAP004', 'MAP005'}}, fwd_codes + + # Reverse shim — round-trip back to UNTP. + back, rev_warnings = to_untp_0_7(cirpass) + assert isinstance(back, dict), type(back) + assert '@context' in back and 'credentialSubject' in back, sorted(back.keys()) + + # Round-tripped UNTP must validate cleanly against the v0.7 schema. + r_back = ValidationEngine(schema_version='0.7.0').validate(back) + assert r_back.valid, [e.message for e in r_back.errors[:3]] + assert r_back.schema_version == '0.7.0', r_back.schema_version + + print('OK') + """ + ) + s.assert_( + "UNTP→CIRPASS→UNTP round-trip emits MAP-warnings and re-validates", + r.returncode == 0 and "OK" in r.stdout, + r.stderr or r.stdout, + ) + + # --------------------------------------------------------------------------- # Orchestrator # --------------------------------------------------------------------------- @@ -889,6 +1159,15 @@ def main() -> int: section_doctor(s) section_content_negotiation(s) + # Phase 9 (0.5.0) — pin user-visible 0.5.0 contracts on the + # installed wheel. Run last because they're the freshest layer + # and any flake here is most actionable. + section_exit_codes(s) + section_status_list_index_coercion(s) + section_party_role_gradient(s) + section_deprecation_warnings(s) + section_cross_family_round_trip(s) + return s.report() From 8c51370a97e48997d4140935d4f185f709044cc9 Mon Sep 17 00:00:00 2001 From: Matthias Brenninkmeijer Date: Sun, 10 May 2026 12:41:43 +0200 Subject: [PATCH 4/5] Better documentation --- .github/workflows/ci.yml | 7 + .github/workflows/docs.yml | 5 +- .pre-commit-config.yaml | 11 + AGENTS.md | 21 +- CHANGELOG.md | 12 +- README.md | 69 +- docs/adr/0006-validation-layer-taxonomy.md | 138 ++ docs/changelog.md | 82 +- docs/concepts/untp-versions.md | 10 +- docs/concepts/validation-layers.md | 109 +- docs/dpp/dpp.json | 1164 ----------------- docs/dpp/jsonschema.json | 1341 -------------------- docs/dpp_validator_description.md | 13 - docs/errors/DET001.md | 49 + docs/errors/MAP001.md | 45 + docs/errors/MAP002.md | 44 + docs/errors/MAP003.md | 43 + docs/errors/MAP004.md | 45 + docs/errors/MAP005.md | 43 + docs/faq.md | 52 +- docs/guides/cli-usage.md | 26 +- docs/guides/migration-0-6-to-0-7.md | 2 +- docs/index.md | 2 +- docs/llms-ctx.txt | 129 +- docs/llms.txt | 19 +- docs/plans/DOCS_COHERENCE_PLAN.md | 467 +++++++ docs/plans/GROWTH_PLAN.md | 678 ++++++++++ docs/reference/api/validators.md | 19 +- docs/uv/uv.md | 85 -- llms-ctx.txt | 147 +-- llms.txt | 42 +- mkdocs.yml | 24 +- tests/unit/test_doc_error_code_coverage.py | 155 +++ tools/check_doc_default_version.py | 161 +++ 34 files changed, 2230 insertions(+), 3029 deletions(-) create mode 100644 docs/adr/0006-validation-layer-taxonomy.md delete mode 100644 docs/dpp/dpp.json delete mode 100644 docs/dpp/jsonschema.json delete mode 100644 docs/dpp_validator_description.md create mode 100644 docs/errors/DET001.md create mode 100644 docs/errors/MAP001.md create mode 100644 docs/errors/MAP002.md create mode 100644 docs/errors/MAP003.md create mode 100644 docs/errors/MAP004.md create mode 100644 docs/errors/MAP005.md create mode 100644 docs/plans/DOCS_COHERENCE_PLAN.md create mode 100644 docs/plans/GROWTH_PLAN.md delete mode 100644 docs/uv/uv.md mode change 100644 => 120000 llms-ctx.txt mode change 100644 => 120000 llms.txt create mode 100644 tests/unit/test_doc_error_code_coverage.py create mode 100644 tools/check_doc_default_version.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0417155..c37562b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,13 @@ jobs: - name: Check error documentation run: uv run python scripts/check_error_docs.py + # Phase 7 of the Docs Coherence Plan — guard the "default UNTP + # version" claim in user-facing docs against drifting away from + # DEFAULT_VERSIONS[SchemaFamily.UNTP]. Same script runs in + # .pre-commit-config.yaml. + - name: Check docs default-version coherence + run: uv run python tools/check_doc_default_version.py + # License scanning - ensure all dependencies have compatible licenses - name: Check dependency licenses run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f8dfa75..97c1839 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,5 +21,8 @@ jobs: - name: Install dependencies run: uv sync --group docs - - name: Build and deploy docs + - name: Build docs (strict) + run: uv run mkdocs build --strict + + - name: Deploy docs run: uv run mkdocs gh-deploy --force diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1b6d97..7818cf1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,3 +93,14 @@ repos: language: system pass_filenames: false stages: [ pre-push ] + # Phase 7 of the Docs Coherence Plan — guard against the + # "default UNTP version" claim drifting away from the registry. + # Runs against user-facing docs (README, AGENTS.md, mkdocs pages, + # llms*.txt). See tools/check_doc_default_version.py for the + # exempt list and detection algorithm. + - id: check-doc-default-version + name: check-doc-default-version + entry: uv run python tools/check_doc_default_version.py + language: system + pass_filenames: false + files: ^(README\.md|AGENTS\.md|CLAUDE\.md|docs/(?!plans/|adr/0006).*\.(md|txt)|src/dppvalidator/schemas/registry\.py)$ diff --git a/AGENTS.md b/AGENTS.md index 7574993..f4927ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,9 @@ - **ty** (Astral) for type checking - **pytest** for testing - **GitHub Actions** for CI/CD +- Optional extras: `[cli]` (rich CLI formatting), `[rdf]` (SHACL via + `rdflib` + `pyshacl`), `[all]` (both). SHACL is opt-in to keep the + default install footprint minimal. ## Directory Structure @@ -21,12 +24,14 @@ src/dppvalidator/ # Main package ├── models/ # Pydantic models for DPP entities │ ├── v0_6/ # UNTP 0.6.x models (ProductPassport envelope) │ ├── v0_7/ # UNTP 0.7.0 models (Product as credentialSubject) +│ ├── cirpass/v1_3/ # CIRPASS DPP reference structure 1.3.0 │ └── … # Top-level shims re-export v0.6 for back-compat -├── validators/ # Validation logic (per-version dispatch) -│ ├── rules/v0_6/ # Semantic rules — v0.6 -│ ├── rules/v0_7/ # Semantic rules — v0.7 +├── validators/ # Validation logic (per-version + per-family dispatch) +│ ├── rules/v0_6/ # Semantic rules — UNTP 0.6 +│ ├── rules/v0_7/ # Semantic rules — UNTP 0.7 +│ ├── rules/cirpass_v1_3/ # Quality rules — CIRPASS 1.3 (CQ*) │ └── … -├── compat/ # Cross-version compat shims +├── compat/ # Cross-version + cross-family shims (UPG*, MAP*) ├── verifier/ # Signature and credential verification ├── exporters/ # JSON-LD and EU DPP export formats ├── schemas/ # JSON Schema loading + version registry @@ -46,14 +51,16 @@ tests/ # Test suite └── upstream/ # SHA-pinned upstream samples ``` -## UNTP version handling +## Schema family + version handling -dppvalidator supports **UNTP DPP 0.6.x and 0.7.0** in the same release. +dppvalidator supports **UNTP DPP 0.6.x and 0.7.0** and the **CIRPASS +DPP reference structure 1.3.0** (`SchemaFamily.CIRPASS`) in the same +release. - Version detection: `validators/detection.py` is the only place that decides the version of a payload. - Default version: `schemas.registry.DEFAULT_SCHEMA_VERSION` (currently - `0.6.1`); call `dppvalidator.compat.active_version()` from feature + `0.7.0`); call `dppvalidator.compat.active_version()` from feature code instead of hardcoding the literal. - Adding a new version: see [`.claude/rules/untp-versioning.md`](.claude/rules/untp-versioning.md) diff --git a/CHANGELOG.md b/CHANGELOG.md index e43b5d2..b7c0674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ support alongside UNTP DPP 0.6.x / 0.7.0 and ships the cross-family forward / reverse shims, the EUDPP v1.9.x ontology rebase, two pilot profiles (Textile v2 built-in, Tyres GPL plugin), and a six-code CLI exit surface. The migration plan is at -[`docs/plans/CIRPASS_2_MIGRATION.md`](docs/plans/CIRPASS_2_MIGRATION.md); +[`docs/plans/CIRPASS_2_MIGRATION.md`](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/CIRPASS_2_MIGRATION.md); each phase has its own implementation log there. **Status: Preview / unstable.** The Pydantic v0.7 model layer has @@ -39,7 +39,7 @@ correctness check. `src/dppvalidator/vocabularies/data/ontologies/`; 6 fresh manifest entries, every IRI rebased onto the canonical `https://w3id.org/eudpp#` fragment namespace per - [ADR 0002](docs/adr/0002-canonical-eudpp-iri.md). + [ADR 0002](https://github.com/artiso-ai/dppvalidator/blob/main/docs/adr/0002-canonical-eudpp-iri.md). - **CIRPASS reference Pydantic models** at `dppvalidator.models.cirpass.v1_3.*` (20 classes — Actor, ActorRoleAssignment, ConnectorRelation, Material, LifeCycleAssessment, @@ -140,7 +140,7 @@ correctness check. The Pydantic v0.7 model layer has documented drift from the upstream JSON Schema, catalogued as drift items D3–D27 in -[Phase 8.9 of the migration plan](docs/plans/CIRPASS_2_MIGRATION.md). +[Phase 8.9 of the migration plan](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/CIRPASS_2_MIGRATION.md). Schema validation (Layer 1) catches every contract violation; the drift is confined to the Python API ergonomics layer and will be fully reconciled in 0.6.0 (Phase 10 tasks 10.9–10.15). Specifically: @@ -198,7 +198,7 @@ fully reconciled in 0.6.0 (Phase 10 tasks 10.9–10.15). Specifically: This release adds first-class support for **UNTP DPP 0.7.0** alongside the existing 0.6.x. Both wire formats coexist and are auto-detected from `@context` / `$schema` URLs. The plan is captured in -[`docs/plans/UNTP_0.7.0_MIGRATION.md`](docs/plans/UNTP_0.7.0_MIGRATION.md); +[`docs/plans/UNTP_0.7.0_MIGRATION.md`](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/UNTP_0.7.0_MIGRATION.md); each phase has its own implementation log there. ### Added @@ -249,8 +249,8 @@ each phase has its own implementation log there. `MANIFEST.json` now carry both an SHA-pinned `source_url` and a human-friendly `production_url` (e.g. `untp.unece.org`). - **Documentation**. New - [`docs/concepts/untp-versions.md`](docs/concepts/untp-versions.md) - and [`docs/guides/migration-0-6-to-0-7.md`](docs/guides/migration-0-6-to-0-7.md); + [`docs/concepts/untp-versions.md`](https://github.com/artiso-ai/dppvalidator/blob/main/docs/concepts/untp-versions.md) + and [`docs/guides/migration-0-6-to-0-7.md`](https://github.com/artiso-ai/dppvalidator/blob/main/docs/guides/migration-0-6-to-0-7.md); refreshed schema, JSON-LD, validation, CLI, FAQ, and index pages with both v0.6 and v0.7 examples. diff --git a/README.md b/README.md index 18832ab..10bb42f 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ______________________________________________________________________ -**dppvalidator** is a Python library for validating [Digital Product Passports (DPP)](https://untp.unece.org/docs/specification/DigitalProductPassport/) according to EU ESPR regulations and [UNTP](https://uncefact.unece.org/) standards. +**dppvalidator** is a Python library for validating [Digital Product Passports (DPP)](https://untp.unece.org/docs/specification/DigitalProductPassport/) according to EU ESPR regulations, the [UNTP DPP](https://uncefact.unece.org/) specification, and the [CIRPASS DPP](https://dpp.vocabulary-hub.eu/specifications) reference structure. **Starting 2027, every textile and apparel product sold in the EU must have a Digital Product Passport.** This library ensures your DPP data is compliant *before* it hits production — saving fashion brands from costly compliance failures and enabling seamless integration with the circular economy. @@ -188,11 +188,11 @@ shape; pin explicitly with `--target` (family) and `--schema-version` ### UNTP DPP -| Version | Status | Default? | Wire shape | -| --------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **0.6.0** | Supported (legacy) | no | `credentialSubject` is `ProductPassport` wrapping `Product`. | -| **0.6.1** | Default | **yes** | Same shape as 0.6.0; current `DEFAULT_SCHEMA_VERSION`. | -| **0.7.0** | Fully supported | no | `credentialSubject` IS the `Product` directly. New required fields: `name` (envelope), `idScheme`, `idGranularity`, `productCategory`, `producedAtFacility`, `countryOfProduction`. | +| Version | Status | Default? | Wire shape | +| --------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **0.6.0** | Supported (legacy) | no | `credentialSubject` is `ProductPassport` wrapping `Product`. | +| **0.6.1** | Supported (legacy) | no | Same shape as 0.6.0. | +| **0.7.0** | Default | **yes** | `credentialSubject` IS the `Product` directly. New required fields: `name` (envelope), `idScheme`, `idGranularity`, `productCategory`, `producedAtFacility`, `countryOfProduction`; current `DEFAULT_SCHEMA_VERSION`. | ### CIRPASS DPP reference structure @@ -258,28 +258,36 @@ flowchart TD A[/"📄 Input Data (JSON)"/] end - subgraph Layer0["Layer 0: Schema Detection"] - A0["Auto-detect schema version
from $schema, @context, type"] + subgraph Layer0["Layer 0: Detection"] + A0["Auto-detect family + version
from $schema, @context, type"] end - subgraph Layer1["Layer 1: Schema Validation"] + subgraph Layer1["Layer 1: Schema"] B["JSON Schema Draft 2020-12
Required fields, types, formats"] end - subgraph Layer2["Layer 2: Model Validation"] + subgraph Layer2["Layer 2: Model"] C["Pydantic v2 Models
Type coercion, URL validation"] end - subgraph Layer3["Layer 3: JSON-LD Semantic"] + subgraph Layer3["Layer 3: JSON-LD"] C2["PyLD Expansion
Context resolution, term validation"] end - subgraph Layer4["Layer 4: Business Logic"] - D["Business Rules & Vocabularies
ISO codes, date logic, GTIN checksums"] + subgraph Layer4["Layer 4: Semantic"] + D["Business rules
Date logic, GTIN checksums, sums"] end - subgraph Layer5["Layer 5: Cryptographic"] - E["VC Signature Verification
DID resolution, Ed25519/ECDSA"] + subgraph Layer5["Layer 5: Vocabulary"] + D2["External code lists
ISO 3166, UNECE Rec 20/46, HS"] + end + + subgraph Layer6["Layer 6: Plugin"] + D3["Per-pack rules
e.g. textile TXT, CIRPASS CQ, tyre TYR"] + end + + subgraph Layer7["Layer 7: Signature (reserved)"] + E["VC signature verification
DID resolution, Ed25519/ECDSA"] end subgraph Output @@ -289,12 +297,21 @@ flowchart TD A --> A0 A0 --> B B -->|"SCH001-SCH099"| C - C -->|"MOD001-MOD099"| C2 + C -->|"MDL001-MDL099"| C2 C2 -->|"JLD001-JLD099"| D - D -->|"SEM001-SEM099"| E - E -->|"SIG001-SIG099"| F + D -->|"SEM001-SEM099"| D2 + D2 -->|"VOC001-VOC099"| D3 + D3 -->|"plugin-specific"| E + E -->|"reserved"| F ``` +> Layer 7 codes (`SIG001`–`SIG099`) are reserved for the future +> structured-code migration; the verifier currently surfaces untyped +> error strings via `VerificationResult.errors`. See +> [ADR 0006](docs/adr/0006-validation-layer-taxonomy.md) for the +> canonical layer + prefix table, including non-layer codes +> (`PRS`, `DET`, `VER`, `UPG`, `MAP`, `PRT`). + ### Selective Layer Validation ```python @@ -394,14 +411,14 @@ jsonld = exporter.export(passport) 📚 **Full documentation:** [artiso-ai.github.io/dppvalidator](https://artiso-ai.github.io/dppvalidator/) -| Guide | Description | -| -------------------------------------------------------------------------------------------------- | ----------------------------------------- | -| [Installation](https://artiso-ai.github.io/dppvalidator/getting-started/installation/) | Setup and CLI extras | -| [Quick Start](https://artiso-ai.github.io/dppvalidator/getting-started/quickstart/) | Get started in 5 minutes | -| [CLI Reference](https://artiso-ai.github.io/dppvalidator/guides/cli-usage/) | Command-line interface | -| [Validation Layers](https://artiso-ai.github.io/dppvalidator/concepts/validation-layers/) | Understanding the five-layer architecture | -| [CIRPASS-2 Integration](https://artiso-ai.github.io/dppvalidator/concepts/cirpass-implementation/) | EU DPP ontology alignment | -| [API Reference](https://artiso-ai.github.io/dppvalidator/reference/api/validators/) | Complete Python API | +| Guide | Description | +| -------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| [Installation](https://artiso-ai.github.io/dppvalidator/getting-started/installation/) | Setup and CLI extras | +| [Quick Start](https://artiso-ai.github.io/dppvalidator/getting-started/quickstart/) | Get started in 5 minutes | +| [CLI Reference](https://artiso-ai.github.io/dppvalidator/guides/cli-usage/) | Command-line interface | +| [Validation Layers](https://artiso-ai.github.io/dppvalidator/concepts/validation-layers/) | Understanding the seven-layer architecture | +| [CIRPASS-2 Integration](https://artiso-ai.github.io/dppvalidator/concepts/cirpass-implementation/) | EU DPP ontology alignment | +| [API Reference](https://artiso-ai.github.io/dppvalidator/reference/api/validators/) | Complete Python API | ## Built for Fashion & Textiles diff --git a/docs/adr/0006-validation-layer-taxonomy.md b/docs/adr/0006-validation-layer-taxonomy.md new file mode 100644 index 0000000..0025a5b --- /dev/null +++ b/docs/adr/0006-validation-layer-taxonomy.md @@ -0,0 +1,138 @@ +# ADR 0006 — Canonical validation-layer taxonomy + +**Status:** Accepted (Docs Coherence Plan, 2026-05-10). + +## Context + +By the 0.5.0 (Preview) cut, dppvalidator's documentation surface had +drifted into **four** different taxonomies for the same engine, each +authoritative on its own page: + +| Source | Layer count | Names | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- | +| [`README.md`](https://github.com/artiso-ai/dppvalidator/blob/main/README.md), [`docs/index.md`](../index.md) | 7 | Schema, Model, Semantic, JSON-LD, Vocabulary, Plugin, Signature | +| [`README.md`](https://github.com/artiso-ai/dppvalidator/blob/main/README.md) mermaid + [`docs/concepts/validation-layers.md`](../concepts/validation-layers.md) body | 6 | Detection, Schema, Model, JSON-LD, Business, Cryptographic | +| [`docs/llms-ctx.txt`](../llms-ctx.txt) | 8 | Detection, Schema, Model, Semantic, JSON-LD, Vocabulary, Plugin, Signature | +| [`docs/faq.md`](../faq.md), [`llms.txt`](https://github.com/artiso-ai/dppvalidator/blob/main/llms.txt), [`docs/plans/IMPLEMENTATION_PLAN.md`](../plans/IMPLEMENTATION_PLAN.md), [`README.md`](https://github.com/artiso-ai/dppvalidator/blob/main/README.md) §Documentation | 5 | Schema, Model, Semantic, JSON-LD, Cryptographic | + +Each downstream phase of the Docs Coherence Plan rewrites layer counts +and error-code prefixes; the rewrites cannot proceed until one +taxonomy is canonical. + +## Decision + +The engine emits seven non-detection layers today, plus one detection +phase that produces no error codes of its own (it routes — see +[`src/dppvalidator/validators/detection.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/detection.py)). +We adopt the **"seven layers + detection"** framing and pin the table +below as the single canonical taxonomy. Every user-facing source +(README, mkdocs concept pages, FAQ, error-code reference, +`llms*.txt`, `AGENTS.md`) must use these names, this ordering, and +these prefixes verbatim. + +| Layer | Name | Error-code prefix(es) | Module | +| ------- | ---------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Layer 0 | Detection | _no prefix — routing only_ | [`validators/detection.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/detection.py) | +| Layer 1 | Schema | `SCH001`–`SCH099` | [`validators/schema.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/schema.py) | +| Layer 2 | Model | `MDL001`–`MDL099` | [`validators/model.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/model.py) | +| Layer 3 | JSON-LD | `JLD001`–`JLD099` | [`validators/jsonld_semantic.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/jsonld_semantic.py) | +| Layer 4 | Semantic | `SEM001`–`SEM099` | [`validators/semantic.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/semantic.py) | +| Layer 5 | Vocabulary | `VOC001`–`VOC099` | [`vocabularies/`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/vocabularies/) | +| Layer 6 | Plugin | per-plugin (e.g. `TXT001`–`TXT099` textiles, `CQ001`–`CQ099` CIRPASS quality, `TYR001`–`TYR099` tyres) | [`plugins/`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/plugins/) | +| Layer 7 | Signature | _Reserved_ — `SIG001`–`SIG099` reserved; no structured codes shipped (verifier surfaces untyped error strings today) | [`verifier/`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/verifier/) | + +### Non-layer (cross-cutting) error codes + +Several prefixes do not belong to a layer because they fire outside +the layered pipeline (parsing, version-routing, family-routing, +migration shims, advisory rules). These are documented as **non-layer +codes** so the layer table stays pure: + +| Prefix | Surface | Module | +| ------ | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `PRS` | Input parsing (file IO, JSON syntax) — pre-layer-1 | [`cli/commands/watch.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/cli/commands/watch.py) | +| `DET` | Family-mismatch surfaced when `--target` contradicts detection | [`validators/detection.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/detection.py) | +| `VER` | Version-mismatch when `--schema-version` contradicts detection | [`validators/`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/) | +| `UPG` | UNTP 0.6 → 0.7 upgrade-shim warnings | [`compat/upgrade_0_6_to_0_7.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/upgrade_0_6_to_0_7.py) | +| `MAP` | Cross-family migration-shim warnings (UNTP ↔ CIRPASS) | [`compat/_mapping_codes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_mapping_codes.py) | +| `PRT` | Advisory rules (e.g. role-enum strictness) | [`models/v0_7/identifiers.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/models/v0_7/identifiers.py) | + +`SIG` is **reserved**: an audit during the Docs Coherence Plan +confirmed that the verifier currently returns plain +`errors: list[str]` (see +[`src/dppvalidator/verifier/verifier.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/verifier/verifier.py)). +The prefix is held for the future migration to structured codes; until +that work ships, user-facing docs name the layer as "Signature +(reserved)" and **do not** claim emitted `SIG0xx` codes. + +## Why this taxonomy + +1. **Every named layer has an emitter in source today.** The five-layer + framing dropped Vocabulary (real codes — `VOC001`–`VOC005`), Plugin + (real codes — `TXT*`, `CQ*`, `TYR*`), and Detection (real routing + path); the six-layer framing dropped Vocabulary and Plugin; the + eight-layer framing in `llms-ctx.txt` artificially split Schema + detection from the routing layer. The seven-layer-plus-detection + shape is the only one where layer count = emitter count. +1. **Backwards compatible with the 0.5.0 mermaid diagram.** The diagram + in [`README.md`](https://github.com/artiso-ai/dppvalidator/blob/main/README.md) and + [`docs/concepts/validation-layers.md`](../concepts/validation-layers.md) + already labels Detection as Layer 0 and Schema/Model/JSON-LD/Business/Crypto + as Layers 1-5. Phase 2d extends, rather than rewrites, that diagram + with Vocabulary (5), Plugin (6), Signature (7). +1. **One source of truth for prefixes.** Phase 7 of the Docs Coherence + Plan adds a CI guard that walks `src/` for every `\b(SCH|MDL|JLD|SEM|VOC|PRS|PRT|VER|UPG|MAP|DET|TXT|CQ|TYR)\d{3}\b` + literal and asserts coverage in `mkdocs.yml` `nav.Errors`. The guard + uses *this ADR* as its allow-list source. + +## Migration + +Phases 1–6 of the Docs Coherence Plan rewrite each user-facing source +to match this ADR. The mechanical replacements are: + +| From | To | +| ------------------------------------------------- | ---------------------------------------------------------------- | +| "five-layer", "Five-Layer", "5 validation layers" | "seven-layer", "Seven-Layer", "7 validation layers" | +| "six-layer" (concept page mermaid) | "seven-layer" (extended diagram) | +| `MOD001`-`MOD099` | `MDL001`-`MDL099` | +| `SIG001`-`SIG099` _claimed as emitted_ | `SIG001`-`SIG099` _reserved; verifier emits string errors today_ | +| `MAP` / `DET` _absent from error-code reference_ | `MAP001`-`MAP005` and `DET001` documented as non-layer codes | + +## Consequences + +**Pros** + +- Single canonical layer count for every audience (humans, agents, CI guards). +- The mermaid diagrams in README and concept pages expand by three + subgraphs (Vocabulary, Plugin, Signature) but the existing five + edge labels are unchanged in slot order — `MOD` → `MDL` is the only + in-place rename. +- Phase 7 CI guards become writable (the test asserts against this ADR). + +**Cons** + +- "Seven-layer" is a slightly more elaborate marketing line than + "five-layer". Mitigation: README hero leads with the dual-spec story + (UNTP + CIRPASS), not the layer count; layer detail lives one click + deeper, in the concept page. +- "Reserved" status for `SIG` could surprise readers who scanned the + pre-0.5.0 docs. Mitigation: the concept page calls out the + reserved-vs-emitted distinction explicitly and links to a tracking + issue for the future structured-code migration. + +## Reversal cost + +Low. The ADR is a documentation contract; reversing means another +mechanical pass through the same files. The only persistent cost is +already-published download URLs to `docs/errors/MDL*.md` (which match +source code, so cannot reasonably revert). + +## See also + +- [Docs Coherence Plan](../plans/DOCS_COHERENCE_PLAN.md) — the rollout + schedule for this ADR. +- [`.claude/rules/untp-versioning.md`](https://github.com/artiso-ai/dppvalidator/blob/main/.claude/rules/untp-versioning.md) — + the analogue for UNTP version literals (Phase 1 default-version + drift uses the same pattern). +- [`docs/concepts/validation-layers.md`](../concepts/validation-layers.md) — + expanded by Phase 2d to render this taxonomy in detail. diff --git a/docs/changelog.md b/docs/changelog.md index 2ca09c2..73ac2fa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,77 +1,7 @@ -# Changelog + -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added - -- **Schema Auto-Detection (Phase 0)**: `ValidationEngine` now defaults to - `schema_version="auto"` - - Detects version from `$schema`, `@context`, or `type` array - - New `detect_schema_version()` and `is_dpp_document()` functions -- **JSON-LD Semantic Validation (Phase 1)**: PyLD-based expansion validation - - `JSONLDValidator` class with `CachingDocumentLoader` for remote contexts - - New error codes: JLD001-JLD004 for context and term validation - - Enable with `ValidationEngine(validate_jsonld=True)` -- **Extended Code Lists (Phase 2)**: UNECE Rec 46, HS codes, GTIN validation - - `validate_gtin()`, `is_valid_material_code()`, `is_valid_hs_code()` - - GS1 Digital Link URL parsing and validation - - 85 UNECE Rec 46 material codes, 156 textile HS codes (Chapters 50-63) - - New rules: VOC003, VOC004, VOC005 -- **Verifiable Credential Verification (Phase 3)**: DID resolution and signatures - - `CredentialVerifier`, `DIDResolver`, `SignatureVerifier` in `verifier/` - - Support for `did:web` and `did:key` resolution - - Ed25519, ES256, ES384 signature algorithms - - New `ValidationResult` fields: `signature_valid`, `issuer_did` - - Enable with `ValidationEngine(verify_signatures=True)` -- **Deep/Recursive Validation (Phase 4)**: Supply chain traversal - - `DeepValidator` with async crawling, rate limiting, retry logic - - Cycle detection for circular references - - New `ValidationEngine.validate_deep()` async method -- `CredentialStatus` model for W3C VC v2 revocation checking -- `credentialStatus` field on `DigitalProductPassport` -- `Scope1`, `Scope2`, `Scope3` values to `OperationalScope` enum (GHG Protocol) -- SHA-256 integrity hash for schema v0.6.1 -- `pytest-asyncio` for native async test support -- `CONTRIBUTING.md` with development guidelines -- Stable error code mapping (`PYDANTIC_ERROR_CODES`) for model validation - -### Changed - -- **Breaking**: `ValidationEngine()` defaults to `schema_version="auto"` -- **Architecture**: Validation expanded from 3 to 5 layers -- Core dependencies: `httpx`, `jsonschema`, `pyld`, `cryptography` now required -- Removed all `HAS_*` conditional checks from source code -- `has_vc_support()` now always returns `True` -- `operational_scope` is now optional in `EmissionsPerformance` -- Replaced bare `except Exception` catches with specific exception types -- Consolidated error system: removed unused `EnhancedValidationError` class -- Version now read from `pyproject.toml` via `importlib.metadata` -- Async validation uses `asyncio.to_thread()` instead of deprecated API - -### Fixed - -- README plugin API example now matches actual `SemanticRule` protocol -- Model error codes are now deterministic (based on Pydantic error type) -- Unused imports and arguments cleaned up across verifier module -- Flaky `test_engine_never_crashes_on_binary` deadline increased to 500ms - -## [0.1.0] - 2026-01-29 - -### Added - -- Initial release -- `Material` model for fiber composition with ISO 2076 codes -- `SupplyChainNode` model for traceability -- `DigitalProductPassport` model following EU ESPR regulations -- JSON-LD export for Semantic Web compatibility -- Unit tests for all models and validators -- GitHub Actions CI/CD pipeline -- Pre-commit hooks with ruff - -[0.1.0]: https://github.com/artiso-ai/dppvalidator/releases/tag/v0.1.0 -[unreleased]: https://github.com/artiso-ai/dppvalidator/compare/v0.1.0...HEAD +--8\<-- "CHANGELOG.md" diff --git a/docs/concepts/untp-versions.md b/docs/concepts/untp-versions.md index 3f3a7a0..c35c249 100644 --- a/docs/concepts/untp-versions.md +++ b/docs/concepts/untp-versions.md @@ -15,8 +15,8 @@ see the [migration guide](../guides/migration-0-6-to-0-7.md). | UNTP DPP version | Default? | Status | Wire shape highlight | | ---------------- | -------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | **0.6.0** | no | Supported (legacy) | Same envelope as 0.6.1; minor schema-only fixes only. | -| **0.6.1** | **yes** | Default — set in `dppvalidator.schemas.registry.DEFAULT_SCHEMA_VERSION` | `credentialSubject` is a `ProductPassport` envelope wrapping `Product`. | -| **0.7.0** | no | Fully supported | `credentialSubject` IS the `Product` directly. No `ProductPassport` envelope. | +| **0.6.1** | no | Supported (legacy) | `credentialSubject` is a `ProductPassport` envelope wrapping `Product`. | +| **0.7.0** | **yes** | Default — set in `dppvalidator.schemas.registry.DEFAULT_SCHEMA_VERSION` | `credentialSubject` IS the `Product` directly. No `ProductPassport` envelope. | Future releases may flip the default. The flip is a separate minor release with deprecation warnings — see *Adding a new version* below. @@ -70,10 +70,10 @@ mixing in pipelines. ### CLI pin ```bash -# Validate as 0.7.0 regardless of the payload's declared version. +# Validate as 0.7.0 (current default; the flag is optional). dppvalidator validate passport.json --schema-version 0.7.0 -# Validate as 0.6.1 (current default; the flag is optional). +# Validate as 0.6.1 (legacy) regardless of the payload's declared version. dppvalidator validate passport.json --schema-version 0.6.1 # List every version the registry knows about. @@ -87,7 +87,7 @@ from dppvalidator.schemas.registry import SCHEMA_REGISTRY, SchemaRegistry reg = SchemaRegistry() print(reg.available_versions) # ['0.6.0', '0.6.1', '0.7.0'] -print(reg.default_version) # '0.6.1' +print(reg.default_version) # '0.7.0' # The SHA-pinned upstream URL the bundled bytes came from. print(reg.get_schema_url("0.7.0")) diff --git a/docs/concepts/validation-layers.md b/docs/concepts/validation-layers.md index 7a3d4a7..49505f2 100644 --- a/docs/concepts/validation-layers.md +++ b/docs/concepts/validation-layers.md @@ -1,6 +1,11 @@ # Seven-Layer Validation -dppvalidator uses a seven-layer validation architecture to ensure Digital Product Passports are structurally correct, type-safe, semantically meaningful, cryptographically verifiable, and supply-chain traceable. +dppvalidator uses a seven-layer validation architecture (plus a Layer 0 +detection phase) to ensure Digital Product Passports are structurally +correct, type-safe, semantically meaningful, cryptographically +verifiable, and supply-chain traceable. The canonical layer + error-code +table is pinned by +[ADR 0006](../adr/0006-validation-layer-taxonomy.md). ## Architecture @@ -10,28 +15,36 @@ flowchart TD A[/"📄 Input Data (JSON)"/] end - subgraph Layer0["Layer 0: Schema Detection"] - A0["Auto-detect schema version
from $schema, @context, type"] + subgraph Layer0["Layer 0: Detection"] + A0["Auto-detect family + version
from $schema, @context, type"] end - subgraph Layer1["Layer 1: Schema Validation"] + subgraph Layer1["Layer 1: Schema"] B["JSON Schema Draft 2020-12
Required fields, types, formats"] end - subgraph Layer2["Layer 2: Model Validation"] + subgraph Layer2["Layer 2: Model"] C["Pydantic v2 Models
Type coercion, URL validation"] end - subgraph Layer3["Layer 3: JSON-LD Semantic"] + subgraph Layer3["Layer 3: JSON-LD"] C2["PyLD Expansion
Context resolution, term validation"] end - subgraph Layer4["Layer 4: Business Logic"] - D["Business Rules & Vocabularies
ISO codes, date logic, GTIN checksums"] + subgraph Layer4["Layer 4: Semantic"] + D["Business rules
Date logic, GTIN checksums, sums"] end - subgraph Layer5["Layer 5: Cryptographic"] - E["VC Signature Verification
DID resolution, Ed25519/ECDSA"] + subgraph Layer5["Layer 5: Vocabulary"] + D2["External code lists
ISO 3166, UNECE Rec 20/46, HS"] + end + + subgraph Layer6["Layer 6: Plugin"] + D3["Per-pack rules
e.g. textile TXT, CIRPASS CQ, tyre TYR"] + end + + subgraph Layer7["Layer 7: Signature (reserved)"] + E["VC signature verification
DID resolution, Ed25519/ECDSA"] end subgraph Output @@ -41,17 +54,21 @@ flowchart TD A --> A0 A0 --> B B -->|"SCH001-SCH099"| C - C -->|"MOD001-MOD099"| C2 + C -->|"MDL001-MDL099"| C2 C2 -->|"JLD001-JLD099"| D - D -->|"SEM001-SEM099"| E - E -->|"SIG001-SIG099"| F + D -->|"SEM001-SEM099"| D2 + D2 -->|"VOC001-VOC099"| D3 + D3 -->|"plugin-specific"| E + E -->|"reserved"| F style Layer0 fill:#fce4ec,stroke:#c2185b style Layer1 fill:#e3f2fd,stroke:#1976d2 style Layer2 fill:#fff3e0,stroke:#f57c00 style Layer3 fill:#e0f7fa,stroke:#0097a7 style Layer4 fill:#e8f5e9,stroke:#388e3c - style Layer5 fill:#fff8e1,stroke:#ffa000 + style Layer5 fill:#dcedc8,stroke:#558b2f + style Layer6 fill:#ede7f6,stroke:#5e35b1 + style Layer7 fill:#fff8e1,stroke:#ffa000 style Output fill:#f3e5f5,stroke:#7b1fa2 ``` @@ -68,7 +85,7 @@ Automatically detects the DPP schema version from the input document. - Modern (0.7.0+): `https://vocabulary.uncefact.org/untp/X.Y.Z/context/` 1. `type` array presence → default version 1. Fallback to `dppvalidator.schemas.registry.DEFAULT_SCHEMA_VERSION` - (currently `0.6.1`) + (currently `0.7.0`) ```python from dppvalidator import ValidationEngine @@ -76,12 +93,12 @@ from dppvalidator import ValidationEngine # Auto-detection (default) engine = ValidationEngine() -# Pin v0.6.1 explicitly. A v0.7.0 payload through this engine fails -# fast with VER001 (version mismatch). -engine = ValidationEngine(schema_version="0.6.1") - -# Pin v0.7.0 explicitly. +# Pin v0.7.0 explicitly (matches the current default; the flag is optional). engine = ValidationEngine(schema_version="0.7.0") + +# Pin v0.6.1 (legacy) explicitly. A v0.7.0 payload through this engine +# fails fast with VER001 (version mismatch). +engine = ValidationEngine(schema_version="0.6.1") ``` The full version-handling story (detection internals, default-version @@ -135,7 +152,7 @@ Validates data against Pydantic models with stricter type checking. - Custom field validators - Model-level validators (cross-field) -**Error codes:** `MOD001` - `MOD099` +**Error codes:** `MDL001` - `MDL099` ## Layer 3: JSON-LD Semantic Validation @@ -160,22 +177,52 @@ engine = ValidationEngine(validate_jsonld=True) engine = ValidationEngine(layers=["schema", "model", "jsonld"]) ``` -## Layer 4: Business Logic Validation +## Layer 4: Semantic Validation -Validates business rules and external vocabulary references. +Validates business rules over the parsed Pydantic shape. **What it checks:** -- Vocabulary values (ISO country codes, UN/CEFACT unit codes) -- Material codes (UNECE Rec 46) -- HS codes for product classification +- Mass-fraction sums (composition arrays sum to 100 %) +- Date relationships (`validFrom < validUntil`) - GTIN checksums (GS1 standard) -- Date relationships (validFrom < validUntil) - Cross-reference consistency +- Hazardous-material safety claims +- Item-level serial-number rules + +**Error codes:** `SEM001` - `SEM099` + +## Layer 5: Vocabulary Validation + +Validates external code-list references against the bundled +controlled-vocabulary tables. + +**What it checks:** + +- ISO 3166-1 alpha-2 country codes +- UN/CEFACT Rec 20 unit codes +- UNECE Rec 46 material codes +- HS codes for product classification + +**Error codes:** `VOC001` - `VOC099` + +## Layer 6: Plugin Validation + +Runs version-aware rules registered by entry-point plugins +(`dppvalidator.plugins`). Every active pack contributes its own +prefix; coverage across packs is enumerated by the plugin registry +at runtime. + +**What it checks:** + +- Pack-specific business rules — e.g. textile MVP 2025-12-04 (`TXT*`), + CIRPASS-2 quality (`CQ*`), tyres GDSO Birth/Collection/Retread + (`TYR*`). -**Error codes:** `SEM001` - `SEM099`, `VOC001` - `VOC099` +**Error codes:** per-plugin (`TXT001`–`TXT099`, `CQ001`–`CQ099`, +`TYR001`–`TYR099`, …). -## Layer 5: Cryptographic Verification +## Layer 7: Signature Verification (reserved) Verifies Verifiable Credential signatures and DID resolution. @@ -185,7 +232,9 @@ Verifies Verifiable Credential signatures and DID resolution. - Signature verification (Ed25519, ES256, ES384) - Proof types (Ed25519Signature2020, DataIntegrityProof, JsonWebSignature2020) -**Error codes:** `SIG001` - `SIG099` +**Error codes:** *Reserved*. The `SIG001`–`SIG099` range is held for +future structured signature-verification codes; today the verifier +surfaces untyped error strings via `VerificationResult.errors`. ```python from dppvalidator import ValidationEngine diff --git a/docs/dpp/dpp.json b/docs/dpp/dpp.json deleted file mode 100644 index 08be0ad..0000000 --- a/docs/dpp/dpp.json +++ /dev/null @@ -1,1164 +0,0 @@ -{ - "@context": { - "untp-dpp": "https://test.uncefact.org/vocabulary/untp/dpp/0/", - "schemaorg": "https://schema.org/", - "untp-core": "https://test.uncefact.org/vocabulary/untp/core/0/", - "geojson": "https://purl.org/geojson/vocab#", - "renderMethodPrefix": "https://w3id.org/vc/render-method#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "@protected": true, - "@version": 1.1, - "DigitalProductPassport": { - "@protected": true, - "@id": "untp-dpp:DigitalProductPassport" - }, - "IdentifierScheme": { - "@protected": true, - "@id": "untp-core:IdentifierScheme", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - } - } - }, - "Classification": { - "@protected": true, - "@id": "untp-core:Classification", - "@context": { - "@protected": true, - "code": { - "@id": "untp-core:code", - "@type": "xsd:string" - }, - "name": { - "@id": "schemaorg:name" - }, - "schemeID": { - "@id": "untp-core:schemeID", - "@type": "xsd:string" - }, - "schemeName": { - "@id": "untp-core:schemeName", - "@type": "xsd:string" - } - } - }, - "Party": { - "@protected": true, - "@id": "untp-core:Party", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - }, - "description": { - "@id": "schemaorg:description" - }, - "registrationCountry": { - "@id": "untp-core:registrationCountry", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/CountryId#" - } - }, - "organisationWebsite": { - "@id": "untp-core:organisationWebsite", - "@type": "xsd:string" - }, - "industryCategory": { - "@id": "untp-core:industryCategory", - "@type": "@id" - }, - "partyAlsoKnownAs": { - "@id": "untp-core:Party", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - } - } - }, - "CredentialIssuer": { - "@protected": true, - "@id": "untp-core:CredentialIssuer", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "issuerAlsoKnownAs": { - "@id": "untp-core:Party", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - } - } - }, - "Facility": { - "@protected": true, - "@id": "untp-core:Facility", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - }, - "description": { - "@id": "schemaorg:description" - }, - "countryOfOperation": { - "@id": "untp-core:countryOfOperation", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/CountryId#" - } - }, - "processCategory": { - "@id": "untp-core:processCategory", - "@type": "@id" - }, - "operatedByParty": { - "@id": "untp-core:Party", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - }, - "facilityAlsoKnownAs": { - "@id": "untp-core:Facility", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - }, - "locationInformation": { - "@protected": true, - "@id": "untp-core:locationInformation", - "@context": { - "@protected": true, - "plusCode": { - "@id": "untp-core:plusCode", - "@type": "xsd:string" - }, - "geoLocation": { - "@protected": true, - "@id": "geojson:geoLocation", - "@context": { - "@protected": true, - "type": { - "@id": "geojson:type", - "@type": "xsd:string" - }, - "coordinates": { - "@protected": true, - "@id": "geojson:coordinates", - "@context": { - "@protected": true, - "data": { - "@id": "geojson:data", - "@type": "xsd:double" - } - } - } - } - }, - "geoBoundary": { - "@protected": true, - "@id": "geojson:geoBoundary", - "@context": { - "@protected": true, - "type": { - "@id": "geojson:type", - "@type": "xsd:string" - }, - "coordinates": { - "@protected": true, - "@id": "geojson:coordinates", - "@context": { - "@protected": true, - "data": { - "@protected": true, - "@id": "geojson:data", - "@context": { - "@protected": true, - "data": { - "@id": "geojson:data", - "@type": "xsd:double" - } - } - } - } - } - } - } - } - }, - "address": { - "@protected": true, - "@id": "untp-core:address", - "@context": { - "@protected": true, - "streetAddress": { - "@id": "untp-core:streetAddress", - "@type": "xsd:string" - }, - "postalCode": { - "@id": "untp-core:postalCode", - "@type": "xsd:string" - }, - "addressLocality": { - "@id": "untp-core:addressLocality", - "@type": "xsd:string" - }, - "addressRegion": { - "@id": "untp-core:addressRegion", - "@type": "xsd:string" - }, - "addressCountry": { - "@id": "untp-core:addressCountry", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/CountryId#" - } - } - } - } - } - }, - "Product": { - "@protected": true, - "@id": "untp-core:Product", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - }, - "batchNumber": { - "@id": "untp-core:batchNumber", - "@type": "xsd:string" - }, - "productImage": { - "@protected": true, - "@id": "untp-core:productImage", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - } - } - }, - "description": { - "@id": "schemaorg:description" - }, - "productCategory": { - "@id": "untp-core:productCategory", - "@type": "@id" - }, - "furtherInformation": { - "@protected": true, - "@id": "untp-core:furtherInformation", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - } - } - }, - "producedByParty": { - "@id": "untp-core:Party", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - }, - "producedAtFacility": { - "@id": "untp-core:Facility", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - }, - "productionDate": { - "@id": "untp-core:productionDate", - "@type": "xsd:string" - }, - "countryOfProduction": { - "@id": "untp-core:countryOfProduction", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/CountryId#" - } - }, - "serialNumber": { - "@id": "untp-core:serialNumber", - "@type": "xsd:string" - }, - "dimensions": { - "@protected": true, - "@id": "untp-core:dimensions", - "@context": { - "@protected": true, - "weight": { - "@protected": true, - "@id": "untp-core:weight", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "length": { - "@protected": true, - "@id": "untp-core:length", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "width": { - "@protected": true, - "@id": "untp-core:width", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "height": { - "@protected": true, - "@id": "untp-core:height", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "volume": { - "@protected": true, - "@id": "untp-core:volume", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - } - } - } - } - }, - "Standard": { - "@protected": true, - "@id": "untp-core:Standard", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "issuingParty": { - "@id": "untp-core:Party", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - }, - "issueDate": { - "@id": "untp-core:issueDate", - "@type": "xsd:string" - } - } - }, - "Regulation": { - "@protected": true, - "@id": "untp-core:Regulation", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "jurisdictionCountry": { - "@id": "untp-core:jurisdictionCountry", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/CountryId#" - } - }, - "administeredBy": { - "@id": "untp-core:Party", - "@type": "@id", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "registeredId": { - "@id": "untp-core:registeredId", - "@type": "xsd:string" - }, - "idScheme": { - "@id": "untp-core:idScheme", - "@type": "@id" - } - } - }, - "effectiveDate": { - "@id": "untp-core:effectiveDate", - "@type": "xsd:string" - } - } - }, - "Criterion": { - "@protected": true, - "@id": "untp-core:Criterion", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "description": { - "@id": "schemaorg:description" - }, - "conformityTopic": { - "@id": "untp-core:conformityTopic", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/conformityTopicCode#" - } - }, - "status": { - "@id": "untp-core:status", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/statusCode#" - } - }, - "subCriterion": { - "@id": "untp-core:subCriterion", - "@type": "@id" - }, - "thresholdValue": { - "@protected": true, - "@id": "untp-core:thresholdValue", - "@context": { - "@protected": true, - "metricName": { - "@id": "untp-core:metricName", - "@type": "xsd:string" - }, - "metricValue": { - "@protected": true, - "@id": "untp-core:metricValue", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "score": { - "@id": "untp-core:score", - "@type": "xsd:string" - }, - "accuracy": { - "@id": "untp-core:accuracy", - "@type": "xsd:double" - } - } - }, - "performanceLevel": { - "@id": "untp-core:performanceLevel", - "@type": "xsd:string" - }, - "category": { - "@id": "untp-core:category", - "@type": "@id" - }, - "tag": { - "@id": "untp-core:tag", - "@type": "xsd:string" - } - } - }, - "Claim": { - "@protected": true, - "@id": "untp-core:Claim", - "@context": { - "@protected": true, - "description": { - "@id": "schemaorg:description" - }, - "referenceStandard": { - "@id": "untp-core:referenceStandard", - "@type": "@id" - }, - "referenceRegulation": { - "@id": "untp-core:referenceRegulation", - "@type": "@id" - }, - "assessmentCriteria": { - "@id": "untp-core:assessmentCriteria", - "@type": "@id" - }, - "assessmentDate": { - "@id": "untp-core:assessmentDate", - "@type": "xsd:string" - }, - "declaredValue": { - "@protected": true, - "@id": "untp-core:declaredValue", - "@context": { - "@protected": true, - "metricName": { - "@id": "untp-core:metricName", - "@type": "xsd:string" - }, - "metricValue": { - "@protected": true, - "@id": "untp-core:metricValue", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "score": { - "@id": "untp-core:score", - "@type": "xsd:string" - }, - "accuracy": { - "@id": "untp-core:accuracy", - "@type": "xsd:double" - } - } - }, - "conformance": { - "@id": "untp-core:conformance", - "@type": "xsd:boolean" - }, - "conformityTopic": { - "@id": "untp-core:conformityTopic", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/conformityTopicCode#" - } - }, - "conformityEvidence": { - "@protected": true, - "@id": "untp-core:conformityEvidence", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - }, - "hashDigest": { - "@id": "untp-core:hashDigest", - "@type": "xsd:string" - }, - "hashMethod": { - "@id": "untp-core:hashMethod", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/hashMethodCode#" - } - }, - "encryptionMethod": { - "@id": "untp-core:encryptionMethod", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/encryptionMethodCode#" - } - } - } - } - } - }, - "ProductPassport": { - "@protected": true, - "@id": "untp-dpp:ProductPassport", - "@context": { - "@protected": true, - "product": { - "@id": "untp-core:product", - "@type": "@id" - }, - "granularityLevel": { - "@id": "untp-dpp:granularityLevel", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/dpp/0/granularityCode#" - } - }, - "conformityClaim": { - "@id": "untp-core:conformityClaim", - "@type": "@id" - }, - "emissionsScorecard": { - "@protected": true, - "@id": "untp-core:emissionsScorecard", - "@context": { - "@protected": true, - "carbonFootprint": { - "@id": "untp-core:carbonFootprint", - "@type": "xsd:double" - }, - "declaredUnit": { - "@id": "untp-core:declaredUnit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - }, - "operationalScope": { - "@id": "untp-core:operationalScope", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/operationalScopeCode#" - } - }, - "primarySourcedRatio": { - "@id": "untp-core:primarySourcedRatio", - "@type": "xsd:double" - }, - "reportingStandard": { - "@id": "untp-core:reportingStandard", - "@type": "@id" - } - } - }, - "traceabilityInformation": { - "@protected": true, - "@id": "untp-dpp:traceabilityInformation", - "@context": { - "@protected": true, - "valueChainProcess": { - "@id": "untp-dpp:valueChainProcess", - "@type": "xsd:string" - }, - "verifiedRatio": { - "@id": "untp-dpp:verifiedRatio", - "@type": "xsd:double" - }, - "traceabilityEvent": { - "@protected": true, - "@id": "untp-core:traceabilityEvent", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - }, - "hashDigest": { - "@id": "untp-core:hashDigest", - "@type": "xsd:string" - }, - "hashMethod": { - "@id": "untp-core:hashMethod", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/hashMethodCode#" - } - }, - "encryptionMethod": { - "@id": "untp-core:encryptionMethod", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/encryptionMethodCode#" - } - } - } - } - } - }, - "circularityScorecard": { - "@protected": true, - "@id": "untp-core:circularityScorecard", - "@context": { - "@protected": true, - "recyclingInformation": { - "@protected": true, - "@id": "untp-core:recyclingInformation", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - } - } - }, - "repairInformation": { - "@protected": true, - "@id": "untp-core:repairInformation", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - } - } - }, - "recyclableContent": { - "@id": "untp-core:recyclableContent", - "@type": "xsd:double" - }, - "recycledContent": { - "@id": "untp-core:recycledContent", - "@type": "xsd:double" - }, - "utilityFactor": { - "@id": "untp-core:utilityFactor", - "@type": "xsd:double" - }, - "materialCircularityIndicator": { - "@id": "untp-core:materialCircularityIndicator", - "@type": "xsd:double" - } - } - }, - "dueDiligenceDeclaration": { - "@protected": true, - "@id": "untp-core:dueDiligenceDeclaration", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - } - } - }, - "materialsProvenance": { - "@protected": true, - "@id": "untp-core:materialsProvenance", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "originCountry": { - "@id": "untp-core:originCountry", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/CountryId#" - } - }, - "materialType": { - "@id": "untp-core:materialType", - "@type": "@id" - }, - "massFraction": { - "@id": "untp-core:massFraction", - "@type": "xsd:double" - }, - "mass": { - "@protected": true, - "@id": "untp-core:mass", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "recycledMassFraction": { - "@id": "untp-core:recycledMassFraction", - "@type": "xsd:double" - }, - "hazardous": { - "@id": "untp-core:hazardous", - "@type": "xsd:boolean" - }, - "symbol": { - "@id": "untp-core:symbol", - "@type": "xsd:string" - }, - "materialSafetyInformation": { - "@protected": true, - "@id": "untp-core:materialSafetyInformation", - "@context": { - "@protected": true, - "linkURL": { - "@id": "untp-core:linkURL", - "@type": "xsd:string" - }, - "linkName": { - "@id": "untp-core:linkName", - "@type": "xsd:string" - }, - "linkType": { - "@id": "untp-core:linkType", - "@type": "xsd:string" - } - } - } - } - } - } - }, - "Declaration": { - "@protected": true, - "@id": "untp-core:Declaration", - "@context": { - "@protected": true, - "description": { - "@id": "schemaorg:description" - }, - "referenceStandard": { - "@id": "untp-core:referenceStandard", - "@type": "@id" - }, - "referenceRegulation": { - "@id": "untp-core:referenceRegulation", - "@type": "@id" - }, - "assessmentCriteria": { - "@id": "untp-core:assessmentCriteria", - "@type": "@id" - }, - "assessmentDate": { - "@id": "untp-core:assessmentDate", - "@type": "xsd:string" - }, - "declaredValue": { - "@protected": true, - "@id": "untp-core:declaredValue", - "@context": { - "@protected": true, - "metricName": { - "@id": "untp-core:metricName", - "@type": "xsd:string" - }, - "metricValue": { - "@protected": true, - "@id": "untp-core:metricValue", - "@context": { - "@protected": true, - "value": { - "@id": "untp-core:value", - "@type": "xsd:double" - }, - "unit": { - "@id": "untp-core:unit", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://vocabulary.uncefact.org/UnitMeasureCode#" - } - } - } - }, - "score": { - "@id": "untp-core:score", - "@type": "xsd:string" - }, - "accuracy": { - "@id": "untp-core:accuracy", - "@type": "xsd:double" - } - } - }, - "conformance": { - "@id": "untp-core:conformance", - "@type": "xsd:boolean" - }, - "conformityTopic": { - "@id": "untp-core:conformityTopic", - "@type": "@vocab", - "@context": { - "@protected": true, - "@vocab": "https://test.uncefact.org/vocabulary/untp/core/0/conformityTopicCode#" - } - } - } - }, - "RenderTemplate2024": { - "@protected": true, - "@id": "untp-core:RenderTemplate2024", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "mediaQuery": { - "@id": "untp-core:mediaQuery", - "@type": "xsd:string" - }, - "template": { - "@id": "renderMethodPrefix:template", - "@type": "xsd:string" - }, - "url": { - "@id": "renderMethodPrefix:url", - "@type": "xsd:string" - } - } - }, - "WebRenderingTemplate2022": { - "@protected": true, - "@id": "untp-core:WebRenderingTemplate2022", - "@context": { - "@protected": true, - "name": { - "@id": "schemaorg:name" - }, - "template": { - "@id": "renderMethodPrefix:template", - "@type": "xsd:string" - } - } - } - } -} diff --git a/docs/dpp/jsonschema.json b/docs/dpp/jsonschema.json deleted file mode 100644 index 2454280..0000000 --- a/docs/dpp/jsonschema.json +++ /dev/null @@ -1,1341 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "DigitalProductPassport", - "VerifiableCredential" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "DigitalProductPassport", - "minContains": 1 - } - }, - { - "contains": { - "const": "VerifiableCredential", - "minContains": 1 - } - } - ] - }, - "@context": { - "example": [ - "https://www.w3.org/ns/credentials/v2", - "https://test.uncefact.org/vocabulary/untp/dpp/0.6.0/" - ], - "type": "array", - "items": { - "type": "string" - }, - "description": "A list of JSON-LD context URIs that define the semantic meaning of properties within the credential. ", - "readOnly": true, - "prefixItems": [ - { - "const": "https://www.w3.org/ns/credentials/v2", - "type": "string" - }, - { - "const": "https://test.uncefact.org/vocabulary/untp/dpp/0.6.0/", - "type": "string" - } - ], - "default": [ - "https://www.w3.org/ns/credentials/v2", - "https://test.uncefact.org/vocabulary/untp/dpp/0.6.0/" - ], - "minItems": 2, - "uniqueItems": true - }, - "id": { - "example": "https://example-company.com/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0", - "type": "string", - "format": "uri", - "description": "A unique identifier (URI) assigned to the product passport. May be a UUID" - }, - "issuer": { - "$ref": "#/$defs/CredentialIssuer", - "description": "The organisation that is the issuer of this VC. Note that the \"id\" property MUST be a W3C DID. Other identifiers such as tax registration numbers can be listed in the \"otherIdentifiers\" property." - }, - "validFrom": { - "pattern": "^-?([1-9][0-9]{3,}|0[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?|(24:00:00(\\.0+)?))(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))$", - "example": "2024-03-15T12:00:00Z", - "type": "string", - "format": "date-time", - "description": "The date and time from which the credential is valid." - }, - "validUntil": { - "pattern": "^-?([1-9][0-9]{3,}|0[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?|(24:00:00(\\.0+)?))(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))$", - "example": "2034-03-15T12:00:00Z", - "type": "string", - "format": "date-time", - "description": "The expiry date (if applicable) of this verifiable credential." - }, - "credentialSubject": { - "$ref": "#/$defs/ProductPassport", - "description": "The subject of a digital product passport credential is the identified product." - } - }, - "description": "The Product Passport is a comprehensive data structure that encapsulates various details pertaining to a product, including its identification details, who issued it, batch information, provenance information, circularity information and a set of verifiable product conformity & sustainability claims. ", - "required": [ - "@context", - "id", - "issuer" - ], - "$defs": { - "CredentialIssuer": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "CredentialIssuer" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "CredentialIssuer", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "did:web:identifiers.example-company.com:12345", - "type": "string", - "format": "uri", - "description": "The W3C DID of the issuer - should be a did:web or did:webvh" - }, - "name": { - "example": "Example Company Pty Ltd", - "type": "string", - "description": "The name of the issuer person or organisation" - }, - "issuerAlsoKnownAs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "example": "https://abr.business.gov.au/ABN/View?abn=90664869327", - "type": "string", - "format": "uri", - "description": "The globally unique ID of the party as a URI, ideally as a resolvable ID. " - }, - "name": { - "example": "Sample Company Pty Ltd.", - "type": "string", - "description": "The registered name of the party within the identifier scheme." - }, - "registeredId": { - "example": 90664869327, - "type": "string", - "description": "The registration number (alphanumeric) of the Party within the register. Unique within the register." - } - }, - "required": [ - "id", - "name" - ] - }, - "description": "An optional list of other registered identifiers for this credential issuer " - } - }, - "description": "The issuer party (person or organisation) of a verifiable credential.", - "required": [ - "id", - "name" - ] - }, - "ProductPassport": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "ProductPassport" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "ProductPassport", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "example:product/1234", - "type": "string", - "format": "uri", - "description": "An id is not required in the data but jargon requires for blue class which won't redefine credentialSubject" - }, - "product": { - "$ref": "#/$defs/Product", - "description": "The ProductInformation class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details." - }, - "granularityLevel": { - "type": "string", - "enum": [ - "item", - "batch", - "model" - ], - "example": "item", - "description": "Code to indicate the granularity of this digital product passport - item level, batch level or product class level." - }, - "conformityClaim": { - "type": "array", - "items": { - "$ref": "#/$defs/Claim" - }, - "description": "An array of claim objects representing various product conformity claims about the product / batch. These can be sustainability claims, circularity claims, or any other claim type within the conformity topic list." - }, - "emissionsScorecard": { - "$ref": "#/$defs/EmissionsPerformance", - "description": "An emissions performance scorecard" - }, - "traceabilityInformation": { - "type": "array", - "items": { - "$ref": "#/$defs/TraceabilityPerformance" - }, - "description": "An array of traceability events grouped by value chain process. Where actual traceability events are unavailable or carry sensitive information, passport publishers may specify the extent to which the traceability information has been independently verified. " - }, - "circularityScorecard": { - "$ref": "#/$defs/CircularityPerformance", - "description": "A circularity performance scorecard" - }, - "dueDiligenceDeclaration": { - "$ref": "#/$defs/Link", - "description": "The due diligence declaration that conforms with the regulations of the market into which the product is sold." - }, - "materialsProvenance": { - "type": "array", - "items": { - "$ref": "#/$defs/Material" - }, - "description": "An array of Provenance objects providing details on the origin and mass fraction of components or ingredients of the product batch. " - } - }, - "description": "The ProductClaim class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details, as well as information about the claims being made about the product. " - }, - "Product": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Product" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Product", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "https://id.gs1.org/01/09520123456788/21/12345", - "type": "string", - "format": "uri", - "description": "The globally unique ID of the product as a URI. Ideally as a resolvable URL according to ISO 18975. " - }, - "name": { - "example": "EV battery 300Ah.", - "type": "string", - "description": "The registered name of the product within the identifier scheme. " - }, - "registeredId": { - "example": "09520123456788.21.12345", - "type": "string", - "description": "The registration number (alphanumeric) of the entity within the register. Unique within the register." - }, - "idScheme": { - "$ref": "#/$defs/IdentifierScheme", - "description": "The identifier scheme for this product. Eg a GS1 GTIN or an AU Livestock NLIS, or similar. If self issued then use the party ID of the issuer. " - }, - "batchNumber": { - "example": 6789, - "type": "string", - "description": "Identifier of the specific production batch of the product. Unique within the product class." - }, - "productImage": { - "$ref": "#/$defs/Link", - "description": "Reference information (location, type, name) of an image of the product." - }, - "description": { - "example": "400Ah 24v LiFePO4 battery", - "type": "string", - "description": "A textual description providing details about the product." - }, - "characteristics": { - "$ref": "#/$defs/Characteristics", - "description": "A placeholder for indusutry / product category specific information. " - }, - "productCategory": { - "type": "array", - "items": { - "$ref": "#/$defs/Classification" - }, - "description": "A code representing the product's class, typically using the UN CPC (United Nations Central Product Classification) https://unstats.un.org/unsd/classifications/Econ/cpc" - }, - "furtherInformation": { - "type": "array", - "items": { - "$ref": "#/$defs/Link" - }, - "description": "A URL pointing to further human readable information about the product." - }, - "producedByParty": { - "type": "object", - "properties": { - "id": { - "example": "https://abr.business.gov.au/ABN/View?abn=90664869327", - "type": "string", - "format": "uri", - "description": "The globally unique ID of the party as a URI, ideally as a resolvable ID. " - }, - "name": { - "example": "Sample Company Pty Ltd.", - "type": "string", - "description": "The registered name of the party within the identifier scheme." - }, - "registeredId": { - "example": 90664869327, - "type": "string", - "description": "The registration number (alphanumeric) of the Party within the register. Unique within the register." - } - }, - "required": [ - "id", - "name" - ], - "description": "The Party entity that manufactured the product." - }, - "producedAtFacility": { - "type": "object", - "properties": { - "id": { - "example": "https://sample-facility-register.com/1234567", - "type": "string", - "format": "uri", - "description": "The globally unique ID of the facility as URI, ideally as a resolvable URL." - }, - "name": { - "example": "Greenacres battery factory", - "type": "string", - "description": "The registered name of the facility within the identifier scheme. " - }, - "registeredId": { - "example": 1234567, - "type": "string", - "description": "The registration number (alphanumeric) of the facility within the identifier scheme. Unique within the register." - } - }, - "required": [ - "id", - "name" - ], - "description": "The Facility where the product batch was produced / manufactured." - }, - "productionDate": { - "example": "2024-04-25", - "type": "string", - "format": "date", - "description": "The ISO 8601 date on which the product batch or individual serialised item was manufactured." - }, - "countryOfProduction": { - "type": "string", - "x-external-enumeration": "https://vocabulary.uncefact.org/CountryId#", - "description": "The country in which this item was produced / manufactured.\n\n This is an enumerated value, but the list of valid values are too big, or change too often to include here. You can access the list of allowable values at this URL: https://vocabulary.uncefact.org/CountryId#\n " - }, - "serialNumber": { - "example": 12345678, - "type": "string", - "description": "A number or code representing a specific serialised item of the product. Unique within product class." - }, - "dimensions": { - "$ref": "#/$defs/Dimension", - "description": "The physical dimensions of the product. Not every dimension is relevant to every products. For example bulk materials may have weight and volume but not length, with, or height.\"weight\":{\"value\":10, \"unit\":\"KGM\"}" - } - }, - "description": "The ProductInformation class encapsulates detailed information regarding a specific product, including its identification details, manufacturer, and other pertinent details.", - "required": [ - "id", - "name" - ] - }, - "IdentifierScheme": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "IdentifierScheme" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "IdentifierScheme", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "https://id.gs1.org/01/", - "type": "string", - "format": "uri", - "description": "The globally unique identifier of the registration scheme. The scheme should be registered and discoverable from vocabulary.uncefact.org/identifierSchemes" - }, - "name": { - "example": "Global Trade Identification Number (GTIN)", - "type": "string", - "description": "The name of the identifier scheme. " - } - }, - "description": "An identifier registration scheme for products, facilities, or organisations. Typically operated by a state, national or global authority." - }, - "Link": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Link" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Link", - "minContains": 1 - } - } - ] - }, - "linkURL": { - "example": "https://files.example-certifier.com/1234567.json", - "type": "string", - "format": "uri", - "description": "The URL of the target resource. " - }, - "linkName": { - "example": "GBA rule book conformity certificate", - "type": "string", - "description": "A display name for the target resource " - }, - "linkType": { - "example": "https://test.uncefact.org/vocabulary/linkTypes/dcc", - "type": "string", - "description": "The type of the target resource - drawn from a controlled vocabulary " - } - }, - "description": "A structure to provide a URL link plus metadata associated with the link." - }, - "Characteristics": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Characteristics" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Characteristics", - "minContains": 1 - } - } - ] - } - }, - "description": "Extension point for commodity specific characteristics like battery capacity, clothing size, etc." - }, - "Classification": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Classification" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Classification", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "https://unstats.un.org/unsd/classifications/Econ/cpc/46410", - "type": "string", - "format": "uri", - "description": "The globally unique URI representing the specific classifier value" - }, - "code": { - "example": 46410, - "type": "string", - "description": "classification code within the scheme" - }, - "name": { - "example": "Primary cells and primary batteries", - "type": "string", - "description": "Name of the classification represented by the code" - }, - "schemeID": { - "example": "https://unstats.un.org/unsd/classifications/Econ/cpc/", - "type": "string", - "format": "uri", - "description": "Classification scheme ID" - }, - "schemeName": { - "example": "UN Central Product Classification (CPC)", - "type": "string", - "description": "The name of the classification scheme" - } - }, - "description": "A classification scheme and code / name representing a category value for a product, entity, or facility.", - "required": [ - "id", - "name" - ] - }, - "Dimension": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Dimension" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Dimension", - "minContains": 1 - } - } - ] - }, - "weight": { - "$ref": "#/$defs/Measure", - "description": "the weight of the product. EG {\"value\":10, \"unit\":\"KGM\"}" - }, - "length": { - "$ref": "#/$defs/Measure", - "description": "The length of the product or packaging eg {\"value\":840, \"unit\":\"MMT\"}" - }, - "width": { - "$ref": "#/$defs/Measure", - "description": "The width of the product or packaging. eg {\"value\":150, \"unit\":\"MMT\"}" - }, - "height": { - "$ref": "#/$defs/Measure", - "description": "The height of the product or packaging. eg {\"value\":220, \"unit\":\"MMT\"}" - }, - "volume": { - "$ref": "#/$defs/Measure", - "description": "The displacement volume of the product. eg {\"value\":7.5, \"unit\":\"LTR\"}" - } - }, - "description": "Overall (length, width, height) dimensions and weight/volume of an item." - }, - "Measure": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Measure" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Measure", - "minContains": 1 - } - } - ] - }, - "value": { - "example": 10, - "type": "number", - "description": "The numeric value of the measure" - }, - "unit": { - "type": "string", - "x-external-enumeration": "https://vocabulary.uncefact.org/UnitMeasureCode#", - "description": "Unit of measure drawn from the UNECE Rec20 measure code list.\n\n This is an enumerated value, but the list of valid values are too big, or change too often to include here. You can access the list of allowable values at this URL: https://vocabulary.uncefact.org/UnitMeasureCode#\n " - } - }, - "description": "The measure class defines a numeric measured value (eg 10) and a coded unit of measure (eg KG).", - "required": [ - "value", - "unit" - ] - }, - "Claim": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Claim", - "Declaration" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Claim", - "minContains": 1 - } - }, - { - "contains": { - "const": "Declaration", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "https://products.example-company.com/09520123456788/declarations/12345", - "type": "string", - "format": "uri", - "description": "A unique identifier for the declaration. Often this will be an extension of the product.id or attestation.id. But could be a UUID." - }, - "description": { - "example": "A standardised disclosure of the battery's greenhouse gas emissions intensity, calculated in accordance with the Global Battery Alliance Battery Passport Greenhouse Gas Rulebook V.2.0.", - "type": "string", - "description": "A textual description of the declaration being made." - }, - "referenceStandard": { - "$ref": "#/$defs/Standard", - "description": "The reference to the standard that defines the specification / criteria" - }, - "referenceRegulation": { - "$ref": "#/$defs/Regulation", - "description": "The reference to the regulation that defines the assessment criteria" - }, - "assessmentCriteria": { - "type": "array", - "items": { - "$ref": "#/$defs/Criterion" - }, - "description": "The specification against which the assessment is made." - }, - "assessmentDate": { - "example": "2024-03-15", - "type": "string", - "format": "date", - "description": "The date on which this assessment was made. " - }, - "declaredValue": { - "type": "array", - "items": { - "$ref": "#/$defs/Metric" - }, - "description": "The list of specific values measured as part of this assessment (eg tensile strength)" - }, - "conformance": { - "example": true, - "type": "boolean", - "description": "An indicator of whether or not the claim or assesment conforms to the regulatory or standard criteria." - }, - "conformityTopic": { - "type": "string", - "enum": [ - "environment.energy", - "environment.emissions", - "environment.water", - "environment.waste", - "environment.deforestation", - "environment.biodiversity", - "circularity.content", - "circularity.design", - "social.labour", - "social.rights", - "social.community", - "social.safety", - "governance.ethics", - "governance.compliance", - "governance.transparency" - ], - "example": "environment.energy", - "description": "The conformity topic category for this assessment (eg vocabulary.uncefact.org/sustainability/emissions)" - }, - "conformityEvidence": { - "$ref": "#/$defs/SecureLink", - "description": "A URI pointing to the evidence supporting the claim. SHOULD be a URL to a UNTP Digital Conformity Credential (DCC)" - } - }, - "description": "A declaration of conformance with one or more criteria from a specific standard or regulation. ", - "required": [ - "id", - "conformance", - "conformityTopic" - ] - }, - "Standard": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Standard" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Standard", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf", - "type": "string", - "format": "uri", - "description": "A unique identifier for the standard (eg https://www.iso.org/standard/60857.html)." - }, - "name": { - "example": "GBA Battery Passport Greenhouse Gas Rulebook - V.2.0", - "type": "string", - "description": "The name of the standard" - }, - "issuingParty": { - "type": "object", - "properties": { - "id": { - "example": "https://abr.business.gov.au/ABN/View?abn=90664869327", - "type": "string", - "format": "uri", - "description": "The globally unique ID of the party as a URI, ideally as a resolvable ID. " - }, - "name": { - "example": "Sample Company Pty Ltd.", - "type": "string", - "description": "The registered name of the party within the identifier scheme." - }, - "registeredId": { - "example": 90664869327, - "type": "string", - "description": "The registration number (alphanumeric) of the Party within the register. Unique within the register." - } - }, - "required": [ - "id", - "name" - ], - "description": "The party that issued the standard " - }, - "issueDate": { - "example": "2023-12-05", - "type": "string", - "format": "date", - "description": "The date when the standard was issued." - } - }, - "description": "A standard (eg ISO 14000) that specifies the criteria for conformance.", - "required": [ - "issuingParty" - ] - }, - "Regulation": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Regulation" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Regulation", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "https://www.legislation.gov.au/F2008L02309/latest/versions", - "type": "string", - "format": "uri", - "description": "The globally unique identifier of this regulation. " - }, - "name": { - "example": "NNational Greenhouse and Energy Reporting (Measurement) Determination", - "type": "string", - "description": "The name of the regulation or act." - }, - "jurisdictionCountry": { - "type": "string", - "x-external-enumeration": "https://vocabulary.uncefact.org/CountryId#", - "description": "The legal jurisdiction (country) under which the regulation is issued.\n\n This is an enumerated value, but the list of valid values are too big, or change too often to include here. You can access the list of allowable values at this URL: https://vocabulary.uncefact.org/CountryId#\n " - }, - "administeredBy": { - "type": "object", - "properties": { - "id": { - "example": "https://abr.business.gov.au/ABN/View?abn=90664869327", - "type": "string", - "format": "uri", - "description": "The globally unique ID of the party as a URI, ideally as a resolvable ID. " - }, - "name": { - "example": "Sample Company Pty Ltd.", - "type": "string", - "description": "The registered name of the party within the identifier scheme." - }, - "registeredId": { - "example": 90664869327, - "type": "string", - "description": "The registration number (alphanumeric) of the Party within the register. Unique within the register." - } - }, - "required": [ - "id", - "name" - ], - "description": "the issuing body of the regulation. For example Australian Government Department of Climate Change, Energy, the Environment and Water" - }, - "effectiveDate": { - "example": "2024-03-20", - "type": "string", - "format": "date", - "description": "the date at which the regulation came into effect." - } - }, - "description": "A regulation (eg EU deforestation regulation) that defines the criteria for assessment.", - "required": [ - "administeredBy" - ] - }, - "Criterion": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Criterion" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Criterion", - "minContains": 1 - } - } - ] - }, - "id": { - "example": "https://www.globalbattery.org/media/publications/gba-rulebook-v2.0-master.pdf#BatteryAssembly", - "type": "string", - "format": "uri", - "description": "A unique identifier for the criterion within the scheme, standard or regulation. For example CO2 emissions calculations for liquid fuel combustion." - }, - "name": { - "example": "GBA Battery rule book v2.0 battery assembly guidelines.", - "type": "string", - "description": "A name that describes this criteria." - }, - "description": { - "example": "Battery is designed for easy disassembly and recycling at end-of-life.", - "type": "string", - "description": "A full text description of the criterion that clearly specifies how compliance is achieved and measured. " - }, - "conformityTopic": { - "type": "string", - "enum": [ - "environment.energy", - "environment.emissions", - "environment.water", - "environment.waste", - "environment.deforestation", - "environment.biodiversity", - "circularity.content", - "circularity.design", - "social.labour", - "social.rights", - "social.community", - "social.safety", - "governance.ethics", - "governance.compliance", - "governance.transparency" - ], - "example": "environment.energy", - "description": "A gloabl UN/CEFACT standard sustainability toipic code. " - }, - "status": { - "type": "string", - "enum": [ - "proposed", - "active", - "deprecated" - ], - "example": "proposed", - "description": "The lifecycle status of this criterion. " - }, - "subCriterion": { - "type": "array", - "items": { - "$ref": "#/$defs/Criterion" - }, - "description": "List of criterion that are subordinate to this criterion in the hierarchy." - }, - "thresholdValue": { - "$ref": "#/$defs/Metric", - "description": "A threshold value that defines the minimum compliance level. " - }, - "performanceLevel": { - "example": "\"Category 3 recyclable with 73% recyclability\"", - "type": "string", - "description": "A performance category code to group criterion in a given hierarchyLevel according to pareformance" - }, - "tags": { - "type": "string", - "description": "a category code to represent target stakeholder types or commodity types to which this criteron applies. " - } - }, - "description": "A specific rule or criterion within a standard or regulation. eg a carbon intensity calculation rule within an emissions standard.", - "required": [ - "id", - "name", - "description", - "conformityTopic", - "status" - ] - }, - "Metric": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Metric" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Metric", - "minContains": 1 - } - } - ] - }, - "metricName": { - "example": "GHG emissions intensity", - "type": "string", - "description": "A human readable name for this metric (for example \"Tensile strength\")" - }, - "metricValue": { - "$ref": "#/$defs/Measure", - "description": "A numeric value and unit of measure for this metric. Could be a measured value or a specified threshold. Eg {\"value\":400, \"unit\":\"MPA\"} as tensile strength of structural steel" - }, - "score": { - "example": "BB", - "type": "string", - "description": "A score or rank associated with this measured metric." - }, - "accuracy": { - "example": 0.05, - "type": "number", - "description": "A percentage represented as a numeric between 0 and 1 indicating the rage of accuracy of the claimed value (eg 0.05 means that the actual value is within 5% of the claimed value.)" - } - }, - "description": "A specific measure of performance against the criteria that governs the claim. Expressed as an array of metric (ie unit of measure) / value (ie the actual numeric value) pairs. ", - "required": [ - "metricName", - "metricValue" - ] - }, - "SecureLink": { - "type": "object", - "additionalProperties": false, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "SecureLink", - "Link" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "SecureLink", - "minContains": 1 - } - }, - { - "contains": { - "const": "Link", - "minContains": 1 - } - } - ] - }, - "linkURL": { - "example": "https://files.example-certifier.com/1234567.json", - "type": "string", - "format": "uri", - "description": "The URL of the target resource. " - }, - "linkName": { - "example": "GBA rule book conformity certificate", - "type": "string", - "description": "A display name for the target resource " - }, - "linkType": { - "example": "https://test.uncefact.org/vocabulary/linkTypes/dcc", - "type": "string", - "description": "The type of the target resource - drawn from a controlled vocabulary " - }, - "hashDigest": { - "example": "6239119dda5bd4c8a6ffb832fe16feaa5c27b7dba154d24c53d4470a2c69adc2", - "type": "string", - "description": "The hash of the file." - }, - "hashMethod": { - "type": "string", - "enum": [ - "SHA-256", - "SHA-1" - ], - "example": "SHA-256", - "description": "The hashing algorithm used to create the target hash. SHA-265 is the recommended standard" - }, - "encryptionMethod": { - "type": "string", - "enum": [ - "none", - "AES" - ], - "example": "none", - "description": "The symmetric encryption algorithm used to encrypt the link target. AES is the recommended standard. Decryption keys are expected to be passed out of bounds." - } - }, - "description": "A binary file that is hashed ()for tamper evidence) and optionally encrypted (for confidentiality)." - }, - "EmissionsPerformance": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "EmissionsPerformance" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "EmissionsPerformance", - "minContains": 1 - } - } - ] - }, - "carbonFootprint": { - "example": 1.8, - "type": "number", - "format": "float", - "description": "The carbon footprint of the product in KgCO2e per declared unit." - }, - "declaredUnit": { - "type": "string", - "x-external-enumeration": "https://vocabulary.uncefact.org/UnitMeasureCode#", - "description": "The unit of product (EA, KGM, LTR, etc) that is the basis for carbon footprint.\n\n This is an enumerated value, but the list of valid values are too big, or change too often to include here. You can access the list of allowable values at this URL: https://vocabulary.uncefact.org/UnitMeasureCode#\n " - }, - "operationalScope": { - "type": "string", - "enum": [ - "None", - "CradleToGate", - "CradleToGrave" - ], - "example": "None", - "description": "The operational scope of the emissions performance. Only scope 1 & 2, or including upstream scope 3 (cradle to gate) or upstream and downstream scope 3 (cradle to grave)." - }, - "primarySourcedRatio": { - "example": 0.3, - "type": "number", - "format": "float", - "description": "The ratio of emissions data from primary sources (ie from supplier / product specific information rather than secondary / industry averages)." - }, - "reportingStandard": { - "$ref": "#/$defs/Standard", - "description": "The reporting standard (eg GHG Protocol, IFRS S2, ESRS, etc) against which this product emissions performance is assessed." - } - }, - "description": "Product specific characteristics. This class is an extension point for industry specific product characteristics or performance information such as clothing size or battery capacity.", - "required": [ - "carbonFootprint", - "declaredUnit", - "operationalScope", - "primarySourcedRatio" - ] - }, - "TraceabilityPerformance": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "TraceabilityPerformance" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "TraceabilityPerformance", - "minContains": 1 - } - } - ] - }, - "valueChainProcess": { - "example": "Spinning", - "type": "string", - "description": "Human readable name for the industry specific value chain process representing this traceability data set." - }, - "verifiedRatio": { - "example": 0.5, - "type": "number", - "description": "The proportion (0 to 1) of materials in this value chain process that have been traced using verifiable traceability event." - }, - "traceabilityEvent": { - "type": "array", - "items": { - "$ref": "#/$defs/SecureLink" - }, - "description": "A list of secure links to digital traceability events that support this traceability performance statement. May be encrypted for confidentiality purposes. " - } - }, - "description": "An array of secure links to TraceabilityEvent objects detailing EPCIS events related to the traceability of the product batch. Events are grouped by value chain process (eg \"Weaving\" in textiles supply chain)." - }, - "CircularityPerformance": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "CircularityPerformance" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "CircularityPerformance", - "minContains": 1 - } - } - ] - }, - "recyclingInformation": { - "$ref": "#/$defs/Link", - "description": "A URI pointing to recycling information for the product." - }, - "repairInformation": { - "$ref": "#/$defs/Link", - "description": "A URI pointing to repair instructions for this product." - }, - "recyclableContent": { - "example": 0.5, - "type": "number", - "format": "float", - "description": "The fraction of the this product (my mass) that has been designed to be recyclable / re-usable. This will be be the total fraction that can avoid waste / landfill." - }, - "recycledContent": { - "example": 0.3, - "type": "number", - "format": "float", - "description": "The fraction (by mass) of recycled / repurposed, repaired content in this product. This will be the total virgin content fraction." - }, - "utilityFactor": { - "example": 1.2, - "type": "number", - "format": "float", - "description": "An indicator of durability defined as the lifetime (typically measures as usage cycles) for this product divided by the industry average. For example a battery with a 10,000 cycle lifetime where industry average is 5,000 cycles will have a durability factor of 2. If unknown set to 1 or omit. " - }, - "materialCircularityIndicator": { - "example": 0.67, - "type": "number", - "format": "float", - "description": "The overall circularity performance indicator for this product. Calculated as 1 - (V+W)/2D where - V = Virgin material proportion by mass (will be 1- recycled content) - W = Waste leakage proportion by mass (will be 1 - recyclableContent) - D = Utility factor (set to 1 if unknown). " - } - }, - "description": "High level circularity information about this product. Note that this does not substitute for detailed product circularity data sheet (PCDS) criteria which would be represented as a self-issued UNTP Digital Conformity Credential as a set of assessments against individual ISO PCDS criteria." - }, - "Material": { - "type": "object", - "additionalProperties": true, - "properties": { - "type": { - "type": "array", - "readOnly": true, - "default": [ - "Material" - ], - "items": { - "type": "string" - }, - "allOf": [ - { - "contains": { - "const": "Material", - "minContains": 1 - } - } - ] - }, - "name": { - "example": "Lithium Spodumene", - "type": "string", - "description": "Name of this material (eg \"Egyptian Cotton\")" - }, - "originCountry": { - "type": "string", - "x-external-enumeration": "https://vocabulary.uncefact.org/CountryId#", - "description": "A ISO 3166-1 code representing the country of origin of the component or ingredient.\n\n This is an enumerated value, but the list of valid values are too big, or change too often to include here. You can access the list of allowable values at this URL: https://vocabulary.uncefact.org/CountryId#\n " - }, - "materialType": { - "$ref": "#/$defs/Classification", - "description": "The type of this material - as a value drawn from a controlled vocabulary eg from UN Framework Classification for Resources (UNFC)." - }, - "massFraction": { - "example": 0.2, - "type": "number", - "description": "The mass fraction of the product represented by this material. The sum of all mass fraction values for a given passport should be 1." - }, - "mass": { - "$ref": "#/$defs/Measure", - "description": "The mass of the material component." - }, - "recycledMassFraction": { - "example": 0.5, - "type": "number", - "description": "Mass fraction of this material that is recycled (eg 50% recycled Lithium)" - }, - "hazardous": { - "type": "boolean", - "description": "Indicates whether this material is hazardous. If true then the materialSafetyInformation property must be present" - }, - "symbol": { - "type": "string", - "format": "binary", - "description": "Based 64 encoded binary used to represent a visual symbol for a given material. " - }, - "materialSafetyInformation": { - "$ref": "#/$defs/Link", - "description": "Reference to further information about safe handling of this hazardous material (for example a link to a material safety data sheet)" - } - }, - "description": "The material class encapsulates details about the origin or source of raw materials in a product, including the country of origin and the mass fraction.", - "required": [ - "name" - ] - } - } -} diff --git a/docs/dpp_validator_description.md b/docs/dpp_validator_description.md deleted file mode 100644 index ad4521e..0000000 --- a/docs/dpp_validator_description.md +++ /dev/null @@ -1,13 +0,0 @@ -## Digital Product Passport Validator (open-source) - -### What it does - -Validates Digital Product Passports against EU regulations — seven layers of checks (schema, semantics, cryptographic signatures) in under 1 ms per passport, covering both UN/CEFACT and EU CIRPASS-2 standards. - -### Why fashion needs it - -Starting 2027, every textile product sold in the EU must carry a compliant Digital Product Passport under the ESPR regulation. A non-compliant passport means the product cannot legally enter the market. Brands need to catch errors before production — not at the border. - -### Why open-source - -We publish this as the only comprehensive open-source DPP validator with full EU ontology support. The regulation is new and the standard is still evolving — open-source builds trust and lowers the barrier for brands to start preparing now. For us, it's a natural entry point: every fashion company that validates passports through our tool becomes familiar with our stack. diff --git a/docs/errors/DET001.md b/docs/errors/DET001.md new file mode 100644 index 0000000..da87134 --- /dev/null +++ b/docs/errors/DET001.md @@ -0,0 +1,49 @@ +# DET001 - Family Mismatch + +## Description + +The user-supplied `--target` (or `--to`) family contradicts the family +that the detection layer inferred from the payload itself. + +## Category + +Detection Errors + +## Severity + +`error` + +## When emitted + +Surfaced from +[`validators/detection.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/validators/detection.py) +during the routing phase that runs before any layered validator. +The CLI translates `DET001` into exit code +[`EXIT_FAMILY_MISMATCH` (3)](../reference/cli/exit-codes.md) so wrapper +scripts can branch on it without parsing error messages — see +[ADR 0005](../adr/0005-cli-exit-codes.md). + +## Common causes + +- Running `dppvalidator validate --target untp .json` + on a payload whose `@context` resolves to the CIRPASS namespace. +- Migrating the wrong direction: `migrate --to cirpass-1.3` against a + payload that already declares CIRPASS shape. +- A pipeline that hard-codes `--target` while feeding it mixed-family + fixtures. + +## How to fix + +- Drop the `--target` / `--to` flag and let detection auto-route, or +- Pass the family that matches the payload's declared `@context` / + shape, or +- If you intentionally want the cross-family pathway, use + `dppvalidator migrate` rather than `validate`. + +## See also + +- [Validation pipeline](../concepts/validation-layers.md) — Layer 0 + detection. +- [`ADR 0005 — CLI exit codes`](../adr/0005-cli-exit-codes.md) — how + `DET001` maps to `EXIT_FAMILY_MISMATCH`. +- [Error Overview](index.md) diff --git a/docs/errors/MAP001.md b/docs/errors/MAP001.md new file mode 100644 index 0000000..a505f33 --- /dev/null +++ b/docs/errors/MAP001.md @@ -0,0 +1,45 @@ +# MAP001 - Lossy Mapping Transformation + +## Description + +The cross-family migration shim dropped information from the source +payload because the target shape has no slot for it. + +## Category + +Mapping Errors + +## Severity + +`warning` + +## When emitted + +Emitted by the UNTP ↔ CIRPASS shims in +[`compat/untp_0_7_to_cirpass_1_3.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/untp_0_7_to_cirpass_1_3.py) +and +[`compat/cirpass_1_3_to_untp_0_7.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/cirpass_1_3_to_untp_0_7.py). +Code constant: `MAP_CODE_LOSSY` in +[`compat/_mapping_codes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_mapping_codes.py). + +## Common causes + +- Multiple language entries on a UNTP `name` projecting onto a + CIRPASS field that allows only one. +- UNTP-only fields (e.g. `traceabilityEvents`, durability extensions) + with no CIRPASS reference-structure equivalent. +- CIRPASS-only fields dropped on the reverse projection. + +## How to fix + +`MAP001` is informational; no fix is required if you accept the +lossy projection. To suppress in CI, run the migration with +`--accept-warnings`. To preserve the dropped data, store the source +payload alongside the projected output (the shim is one-way). + +## See also + +- [`docs/guides/migrate-untp-to-cirpass.md`](../guides/migrate-untp-to-cirpass.md) +- [Migration shim plan](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/CIRPASS_2_MIGRATION.md) + — Phase 5 +- [Error Overview](index.md) diff --git a/docs/errors/MAP002.md b/docs/errors/MAP002.md new file mode 100644 index 0000000..7220533 --- /dev/null +++ b/docs/errors/MAP002.md @@ -0,0 +1,44 @@ +# MAP002 - Synthesised Value During Mapping + +## Description + +A required target-side field had no donor on the source side, so the +shim invented a default value (typically a default identifier scheme, +role enum, or language tag). + +## Category + +Mapping Errors + +## Severity + +`warning` + +## When emitted + +Emitted by the UNTP ↔ CIRPASS shims via +[`compat/_mapping_codes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_mapping_codes.py) +constant `MAP_CODE_SYNTHESISED`. Common synthesis points include UNTP +roles falling back to `EUDPPRoleClass.ECONOMIC_OPERATOR` and +identifier schemes defaulting when the source uses a free-form URI. + +## Common causes + +- UNTP `Party` entries with no `role` mapping into CIRPASS + `relatedActors` — the shim emits `MAP002` and uses + `ECONOMIC_OPERATOR` as a permissive default. +- UNTP identifier schemes that don't appear in + [`compat/_identifier_schemes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_identifier_schemes.py) + — the shim wraps them and emits `MAP002`. + +## How to fix + +- Inspect the synthesised value and override it in the source payload + if the default doesn't match your intent. +- Re-run the migration after enriching the source side — `MAP002` + goes away once the donor field is populated. + +## See also + +- [`docs/concepts/untp-cirpass-mapping.md`](../concepts/untp-cirpass-mapping.md) +- [Error Overview](index.md) diff --git a/docs/errors/MAP003.md b/docs/errors/MAP003.md new file mode 100644 index 0000000..2800694 --- /dev/null +++ b/docs/errors/MAP003.md @@ -0,0 +1,43 @@ +# MAP003 - Unmapped Field Passthrough + +## Description + +No mapping rule applied to a source field; the value passed through +unchanged into an extension-style slot on the target. Surfaced so +consumers can decide whether to keep the passthrough or strip it. + +## Category + +Mapping Errors + +## Severity + +`warning` + +## When emitted + +Emitted by the UNTP ↔ CIRPASS shims via +[`compat/_mapping_codes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_mapping_codes.py) +constant `MAP_CODE_UNMAPPED`. Most identifier-scheme passthrough +events surface here — see +[`compat/_identifier_schemes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_identifier_schemes.py) +for the lookup table. + +## Common causes + +- A UNTP identifier scheme URI that isn't in the mapping table. +- A CIRPASS field with no UNTP equivalent that survives the reverse + projection as a typed-but-unmapped extension. + +## How to fix + +- Add the missing scheme/field to the mapping table in + [`compat/_identifier_schemes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_identifier_schemes.py) + if the omission is a genuine gap. +- Otherwise: accept the passthrough; the projection is round-trippable + for `MAP003` events. + +## See also + +- [`docs/concepts/untp-cirpass-mapping.md`](../concepts/untp-cirpass-mapping.md) +- [Error Overview](index.md) diff --git a/docs/errors/MAP004.md b/docs/errors/MAP004.md new file mode 100644 index 0000000..9c533c1 --- /dev/null +++ b/docs/errors/MAP004.md @@ -0,0 +1,45 @@ +# MAP004 - Required Field Missing on Target + +## Description + +The source payload cannot supply a field that the target shape +requires. The migration output will fail target-side validation +until the caller fills the field in. + +## Category + +Mapping Errors + +## Severity + +`error` + +## When emitted + +Emitted by the UNTP ↔ CIRPASS shims via +[`compat/_mapping_codes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_mapping_codes.py) +constant `MAP_CODE_REQUIRED_FIELD_MISSING`. This is the only `MAP00X` +code at `error` severity — it always blocks unless +`--accept-warnings` is set. + +## Common causes + +- Migrating a UNTP 0.7 payload to CIRPASS 1.3 without a + `productCategory` value — CIRPASS requires it; UNTP does not. +- Migrating a CIRPASS payload missing `dppIdentifier` to UNTP — the + reverse projection has no donor. + +## How to fix + +- Populate the required source-side field, then re-run the + migration. +- If the missing field is genuinely unknowable at migration time, + emit a fixup pass after the shim that sets the field from a + database lookup or operator-supplied default. + +## See also + +- [`docs/concepts/untp-cirpass-mapping.md`](../concepts/untp-cirpass-mapping.md) +- [Migration shim plan](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/CIRPASS_2_MIGRATION.md) + — Phase 5 +- [Error Overview](index.md) diff --git a/docs/errors/MAP005.md b/docs/errors/MAP005.md new file mode 100644 index 0000000..a9c7ac8 --- /dev/null +++ b/docs/errors/MAP005.md @@ -0,0 +1,43 @@ +# MAP005 - Temporal Collapse + +## Description + +Source-side temporal semantics collapsed into a less-expressive +target shape — for example, UNTP's `validFrom` + `validUntil` pair +folded into a single CIRPASS `EffectivePeriod`. + +## Category + +Mapping Errors + +## Severity + +`warning` + +## When emitted + +Emitted by the UNTP ↔ CIRPASS shims via +[`compat/_mapping_codes.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/_mapping_codes.py) +constant `MAP_CODE_TEMPORAL_COLLAPSE`. The forward projection emits +`MAP005` whenever both endpoints of a UNTP validity period are +present and the target only carries one. + +## Common causes + +- UNTP payloads with both `validFrom` and `validUntil` projecting onto + CIRPASS `EffectivePeriod` (which carries the same information but + via a single nested shape). +- Reverse projection rebuilding a UNTP validity period from a CIRPASS + `EffectivePeriod` — same warning fires when the reverse shape + changes. + +## How to fix + +`MAP005` is informational; the projection preserves the temporal +information, only the surface shape changes. Suppress in CI with +`--accept-warnings` if the collapse is intentional. + +## See also + +- [`docs/concepts/untp-cirpass-mapping.md`](../concepts/untp-cirpass-mapping.md) +- [Error Overview](index.md) diff --git a/docs/faq.md b/docs/faq.md index 01fe309..bb4c5e1 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -12,7 +12,7 @@ dppvalidator is a Python library that validates Digital Product Passports (DPP) **Core capabilities:** -- **Validate** DPP JSON data through five validation layers +- **Validate** DPP JSON data through seven validation layers (plus a Layer 0 detection phase) - **Parse** DPP data into type-safe Pydantic models - **Export** validated passports to JSON-LD format for W3C Verifiable Credentials - **Verify** cryptographic signatures on signed credentials @@ -85,15 +85,15 @@ Optional extras: Currently supported: - **UNTP DPP 0.6.0** — supported (legacy) -- **UNTP DPP 0.6.1** — default -- **UNTP DPP 0.7.0** — fully supported +- **UNTP DPP 0.6.1** — supported (legacy) +- **UNTP DPP 0.7.0** — default Schema version is auto-detected from `@context` or `$schema` fields. You can also specify it explicitly: ```python -engine = ValidationEngine(schema_version="0.6.1") # current default -engine = ValidationEngine(schema_version="0.7.0") # opt in to v0.7 +engine = ValidationEngine(schema_version="0.7.0") # current default +engine = ValidationEngine(schema_version="0.6.1") # opt in to legacy v0.6 ``` For the full version-handling story see @@ -131,14 +131,19 @@ ______________________________________________________________________ ## Validation Questions -### What are the five validation layers? +### What are the seven validation layers? -1. **Layer 0: Schema Detection** — Auto-detects DPP schema version -1. **Layer 1: Schema Validation** — JSON Schema structure validation -1. **Layer 2: Model Validation** — Pydantic type validation -1. **Layer 3: JSON-LD Semantic** — Context expansion and term resolution -1. **Layer 4: Business Logic** — Vocabulary codes, date logic, GTIN checksums -1. **Layer 5: Cryptographic** — VC signature verification (optional) +1. **Layer 0: Detection** — Auto-detects DPP family + schema version +1. **Layer 1: Schema** — JSON Schema structure validation (`SCH001`–`SCH099`) +1. **Layer 2: Model** — Pydantic type validation (`MDL001`–`MDL099`) +1. **Layer 3: JSON-LD** — Context expansion and term resolution (`JLD001`–`JLD099`) +1. **Layer 4: Semantic** — Date logic, GTIN checksums, mass-fraction sums (`SEM001`–`SEM099`) +1. **Layer 5: Vocabulary** — ISO / UNECE / HS code lists (`VOC001`–`VOC099`) +1. **Layer 6: Plugin** — Per-pack rules (`TXT*`, `CQ*`, `TYR*`, …) +1. **Layer 7: Signature** *(reserved)* — VC signature verification (optional; codes pending) + +The canonical taxonomy lives in +[ADR 0006](adr/0006-validation-layer-taxonomy.md). ### Can I run only specific layers? @@ -157,15 +162,20 @@ engine = ValidationEngine(layers=["schema", "model", "semantic", "jsonld"]) ### What error codes does dppvalidator use? -| Prefix | Layer | Description | -| ------ | ---------- | ------------------------------ | -| `SCH` | Schema | JSON Schema validation errors | -| `MOD` | Model | Pydantic validation errors | -| `JLD` | JSON-LD | Context/term resolution errors | -| `SEM` | Semantic | Business rule violations | -| `VOC` | Vocabulary | Code list validation errors | -| `SIG` | Signature | Credential verification errors | -| `PRS` | Parse | Input parsing errors | +| Prefix | Surface | Description | +| ------ | ------------------------------- | ---------------------------------------------- | +| `SCH` | Schema (Layer 1) | JSON Schema validation errors | +| `MDL` | Model (Layer 2) | Pydantic validation errors | +| `JLD` | JSON-LD (Layer 3) | Context/term resolution errors | +| `SEM` | Semantic (Layer 4) | Business rule violations | +| `VOC` | Vocabulary (Layer 5) | Code list validation errors | +| `SIG` | Signature (Layer 7) | *Reserved*; verifier emits string errors today | +| `PRS` | Parse (pre-Layer 1) | Input parsing errors | +| `DET` | Detection (Layer 0) | Family-mismatch routing errors | +| `VER` | Version routing | UNTP version-mismatch errors | +| `UPG` | Upgrade shim (0.6 → 0.7) | Intra-family upgrade warnings | +| `MAP` | Migration shim (UNTP ↔ CIRPASS) | Cross-family mapping warnings | +| `PRT` | Advisory rules | Role-enum strictness and other advisory checks | ### How do I handle validation errors? diff --git a/docs/guides/cli-usage.md b/docs/guides/cli-usage.md index 83d31fe..4cadd9e 100644 --- a/docs/guides/cli-usage.md +++ b/docs/guides/cli-usage.md @@ -24,21 +24,21 @@ dppvalidator validate ... [options] - `-s, --strict` — Enable strict JSON Schema validation - `-f, --format` — Output format: `text`, `json`, `table` (default: text) -- `--schema-version` — Schema version (default: `0.6.1`; one of +- `--schema-version` — Schema version (default: `0.7.0`; one of `0.6.0`, `0.6.1`, `0.7.0`) - `--upgrade-from` — Run the v0.6 → v0.7 compat shim before validating (Phase 4); accepts `0.6.0` / `0.6.1` - `--fail-fast` — Stop on first error - `--max-errors` — Maximum errors to report (default: 100) -**v0.6.x examples:** +**v0.7.0 examples (default):** ```bash -# Validate a single file (default schema-version is 0.6.1) +# Validate a single file (default schema-version is 0.7.0) dppvalidator validate passport.json -# Pin v0.6.1 explicitly. A v0.7.0 payload through this command fails -# fast with VER001 (version mismatch). +# Pin v0.6.1 (legacy) explicitly. A v0.7.0 payload through this command +# fails fast with VER001 (version mismatch). dppvalidator validate passport.json --schema-version 0.6.1 # Validate multiple files @@ -60,11 +60,11 @@ dppvalidator validate "*.json" --format json dppvalidator validate "*.json" --format table ``` -**v0.7.0 examples:** +**v0.6.x examples (legacy):** ```bash -# Pin v0.7.0 explicitly. The detection layer otherwise auto-detects -# from the payload's @context URL. +# Pin v0.7.0 explicitly (matches the current default). The detection +# layer otherwise auto-detects from the payload's @context URL. dppvalidator validate passport-v07.json --schema-version 0.7.0 # Run the compat shim, then validate as v0.7.0. Upgrade warnings are @@ -121,7 +121,7 @@ dppvalidator schema [options] **Options (for info/download):** -- `-v, --version` — Schema version (default: 0.6.1) +- `-v, --version` — Schema version (default: 0.7.0) - `-o, --output` — Output directory for download **Examples:** @@ -130,12 +130,12 @@ dppvalidator schema [options] # List every registered version (currently 0.6.0, 0.6.1, 0.7.0). dppvalidator schema list -# Show schema info for v0.6.1. -dppvalidator schema info -v 0.6.1 - -# Show schema info for v0.7.0. +# Show schema info for v0.7.0 (current default). dppvalidator schema info -v 0.7.0 +# Show schema info for v0.6.1 (legacy). +dppvalidator schema info -v 0.6.1 + # Download schema to local directory. dppvalidator schema download -v 0.7.0 -o ./schemas/ ``` diff --git a/docs/guides/migration-0-6-to-0-7.md b/docs/guides/migration-0-6-to-0-7.md index 107d759..c6a49a6 100644 --- a/docs/guides/migration-0-6-to-0-7.md +++ b/docs/guides/migration-0-6-to-0-7.md @@ -188,7 +188,7 @@ warning explains why.** - [UNTP DPP versions](../concepts/untp-versions.md) — overall version handling, default version, detection rules. -- [Five-layer validation](../concepts/validation-layers.md) — how the +- [Seven-layer validation](../concepts/validation-layers.md) — how the upgraded payload then flows through validation. - [`upgrade_0_6_to_0_7.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/compat/upgrade_0_6_to_0_7.py) — full implementation of the 17 transformation steps. diff --git a/docs/index.md b/docs/index.md index ea3f080..036f133 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ ______________________________________________________________________ - :octicons-check-circle-16:{ .text-green } **Seven-Layer Validation** — Schema, Model, Semantic, JSON-LD, Vocabulary, Plugin, and Signature validation - :octicons-package-16: **UNTP DPP Schema Support** — Both **0.6.x** - (default) and **0.7.0** wire formats; auto-detected from + and **0.7.0** (default) wire formats; auto-detected from `@context` / `$schema` URLs. See [UNTP DPP versions](concepts/untp-versions.md). - :octicons-arrow-switch-16: **Compat shim 0.6 → 0.7** — diff --git a/docs/llms-ctx.txt b/docs/llms-ctx.txt index 0cf0b75..dce8b98 100644 --- a/docs/llms-ctx.txt +++ b/docs/llms-ctx.txt @@ -1,42 +1,68 @@ # dppvalidator - Extended Context for LLMs -> Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations and UNTP standards. +> Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations, the UNTP DPP specification, and the CIRPASS DPP reference structure. ## Overview -dppvalidator is the open-source compliance engine for EU Digital Product Passports. The EU's Ecodesign for Sustainable Products Regulation (ESPR) mandates Digital Product Passports for textiles starting 2027. This library ensures DPP data is compliant before production. +dppvalidator is the open-source compliance engine for EU Digital Product Passports. The EU's Ecodesign for Sustainable Products Regulation (ESPR) mandates Digital Product Passports for textiles starting 2027. This library ensures DPP data is compliant before production. dppvalidator validates two parallel families in the same release: **UNTP DPP** (UN/CEFACT Verifiable Credential format) and **CIRPASS DPP** (CIRPASS-2 hierarchical reference structure). ## Tech Stack - Python 3.10+ - Pydantic v2 for validation models - Included: httpx, jsonschema, pyld, cryptography, PyJWT -- Optional: rich (CLI formatting via `[cli]` extra) +- Optional extras: `[cli]` (rich CLI formatting), `[rdf]` (SHACL via rdflib + pyshacl), `[all]` (both) ## Installation ```bash # Using uv (recommended) -uv add dppvalidator # Full functionality -uv add "dppvalidator[cli]" # With rich CLI formatting +uv add dppvalidator # Full functionality +uv add "dppvalidator[cli]" # With rich CLI formatting +uv add "dppvalidator[rdf]" # With SHACL via rdflib + pyshacl +uv add "dppvalidator[all]" # Both extras # Or using pip pip install dppvalidator pip install "dppvalidator[cli]" +pip install "dppvalidator[rdf]" +pip install "dppvalidator[all]" ``` +## Supported specs + +| Family | Version | Default? | Wire shape highlight | +| ------- | ------- | -------- | ----------------------------------------------------------------------------- | +| UNTP | 0.6.0 | no | Same envelope as 0.6.1; minor schema-only fixes only. | +| UNTP | 0.6.1 | no | `credentialSubject` is a `ProductPassport` envelope wrapping `Product`. | +| UNTP | 0.7.0 | **yes** | `credentialSubject` IS the `Product` directly. | +| CIRPASS | 1.3.0 | (cross-family) | Hierarchical message: `dppIdentifier`, `product`, `relatedActors`, `composition`, `substancesOfConcern`, `lca`, `connectorRelations`. | + +The default UNTP version lives in `dppvalidator.schemas.registry.DEFAULT_SCHEMA_VERSION` (currently `0.7.0`). Auto-detection from `@context` / `$schema` URLs picks the family and version; pin explicitly with `--target` (family) and `--schema-version` (version), or `target=` / `schema_version=` in Python. + ## Architecture -### Seven-Layer Validation +### Seven-Layer Validation (plus Layer 0 detection) -0. **Schema Detection**: Auto-detect DPP schema version from $schema/@context +0. **Detection**: Auto-detect DPP family + schema version from `$schema`/`@context` 1. **Schema Layer (SCH001-SCH099)**: JSON Schema Draft 2020-12 validation -2. **Model Layer (MOD001-MOD099)**: Pydantic v2 type validation and coercion -3. **Semantic Layer (SEM001-SEM099)**: Business rules, ISO codes, GTIN checksums -4. **JSON-LD Layer (JLD001-JLD099)**: PyLD expansion, context resolution -5. **Vocabulary Layer (VOC001-VOC099)**: External code lists, ontology validation -6. **Plugin Layer**: Custom validator plugins via entry points -7. **Signature Layer (SIG001-SIG099)**: VC signature verification, DID resolution +2. **Model Layer (MDL001-MDL099)**: Pydantic v2 type validation and coercion +3. **JSON-LD Layer (JLD001-JLD099)**: PyLD expansion, context resolution +4. **Semantic Layer (SEM001-SEM099)**: Business rules, GTIN checksums, mass-fraction sums +5. **Vocabulary Layer (VOC001-VOC099)**: External code lists (ISO 3166, UNECE Rec 20/46, HS) +6. **Plugin Layer**: Per-pack rules (`TXT*`, `CQ*`, `TYR*`, …) via entry-point plugins +7. **Signature Layer (SIG001-SIG099 reserved)**: VC signature verification, DID resolution. The verifier currently surfaces untyped error strings via `VerificationResult.errors`; the SIG prefix is reserved for future structured codes. + +### Non-layer error prefixes + +- `PRS` — Input parsing (file IO, JSON syntax) +- `DET` — Family-mismatch routing (`DET001` when `--target` contradicts detection) +- `VER` — Version-mismatch routing +- `UPG` — UNTP 0.6 → 0.7 upgrade-shim warnings (`UPG001`–`UPG004`) +- `MAP` — Cross-family migration-shim warnings (`MAP001`–`MAP005`) +- `PRT` — Advisory rules (e.g. role-enum strictness) + +The canonical layer + prefix table is pinned by [ADR 0006](https://artiso-ai.github.io/dppvalidator/adr/0006-validation-layer-taxonomy/). ### Performance @@ -62,6 +88,12 @@ engine = ValidationEngine(layers=["model", "semantic"]) # Strict mode (warnings become errors) engine = ValidationEngine(strict_mode=True) +# Pin a UNTP version (auto-detection bypassed) +engine = ValidationEngine(schema_version="0.7.0") + +# Pin a family (CIRPASS reference structure) +engine = ValidationEngine(target="cirpass") + # Validate result = engine.validate(dpp_dict) # Returns: ValidationResult with .valid, .errors, .warnings, .validation_time_ms @@ -74,7 +106,8 @@ from dppvalidator.models import ( DigitalProductPassport, CredentialIssuer, Product, - IdentifierScheme, + ProductBatch, + Identifier, Link, ) @@ -84,13 +117,30 @@ passport = DigitalProductPassport( ) ``` +Per-version model packages live at `dppvalidator.models.v0_6.*` and +`dppvalidator.models.v0_7.*`; the top-level `dppvalidator.models` re- +exports the v0.6 surface for back-compat. CIRPASS models are at +`dppvalidator.models.cirpass.v1_3.*`. + ### Exporters ```python -from dppvalidator.exporters import JSONLDExporter +from dppvalidator.exporters import JSONLDExporter, EUDPPJsonLDExporter -exporter = JSONLDExporter() -jsonld = exporter.export(passport) # W3C Verifiable Credential format +JSONLDExporter().export(passport) # UNTP / W3C VC format +EUDPPJsonLDExporter(map_terms=True).export(passport) # EU DPP-aligned +``` + +### Migration shims + +```python +from dppvalidator.compat import upgrade # UNTP 0.6 → 0.7 (intra-family) + +upgraded, warnings = upgrade(payload_v06, country_lookup={"DE": "Germany"}) +# warnings is a list of UpgradeWarning(code=UPG001..UPG004, ...) + +# Cross-family (UNTP 0.7 ↔ CIRPASS 1.3) lives behind the migrate CLI +# and emits MAP001..MAP005 warnings. ``` ### Plugin System @@ -102,20 +152,15 @@ from dppvalidator.validators import ValidationEngine # Define a validator implementing the SemanticRule protocol class CustomValidator: rule_id = "CUSTOM001" - description = "Custom validation rule" + description = "..." severity = "error" def check(self, passport): - """Return list of (json_path, error_message) tuples.""" violations = [] - # Custom validation logic return violations -# Manual registration registry = PluginRegistry(auto_discover=False) registry.register_validator("custom", CustomValidator) - -# Or use entry points for automatic discovery engine = ValidationEngine(load_plugins=True) ``` @@ -124,28 +169,46 @@ engine = ValidationEngine(load_plugins=True) ```bash dppvalidator validate passport.json dppvalidator validate passport.json --strict +dppvalidator validate passport.json --schema-version 0.7.0 +dppvalidator validate passport.json --target cirpass dppvalidator export passport.json --format jsonld --output out.jsonld -dppvalidator schema --version 0.6.1 +dppvalidator schema --version 0.7.0 + +# Migration shims +dppvalidator migrate passport-v06.json -o passport-v07.json +dppvalidator migrate passport-v07.json --to cirpass-1.3 -o cirpass.json --accept-warnings +dppvalidator migrate cirpass.json --to untp-0.7 -o untp.json --accept-warnings ``` +CLI exit codes are documented at + +and pinned by [ADR 0005](https://artiso-ai.github.io/dppvalidator/adr/0005-cli-exit-codes/). + ## Directory Structure ``` src/dppvalidator/ -├── models/ # Pydantic models for DPP entities -├── validators/ # Validation engine and layers -├── verifier/ # Signature and credential verification -├── exporters/ # JSON-LD and other export formats -├── schemas/ # JSON Schema loading and caching -├── vocabularies/ # Controlled vocabulary loading -├── cli/ # Command-line interface -├── plugins/ # Plugin system -└── __init__.py +├── models/ +│ ├── v0_6/ # UNTP 0.6.x models +│ ├── v0_7/ # UNTP 0.7.0 models +│ └── cirpass/v1_3/ # CIRPASS DPP reference structure +├── validators/ +│ ├── rules/v0_6/ # UNTP 0.6 semantic rules +│ ├── rules/v0_7/ # UNTP 0.7 semantic rules +│ └── rules/cirpass_v1_3/ # CIRPASS quality rules (CQ*) +├── compat/ # Cross-version + cross-family shims +├── verifier/ # VC signature verification +├── exporters/ # JSON-LD and EU DPP exporters +├── schemas/ # Schema loading + version registry +├── vocabularies/ # Controlled vocabularies + EU DPP ontology +├── cli/ # CLI entry points +└── plugins/ # Plugin entry-points discovery ``` ## Related Standards - UNTP Digital Product Passport: https://untp.unece.org/docs/specification/DigitalProductPassport/ +- CIRPASS-2 (EU DPP): https://dpp.vocabulary-hub.eu/specifications - EU ESPR Regulation: https://environment.ec.europa.eu/topics/circular-economy/ecodesign-sustainable-products-regulation_en - W3C Verifiable Credentials: https://www.w3.org/TR/vc-data-model/ diff --git a/docs/llms.txt b/docs/llms.txt index f16f9c3..3d83232 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,21 +1,24 @@ # dppvalidator -> Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations and UNTP standards. +> Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations, the UNTP DPP specification, and the CIRPASS DPP reference structure. -dppvalidator is the open-source compliance engine for EU Digital Product Passports. Starting 2027, every textile product sold in the EU must have a Digital Product Passport. This library ensures your DPP data is compliant before production. +dppvalidator is the open-source compliance engine for EU Digital Product Passports. Starting 2027, every textile product sold in the EU must have a Digital Product Passport. This library ensures your DPP data is compliant before production. dppvalidator validates two parallel families in the same release: **UNTP DPP** (UN/CEFACT Verifiable Credential format) and **CIRPASS DPP** (CIRPASS-2 hierarchical reference structure). ## Core Features -- Seven-layer validation: schema, model, semantic, JSON-LD, vocabulary, plugin, and signature -- Built-in UNTP DPP 0.6.1 schema support -- JSON-LD export for W3C Verifiable Credentials -- Plugin system for custom validators -- CLI and Python API +- Seven-layer validation: schema, model, JSON-LD, semantic, vocabulary, plugin, and signature (plus a Layer 0 detection phase) +- Schema support: UNTP DPP 0.6.0 / 0.6.1 / 0.7.0 (default) and CIRPASS DPP reference structure 1.3.0 +- JSON-LD export for W3C Verifiable Credentials; EU DPP-aligned export with term mapping +- Migration shims: `dppvalidator migrate` between UNTP 0.6 ↔ 0.7 and UNTP 0.7 ↔ CIRPASS 1.3 +- Plugin system for custom validators (textile / CIRPASS quality / tyre packs already shipped) +- CLI and Python API; Python 3.10+ ## Installation -``` +```bash pip install dppvalidator +pip install "dppvalidator[cli]" # rich CLI formatting +pip install "dppvalidator[rdf]" # SHACL via rdflib + pyshacl ``` ## Quick Example diff --git a/docs/plans/DOCS_COHERENCE_PLAN.md b/docs/plans/DOCS_COHERENCE_PLAN.md new file mode 100644 index 0000000..98ff3e3 --- /dev/null +++ b/docs/plans/DOCS_COHERENCE_PLAN.md @@ -0,0 +1,467 @@ +# Documentation Coherence Plan + +**Status**: Draft — 2026-05-10 +**Scope**: surgical fixes for cross-source documentation drift surfaced after +the 0.5.0 (Preview) cut. +**Out of scope**: rewriting concept pages, adding new guides, regenerating +benchmarks, plugin docs. + +This plan is mechanical wherever possible. Each phase has a scoped touch +list, an exit gate, and a verification step. Phases 0–2 are blocking +(they encode decisions every later phase consults); 3–7 can land in +parallel once 0–2 are merged. + +______________________________________________________________________ + +## Audit summary + +Eight independent drift classes were found. The first three are +load-bearing for everything else: + +| ID | Class | Severity | Phase | +| -- | -------------------------------------------- | -------- | ----- | +| A | Default UNTP version: docs say `0.6.1`, code says `0.7.0` since 0.5.0 | High | 1 | +| B | Validation-layer taxonomy split (`five` vs `seven`; 5/6/7/8 numberings) | High | 0 + 2 | +| C | Error-code prefix drift (`MOD` in docs, `MDL` in code; missing `MAP`/`DET`; phantom `SIG`) | High | 2 | +| D | `docs/changelog.md` last entry is `0.1.0`; root `CHANGELOG.md` is at `0.5.0` | Medium | 3 | +| E | mkdocs orphan files in `docs/` not in nav and not in `exclude_docs:` | Medium | 4 | +| F | Project framing omits CIRPASS-2 (`mkdocs.yml site_description`, `llms*.txt`, README hero) | Medium | 5 | +| G | `llms.txt` (root) and `docs/llms.txt` are out of sync with each other | Medium | 5 | +| H | `AGENTS.md` / `CLAUDE.md` claim default `0.6.1` and omit CIRPASS family | Low | 6 | + +No CI guard catches any of these today. Phase 7 closes that gap. + +______________________________________________________________________ + +## Phase 0 — Pick a canonical layer taxonomy (decision-only) + +**Why blocking**: Phases 2, 3, 5, 6 all rewrite layer counts and error-code +prefixes. They cannot proceed until one taxonomy is canonical. + +The current state mixes four taxonomies: + +| Source | Count | Names | +| ------ | ----- | ----- | +| [README.md:20](../../README.md#L20), [docs/index.md:20](../index.md#L20) | 7 | Schema, Model, Semantic, JSON-LD, Vocabulary, Plugin, Signature | +| [README.md:253](../../README.md#L253) mermaid + [docs/concepts/validation-layers.md](../concepts/validation-layers.md) body | 6 | Detection, Schema, Model, JSON-LD, Business, Cryptographic | +| [docs/llms-ctx.txt:30–39](../llms-ctx.txt) | 8 | Detection, Schema, Model, Semantic, JSON-LD, Vocabulary, Plugin, Signature | +| [docs/faq.md:15, :134](../faq.md), [llms.txt](../../llms.txt), [docs/IMPLEMENTATION_PLAN.md:352](../IMPLEMENTATION_PLAN.md#L352), [README.md:402](../../README.md#L402) | 5 | Schema, Model, Semantic, JSON-LD, Cryptographic | + +### Action + +Open an ADR — `docs/adr/0006-validation-layer-taxonomy.md` — recording +the decision. Recommended canonical taxonomy (matches what the engine +code actually emits today): + +``` +Layer 0 Detection (no error prefix; routing only) +Layer 1 Schema SCH001–SCH099 +Layer 2 Model MDL001–MDL099 ← NOT MOD +Layer 3 JSON-LD JLD001–JLD099 +Layer 4 Semantic SEM001–SEM099 +Layer 5 Vocabulary VOC001–VOC099 +Layer 6 Plugin (per-plugin; e.g. TXT001–TXT099, CQ001–CQ099, TYR001–TYR099) +Layer 7 Signature (verifier/, no prefix registered yet — see Phase 2) +``` + +This is the "seven layers + detection" framing, and it's the only one +where every layer has an emitter in the codebase today. Cross-cutting +codes (`PRS`, `VER`, `UPG`, `PRT`, `MAP`, `DET`) are documented as +"non-layer" codes in the same ADR. + +### Exit gate + +- ADR merged. +- Single sentence in [docs/concepts/validation-layers.md](../concepts/validation-layers.md) + intro pins the chosen count and links the ADR. + +______________________________________________________________________ + +## Phase 1 — Default UNTP version drift (`0.6.1` → `0.7.0`) + +The 0.5.0 changelog records the flip +(`DEFAULT_VERSIONS[UNTP]: "0.6.1" → "0.7.0"` at +[src/dppvalidator/schemas/registry.py:191](../../src/dppvalidator/schemas/registry.py#L191)), +but the public docs still tell users the default is `0.6.1`. + +### Touch list + +Mechanical edits — every occurrence of "default 0.6.1", "current default += 0.6.1", or `default_version → '0.6.1'` flips to `0.7.0` and the +example payloads should follow: + +- [docs/index.md:21–22](../index.md#L21) — `(default)` annotation moves from `0.6.x` to `0.7.0`. +- [docs/faq.md:88, :95, :114](../faq.md) — version table marker; `# current default` comment; `--upgrade-from` example unchanged but reframe. +- [docs/concepts/untp-versions.md:18, :77, :90](../concepts/untp-versions.md) — table marker, comment in CLI block, `print(reg.default_version)`. +- [docs/concepts/validation-layers.md:71, :79–81](../concepts/validation-layers.md) — `(currently 0.6.1)` and pin example. +- [docs/guides/cli-usage.md:27–30, :37, :42, :124, :134, :165](../guides/cli-usage.md) — every `default: 0.6.1` and the comment-only examples. +- [docs/llms-ctx.txt:128](../llms-ctx.txt) — `dppvalidator schema --version 0.6.1` example flips. +- [docs/llms.txt:10](../llms.txt), [llms.txt:10](../../llms.txt) — "Built-in UNTP DPP 0.6.1 schema support" → "UNTP DPP 0.6.x + 0.7.0 + CIRPASS-2 v1.3.0" (handled fully in Phase 5; Phase 1 just removes the false `0.6.1`-only claim). +- [AGENTS.md](../../AGENTS.md) — "Default version: ... (currently 0.6.1)" → `0.7.0` with a note that `compat.active_version()` is the runtime accessor (already mandated by [.claude/rules/untp-versioning.md](../../.claude/rules/untp-versioning.md)). + +### Verification + +```bash +# Should return zero hits in user-facing docs after Phase 1. +rg -n '(default|currently)\s*[:=]?\s*[`"]?0\.6\.1' \ + README.md docs/ AGENTS.md CLAUDE.md llms.txt \ + --glob '!docs/plans/**' --glob '!docs/IMPLEMENTATION_PLAN.md' +``` + +### Exit gate + +- Grep above is clean. +- Cardinal rule §1 (no bare UNTP version literals outside `registry.py` / + `contexts.py`) is unaffected — these are docs, not code. + +______________________________________________________________________ + +## Phase 2 — Error-code prefix and layer-numbering rewrite + +Driven by Phase 0. Rewrite every place that names error prefixes or layer +numbers. + +### 2a · `MOD` → `MDL` (concrete bug) + +The actual code emits `MDL001`–`MDL099` +([src/dppvalidator/validators/model.py:93](../../src/dppvalidator/validators/model.py#L93), +[src/dppvalidator/validators/errors.py:61](../../src/dppvalidator/validators/errors.py#L61)), +and every `docs/errors/MDL*.md` file uses that prefix. The docs' `MOD` +references are dead links. + +Touch list: + +- [README.md:292](../../README.md#L292) — mermaid edge label. +- [docs/concepts/validation-layers.md:44, :138](../concepts/validation-layers.md) — mermaid + body. +- [docs/llms-ctx.txt:34](../llms-ctx.txt). +- [docs/reference/api/validators.md:107](../reference/api/validators.md#L107). +- [docs/faq.md:160–168](../faq.md) error-code prefix table. + +### 2b · Add `MAP` and `DET` to the public error catalogue + +`MAP001`–`MAP005` are emitted by the cross-family compat shims +([src/dppvalidator/compat/_untp_cirpass_map.py](../../src/dppvalidator/compat/_untp_cirpass_map.py), +[src/dppvalidator/compat/cirpass_1_3_to_untp_0_7.py](../../src/dppvalidator/compat/cirpass_1_3_to_untp_0_7.py)), +and `DET001` by family detection +([src/dppvalidator/validators/detection.py:36](../../src/dppvalidator/validators/detection.py#L36)). +Neither has a `docs/errors/*.md` page or a [mkdocs.yml](../../mkdocs.yml) +nav entry. + +Add: + +- `docs/errors/DET001.md` (Family Mismatch). +- `docs/errors/MAP001.md`–`MAP005.md` (one per code; bodies can be + short — title, when-emitted, fix suggestion, link to the relevant + shim). +- Two new sections in [mkdocs.yml](../../mkdocs.yml) `nav.Errors`: + `Detection Errors` (`DET001`) and `Mapping Errors` (`MAP001`–`MAP005`). +- A row each in [docs/faq.md:160–168](../faq.md) error-code prefix table. + +### 2c · `SIG` prefix audit + +Multiple docs claim `SIG001`–`SIG099` +([README.md:295](../../README.md#L295), +[docs/concepts/validation-layers.md:46, :188](../concepts/validation-layers.md), +[docs/llms-ctx.txt:39](../llms-ctx.txt)), +but `rg "SIG0\d{2}" src/` returns nothing. Either: + +- the verifier emits codes under a different prefix (`MDL010` family + covers issuer errors; signature failures may currently surface as + `MDL` or untyped exceptions) — in which case the docs are wrong, or +- the prefix is reserved for a planned phase and should be marked + *Reserved* until codes ship. + +**Action**: confirm by reading +[src/dppvalidator/verifier/](../../src/dppvalidator/verifier/) and +[src/dppvalidator/validators/results.py](../../src/dppvalidator/validators/results.py). +Fix whichever side is wrong; do not invent codes to satisfy docs. + +### 2d · Layer-numbering rewrite + +Rewrite all four off-canon framings to match the Phase 0 ADR. Highest- +churn files: + +- [README.md:253–296](../../README.md#L253) — mermaid + table; rename + the section "Seven-Layer Validation Architecture" and add Vocabulary + / Plugin / Signature subgraphs (or document why mermaid only renders + the dispatch path). +- [README.md:402](../../README.md#L402) — "five-layer architecture" + → "seven-layer architecture". +- [docs/concepts/validation-layers.md](../concepts/validation-layers.md) — + expand body to match title; either add Vocabulary / Plugin / + Signature sections or rename to "Six-Layer". (Recommendation: add + the missing sections — they exist in code, just not in prose.) +- [docs/faq.md:15, :134–141](../faq.md) — flip "five" → "seven"; + rewrite the numbered list. +- [docs/IMPLEMENTATION_PLAN.md:352, :957](../IMPLEMENTATION_PLAN.md) — + internal plan; quickest fix is to mark it historical (this file + becomes Phase 4 candidate for `exclude_docs:`). +- [llms.txt:9](../../llms.txt), [docs/llms.txt:9](../llms.txt) — flip + to canonical seven-layer phrasing (Phase 5 regenerates these + fully). + +### Exit gate + +```bash +# After Phase 2 these should all return zero hits in user-facing docs: +rg -n 'MOD0\d{2}' README.md docs/ --glob '!docs/plans/**' +rg -n '(five|5)[\s-]?(layer|validation layer)' README.md docs/ llms.txt --glob '!docs/plans/**' +``` + +______________________________________________________________________ + +## Phase 3 — Changelog reconciliation + +[docs/changelog.md](../changelog.md) stops at `[0.1.0] - 2026-01-29`. +The root [CHANGELOG.md](../../CHANGELOG.md) is at `[0.5.0] - 2026-05-09 (Preview)`. + +### Action + +Pick one of two patterns; both are common: + +1. **Mirror via include** — replace [docs/changelog.md](../changelog.md) + contents with a one-line snippet: + + ```markdown + --8<-- "CHANGELOG.md" + ``` + + `pymdownx.snippets` (already loaded in [mkdocs.yml:84](../../mkdocs.yml#L84)) + will inline it at build time. Add `CHANGELOG.md` to the snippets + `base_path`. + +2. **Symlink** — `ln -s ../../CHANGELOG.md docs/changelog.md`. Simpler, + but breaks `edit_uri` links. + +Recommended: option 1. + +### Exit gate + +`docs/changelog.md` rendered output equals root `CHANGELOG.md` content +on every build. Add a CI assertion (Phase 7). + +______________________________________________________________________ + +## Phase 4 — mkdocs build hygiene + +Today [.github/workflows/docs.yml:24](../../.github/workflows/docs.yml#L24) +runs `mkdocs gh-deploy --force` without `--strict`, so orphan files in +`docs/` silently ship to the deployed site without nav placement. + +### Inventory of orphans + +Files / directories in `docs/` that are neither in `nav:` nor matched by +`exclude_docs:`: + +- `docs/IMPLEMENTATION_PLAN.md` +- `docs/IMPROVEMENT_ROADMAP.md` +- `docs/REFACTORING_PLAN.md` +- `docs/STRATEGIC_ROADMAP.md` +- `docs/UNTTP_PLUGIN_PLAN.md` +- `docs/VC_WALLET_ROADMAP.md` +- `docs/dpp_validator_description.md` +- `docs/plugins.md` (note: `docs/plugins/tyres.md` *is* in nav) +- `docs/contributing/SECURITY_SETUP.md` +- `docs/dpp/` (raw schema fixtures: `dpp.json`, `jsonschema.json`) +- `docs/uv/uv.md` +- `docs/windsurf/cascade-reference.md` + +### Action + +Decide per file (one of three): + +1. **Keep + add to nav** — only for files that are user-facing (e.g. + `docs/plugins.md` may merit a top-level Plugins overview; promote it + into nav above the per-plugin pages). +2. **Move to `docs/plans/`** — internal plans / roadmaps. `plans/` is + already in `exclude_docs:`, so nothing else to do. +3. **Delete** — if superseded (e.g. + `docs/dpp_validator_description.md`, `docs/dpp/*.json`, + `docs/uv/uv.md`, `docs/windsurf/cascade-reference.md` look like + scratch artefacts; verify with `git log` first). + +Recommended split: + +- → `docs/plans/`: `IMPLEMENTATION_PLAN.md`, `IMPROVEMENT_ROADMAP.md`, + `REFACTORING_PLAN.md`, `STRATEGIC_ROADMAP.md`, `UNTTP_PLUGIN_PLAN.md`, + `VC_WALLET_ROADMAP.md`. +- → `nav:` (Contributing tab): `contributing/SECURITY_SETUP.md`. +- → `nav:` (Plugins overview, above tyres): `docs/plugins.md` if it's + the canonical overview; otherwise delete. +- → delete (after `git log` review): `docs/dpp/`, `docs/uv/`, + `docs/windsurf/`, `docs/dpp_validator_description.md`. + +### Then + +Add `--strict` to [.github/workflows/docs.yml](../../.github/workflows/docs.yml): + +```yaml +- name: Build docs (strict) + run: uv run mkdocs build --strict +- name: Deploy docs + if: github.ref == 'refs/heads/main' + run: uv run mkdocs gh-deploy --force +``` + +`--strict` will additionally catch broken cross-doc links from Phase 1/2 +and any new orphans. + +### Exit gate + +`uv run mkdocs build --strict` exits 0. + +______________________________________________________________________ + +## Phase 5 — `llms*.txt` and project framing + +### 5a · Single-source `llms.txt` + +There are two copies — [llms.txt](../../llms.txt) (repo root) and +[docs/llms.txt](../llms.txt) — and they disagree (root says +"Five-layer", docs/ says "Seven-layer", both claim `UNTP DPP 0.6.1` +support without `0.7.0` or CIRPASS-2). + +Action: make root `llms.txt` a symlink to `docs/llms.txt` (or vice +versa) and rewrite once. Same for [llms-ctx.txt](../../llms-ctx.txt) ↔ +[docs/llms-ctx.txt](../llms-ctx.txt). + +Update content (post-Phase 0/1/2): + +- "Seven-layer validation: schema, model, semantic, JSON-LD, vocabulary, + plugin, signature". +- "Schema support: UNTP DPP 0.6.0 / 0.6.1 / 0.7.0 (default) and CIRPASS + v1.3.0". +- Add `[rdf]` extra to the install snippet + ([docs/llms-ctx.txt:25](../llms-ctx.txt) currently omits it). +- Add the `MDL` / `MAP` / `DET` prefixes wherever `MOD` / `SIG` were + cited. + +### 5b · `mkdocs.yml site_description` and README hero + +- [mkdocs.yml:2](../../mkdocs.yml#L2): currently + `Python library for validating Digital Product Passports (UNTP DPP)`. + Sync to `pyproject.toml` description (which already names CIRPASS): + `Python library for validating Digital Product Passports (DPP) + according to EU ESPR regulations and CIRPASS/UNECE ontologies`. +- [README.md:14](../../README.md#L14): "according to EU ESPR + regulations and UNTP standards" — extend to mention CIRPASS-2 and + UNTP, since 0.5.0 ships both as first-class families. + +### Exit gate + +`pyproject.toml` description, `mkdocs.yml site_description`, +`README.md` hero, and `llms*.txt` Overview lines describe the same +scope using CIRPASS + UNTP + ESPR phrasing. + +______________________________________________________________________ + +## Phase 6 — `AGENTS.md` / `CLAUDE.md` repo-instruction sync + +[AGENTS.md](../../AGENTS.md) is imported by +[CLAUDE.md](../../CLAUDE.md), so one edit covers both. + +Touch list: + +- "Default version: ... (currently `0.6.1`)" → `0.7.0`. (Already in + Phase 1 grep, listed here for completeness.) +- "dppvalidator supports **UNTP DPP 0.6.x and 0.7.0** in the same + release" → extend with "and CIRPASS DPP reference structure 1.3.0 + (`SchemaFamily.CIRPASS`)". +- Tech-stack block: add `[rdf]` extra and the SHACL story; mention + `dppvalidator.compat.active_version()` (already mentioned but worth + surfacing in the version-handling section). +- Directory tree: add `models/cirpass/v1_3/` and + `validators/rules/cirpass_v1_3/` (both shipped in 0.5.0). + +### Exit gate + +`AGENTS.md` factually matches `src/dppvalidator/` tree and the 0.5.0 +changelog. + +______________________________________________________________________ + +## Phase 7 — CI guards (regression prevention) + +Without guards, the same drift will recur on every release. Add three: + +### 7a · Strict mkdocs build (covered in Phase 4) + +```yaml +# .github/workflows/docs.yml +- run: uv run mkdocs build --strict +``` + +Catches: orphan files, broken intra-doc links, undefined nav targets. + +### 7b · Pre-commit "no stale default-version literal" guard + +A small `tools/check_doc_default_version.py` script that greps user- +facing docs for `default.*0\.\d+\.\d+` and asserts the version matches +`DEFAULT_VERSIONS[SchemaFamily.UNTP]` from +[src/dppvalidator/schemas/registry.py](../../src/dppvalidator/schemas/registry.py). +Wire it into `.pre-commit-config.yaml` and the `ci.yml` lint job. + +This is the docs-side analogue of the existing +[tests/unit/test_no_version_literals.py](../../tests/unit/test_no_version_literals.py) +guard for source code. + +### 7c · Error-code-prefix coverage test + +`tests/unit/test_doc_error_code_coverage.py`: + +- Walk `src/` for every `\b(SCH|MDL|JLD|SEM|VOC|PRS|PRT|VER|UPG|MAP|DET|TXT|CQ|TYR)\d{3}\b` literal. +- Assert each appears in `mkdocs.yml` `nav.Errors` AND has a + corresponding `docs/errors/.md`. +- Inverse: every `docs/errors/.md` file maps back to a code + emitted by `src/`. + +This catches both the `MOD`/`MDL` class of typos *and* future +unmaintained docs/errors entries. + +### Exit gate + +All three guards green on `develop` after Phases 0–6 land. + +______________________________________________________________________ + +## Sequencing and PR boundaries + +| Phase | Depends on | Suggested PR | +| ----- | ---------- | ------------ | +| 0 | — | `docs(adr): canonical validation-layer taxonomy` | +| 1 | 0 | `docs: flip default UNTP version to 0.7.0` | +| 2a | 0 | `docs: rename MOD error-code prefix to MDL` | +| 2b | 0 | `docs(errors): add DET and MAP error pages` | +| 2c | 0 | `docs(errors): audit SIG prefix` (may be a code-side fix) | +| 2d | 0, 2a | `docs: align validation-layer taxonomy across all sources` | +| 3 | — | `docs: include root CHANGELOG via snippets` | +| 4 | 1, 2, 3 | `docs: mkdocs build hygiene + --strict` | +| 5 | 0, 1, 2 | `docs: refresh llms*.txt and CIRPASS-2 framing` | +| 6 | 1, 5 | `docs: sync AGENTS.md / CLAUDE.md with 0.5.0 reality` | +| 7 | 4 | `ci: docs coherence guards` | + +Total: ~10 small PRs. Each phase is independently revertible. + +______________________________________________________________________ + +## Verification checklist (final) + +After all phases land, this single command should be silent: + +```bash +# Default-version drift +! rg -nq '(default|currently).{0,20}\b0\.6\.1\b' README.md docs/ llms.txt llms-ctx.txt AGENTS.md \ + --glob '!docs/plans/**' +# Layer-count drift +! rg -nq '\bfive[ -](validation )?layer' README.md docs/ llms.txt --glob '!docs/plans/**' +# Stale error-code prefix +! rg -nq '\bMOD0\d{2}\b' README.md docs/ --glob '!docs/plans/**' +# Strict mkdocs build +uv run mkdocs build --strict +# Pre-commit / CI guards +uv run pytest tests/unit/test_doc_error_code_coverage.py -v +``` + +When every line of that block exits 0, the documentation surface is +coherent with the codebase as of 0.5.0 and protected against the same +drift recurring at 0.6.0. diff --git a/docs/plans/GROWTH_PLAN.md b/docs/plans/GROWTH_PLAN.md new file mode 100644 index 0000000..4f70377 --- /dev/null +++ b/docs/plans/GROWTH_PLAN.md @@ -0,0 +1,678 @@ +# dppvalidator Growth Plan + +> **Version**: 1.0 +> **Created**: 2026-05-10 +> **Status**: Strategic — adoption & reach +> **Audience**: Maintainers, ARTISO leadership, contributors +> **Pairs with**: [`STRATEGIC_ROADMAP.md`](STRATEGIC_ROADMAP.md) (capability roadmap) +> and [`IMPROVEMENT_ROADMAP.md`](IMPROVEMENT_ROADMAP.md) (technical gap closure). + +This document focuses on **distribution, discoverability, and adoption** — not on what we +build, but on how the work we ship becomes the default tool engineers reach for when they +hear "Digital Product Passport." The technical roadmaps already chart capability growth; +this plan layers a growth strategy on top, with explicit emphasis on the **agent-native +ecosystem** (MCP, Claude Code plugins, llms.txt) where dppvalidator can win +disproportionately because virtually no DPP tool is positioned for it today. + +______________________________________________________________________ + +## Executive summary + +| Layer | Today (2026-05-10) | 12-month target | +| ------------------------- | ------------------------ | -------------------------------- | +| PyPI downloads / month | **176** | **5,000–10,000** | +| GitHub stars | **8** | **600+** | +| GitHub forks | **0** | **30+** | +| External contributors | 0 | **8+** (≥ 3 from non-ARTISO) | +| Reachable agents (MCP) | 0 | **All major MCP clients** | +| Claude Code installs | 0 | **listed in official marketplace** | +| Listed in DPP registries | 0 | UN/CEFACT, CIRPASS-2, EU DPP-WG | +| Reference integrations | 1 (CI snippet) | **8+** (FastAPI, Action, Docker, dbt, Airflow, Shopify, Cursor, ChatGPT) | + +**Strategic thesis (one paragraph).** dppvalidator is technically ahead of every public +DPP validator we can find (7-layer pipeline, dual-family UNTP + CIRPASS-2 support, plugin +system, JSON-LD and VC verification). It is **not** ahead on discovery: a brand engineer +hitting the 2027 ESPR deadline and asking ChatGPT/Claude/Cursor "how do I validate a DPP +in Python?" today gets nothing useful, because the model has nothing in its training set +and no tool to call. The single highest-leverage growth move is therefore to make +dppvalidator **the validator that LLM agents reach for**, by shipping an MCP server, a +Claude Code plugin, and richly cross-linked llms.txt content. The mandate timeline (ESPR +textiles **2027-07**) gives us a ~14-month window before the market hardens around +whichever tools are easiest to find. + +______________________________________________________________________ + +## Critical evaluation of the current state + +### What is already good (do not rebuild) + +- **Engineering quality.** Multi-Python CI, mutation testing, property tests, ≥90 % + coverage, ruff + ty, SBOM, pip-audit. This is a credibility asset — most OSS in the + ESPR space is at "v0.1.0, no tests" maturity. The plan should *amplify* this signal, + not duplicate it. +- **Two specs in one package.** `dppvalidator migrate` between UNTP 0.6/0.7 and the + CIRPASS-2 reference structure 1.3 is a unique selling point. No other tool we found + does this. +- **Documentation surface.** mkdocs site, [`llms.txt`](../llms.txt) and + [`llms-ctx.txt`](../llms-ctx.txt) are already published — better than 95 % of OSS + Python packages. +- **Plugin architecture & licence isolation.** Cleanly separates MIT core from + GPL-3.0 plugins; lets us ship industry packs without licence drag on the core. + +### Where growth is bottlenecked (this plan's targets) + +| # | Gap | Cost of leaving it | Leverage | +| -- | ------------------------------------------------------- | ------------------------------ | --------------------------------------- | +| 1 | **No agent-callable surface** (no MCP server) | Invisible to ChatGPT/Claude/Cursor agents in 2026 | **Highest** — primary thesis | +| 2 | **No Claude Code plugin** for end users | Loses every "vibe coder" brand engineer | **High** — small effort, large signal | +| 3 | **PyPI metadata is dev-only** | Doesn't surface to non-Python searchers | Medium — quick wins in keywords/READMEs | +| 4 | **No UNTP/CIRPASS conformance badge or report** | Buyers cannot verify our claims | High — authority asset | +| 5 | **No reference integrations** (Action, Docker, FastAPI starter, dbt, Airflow) | Each is a missing onramp | Medium-high — multiplies install paths | +| 6 | **No comparison/alternatives content** | Loses Google + LLM SEO | Medium — write once, reused forever | +| 7 | **No JS/TS port or WASM playground** | Excludes the Web/Node majority of fashion-tech buyers | High — but expensive (Phase 5) | +| 8 | **No multi-channel distribution** (Conda-Forge, Docker Hub, Homebrew tap, pre-commit-hooks.org) | Single PyPI bottleneck | Low — one-time setup, perpetual benefit | +| 9 | **No public roadmap or release cadence advertised** | Looks abandoned to first-time visitors | Low — pure communication | +| 10 | **No conference / regulatory presence** | UN/CEFACT, CIRPASS-2 working groups don't know we exist | Variable — high for authority | + +### Anti-patterns to avoid (explicit guardrails) + +- **Do not** rebuild for a JS audience before the Python core has 1k stars. Cost of a TS + port is 6+ person-weeks and a parallel maintenance burden; we get more leverage from + shipping an MCP server (which makes us *language-agnostic* to agent users without a + port). +- **Do not** spam CIRPASS / DPP Slack / LinkedIn. Authority comes from being cited *by + others*; outbound posting before we have a UN/CEFACT-published conformance report + burns credibility we cannot earn back. +- **Do not** add capability-driven roadmap items to this document. Capability lives in + [`STRATEGIC_ROADMAP.md`](STRATEGIC_ROADMAP.md). This plan only covers reach. +- **Do not** re-license the core. MIT is the right floor for adoption; the GPL-3.0 + plugin pattern already exists for spec-aligned extensions. +- **Do not** ship a hosted SaaS validation endpoint until Phase 5. It splits maintenance + effort and creates a perceived two-tier project; a public Pyodide-powered playground + achieves the same demo value with zero ops cost. + +______________________________________________________________________ + +## Cross-cutting threads + +These run through every phase rather than living in one of them: + +1. **Agent-native by default.** Every artifact we ship — docs page, error message, + release note, CLI help — is written so an LLM can quote it back to a human. The + `llms.txt` / `llms-ctx.txt` already do this for the package surface; we extend the + discipline to the docs site, error registry, and migration guides. +2. **Measurement before motion.** Phase 0 wires up dashboards. Subsequent phases each + declare a numeric target *before* shipping. We do not declare phases "done" on the + basis of activity. +3. **Compounding artifacts > one-shot promotion.** Each phase outputs a durable artifact + (Action, plugin, badge, registry entry, comparison page) that keeps earning attention + after we stop pushing it. No phase ends in a tweet thread or a one-off blog post. + +______________________________________________________________________ + +## Phase 0 — Baseline & instrumentation (Week 0, 2 days) + +> Without numbers we are guessing. The cost is low; the signal lasts for the whole plan. + +### Deliverables + +- **Public KPI dashboard.** A single + [`docs/internal/growth-metrics.md`](internal/growth-metrics.md) (or a Notion/Sheets + link in `STRATEGIC_ROADMAP.md`) with a small Python script in + [`scripts/`](../scripts/) that snapshots monthly: + - `pypistats.org` weekly + monthly downloads + - GitHub stars, forks, watchers, open issues, contributor count + - GitHub Pages traffic (Plausible or GitHub-native insights) + - PyPI search rank for keywords `dpp`, `digital product passport`, `untp`, `espr`, + `cirpass` + - Mentions across `news.ycombinator.com`, `dev.to`, `medium.com`, `reddit.com/r/python` +- **GitHub repository hygiene** (1 hour). Topics set on the repo: `dpp`, `untp`, + `cirpass`, `espr`, `digital-product-passport`, `eu-regulation`, `mcp`, + `claude-code-plugin`, `verifiable-credentials`, `circular-economy`. Repository + description rewritten to lead with the verb (currently "GDPR compliance engine for + physical products" → "Validate EU Digital Product Passports in Python: 7-layer + pipeline, UNTP 0.6/0.7 + CIRPASS-2, MCP-callable."). +- **Release-notes template** committed to `.github/RELEASE_TEMPLATE.md` so every + release ships with a "What changed for users / agents / integrators" matrix. + +### Exit criteria + +- Dashboard is auto-updated weekly by a scheduled GitHub Action. +- Baseline values for all KPIs in the table above are captured. + +______________________________________________________________________ + +## Phase 1 — Discoverability hardening (Weeks 1–3) + +> Make the package findable through *non-search* channels. The work in this phase pays +> back forever and unblocks every later phase. + +### 1.1 PyPI / GitHub frontstage (1 week) + +- **Keyword expansion in [`pyproject.toml`](../pyproject.toml).** Existing keyword list + is good; add `mcp`, `model-context-protocol`, `claude-code`, `agentic`, `llm-tooling`, + `eu-dpp`, `ecodesign`, `passport-textile`, `passport-battery`, `passport-tyre`, + `verifiable-credential`, `eudi`, `vc-jwt`, `sd-jwt`. (Order matters; PyPI weights the + first ~10.) +- **README hero rewrite.** Current README is engineer-shaped. Replace the first 30 lines + with a three-column "What you get" block targeting: + - **Python developers** ("validate in 3 lines"), + - **Agents / LLM tools** ("MCP-callable, plugin available"), + - **Compliance teams** ("ESPR-aligned, dual-family UNTP+CIRPASS"). + Lead each column with a copy-pasteable code snippet. +- **README badges row** add: PyPI status (Beta), conda-forge (once shipped), MCP + registry, Claude Code plugin marketplace, CIRPASS-2 alignment, GitHub Action + marketplace. +- **GitHub social preview image** rendered from + [`docs/assets/logo.png`](assets/) with the tagline. +- **`SECURITY.md`** already present — link it from the README to surface + professionalism. + +### 1.2 Documentation site amplification (1 week) + +- **mkdocs front-page rewrite** to mirror the README hero, adding a "Choose your path" + navigation: Brand engineer / Compliance lead / AI agent author / Plugin author. +- **`/docs/comparison.md`** — an honest "alternatives" page covering UNTP reference + validators, generic JSON-LD tools, and the (mostly closed-source) enterprise + platforms. Includes a feature matrix and links *to* competitors. This page is the + single biggest LLM-SEO investment we make: when a user asks any LLM "what's the best + Python DPP validator?" the model returns whatever page best answers the comparison + question, and ours will be the first one written. +- **Per-version landing pages** at `/docs/versions/0.6.x`, `/0.7.0`, `/cirpass-1.3` so + the docs canonicalise the keywords most users type. +- **Migration guide upgrade.** [`docs/guides/migration-0-6-to-0-7.md`](guides/migration-0-6-to-0-7.md) is + already strong; add a "Migration cookbook" with 6–10 specific symptom-to-fix entries + copy-pasted from real failure logs, because that is what people actually paste into + search engines. + +### 1.3 Agent-readable surface (3 days) + +- **Extend `llms.txt`** to include canonical URLs for the comparison page, the migration + cookbook, the error catalogue, and the version landing pages. +- **Publish per-section bundles** at `/docs/llms/validate.txt`, + `/docs/llms/migrate.txt`, `/docs/llms/sign.txt`, etc. so that agent authors can pull + scoped context without a 50KB blob. +- **Error catalogue as JSON.** Already generated by + [`scripts/generate_error_docs.py`](../scripts/generate_error_docs.py); also publish + the machine-readable `errors.json` at a stable URL on the docs site so agents can + resolve `SCH001` / `MDL003` / etc. into structured fixes. + +### 1.4 Distribution channel widening (3 days) + +- **Conda-Forge feedstock** (~1 day end-to-end). Captures the data-science / + conda-locked enterprise audience that pip never reaches. +- **Pre-commit-hooks.org listing.** Already have + `dppvalidator-precommit` exposed in [`pyproject.toml`](../pyproject.toml#L64); just + needs a `.pre-commit-hooks.yaml` and a PR to `pre-commit/pre-commit-hooks` README. +- **Homebrew tap** (`artiso-ai/homebrew-dppvalidator`) for the macOS-first DevX. +- **Docker Hub image** `artiso/dppvalidator:` with the `[cli,rdf]` extras + pre-installed and a 30-line `README` showing one-line `docker run` validation. +- **GitHub Action** in `.github/actions/validate-dpp/action.yml` and + `marketplace.yml` so `uses: artiso-ai/dppvalidator-action@v1` works in any repo. + Submit it to the GitHub Marketplace. + +### Exit criteria + +- KPI dashboard shows a measurable lift in `last_month` PyPI downloads (target: ≥ 400 vs + 176 baseline). +- README hero passes the "30-second test" with three external readers (1 brand engineer, + 1 backend dev, 1 compliance lead). +- Comparison page is indexed by Google and cited at least once when asking + ChatGPT/Claude "compare DPP validators." + +______________________________________________________________________ + +## Phase 2 — Agent-native distribution v1 (Weeks 4–6) + +> The thesis phase. We make dppvalidator the validator any AI agent can call. + +Background: the [Claude Code skills doc](https://code.claude.com/docs/en/skills), the +[plugins doc](https://code.claude.com/docs/en/plugins), the +[plugin marketplaces doc](https://code.claude.com/docs/en/plugin-marketplaces) and the +[plugin-hint protocol](https://code.claude.com/docs/en/plugin-hints) collectively define +a complete distribution stack: SKILL.md → plugin → marketplace → CLI hint, with the MCP +protocol as the orthogonal cross-vendor surface (ChatGPT, Cursor, Continue.dev, Claude +Desktop, etc.). + +### 2.1 Ship `dppvalidator-mcp` (Week 4, ~5 days) + +A standalone, side-effect-free MCP server that wraps the existing `ValidationEngine`, +`migrate`, and `JSONLDExporter` surface. + +- **Tools exposed** (start small, expand later): + - `validate_dpp(passport: dict | str, layers?: list[str], strict?: bool, target?: str, schema_version?: str) -> ValidationResult` + - `detect_dpp_version(passport: dict | str) -> {family, version, confidence}` + - `migrate_dpp(passport: dict | str, to: str) -> {migrated, warnings}` + - `export_dpp_jsonld(passport: dict | str, mode: "untp" | "eu-dpp") -> str` + - `explain_error(code: str) -> {description, fix, docs_url}` + - `list_supported_versions() -> list[str]` +- **Implementation.** Build on `mcp` Python SDK / FastMCP; package as + `dppvalidator[mcp]` extra and publish a `dppvalidator-mcp` console script. +- **Distribution surfaces:** + - PyPI extra: `pip install "dppvalidator[mcp]"` + - `uvx`: `uvx dppvalidator-mcp` (zero-install for `claude mcp add`) + - Anthropic MCP Registry submission: + - Listed in the README "Use from any agent" section, with pasteable + `claude mcp add dppvalidator -- uvx dppvalidator-mcp` and ChatGPT/Cursor configs. +- **Resources** (read-only): expose `dppvalidator://schemas/{version}` and + `dppvalidator://errors/{code}` so agents can browse them without tool calls. + +The MCP server is **the multiplier**: every agent ecosystem (Claude, ChatGPT, Cursor, +Continue, Cline, Open Interpreter, custom n8n nodes) gains DPP validation simultaneously. +A 5-day cost buys orders-of-magnitude more reach than any individual integration. + +### 2.2 Claude Code plugin: `dppvalidator` (Week 5, ~3 days) + +Three components in one plugin: + +```text +plugins/claude-code/dppvalidator/ +├── .claude-plugin/ +│ └── plugin.json +├── skills/ +│ ├── validate/SKILL.md # /dppvalidator:validate +│ ├── migrate/SKILL.md # /dppvalidator:migrate --to +│ ├── explain-error/SKILL.md # /dppvalidator:explain-error +│ └── scaffold/SKILL.md # /dppvalidator:scaffold +├── agents/ +│ └── dpp-reviewer.md # subagent that runs full validation + risk summary +├── hooks/ +│ └── hooks.json # PostToolUse on Write|Edit *.dpp.json -> auto-validate +├── .mcp.json # registers dppvalidator-mcp at plugin scope +└── README.md +``` + +- **`SKILL.md` frontmatter conventions** (per the + [skills reference](https://code.claude.com/docs/en/skills)): + - `description:` first sentence is the one Claude matches against — front-load DPP / + UNTP / CIRPASS keywords. + - `paths:` set to `**/*.dpp.json,**/passport*.json` so the skill auto-loads when the + user is editing DPP fixtures. + - `allowed-tools:` `Bash(uvx dppvalidator *)` so the skill never prompts the user for + permission to run validation. +- **`dpp-reviewer` subagent** with `tools: Read, Grep, Glob, Bash(uvx dppvalidator *)` + and a system prompt that instructs it to run validation, group errors by severity, + and emit a markdown report. Triggered automatically by Claude when the user pastes + DPP JSON or asks "is this DPP valid?". +- **Hook**: `PostToolUse` matcher on `Write|Edit` — when a `*.dpp.json` file is + saved, run `uvx dppvalidator validate "$file" --strict --json` and attach the + result. Replicates the ergonomics of `eslint --fix on save` for DPP authors. + +Distribution: + +- **Marketplace.** Create + [`artiso-ai/claude-plugins`](https://github.com/artiso-ai/claude-plugins) with a + `.claude-plugin/marketplace.json` listing `dppvalidator` (and any future ARTISO + plugins). Users install with: + ```text + /plugin marketplace add artiso-ai/claude-plugins + /plugin install dppvalidator@artiso-claude-plugins + ``` +- **Submit to the official Anthropic marketplace** via + . A listing there is the prerequisite for + Phase 2.3. +- **Plugin hint protocol** (Phase 2.3) is the thing that actually drives installs. + +### 2.3 CLI plugin-hint emission (Week 5, 1 day) + +Per the [plugin hints doc](https://code.claude.com/docs/en/plugin-hints), once the +plugin is in the official marketplace, the CLI emits a one-line stderr marker that +prompts Claude Code users to install it on first contact. Add to +`src/dppvalidator/cli/__init__.py`: + +```python +import os, sys +if os.environ.get("CLAUDECODE"): + print( + '', + file=sys.stderr, + ) +``` + +Gate emission on the `--help` path and unknown-subcommand errors (the two highest-yield +touchpoints called out in the docs). Cost: <30 lines of code; benefit: every Claude +Code user who runs `dppvalidator` gets a one-tap install prompt. + +### 2.4 ChatGPT Custom Connector + Cursor / Continue.dev recipes (Week 6, 2 days) + +The MCP server unlocks all of these with no code changes — only documentation. + +- `docs/integrations/chatgpt.md` — ChatGPT custom connector via the OpenAI MCP + hosting story (or via `mcp-proxy` while native support stabilises). +- `docs/integrations/cursor.md` — Cursor's `.cursor/mcp.json`. +- `docs/integrations/continue.md` — Continue.dev MCP block. +- `docs/integrations/claude-desktop.md` — Claude Desktop config. +- Each page ends with a 30-second screencast (.gif) of "edit a DPP, get a validation + result inline." These gifs are the unit of social-media currency in this market. + +### Exit criteria + +- `dppvalidator-mcp` is installable and listed in the Anthropic MCP Registry. +- `dppvalidator` Claude Code plugin is in the official marketplace, with ≥1 install + prompt fired in our own dogfooding session. +- One agent-ecosystem integration page goes viral on Hacker News / r/LocalLLaMA / X + (we don't *plan* the virality; we plan the *artifact*). +- KPI: ≥ 750 PyPI downloads in the trailing 30 days; ≥ 50 GitHub stars. + +______________________________________________________________________ + +## Phase 3 — Integration & onramps (Weeks 7–10) + +> Once an agent or developer says "yes, I want this," every additional onramp +> compounds. Phase 3 is high-volume, low-creativity execution. + +### 3.1 Reference apps (Week 7, 3 days) + +A new top-level [`examples/`](../examples/) tree (we already have a starter +`dppvalidator_example_plugin`) containing: + +- **`fastapi-validation-service/`** — minimal POST /validate endpoint, Dockerfile, + Helm chart, OpenAPI spec. +- **`django-supplier-portal/`** — accepts supplier DPP submissions, validates server-side, + surfaces errors in the admin. +- **`airflow-dag/`** — a `dppvalidator validate` operator + a sensor that watches an S3 + bucket of DPP fixtures. +- **`dbt-project/`** — DPP rows in a warehouse validated as a dbt test. +- **`shopify-admin-app/`** — Remix-based Shopify embedded app skeleton that calls the + MCP server. +- **`cli-batch/`** — a `find ... -name "*.dpp.json" | xargs dppvalidator` recipe with + `--accept-warnings`, `--strict`, etc. + +Each example **lives in its own README with screen recordings** and is referenced from +the main docs' "Choose your path" navigation. + +### 3.2 GitHub Action polish (Week 8, 2 days) + +The Phase 1 Action ships as MVP. Phase 3 promotes it to v1: + +- Inputs: `path`, `strict`, `target`, `schema-version`, `format` (json | sarif | + checkstyle), `fail-on-warning`. +- **SARIF output** so violations show up in the GitHub Code Scanning UI exactly like a + CodeQL alert. This is the killer feature that lifts adoption from "tool people add" + to "tool people forget they added." +- Marketplace submission with examples; pin the action to a major version branch so we + can ship patches without breaking pinned pipelines. + +### 3.3 IDE + Editor integrations (Week 9, 3 days) + +- **VS Code extension** — wraps the CLI, surfaces errors as squiggles via the LSP + pattern. *Or* shortcut: publish a JSON Schema file at a stable URL so VS Code's + built-in JSON validation catches schema-level issues with zero extension. Ship the + schema file first; build the extension only if metrics justify it. +- **JetBrains plugin** — same shortcut: publish JSON Schema mappings. +- **JSON Schema Store entry** so `passport*.json` auto-validates everywhere + (). + +### 3.4 Pre-commit + lint integrations (Week 9, 1 day) + +- Already publish `dppvalidator-precommit`. Phase 3 adds the + `.pre-commit-hooks.yaml`, a PR to `pre-commit/awesome-pre-commit` lists, and a docs + page ([`docs/integrations/pre-commit.md`](integrations/)) with the canonical block. + +### 3.5 Cross-runtime callability (Week 10, 3 days) + +This is the targeted, surgical alternative to a full TS port: + +- **Pyodide-powered playground** at `https://artiso-ai.github.io/dppvalidator/play/`. + Validates in-browser, no backend. Source in + [`docs/play/`](play/) with a 100-line `index.html`. The + cost is one weekend; the SEO and demo value lasts forever. +- **Single-file CLI binary** via `pyinstaller` or `pex`, attached to GitHub Releases so + non-Python users can `curl | sh` it. Targets macOS (arm64, x86_64) and Linux + (x86_64); Windows can wait. + +### Exit criteria + +- ≥ 5 integration pages published, each with a working repo and a 30-second screencast. +- GitHub Action shows ≥ 100 workflow runs across non-ARTISO repos in the trailing + 30 days (queryable via the Action API). +- Playground load count ≥ 1,000 in the trailing 30 days. +- KPI: ≥ 1,500 PyPI downloads in the trailing 30 days; ≥ 150 GitHub stars. + +______________________________________________________________________ + +## Phase 4 — Authority & community (Weeks 11–16) + +> Adoption beyond hobby use requires that "ARTISO says it's compliant" be replaced by +> "UN/CEFACT says it is." + +### 4.1 Conformance & certification (Weeks 11–13) + +- **Public conformance test report.** Run dppvalidator against the UN/CEFACT-published + test fixtures, both 0.6.x and 0.7.0, and publish a signed report at + `https://artiso-ai.github.io/dppvalidator/conformance//`. Include both pass + *and* fail cases — transparency is the asset. +- **Submit to UN/CEFACT.** Apply to the UNTP DPP working group for our validator to be + listed as a conforming implementation. Track via + [`docs/conformance/untp-application.md`](conformance/). +- **Submit to CIRPASS-2 / EU DPP-WG.** ARTISO's existing relationships are the lever + here; this plan only schedules the milestone. +- **Aligned with `dpp.vocabulary-hub.eu`** statement is already in the README; back it + up with a versioned diff page (`docs/conformance/eu-dpp-vocabulary.md`) so claims are + auditable. + +### 4.2 Domain plugin packs (Weeks 13–15) + +The plugin system is built and unused in the wild. Each pack is a credibility asset and +a separate PyPI package: + +- **`dppvalidator-batteries`** — EU Battery Regulation 2023/1542 fields. Highest demand + outside textiles; mandate begins **2027-02-18**. +- **`dppvalidator-electronics`** — paired with the upcoming ESPR electronics delegated + act. Signal early. +- **`dppvalidator-construction`** — CPR + EPC fields. Adjacent vertical, large market. +- **`dppvalidator-tyres`** already exists (GPL-3.0); promote, harden, and document. + +Each plugin ships its own SKILL.md set in the Claude Code plugin (Phase 2 stays generic; +Phase 4 layers vertical depth on top). + +### 4.3 Community infrastructure (Weeks 11–12, 2 days) + +- **GitHub Discussions** enabled with three pinned categories: Q&A, Show & Tell, + Feature Requests. +- **`CODE_OF_CONDUCT.md`** and **`GOVERNANCE.md`** committed; ARTISO is the BDFL but the + governance doc names the path to an open contributor council once we hit 5 external + committers. +- **Issue & PR templates** for bug reports, version-bump migrations (we already have + the migration plan template), feature requests, and security reports. +- **`good-first-issue` tagging pass** on the existing 1 open issue and on every issue + created from Phase 3 onward. + +### 4.4 Content & SEO (Weeks 13–16) + +Each output is a permanent asset, not a tweet. + +- **Five long-form pieces** at `docs/blog/`: + 1. "How EU ESPR breaks every DPP we tested in production" (controversy + concrete). + 2. "Validating UNTP 0.7.0 in 200 lines of Python" (canonical search term). + 3. "Why your DPP probably isn't a valid Verifiable Credential (yet)" (signature + JSON-LD). + 4. "From CSV to CIRPASS-2: a migration story" (operational). + 5. "Calling dppvalidator from ChatGPT, Claude, and Cursor" (the agent thesis). +- **Cross-publish** to dev.to and Medium *with canonical links back to the docs site*. +- **Conference talks** (proposals submitted in Phase 1, talks delivered in Phase 4): + PyCon DE, FOSDEM, SustainabilityCon, Textile Exchange, Première Vision Tech. +- **Podcast circuit.** Aim for 3 appearances on the Python (Talk Python To Me), + sustainability (The Sustainability Story), and AI-tooling (Latent Space) podcasts. + Each appearance gets a transcript hosted on the docs site. + +### 4.5 Strategic partnerships (Weeks 14–16, ongoing) + +- **EUDI Wallet reference projects.** Drop a one-paragraph PR to the EU eIDAS reference + implementations adding dppvalidator as a recommended verifier for product-related + credentials. +- **OpenSCM / Open Footprint / Catena-X.** Each of these has a DPP angle; one + integration repo + a PR per project is enough. +- **fashion-for-good / Textile Exchange.** Listing as a tool partner. +- **Major fashion brands' tech blogs.** Where ARTISO's existing relationships allow, + ghost-write or co-write a "we used dppvalidator" case study. + +### Exit criteria + +- ≥ 1 listing in an official UN/CEFACT or EU DPP-WG document. +- ≥ 2 external plugin packages published by non-ARTISO contributors. +- ≥ 3 conference talks delivered with recordings hosted on docs site. +- KPI: ≥ 3,000 PyPI downloads in the trailing 30 days; ≥ 350 GitHub stars; ≥ 5 external + contributors. + +______________________________________________________________________ + +## Phase 5 — Network effects (Weeks 17–26) + +> The point at which we are not pushing growth so much as removing friction from growth +> we already have. If Phase 4 succeeded, Phase 5 is mostly *enabling* others to build on +> top of dppvalidator. + +### 5.1 Hosted services (optional) + +Prerequisite: ≥ 5,000 monthly downloads and ≥ 3 inbound enterprise inquiries; otherwise +defer. + +- **Hosted validation API** at `https://api.dppvalidator.io/` (subdomain, not the + ARTISO root domain — keeps brands neutral). Free tier 1k requests/day, paid above. +- **Conformance dashboard** (multi-tenant) for brands' supplier networks — same + validation engine, sold as a thin SaaS skin. +- These exist primarily to **fund** the OSS work and to absorb enterprise demand + cleanly; they do not change the OSS core. + +### 5.2 JS / WASM port (Weeks 20–26) + +Now justified by usage: + +- **`@artiso-ai/dppvalidator` npm package** with the same surface area as the Python + CLI/API. Implementation: compile the Python core to WASM via Pyodide, or hand-port + the stable subset (schema + model + JSON-LD layers). Go with WASM first for time to + market; switch to a native port only if WASM bundle size becomes a blocker. +- **Browser SDK + Web Component** `` for embedding in product pages. +- **TypeScript types generated from Pydantic models** via `datamodel-code-generator` + + a small post-processor. Single source of truth stays in Python. + +### 5.3 Marketplace presences + +- **Cloudflare Workers / Vercel Edge / Deno Deploy** templates that wrap the WASM + build into a one-click hosted validator. +- **AWS Marketplace / GCP Marketplace** SaaS listings of the hosted service (only if + Phase 5.1 ships). +- **Salesforce / Shopify / Adobe Commerce app stores** wrapping the MCP server. + +### 5.4 Standards body engagement + +By Phase 5 we should have earned a seat at the table: + +- Active participation in UN/CEFACT UNTP working group (track with a public + [`docs/standards/untp-engagement-log.md`](standards/)). +- Submit RFCs / issues to UNTP-DPP and CIRPASS-2 repos based on real-world bugs we + catch in fixtures. +- Sponsor a CIRPASS-2 plugfest if budget allows. + +### Exit criteria + +- ≥ 10,000 monthly PyPI downloads. +- npm package crosses 1k weekly downloads. +- dppvalidator referenced as the canonical Python implementation in ≥ 1 standards-body + document. +- ≥ 1 ARTISO contributor on a UN/CEFACT or EU DPP-WG editorial team. + +______________________________________________________________________ + +## Phase 6 — Cadence (continuous) + +> What we keep doing forever, not phase-bound. + +### Release rhythm + +- **Patch release every 2 weeks** for the first 6 months of this plan; **monthly** + thereafter unless a security/conformance fix forces faster. +- **Every release ships:** + - Conventional-commit changelog (already in place). + - Updated `llms-ctx.txt` snapshot. + - Migration notes if any wire-shape semantics changed. + - One tweet-length announcement per release, posted to + Mastodon/Bluesky/LinkedIn from the + `@artisoai` accounts. (No Twitter/X amplification — the audience is mostly EU, + LinkedIn pulls 10× the click-through.) + +### Maintenance commitments (advertised) + +Publish in [`SECURITY.md`](../SECURITY.md) and [`CONTRIBUTING.md`](../CONTRIBUTING.md): + +- Security fix SLA: **48h triage, 7-day patch** for high-severity. +- Spec-tracking SLA: **30 days** from a UNTP/CIRPASS minor release to a dppvalidator + release with full support. +- Python version policy: support last 3 minor versions; drop with one full release of + warning. + +### Telemetry (opt-in) + +- An **opt-in** anonymous usage ping (`DPPVALIDATOR_TELEMETRY=1`) so we know which + layers and versions are actually used in the wild. Defaulted to *off*; users opt in + during `dppvalidator init`. Without telemetry we are guessing on what to deprecate. + +______________________________________________________________________ + +## Risk register + +| Risk | P | Impact | Mitigation | +| --------------------------------------------------------------------- | -- | ------ | ------------------------------------------------------------------------------------------- | +| UNTP 1.0 ships breaking changes mid-plan | M | H | Existing detection layer + compat shims; treat as scheduled work in Phase 4 | +| Anthropic plugin marketplace approval slow / rejected | L | M | Self-hosted marketplace (Phase 2.2) ships *first*; official listing is amplification | +| Competitor lands a hosted SaaS with marketing budget | M | M | The OSS + agent-native moat is the answer; don't compete on SaaS until Phase 5 | +| MCP protocol changes break our server | M | M | Pin `mcp` SDK floor; add an integration test that runs against the latest Claude Desktop | +| Plugin maintainer burnout (one-person Tyre/Textile pack) | H | M | Governance doc states maintainer SLA; auto-archive plugins not updated in 6 months | +| GPL-3.0 contamination via a careless plugin import | L | H | [`/.claude/rules/plugin-licenses.md`](../.claude/rules/plugin-licenses.md) already enforced; add CI check | +| Conformance report exposes embarrassing failures | M | L | Publish the report **with** the failures and a fix ETA. Transparency wins faster than hiding | + +______________________________________________________________________ + +## How this plan relates to other planning documents + +| Document | Scope | +| ----------------------------------------------------------------- | -------------------------------------------------- | +| [`STRATEGIC_ROADMAP.md`](STRATEGIC_ROADMAP.md) | Capability roadmap (what we *build*) | +| [`IMPROVEMENT_ROADMAP.md`](IMPROVEMENT_ROADMAP.md) | Technical gap closure (what we *fix*) | +| [`REFACTORING_PLAN.md`](REFACTORING_PLAN.md) | Internal architecture work | +| [`UNTTP_PLUGIN_PLAN.md`](UNTTP_PLUGIN_PLAN.md) | Single-plugin tactical plan (textiles) | +| [`VC_WALLET_ROADMAP.md`](VC_WALLET_ROADMAP.md) | Wallet-readiness sub-roadmap | +| `docs/plans/CIRPASS_2_MIGRATION.md` | One-shot migration plan | +| **`docs/GROWTH_PLAN.md` (this document)** | Reach, distribution, adoption — the *who* and *how it spreads* | + +Capability work and growth work compete for the same maintainer-week. When forced to +choose: ship the capability work that *unlocks* a growth phase (e.g. UNTP 1.0 support +unlocks Phase 4.1 conformance), and *defer* the capability work that doesn't. This plan +exists so we know which is which. + +______________________________________________________________________ + +## Appendix A — Phase summary at a glance + +| Phase | Window | Theme | Headline deliverable | KPI gate (cumulative) | +| ----- | ----------- | --------------------------- | ----------------------------------------------------- | -------------------------- | +| 0 | Week 0 | Instrumentation | KPI dashboard + repo hygiene | Baseline captured | +| 1 | Weeks 1–3 | Discoverability | Comparison page, GitHub Action, Conda-Forge, Docker | 400 dl/mo, 25 stars | +| 2 | Weeks 4–6 | Agent-native distribution | MCP server + Claude Code plugin in marketplaces | 750 dl/mo, 50 stars | +| 3 | Weeks 7–10 | Integration & onramps | 5+ reference apps, SARIF Action, Pyodide playground | 1,500 dl/mo, 150 stars | +| 4 | Weeks 11–16 | Authority & community | UN/CEFACT listing, vertical plugins, conf talks | 3,000 dl/mo, 350 stars | +| 5 | Weeks 17–26 | Network effects | npm/WASM, hosted API (optional), standards seat | 10,000 dl/mo, 600+ stars | +| 6 | Continuous | Cadence | Bi-weekly → monthly releases, telemetry, SLA | Sustained | + +______________________________________________________________________ + +## Appendix B — Suggested first commits (Week 0) + +These are the smallest possible motions that move the plan from doc to action. +Each is < 1 hour: + +1. **PR `chore(repo): expand topics, rewrite GitHub description and social preview`** — + pure metadata, no code. +2. **PR `docs(plan): add growth plan and link from STRATEGIC_ROADMAP`** — this file + + one paragraph in `STRATEGIC_ROADMAP.md`. +3. **PR `feat(cli): emit Claude Code install hint when CLAUDECODE=1`** — 8 lines in + `src/dppvalidator/cli/__init__.py`. Tied off by Phase 2.3 once the plugin is + marketplace-listed; the hint until then is a no-op (no plugin to install) but the + wiring is ready. +4. **PR `chore(pyproject): expand keywords for agent / MCP discoverability`** — extend + the keyword list in [`pyproject.toml`](../pyproject.toml). +5. **Issue `meta: track Phase 0 KPI baseline`** opened with the table from + §"Executive summary" embedded. + +Once these five land, every subsequent phase has a hook to attach to. diff --git a/docs/reference/api/validators.md b/docs/reference/api/validators.md index 4acdb35..dfd7652 100644 --- a/docs/reference/api/validators.md +++ b/docs/reference/api/validators.md @@ -100,14 +100,15 @@ reset_default_registry() ## Error Codes -| Code | Layer | Description | -| ------ | -------- | ------------------------ | -| SCH001 | schema | Required field missing | -| SCH002 | schema | Invalid type | -| MOD001 | model | Model validation error | -| JLD001 | jsonld | Invalid context | -| SEM001 | semantic | Invalid vocabulary value | -| SEM002 | semantic | Invalid date range | -| SIG001 | crypto | Invalid signature | +| Code | Surface | Description | +| ------ | --------- | -------------------------------- | +| SCH001 | schema | Required field missing | +| SCH002 | schema | Invalid type | +| MDL001 | model | Model validation error | +| JLD001 | jsonld | Invalid context | +| SEM001 | semantic | Invalid vocabulary value | +| SEM002 | semantic | Invalid date range | +| DET001 | detection | Family mismatch | +| MAP004 | mapping | Required field missing on target | > **Note:** This table shows common examples. See [Error Reference](../../errors/index.md) for the complete list of 70+ error codes. diff --git a/docs/uv/uv.md b/docs/uv/uv.md deleted file mode 100644 index 9c7148d..0000000 --- a/docs/uv/uv.md +++ /dev/null @@ -1,85 +0,0 @@ -# uv - -> uv is an extremely fast Python package and project manager, written in Rust. - -You can use uv to install Python dependencies, run scripts, manage virtual environments, -build and publish packages, and even install Python itself. uv is capable of replacing -`pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `twine`, `virtualenv`, and more. - -uv includes both a pip-compatible CLI (prepend `uv` to a pip command, e.g., `uv pip install ruff`) -and a first-class project interface (e.g., `uv add ruff`) complete with lockfiles and -workspace support. - -When fetching documentation, use explicit `index.md` paths for directories, e.g., -`https://docs.astral.sh/uv/concepts/projects/dependencies/index.md`. This returns -clean markdown instead of rendered HTML with JS/CSS. - -## Getting started - -- [Features](https://docs.astral.sh/uv/getting-started/features/index.md) -- [First steps](https://docs.astral.sh/uv/getting-started/first-steps/index.md) -- [Installation](https://docs.astral.sh/uv/getting-started/installation/index.md) - -## Guides - -- [Installing Python](https://docs.astral.sh/uv/guides/install-python/index.md) -- [Publishing packages](https://docs.astral.sh/uv/guides/package/index.md) -- [Working on projects](https://docs.astral.sh/uv/guides/projects/index.md) -- [Running scripts](https://docs.astral.sh/uv/guides/scripts/index.md) -- [Using tools](https://docs.astral.sh/uv/guides/tools/index.md) - -## Integrations - -- [Alternative indexes](https://docs.astral.sh/uv/guides/integration/alternative-indexes/index.md) -- [AWS Lambda](https://docs.astral.sh/uv/guides/integration/aws-lambda/index.md) -- [Coiled](https://docs.astral.sh/uv/guides/integration/coiled/index.md) -- [Dependency bots](https://docs.astral.sh/uv/guides/integration/dependency-bots/index.md) -- [Docker](https://docs.astral.sh/uv/guides/integration/docker/index.md) -- [FastAPI](https://docs.astral.sh/uv/guides/integration/fastapi/index.md) -- [GitHub Actions](https://docs.astral.sh/uv/guides/integration/github/index.md) -- [GitLab CI/CD](https://docs.astral.sh/uv/guides/integration/gitlab/index.md) -- [Jupyter](https://docs.astral.sh/uv/guides/integration/jupyter/index.md) -- [marimo](https://docs.astral.sh/uv/guides/integration/marimo/index.md) -- [Pre-commit](https://docs.astral.sh/uv/guides/integration/pre-commit/index.md) -- [PyTorch](https://docs.astral.sh/uv/guides/integration/pytorch/index.md) - -## Projects - -- [Building distributions](https://docs.astral.sh/uv/concepts/projects/build/index.md) -- [Configuring projects](https://docs.astral.sh/uv/concepts/projects/config/index.md) -- [Managing dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/index.md) -- [Exporting lockfiles](https://docs.astral.sh/uv/concepts/projects/export/index.md) -- [Creating projects](https://docs.astral.sh/uv/concepts/projects/init/index.md) -- [Structure and files](https://docs.astral.sh/uv/concepts/projects/layout/index.md) -- [Running commands](https://docs.astral.sh/uv/concepts/projects/run/index.md) -- [Locking and syncing](https://docs.astral.sh/uv/concepts/projects/sync/index.md) -- [Using workspaces](https://docs.astral.sh/uv/concepts/projects/workspaces/index.md) - -## Features - -- [Build backend](https://docs.astral.sh/uv/concepts/build-backend/index.md) -- [Caching](https://docs.astral.sh/uv/concepts/cache/index.md) -- [Configuration files](https://docs.astral.sh/uv/concepts/configuration-files/index.md) -- [Package indexes](https://docs.astral.sh/uv/concepts/indexes/index.md) -- [Preview features](https://docs.astral.sh/uv/concepts/preview/index.md) -- [Python versions](https://docs.astral.sh/uv/concepts/python-versions/index.md) -- [Resolution](https://docs.astral.sh/uv/concepts/resolution/index.md) -- [Tools](https://docs.astral.sh/uv/concepts/tools/index.md) -- [Authentication](https://docs.astral.sh/uv/concepts/authentication/index.md) - -## The pip interface - -- [Compatibility with pip](https://docs.astral.sh/uv/pip/compatibility/index.md) -- [Locking environments](https://docs.astral.sh/uv/pip/compile/index.md) -- [Declaring dependencies](https://docs.astral.sh/uv/pip/dependencies/index.md) -- [Using environments](https://docs.astral.sh/uv/pip/environments/index.md) -- [Inspecting environments](https://docs.astral.sh/uv/pip/inspection/index.md) -- [Managing packages](https://docs.astral.sh/uv/pip/packages/index.md) - -## Reference - -- [Commands](https://docs.astral.sh/uv/reference/cli/index.md) -- [Environment variables](https://docs.astral.sh/uv/reference/environment/index.md) -- [Installer options](https://docs.astral.sh/uv/reference/installer/index.md) -- [Settings](https://docs.astral.sh/uv/reference/settings/index.md) -- [Storage](https://docs.astral.sh/uv/reference/storage/index.md) diff --git a/llms-ctx.txt b/llms-ctx.txt deleted file mode 100644 index c0b7ce0..0000000 --- a/llms-ctx.txt +++ /dev/null @@ -1,146 +0,0 @@ -# dppvalidator - Extended Context for LLMs - -> Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations and UNTP standards. - -## Overview - -dppvalidator is the open-source compliance engine for EU Digital Product Passports. The EU's Ecodesign for Sustainable Products Regulation (ESPR) mandates Digital Product Passports for textiles starting 2027. This library ensures DPP data is compliant before production. - -## Tech Stack - -- Python 3.10+ -- Pydantic v2 for validation models -- Included: httpx, jsonschema, pyld, cryptography, PyJWT -- Optional: rich (CLI formatting via `[cli]` extra) - -## Installation - -```bash -# Using uv (recommended) -uv add dppvalidator # Full functionality -uv add "dppvalidator[cli]" # With rich CLI formatting - -# Or using pip -pip install dppvalidator -pip install "dppvalidator[cli]" -``` - -## Architecture - -### Five-Layer Validation - -0. **Schema Detection**: Auto-detect DPP schema version from $schema/@context -1. **Schema Layer (SCH001-SCH099)**: JSON Schema Draft 2020-12 validation -2. **Model Layer (MOD001-MOD099)**: Pydantic v2 type validation and coercion -3. **JSON-LD Layer (JLD001-JLD099)**: PyLD expansion, context resolution -4. **Semantic Layer (SEM001-SEM099)**: Business rules, ISO codes, GTIN checksums -5. **Cryptographic Layer (SIG001-SIG099)**: VC signature verification, DID resolution - -### Performance - -- Model (minimal): 0.011ms (87k ops/sec) -- Model (full): 0.016ms (62k ops/sec) -- Semantic: 0.005ms (194k ops/sec) -- Full (Model+Sem): 0.017ms (58k ops/sec) - -## Core API - -### ValidationEngine - -```python -from dppvalidator.validators import ValidationEngine - -# All layers (default) -engine = ValidationEngine() - -# Specific layers -engine = ValidationEngine(layers=["schema"]) -engine = ValidationEngine(layers=["model", "semantic"]) - -# Strict mode (warnings become errors) -engine = ValidationEngine(strict_mode=True) - -# Validate -result = engine.validate(dpp_dict) -# Returns: ValidationResult with .valid, .errors, .warnings, .validation_time_ms -``` - -### Models - -```python -from dppvalidator.models import ( - DigitalProductPassport, - CredentialIssuer, - Product, - ProductBatch, - Identifier, - Link, -) - -passport = DigitalProductPassport( - id="https://example.com/dpp/product-001", - issuer=CredentialIssuer(id="https://example.com/issuer", name="Company"), -) -``` - -### Exporters - -```python -from dppvalidator.exporters import JSONLDExporter - -exporter = JSONLDExporter() -jsonld = exporter.export(passport) # W3C Verifiable Credential format -``` - -### Plugin System - -```python -from dppvalidator.plugins import PluginRegistry - -registry = PluginRegistry() - -@registry.register_validator("custom") -class CustomValidator: - def validate(self, data: dict) -> list: - errors = [] - # Custom validation logic - return errors - -engine = ValidationEngine(plugins=registry) -``` - -## CLI Usage - -```bash -dppvalidator validate passport.json -dppvalidator validate passport.json --strict -dppvalidator export passport.json --format jsonld --output out.jsonld -dppvalidator schema --version 0.6.1 -``` - -## Directory Structure - -``` -src/dppvalidator/ -├── models/ # Pydantic models for DPP entities -├── validators/ # Validation engine and layers -├── exporters/ # JSON-LD and other export formats -├── schemas/ # JSON Schema loading and caching -├── vocabularies/ # Controlled vocabulary loading -├── cli/ # Command-line interface -├── plugins/ # Plugin system -└── __init__.py -``` - -## Related Standards - -- UNTP Digital Product Passport: https://untp.unece.org/docs/specification/DigitalProductPassport/ -- EU ESPR Regulation: https://environment.ec.europa.eu/topics/circular-economy/ecodesign-sustainable-products-regulation_en -- W3C Verifiable Credentials: https://www.w3.org/TR/vc-data-model/ - -## Links - -- Documentation: https://artiso-ai.github.io/dppvalidator/ -- PyPI: https://pypi.org/project/dppvalidator/ -- GitHub: https://github.com/artiso-ai/dppvalidator -- Issues: https://github.com/artiso-ai/dppvalidator/issues diff --git a/llms-ctx.txt b/llms-ctx.txt new file mode 120000 index 0000000..e33962a --- /dev/null +++ b/llms-ctx.txt @@ -0,0 +1 @@ +docs/llms-ctx.txt \ No newline at end of file diff --git a/llms.txt b/llms.txt deleted file mode 100644 index b609da3..0000000 --- a/llms.txt +++ /dev/null @@ -1,41 +0,0 @@ -# dppvalidator - -> Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations and UNTP standards. - -dppvalidator is the open-source compliance engine for EU Digital Product Passports. Starting 2027, every textile product sold in the EU must have a Digital Product Passport. This library ensures your DPP data is compliant before production. - -## Core Features - -- Five-layer validation: schema, model, JSON-LD, semantic, and cryptographic -- Built-in UNTP DPP 0.6.1 schema support -- JSON-LD export for W3C Verifiable Credentials -- Plugin system for custom validators -- CLI and Python API - -## Installation - -``` -pip install dppvalidator -``` - -## Quick Example - -```python -from dppvalidator.validators import ValidationEngine - -engine = ValidationEngine() -result = engine.validate(dpp_data) - -if result.valid: - print("✓ Valid DPP") -else: - for error in result.errors: - print(f"✗ {error.code}: {error.message}") -``` - -## Links - -- Docs: https://artiso-ai.github.io/dppvalidator/ -- PyPI: https://pypi.org/project/dppvalidator/ -- GitHub: https://github.com/artiso-ai/dppvalidator -- Extended context: https://artiso-ai.github.io/dppvalidator/llms-ctx.txt diff --git a/llms.txt b/llms.txt new file mode 120000 index 0000000..afbd6a7 --- /dev/null +++ b/llms.txt @@ -0,0 +1 @@ +docs/llms.txt \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1ff8232..0ef533d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: dppvalidator -site_description: Python library for validating Digital Product Passports (UNTP DPP) +site_description: Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations and CIRPASS/UNECE ontologies site_url: https://artiso-ai.github.io/dppvalidator repo_url: https://github.com/artiso-ai/dppvalidator repo_name: artiso-ai/dppvalidator @@ -81,7 +81,9 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format "" - pymdownx.tabbed: alternate_style: true - - pymdownx.snippets + - pymdownx.snippets: + base_path: ['.', 'docs'] + check_paths: true - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji "" emoji_generator: !!python/name:material.extensions.emoji.to_svg "" @@ -176,9 +178,11 @@ nav: - EU DPP Ontology Alignment (legacy): concepts/eudpp-ontology-alignment.md - CIRPASS-2 Implementation (legacy): concepts/cirpass-implementation.md - Plugins: + - Overview: plugins.md - Tyres (Pre-1.0): plugins/tyres.md - Contributing: - Development Setup: contributing/development-setup.md + - Security Setup: contributing/SECURITY_SETUP.md - Code Style: contributing/code-style.md - Testing: contributing/testing.md - Errors: @@ -290,5 +294,21 @@ nav: - UPG002 - Synthesised Value During Upgrade: errors/UPG002.md - UPG003 - Unmapped Country Code: errors/UPG003.md - UPG004 - Required v0.7 Field Missing: errors/UPG004.md + - Detection Errors: + - DET001 - Family Mismatch: errors/DET001.md + - Mapping Errors: + - MAP001 - Lossy Mapping Transformation: errors/MAP001.md + - MAP002 - Synthesised Value During Mapping: errors/MAP002.md + - MAP003 - Unmapped Field Passthrough: errors/MAP003.md + - MAP004 - Required Field Missing on Target: errors/MAP004.md + - MAP005 - Temporal Collapse: errors/MAP005.md + - Architecture Decisions: + - Overview: adr/README.md + - 0001 - CIRPASS JSON Schema Derivation: adr/0001-cirpass-json-schema-derivation.md + - 0002 - Canonical EUDPP IRI: adr/0002-canonical-eudpp-iri.md + - 0003 - Tyre License: adr/0003-tyre-license.md + - 0004 - Textile v2 Built-in: adr/0004-textile-v2-built-in.md + - 0005 - CLI Exit Codes: adr/0005-cli-exit-codes.md + - 0006 - Validation Layer Taxonomy: adr/0006-validation-layer-taxonomy.md - FAQ: faq.md - Changelog: changelog.md diff --git a/tests/unit/test_doc_error_code_coverage.py b/tests/unit/test_doc_error_code_coverage.py new file mode 100644 index 0000000..5f16f8a --- /dev/null +++ b/tests/unit/test_doc_error_code_coverage.py @@ -0,0 +1,155 @@ +"""Docs guard: every emitted error code has a docs page and nav entry. + +Phase 7 of the [Docs Coherence Plan] +(../../docs/plans/DOCS_COHERENCE_PLAN.md). This test walks ``src/`` +for error-code literals matching the prefixes pinned by +[ADR 0006](../../docs/adr/0006-validation-layer-taxonomy.md) and +asserts: + +1. Every emitted code has a corresponding ``docs/errors/.md`` page. +2. Every emitted code is listed in ``mkdocs.yml`` under ``nav.Errors``. +3. Every ``docs/errors/.md`` page maps back to a code emitted by + ``src/`` (no orphaned doc pages). + +Together with ``tools/check_doc_default_version.py`` and the +``mkdocs build --strict`` gate added in Phase 4, this prevents the +``MOD`` → ``MDL`` style drift that motivated the plan. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +# Prefix allow-list — every prefix that ADR 0006 names as a valid +# error-code surface. Adding a new prefix means updating this set +# *and* documenting it in ADR 0006. +KNOWN_PREFIXES: frozenset[str] = frozenset( + { + # Layered prefixes (Layer 1..6 — Layer 0 detection has no codes + # of its own; Layer 7 signature codes are reserved). + "SCH", + "MDL", + "JLD", + "SEM", + "VOC", + # Plugin packs. + "TXT", # Textile + "CQ", # CIRPASS Quality + "TYR", # Tyres + # Non-layer / cross-cutting. + "PRS", # Parsing (pre-Layer 1) + "DET", # Detection routing + "VER", # Version routing + "UPG", # 0.6 → 0.7 upgrade shim + "MAP", # Cross-family migration shim + "PRT", # Advisory rules (Party-Role) + } +) + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent +SRC = REPO_ROOT / "src" +ERRORS_DIR = REPO_ROOT / "docs" / "errors" +MKDOCS_YML = REPO_ROOT / "mkdocs.yml" + +# Prefixes we don't expect every code under to have its own docs page. +# Plugin-pack codes (TXT, CQ, TYR) are documented as a group in their +# plugin pages; per-code coverage is enforced by the plugin's own +# integration tests, not this guard. Plugin authors can opt in by +# dropping ``docs/errors/.md`` files. +DOCS_OPTIONAL_PREFIXES: frozenset[str] = frozenset({"TXT", "CQ", "TYR"}) + +# Exclude directories from the source scan. +SKIP_DIR_PARTS = frozenset({"__pycache__", ".pytest_cache", ".ruff_cache", "site"}) + + +# Match a 3-digit code preceded by a known prefix, with both prefix and +# digits as a single token. We require the prefix to be in +# KNOWN_PREFIXES so that random three-letter sequences in test data +# don't trip the scan (e.g. an unrelated "MAY001" identifier). +_CODE_PATTERN = re.compile(r"\b(?P(?:" + "|".join(sorted(KNOWN_PREFIXES)) + r")\d{3})\b") + + +def _emitted_codes() -> set[str]: + """Walk ``src/`` for code literals matching :data:`_CODE_PATTERN`.""" + codes: set[str] = set() + for py in SRC.rglob("*.py"): + if any(part in SKIP_DIR_PARTS for part in py.parts): + continue + text = py.read_text(encoding="utf-8") + for hit in _CODE_PATTERN.finditer(text): + codes.add(hit.group("code")) + return codes + + +def _doc_codes() -> set[str]: + """Codes that have a ``docs/errors/.md`` page.""" + return {p.stem for p in ERRORS_DIR.glob("*.md") if p.stem != "index"} + + +def _nav_codes() -> set[str]: + """Codes referenced by an ``errors/.md`` entry in mkdocs nav. + + mkdocs.yml uses ``!!python/name:...`` tag literals that PyYAML's + SafeLoader rejects. We don't need to evaluate the nav tree + structurally — a regex over the file is sufficient and side-steps + the YAML loader entirely. + """ + text = MKDOCS_YML.read_text(encoding="utf-8") + return set(re.findall(r"errors/([A-Z]+\d{3})\.md", text)) + + +def _filter_required(codes: set[str]) -> set[str]: + return {c for c in codes if not any(c.startswith(p) for p in DOCS_OPTIONAL_PREFIXES)} + + +def test_emitted_codes_have_doc_pages() -> None: + """Every code emitted by src/ has a docs/errors/.md page.""" + emitted = _emitted_codes() + documented = _doc_codes() + required = _filter_required(emitted) + missing = sorted(required - documented) + assert not missing, ( + f"Codes emitted by src/ but missing docs/errors/.md: {missing}. " + "Either add the page (template: docs/errors/MDL001.md) or, for " + "plugin-pack prefixes, extend DOCS_OPTIONAL_PREFIXES in this test." + ) + + +def test_emitted_codes_are_in_mkdocs_nav() -> None: + """Every code emitted by src/ is listed in mkdocs.yml nav.Errors.""" + emitted = _emitted_codes() + in_nav = _nav_codes() + required = _filter_required(emitted) + missing = sorted(required - in_nav) + assert not missing, ( + f"Codes emitted by src/ but missing from mkdocs.yml nav.Errors: " + f"{missing}. Add an entry under the appropriate section " + "(see Schema/Model/JSON-LD/... groupings)." + ) + + +def test_no_orphan_doc_pages() -> None: + """Every docs/errors/.md maps to a code emitted by src/.""" + emitted = _emitted_codes() + documented = _doc_codes() + orphans = sorted(documented - emitted) + assert not orphans, ( + f"docs/errors/.md pages with no matching emitter in src/: " + f"{orphans}. Either delete the page or add the emitter." + ) + + +@pytest.mark.parametrize("code", sorted(KNOWN_PREFIXES)) +def test_known_prefixes_match_adr(code: str) -> None: + """Sanity: the allow-list and ADR 0006's prefix table agree.""" + adr = (REPO_ROOT / "docs" / "adr" / "0006-validation-layer-taxonomy.md").read_text( + encoding="utf-8" + ) + assert code in adr, ( + f"prefix {code!r} is in KNOWN_PREFIXES but not mentioned in " + "docs/adr/0006-validation-layer-taxonomy.md — update either the " + "ADR or this test's allow-list." + ) diff --git a/tools/check_doc_default_version.py b/tools/check_doc_default_version.py new file mode 100644 index 0000000..be8d999 --- /dev/null +++ b/tools/check_doc_default_version.py @@ -0,0 +1,161 @@ +"""Docs guard: pin user-facing default-version literals to the registry. + +Scans the user-facing documentation surface for assertions of the form +``default ... 0.X.Y`` (with ``current``, ``current default``, +``currently``, etc.) and asserts the literal matches +``DEFAULT_VERSIONS[SchemaFamily.UNTP]`` from +``src/dppvalidator/schemas/registry.py``. + +This is the docs-side analogue of +``tests/unit/test_no_version_literals.py`` (which guards source files +under ``src/dppvalidator/`` from drifting). Phase 1 of the +[Docs Coherence Plan](docs/plans/DOCS_COHERENCE_PLAN.md) flipped every +known reference from ``0.6.1`` to ``0.7.0``; Phase 7 prevents the same +drift from recurring on the next default-version flip. + +Wired into ``.pre-commit-config.yaml`` and the ``ci.yml`` lint job. + +Exit codes +---------- +0 - All user-facing docs agree with the registry. +1 - At least one drift hit was found; offending lines printed to stderr. +2 - Could not import / parse the registry (configuration error). +""" + +from __future__ import annotations + +import re +import sys +from collections.abc import Iterable +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Files that are docs-shaped but explicitly exempt from this guard: +# - ADR 0006 documents the migration *from* the old default; it +# intentionally cites ``0.6.1`` as the legacy literal. +# - The Docs Coherence Plan and other plan docs in ``docs/plans/`` +# describe the rollout; they intentionally cite both the source and +# target literals during the migration. +# - The CHANGELOG records the historical flip; ``0.6.1`` lives there +# forever as part of the release history. +EXEMPT = ( + "docs/adr/0006-validation-layer-taxonomy.md", + "docs/plans/", + "CHANGELOG.md", + "docs/changelog.md", # snippet include of CHANGELOG.md +) + +# Files that the guard reads. These are the user-facing surfaces that +# Phase 1 of the Docs Coherence Plan rewrote. +TARGETS = ( + "README.md", + "AGENTS.md", + "CLAUDE.md", + "docs/index.md", + "docs/faq.md", + "docs/llms.txt", + "docs/llms-ctx.txt", + "docs/concepts/untp-versions.md", + "docs/concepts/validation-layers.md", + "docs/guides/cli-usage.md", + "docs/guides/migration-0-6-to-0-7.md", +) + +# Pattern: a trigger word ("default", "currently", "current default") +# followed within ~80 characters by one *or more* version literals on +# the same line. We only flag when NO literal in the window matches the +# expected default — that lets enumerations like "currently 0.6.0, +# 0.6.1, 0.7.0" pass (the expected version is present) while still +# catching "current default = 0.6.1" (no expected version in window). +_TRIGGER = re.compile(r"\b(default|currently|current default)\b", re.IGNORECASE) +_VERSION = re.compile(r"\b0\.\d+\.\d+\b") +_WINDOW = 80 + + +def _read_default_version() -> str: + """Parse the canonical default UNTP version from ``registry.py``. + + The registry uses ``DEFAULT_VERSIONS[SchemaFamily.UNTP]`` as the + single source of truth. We avoid importing ``dppvalidator`` here + so the guard runs in a minimal pre-commit environment. + """ + registry = REPO_ROOT / "src" / "dppvalidator" / "schemas" / "registry.py" + text = registry.read_text(encoding="utf-8") + # Match: SchemaFamily.UNTP: "0.X.Y" (with optional whitespace). + match = re.search( + r"SchemaFamily\.UNTP\s*:\s*[\"'](?P0\.\d+\.\d+)[\"']", + text, + ) + if not match: + msg = f"could not locate DEFAULT_VERSIONS[SchemaFamily.UNTP] in {registry}" + raise RuntimeError(msg) + return match.group("ver") + + +def _is_exempt(path: Path) -> bool: + rel = path.relative_to(REPO_ROOT).as_posix() + return any(rel == ex or rel.startswith(ex) for ex in EXEMPT) + + +def _scan(path: Path, expected: str) -> Iterable[tuple[int, str, str]]: + if _is_exempt(path): + return + text = path.read_text(encoding="utf-8") + for line_no, line in enumerate(text.splitlines(), start=1): + for trigger in _TRIGGER.finditer(line): + window = line[trigger.end() : trigger.end() + _WINDOW] + versions = [m.group(0) for m in _VERSION.finditer(window)] + if not versions: + continue + # If the expected default is anywhere in the trigger window, + # the line is fine — even if other versions are listed too. + if expected in versions: + continue + # Otherwise, the trigger asserts a default that doesn't + # match the registry. Report the first off-default literal. + yield line_no, line, versions[0] + + +def main() -> int: + try: + expected = _read_default_version() + except (OSError, RuntimeError) as exc: + print(f"check_doc_default_version: {exc}", file=sys.stderr) + return 2 + + drift: list[tuple[Path, int, str, str]] = [] + for rel in TARGETS: + path = REPO_ROOT / rel + if not path.exists(): + continue + for line_no, line, ver in _scan(path, expected): + drift.append((path, line_no, line, ver)) + + if drift: + print( + "check_doc_default_version: docs cite a default UNTP version " + f"that no longer matches the registry " + f"(DEFAULT_VERSIONS[SchemaFamily.UNTP] = {expected!r}).", + file=sys.stderr, + ) + print(file=sys.stderr) + for path, line_no, line, ver in drift: + rel = path.relative_to(REPO_ROOT).as_posix() + print(f" {rel}:{line_no}: cites {ver!r}", file=sys.stderr) + print(f" > {line.strip()}", file=sys.stderr) + print(file=sys.stderr) + print( + "Update the cited literal to match the registry, or add the " + "file to EXEMPT in tools/check_doc_default_version.py if the " + "mention is genuinely historical (e.g. an ADR documenting " + "the migration).", + file=sys.stderr, + ) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 9234856b5973a40611ab9ed179f2a61185e4de5e Mon Sep 17 00:00:00 2001 From: Matthias Brenninkmeijer Date: Sun, 10 May 2026 15:57:12 +0200 Subject: [PATCH 5/5] chore(release): 0.5.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation-only patch — Docs Coherence Plan rollout. No runtime behaviour changed. See CHANGELOG.md for the full notes; the plan itself is at docs/plans/DOCS_COHERENCE_PLAN.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7c0674..e585896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,92 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.1] - 2026-05-10 + +Documentation-only patch. No runtime behaviour changed; no API, +schema, validator, or CLI surface was touched. The release executes +the [Docs Coherence Plan](https://github.com/artiso-ai/dppvalidator/blob/main/docs/plans/DOCS_COHERENCE_PLAN.md) +end-to-end: every public source now agrees on the active default +version, the canonical seven-layer taxonomy, and the error-code +prefix table. + +### Added + +- [ADR 0006](https://github.com/artiso-ai/dppvalidator/blob/main/docs/adr/0006-validation-layer-taxonomy.md) + pinning the canonical "seven layers + Layer 0 detection" taxonomy + and the non-layer error-code prefix list (`PRS`, `DET`, `VER`, + `UPG`, `MAP`, `PRT`). +- New error-page coverage for codes that ship in 0.5.0 but had no + reference page: `DET001` (family mismatch), `MAP001`–`MAP005` + (cross-family migration warnings). +- `tools/check_doc_default_version.py` — CI guard that walks the + user-facing docs surface and asserts every "default UNTP version" + claim matches `DEFAULT_VERSIONS[SchemaFamily.UNTP]`. Wired into + pre-commit and `ci.yml` lint. +- `tests/unit/test_doc_error_code_coverage.py` — asserts every error + code emitted by `src/` has a `docs/errors/.md` page and an + `mkdocs.yml` nav entry, and the prefix allow-list agrees with + ADR 0006. +- `mkdocs build --strict` is now a hard gate in + [`.github/workflows/docs.yml`](.github/workflows/docs.yml). + +### Changed + +- Default UNTP version flipped `0.6.1` → `0.7.0` across every + user-facing surface (README, mkdocs index/concepts/guides/FAQ, + AGENTS.md, both `llms*.txt`). The runtime default already lived at + `0.7.0` in [`schemas/registry.py`](https://github.com/artiso-ai/dppvalidator/blob/main/src/dppvalidator/schemas/registry.py) + since 0.5.0; the docs caught up. +- Error-code prefix `MOD` → `MDL` in every doc that referenced it + (the source code has emitted `MDL001`–`MDL099` since 0.5.0; only + the docs lagged). +- The README + concept-page mermaid diagrams now render the full + seven-layer flow (Detection → Schema → Model → JSON-LD → + Semantic → Vocabulary → Plugin → Signature) instead of the older + five-layer dispatch path. +- `SIG001`–`SIG099` is documented as **reserved**: the verifier + currently surfaces untyped error strings via + `VerificationResult.errors`; the `SIG` prefix is held for the + future structured-code migration. +- README hero, `mkdocs.yml site_description`, and both `llms*.txt` + framings now name **CIRPASS DPP reference structure 1.3.0** + alongside UNTP 0.6.x / 0.7.0; previous wording mentioned only + UNTP. +- `docs/changelog.md` is now a one-line `pymdownx.snippets` include + of the root `CHANGELOG.md` — single source of truth, both + surfaces stay in sync at build time. +- Root `llms.txt` and `llms-ctx.txt` are symlinks into `docs/`; the + two copies can no longer drift. +- FAQ error-prefix table extended from 7 rows to 12 (adds `DET`, + `VER`, `UPG`, `MAP`, `PRT`; clarifies `SIG` reservation). +- `AGENTS.md` directory tree adds `models/cirpass/v1_3/` and + `validators/rules/cirpass_v1_3/`; "UNTP version handling" section + renamed to "Schema family + version handling" and now names + `SchemaFamily.CIRPASS`; `[rdf]` / `[cli]` / `[all]` extras called + out. + +### Removed + +- Scratch artefacts that had drifted into `docs/`: `docs/dpp/*.json` + (raw schema fixtures duplicated under `tests/fixtures/`), + `docs/uv/uv.md` and `docs/windsurf/cascade-reference.md` + (third-party docs copies), + `docs/dpp_validator_description.md` (early marketing draft). +- Internal planning documents moved out of the published docs site + into `docs/plans/` (already in `exclude_docs:`): + `IMPLEMENTATION_PLAN.md`, `IMPROVEMENT_ROADMAP.md`, + `REFACTORING_PLAN.md`, `STRATEGIC_ROADMAP.md`, + `UNTTP_PLUGIN_PLAN.md`, `VC_WALLET_ROADMAP.md`. Their content is + unchanged; the relocation closes the mkdocs `--strict` orphan + gap. + +### Maintenance + +- All cross-document references in this `CHANGELOG.md` rewritten as + GitHub-absolute URLs so the snippet-included copy at + [`docs/changelog.md`](https://artiso-ai.github.io/dppvalidator/changelog/) + resolves cleanly under `mkdocs build --strict`. + ## [0.5.0] - 2026-05-09 (Preview) This release adds **end-to-end CIRPASS-2 reference structure v1.3.0** diff --git a/pyproject.toml b/pyproject.toml index d8f1402..742992a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dppvalidator" -version = "0.5.0" +version = "0.5.1" description = "Python library for validating Digital Product Passports (DPP) according to EU ESPR regulations and CIRPASS/UNECE ontologies" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index e83e5a5..d0b0a58 100644 --- a/uv.lock +++ b/uv.lock @@ -614,7 +614,7 @@ wheels = [ [[package]] name = "dppvalidator" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "base58" },